├── .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 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 |
Column 1Column 2Column 3Column 4
............
............
............
75 | 76 | ### Directives 77 | 78 | #### wt-responsive-table 79 | 80 | * table: wt-responsive-table 81 | * td: responsive-omit-title: title should be ommited 82 | * td: responsive-omit-if-empty: no row for empty cells 83 | * td: data-title: use to override the header for a given row/cell 84 | 85 | ## Installation 86 | 87 | ### npm 88 | 89 | npm install --save angular-responsive-tables 90 | 91 | ### Bower 92 | 93 | bower install angular-responsive-tables --save 94 | 95 | ### Application 96 | 97 | #### HTML 98 | 99 | 100 | 101 | 102 | #### JavaScript 103 | 104 | var app = angular.module('app', ['wt.responsive']); 105 | 106 | ## Special cases 107 | 108 | ### Header doesn't appear for a row / need to override header 109 | 110 | It's possible to override a header with a `data-title` attribute: 111 | 112 | 113 | tom 114 | jerry 115 | 116 | 117 | ### Changes to header text doesn't reflect in responsive mode 118 | 119 | This is by design. To avoid expensive digest cycles only the content from the first digest cycle is used. 120 | There are no watchers being setup. 121 | 122 | ### Dynamic column names 123 | 124 | When loading column names with an asynchronous task, that is, column names are not available when first compiling the table element, rows in responsive mode won't have headers even after the task completes. 125 | 126 | To avoid this problem, use an `ng-if` to conditionally present the element on screen. 127 | 128 | 129 | 130 | ### IE9 responsive hack 131 | 132 | Because IE9 doesn't handle correctly a `display` CSS rule for `', function (done) { 52 | var markup = [ 53 | '
`, 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 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 |
NameVersionLanguageMaintainerStars
AngularJS1.5JavaScriptGoogle35000
Bootstrap3.3CSSTwitter23000
UI-Router0.13JavaScriptAngularUI15000
65 | 66 |

Simple table with ng-repeat

67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 |
NameVersionLanguageMaintainerStars
{{item.name}}{{item.version}}{{item.language}}{{item.maintainer}}{{item.stars}}
87 | 88 |

Simple table with no thead, tbody

89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 |
NameVersionLanguageMaintainerStars
{{item.name}}{{item.version}}{{item.language}}{{item.maintainer}}{{item.stars}}
105 | 106 |

Simple table with colspan

107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 |
First titleSecond titleThird titleForth title
This cell spans for 3 columnsForth column
First columnSecond columnThird columnForth column
129 | 130 |

Simple table, headers on first column

131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 |
Name{{item.name}}
Version{{item.version}}
Language{{item.language}}
Maintainer{{item.maintainer}}
Stars{{item.stars}}
155 | 156 |

With Bootstrap

157 | 158 |

Static table

159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 |
NameVersionLanguageMaintainerStars
AngularJS1.5JavaScriptGoogle35000
Bootstrap3.3CSSTwitter23000
UI-Router0.13JavaScriptAngularUI15000
193 | 194 |

Simple table with ng-repeat

195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 |
NameVersionLanguageMaintainerStars
{{item.name}}{{item.version}}{{item.language}}{{item.maintainer}}{{item.stars}}
215 | 216 |

Simple table with no thead, tbody

217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 |
NameVersionLanguageMaintainerStars
{{item.name}}{{item.version}}{{item.language}}{{item.maintainer}}{{item.stars}}
233 | 234 |

Nested tables

235 | 236 |

Non-responsive nested table

237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 255 | 256 | 257 |
First titleSecond title
First column 248 | 249 | 250 | 251 | 252 | 253 |
nested titlenested table
254 |
258 | 259 |

Responsive nested table

260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 278 | 279 | 280 |
First titleSecond title
First column 271 | 272 | 273 | 274 | 275 | 276 |
nested titlenested table
277 |
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 | ' ', 20 | ' ', 21 | ' ', 22 | ' ', 23 | ' ', 24 | ' ', 25 | ' ', 26 | ' ', 27 | ' ', 28 | ' ', 29 | ' ', 30 | ' ', 31 | ' ', 32 | ' ', 33 | ' ', 34 | ' ', 35 | ' ', 36 | ' ', 37 | ' ', 38 | ' ', 39 | '
First titleSecond titleThird titleForth title
First columnSecond columnThird columnForth column
First columnSecond columnThird columnForth column
' 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
', 54 | ' ', 55 | ' ', 56 | ' ', 57 | ' ', 58 | ' ', 59 | ' ', 60 | ' ', 61 | ' ', 62 | ' ', 63 | ' ', 64 | ' ', 65 | ' ', 66 | '
First titleSecond titleThird titleForth title
First columnSecond columnThird columnForth column
' 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 | ' ', 97 | ' ', 98 | ' ', 99 | ' ', 100 | ' ', 101 | ' ', 102 | ' ', 103 | ' ', 104 | ' ', 105 | ' ', 106 | ' ', 107 | ' ', 108 | ' ', 109 | ' ', 110 | ' ', 111 | '
First titleFirst column
Second titleSecond column
Third titleThird column
Forth titleForth column
' 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 | ' ', 143 | ' ', 144 | ' ', 145 | ' ', 146 | ' ', 147 | ' ', 148 | ' ', 149 | ' ', 150 | ' ', 151 | ' ', 152 | ' ', 153 | ' ', 154 | ' ', 155 | ' ', 156 | ' ', 157 | ' ', 158 | ' ', 159 | ' ', 160 | '
First titleSecond titleThird titleForth title
This cell spans for 3 columnsForth column
First columnSecond columnThird columnForth column
' 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 | ' ', 184 | ' ', 185 | ' ', 186 | ' ', 187 | ' ', 188 | ' ', 189 | ' ', 190 | ' ', 191 | ' ', 192 | ' ', 193 | ' ', 194 | ' ', 195 | ' ', 196 | ' ', 197 | ' ', 198 | ' ', 199 | ' ', 200 | ' ', 201 | ' ', 202 | ' ', 203 | '
First titleSecond titleThird titleForth title
First columnSecond columnThird columnForth column
First columnSecond columnThird columnForth column
' 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 | ' ', 230 | ' ', 231 | ' ', 232 | ' ', 233 | ' ', 234 | ' ', 235 | ' ', 236 | ' ', 237 | ' ', 238 | ' ', 239 | ' ', 240 | ' ', 241 | ' ', 242 | ' ', 243 | '
First titleSecond titleThird titleForth title
First columnSecond columnThird columnForth column
' 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 | ' ', 270 | ' ', 271 | ' ', 272 | ' ', 273 | ' ', 274 | ' ', 275 | ' ', 276 | ' ', 277 | ' ', 278 | '
{{header}}
Column 1 - ContentColumn 2 - Content
' 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 | ' ', 305 | ' ', 306 | ' ', 307 | ' ', 308 | ' ', 309 | ' ', 310 | ' ', 311 | ' ', 312 | ' ', 313 | '
column
tomjerry
' 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 | ' ', 333 | ' ', 334 | ' ', 335 | ' ', 336 | ' ', 337 | ' ', 338 | ' ', 339 | ' ', 340 | ' ', 341 | ' ', 342 | ' ', 343 | ' ', 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 | ' ', 372 | ' ', 373 | ' ', 374 | ' ', 375 | ' ', 376 | ' ', 377 | ' ', 378 | ' ', 384 | ' ', 385 | ' ', 386 | '
First titleSecond title
First column', 379 | ' ', 380 | ' ', 381 | ' ', 382 | '
nested title
nested table
', 383 | '
' 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 | ' ', 434 | ' ', 435 | ' ', 436 | ' ', 437 | ' ', 438 | ' ', 439 | ' ', 440 | ' ', 441 | ' ', 442 | ' ', 443 | '
columnsimple
jerrytom
' 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 | ' ', 464 | ' ', 465 | ' ', 466 | ' ', 467 | ' ', 468 | ' ', 469 | ' ', 470 | ' ', 471 | ' ', 472 | '
column
tomjerry
' 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 | ' ', 495 | ' ', 496 | ' ', 497 | ' ', 498 | ' ', 499 | ' ', 500 | ' ', 501 | ' ', 502 | ' ', 503 | ' ', 504 | ' ', 505 | '
columnsimple
tomjerrysimple
' 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 | }; --------------------------------------------------------------------------------