', {
156 | class: scope.options.handleClass || 'rz-handle'
157 | });
158 | $(column).prepend(handle);
159 |
160 | // Add handles to handles for later removal
161 | handles.push(handle)
162 |
163 | // Use the middleware to decide which columns this handle controls
164 | var controlledColumn = resizer.handleMiddleware(handle, column)
165 |
166 | // Bind mousedown, mousemove & mouseup events
167 | bindEventToHandle(scope, table, handle, controlledColumn);
168 | }
169 |
170 | function bindEventToHandle(scope, table, handle, column) {
171 |
172 | // This event starts the dragging
173 | $(handle).mousedown(function(event) {
174 | if (isFirstDrag) {
175 | resizer.onFirstDrag(column, handle);
176 | resizer.onTableReady();
177 | isFirstDrag = false;
178 | }
179 |
180 | scope.options.onResizeStarted && scope.options.onResizeStarted(column)
181 |
182 | var optional = {}
183 | if (resizer.intervene) {
184 | optional = resizer.intervene.selector(column);
185 | optional.column = optional;
186 | optional.orgWidth = $(optional).width();
187 | }
188 |
189 | // Prevent text-selection, object dragging ect.
190 | event.preventDefault();
191 |
192 | // Change css styles for the handle
193 | $(handle).addClass(scope.options.handleClassActive || 'rz-handle-active');
194 |
195 | // Get mouse and column origin measurements
196 | var orgX = event.clientX;
197 | var orgWidth = $(column).width();
198 |
199 | // On every mouse move, calculate the new width
200 | listener = calculateWidthEvent(scope, column, orgX, orgWidth, optional)
201 | $(window).mousemove(listener)
202 |
203 | // Stop dragging as soon as the mouse is released
204 | $(window).one('mouseup', unbindEvent(scope, column, handle))
205 | })
206 | }
207 |
208 | function calculateWidthEvent(scope, column, orgX, orgWidth, optional) {
209 | return function(event) {
210 | // Get current mouse position
211 | var newX = event.clientX;
212 |
213 | // Use calculator function to calculate new width
214 | var diffX = newX - orgX;
215 | var newWidth = resizer.calculate(orgWidth, diffX);
216 |
217 | if (newWidth < getMinWidth(column)) return;
218 | if (resizer.restrict(newWidth, diffX)) return;
219 |
220 | // Extra optional column
221 | if (resizer.intervene){
222 | var optWidth = resizer.intervene.calculator(optional.orgWidth, diffX);
223 | if (optWidth < getMinWidth(optional.column)) return;
224 | if (resizer.intervene.restrict(optWidth, diffX)) return;
225 | $(optional.column).width(optWidth)
226 | }
227 |
228 | scope.options.onResizeInProgress && scope.options.onResizeInProgress(column, newWidth, diffX)
229 |
230 | // Set size
231 | $(column).width(newWidth);
232 | }
233 | }
234 |
235 | function getMinWidth(column) {
236 | // "25px" -> 25
237 | return parseInt($(column).css('min-width')) || 0;
238 | }
239 |
240 | function getResizer(scope, attr) {
241 | try {
242 | var mode = attr.rzMode ? scope.mode : 'BasicResizer';
243 | var Resizer = $injector.get(mode)
244 | return Resizer;
245 | } catch (e) {
246 | console.error("The resizer "+ scope.mode +" was not found");
247 | return null;
248 | }
249 | }
250 |
251 |
252 | function unbindEvent(scope, column, handle) {
253 | // Event called at end of drag
254 | return function( /*event*/ ) {
255 | $(handle).removeClass(scope.options.handleClassActive || 'rz-handle-active');
256 |
257 | if (listener) {
258 | $(window).unbind('mousemove', listener);
259 | }
260 |
261 | scope.options.onResizeEnded && scope.options.onResizeEnded(column)
262 |
263 | resizer.onEndDrag();
264 |
265 | saveColumnSizes();
266 | }
267 | }
268 |
269 | function saveColumnSizes() {
270 | if (!saveTableSizes) return;
271 |
272 | if (!cache) cache = {};
273 | $(columns).each(function(index, column) {
274 | var colScope = angular.element(column).scope()
275 | var id = colScope.rzCol || $(column).attr('id')
276 | if (!id) return;
277 | cache[id] = resizer.saveAttr(column);
278 | })
279 |
280 | resizeStorage.saveTableSizes(table, mode, profile, cache);
281 | }
282 |
283 | function setColumnSizes(cache) {
284 | if (!cache) {
285 | return;
286 | }
287 |
288 | $(table).width('auto');
289 |
290 | ctrlColumns.each(function(index, column){
291 | var colScope = angular.element(column).scope()
292 | var id = colScope.rzCol || $(column).attr('id')
293 | var cacheWidth = cache[id];
294 | $(column).css({ width: cacheWidth });
295 | })
296 |
297 | resizer.onTableReady();
298 | }
299 |
300 | // Return this directive as a object literal
301 | return {
302 | restrict: 'A',
303 | link: link,
304 | controller: RzController,
305 | scope: {
306 | // rzMode will determine the rezising behavior
307 | mode: '=rzMode',
308 | // rzProfile loads a profile from local storage
309 | profile: '=?rzProfile',
310 | // rzBusy will postpone initialisation
311 | busy: '=?rzBusy',
312 | // rzSave saves columns to local storage
313 | saveTableSizes: '=?rzSave',
314 | // rzOptions supplies addition options
315 | options: '=?rzOptions',
316 | // rzModel binds utility function to controller
317 | model: '=rzModel',
318 | // rzContainer is a query selector for the container DOM
319 | container: '@rzContainer'
320 | }
321 | };
322 |
323 | }]);
324 |
325 | angular.module("rzTable").directive('rzCol', [function() {
326 | // Return this directive as a object literal
327 | return {
328 | restrict: 'A',
329 | priority: 650, /* before ng-if */
330 | link: link,
331 | require: '^^rzTable',
332 | scope: true
333 | };
334 |
335 | function link(scope, element, attr) {
336 | scope.rzCol = scope.$eval(attr.rzCol)
337 | }
338 | }])
339 | angular.module("rzTable").service('resizeStorage', ['$window', function($window) {
340 |
341 | var prefix = "ngColumnResize";
342 |
343 | this.loadTableSizes = function(table, mode, profile) {
344 | var key = getStorageKey(table, mode, profile);
345 | var object = $window.localStorage.getItem(key);
346 | return JSON.parse(object);
347 | }
348 |
349 | this.saveTableSizes = function(table, mode, profile, sizes) {
350 | var key = getStorageKey(table, mode, profile);
351 | if (!key) return;
352 | var string = JSON.stringify(sizes);
353 | $window.localStorage.setItem(key, string)
354 | }
355 |
356 | this.clearAll = function() {
357 | var keys = []
358 | for (var i = 0; i < $window.localStorage.length; ++i) {
359 | var key = localStorage.key(i)
360 | if (key && key.startsWith(prefix)) {
361 | keys.push(key)
362 | }
363 | }
364 | keys.map(function(k) { $window.localStorage.removeItem(k) })
365 | }
366 |
367 | this.clearCurrent = function(table, mode, profile) {
368 | var key = getStorageKey(table, mode, profile);
369 | if (key) {
370 | $window.localStorage.removeItem(key)
371 | }
372 | }
373 |
374 | function getStorageKey(table, mode, profile) {
375 | var id = table.attr('id');
376 | if (!id) {
377 | console.error("Table has no id", table);
378 | return undefined;
379 | }
380 | return prefix + '.' + table.attr('id') + '.' + mode + (profile ? '.' + profile : '');
381 | }
382 |
383 | }]);
384 |
385 | angular.module("rzTable").factory("ResizerModel", [function() {
386 |
387 | function ResizerModel(table, columns, container){
388 | this.table = table;
389 | this.columns = columns;
390 | this.container = container;
391 |
392 | this.handleColumns = this.handles();
393 | this.ctrlColumns = this.ctrlColumns();
394 | }
395 |
396 | ResizerModel.prototype.setup = function() {
397 | // Hide overflow by default
398 | $(this.container).css({
399 | overflowX: 'hidden'
400 | })
401 | }
402 |
403 | ResizerModel.prototype.onTableReady = function () {
404 | // Table is by default 100% width
405 | $(this.table).outerWidth('100%');
406 | };
407 |
408 | ResizerModel.prototype.getMinWidth = function(column) {
409 | // "25px" -> 25
410 | return parseInt($(column).css('min-width')) || 0;
411 | }
412 |
413 | ResizerModel.prototype.handles = function () {
414 | // By default all columns should be assigned a handle
415 | return this.columns;
416 | };
417 |
418 | ResizerModel.prototype.ctrlColumns = function () {
419 | // By default all columns assigned a handle are resized
420 | return this.handleColumns;
421 | };
422 |
423 | ResizerModel.prototype.onFirstDrag = function () {
424 | // By default, set all columns to absolute widths
425 | $(this.ctrlColumns).each(function(index, column) {
426 | $(column).width($(column).width());
427 | })
428 | };
429 |
430 | ResizerModel.prototype.handleMiddleware = function (handle, column) {
431 | // By default, every handle controls the column it is placed in
432 | return column;
433 | };
434 |
435 | ResizerModel.prototype.restrict = function (newWidth) {
436 | return false;
437 | };
438 |
439 | ResizerModel.prototype.calculate = function (orgWidth, diffX) {
440 | // By default, simply add the width difference to the original
441 | return orgWidth + diffX;
442 | };
443 |
444 | ResizerModel.prototype.onEndDrag = function () {
445 | // By default, do nothing when dragging a column ends
446 | return;
447 | };
448 |
449 | ResizerModel.prototype.saveAttr = function (column) {
450 | return $(column).outerWidth();
451 | };
452 |
453 | return ResizerModel;
454 | }]);
455 |
456 | angular.module("rzTable").factory("BasicResizer", ["ResizerModel", function(ResizerModel) {
457 |
458 | function BasicResizer(table, columns, container) {
459 | // Call super constructor
460 | ResizerModel.call(this, table, columns, container)
461 |
462 | // All columns are controlled in basic mode
463 | this.ctrlColumns = this.columns;
464 |
465 | this.intervene = {
466 | selector: interveneSelector,
467 | calculator: interveneCalculator,
468 | restrict: interveneRestrict
469 | }
470 | }
471 |
472 | // Inherit by prototypal inheritance
473 | BasicResizer.prototype = Object.create(ResizerModel.prototype);
474 |
475 | function interveneSelector(column) {
476 | return $(column).next()
477 | }
478 |
479 | function interveneCalculator(orgWidth, diffX) {
480 | return orgWidth - diffX;
481 | }
482 |
483 | function interveneRestrict(newWidth){
484 | return newWidth < 25;
485 | }
486 |
487 | BasicResizer.prototype.setup = function() {
488 | // Hide overflow in mode fixed
489 | $(this.container).css({
490 | overflowX: 'hidden'
491 | })
492 |
493 | $(this.table).css({
494 | width: '100%'
495 | })
496 | };
497 |
498 | BasicResizer.prototype.handles = function() {
499 | // Mode fixed does not require handler on last column
500 | return $(this.columns).not(':last')
501 | };
502 |
503 | BasicResizer.prototype.onFirstDrag = function() {
504 | // Replace all column's width with absolute measurements
505 | this.onEndDrag()
506 | };
507 |
508 | BasicResizer.prototype.onEndDrag = function () {
509 | // Calculates the percent width of each column
510 | var totWidth = $(this.table).outerWidth();
511 |
512 | var callbacks = []
513 |
514 | // Calculate the width of every column
515 | $(this.columns).each(function(index, column) {
516 | var colWidth = $(column).outerWidth();
517 | var percentWidth = colWidth / totWidth * 100 + '%';
518 | callbacks.push(function() {
519 | $(column).css({ width: percentWidth });
520 | })
521 | })
522 |
523 | // Apply the calculated width of every column
524 | callbacks.map(function(cb) { cb() })
525 | };
526 |
527 | BasicResizer.prototype.saveAttr = function (column) {
528 | return $(column)[0].style.width;
529 | };
530 |
531 | // Return constructor
532 | return BasicResizer;
533 |
534 | }]);
535 |
536 | angular.module("rzTable").factory("FixedResizer", ["ResizerModel", function(ResizerModel) {
537 |
538 | function FixedResizer(table, columns, container) {
539 | // Call super constructor
540 | ResizerModel.call(this, table, columns, container)
541 |
542 | this.fixedColumn = $(table).find('th').first();
543 | this.bound = false;
544 | }
545 |
546 | // Inherit by prototypal inheritance
547 | FixedResizer.prototype = Object.create(ResizerModel.prototype);
548 |
549 | FixedResizer.prototype.setup = function() {
550 | // Hide overflow in mode fixed
551 | $(this.container).css({
552 | overflowX: 'hidden'
553 | })
554 |
555 | $(this.table).css({
556 | width: '100%'
557 | })
558 |
559 | // First column is auto to compensate for 100% table width
560 | $(this.columns).first().css({
561 | width: 'auto'
562 | });
563 | };
564 |
565 | FixedResizer.prototype.handles = function() {
566 | // Mode fixed does not require handler on last column
567 | return $(this.columns).not(':last')
568 | };
569 |
570 | FixedResizer.prototype.ctrlColumns = function() {
571 | // In mode fixed, all but the first column should be resized
572 | return $(this.columns).not(':first');
573 | };
574 |
575 | FixedResizer.prototype.onFirstDrag = function() {
576 | // Replace each column's width with absolute measurements
577 | $(this.ctrlColumns).each(function(index, column) {
578 | $(column).width($(column).width());
579 | })
580 | };
581 |
582 | FixedResizer.prototype.handleMiddleware = function (handle, column) {
583 | // Fixed mode handles always controll next neightbour column
584 | return $(column).next();
585 | };
586 |
587 | FixedResizer.prototype.restrict = function (newWidth, diffX) {
588 | if (this.bound && this.bound < diffX) {
589 | this.bound = false
590 | return false
591 | } if (this.bound && this.bound > diffX) {
592 | return true
593 | } else if (this.fixedColumn.width() <= this.getMinWidth(this.fixedColumn)) {
594 | this.bound = diffX
595 | $(this.fixedColumn).width(this.minWidth);
596 | return true;
597 | }
598 | };
599 |
600 | FixedResizer.prototype.onEndDrag = function () {
601 | this.bound = false
602 | };
603 |
604 | FixedResizer.prototype.calculate = function (orgWidth, diffX) {
605 | // Subtract difference - neightbour grows
606 | return orgWidth - diffX;
607 | };
608 |
609 | // Return constructor
610 | return FixedResizer;
611 |
612 | }]);
613 |
614 | angular.module("rzTable").factory("OverflowResizer", ["ResizerModel", function(ResizerModel) {
615 |
616 | function OverflowResizer(table, columns, container) {
617 | // Call super constructor
618 | ResizerModel.call(this, table, columns, container)
619 | }
620 |
621 | // Inherit by prototypal inheritance
622 | OverflowResizer.prototype = Object.create(ResizerModel.prototype);
623 |
624 |
625 | OverflowResizer.prototype.setup = function() {
626 | // Allow overflow in this mode
627 | $(this.container).css({
628 | overflow: 'auto'
629 | });
630 | };
631 |
632 | OverflowResizer.prototype.onTableReady = function() {
633 | // For mode overflow, make table as small as possible
634 | $(this.table).width(1);
635 | };
636 |
637 | // Return constructor
638 | return OverflowResizer;
639 |
640 | }]);
641 |
--------------------------------------------------------------------------------
/dist/angular-table-resize.min.css:
--------------------------------------------------------------------------------
1 | table.rz-table{table-layout:fixed;border-collapse:collapse}table.rz-table th{position:relative;min-width:25px}table.rz-table th .rz-handle{width:10px;height:100%;position:absolute;top:0;right:0;cursor:ew-resize!important}table.rz-table th .rz-handle.rz-handle-active{border-right:1px dotted #000}
--------------------------------------------------------------------------------
/dist/angular-table-resize.min.js:
--------------------------------------------------------------------------------
1 | angular.module("rzTable",[]),angular.module("rzTable").directive("rzTable",["resizeStorage","$injector","$parse",function(t,n,e){function o(t){}function r(t,n,e){W=n,I=t.container?$(t.container):$(W).parent(),t.options=e.rzOptions?t.options||{}:{},$(W).addClass(t.options.tableClass||"rz-table"),h(W,e,t),u(W,e,t),a(W,e,t),s(W,e,t)}function i(t,n,e){return function(o,r){e.busy!==!0&&void 0!==r&&r!==o&&(l(t),h(t,n,e))}}function s(t,n,e){e.$watch("profile",i(t,n,e)),e.$watch("mode",i(t,n,e)),e.$watch("busy",i(t,n,e))}function a(t,n,e){e.$watch(function(){return $(t).find("th").length},i(t,n,e))}function u(n,o,r){if(o.rzModel){var i=e(o.rzModel);i.assign(r.$parent,{update:function(){l(n),h(n,o,r)},reset:function(){c(n),this.clearStorageActive(),this.update()},clearStorage:function(){t.clearAll()},clearStorageActive:function(){t.clearCurrent(n,w,S)}})}}function l(t){x=!0,d(t)}function c(t){$(t).outerWidth("100%"),$(t).find("th").width("auto")}function d(t){D.map(function(t){t.remove()}),D=[]}function h(n,e,o){if(!o.busy){T=$(n).find("th"),w=o.mode,C=!angular.isDefined(o.saveTableSizes)||o.saveTableSizes,S=o.profile;var r=z(o,e);r&&(O=new r(n,T,I),C&&(E=t.loadTableSizes(n,o.mode,o.profile)),M=O.handles(T),R=O.ctrlColumns,O.setup(),g(E),M.each(function(t,e){f(o,n,e)}))}}function f(t,n,e){var o=$("
",{"class":t.options.handleClass||"rz-handle"});$(e).prepend(o),D.push(o);var r=O.handleMiddleware(o,e);p(t,n,o,r)}function p(t,n,e,o){$(e).mousedown(function(n){x&&(O.onFirstDrag(o,e),O.onTableReady(),x=!1),t.options.onResizeStarted&&t.options.onResizeStarted(o);var r={};O.intervene&&(r=O.intervene.selector(o),r.column=r,r.orgWidth=$(r).width()),n.preventDefault(),$(e).addClass(t.options.handleClassActive||"rz-handle-active");var i=n.clientX,s=$(o).width();A=m(t,o,i,s,r),$(window).mousemove(A),$(window).one("mouseup",y(t,o,e))})}function m(t,n,e,o,r){return function(i){var s=i.clientX,a=s-e,u=O.calculate(o,a);if(!(u
n)||(this.fixedColumn.width()<=this.getMinWidth(this.fixedColumn)?(this.bound=n,$(this.fixedColumn).width(this.minWidth),!0):void 0)},n.prototype.onEndDrag=function(){this.bound=!1},n.prototype.calculate=function(t,n){return t-n},n}]),angular.module("rzTable").factory("OverflowResizer",["ResizerModel",function(t){function n(n,e,o){t.call(this,n,e,o)}return n.prototype=Object.create(t.prototype),n.prototype.setup=function(){$(this.container).css({overflow:"auto"})},n.prototype.onTableReady=function(){$(this.table).width(1)},n}]);
--------------------------------------------------------------------------------
/gulpfile.js:
--------------------------------------------------------------------------------
1 | /*jshint esversion: 6 */
2 | const gulp = require('gulp');
3 | const browsersync = require('browser-sync').create();
4 | const runSequence = require('run-sequence');
5 | const uglify = require('gulp-uglify');
6 | const concat = require('gulp-concat');
7 | const rename = require('gulp-rename');
8 | const cleancss = require('gulp-clean-css');
9 |
10 | const jsFiles = [
11 | "scripts/angular-table-resize.js",
12 | "scripts/directives/resize-table-directive.js",
13 | "scripts/directives/resize-col-directive.js",
14 | "scripts/services/resize-storage-service.js",
15 | "scripts/services/resizer-factory.js",
16 | "scripts/resizers/basic-resizer.js",
17 | "scripts/resizers/fixed-resizer.js",
18 | "scripts/resizers/overflow-resizer.js"
19 | ]
20 |
21 | const cssFiles = [
22 | "css/angular-table-resize.css"
23 | ]
24 |
25 | const DIST = './dist/';
26 |
27 | gulp.task('serve', function() {
28 | browsersync.init({
29 | port: 3001,
30 | server: {
31 | baseDir: "./"
32 | }
33 | });
34 | });
35 |
36 | gulp.task('update', function() {
37 | browsersync.update();
38 | });
39 |
40 | gulp.task('watch', function() {
41 | gulp.watch(['index.html', 'css/**', 'scripts/**', 'views/**', 'demo/**'], function() {
42 | runSequence('build', browsersync.reload)
43 | });
44 | });
45 |
46 | gulp.task('dev', function() {
47 | runSequence('serve', 'watch');
48 | });
49 |
50 | gulp.task('build:js', function() {
51 | return gulp.src(jsFiles)
52 | .pipe(concat('angular-table-resize.js'))
53 | .pipe(gulp.dest(DIST))
54 | .pipe(uglify())
55 | .pipe(rename({ extname: '.min.js' }))
56 | .pipe(gulp.dest(DIST))
57 | })
58 |
59 | gulp.task('build:css', function() {
60 | return gulp.src(cssFiles)
61 | .pipe(gulp.dest(DIST))
62 | .pipe(cleancss({ compatibility: 'ie8' }))
63 | .pipe(rename({ extname: '.min.css' }))
64 | .pipe(gulp.dest(DIST))
65 | })
66 |
67 | gulp.task('build', ['build:js', 'build:css'])
68 |
69 | gulp.task('default', ['dev']);
--------------------------------------------------------------------------------
/karma.conf.js:
--------------------------------------------------------------------------------
1 | // Karma configuration
2 | // Generated on Sun Aug 07 2016 17:59:17 GMT+0200 (Romance Daylight Time)
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: ['mocha', 'chai'],
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 | 'node_modules/angular-mocks/angular-mocks.js',
21 | 'dist/angular-table-resize.js',
22 | 'dist/table-resize.css',
23 | 'test/*.js'
24 | ],
25 |
26 |
27 | // list of files to exclude
28 | exclude: [
29 | ],
30 |
31 |
32 | // preprocess matching files before serving them to the browser
33 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
34 | preprocessors: {
35 | },
36 |
37 |
38 | // test results reporter to use
39 | // possible values: 'dots', 'progress'
40 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter
41 | reporters: ['progress'],
42 |
43 |
44 | // web server port
45 | port: 9876,
46 |
47 |
48 | // enable / disable colors in the output (reporters and logs)
49 | colors: true,
50 |
51 |
52 | // level of logging
53 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
54 | logLevel: config.LOG_INFO,
55 |
56 |
57 | // enable / disable watching file and executing tests whenever any file changes
58 | autoWatch: true,
59 |
60 |
61 | // start these browsers
62 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
63 | browsers: ['PhantomJS'],
64 |
65 |
66 | // Continuous Integration mode
67 | // if true, Karma captures browsers, runs the tests and exits
68 | singleRun: false,
69 |
70 | // Concurrency level
71 | // how many browser should be started simultaneous
72 | concurrency: Infinity
73 | })
74 | }
75 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "angular-table-resize",
3 | "version": "2.0.1",
4 | "description": "An AngularJS module for resizing table columns!",
5 | "main": "./dist/angular-table-resize.js",
6 | "scripts": {
7 | "test": "karma start"
8 | },
9 | "repository": {
10 | "type": "git",
11 | "url": "git+https://github.com/Tympanix/angular-table-resize.git"
12 | },
13 | "author": "Mathias Mortensen",
14 | "license": "MIT",
15 | "devDependencies": {
16 | "angular-mocks": "^1.5.8",
17 | "browser-sync": "^2.12.10",
18 | "chai": "^3.5.0",
19 | "gulp": "^3.9.1",
20 | "gulp-clean-css": "^2.0.11",
21 | "gulp-concat": "^2.6.0",
22 | "gulp-rename": "^1.2.2",
23 | "gulp-uglify": "^1.5.4",
24 | "karma": "^1.1.2",
25 | "karma-chai": "^0.1.0",
26 | "mocha": "^3.0.1",
27 | "run-sequence": "^1.2.1"
28 | },
29 | "dependencies": {
30 | "angular": "^1.5.7",
31 | "jquery": "^3.0.0"
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/scripts/angular-table-resize.js:
--------------------------------------------------------------------------------
1 | angular.module("rzTable", []);
2 |
--------------------------------------------------------------------------------
/scripts/directives/resize-col-directive.js:
--------------------------------------------------------------------------------
1 | angular.module("rzTable").directive('rzCol', [function() {
2 | // Return this directive as a object literal
3 | return {
4 | restrict: 'A',
5 | priority: 650, /* before ng-if */
6 | link: link,
7 | require: '^^rzTable',
8 | scope: true
9 | };
10 |
11 | function link(scope, element, attr) {
12 | scope.rzCol = scope.$eval(attr.rzCol)
13 | }
14 | }])
--------------------------------------------------------------------------------
/scripts/directives/resize-table-directive.js:
--------------------------------------------------------------------------------
1 | angular.module("rzTable").directive('rzTable', ['resizeStorage', '$injector', '$parse', function(resizeStorage, $injector, $parse) {
2 |
3 | var mode;
4 | var saveTableSizes;
5 | var profile;
6 |
7 | var columns = null;
8 | var ctrlColumns = null;
9 | var handleColumns = null;
10 | var listener = null;
11 | var handles = []
12 | var table = null;
13 | var container = null;
14 | var resizer = null;
15 | var isFirstDrag = true;
16 |
17 | var cache = null;
18 |
19 | RzController.$inject = ['$scope', '$attrs', '$element'];
20 |
21 | function RzController($scope) {
22 |
23 | }
24 |
25 | function link(scope, element, attr) {
26 | // Set global reference to table
27 | table = element;
28 |
29 | // Set global reference to container
30 | container = scope.container ? $(scope.container) : $(table).parent();
31 |
32 | // Set options to an empty object if undefined
33 | scope.options = attr.rzOptions ? scope.options || {} : {}
34 |
35 | // Add css styling/properties to table
36 | $(table).addClass(scope.options.tableClass || 'rz-table');
37 |
38 | // Initialise handlers, bindings and modes
39 | initialiseAll(table, attr, scope);
40 |
41 | // Bind utility functions to scope object
42 | bindUtilityFunctions(table, attr, scope)
43 |
44 | // Watch for changes in columns
45 | watchTableChanges(table, attr, scope)
46 |
47 | // Watch for scope bindings
48 | setUpWatchers(table, attr, scope)
49 | }
50 |
51 | function renderWatch(table, attr, scope) {
52 | return function(oldVal, newVal) {
53 | if (scope.busy === true) return
54 | if (newVal === undefined) return
55 | if (newVal !== oldVal) {
56 | cleanUpAll(table);
57 | initialiseAll(table, attr, scope);
58 | }
59 | }
60 | }
61 |
62 | function setUpWatchers(table, attr, scope) {
63 | scope.$watch('profile', renderWatch(table, attr, scope))
64 | scope.$watch('mode', renderWatch(table, attr, scope))
65 | scope.$watch('busy', renderWatch(table, attr, scope))
66 | }
67 |
68 | function watchTableChanges(table, attr, scope) {
69 | scope.$watch(function () {
70 | return $(table).find('th').length;
71 | }, renderWatch(table, attr, scope));
72 | }
73 |
74 | function bindUtilityFunctions(table, attr, scope) {
75 | if (!attr.rzModel) return;
76 | var model = $parse(attr.rzModel)
77 | model.assign(scope.$parent, {
78 | update: function() {
79 | cleanUpAll(table)
80 | initialiseAll(table, attr, scope)
81 | },
82 | reset: function() {
83 | resetTable(table)
84 | this.clearStorageActive()
85 | this.update()
86 | },
87 | clearStorage: function() {
88 | resizeStorage.clearAll()
89 | },
90 | clearStorageActive: function() {
91 | resizeStorage.clearCurrent(table, mode, profile)
92 | }
93 | })
94 | }
95 |
96 | function cleanUpAll(table) {
97 | isFirstDrag = true;
98 | deleteHandles(table);
99 | }
100 |
101 | function resetTable(table) {
102 | $(table).outerWidth('100%');
103 | $(table).find('th').width('auto');
104 | }
105 |
106 | function deleteHandles(table) {
107 | handles.map(function(h) { h.remove() })
108 | handles = []
109 | }
110 |
111 | function initialiseAll(table, attr, scope) {
112 | // If busy, postpone initialization
113 | if (scope.busy) return
114 |
115 | // Get all column headers
116 | columns = $(table).find('th');
117 |
118 | mode = scope.mode;
119 | saveTableSizes = angular.isDefined(scope.saveTableSizes) ? scope.saveTableSizes : true;
120 | profile = scope.profile
121 |
122 | // Get the resizer object for the current mode
123 | var ResizeModel = getResizer(scope, attr);
124 | if (!ResizeModel) return;
125 | resizer = new ResizeModel(table, columns, container);
126 |
127 | if (saveTableSizes) {
128 | // Load column sizes from saved storage
129 | cache = resizeStorage.loadTableSizes(table, scope.mode, scope.profile)
130 | }
131 |
132 | // Decide which columns should have a handler attached
133 | handleColumns = resizer.handles(columns);
134 |
135 | // Decide which columns are controlled and resized
136 | ctrlColumns = resizer.ctrlColumns;
137 |
138 | // Execute setup function for the given resizer mode
139 | resizer.setup();
140 |
141 | // Set column sizes from cache
142 | setColumnSizes(cache);
143 |
144 | // Initialise all handlers for every column
145 | handleColumns.each(function(index, column) {
146 | initHandle(scope, table, column);
147 | })
148 |
149 | }
150 |
151 | function initHandle(scope, table, column) {
152 | // Prepend a new handle div to the column
153 | var handle = $('', {
154 | class: scope.options.handleClass || 'rz-handle'
155 | });
156 | $(column).prepend(handle);
157 |
158 | // Add handles to handles for later removal
159 | handles.push(handle)
160 |
161 | // Use the middleware to decide which columns this handle controls
162 | var controlledColumn = resizer.handleMiddleware(handle, column)
163 |
164 | // Bind mousedown, mousemove & mouseup events
165 | bindEventToHandle(scope, table, handle, controlledColumn);
166 | }
167 |
168 | function bindEventToHandle(scope, table, handle, column) {
169 |
170 | // This event starts the dragging
171 | $(handle).mousedown(function(event) {
172 | if (isFirstDrag) {
173 | resizer.onFirstDrag(column, handle);
174 | resizer.onTableReady();
175 | isFirstDrag = false;
176 | }
177 |
178 | scope.options.onResizeStarted && scope.options.onResizeStarted(column)
179 |
180 | var optional = {}
181 | if (resizer.intervene) {
182 | optional = resizer.intervene.selector(column);
183 | optional.column = optional;
184 | optional.orgWidth = $(optional).width();
185 | }
186 |
187 | // Prevent text-selection, object dragging ect.
188 | event.preventDefault();
189 |
190 | // Change css styles for the handle
191 | $(handle).addClass(scope.options.handleClassActive || 'rz-handle-active');
192 |
193 | // Get mouse and column origin measurements
194 | var orgX = event.clientX;
195 | var orgWidth = $(column).width();
196 |
197 | // On every mouse move, calculate the new width
198 | listener = calculateWidthEvent(scope, column, orgX, orgWidth, optional)
199 | $(window).mousemove(listener)
200 |
201 | // Stop dragging as soon as the mouse is released
202 | $(window).one('mouseup', unbindEvent(scope, column, handle))
203 | })
204 | }
205 |
206 | function calculateWidthEvent(scope, column, orgX, orgWidth, optional) {
207 | return function(event) {
208 | // Get current mouse position
209 | var newX = event.clientX;
210 |
211 | // Use calculator function to calculate new width
212 | var diffX = newX - orgX;
213 | var newWidth = resizer.calculate(orgWidth, diffX);
214 |
215 | if (newWidth < getMinWidth(column)) return;
216 | if (resizer.restrict(newWidth, diffX)) return;
217 |
218 | // Extra optional column
219 | if (resizer.intervene){
220 | var optWidth = resizer.intervene.calculator(optional.orgWidth, diffX);
221 | if (optWidth < getMinWidth(optional.column)) return;
222 | if (resizer.intervene.restrict(optWidth, diffX)) return;
223 | $(optional.column).width(optWidth)
224 | }
225 |
226 | scope.options.onResizeInProgress && scope.options.onResizeInProgress(column, newWidth, diffX)
227 |
228 | // Set size
229 | $(column).width(newWidth);
230 | }
231 | }
232 |
233 | function getMinWidth(column) {
234 | // "25px" -> 25
235 | return parseInt($(column).css('min-width')) || 0;
236 | }
237 |
238 | function getResizer(scope, attr) {
239 | try {
240 | var mode = attr.rzMode ? scope.mode : 'BasicResizer';
241 | var Resizer = $injector.get(mode)
242 | return Resizer;
243 | } catch (e) {
244 | console.error("The resizer "+ scope.mode +" was not found");
245 | return null;
246 | }
247 | }
248 |
249 |
250 | function unbindEvent(scope, column, handle) {
251 | // Event called at end of drag
252 | return function( /*event*/ ) {
253 | $(handle).removeClass(scope.options.handleClassActive || 'rz-handle-active');
254 |
255 | if (listener) {
256 | $(window).unbind('mousemove', listener);
257 | }
258 |
259 | scope.options.onResizeEnded && scope.options.onResizeEnded(column)
260 |
261 | resizer.onEndDrag();
262 |
263 | saveColumnSizes();
264 | }
265 | }
266 |
267 | function saveColumnSizes() {
268 | if (!saveTableSizes) return;
269 |
270 | if (!cache) cache = {};
271 | $(columns).each(function(index, column) {
272 | var colScope = angular.element(column).scope()
273 | var id = colScope.rzCol || $(column).attr('id')
274 | if (!id) return;
275 | cache[id] = resizer.saveAttr(column);
276 | })
277 |
278 | resizeStorage.saveTableSizes(table, mode, profile, cache);
279 | }
280 |
281 | function setColumnSizes(cache) {
282 | if (!cache) {
283 | return;
284 | }
285 |
286 | $(table).width('auto');
287 |
288 | ctrlColumns.each(function(index, column){
289 | var colScope = angular.element(column).scope()
290 | var id = colScope.rzCol || $(column).attr('id')
291 | var cacheWidth = cache[id];
292 | $(column).css({ width: cacheWidth });
293 | })
294 |
295 | resizer.onTableReady();
296 | }
297 |
298 | // Return this directive as a object literal
299 | return {
300 | restrict: 'A',
301 | link: link,
302 | controller: RzController,
303 | scope: {
304 | // rzMode will determine the rezising behavior
305 | mode: '=rzMode',
306 | // rzProfile loads a profile from local storage
307 | profile: '=?rzProfile',
308 | // rzBusy will postpone initialisation
309 | busy: '=?rzBusy',
310 | // rzSave saves columns to local storage
311 | saveTableSizes: '=?rzSave',
312 | // rzOptions supplies addition options
313 | options: '=?rzOptions',
314 | // rzModel binds utility function to controller
315 | model: '=rzModel',
316 | // rzContainer is a query selector for the container DOM
317 | container: '@rzContainer'
318 | }
319 | };
320 |
321 | }]);
322 |
--------------------------------------------------------------------------------
/scripts/resizers/basic-resizer.js:
--------------------------------------------------------------------------------
1 | angular.module("rzTable").factory("BasicResizer", ["ResizerModel", function(ResizerModel) {
2 |
3 | function BasicResizer(table, columns, container) {
4 | // Call super constructor
5 | ResizerModel.call(this, table, columns, container)
6 |
7 | // All columns are controlled in basic mode
8 | this.ctrlColumns = this.columns;
9 |
10 | this.intervene = {
11 | selector: interveneSelector,
12 | calculator: interveneCalculator,
13 | restrict: interveneRestrict
14 | }
15 | }
16 |
17 | // Inherit by prototypal inheritance
18 | BasicResizer.prototype = Object.create(ResizerModel.prototype);
19 |
20 | function interveneSelector(column) {
21 | return $(column).next()
22 | }
23 |
24 | function interveneCalculator(orgWidth, diffX) {
25 | return orgWidth - diffX;
26 | }
27 |
28 | function interveneRestrict(newWidth){
29 | return newWidth < 25;
30 | }
31 |
32 | BasicResizer.prototype.setup = function() {
33 | // Hide overflow in mode fixed
34 | $(this.container).css({
35 | overflowX: 'hidden'
36 | })
37 |
38 | $(this.table).css({
39 | width: '100%'
40 | })
41 | };
42 |
43 | BasicResizer.prototype.handles = function() {
44 | // Mode fixed does not require handler on last column
45 | return $(this.columns).not(':last')
46 | };
47 |
48 | BasicResizer.prototype.onFirstDrag = function() {
49 | // Replace all column's width with absolute measurements
50 | this.onEndDrag()
51 | };
52 |
53 | BasicResizer.prototype.onEndDrag = function () {
54 | // Calculates the percent width of each column
55 | var totWidth = $(this.table).outerWidth();
56 |
57 | var callbacks = []
58 |
59 | // Calculate the width of every column
60 | $(this.columns).each(function(index, column) {
61 | var colWidth = $(column).outerWidth();
62 | var percentWidth = colWidth / totWidth * 100 + '%';
63 | callbacks.push(function() {
64 | $(column).css({ width: percentWidth });
65 | })
66 | })
67 |
68 | // Apply the calculated width of every column
69 | callbacks.map(function(cb) { cb() })
70 | };
71 |
72 | BasicResizer.prototype.saveAttr = function (column) {
73 | return $(column)[0].style.width;
74 | };
75 |
76 | // Return constructor
77 | return BasicResizer;
78 |
79 | }]);
80 |
--------------------------------------------------------------------------------
/scripts/resizers/fixed-resizer.js:
--------------------------------------------------------------------------------
1 | angular.module("rzTable").factory("FixedResizer", ["ResizerModel", function(ResizerModel) {
2 |
3 | function FixedResizer(table, columns, container) {
4 | // Call super constructor
5 | ResizerModel.call(this, table, columns, container)
6 |
7 | this.fixedColumn = $(table).find('th').first();
8 | this.bound = false;
9 | }
10 |
11 | // Inherit by prototypal inheritance
12 | FixedResizer.prototype = Object.create(ResizerModel.prototype);
13 |
14 | FixedResizer.prototype.setup = function() {
15 | // Hide overflow in mode fixed
16 | $(this.container).css({
17 | overflowX: 'hidden'
18 | })
19 |
20 | $(this.table).css({
21 | width: '100%'
22 | })
23 |
24 | // First column is auto to compensate for 100% table width
25 | $(this.columns).first().css({
26 | width: 'auto'
27 | });
28 | };
29 |
30 | FixedResizer.prototype.handles = function() {
31 | // Mode fixed does not require handler on last column
32 | return $(this.columns).not(':last')
33 | };
34 |
35 | FixedResizer.prototype.ctrlColumns = function() {
36 | // In mode fixed, all but the first column should be resized
37 | return $(this.columns).not(':first');
38 | };
39 |
40 | FixedResizer.prototype.onFirstDrag = function() {
41 | // Replace each column's width with absolute measurements
42 | $(this.ctrlColumns).each(function(index, column) {
43 | $(column).width($(column).width());
44 | })
45 | };
46 |
47 | FixedResizer.prototype.handleMiddleware = function (handle, column) {
48 | // Fixed mode handles always controll next neightbour column
49 | return $(column).next();
50 | };
51 |
52 | FixedResizer.prototype.restrict = function (newWidth, diffX) {
53 | if (this.bound && this.bound < diffX) {
54 | this.bound = false
55 | return false
56 | } if (this.bound && this.bound > diffX) {
57 | return true
58 | } else if (this.fixedColumn.width() <= this.getMinWidth(this.fixedColumn)) {
59 | this.bound = diffX
60 | $(this.fixedColumn).width(this.minWidth);
61 | return true;
62 | }
63 | };
64 |
65 | FixedResizer.prototype.onEndDrag = function () {
66 | this.bound = false
67 | };
68 |
69 | FixedResizer.prototype.calculate = function (orgWidth, diffX) {
70 | // Subtract difference - neightbour grows
71 | return orgWidth - diffX;
72 | };
73 |
74 | // Return constructor
75 | return FixedResizer;
76 |
77 | }]);
78 |
--------------------------------------------------------------------------------
/scripts/resizers/overflow-resizer.js:
--------------------------------------------------------------------------------
1 | angular.module("rzTable").factory("OverflowResizer", ["ResizerModel", function(ResizerModel) {
2 |
3 | function OverflowResizer(table, columns, container) {
4 | // Call super constructor
5 | ResizerModel.call(this, table, columns, container)
6 | }
7 |
8 | // Inherit by prototypal inheritance
9 | OverflowResizer.prototype = Object.create(ResizerModel.prototype);
10 |
11 |
12 | OverflowResizer.prototype.setup = function() {
13 | // Allow overflow in this mode
14 | $(this.container).css({
15 | overflow: 'auto'
16 | });
17 | };
18 |
19 | OverflowResizer.prototype.onTableReady = function() {
20 | // For mode overflow, make table as small as possible
21 | $(this.table).width(1);
22 | };
23 |
24 | // Return constructor
25 | return OverflowResizer;
26 |
27 | }]);
28 |
--------------------------------------------------------------------------------
/scripts/services/resize-storage-service.js:
--------------------------------------------------------------------------------
1 | angular.module("rzTable").service('resizeStorage', ['$window', function($window) {
2 |
3 | var prefix = "ngColumnResize";
4 |
5 | this.loadTableSizes = function(table, mode, profile) {
6 | var key = getStorageKey(table, mode, profile);
7 | var object = $window.localStorage.getItem(key);
8 | return JSON.parse(object);
9 | }
10 |
11 | this.saveTableSizes = function(table, mode, profile, sizes) {
12 | var key = getStorageKey(table, mode, profile);
13 | if (!key) return;
14 | var string = JSON.stringify(sizes);
15 | $window.localStorage.setItem(key, string)
16 | }
17 |
18 | this.clearAll = function() {
19 | var keys = []
20 | for (var i = 0; i < $window.localStorage.length; ++i) {
21 | var key = localStorage.key(i)
22 | if (key && key.startsWith(prefix)) {
23 | keys.push(key)
24 | }
25 | }
26 | keys.map(function(k) { $window.localStorage.removeItem(k) })
27 | }
28 |
29 | this.clearCurrent = function(table, mode, profile) {
30 | var key = getStorageKey(table, mode, profile);
31 | if (key) {
32 | $window.localStorage.removeItem(key)
33 | }
34 | }
35 |
36 | function getStorageKey(table, mode, profile) {
37 | var id = table.attr('id');
38 | if (!id) {
39 | console.error("Table has no id", table);
40 | return undefined;
41 | }
42 | return prefix + '.' + table.attr('id') + '.' + mode + (profile ? '.' + profile : '');
43 | }
44 |
45 | }]);
46 |
--------------------------------------------------------------------------------
/scripts/services/resizer-factory.js:
--------------------------------------------------------------------------------
1 | angular.module("rzTable").factory("ResizerModel", [function() {
2 |
3 | function ResizerModel(table, columns, container){
4 | this.table = table;
5 | this.columns = columns;
6 | this.container = container;
7 |
8 | this.handleColumns = this.handles();
9 | this.ctrlColumns = this.ctrlColumns();
10 | }
11 |
12 | ResizerModel.prototype.setup = function() {
13 | // Hide overflow by default
14 | $(this.container).css({
15 | overflowX: 'hidden'
16 | })
17 | }
18 |
19 | ResizerModel.prototype.onTableReady = function () {
20 | // Table is by default 100% width
21 | $(this.table).outerWidth('100%');
22 | };
23 |
24 | ResizerModel.prototype.getMinWidth = function(column) {
25 | // "25px" -> 25
26 | return parseInt($(column).css('min-width')) || 0;
27 | }
28 |
29 | ResizerModel.prototype.handles = function () {
30 | // By default all columns should be assigned a handle
31 | return this.columns;
32 | };
33 |
34 | ResizerModel.prototype.ctrlColumns = function () {
35 | // By default all columns assigned a handle are resized
36 | return this.handleColumns;
37 | };
38 |
39 | ResizerModel.prototype.onFirstDrag = function () {
40 | // By default, set all columns to absolute widths
41 | $(this.ctrlColumns).each(function(index, column) {
42 | $(column).width($(column).width());
43 | })
44 | };
45 |
46 | ResizerModel.prototype.handleMiddleware = function (handle, column) {
47 | // By default, every handle controls the column it is placed in
48 | return column;
49 | };
50 |
51 | ResizerModel.prototype.restrict = function (newWidth) {
52 | return false;
53 | };
54 |
55 | ResizerModel.prototype.calculate = function (orgWidth, diffX) {
56 | // By default, simply add the width difference to the original
57 | return orgWidth + diffX;
58 | };
59 |
60 | ResizerModel.prototype.onEndDrag = function () {
61 | // By default, do nothing when dragging a column ends
62 | return;
63 | };
64 |
65 | ResizerModel.prototype.saveAttr = function (column) {
66 | return $(column).outerWidth();
67 | };
68 |
69 | return ResizerModel;
70 | }]);
71 |
--------------------------------------------------------------------------------
/test/test.js:
--------------------------------------------------------------------------------
1 | describe('ngTableResize', function() {
2 |
3 | beforeEach(module('ngTableResize'));
4 |
5 | describe('basic resizer', function() {
6 |
7 | var element;
8 | var outerScope;
9 | var innerScope;
10 |
11 | beforeEach(inject(function($rootScope, $compile) {
12 | outerScope = $rootScope;
13 |
14 | element = angular.element(
15 | '
' +
16 | '' +
17 | ' | ' +
18 | ' | ' +
19 | ' | ' +
20 | '' +
21 | '
');
22 |
23 | $compile(element)(outerScope);
24 |
25 | innerScope = element.isolateScope();
26 |
27 | $rootScope.$digest();
28 | }));
29 |
30 | it('should have handles in all but the last column', function() {
31 | var columns = element.find('th');
32 | var last = columns[columns.length - 1];
33 |
34 | columns.each(function(index, column) {
35 | var handle = $(column).find('div');
36 | if (column !== last) {
37 | expect(handle.length).to.equal(1)
38 | expect(handle.attr('class')).to.equal('handle');
39 | } else {
40 | expect(handle.length).to.equal(0)
41 | }
42 | })
43 |
44 | })
45 | })
46 |
47 | describe('fixed resizer', function() {
48 |
49 | var element;
50 | var outerScope;
51 | var innerScope;
52 |
53 | beforeEach(inject(function($rootScope, $compile) {
54 | outerScope = $rootScope;
55 |
56 | element = angular.element(
57 | '
' +
58 | '' +
59 | ' | ' +
60 | ' | ' +
61 | ' | ' +
62 | '' +
63 | '
');
64 |
65 | $compile(element)(outerScope);
66 |
67 | innerScope = element.isolateScope();
68 |
69 | $rootScope.$digest();
70 | }));
71 |
72 | it('should have handles in all but the first column', function() {
73 | var columns = element.find('th');
74 | var first = columns[0];
75 | console.log(columns);
76 |
77 | for (var i = 0; i < columns.length; i++){
78 | console.log(columns[i]);
79 | }
80 |
81 | columns.each(function(index, column) {
82 | var handle = $(column).find('div');
83 | if (column !== first) {
84 | expect(handle.length).to.equal(1)
85 | expect(handle.attr('class')).to.equal('handle');
86 | } else {
87 | expect(handle.length).to.equal(0)
88 | }
89 | })
90 | });
91 |
92 | })
93 |
94 | })
--------------------------------------------------------------------------------