(function (angular) { 'use strict'; angular.module('scrollable-table', []) .directive('scrollableTable', ['$timeout', '$q', '$parse', '$document', function ($timeout, $q, $parse, $document) { return { transclude: true, restrict: 'A', scope: { rows: '=watch', sortFn: '=' }, template: '
' + '
' + '
' + '
', controller: ['$scope', '$element', '$attrs', function ($scope, $element, $attrs) { //传入参数调整表格高度 var topHeight = $attrs.topheight ? $attrs.topheight : 50;//页面顶部高度 var queryHeight = $attrs.queryheight ? $attrs.queryheight : 70;//查询框高度 var spaceBottonHeight = $attrs.spacebottonheight ? $attrs.spacebottonheight : 20;//表格底边距高度 var tableTitleHeight = $attrs.tabletitleheight ? $attrs.tabletitleheight : 40;//表格标题栏高度 var otherHeight = $attrs.otherheight ? $attrs.otherheight : 0;//以上以外的所有需要去掉的高度 var innerHeight = $attrs.bodyheight ? (parseInt($attrs.bodyheight) + parseInt(topHeight) + parseInt(queryHeight)) : $document.innerHeight(); var containerHeight = ((innerHeight - topHeight - queryHeight - otherHeight) - (spaceBottonHeight + tableTitleHeight) * $attrs.times) / $attrs.times; $element[0].firstChild.style.height = containerHeight + "px"; // define an API for child directives to view and modify sorting parameters this.getSortExpr = function () { return $scope.sortExpr; }; this.isAsc = function () { return $scope.asc; }; this.setSortExpr = function (exp) { $scope.asc = true; $scope.sortExpr = exp; }; this.toggleSort = function () { $scope.asc = !$scope.asc; }; this.doSort = function (comparatorFn) { if (comparatorFn) { $scope.rows.sort(function (r1, r2) { var compared = comparatorFn(r1, r2); return $scope.asc ? compared : compared * -1; }); } else { $scope.rows.sort(function (r1, r2) { var compared = defaultCompare(r1, r2); return $scope.asc ? compared : compared * -1; }); } }; this.renderTalble = function () { return waitForRender().then(fixHeaderWidths); }; this.getTableElement = function () { return $element; }; /** * append handle function to execute after table header resize. */ this.appendTableResizingHandler = function (handler) { var handlerSequence = $scope.headerResizeHanlers || []; for (var i = 0; i < handlerSequence.length; i++) { if (handlerSequence[i].name === handler.name) { return; } } handlerSequence.push(handler); $scope.headerResizeHanlers = handlerSequence; }; function defaultCompare(row1, row2) { var exprParts = $scope.sortExpr.match(/(.+)\s+as\s+(.+)/); var scope = {}; scope[exprParts[1]] = row1; var x = $parse(exprParts[2])(scope); scope[exprParts[1]] = row2; var y = $parse(exprParts[2])(scope); if (x === y) return 0; return x > y ? 1 : -1; } function scrollToRow(row) { var offset = $element.find(".headerSpacer").height(); var currentScrollTop = $element.find(".scrollArea").scrollTop(); $element.find(".scrollArea").scrollTop(currentScrollTop + row.position().top - offset); } $scope.$on('rowSelected', function (event, rowId) { var row = $element.find(".scrollArea table tr[row-id='" + rowId + "']"); if (row.length === 1) { // Ensure that the headers have been fixed before scrolling, to ensure accurate // position calculations $q.all([waitForRender(), headersAreFixed.promise]).then(function () { scrollToRow(row); }); } }); // Set fixed widths for the table headers in case the text overflows. // There's no callback for when rendering is complete, so check the visibility of the table // periodically -- see http://stackoverflow.com/questions/11125078 function waitForRender() { var deferredRender = $q.defer(); function wait() { if ($element.find("table:visible").length === 0) { $timeout(wait, 100); } else { deferredRender.resolve(); } } $timeout(wait); return deferredRender.promise; } var headersAreFixed = $q.defer(); function fixHeaderWidths() { if (!$element.find("thead th .th-inner").length) { $element.find("thead th").wrapInner('
'); } if ($element.find("thead th .th-inner:not(:has(.box))").length) { $element.find("thead th .th-inner:not(:has(.box))").wrapInner('
'); } $element.find("table th .th-inner:visible").each(function (index, el) { el = angular.element(el); var width = el.parent().width(), lastCol = $element.find("table th:visible:last"), headerWidth = width; if (lastCol.css("text-align") !== "center") { var hasScrollbar = $element.find(".scrollArea").height() < $element.find("table").height(); if (lastCol[0] == el.parent()[0] && hasScrollbar) { headerWidth += $element.find(".scrollArea").width() - $element.find("tbody tr").width(); headerWidth = Math.max(headerWidth, width); } } var minWidth = _getScale(el.parent().css('min-width')), title = el.parent().attr("title"); headerWidth = Math.max(minWidth, headerWidth); el.css("width", headerWidth); if (!title) { // ordinary column(not sortableHeader) has box child div element that contained title string. title = el.find(".title .ng-scope").html() || el.find(".box").html(); } el.attr("title", title.trim()); }); headersAreFixed.resolve(); } // when the data model changes, fix the header widths. See the comments here: // http://docs.angularjs.org/api/ng.$timeout $scope.$watch('rows', function (newValue, oldValue) { if (newValue) { renderChains($element.find('.scrollArea').width()); // clean sort status and scroll to top of table once records replaced. $scope.sortExpr = null; // FIXME what is the reason here must scroll to top? This may cause confusing if using scrolling to implement pagination. $element.find('.scrollArea').scrollTop(0); } }); $scope.asc = !$attrs.hasOwnProperty("desc"); $scope.sortAttr = $attrs.sortAttr; $element.find(".scrollArea").scroll(function (event) { $element.find("thead th .th-inner").css('margin-left', 0 - event.target.scrollLeft); }); $scope.$on("renderScrollableTable", function () { renderChains($element.find('.scrollArea').width()); }); angular.element(window).on('resize', function () { $timeout(function () { $scope.$apply(); }); }); $scope.$watch(function () { return $element.find('.scrollArea').width(); }, function (newWidth, oldWidth) { if (newWidth * oldWidth <= 0) { return; } renderChains(); }); function renderChains() { var resizeQueue = waitForRender().then(fixHeaderWidths), customHandlers = $scope.headerResizeHanlers || []; for (var i = 0; i < customHandlers.length; i++) { resizeQueue = resizeQueue.then(customHandlers[i]); } return resizeQueue; } }] }; }]) .directive('sortableHeader', [function () { return { transclude: true, scope: true, require: '^scrollableTable', template: '
' + '
' + '
' + '' + '' + '' + '' + '' + '' + '
' + '
', link: function (scope, elm, attrs, tableController) { var expr = attrs.on || "a as a." + attrs.col; scope.element = angular.element(elm); scope.isActive = function () { return tableController.getSortExpr() === expr; }; scope.toggleSort = function (e) { if (scope.isActive()) { tableController.toggleSort(); } else { tableController.setSortExpr(expr); } tableController.doSort(scope[attrs.comparatorFn]); e.preventDefault(); }; scope.isAscending = function () { if (scope.focused && !scope.isActive()) { return true; } else { return tableController.isAsc(); } }; scope.enter = function () { scope.focused = true; }; scope.leave = function () { scope.focused = false; }; scope.isLastCol = function () { return elm.parent().find("th:last-child").get(0) === elm.get(0); }; } }; }]) .directive('resizable', ['$compile', function ($compile) { return { restrict: 'A', priority: 0, scope: false, require: 'scrollableTable', link: function postLink(scope, elm, attrs, tableController) { tableController.appendTableResizingHandler(function () { _init(); }); tableController.appendTableResizingHandler(function relayoutHeaders() { var tableElement = tableController.getTableElement().find('.scrollArea table'); if (tableElement.css('table-layout') === 'auto') { initRodPos(); } else { _resetColumnsSize(tableElement.parent().width()); } }); scope.resizing = function (e) { var screenOffset = tableController.getTableElement().find('.scrollArea').scrollLeft(), thInnerElm = angular.element(e.target).parent(), thElm = thInnerElm.parent(), startPoint = _getScale(thInnerElm.css('left')) + thInnerElm.width() - screenOffset, movingPos = e.pageX, _document = angular.element(document), _body = angular.element('body'), coverPanel = angular.element('.scrollableContainer .resizing-cover'), scaler = angular.element('
'); _body.addClass('scrollable-resizing'); coverPanel.addClass('active'); angular.element('.scrollableContainer').append(scaler); scaler.css('left', startPoint); _document.bind('mousemove', function (e) { var offsetX = e.pageX - movingPos, movedOffset = _getScale(scaler.css('left')) - startPoint, widthOfActiveCol = thElm.width(), nextElm = thElm.nextAll('th:visible').first(), minWidthOfActiveCol = _getScale(thElm.css('min-width')), widthOfNextColOfActive = nextElm.width(), minWidthOfNextColOfActive = _getScale(nextElm.css('min-width')); movingPos = e.pageX; e.preventDefault(); if ((offsetX > 0 && widthOfNextColOfActive - movedOffset <= minWidthOfNextColOfActive) || (offsetX < 0 && widthOfActiveCol + movedOffset <= minWidthOfActiveCol)) { //stopping resize if user trying to extension and the active/next column already minimised. return; } scaler.css('left', _getScale(scaler.css('left')) + offsetX); }); _document.bind('mouseup', function (e) { e.preventDefault(); scaler.remove(); _body.removeClass('scrollable-resizing'); coverPanel.removeClass('active'); _document.unbind('mousemove'); _document.unbind('mouseup'); var offsetX = _getScale(scaler.css('left')) - startPoint, newWidth = thElm.width(), minWidth = _getScale(thElm.css('min-width')), nextElm = thElm.nextAll('th:visible').first(), widthOfNextColOfActive = nextElm.width(), minWidthOfNextColOfActive = _getScale(nextElm.css('min-width')), tableElement = tableController.getTableElement().find('.scrollArea table'); //hold original width of cells, to display cells as their original width after turn table-layout to fixed. if (tableElement.css('table-layout') === 'auto') { tableElement.find("th .th-inner").each(function (index, el) { el = angular.element(el); var width = el.parent().width(); el.parent().css('width', width); }); } tableElement.css('table-layout', 'fixed'); if (offsetX > 0 && widthOfNextColOfActive - offsetX <= minWidthOfNextColOfActive) { offsetX = widthOfNextColOfActive - minWidthOfNextColOfActive; } nextElm.removeAttr('style'); newWidth += offsetX; thElm.css('width', Math.max(minWidth, newWidth)); nextElm.css('width', widthOfNextColOfActive - offsetX); tableController.renderTalble().then(resizeHeaderWidth()); }); }; function _init() { var thInnerElms = elm.find('table th:not(:last-child) .th-inner'); if (thInnerElms.find('.resize-rod').length == 0) { tableController.getTableElement().find('.scrollArea table').css('table-layout', 'auto'); var resizeRod = angular.element('
'); thInnerElms.append($compile(resizeRod)(scope)); } } function initRodPos() { var tableElement = tableController.getTableElement(); var headerPos = 1;// 1 is the width of right border; tableElement.find("table th .th-inner:visible").each(function (index, el) { el = angular.element(el); var width = el.parent().width(), //to made header consistent with its parent. // if it's the last header, add space for the scrollbar equivalent unless it's centered minWidth = _getScale(el.parent().css('min-width')); width = Math.max(minWidth, width); el.css("left", headerPos); headerPos += width; }); } function resizeHeaderWidth() { var headerPos = 1,// 1 is the width of right border; tableElement = tableController.getTableElement(); tableController.getTableElement().find("table th .th-inner:visible").each(function (index, el) { el = angular.element(el); var width = el.parent().width(), //to made header consistent with its parent. // if it's the last header, add space for the scrollbar equivalent unless it's centered lastCol = tableElement.find("table th:visible:last"), minWidth = _getScale(el.parent().css('min-width')); width = Math.max(minWidth, width); //following are resize stuff, to made th-inner position correct. //last column's width should be automatically, to avoid horizontal scroll. if (lastCol[0] != el.parent()[0]) { el.parent().css('width', width); } el.css("left", headerPos); headerPos += width; }); } function _resetColumnsSize(tableWidth) { var tableElement = tableController.getTableElement(), columnLength = tableElement.find("table th:visible").length, lastCol = tableElement.find("table th:visible:last"); tableElement.find("table th:visible").each(function (index, el) { el = angular.element(el); if (lastCol.get(0) == el.get(0)) { //last column's width should be automaically, to avoid horizontal scroll. el.css('width', 'auto'); return; } var _width = el.data('width'); if (/\d+%$/.test(_width)) { //percentage _width = Math.ceil(tableWidth * _getScale(_width) / 100); } else { // if data-width not exist, use average width for each columns. _width = tableWidth / columnLength; } el.css('width', _width + 'px'); }); tableController.renderTalble().then(resizeHeaderWidth()); } } } }]) ; function _getScale(sizeCss) { return parseInt(sizeCss.replace(/px|%/, ''), 10); } })(angular);