├── .gitignore
├── .travis.yml
├── LICENSE
├── README.md
├── bower.json
├── examples
├── controller.js
└── index.html
├── karma.conf.js
├── package.json
├── release.js
├── release
├── angular-responsive-tables.css
├── angular-responsive-tables.js
├── angular-responsive-tables.min.css
└── angular-responsive-tables.min.js
├── src
├── directive.js
├── module.js
└── style.css
└── tests
├── directive.spec.js
└── lib.js
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea/
2 | bower_components/
3 | node_modules/
4 | typings/
5 | npm-debug.log
6 | commit.txt
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - "6"
4 | addons:
5 | chrome: stable
6 | cache:
7 | directories:
8 | - $HOME/.npm
9 | - $HOME/.cache/bower
10 | - node_modules
11 | - bower_components
12 | before_install:
13 | - # start your web application and listen on `localhost`
14 | - google-chrome-stable --headless --disable-gpu --remote-debugging-port=9222 http://localhost &
15 | install:
16 | - npm install -g bower
17 | - npm install
18 | - bower install
19 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 André Werlang
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 |
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # angular-responsive-tables
2 |
3 | Make your HTML tables look great on every device.
4 | Compatible with **AngularJS 1.3.4+**.
5 |
6 | [Live Demo](http://awerlang.github.io/angular-responsive-tables/examples/)
7 |
8 | ## Why?
9 |
10 | Currently, browsers for mobile devices like smartphones doesn't do anything to have a proper presentation of tables,
11 | and then scrollbars will show up and ruin your design.
12 |
13 | In the search of a solution to this problem I have found many different approaches. Some of them
14 | still rely on horizontal scrollbars. While I believe this layout could be useful for some use cases,
15 | I felt that a default solution should avoid horizontal scrollbars entirely. Then I came up with this
16 | highly reusable directive.
17 |
18 | All this work is based on the following assumptions:
19 |
20 | * If it is *flexible*, then it would solve most problems, even ones not aimed by the library author's;
21 | * Focusing on the task of *adding responsiveness*, in order to accomplish a greater objective (*easy to use tabular data*);
22 | * Do work with a *standard HTML table*, not requiring any extraneous markup;
23 | * Do *not change default tabular layout* unless a smaller display is detected;
24 | * Provide *convenience* without sacrificing flexibility;
25 | * By keeping *code base simple*, it is easier to reason about and evolve;
26 | * By fully covering with tests, it can *evolve without introducing bugs*.
27 |
28 | ## Features
29 |
30 | * Angular native implementation compatible with 1.3.4+;
31 | * Keep things DRY;
32 | * Supports static and dynamic (ng-repeat) rows;
33 | * Supports conditionally shown (ng-if) columns;
34 | * Supports dynamic headers (ng-repeat);
35 | * Supports nested tables (responsive or not in their own right);
36 | * Easy to apply any style on top of it;
37 | * Works with any base CSS framework;
38 | * Should integrate seamlessly with any table component you might choose to use.
39 |
40 | ### Future Work
41 |
42 | * Choose what columns to show/hide according to a given screen resolution;
43 | * Choose when it would be best to hide columns or collapse all columns;
44 | * Define a header and/or custom template for collapsed columns/row;
45 | * Allow collapse/expand column details.
46 |
47 | ## Usage
48 |
49 |
129 |
130 | ### IE9 responsive hack
131 |
132 | Because IE9 doesn't handle correctly a `display` CSS rule for ``, if you need to support it, you can use the following style, only for IE9:
133 |
134 | ```css
135 |
147 | ```
148 |
149 | ## Credits
150 |
151 | CSS based on original work by Chris Coyier (http://css-tricks.com/responsive-data-tables/). In this article, he covers approaches to responsive tables. I modified it to work around CSS specificity and to keep things DRY.
152 |
153 | ## License
154 |
155 | MIT
156 |
--------------------------------------------------------------------------------
/bower.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "angular-responsive-tables",
3 | "version": "0.4.4",
4 | "homepage": "https://github.com/awerlang/angular-responsive-tables",
5 | "authors": [
6 | "André Werlang"
7 | ],
8 | "description": "Make your HTML tables look great on every device",
9 | "main": [
10 | "release/angular-responsive-tables.js",
11 | "release/angular-responsive-tables.css"
12 | ],
13 | "keywords": [
14 | "angular",
15 | "angularjs",
16 | "table",
17 | "tables",
18 | "responsive",
19 | "adaptive",
20 | "mobile"
21 | ],
22 | "license": "MIT",
23 | "ignore": [
24 | "**/.*",
25 | "node_modules",
26 | "bower_components",
27 | "test",
28 | "tests"
29 | ],
30 | "dependencies": {
31 | "angular": "^1.3.4"
32 | },
33 | "devDependencies": {
34 | "angular-mocks": "^1.3.4",
35 | "jquery": "~2.1.4",
36 | "bootstrap": "*"
37 | },
38 | "resolutions": {
39 | "angular": "1.3.4"
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/examples/controller.js:
--------------------------------------------------------------------------------
1 | function TestController() {
2 | this.projects = [
3 | {name: "AngularJS", version: "1.5", language: "JavaScript", maintainer: "Google", stars: 35000},
4 | {name: "Bootstrap", version: "3.3", language: "CSS", maintainer: "Twitter", stars: 23000},
5 | {name: "UI-Router", version: "0.13", language: "JavaScript", maintainer: "AngularUI", stars: 15000}
6 | ];
7 | }
8 |
9 | angular.module('app', ['wt.responsive'])
10 | .controller('TestController', TestController);
11 |
--------------------------------------------------------------------------------
/examples/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
24 |
25 |
26 | Angular Responsive Tables
27 |
28 | Simple styling
29 |
30 | Static table
31 |
32 |
33 |
34 | Name
35 | Version
36 | Language
37 | Maintainer
38 | Stars
39 |
40 |
41 |
42 |
43 | AngularJS
44 | 1.5
45 | JavaScript
46 | Google
47 | 35000
48 |
49 |
50 | Bootstrap
51 | 3.3
52 | CSS
53 | Twitter
54 | 23000
55 |
56 |
57 | UI-Router
58 | 0.13
59 | JavaScript
60 | AngularUI
61 | 15000
62 |
63 |
64 |
65 |
66 | Simple table with ng-repeat
67 |
68 |
69 |
70 | Name
71 | Version
72 | Language
73 | Maintainer
74 | Stars
75 |
76 |
77 |
78 |
79 | {{item.name}}
80 | {{item.version}}
81 | {{item.language}}
82 | {{item.maintainer}}
83 | {{item.stars}}
84 |
85 |
86 |
87 |
88 | Simple table with no thead, tbody
89 |
90 |
91 | Name
92 | Version
93 | Language
94 | Maintainer
95 | Stars
96 |
97 |
98 | {{item.name}}
99 | {{item.version}}
100 | {{item.language}}
101 | {{item.maintainer}}
102 | {{item.stars}}
103 |
104 |
105 |
106 | Simple table with colspan
107 |
108 |
109 |
110 | First title
111 | Second title
112 | Third title
113 | Forth title
114 |
115 |
116 |
117 |
118 | This cell spans for 3 columns
119 | Forth column
120 |
121 |
122 | First column
123 | Second column
124 | Third column
125 | Forth column
126 |
127 |
128 |
129 |
130 | Simple table, headers on first column
131 |
132 |
133 |
134 | Name
135 | {{item.name}}
136 |
137 |
138 | Version
139 | {{item.version}}
140 |
141 |
142 | Language
143 | {{item.language}}
144 |
145 |
146 | Maintainer
147 | {{item.maintainer}}
148 |
149 |
150 | Stars
151 | {{item.stars}}
152 |
153 |
154 |
155 |
156 | With Bootstrap
157 |
158 | Static table
159 |
160 |
161 |
162 | Name
163 | Version
164 | Language
165 | Maintainer
166 | Stars
167 |
168 |
169 |
170 |
171 | AngularJS
172 | 1.5
173 | JavaScript
174 | Google
175 | 35000
176 |
177 |
178 | Bootstrap
179 | 3.3
180 | CSS
181 | Twitter
182 | 23000
183 |
184 |
185 | UI-Router
186 | 0.13
187 | JavaScript
188 | AngularUI
189 | 15000
190 |
191 |
192 |
193 |
194 | Simple table with ng-repeat
195 |
196 |
197 |
198 | Name
199 | Version
200 | Language
201 | Maintainer
202 | Stars
203 |
204 |
205 |
206 |
207 | {{item.name}}
208 | {{item.version}}
209 | {{item.language}}
210 | {{item.maintainer}}
211 | {{item.stars}}
212 |
213 |
214 |
215 |
216 | Simple table with no thead, tbody
217 |
218 |
219 | Name
220 | Version
221 | Language
222 | Maintainer
223 | Stars
224 |
225 |
226 | {{item.name}}
227 | {{item.version}}
228 | {{item.language}}
229 | {{item.maintainer}}
230 | {{item.stars}}
231 |
232 |
233 |
234 | Nested tables
235 |
236 | Non-responsive nested table
237 |
238 |
239 |
240 | First title
241 | Second title
242 |
243 |
244 |
245 |
246 | First column
247 |
248 |
249 |
250 | nested title
251 | nested table
252 |
253 |
254 |
255 |
256 |
257 |
258 |
259 | Responsive nested table
260 |
261 |
262 |
263 | First title
264 | Second title
265 |
266 |
267 |
268 |
269 | First column
270 |
271 |
272 |
273 | nested title
274 | nested table
275 |
276 |
277 |
278 |
279 |
280 |
281 |
282 |
283 |
--------------------------------------------------------------------------------
/karma.conf.js:
--------------------------------------------------------------------------------
1 | // Karma configuration
2 | // Generated on Mon Apr 27 2015 01:22:27 GMT-0300 (Hora oficial do Brasil)
3 |
4 | module.exports = function(config) {
5 | config.set({
6 |
7 | // base path that will be used to resolve all patterns (eg. files, exclude)
8 | basePath: '',
9 |
10 |
11 | // frameworks to use
12 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter
13 | frameworks: ['jasmine'],
14 |
15 |
16 | // list of files / patterns to load in the browser
17 | files: [
18 | 'bower_components/jquery/dist/jquery.js',
19 | 'bower_components/angular/angular.js',
20 | 'bower_components/angular-mocks/angular-mocks.js',
21 | 'src/**/*.js',
22 | 'src/**/*.css',
23 | 'bower_components/bootstrap/dist/css/bootstrap.css',
24 | 'tests/**/*.js'
25 | ],
26 |
27 |
28 | // list of files to exclude
29 | exclude: [
30 | ],
31 |
32 |
33 | // preprocess matching files before serving them to the browser
34 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
35 | preprocessors: {
36 | },
37 |
38 |
39 | // test results reporter to use
40 | // possible values: 'dots', 'progress'
41 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter
42 | reporters: ['progress'],
43 |
44 |
45 | // web server port
46 | port: 9876,
47 |
48 |
49 | // enable / disable colors in the output (reporters and logs)
50 | colors: true,
51 |
52 |
53 | // level of logging
54 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
55 | logLevel: config.LOG_INFO,
56 |
57 |
58 | // enable / disable watching file and executing tests whenever any file changes
59 | autoWatch: true,
60 |
61 |
62 | // start these browsers
63 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
64 | browsers: ['mobile'],
65 |
66 | customLaunchers: {
67 | mobile: {
68 | base: "ChromeHeadless",
69 | flags: ["--window-size=320,600"]
70 | },
71 | desktop: {
72 | base: "ChromeHeadless"
73 | }
74 | },
75 |
76 |
77 | // Continuous Integration mode
78 | // if true, Karma captures browsers, runs the tests and exits
79 | singleRun: false
80 | });
81 | };
82 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "angular-responsive-tables",
3 | "version": "0.4.4",
4 | "description": "Make your HTML tables look great on every device",
5 | "main": "release/angular-responsive-tables.js",
6 | "files": [
7 | "release/"
8 | ],
9 | "scripts": {
10 | "build": "./node_modules/.bin/uglifyjs src/directive.js src/module.js -b -e --preamble \"// https://github.com/awerlang/angular-responsive-tables\" -o release/angular-responsive-tables.js",
11 | "release": "./node_modules/.bin/uglifyjs src/directive.js src/module.js -c -e --preamble \"// https://github.com/awerlang/angular-responsive-tables\" -o release/angular-responsive-tables.min.js",
12 | "css": "type src\\style.css > release\\angular-responsive-tables.css",
13 | "cssmin": "type src\\style.css | \"./node_modules/.bin/cleancss\" -o release\\angular-responsive-tables.min.css",
14 | "test": "karma start --single-run"
15 | },
16 | "repository": {
17 | "type": "git",
18 | "url": "https://github.com/awerlang/angular-responsive-tables.git"
19 | },
20 | "keywords": [
21 | "angular",
22 | "angularjs",
23 | "table",
24 | "tables",
25 | "responsive",
26 | "adaptive",
27 | "mobile"
28 | ],
29 | "author": "André Werlang",
30 | "license": "MIT",
31 | "bugs": {
32 | "url": "https://github.com/awerlang/angular-responsive-tables/issues"
33 | },
34 | "homepage": "https://github.com/awerlang/angular-responsive-tables",
35 | "devDependencies": {
36 | "clean-css": "^3.2.10",
37 | "conventional-changelog": "0.0.17",
38 | "jasmine-core": "^2.8.0",
39 | "karma": "^1.7.1",
40 | "karma-chrome-launcher": "^2.2.0",
41 | "karma-jasmine": "^1.1.1",
42 | "uglify-js": "^2.4.20"
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/release.js:
--------------------------------------------------------------------------------
1 | require('conventional-changelog')({
2 | repository: 'https://github.com/awerlang/angular-responsive-tables',
3 | version: require('./package.json').version
4 | }, function(err, log) {
5 | var fs = require('fs');
6 |
7 | fs.writeFile('CHANGELOG.md', log, function (err){
8 | if(err) {
9 | console.log(err);
10 | } else {
11 | console.log("Change log generated and saved:", 'CHANGELOG.md');
12 | }
13 | });
14 | });
--------------------------------------------------------------------------------
/release/angular-responsive-tables.css:
--------------------------------------------------------------------------------
1 | /* original: http://css-tricks.com/responsive-data-tables/ */
2 |
3 | .responsive {
4 | width: 100%;
5 | border-collapse: collapse;
6 | }
7 |
8 | @media only screen and (max-width: 800px) {
9 |
10 | /* Force table to not be like tables anymore */
11 | /* table.responsive,*/
12 | .responsive > thead,
13 | .responsive > tbody,
14 | .responsive > tbody > tr,
15 | .responsive > thead > th {
16 | display: block;
17 | }
18 |
19 | /* Hide table headers (but not display: none;, for accessibility) */
20 | .responsive > thead > tr,
21 | .responsive > thead > tr > th,
22 | .responsive > tbody > tr > th {
23 | position: absolute;
24 | top: -9999px;
25 | left: -9999px;
26 | }
27 |
28 | .responsive > tbody > tr {
29 | border: 1px solid #ccc;
30 | }
31 |
32 | .responsive > tbody > tr > td {
33 | /* Behave like a "row" */
34 | border: none;
35 | border-bottom: 1px solid #eee;
36 | position: relative;
37 | padding-left: 50% !important;
38 | white-space: normal;
39 | text-align: left;
40 |
41 | display: block;
42 | -webkit-box-sizing: content-box;
43 | -moz-box-sizing: content-box;
44 | box-sizing: content-box;
45 | min-height: 1em;
46 | }
47 |
48 | .responsive > tbody > tr > td::before {
49 | /* Now like a table header */
50 | position: absolute;
51 | /* Top/left values mimic padding */
52 | left: 6px;
53 | width: 45%;
54 | padding-right: 10px;
55 | -ms-word-wrap: break-word;
56 | word-wrap: break-word;
57 | text-align: left;
58 | font-weight: bold;
59 | /*
60 | Label the data
61 | */
62 | content: attr(data-title);
63 | }
64 |
65 | .responsive td.responsive-omit-title:nth-child(odd), .responsive td.responsive-omit-title:nth-child(even) {
66 | padding-left: 6px;
67 | }
68 |
69 | .responsive td.responsive-omit-title::before {
70 | display: none;
71 | }
72 |
73 | .responsive td.responsive-omit-if-empty:empty {
74 | display: none;
75 | }
76 | }
--------------------------------------------------------------------------------
/release/angular-responsive-tables.js:
--------------------------------------------------------------------------------
1 | // https://github.com/awerlang/angular-responsive-tables
2 | (function() {
3 | "use strict";
4 | function getFirstHeaderInRow(tr) {
5 | var th = tr.firstChild;
6 | while (th) {
7 | if (th.tagName === "TH") break;
8 | if (th.tagName === "TD") {
9 | th = null;
10 | break;
11 | }
12 | th = th.nextSibling;
13 | }
14 | return th;
15 | }
16 | function getHeaders(element) {
17 | return [].filter.call(element.children().children().children(), function(it) {
18 | return it.tagName === "TH";
19 | });
20 | }
21 | function updateTitle(td, th) {
22 | var title = th && th.textContent;
23 | if (title && (td.getAttribute("data-title-override") || !td.getAttribute("data-title"))) {
24 | td.setAttribute("data-title", title);
25 | td.setAttribute("data-title-override", title);
26 | }
27 | }
28 | function colspan(td) {
29 | var colspan = td.getAttribute("colspan");
30 | return colspan ? parseInt(colspan) : 1;
31 | }
32 | function wtResponsiveTable() {
33 | return {
34 | restrict: "A",
35 | controller: [ "$element", function($element) {
36 | angular.extend(this, {
37 | contains: function(td) {
38 | var tableEl = $element[0];
39 | var el = td;
40 | do {
41 | if (el === tableEl) return true;
42 | if (el.tagName === "TABLE") return false;
43 | el = el.parentElement;
44 | } while (el);
45 | throw new Error("Table element not found for " + td);
46 | },
47 | getHeader: function(td) {
48 | var firstHeader = getFirstHeaderInRow(td.parentElement);
49 | if (firstHeader) return firstHeader;
50 | var headers = getHeaders($element);
51 | if (headers.length) {
52 | var row = td.parentElement;
53 | var headerIndex = 0;
54 | var found = Array.prototype.some.call(row.children, function(value, index) {
55 | if (value.tagName !== "TD") return false;
56 | if (value === td) {
57 | return true;
58 | }
59 | headerIndex += colspan(value);
60 | });
61 | return found ? headers[headerIndex] : null;
62 | }
63 | }
64 | });
65 | } ],
66 | compile: function(element, attrs) {
67 | element.addClass("responsive");
68 | var headers = getHeaders(element);
69 | if (headers.length) {
70 | var rows = [].filter.call(element.children(), function(it) {
71 | return it.tagName === "TBODY";
72 | })[0].children;
73 | Array.prototype.forEach.call(rows, function(row) {
74 | var headerIndex = 0;
75 | [].forEach.call(row.children, function(value, index) {
76 | if (value.tagName !== "TD") return;
77 | var th = getFirstHeaderInRow(value.parentElement);
78 | th = th || headers[headerIndex];
79 | updateTitle(value, th);
80 | headerIndex += colspan(value);
81 | });
82 | });
83 | }
84 | }
85 | };
86 | }
87 | function wtResponsiveDynamic() {
88 | return {
89 | restrict: "E",
90 | require: "?^^wtResponsiveTable",
91 | link: function(scope, element, attrs, tableCtrl) {
92 | if (!tableCtrl) return;
93 | if (!tableCtrl.contains(element[0])) return;
94 | setTimeout(function() {
95 | [].forEach.call(element[0].parentElement.children, function(td) {
96 | if (td.tagName !== "TD") return;
97 | var th = tableCtrl.getHeader(td);
98 | updateTitle(td, th);
99 | });
100 | }, 0);
101 | }
102 | };
103 | }
104 | "use strict";
105 | angular.module("wt.responsive", []).directive("wtResponsiveTable", [ wtResponsiveTable ]).directive("td", [ wtResponsiveDynamic ]);
106 | })();
--------------------------------------------------------------------------------
/release/angular-responsive-tables.min.css:
--------------------------------------------------------------------------------
1 | .responsive{width:100%;border-collapse:collapse}@media only screen and (max-width:800px){.responsive>tbody,.responsive>tbody>tr,.responsive>thead,.responsive>thead>th{display:block}.responsive>tbody>tr>th,.responsive>thead>tr,.responsive>thead>tr>th{position:absolute;top:-9999px;left:-9999px}.responsive>tbody>tr{border:1px solid #ccc}.responsive>tbody>tr>td{border:none;border-bottom:1px solid #eee;position:relative;padding-left:50%!important;white-space:normal;text-align:left;display:block;-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;min-height:1em}.responsive>tbody>tr>td::before{position:absolute;left:6px;width:45%;padding-right:10px;-ms-word-wrap:break-word;word-wrap:break-word;text-align:left;font-weight:700;content:attr(data-title)}.responsive td.responsive-omit-title:nth-child(even),.responsive td.responsive-omit-title:nth-child(odd){padding-left:6px}.responsive td.responsive-omit-if-empty:empty,.responsive td.responsive-omit-title::before{display:none}}
--------------------------------------------------------------------------------
/release/angular-responsive-tables.min.js:
--------------------------------------------------------------------------------
1 | // https://github.com/awerlang/angular-responsive-tables
2 | !function(){"use strict";function getFirstHeaderInRow(tr){for(var th=tr.firstChild;th&&"TH"!==th.tagName;){if("TD"===th.tagName){th=null;break}th=th.nextSibling}return th}function getHeaders(element){return[].filter.call(element.children().children().children(),function(it){return"TH"===it.tagName})}function updateTitle(td,th){var title=th&&th.textContent;!title||!td.getAttribute("data-title-override")&&td.getAttribute("data-title")||(td.setAttribute("data-title",title),td.setAttribute("data-title-override",title))}function colspan(td){var colspan=td.getAttribute("colspan");return colspan?parseInt(colspan):1}function wtResponsiveTable(){return{restrict:"A",controller:["$element",function($element){angular.extend(this,{contains:function(td){var tableEl=$element[0],el=td;do{if(el===tableEl)return!0;if("TABLE"===el.tagName)return!1;el=el.parentElement}while(el);throw new Error("Table element not found for "+td)},getHeader:function(td){var firstHeader=getFirstHeaderInRow(td.parentElement);if(firstHeader)return firstHeader;var headers=getHeaders($element);if(headers.length){var row=td.parentElement,headerIndex=0,found=Array.prototype.some.call(row.children,function(value,index){return"TD"!==value.tagName?!1:value===td?!0:void(headerIndex+=colspan(value))});return found?headers[headerIndex]:null}}})}],compile:function(element,attrs){element.addClass("responsive");var headers=getHeaders(element);if(headers.length){var rows=[].filter.call(element.children(),function(it){return"TBODY"===it.tagName})[0].children;Array.prototype.forEach.call(rows,function(row){var headerIndex=0;[].forEach.call(row.children,function(value,index){if("TD"===value.tagName){var th=getFirstHeaderInRow(value.parentElement);th=th||headers[headerIndex],updateTitle(value,th),headerIndex+=colspan(value)}})})}}}}function wtResponsiveDynamic(){return{restrict:"E",require:"?^^wtResponsiveTable",link:function(scope,element,attrs,tableCtrl){tableCtrl&&tableCtrl.contains(element[0])&&setTimeout(function(){[].forEach.call(element[0].parentElement.children,function(td){if("TD"===td.tagName){var th=tableCtrl.getHeader(td);updateTitle(td,th)}})},0)}}}angular.module("wt.responsive",[]).directive("wtResponsiveTable",[wtResponsiveTable]).directive("td",[wtResponsiveDynamic])}();
--------------------------------------------------------------------------------
/src/directive.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | function getFirstHeaderInRow(tr) {
4 | var th = tr.firstChild;
5 | while (th) {
6 | if (th.tagName === 'TH') break;
7 | if (th.tagName === 'TD') {
8 | th = null;
9 | break;
10 | }
11 | th = th.nextSibling;
12 | }
13 | return th;
14 | }
15 |
16 | function getHeaders(element) {
17 | return [].filter.call(element.children().children().children(), function (it) {
18 | return it.tagName === 'TH';
19 | });
20 | }
21 |
22 | function updateTitle(td, th) {
23 | var title = th && th.textContent;
24 | if (title && (td.getAttribute('data-title-override') || !td.getAttribute('data-title'))) {
25 | td.setAttribute('data-title', title);
26 | td.setAttribute('data-title-override', title);
27 | }
28 | }
29 |
30 | function colspan(td) {
31 | var colspan = td.getAttribute('colspan');
32 | return colspan ? parseInt(colspan) : 1;
33 | }
34 |
35 | function wtResponsiveTable() {
36 | return {
37 | restrict: 'A',
38 | controller: ['$element', function ($element) {
39 | angular.extend(this, {
40 | contains: function (td) {
41 | var tableEl = $element[0];
42 | var el = td;
43 | do {
44 | if (el === tableEl) return true;
45 | if (el.tagName === 'TABLE') return false;
46 |
47 | el = el.parentElement;
48 | } while (el);
49 | throw new Error('Table element not found for ' + td);
50 | },
51 |
52 | getHeader: function (td) {
53 | var firstHeader = getFirstHeaderInRow(td.parentElement);
54 | if (firstHeader) return firstHeader;
55 |
56 | var headers = getHeaders($element);
57 | if (headers.length) {
58 | var row = td.parentElement;
59 | var headerIndex = 0;
60 | var found = Array.prototype.some.call(row.children, function (value, index) {
61 | if (value.tagName !== 'TD') return false;
62 | if (value === td) {
63 | return true;
64 | }
65 |
66 | headerIndex += colspan(value);
67 | });
68 |
69 | return found ? headers[headerIndex] : null;
70 | }
71 | },
72 | });
73 | }],
74 | compile: function (element, attrs) {
75 | element.addClass("responsive");
76 | var headers = getHeaders(element);
77 | if (headers.length) {
78 | var rows = [].filter.call(element.children(), function (it) {
79 | return it.tagName === 'TBODY';
80 | })[0].children;
81 | Array.prototype.forEach.call(rows, function(row) {
82 | var headerIndex = 0;
83 | [].forEach.call(row.children, function (value, index) {
84 | if (value.tagName !== 'TD') return;
85 |
86 | var th = getFirstHeaderInRow(value.parentElement);
87 | th = th || headers[headerIndex];
88 | updateTitle(value, th);
89 |
90 | headerIndex += colspan(value);
91 | });
92 | });
93 | }
94 | }
95 | };
96 | }
97 |
98 | function wtResponsiveDynamic() {
99 | return {
100 | restrict: 'E',
101 | require: '?^^wtResponsiveTable',
102 | link: function (scope, element, attrs, tableCtrl) {
103 | if (!tableCtrl) return;
104 | if (!tableCtrl.contains(element[0])) return;
105 |
106 | setTimeout(function () {
107 | [].forEach.call(element[0].parentElement.children, function (td) {
108 | if (td.tagName !== 'TD') return;
109 |
110 | var th = tableCtrl.getHeader(td);
111 | updateTitle(td, th);
112 | });
113 | }, 0);
114 | }
115 | };
116 | }
117 |
--------------------------------------------------------------------------------
/src/module.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | angular.module('wt.responsive', [])
4 | .directive('wtResponsiveTable', [wtResponsiveTable])
5 | .directive('td', [wtResponsiveDynamic]);
--------------------------------------------------------------------------------
/src/style.css:
--------------------------------------------------------------------------------
1 | /* original: http://css-tricks.com/responsive-data-tables/ */
2 |
3 | .responsive {
4 | width: 100%;
5 | border-collapse: collapse;
6 | }
7 |
8 | @media only screen and (max-width: 800px) {
9 |
10 | /* Force table to not be like tables anymore */
11 | /* table.responsive,*/
12 | .responsive > thead,
13 | .responsive > tbody,
14 | .responsive > tbody > tr,
15 | .responsive > thead > th {
16 | display: block;
17 | }
18 |
19 | /* Hide table headers (but not display: none;, for accessibility) */
20 | .responsive > thead > tr,
21 | .responsive > thead > tr > th,
22 | .responsive > tbody > tr > th {
23 | position: absolute;
24 | top: -9999px;
25 | left: -9999px;
26 | }
27 |
28 | .responsive > tbody > tr {
29 | border: 1px solid #ccc;
30 | }
31 |
32 | .responsive > tbody > tr > td {
33 | /* Behave like a "row" */
34 | border: none;
35 | border-bottom: 1px solid #eee;
36 | position: relative;
37 | padding-left: 50% !important;
38 | white-space: normal;
39 | text-align: left;
40 |
41 | display: block;
42 | -webkit-box-sizing: content-box;
43 | -moz-box-sizing: content-box;
44 | box-sizing: content-box;
45 | min-height: 1em;
46 | }
47 |
48 | .responsive > tbody > tr > td::before {
49 | /* Now like a table header */
50 | position: absolute;
51 | /* Top/left values mimic padding */
52 | left: 6px;
53 | width: 45%;
54 | padding-right: 10px;
55 | -ms-word-wrap: break-word;
56 | word-wrap: break-word;
57 | text-align: left;
58 | font-weight: bold;
59 | /*
60 | Label the data
61 | */
62 | content: attr(data-title);
63 | }
64 |
65 | .responsive td.responsive-omit-title:nth-child(odd), .responsive td.responsive-omit-title:nth-child(even) {
66 | padding-left: 6px;
67 | }
68 |
69 | .responsive td.responsive-omit-title::before {
70 | display: none;
71 | }
72 |
73 | .responsive td.responsive-omit-if-empty:empty {
74 | display: none;
75 | }
76 | }
--------------------------------------------------------------------------------
/tests/directive.spec.js:
--------------------------------------------------------------------------------
1 | /* global inject */
2 | ///
3 | ///
4 | describe('directive', function () {
5 | var $compile,
6 | $rootScope;
7 |
8 | beforeEach(module('wt.responsive'));
9 | beforeEach(inject(function (_$rootScope_, _$compile_) {
10 | $compile = _$compile_;
11 | $rootScope = _$rootScope_;
12 | }));
13 |
14 | it('header rows are visible but offscreen', function () {
15 | var markup = [
16 | '',
17 | ' ',
18 | ' ',
19 | ' First title ',
20 | ' Second title ',
21 | ' Third title ',
22 | ' Forth title ',
23 | ' ',
24 | ' ',
25 | ' ',
26 | ' ',
27 | ' First column ',
28 | ' Second column ',
29 | ' Third column ',
30 | ' Forth column ',
31 | ' ',
32 | ' ',
33 | ' First column ',
34 | ' Second column ',
35 | ' Third column ',
36 | ' Forth column ',
37 | ' ',
38 | ' ',
39 | '
'
40 | ].join('');
41 | var element = angular.element(markup);
42 | document.body.appendChild(element[0]);
43 | $compile(element);
44 | $rootScope.$digest();
45 |
46 | var headerRow = element.find('th');
47 | expect(headerRow.is(':visible')).toBe(true);
48 | expect(headerRow.is(':offscreen')).toBe(true);
49 | });
50 |
51 | it('supports rows with no ', function (done) {
52 | var markup = [
53 | '',
54 | ' ',
55 | ' First title ',
56 | ' Second title ',
57 | ' Third title ',
58 | ' Forth title ',
59 | ' ',
60 | ' ',
61 | ' First column ',
62 | ' Second column ',
63 | ' Third column ',
64 | ' Forth column ',
65 | ' ',
66 | '
'
67 | ].join('');
68 | var element = angular.element(markup);
69 | document.body.appendChild(element[0]);
70 |
71 | var scope = $rootScope.$new();
72 | $compile(element)(scope);
73 | scope.$digest();
74 |
75 | setTimeout(function () {
76 | var firstDataRow = element.find('tr td');
77 | expect(firstDataRow.eq(0).attr('data-title')).toBe('First title');
78 | expect(firstDataRow.eq(1).attr('data-title')).toBe('Second title');
79 | expect(firstDataRow.eq(2).attr('data-title')).toBe('Third title');
80 | expect(firstDataRow.eq(3).attr('data-title')).toBe('Forth title');
81 |
82 | var headerRow = element.find('tr th');
83 | expect(headerRow.eq(0).attr('data-title')).toBeUndefined();
84 | expect(headerRow.eq(1).attr('data-title')).toBeUndefined();
85 | expect(headerRow.eq(2).attr('data-title')).toBeUndefined();
86 | expect(headerRow.eq(3).attr('data-title')).toBeUndefined();
87 |
88 | done();
89 | }, 0);
90 | });
91 |
92 | it('supports as first column of each ', function (done) {
93 | var markup = [
94 | '',
95 | ' ',
96 | ' First title ',
97 | ' First column ',
98 | ' ',
99 | ' ',
100 | ' Second title ',
101 | ' Second column ',
102 | ' ',
103 | ' ',
104 | ' Third title ',
105 | ' Third column ',
106 | ' ',
107 | ' ',
108 | ' Forth title ',
109 | ' Forth column ',
110 | ' ',
111 | '
'
112 | ].join('');
113 | var element = angular.element(markup);
114 | document.body.appendChild(element[0]);
115 |
116 | var scope = $rootScope.$new();
117 | $compile(element)(scope);
118 | scope.$digest();
119 |
120 | setTimeout(function() {
121 | var firstDataRow = element.find('tr td');
122 | expect(firstDataRow.eq(0).attr('data-title')).toBe('First title');
123 | expect(firstDataRow.eq(1).attr('data-title')).toBe('Second title');
124 | expect(firstDataRow.eq(2).attr('data-title')).toBe('Third title');
125 | expect(firstDataRow.eq(3).attr('data-title')).toBe('Forth title');
126 |
127 | var headerRow = element.find('tr th');
128 | expect(headerRow.eq(0).attr('data-title')).toBeUndefined();
129 | expect(headerRow.eq(1).attr('data-title')).toBeUndefined();
130 | expect(headerRow.eq(2).attr('data-title')).toBeUndefined();
131 | expect(headerRow.eq(3).attr('data-title')).toBeUndefined();
132 |
133 | done();
134 | }, 0);
135 | });
136 |
137 | it('supports colspan', function (done) {
138 | var markup = [
139 | '',
140 | ' ',
141 | ' ',
142 | ' First title ',
143 | ' Second title ',
144 | ' Third title ',
145 | ' Forth title ',
146 | ' ',
147 | ' ',
148 | ' ',
149 | ' ',
150 | ' This cell spans for 3 columns ',
151 | ' Forth column ',
152 | ' ',
153 | ' ',
154 | ' First column ',
155 | ' Second column ',
156 | ' Third column ',
157 | ' Forth column ',
158 | ' ',
159 | ' ',
160 | '
'
161 | ].join('');
162 | var element = angular.element(markup);
163 |
164 | var firstDataRow = element.find('tbody tr:first td');
165 | expect(firstDataRow.attr('data-title')).toBeUndefined();
166 |
167 | var scope = $rootScope.$new();
168 | $compile(element)(scope);
169 | scope.$digest();
170 |
171 | setTimeout(function () {
172 | expect(firstDataRow.eq(0).attr('data-title')).toBe('First title');
173 | expect(firstDataRow.eq(1).attr('data-title')).toBe('Forth title');
174 | done();
175 | }, 0);
176 | });
177 |
178 | it('support tables with multiple static rows', function (done) {
179 | var markup = [
180 | '',
181 | ' ',
182 | ' ',
183 | ' First title ',
184 | ' Second title ',
185 | ' Third title ',
186 | ' Forth title ',
187 | ' ',
188 | ' ',
189 | ' ',
190 | ' ',
191 | ' First column ',
192 | ' Second column ',
193 | ' Third column ',
194 | ' Forth column ',
195 | ' ',
196 | ' ',
197 | ' First column ',
198 | ' Second column ',
199 | ' Third column ',
200 | ' Forth column ',
201 | ' ',
202 | ' ',
203 | '
'
204 | ].join('');
205 | var element = angular.element(markup);
206 |
207 | var rows = element.find('tbody tr');
208 |
209 | var scope = $rootScope.$new();
210 | $compile(element)(scope);
211 | scope.$digest();
212 |
213 | setTimeout(function () {
214 | rows.each(function (index, element) {
215 | var titles = Array.prototype.map.call(element.querySelectorAll('td'), function (item) {
216 | return item.getAttribute('data-title');
217 | });
218 | expect(titles).toEqual(['First title', 'Second title', 'Third title', 'Forth title']);
219 | });
220 | done();
221 | }, 0);
222 | });
223 |
224 | it('supports ng-repeat applied on TR', function (done) {
225 | var markup = [
226 | '',
227 | ' ',
228 | ' ',
229 | ' First title ',
230 | ' Second title ',
231 | ' Third title ',
232 | ' Forth title ',
233 | ' ',
234 | ' ',
235 | ' ',
236 | ' ',
237 | ' First column ',
238 | ' Second column ',
239 | ' Third column ',
240 | ' Forth column ',
241 | ' ',
242 | ' ',
243 | '
'
244 | ].join('');
245 | var element = angular.element(markup);
246 | var scope = $rootScope.$new();
247 | scope.rows = [0, 1];
248 |
249 | $compile(element)(scope);
250 | scope.$digest();
251 |
252 | setTimeout(function () {
253 | var firstDataRow = element.find('tbody tr:first td');
254 |
255 | expect(firstDataRow.eq(0).attr('data-title')).toBe('First title');
256 | expect(firstDataRow.eq(1).attr('data-title')).toBe('Second title');
257 | expect(firstDataRow.eq(2).attr('data-title')).toBe('Third title');
258 | expect(firstDataRow.eq(3).attr('data-title')).toBe('Forth title');
259 |
260 | done();
261 | }, 0);
262 | });
263 |
264 | it('supports ng-repeat applied on TH', function (done) {
265 | var markup = [
266 | '',
267 | ' ',
268 | ' ',
269 | ' {{header}} ',
270 | ' ',
271 | ' ',
272 | ' ',
273 | ' ',
274 | ' Column 1 - Content ',
275 | ' Column 2 - Content ',
276 | ' ',
277 | ' ',
278 | '
'
279 | ].join('');
280 | var element = angular.element(markup);
281 | var scope = $rootScope.$new();
282 | scope.headers = ['Column 1', 'Column 2'];
283 |
284 | $compile(element)(scope);
285 | scope.$digest();
286 |
287 | var firstDataRow = element.find('tbody tr:first td');
288 |
289 | setTimeout(() => {
290 | expect(firstDataRow.eq(0).attr('data-title')).toBe('Column 1');
291 | expect(firstDataRow.eq(0).text()).toBe('Column 1 - Content');
292 | expect(firstDataRow.eq(1).attr('data-title')).toBe('Column 2');
293 | expect(firstDataRow.eq(1).text()).toBe('Column 2 - Content');
294 |
295 | done();
296 | }, 0);
297 | });
298 |
299 | it('supports ng-if applied on TD with data-title', function () {
300 | var markup = [
301 | '',
302 | ' ',
303 | ' ',
304 | ' column ',
305 | ' ',
306 | ' ',
307 | ' ',
308 | ' ',
309 | ' tom ',
310 | ' jerry ',
311 | ' ',
312 | ' ',
313 | '
'
314 | ].join('');
315 | var element = angular.element(markup);
316 | var scope = $rootScope.$new();
317 | scope.condition = true;
318 |
319 | var firstDataRow = element.find('tbody tr:first td');
320 |
321 | $compile(element)(scope);
322 |
323 | expect(firstDataRow.eq(1).text()).toBe('jerry');
324 | expect(firstDataRow.eq(1).attr('data-title')).toBe('column');
325 | });
326 |
327 | it('supports bootstrap', function () {
328 | var markup = [
329 | '',
330 | ' ',
331 | ' ',
332 | ' First title ',
333 | ' Second title ',
334 | ' Third title ',
335 | ' Forth title ',
336 | ' ',
337 | ' ',
338 | ' ',
339 | ' ',
340 | ' First column ',
341 | ' Second column ',
342 | ' Third column ',
343 | ' Forth column ',
344 | ' ',
345 | ' ',
346 | '
'
347 | ].join('');
348 | var element = angular.element(markup);
349 | angular.element("body").append(element);
350 |
351 | var firstDataRow = element.find('tbody tr td');
352 |
353 | var styles = getComputedStyle(firstDataRow[0]);
354 | expect(styles.paddingLeft).toBe('12px');
355 |
356 | $compile(element);
357 | $rootScope.$digest();
358 |
359 | expect(styles.paddingLeft).toBe('50%');
360 | element.remove();
361 | });
362 |
363 | describe('nested tables', function () {
364 |
365 | var element;
366 | beforeEach(function () {
367 | var markup = [
368 | '',
369 | ' ',
370 | ' ',
371 | ' First title ',
372 | ' Second title ',
373 | ' ',
374 | ' ',
375 | ' ',
376 | ' ',
377 | ' First column ',
378 | ' ',
379 | ' ',
380 | ' nested title ',
381 | ' nested table ',
382 | '
',
383 | ' ',
384 | ' ',
385 | ' ',
386 | '
'
387 | ].join('');
388 | element = angular.element(markup);
389 |
390 | var scope = $rootScope.$new();
391 | $compile(element)(scope);
392 | scope.$digest();
393 | });
394 |
395 | it('nested tables are ignored', function (done) {
396 | var tds = element.find('table td');
397 |
398 | setTimeout(function () {
399 | var content = Array.prototype.map.call(tds, function (item) {
400 | return item.textContent;
401 | });
402 | expect(content).toEqual(['nested table']);
403 | var titles = Array.prototype.map.call(tds, function (item) {
404 | return item.getAttribute('data-title');
405 | });
406 | expect(titles).toEqual([null]);
407 | done();
408 | }, 0);
409 | });
410 |
411 | it('nested tables does not match responsive CSS', function (done) {
412 | setTimeout(function () {
413 | angular.element("body").append(element);
414 | var thStyles = getComputedStyle(element.find('table th')[0]);
415 | expect(thStyles.position).toBe('static');
416 | var tdStyles = getComputedStyle(element.find('table td')[0]);
417 | expect(tdStyles.display).toBe('table-cell');
418 | var pseudoElStyles = getComputedStyle(element.find('table td')[0], '::before');
419 | expect(pseudoElStyles.position).toBe('static');
420 | element.remove();
421 | done();
422 | }, 100);
423 | });
424 | });
425 |
426 | describe('responsive-dynamic', function () {
427 |
428 | it('TDs are handled only on tables with wt-responsive-table', function (done) {
429 | var markup = [
430 | '',
431 | ' ',
432 | ' ',
433 | ' column ',
434 | ' simple ',
435 | ' ',
436 | ' ',
437 | ' ',
438 | ' ',
439 | ' jerry ',
440 | ' tom ',
441 | ' ',
442 | ' ',
443 | '
'
444 | ].join('');
445 | var element = angular.element(markup);
446 | var scope = $rootScope.$new();
447 | $compile(element)(scope);
448 | scope.$digest();
449 |
450 | var els = element.find('tbody tr:first td');
451 | setTimeout(function () {
452 | expect(els.attr('data-title')).toBeUndefined();
453 | done();
454 | }, 0);
455 | });
456 |
457 |
458 | it('supports ng-if applied on all TDs', function (done) {
459 | var markup = [
460 | '',
461 | ' ',
462 | ' ',
463 | ' column ',
464 | ' ',
465 | ' ',
466 | ' ',
467 | ' ',
468 | ' tom ',
469 | ' jerry ',
470 | ' ',
471 | ' ',
472 | '
'
473 | ].join('');
474 | var element = angular.element(markup);
475 | var scope = $rootScope.$new();
476 | scope.condition = true;
477 |
478 | $compile(element)(scope);
479 | scope.$digest();
480 |
481 | var els = element.find('tbody tr:first td');
482 | setTimeout(function () {
483 | expect(els.eq(0).text()).toBe('jerry');
484 | expect(els.eq(0).attr('data-title')).toBe('column');
485 | done();
486 | }, 0);
487 | });
488 |
489 | it('supports ng-if applied on some TDs', function (done) {
490 | var markup = [
491 | '',
492 | ' ',
493 | ' ',
494 | ' column ',
495 | ' simple ',
496 | ' ',
497 | ' ',
498 | ' ',
499 | ' ',
500 | ' tom ',
501 | ' jerry ',
502 | ' simple ',
503 | ' ',
504 | ' ',
505 | '
'
506 | ].join('');
507 | var element = angular.element(markup);
508 | var scope = $rootScope.$new();
509 | scope.condition = true;
510 |
511 | $compile(element)(scope);
512 | scope.$digest();
513 |
514 | var els = element.find('tbody tr:first td');
515 | setTimeout(function () {
516 | expect(els.eq(0).text()).toBe('jerry');
517 | expect(els.eq(0).attr('data-title')).toBe('column');
518 | expect(els.eq(1).text()).toBe('simple');
519 | expect(els.eq(1).attr('data-title')).toBe('simple');
520 | done();
521 | }, 0);
522 | });
523 |
524 | });
525 | });
526 |
--------------------------------------------------------------------------------
/tests/lib.js:
--------------------------------------------------------------------------------
1 | ///
2 | jQuery.expr.filters.offscreen = function(el) {
3 | //console.log('el(' + el.textContent + '): (' + el.offsetLeft + ' x ' + el.offsetTop + ') -> (' + (el.offsetLeft+el.offsetWidth) + ' x ' + (el.offsetTop+el.offsetHeight) + ')');
4 | return (
5 | (el.offsetLeft + el.offsetWidth) < 0
6 | || (el.offsetTop + el.offsetHeight) < 0
7 | || (el.offsetLeft > window.innerWidth || el.offsetTop > window.innerHeight)
8 | );
9 | };
--------------------------------------------------------------------------------