├── .gitattributes
├── .gitignore
├── LICENSE
├── README.md
├── angular-scrollable-table.js
├── angular-scrollable-table.min.js
├── bower.json
├── demo
├── app.js
└── index.html
├── package.json
└── scrollable-table.css
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 |
4 | # Custom for Visual Studio
5 | *.cs diff=csharp
6 | *.sln merge=union
7 | *.csproj merge=union
8 | *.vbproj merge=union
9 | *.fsproj merge=union
10 | *.dbproj merge=union
11 |
12 | # Standard to msysgit
13 | *.doc diff=astextplain
14 | *.DOC diff=astextplain
15 | *.docx diff=astextplain
16 | *.DOCX diff=astextplain
17 | *.dot diff=astextplain
18 | *.DOT diff=astextplain
19 | *.pdf diff=astextplain
20 | *.PDF diff=astextplain
21 | *.rtf diff=astextplain
22 | *.RTF diff=astextplain
23 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | #################
2 | ## Eclipse
3 | #################
4 |
5 | *.pydevproject
6 | .project
7 | .metadata
8 | bin/
9 | tmp/
10 | *.tmp
11 | *.bak
12 | *.swp
13 | *~.nib
14 | local.properties
15 | .classpath
16 | .settings/
17 | .loadpath
18 | node_modules
19 |
20 | # External tool builders
21 | .externalToolBuilders/
22 |
23 | # Locally stored "Eclipse launch configurations"
24 | *.launch
25 |
26 | # CDT-specific
27 | .cproject
28 |
29 | # PDT-specific
30 | .buildpath
31 |
32 |
33 | #################
34 | ## Visual Studio
35 | #################
36 |
37 | ## Ignore Visual Studio temporary files, build results, and
38 | ## files generated by popular Visual Studio add-ons.
39 |
40 | # User-specific files
41 | *.suo
42 | *.user
43 | *.sln.docstates
44 |
45 | # Build results
46 |
47 | [Dd]ebug/
48 | [Rr]elease/
49 | x64/
50 | build/
51 | [Bb]in/
52 | [Oo]bj/
53 |
54 | # MSTest test Results
55 | [Tt]est[Rr]esult*/
56 | [Bb]uild[Ll]og.*
57 |
58 | *_i.c
59 | *_p.c
60 | *.ilk
61 | *.meta
62 | *.obj
63 | *.pch
64 | *.pdb
65 | *.pgc
66 | *.pgd
67 | *.rsp
68 | *.sbr
69 | *.tlb
70 | *.tli
71 | *.tlh
72 | *.tmp
73 | *.tmp_proj
74 | *.log
75 | *.vspscc
76 | *.vssscc
77 | .builds
78 | *.pidb
79 | *.log
80 | *.scc
81 |
82 | # Visual C++ cache files
83 | ipch/
84 | *.aps
85 | *.ncb
86 | *.opensdf
87 | *.sdf
88 | *.cachefile
89 |
90 | # Visual Studio profiler
91 | *.psess
92 | *.vsp
93 | *.vspx
94 |
95 | # Guidance Automation Toolkit
96 | *.gpState
97 |
98 | # ReSharper is a .NET coding add-in
99 | _ReSharper*/
100 | *.[Rr]e[Ss]harper
101 |
102 | # TeamCity is a build add-in
103 | _TeamCity*
104 |
105 | # DotCover is a Code Coverage Tool
106 | *.dotCover
107 |
108 | # NCrunch
109 | *.ncrunch*
110 | .*crunch*.local.xml
111 |
112 | # Installshield output folder
113 | [Ee]xpress/
114 |
115 | # DocProject is a documentation generator add-in
116 | DocProject/buildhelp/
117 | DocProject/Help/*.HxT
118 | DocProject/Help/*.HxC
119 | DocProject/Help/*.hhc
120 | DocProject/Help/*.hhk
121 | DocProject/Help/*.hhp
122 | DocProject/Help/Html2
123 | DocProject/Help/html
124 |
125 | # Click-Once directory
126 | publish/
127 |
128 | # Publish Web Output
129 | *.Publish.xml
130 | *.pubxml
131 |
132 | # NuGet Packages Directory
133 | ## TODO: If you have NuGet Package Restore enabled, uncomment the next line
134 | #packages/
135 |
136 | # Windows Azure Build Output
137 | csx
138 | *.build.csdef
139 |
140 | # Windows Store app package directory
141 | AppPackages/
142 |
143 | # Others
144 | sql/
145 | *.Cache
146 | ClientBin/
147 | [Ss]tyle[Cc]op.*
148 | ~$*
149 | *~
150 | *.dbmdl
151 | *.[Pp]ublish.xml
152 | *.pfx
153 | *.publishsettings
154 |
155 | # RIA/Silverlight projects
156 | Generated_Code/
157 |
158 | # Backup & report files from converting an old project file to a newer
159 | # Visual Studio version. Backup files are not needed, because we have git ;-)
160 | _UpgradeReport_Files/
161 | Backup*/
162 | UpgradeLog*.XML
163 | UpgradeLog*.htm
164 |
165 | # SQL Server files
166 | App_Data/*.mdf
167 | App_Data/*.ldf
168 |
169 | #############
170 | ## Windows detritus
171 | #############
172 |
173 | # Windows image file caches
174 | Thumbs.db
175 | ehthumbs.db
176 |
177 | # Folder config file
178 | Desktop.ini
179 |
180 | # Recycle Bin used on file shares
181 | $RECYCLE.BIN/
182 |
183 | # Mac crap
184 | .DS_Store
185 |
186 |
187 | #############
188 | ## Python
189 | #############
190 |
191 | *.py[co]
192 |
193 | # Packages
194 | *.egg
195 | *.egg-info
196 | dist/
197 | build/
198 | eggs/
199 | parts/
200 | var/
201 | sdist/
202 | develop-eggs/
203 | .installed.cfg
204 |
205 | # Installer logs
206 | pip-log.txt
207 |
208 | # Unit test / coverage reports
209 | .coverage
210 | .tox
211 |
212 | #Translations
213 | *.mo
214 |
215 | #Mr Developer
216 | .mr.developer.cfg
217 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2014 Alec LaLonde and contributors
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of
6 | this software and associated documentation files (the "Software"), to deal in
7 | the Software without restriction, including without limitation the rights to
8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9 | the Software, and to permit persons to whom the Software is furnished to do so,
10 | 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, FITNESS
17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | angular-scrollable-table
2 | ========================
3 |
4 | Yet another table directive for AngularJS.
5 |
6 | This one features a fixed header that elegantly handles overly-long column header names.
7 |
8 | Other features:
9 | * Scroll to row
10 | * Sortable header with custom comparator functions
11 | * Resizable columns
12 |
13 | ## Installation
14 | ```
15 | npm install angular-scrollable-table
16 | ```
17 | OR
18 | ```
19 | bower install angular-scrollable-table
20 | ```
21 |
22 | ## Usage
23 |
24 | ```js
25 | angular.module('myApp', ['scrollable-table', ...]);
26 | ```
27 |
28 | ## Example
29 |
30 | ```html
31 |
32 |
33 |
34 |
35 | Facility |
36 | ...
37 |
38 |
39 |
40 |
42 | {{ proj.facility }} |
43 | ...
44 |
45 |
46 |
47 |
48 | ```
49 |
50 | where the controller contains
51 |
52 | ```js
53 | $scope.visibleProjects = [{
54 | facility: "Atlanta",
55 | code: "C-RD34",
56 | cost: 540000,
57 | conditionRating: 52,
58 | extent: 100,
59 | planYear: 2014
60 | }, ...];
61 |
62 | $scope.$watch('selected', function(fac) {
63 | $scope.$broadcast("rowSelected", fac);
64 | });
65 | })
66 | ```
67 |
68 | Third-party dependencies:
69 | * jQuery
70 | * Bootstrap 3 CSS (for styling, optional. See the 'bootstrap2' branch also)
71 |
72 | Demo here: https://jsfiddle.net/alalonde/BrTzg/
73 |
74 | More infomation here: http://blog.boxelderweb.com/2013/12/19/angularjs-fixed-header-scrollable-table/
75 |
76 | License: MIT
77 |
78 | ## FAQ
79 |
80 | 1. How do I change the height of the table?
81 |
82 | See here: https://jsfiddle.net/alalonde/qgc2gp7d/2/
83 |
--------------------------------------------------------------------------------
/angular-scrollable-table.js:
--------------------------------------------------------------------------------
1 | (function (angular) {
2 | 'use strict';
3 |
4 | var isFirefox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1;
5 | angular.module('scrollable-table', [])
6 | .directive('scrollableTable', ['$timeout', '$q', '$parse', function ($timeout, $q, $parse) {
7 | return {
8 | transclude: true,
9 | restrict: 'E',
10 | scope: {
11 | rows: '=watch',
12 | sortFn: '='
13 | },
14 | template: '
',
18 | controller: ['$scope', '$element', '$attrs', function ($scope, $element, $attrs) {
19 | // define an API for child directives to view and modify sorting parameters
20 | this.getSortExpr = function () {
21 | return $scope.sortExpr;
22 | };
23 | this.isAsc = function () {
24 | return $scope.asc;
25 | };
26 | this.setSortExpr = function (exp) {
27 | $scope.asc = true;
28 | $scope.sortExpr = exp;
29 | };
30 | this.toggleSort = function () {
31 | $scope.asc = !$scope.asc;
32 | };
33 |
34 | this.doSort = function (comparatorFn) {
35 | if (comparatorFn) {
36 | $scope.rows.sort(function (r1, r2) {
37 | var compared = comparatorFn(r1, r2);
38 | return $scope.asc ? compared : compared * -1;
39 | });
40 | } else {
41 | $scope.rows.sort(function (r1, r2) {
42 | var compared = defaultCompare(r1, r2);
43 | return $scope.asc ? compared : compared * -1;
44 | });
45 | }
46 | };
47 |
48 | this.renderTalble = function (){
49 | return waitForRender().then(fixHeaderWidths);
50 | };
51 |
52 | this.getTableElement = function (){
53 | return $element;
54 | };
55 |
56 | /**
57 | * append handle function to execute after table header resize.
58 | */
59 | this.appendTableResizingHandler = function (handler){
60 | var handlerSequence = $scope.headerResizeHanlers || [];
61 | for(var i = 0;i < handlerSequence.length;i++){
62 | if(handlerSequence[i].name === handler.name){
63 | return;
64 | }
65 | }
66 | handlerSequence.push(handler);
67 | $scope.headerResizeHanlers = handlerSequence;
68 | };
69 |
70 | function defaultCompare(row1, row2) {
71 | var exprParts = $scope.sortExpr.match(/(.+)\s+as\s+(.+)/);
72 | var scope = {};
73 | scope[exprParts[1]] = row1;
74 | var x = $parse(exprParts[2])(scope);
75 |
76 | scope[exprParts[1]] = row2;
77 | var y = $parse(exprParts[2])(scope);
78 |
79 | if (x === y) return 0;
80 | return x > y ? 1 : -1;
81 | }
82 |
83 | function scrollToRow(row) {
84 | var offset = $element.find(".headerSpacer").height();
85 | var currentScrollTop = $element.find(".scrollArea").scrollTop();
86 | $element.find(".scrollArea").scrollTop(currentScrollTop + row.position().top - offset);
87 | }
88 |
89 | $scope.$on('rowSelected', function (event, rowId) {
90 | var row = $element.find(".scrollArea table tr[row-id='" + rowId + "']");
91 | if (row.length === 1) {
92 | // Ensure that the headers have been fixed before scrolling, to ensure accurate
93 | // position calculations
94 | $q.all([waitForRender(), headersAreFixed.promise]).then(function () {
95 | scrollToRow(row);
96 | });
97 | }
98 | });
99 |
100 | // Set fixed widths for the table headers in case the text overflows.
101 | // There's no callback for when rendering is complete, so check the visibility of the table
102 | // periodically -- see http://stackoverflow.com/questions/11125078
103 | function waitForRender() {
104 | var deferredRender = $q.defer();
105 | function wait() {
106 | if ($element.find("table:visible").length === 0) {
107 | $timeout(wait, 100);
108 | } else {
109 | deferredRender.resolve();
110 | }
111 | }
112 |
113 | $timeout(wait);
114 | return deferredRender.promise;
115 | }
116 |
117 | var headersAreFixed = $q.defer();
118 |
119 | function fixHeaderWidths() {
120 | if (!$element.find("thead th .th-inner").length) {
121 | $element.find("thead th").wrapInner('');
122 | }
123 | if($element.find("thead th .th-inner:not(:has(.box))").length) {
124 | $element.find("thead th .th-inner:not(:has(.box))").wrapInner('');
125 | }
126 |
127 | $element.find("table th .th-inner:visible").each(function (index, el) {
128 | el = angular.element(el);
129 | var width = el.parent().width(),
130 | lastCol = $element.find("table th:visible:last"),
131 | headerWidth = width;
132 | if (lastCol.css("text-align") !== "center") {
133 | var hasScrollbar = $element.find(".scrollArea").height() < $element.find("table").height();
134 | if (lastCol[0] == el.parent()[0] && hasScrollbar) {
135 | headerWidth += $element.find(".scrollArea").width() - $element.find("tbody tr").width();
136 | headerWidth = Math.max(headerWidth, width);
137 | }
138 | }
139 | var minWidth = _getScale(el.parent().css('min-width')),
140 | title = el.parent().attr("title");
141 | headerWidth = Math.max(minWidth, headerWidth);
142 | el.css("width", headerWidth);
143 | if (!title) {
144 | // ordinary column(not sortableHeader) has box child div element that contained title string.
145 | title = el.find(".title .ng-scope").text() || el.find(".box").text();
146 | }
147 | el.attr("title", title.trim());
148 | });
149 | headersAreFixed.resolve();
150 | }
151 |
152 | // when the data model changes, fix the header widths. See the comments here:
153 | // http://docs.angularjs.org/api/ng.$timeout
154 | $scope.$watch('rows', function (newValue, oldValue) {
155 | if (newValue) {
156 | renderChains($element.find('.scrollArea').width());
157 | // clean sort status and scroll to top of table once records replaced.
158 | $scope.sortExpr = null;
159 | // FIXME what is the reason here must scroll to top? This may cause confusing if using scrolling to implement pagination.
160 | $element.find('.scrollArea').scrollTop(0);
161 | }
162 | });
163 |
164 | $scope.asc = !$attrs.hasOwnProperty("desc");
165 | $scope.sortAttr = $attrs.sortAttr;
166 |
167 | var headerElementToFakeScroll = isFirefox ? "thead" : "thead th .th-inner";
168 | $element.find(".scrollArea").on("scroll", function (event) {
169 | $element.find(headerElementToFakeScroll).css('margin-left', 0 - event.target.scrollLeft);
170 | });
171 |
172 | $scope.$on("renderScrollableTable", function() {
173 | renderChains($element.find('.scrollArea').width());
174 | });
175 |
176 | var onResizeCallback = function(){
177 | $timeout(function(){
178 | $scope.$apply();
179 | });
180 | };
181 | angular.element(window).on('resize', onResizeCallback);
182 | $scope.$on('$destroy', function() {
183 | angular.element(window).off('resize', onResizeCallback);
184 | });
185 | $scope.$watch(function(){
186 | return $element.find('.scrollArea').width();
187 | }, function(newWidth, oldWidth){
188 | if(newWidth * oldWidth <= 0){
189 | return;
190 | }
191 | renderChains();
192 | });
193 |
194 | function renderChains(){
195 | var resizeQueue = waitForRender().then(fixHeaderWidths),
196 | customHandlers = $scope.headerResizeHanlers || [];
197 | for(var i = 0;i < customHandlers.length;i++){
198 | resizeQueue = resizeQueue.then(customHandlers[i]);
199 | }
200 | return resizeQueue;
201 | }
202 | }]
203 | };
204 | }])
205 | .directive('sortableHeader', [function () {
206 | return {
207 | transclude: true,
208 | scope: true,
209 | require: '^scrollableTable',
210 | template:
211 | '' +
212 | '
' +
213 | '
' +
214 | '
' +
215 | '' +
216 | '' +
217 | '' +
218 | '' +
219 | '' +
220 | '
' +
221 | '
',
222 | link: function (scope, elm, attrs, tableController) {
223 | var expr = attrs.on || "a as a." + attrs.col;
224 | scope.element = angular.element(elm);
225 | scope.isActive = function () {
226 | return tableController.getSortExpr() === expr;
227 | };
228 | scope.toggleSort = function (e) {
229 | if (scope.isActive()) {
230 | tableController.toggleSort();
231 | } else {
232 | tableController.setSortExpr(expr);
233 | }
234 | tableController.doSort(scope[attrs.comparatorFn]);
235 | e.preventDefault();
236 | };
237 | scope.isAscending = function () {
238 | if (scope.focused && !scope.isActive()) {
239 | return true;
240 | } else {
241 | return tableController.isAsc();
242 | }
243 | };
244 |
245 | scope.enter = function () {
246 | scope.focused = true;
247 | };
248 | scope.leave = function () {
249 | scope.focused = false;
250 | };
251 |
252 | scope.isLastCol = function() {
253 | return elm.parent().find("th:last-child").get(0) === elm.get(0);
254 | };
255 | }
256 | };
257 | }])
258 | .directive('resizable', ['$compile', function($compile){
259 | return {
260 | restrict: 'A',
261 | priority: 0,
262 | scope: false,
263 | require: 'scrollableTable',
264 | link: function postLink(scope, elm, attrs, tableController){
265 | tableController.appendTableResizingHandler(function(){
266 | _init();
267 | });
268 |
269 | tableController.appendTableResizingHandler(function relayoutHeaders(){
270 | var tableElement = tableController.getTableElement().find('.scrollArea table');
271 | if(tableElement.css('table-layout') === 'auto'){
272 | initRodPos();
273 | }else{
274 | _resetColumnsSize(tableElement.parent().width());
275 | }
276 | });
277 |
278 | scope.resizing = function(e){
279 | var screenOffset = tableController.getTableElement().find('.scrollArea').scrollLeft(),
280 | thInnerElm = angular.element(e.target).parent(),
281 | thElm = thInnerElm.parent(),
282 | startPoint = _getScale(thInnerElm.css('left')) + thInnerElm.width() - screenOffset,
283 | movingPos = e.pageX,
284 | _document = angular.element(document),
285 | _body = angular.element('body'),
286 | coverPanel = angular.element('.scrollableContainer .resizing-cover'),
287 | scaler = angular.element('');
288 |
289 | _body.addClass('scrollable-resizing');
290 | coverPanel.addClass('active');
291 | angular.element('.scrollableContainer').append(scaler);
292 | scaler.css('left', startPoint);
293 |
294 | _document.bind('mousemove', function (e){
295 | var offsetX = e.pageX - movingPos,
296 | movedOffset = _getScale(scaler.css('left')) - startPoint,
297 | widthOfActiveCol = thElm.width(),
298 | nextElm = thElm.nextAll('th:visible').first(),
299 | minWidthOfActiveCol = _getScale(thElm.css('min-width')),
300 | widthOfNextColOfActive = nextElm.width(),
301 | minWidthOfNextColOfActive = _getScale(nextElm.css('min-width'));
302 | movingPos = e.pageX;
303 | e.preventDefault();
304 | if((offsetX > 0 && widthOfNextColOfActive - movedOffset <= minWidthOfNextColOfActive)
305 | || (offsetX < 0 && widthOfActiveCol + movedOffset <= minWidthOfActiveCol)){
306 | //stopping resize if user trying to extension and the active/next column already minimised.
307 | return;
308 | }
309 | scaler.css('left', _getScale(scaler.css('left')) + offsetX);
310 | });
311 | _document.bind('mouseup', function (e) {
312 | e.preventDefault();
313 | scaler.remove();
314 | _body.removeClass('scrollable-resizing');
315 | coverPanel.removeClass('active');
316 | _document.unbind('mousemove');
317 | _document.unbind('mouseup');
318 |
319 | var offsetX = _getScale(scaler.css('left')) - startPoint,
320 | newWidth = thElm.width(),
321 | minWidth = _getScale(thElm.css('min-width')),
322 | nextElm = thElm.nextAll('th:visible').first(),
323 | widthOfNextColOfActive = nextElm.width(),
324 | minWidthOfNextColOfActive = _getScale(nextElm.css('min-width')),
325 | tableElement = tableController.getTableElement().find('.scrollArea table');
326 |
327 | //hold original width of cells, to display cells as their original width after turn table-layout to fixed.
328 | if(tableElement.css('table-layout') === 'auto'){
329 | tableElement.find("th .th-inner").each(function (index, el) {
330 | el = angular.element(el);
331 | var width = el.parent().width();
332 | el.parent().css('width', width);
333 | });
334 | }
335 |
336 | tableElement.css('table-layout', 'fixed');
337 |
338 | if(offsetX > 0 && widthOfNextColOfActive - offsetX <= minWidthOfNextColOfActive){
339 | offsetX = widthOfNextColOfActive - minWidthOfNextColOfActive;
340 | }
341 | nextElm.removeAttr('style');
342 | newWidth += offsetX;
343 | thElm.css('width', Math.max(minWidth, newWidth));
344 | nextElm.css('width', widthOfNextColOfActive - offsetX);
345 | tableController.renderTalble().then(resizeHeaderWidth());
346 | });
347 | };
348 |
349 | function _init(){
350 | var thInnerElms = elm.find('table th:not(:last-child) .th-inner');
351 | if(thInnerElms.find('.resize-rod').length == 0){
352 | tableController.getTableElement().find('.scrollArea table').css('table-layout', 'auto');
353 | var resizeRod = angular.element('
');
354 | thInnerElms.append($compile(resizeRod)(scope));
355 | }
356 | }
357 |
358 | function initRodPos(){
359 | var tableElement = tableController.getTableElement();
360 | var headerPos = 1;// 1 is the width of right border;
361 | tableElement.find("table th .th-inner:visible").each(function (index, el) {
362 | el = angular.element(el);
363 | var width = el.parent().width(), //to made header consistent with its parent.
364 | // if it's the last header, add space for the scrollbar equivalent unless it's centered
365 | minWidth = _getScale(el.parent().css('min-width'));
366 | width = Math.max(minWidth, width);
367 | el.css("left", headerPos);
368 | headerPos += width;
369 | });
370 | }
371 |
372 | function resizeHeaderWidth(){
373 | var headerPos = 1,// 1 is the width of right border;
374 | tableElement = tableController.getTableElement();
375 | tableController.getTableElement().find("table th .th-inner:visible").each(function (index, el) {
376 | el = angular.element(el);
377 | var width = el.parent().width(), //to made header consistent with its parent.
378 | // if it's the last header, add space for the scrollbar equivalent unless it's centered
379 | lastCol = tableElement.find("table th:visible:last"),
380 | minWidth = _getScale(el.parent().css('min-width'));
381 | width = Math.max(minWidth, width);
382 | //following are resize stuff, to made th-inner position correct.
383 | //last column's width should be automatically, to avoid horizontal scroll.
384 | if (lastCol[0] != el.parent()[0]){
385 | el.parent().css('width', width);
386 | }
387 | el.css("left", headerPos);
388 | headerPos += width;
389 | });
390 | }
391 |
392 | function _resetColumnsSize(tableWidth){
393 | var tableElement = tableController.getTableElement(),
394 | columnLength = tableElement.find("table th:visible").length,
395 | lastCol = tableElement.find("table th:visible:last");
396 | tableElement.find("table th:visible").each(function (index, el) {
397 | el = angular.element(el);
398 | if(lastCol.get(0) == el.get(0)){
399 | //last column's width should be automaically, to avoid horizontal scroll.
400 | el.css('width', 'auto');
401 | return;
402 | }
403 | var _width = el.data('width');
404 | if(/\d+%$/.test(_width)){ //percentage
405 | _width = Math.ceil(tableWidth * _getScale(_width) / 100);
406 | } else {
407 | // if data-width not exist, use average width for each columns.
408 | _width = tableWidth / columnLength;
409 | }
410 | el.css('width', _width + 'px');
411 | });
412 | tableController.renderTalble().then(resizeHeaderWidth());
413 | }
414 | }
415 | }
416 | }]);
417 |
418 | function _getScale(sizeCss){
419 | return parseInt(sizeCss.replace(/px|%/, ''), 10);
420 | }
421 | })(angular);
422 |
--------------------------------------------------------------------------------
/angular-scrollable-table.min.js:
--------------------------------------------------------------------------------
1 | !function(e){"use strict";function t(e){return parseInt(e.replace(/px|%/,""),10)}var n=navigator.userAgent.toLowerCase().indexOf("firefox")>-1;e.module("scrollable-table",[]).directive("scrollableTable",["$timeout","$q","$parse",function(i,r,s){return{transclude:!0,restrict:"E",scope:{rows:"=watch",sortFn:"="},template:'
',controller:["$scope","$element","$attrs",function(a,l,o){function c(e,t){var n=a.sortExpr.match(/(.+)\s+as\s+(.+)/),i={};i[n[1]]=e;var r=s(n[2])(i);i[n[1]]=t;var l=s(n[2])(i);return r===l?0:r>l?1:-1}function d(e){var t=l.find(".headerSpacer").height(),n=l.find(".scrollArea").scrollTop();l.find(".scrollArea").scrollTop(n+e.position().top-t)}function h(){function e(){0===l.find("table:visible").length?i(e,100):t.resolve()}var t=r.defer();return i(e),t.promise}function f(){l.find("thead th .th-inner").length||l.find("thead th").wrapInner('
'),l.find("thead th .th-inner:not(:has(.box))").length&&l.find("thead th .th-inner:not(:has(.box))").wrapInner('
'),l.find("table th .th-inner:visible").each(function(n,i){var r=(i=e.element(i)).parent().width(),s=l.find("table th:visible:last"),a=r;if("center"!==s.css("text-align")){var o=l.find(".scrollArea").height()
',link:function(t,n,i,r){var s=i.on||"a as a."+i.col;t.element=e.element(n),t.isActive=function(){return r.getSortExpr()===s},t.toggleSort=function(e){t.isActive()?r.toggleSort():r.setSortExpr(s),r.doSort(t[i.comparatorFn]),e.preventDefault()},t.isAscending=function(){return!(!t.focused||t.isActive())||r.isAsc()},t.enter=function(){t.focused=!0},t.leave=function(){t.focused=!1},t.isLastCol=function(){return n.parent().find("th:last-child").get(0)===n.get(0)}}}}]).directive("resizable",["$compile",function(n){return{restrict:"A",priority:0,scope:!1,require:"scrollableTable",link:function(i,r,s,a){function l(){var t=r.find("table th:not(:last-child) .th-inner");if(0==t.find(".resize-rod").length){a.getTableElement().find(".scrollArea table").css("table-layout","auto");var s=e.element('');t.append(n(s)(i))}}function o(){var n=1;a.getTableElement().find("table th .th-inner:visible").each(function(i,r){var s=(r=e.element(r)).parent().width(),a=t(r.parent().css("min-width"));s=Math.max(a,s),r.css("left",n),n+=s})}function c(){var n=1,i=a.getTableElement();a.getTableElement().find("table th .th-inner:visible").each(function(r,s){var a=(s=e.element(s)).parent().width(),l=i.find("table th:visible:last"),o=t(s.parent().css("min-width"));a=Math.max(o,a),l[0]!=s.parent()[0]&&s.parent().css("width",a),s.css("left",n),n+=a})}function d(n){var i=a.getTableElement(),r=i.find("table th:visible").length,s=i.find("table th:visible:last");i.find("table th:visible").each(function(i,a){if(a=e.element(a),s.get(0)!=a.get(0)){var l=a.data("width");l=/\d+%$/.test(l)?Math.ceil(n*t(l)/100):n/r,a.css("width",l+"px")}else a.css("width","auto")}),a.renderTalble().then(c())}a.appendTableResizingHandler(function(){l()}),a.appendTableResizingHandler(function(){var e=a.getTableElement().find(".scrollArea table");"auto"===e.css("table-layout")?o():d(e.parent().width())}),i.resizing=function(n){var i=a.getTableElement().find(".scrollArea").scrollLeft(),r=e.element(n.target).parent(),s=r.parent(),l=t(r.css("left"))+r.width()-i,o=n.pageX,d=e.element(document),h=e.element("body"),f=e.element(".scrollableContainer .resizing-cover"),u=e.element('');h.addClass("scrollable-resizing"),f.addClass("active"),e.element(".scrollableContainer").append(u),u.css("left",l),d.bind("mousemove",function(e){var n=e.pageX-o,i=t(u.css("left"))-l,r=s.width(),a=s.nextAll("th:visible").first(),c=t(s.css("min-width")),d=a.width(),h=t(a.css("min-width"));o=e.pageX,e.preventDefault(),n>0&&d-i<=h||n<0&&r+i<=c||u.css("left",t(u.css("left"))+n)}),d.bind("mouseup",function(n){n.preventDefault(),u.remove(),h.removeClass("scrollable-resizing"),f.removeClass("active"),d.unbind("mousemove"),d.unbind("mouseup");var i=t(u.css("left"))-l,r=s.width(),o=t(s.css("min-width")),v=s.nextAll("th:visible").first(),p=v.width(),b=t(v.css("min-width")),g=a.getTableElement().find(".scrollArea table");"auto"===g.css("table-layout")&&g.find("th .th-inner").each(function(t,n){var i=(n=e.element(n)).parent().width();n.parent().css("width",i)}),g.css("table-layout","fixed"),i>0&&p-i<=b&&(i=p-b),v.removeAttr("style"),r+=i,s.css("width",Math.max(o,r)),v.css("width",p-i),a.renderTalble().then(c())})}}}}])}(angular);
--------------------------------------------------------------------------------
/bower.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "angular-scrollable-table",
3 | "main": [
4 | "angular-scrollable-table.js",
5 | "scrollable-table.css"
6 | ],
7 | "homepage": "https://github.com/alalonde/angular-scrollable-table",
8 | "authors": [
9 | "Alec LaLonde
"
10 | ],
11 | "description": "A fixed-header scrollable table for AngularJS, featuring resizable columns and sorting",
12 | "keywords": [
13 | "scrollable-table"
14 | ],
15 | "license": "MIT",
16 | "ignore": [
17 | "**/.*",
18 | "node_modules",
19 | "bower_components",
20 | "test",
21 | "tests"
22 | ],
23 | "dependencies": {
24 | "angular": "~1.3.9"
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/demo/app.js:
--------------------------------------------------------------------------------
1 | /* global angular:false */
2 | 'use strict';
3 |
4 | var myApp = angular.module('myApp',['scrollable-table'])
5 | .service('Data', function() {
6 | this.get = function() {
7 | return [{
8 | facility: "Atlanta",
9 | code: "C-RD34",
10 | cost: 540000,
11 | conditionRating: 52,
12 | extent: 100,
13 | planYear: 2014
14 | }, {
15 | facility: "Seattle",
16 | code: "CRDm-4",
17 | cost: 23000,
18 | conditionRating: 40,
19 | extent: 88,
20 | planYear: 2014
21 | }, {
22 | facility: "Austin",
23 | code: "GR-5",
24 | cost: 1200000,
25 | conditionRating: 92,
26 | extent: 90,
27 | planYear: 2014
28 | }, {
29 | facility: "Dayton",
30 | code: "LY-7",
31 | cost: 123000,
32 | conditionRating: 71,
33 | extent: 98,
34 | planYear: 2014
35 | }, {
36 | facility: "Portland",
37 | code: "Dm-4",
38 | cost: 149000,
39 | conditionRating: 89,
40 | extent: 77,
41 | planYear: 2014
42 | }, {
43 | facility: "Dallas",
44 | code: "AW-3",
45 | cost: 14000,
46 | conditionRating: 89,
47 | extent: 79,
48 | planYear: 2014
49 | }, {
50 | facility: "Houston",
51 | code: "Dm-4",
52 | cost: 1100000,
53 | conditionRating: 93,
54 | extent: 79,
55 | planYear: 2014
56 | }, {
57 | facility: "Boston",
58 | code: "DD3",
59 | cost: 1940000,
60 | conditionRating: 86,
61 | extent: 80,
62 | planYear: 2015
63 | }, {
64 | facility: "New York",
65 | code: "ER1",
66 | cost: 910000,
67 | conditionRating: 87,
68 | extent: 82,
69 | planYear: 2015
70 | }];
71 | };
72 | })
73 | // when sorting by year, sort by year and then replace %
74 | .service("Comparators", function() {
75 | this.year = function(r1, r2) {
76 | if(r1.planYear === r2.planYear) {
77 | if (r1.extent === r2.extent) return 0;
78 | return r1.extent > r2.extent ? 1 : -1;
79 | } else if(!r1.planYear || !r2.planYear) {
80 | return !r1.planYear && !r2.planYear ? 0 : (!r1.planYear ? 1 : -1);
81 | }
82 | return r1.planYear > r2.planYear ? 1 : -1;
83 | };
84 | })
85 | .controller('MyCtrl', function($scope, $timeout, $window, Data, Comparators) {
86 | $scope.visibleProjects = Data.get();
87 | $scope.comparator = Comparators.year;
88 | $scope.facilities = [];
89 | for(var i = 0; i < $scope.visibleProjects.length; i++) {
90 | $scope.facilities.push($scope.visibleProjects[i].facility);
91 | }
92 |
93 | $scope.$watch('selected', function(fac) {
94 | $scope.$broadcast("rowSelected", fac);
95 | });
96 |
97 | $scope.changeRecord = function(){
98 | $scope.visibleProjects[3].code = 'aaabbbccc';
99 | $scope.$broadcast("renderScrollableTable");
100 | };
101 |
102 | $scope.replaceRecords = function(){
103 | $scope.visibleProjects = Data.get();
104 | };
105 |
106 | $scope.toggleCol = function() {
107 | $scope.toggleColumn = !$scope.toggleColumn;
108 | $scope.$broadcast("renderScrollableTable");
109 | };
110 | })
111 | ;
--------------------------------------------------------------------------------
/demo/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Angular-scrollable-table directive demo
7 |
8 |
10 |
11 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
35 |
36 |
37 |
38 |
39 | Facility |
40 | Unit code |
41 | Cost |
42 | Condition score |
43 | Not sortable |
44 | Plan year |
45 |
46 |
47 |
48 |
50 | {{ response.facility }} |
51 | {{ response.code }} |
52 | {{ response.cost }} |
53 | {{ response.conditionRating }} |
54 | {{ response.extent }} |
55 | {{ response.planYear }} |
56 |
57 |
58 |
59 |
60 |
64 |
65 |
66 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "angular-scrollable-table",
3 | "keywords": [
4 | "angular",
5 | "table",
6 | "scrollable",
7 | "fixed-header"
8 | ],
9 | "author": "Alec LaLonde",
10 | "description": "A fixed-header scrollable table which auto-truncates overly-long headers, has resizable columns and sortable headers",
11 | "version": "1.1.2",
12 | "license": "MIT",
13 | "repository": {
14 | "type": "git",
15 | "url": "https://github.com/alalonde/angular-scrollable-table"
16 | },
17 | "scripts": {
18 | "compress": "uglifyjs --compress --mangle -o angular-scrollable-table.min.js -- angular-scrollable-table.js"
19 | },
20 | "dependencies": {},
21 | "devDependencies": {
22 | "uglify-js": "^2.7.5"
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/scrollable-table.css:
--------------------------------------------------------------------------------
1 | .scrollableContainer {
2 | height: 310px;
3 | position: relative;
4 | padding-top: 35px;
5 | overflow: hidden;
6 | }
7 | .scrollableContainer .headerSpacer {
8 | border: 1px solid #d5d5d5;
9 | border-bottom-color: #bbb;
10 | position: absolute;
11 | height: 36px;
12 | top: 0;
13 | right: 0;
14 | left: 0;
15 | }
16 | .scrollableContainer th .orderWrapper {
17 | position: absolute;
18 | top: 0;
19 | right: 2px;
20 | cursor: pointer;
21 | }
22 | .scrollableContainer th .orderWrapper .order {
23 | font-size: 8pt;
24 | color: #BDBDBD;
25 | }
26 | .scrollableContainer th .orderWrapper .active {
27 | color: #464646;
28 | }
29 |
30 | .scrollArea {
31 | height: 100%;
32 | overflow-x: auto;
33 | overflow-y: auto;
34 | border: 1px solid #d5d5d5;
35 | /* the implementation of this is still quite buggy; specifically, it doesn't like the
36 | absolutely positioned headers within
37 | -webkit-overflow-scrolling: touch; */
38 | -webkit-overflow-scrolling: auto;
39 | }
40 | .scrollArea table {
41 | overflow-x: auto;
42 | overflow-y: auto;
43 | margin-bottom: 0;
44 | width: 100%;
45 | border: none;
46 | /*border-collapse: separate;*/
47 | }
48 | .scrollArea table th {
49 | padding: 0 !important;
50 | border: none !important;
51 | min-width: 40px;
52 | }
53 | .scrollArea table .th-inner {
54 | overflow: hidden;
55 | text-overflow: ellipsis;
56 | white-space: nowrap;
57 | position: absolute;
58 | top: 0;
59 | height: 36px;
60 | line-height: 36px;
61 | }
62 |
63 | .scrollArea table th .box {
64 | padding: 0 8px;
65 | padding-right: 11px; /* order icon width*/
66 | border-left: 1px solid #ddd;
67 | }
68 |
69 | /* to hack fix firefox border issue */
70 | @-moz-document url-prefix() {
71 | .scrollArea table th .box{
72 | border-right: 1px solid #ddd;
73 | border-left: none;
74 | }
75 | }
76 |
77 | .scrollArea table .th-inner .ng-scope {
78 | display: block;
79 | overflow: hidden;
80 | text-overflow: ellipsis;
81 | }
82 | .scrollArea table tr th:first-child th .box {
83 | border-left: none;
84 | }
85 | .scrollArea table .th-inner.condensed {
86 | padding: 0 3px;
87 | }
88 | .scrollArea table tbody tr td:first-child {
89 | border-left: none;
90 | }
91 | .scrollArea table tbody tr td:last-child {
92 | border-right: none;
93 | }
94 | .scrollArea table tbody tr:first-child td {
95 | border-top: none;
96 | }
97 | .scrollArea table tbody tr:last-child td {
98 | border-bottom: 2px solid #ddd;
99 | }
100 | .scrollArea table tbody tr td {
101 | border-bottom: 1px solid #ddd;
102 | overflow: hidden;
103 | text-overflow: ellipsis;
104 | }
105 |
106 | .scrollableContainer .scaler {
107 | position: absolute;
108 | top: 0px;
109 | width: 2px;
110 | height: 100%;
111 | background-color: #CFCFCF;
112 | }
113 |
114 | .scrollableContainer th .resize-rod {
115 | position: absolute;
116 | top: 0;
117 | right: 0;
118 | cursor: col-resize;
119 | width: 4px;
120 | height: 100%;
121 | }
122 |
123 | .scrollable-resizing .scrollableContainer {
124 | cursor: col-resize;
125 | -moz-user-select: none;
126 | -webkit-user-select: none;
127 | -ms-user-select: none;
128 | }
--------------------------------------------------------------------------------