From c1462e2f8807437707aecf33c7352b2d5709f4f7 Mon Sep 17 00:00:00 2001 From: Chris Liechty Date: Mon, 3 Nov 2014 10:26:06 -0700 Subject: [PATCH 01/99] fixed npm dependencies -- karma versions were not compatible with current tests. --- .bowerrc | 5 +++-- package.json | 21 ++++++++------------- 2 files changed, 11 insertions(+), 15 deletions(-) diff --git a/.bowerrc b/.bowerrc index 4dece5f..1a1728c 100644 --- a/.bowerrc +++ b/.bowerrc @@ -1,3 +1,4 @@ { - "directory": "vendor" -} \ No newline at end of file + "directory": "vendor", + "strict-ssl": false +} diff --git a/package.json b/package.json index dc38ec5..8f281ff 100644 --- a/package.json +++ b/package.json @@ -6,22 +6,17 @@ "author": "Michael Bromley", "license": "MIT", "devDependencies": { + "bower": "^1.3.1", "grunt": "~0.4.2", - "karma-script-launcher": "~0.1.0", - "karma-chrome-launcher": "~0.1.2", - "karma-firefox-launcher": "~0.1.3", - "karma-html2js-preprocessor": "~0.1.0", - "karma-jasmine": "^0.2.1", - "karma-coffee-preprocessor": "~0.1.2", - "requirejs": "~2.1.10", - "karma-requirejs": "~0.2.1", - "karma-phantomjs-launcher": "~0.1.1", - "karma": "~0.10.9", - "grunt-karma": "~0.6.2", + "grunt-contrib-copy": "^0.5.0", "grunt-contrib-jshint": "~0.8.0", - "bower": "^1.3.1", "grunt-contrib-watch": "^0.6.1", "grunt-html2js": "^0.2.4", - "grunt-contrib-copy": "^0.5.0" + "grunt-karma": "~0.9.0", + "karma-chrome-launcher": "^0.1.5", + "karma-firefox-launcher": "^0.1.3", + "karma-jasmine": "^0.2.2", + "karma-phantomjs-launcher": "^0.1.4", + "requirejs": "~2.1.10" } } From 2f708487ab6f0e8f8c71c539473d3625eee7526a Mon Sep 17 00:00:00 2001 From: Chris Liechty Date: Mon, 3 Nov 2014 10:42:24 -0700 Subject: [PATCH 02/99] Fixed issue where removing item from collection causes paginationControls to generate incorrect pages array. --- src/directives/pagination/dirPagination.js | 6 ++++-- src/directives/pagination/dirPagination.spec.js | 16 +++++++++++++++- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/src/directives/pagination/dirPagination.js b/src/directives/pagination/dirPagination.js index 2c53b2e..ba59bb3 100644 --- a/src/directives/pagination/dirPagination.js +++ b/src/directives/pagination/dirPagination.js @@ -238,8 +238,10 @@ } function generatePagination() { - scope.pages = generatePagesArray(1, paginationService.getCollectionLength(paginationId), paginationService.getItemsPerPage(paginationId), paginationRange); - scope.pagination.current = parseInt(paginationService.getCurrentPage(paginationId)); + var page = parseInt(paginationService.getCurrentPage(paginationId)) || 1; + + scope.pages = generatePagesArray(page, paginationService.getCollectionLength(paginationId), paginationService.getItemsPerPage(paginationId), paginationRange); + scope.pagination.current = page; scope.pagination.last = scope.pages[scope.pages.length - 1]; if (scope.pagination.last < scope.pagination.current) { scope.setCurrent(scope.pagination.last); diff --git a/src/directives/pagination/dirPagination.spec.js b/src/directives/pagination/dirPagination.spec.js index 767d081..5ba1dbd 100644 --- a/src/directives/pagination/dirPagination.spec.js +++ b/src/directives/pagination/dirPagination.spec.js @@ -1,4 +1,4 @@ -/** + /** * Created by Michael on 04/05/14. */ @@ -274,6 +274,20 @@ describe('dirPagination directive', function() { expect(pageLinks).toEqual(['‹','1', '...', '94', '95', '96', '97', '98', '99', '100', '›']); }); + it('should show the correct pagination links after item removed from cllection', function() { + compileElement(myCollection, 1); + $scope.$apply(function() { + $scope.currentPage = 98; + }); + + $scope.$apply(function() { + $scope.collection.pop(); + }); + var pageLinks = getPageLinksArray(); + + expect(pageLinks).toEqual(['‹','1', '...', '93', '94', '95', '96', '97', '98', '99', '›']); + }); + it('should calculate pages based off collection after all filters are applied', function() { $scope.filterBy = '2'; var customExpression = "item in collection | filter: filterBy | itemsPerPage: itemsPerPage"; From 20deac7a8b9a1d0f5718de9650ffb8c3b8d24a0a Mon Sep 17 00:00:00 2001 From: Chris Liechty Date: Mon, 3 Nov 2014 10:48:34 -0700 Subject: [PATCH 03/99] revert .bowerrc to original for other users --- .bowerrc | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.bowerrc b/.bowerrc index 1a1728c..4dece5f 100644 --- a/.bowerrc +++ b/.bowerrc @@ -1,4 +1,3 @@ { - "directory": "vendor", - "strict-ssl": false -} + "directory": "vendor" +} \ No newline at end of file From 25203847ddfee16366bbad5bb5d6de88aba23811 Mon Sep 17 00:00:00 2001 From: Michael Bromley Date: Thu, 6 Nov 2014 17:07:46 +0100 Subject: [PATCH 04/99] dirPagination: fix issue 78 https://github.com/michaelbromley/angularUtils/issues/78 Register the dirPaginate instance id in the compile function rather than the linking function, allowing the pagination controls to be linked before the pagination list if needed. --- src/directives/pagination/dirPagination.js | 10 +++---- .../pagination/dirPagination.spec.js | 27 +++++++++++++++++++ .../terminalType/dirTerminalType.spec.js | 2 +- 3 files changed, 33 insertions(+), 6 deletions(-) diff --git a/src/directives/pagination/dirPagination.js b/src/directives/pagination/dirPagination.js index ba59bb3..70372fc 100644 --- a/src/directives/pagination/dirPagination.js +++ b/src/directives/pagination/dirPagination.js @@ -60,13 +60,12 @@ var itemsPerPageFilterRemoved = match[2].replace(filterPattern, ''); var collectionGetter = $parse(itemsPerPageFilterRemoved); + var paginationId = tAttrs.paginationId || '__default'; + paginationService.registerInstance(paginationId); + return function dirPaginationLinkFn(scope, element, attrs){ - var paginationId; var compiled = $compile(element, false, 5000); // we manually compile the element again, as we have now added ng-repeat. Priority less than 5000 prevents infinite recursion of compiling dirPaginate - paginationId = attrs.paginationId || '__default'; - paginationService.registerInstance(paginationId); - var currentPageGetter; if (attrs.currentPage) { currentPageGetter = $parse(attrs.currentPage); @@ -311,7 +310,8 @@ instances[instanceId].currentPageParser.assign(instances[instanceId].context, val); }; this.getCurrentPage = function(instanceId) { - return instances[instanceId].currentPageParser(instances[instanceId].context); + var parser = instances[instanceId].currentPageParser; + return parser ? parser(instances[instanceId].context) : 1; }; this.setItemsPerPage = function(instanceId, val) { diff --git a/src/directives/pagination/dirPagination.spec.js b/src/directives/pagination/dirPagination.spec.js index 5ba1dbd..49b62fe 100644 --- a/src/directives/pagination/dirPagination.spec.js +++ b/src/directives/pagination/dirPagination.spec.js @@ -311,6 +311,33 @@ describe('dirPagination directive', function() { expect(activeLink.html()).toContain(1); }); + /** + * Issue raised here https://github.com/michaelbromley/angularUtils/issues/78 + * Where the dir-paginate directive is inside an ngSwitch block (which is initially hidden), so the linking function is not immediately executed. + * The dir-pagination-controls directive is *outside* the switch block, so it gets both compiled *and* linked on page load. + */ + it('should allow paginate directive to be defined in a deferred-linking situation without error', function() { + function compile() { + var html; + $scope.collection = myCollection; + $scope.showList = false; + html = '
' + + '
  • {{ item }}
' + + '
' + + ''; + containingElement.append($compile(html)($scope)); + $scope.$apply(); + } + + expect(compile).not.toThrow(); + expect(getListItems().length).toEqual(0); + + $scope.$apply(function() { + $scope.showList = true; + }); + expect(getListItems().length).toEqual(10); + }); + describe('optional attributes', function() { function compileWithAttributes(attributes) { diff --git a/src/directives/terminalType/dirTerminalType.spec.js b/src/directives/terminalType/dirTerminalType.spec.js index 988a2f1..1df00c6 100644 --- a/src/directives/terminalType/dirTerminalType.spec.js +++ b/src/directives/terminalType/dirTerminalType.spec.js @@ -1,5 +1,5 @@ -describe('dirTerminalType directive', function() { +xdescribe('dirTerminalType directive', function() { var $compile; var $scope; From 886b323971d2d0e98a67a68143cc133f35eda76b Mon Sep 17 00:00:00 2001 From: Bruno Porto Date: Fri, 14 Nov 2014 13:00:59 -0200 Subject: [PATCH 05/99] Update dirPagination.js add watch on getItemsPerPage to make possible change itemsPerPage from scope. ``` row in rows | itemsPerPage: rowsPerPage ``` --- src/directives/pagination/dirPagination.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/directives/pagination/dirPagination.js b/src/directives/pagination/dirPagination.js index 70372fc..34b29dc 100644 --- a/src/directives/pagination/dirPagination.js +++ b/src/directives/pagination/dirPagination.js @@ -209,6 +209,14 @@ generatePagination(); } }); + + scope.$watch(function() { + return (paginationService.getItemsPerPage(paginationId)); + }, function(prev,cur) { + if (prev != cur) { + goToPage(scope.pagination.current); + } + }); scope.$watch(function() { return paginationService.getCurrentPage(paginationId); From c42d7d8e8b1712e66a2af67ba28257e9fa3d49a2 Mon Sep 17 00:00:00 2001 From: Jonny Taylor Date: Wed, 19 Nov 2014 14:30:46 +0000 Subject: [PATCH 06/99] Make templatePath globally configurable --- src/directives/pagination/dirPagination.js | 23 ++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/src/directives/pagination/dirPagination.js b/src/directives/pagination/dirPagination.js index 34b29dc..9a77c9f 100644 --- a/src/directives/pagination/dirPagination.js +++ b/src/directives/pagination/dirPagination.js @@ -20,7 +20,6 @@ * Config */ var moduleName = 'angularUtils.directives.dirPagination'; - var templatePath = 'directives/pagination/dirPagination.tpl.html'; /** * Module @@ -103,7 +102,7 @@ }; }]); - module.directive('dirPaginationControls', ['paginationService', function(paginationService) { + module.directive('dirPaginationControls', ['paginationService', 'paginationTemplate', function(paginationService, paginationTemplate) { var numberRegex = /^\d+$/; /** * Generate an array of page numbers (or the '...' string) which is used in an ng-repeat to generate the @@ -177,7 +176,7 @@ return { restrict: 'AE', templateUrl: function(elem, attrs) { - return attrs.templateUrl || templatePath; + return attrs.templateUrl || paginationTemplate.getPath(); }, scope: { maxSize: '=?', @@ -344,4 +343,20 @@ return instances[instanceId].asyncMode; }; }); -})(); + + module.provider('paginationTemplate', function() { + var templatePath = 'directives/pagination/dirPagination.tpl.html'; + + this.setPath = function(path) { + templatePath = path; + }; + + this.$get = function() { + return { + getPath: function() { + return templatePath; + } + }; + }; + }); +})(); \ No newline at end of file From 8acd69a806615a9c29d9dd614a7cbe11e955e5d9 Mon Sep 17 00:00:00 2001 From: Michael Bromley Date: Thu, 20 Nov 2014 08:47:16 +0100 Subject: [PATCH 07/99] Update readme Add info about the new paginationTemplateProvider method of setting the template globally. --- src/directives/pagination/README.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/directives/pagination/README.md b/src/directives/pagination/README.md index 034bd35..cd00d93 100644 --- a/src/directives/pagination/README.md +++ b/src/directives/pagination/README.md @@ -71,6 +71,26 @@ And finally include the pagination itself. ``` +### Specifying The Template + +There are two ways to specify the template of the pagination controls directive: + +**1. Use the `paginationTemplateProvider` in your app's config block to set a global template for your app:** + +```JavaScript +myApp.config(function(paginationTemplateProvider) { + paginationTemplateProvider.setPath('path/to/dirPagination.tpl.html'); +}); +``` + +**2. Use the `template-url` attribute on each pagination controls directive:** + +```HTML + +``` + +## Directives API + ### `dir-paginate` * **`expression`** Under the hood, this directive delegates to the `ng-repeat` directive, so the syntax for the @@ -269,3 +289,5 @@ from their pagination directive. * StackOverflow: http://stackoverflow.com/questions/10816073/how-to-do-paging-in-angularjs. Picked up a lot of ideas from the various contributors to this thread. + +* Massive credit is due to all the [contributors](https://github.com/michaelbromley/angularUtils/graphs/contributors) to this project - they have brought improvements that I would not have the time or insight to figure out myself. From 09fcf0427fd2ed96300de8f49470e11b45e928b9 Mon Sep 17 00:00:00 2001 From: Michael Bromley Date: Thu, 20 Nov 2014 08:55:08 +0100 Subject: [PATCH 08/99] dirPagination: Minor style fixes --- src/directives/pagination/dirPagination.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/directives/pagination/dirPagination.js b/src/directives/pagination/dirPagination.js index 9a77c9f..52c995a 100644 --- a/src/directives/pagination/dirPagination.js +++ b/src/directives/pagination/dirPagination.js @@ -33,6 +33,7 @@ } module.directive('dirPaginate', ['$compile', '$parse', '$timeout', 'paginationService', function($compile, $parse, $timeout, paginationService) { + return { terminal: true, multiElement: true, @@ -103,7 +104,9 @@ }]); module.directive('dirPaginationControls', ['paginationService', 'paginationTemplate', function(paginationService, paginationTemplate) { + var numberRegex = /^\d+$/; + /** * Generate an array of page numbers (or the '...' string) which is used in an ng-repeat to generate the * links used in pagination @@ -183,6 +186,7 @@ onPageChange: '&?' }, link: function(scope, element, attrs) { + var paginationId; paginationId = attrs.paginationId || '__default'; if (!scope.maxSize) { scope.maxSize = 9; } @@ -211,8 +215,8 @@ scope.$watch(function() { return (paginationService.getItemsPerPage(paginationId)); - }, function(prev,cur) { - if (prev != cur) { + }, function(current, previous) { + if (current != previous) { goToPage(scope.pagination.current); } }); @@ -262,6 +266,7 @@ }]); module.filter('itemsPerPage', ['paginationService', function(paginationService) { + return function(collection, itemsPerPage, paginationId) { if (typeof (paginationId) === 'undefined') { paginationId = '__default'; @@ -289,6 +294,7 @@ }]); module.service('paginationService', function() { + var instances = {}; var lastRegisteredInstance; @@ -345,6 +351,7 @@ }); module.provider('paginationTemplate', function() { + var templatePath = 'directives/pagination/dirPagination.tpl.html'; this.setPath = function(path) { From 6979be4dc7a08aed231f71bcc021e9bb1389bdf8 Mon Sep 17 00:00:00 2001 From: Michael Bromley Date: Tue, 16 Dec 2014 16:59:32 +0100 Subject: [PATCH 09/99] uiBreadcrumbs: Annotate directive with array syntax Make it safe for minification without requiring ng-annotate. See https://github.com/michaelbromley/angularUtils/issues/86 --- src/directives/uiBreadcrumbs/uiBreadcrumbs.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/directives/uiBreadcrumbs/uiBreadcrumbs.js b/src/directives/uiBreadcrumbs/uiBreadcrumbs.js index 05a45ca..de7ab7e 100644 --- a/src/directives/uiBreadcrumbs/uiBreadcrumbs.js +++ b/src/directives/uiBreadcrumbs/uiBreadcrumbs.js @@ -26,7 +26,7 @@ module = angular.module(moduleName, ['ui.router']); } - module.directive('uiBreadcrumbs', function($interpolate, $state) { + module.directive('uiBreadcrumbs', ['$interpolate', '$state', function($interpolate, $state) { return { restrict: 'E', templateUrl: function(elem, attrs) { @@ -171,5 +171,5 @@ } } }; - }); + }]); })(); \ No newline at end of file From b178ca95e5027482038bbfa3d814c246be93a100 Mon Sep 17 00:00:00 2001 From: Michael Bromley Date: Thu, 18 Dec 2014 11:41:40 +0100 Subject: [PATCH 10/99] dirTagbox: Make matching case-insensitive Fixes https://github.com/michaelbromley/angularUtils/issues/66 Also refactored into IIFE and named some anonymous functions. --- src/directives/tagbox/dirTagbox.js | 366 ++++++++++++------------ src/directives/tagbox/dirTagbox.spec.js | 11 +- 2 files changed, 197 insertions(+), 180 deletions(-) diff --git a/src/directives/tagbox/dirTagbox.js b/src/directives/tagbox/dirTagbox.js index 77c3e35..3f668e9 100644 --- a/src/directives/tagbox/dirTagbox.js +++ b/src/directives/tagbox/dirTagbox.js @@ -1,4 +1,6 @@ /** + * Version 0.1.2 + * * A directive to enable tagging auto-complete on an input or textarea. * * For documentation, see the README.md file in this directory @@ -7,207 +9,213 @@ * Copyright Michael Bromley 2014 * Available under the MIT license. */ -angular.module('angularUtils.directives.dirTagBox', []) - - .directive('dirTagbox', function($compile, $parse) { - return { - restrict: 'A', - scope: { - tags: '=dirTagbox', - callback: '&dirOnTagSelect' - }, - link: function(scope, element, attrs) { - var TOKEN = attrs.dirTagtoken !== undefined ? attrs.dirTagtoken : ''; - - var input = element; - var isValidInputType = (input[0].nodeName === 'INPUT' && (input[0].type === 'text' || input[0].type === 'search' || input[0].type === 'email')); - if (input[0].nodeName !== 'TEXTAREA' && !isValidInputType) { - return; - } - // create wrapper div - var wrapper = angular.element('
'); - input.wrap(wrapper); - - // create the suggestions div - var suggestions = makeSuggestionsBox(); - input.parent().append(suggestions); - - scope.candidateHashtag = "?"; - scope.candidate = { - start: 0, - end: 0 - }; - scope.selectedIndex = null; - scope.filteredTags = []; - scope.isFocussed = ('autofocus' in input[0]); - var mouseIsOverSuggestions = false; - - suggestions.on('click', function(e) { - var selectedTag = e.target.innerHTML.substring(TOKEN.length); - insertSelectedTag(selectedTag); - input[0].focus(); - scope.$apply(function() { - scope.candidateHashtag = "?"; +(function() { + + angular.module('angularUtils.directives.dirTagBox', []) + + .directive('dirTagbox', function dirTagbox($compile, $parse) { + return { + restrict: 'A', + scope: { + tags: '=dirTagbox', + callback: '&dirOnTagSelect' + }, + link: function dirTagboxLinkingFn(scope, element, attrs) { + + var TOKEN = attrs.dirTagtoken !== undefined ? attrs.dirTagtoken : ''; + + var input = element; + var isValidInputType = (input[0].nodeName === 'INPUT' && (input[0].type === 'text' || input[0].type === 'search' || input[0].type === 'email')); + if (input[0].nodeName !== 'TEXTAREA' && !isValidInputType) { + return; + } + + // create wrapper div + var wrapper = angular.element('
'); + input.wrap(wrapper); + + // create the suggestions div + var suggestions = makeSuggestionsBox(); + input.parent().append(suggestions); + + scope.candidateHashtag = "?"; + scope.candidate = { + start: 0, + end: 0 + }; + scope.selectedIndex = null; + scope.filteredTags = []; + scope.isFocussed = ('autofocus' in input[0]); + var mouseIsOverSuggestions = false; + + suggestions.on('click', function(e) { + var selectedTag = e.target.innerHTML.substring(TOKEN.length); + insertSelectedTag(selectedTag); + input[0].focus(); + scope.$apply(function() { + scope.candidateHashtag = "?"; + }); }); - }); - suggestions.on('mouseover', function() { - mouseIsOverSuggestions = true; - scope.$apply(function() { - scope.selectedIndex = null; + suggestions.on('mouseover', function() { + mouseIsOverSuggestions = true; + scope.$apply(function() { + scope.selectedIndex = null; + }); }); - }); - suggestions.on('mouseout', function() { - mouseIsOverSuggestions = false; - }); - - input.on('focus', function() { - scope.$apply(function() { - scope.isFocussed = true; + suggestions.on('mouseout', function() { + mouseIsOverSuggestions = false; }); - }); - input.on('blur', function() { - if (!mouseIsOverSuggestions) { + input.on('focus', function() { scope.$apply(function() { - scope.isFocussed = false; + scope.isFocussed = true; }); - } - }); - - input.on('keyup', function() { - // is the caret inside a hashtag? - var candidateChanged = false; - var currentCaretIndex = getCaret(input[0]); - var text = input.val(); - var regexp; - if (TOKEN !== '') { - regexp = new RegExp('\\B' + TOKEN + '\\w+', 'g'); - } else { - regexp = new RegExp('\\b\\w+', 'g'); - } - var match; - while ((match = regexp.exec(text)) !== null) { - var startOfHashtag = match.index; - var endOfHashtag = startOfHashtag + match[0].length; - - if (startOfHashtag <= currentCaretIndex && currentCaretIndex <= endOfHashtag) { - candidateChanged = match[0].substring(TOKEN.length); - scope.candidate.start = startOfHashtag; - scope.candidate.end = endOfHashtag; - } - } - scope.$apply(function() { - scope.candidateHashtag = candidateChanged ? candidateChanged : "?"; }); - }); - - input.on('keydown', function(e) { - var listLength = scope.filteredTags.length; - if (0 < listLength) { - var currentIndex; - var nextIndex = null; - - if (e.keyCode === 40) { - // down arrow pressed - e.preventDefault(); - currentIndex = scope.selectedIndex === null ? -1 : parseInt(scope.selectedIndex, 10); - nextIndex = currentIndex === listLength - 1 ? 0 : currentIndex + 1; - } else if (e.keyCode === 38) { - // up arrow pressed - e.preventDefault(); - currentIndex = scope.selectedIndex === null ? 0 : parseInt(scope.selectedIndex, 10); - nextIndex = currentIndex === 0 ? listLength - 1 : currentIndex - 1; - } else if (e.keyCode === 13) { - // enter key pressed - e.preventDefault(); - var selectedTag = scope.filteredTags[scope.selectedIndex]; - insertSelectedTag(selectedTag); + + input.on('blur', function() { + if (!mouseIsOverSuggestions) { + scope.$apply(function() { + scope.isFocussed = false; + }); } + }); + input.on('keyup', function() { + // is the caret inside a hashtag? + var candidateChanged = false; + var currentCaretIndex = getCaret(input[0]); + var text = input.val(); + var regexp; + if (TOKEN !== '') { + regexp = new RegExp('\\B' + TOKEN + '\\w+', 'g'); + } else { + regexp = new RegExp('\\b\\w+', 'g'); + } + var match; + while ((match = regexp.exec(text)) !== null) { + var startOfHashtag = match.index; + var endOfHashtag = startOfHashtag + match[0].length; + + if (startOfHashtag <= currentCaretIndex && currentCaretIndex <= endOfHashtag) { + candidateChanged = match[0].substring(TOKEN.length); + scope.candidate.start = startOfHashtag; + scope.candidate.end = endOfHashtag; + } + } scope.$apply(function() { - scope.selectedIndex = nextIndex; + scope.candidateHashtag = candidateChanged ? candidateChanged : "?"; }); - } - }); - - function makeSuggestionsBox() { - var suggestions =angular.element( - '
' + - '
' + TOKEN + '{{ tag }}
' + - '
'); - suggestions.css({ - 'position': 'absolute', - 'width': input[0].offsetWidth + 'px', - 'left': input[0].offsetLeft + 'px', - 'max-height': '200px', - 'overflow': 'auto', - 'z-index': 100 }); - $compile(suggestions)(scope); - return suggestions; - } - - function insertSelectedTag(selectedTag) { - var inputVal = input.val(); - var output = inputVal.substring(0, scope.candidate.start) + TOKEN + selectedTag + inputVal.substring(scope.candidate.end); - scope.$parent.$apply(function() { - if (attrs.ngModel) { - var setter = $parse(attrs.ngModel).assign; - setter(scope.$parent, output); + input.on('keydown', function(e) { + var listLength = scope.filteredTags.length; + if (0 < listLength) { + var currentIndex; + var nextIndex = null; + + if (e.keyCode === 40) { + // down arrow pressed + e.preventDefault(); + currentIndex = scope.selectedIndex === null ? -1 : parseInt(scope.selectedIndex, 10); + nextIndex = currentIndex === listLength - 1 ? 0 : currentIndex + 1; + } else if (e.keyCode === 38) { + // up arrow pressed + e.preventDefault(); + currentIndex = scope.selectedIndex === null ? 0 : parseInt(scope.selectedIndex, 10); + nextIndex = currentIndex === 0 ? listLength - 1 : currentIndex - 1; + } else if (e.keyCode === 13) { + // enter key pressed + e.preventDefault(); + var selectedTag = scope.filteredTags[scope.selectedIndex]; + insertSelectedTag(selectedTag); + } + + scope.$apply(function() { + scope.selectedIndex = nextIndex; + }); } - input.val(output); }); - if(scope.callback) { - scope.callback(); + function makeSuggestionsBox() { + var suggestions =angular.element( + '
' + + '
' + TOKEN + '{{ tag }}
' + + '
'); + suggestions.css({ + 'position': 'absolute', + 'width': input[0].offsetWidth + 'px', + 'left': input[0].offsetLeft + 'px', + 'max-height': '200px', + 'overflow': 'auto', + 'z-index': 100 + }); + $compile(suggestions)(scope); + return suggestions; } - } - /** - * function taken from http://stackoverflow.com/a/263796/772859 - * @param el - * @returns {*} - */ - function getCaret(el) { - if (el.selectionStart) { - return el.selectionStart; - } else if (document.selection) { - el.focus(); - - var r = document.selection.createRange(); - if (r === null) { - return 0; - } + function insertSelectedTag(selectedTag) { + var inputVal = input.val(); + var output = inputVal.substring(0, scope.candidate.start) + TOKEN + selectedTag + inputVal.substring(scope.candidate.end); + + scope.$parent.$apply(function() { + if (attrs.ngModel) { + var setter = $parse(attrs.ngModel).assign; + setter(scope.$parent, output); + } + input.val(output); + }); - var re = el.createTextRange(), - rc = re.duplicate(); - re.moveToBookmark(r.getBookmark()); - rc.setEndPoint('EndToStart', re); + if(scope.callback) { + scope.callback(); + } + } - return rc.text.length; + /** + * function taken from http://stackoverflow.com/a/263796/772859 + * @param el + * @returns {*} + */ + function getCaret(el) { + if (el.selectionStart) { + return el.selectionStart; + } else if (document.selection) { + el.focus(); + + var r = document.selection.createRange(); + if (r === null) { + return 0; + } + + var re = el.createTextRange(), + rc = re.duplicate(); + re.moveToBookmark(r.getBookmark()); + rc.setEndPoint('EndToStart', re); + + return rc.text.length; + } + return 0; } - return 0; } - } - }; - }) - -/** - * Note - this filter is included since the default Angular `filter` filter will match a string that appears anywhere in the target string, but typically in a tag autocomplete, we only care about - * matching the start of the string. - */ - .filter('startsWith', function() { - return function(array, search) { - var matches = []; - for(var i = 0; i < array.length; i++) { - if (array[i].indexOf(search) === 0 && - search.length < array[i].length) { - matches.push(array[i]); + }; + }) + + /** + * Note - this filter is included since the default Angular `filter` filter will match a string that appears anywhere in the target string, but typically in a tag autocomplete, we only care about + * matching the start of the string. + */ + .filter('startsWith', function() { + return function(array, search) { + var matches = []; + for(var i = 0; i < array.length; i++) { + if (array[i].toLowerCase().indexOf(search.toLowerCase()) === 0 && + search.length < array[i].length) { + matches.push(array[i]); + } } - } - return matches; - }; - }); + return matches; + }; + }); + +})(); \ No newline at end of file diff --git a/src/directives/tagbox/dirTagbox.spec.js b/src/directives/tagbox/dirTagbox.spec.js index 47916d6..179831f 100644 --- a/src/directives/tagbox/dirTagbox.spec.js +++ b/src/directives/tagbox/dirTagbox.spec.js @@ -9,7 +9,6 @@ describe('tagbox directive', function() { var scope; beforeEach(module('angularUtils.directives.dirTagBox')); - beforeEach(module('angularUtils.filters.startsWith')); beforeEach(inject(function(_$compile_, _$rootScope_) { scope = _$rootScope_; @@ -67,6 +66,16 @@ describe('tagbox directive', function() { expect(suggestions.children()[0].innerHTML).toBe('#hammer'); }); + it('should be case-insensitive', function() { + textarea.val('#C'); + textarea[0].selectionStart = 2; + + textarea.triggerHandler('keyup'); + scope.$apply(); + expect(suggestions.children()[0].innerHTML).toBe('#cake'); + expect(suggestions.children()[1].innerHTML).toBe('#cup'); + }); + describe('specifying the tagToken', function() { var input; var suggestions; From eafafdfd10003f8ba0342ece82eb545031cb7ac2 Mon Sep 17 00:00:00 2001 From: Michael Bromley Date: Thu, 18 Dec 2014 14:08:41 +0100 Subject: [PATCH 11/99] dirPagination: Add range API to pagination controls The dirPaginationControls directive now exposes a `range` object which can be used in the template to allow things like "displaying x - y of z items". Also updated the docs to include an example of usage. --- src/directives/pagination/README.md | 49 ++++++++++++++- src/directives/pagination/dirPagination.js | 24 +++++++- .../pagination/dirPagination.spec.js | 61 +++++++++++++++++++ .../pagination/testTemplate.tpl.html | 8 +++ 4 files changed, 140 insertions(+), 2 deletions(-) create mode 100644 src/directives/pagination/testTemplate.tpl.html diff --git a/src/directives/pagination/README.md b/src/directives/pagination/README.md index cd00d93..426705f 100644 --- a/src/directives/pagination/README.md +++ b/src/directives/pagination/README.md @@ -11,7 +11,26 @@ an attribute, drop in your navigation wherever you like, and boom - instant, ful [Here is a working demo on Plunker](http://plnkr.co/edit/Wtkv71LIqUR4OhzhgpqL?p=preview) which demonstrates some cool features such as live-binding the "itemsPerPage" and filtering of the collection. -## Example +# Table of Contents + +- [Basic Example](#basic-example) +- [Installation](#installation) +- [Usage](#usage) + - [Specifying The Template](#specifying-the-template) +- [Directives API](#directives-api) + - [dir-paginate](#dir-paginate) + - [dir-pagination-controls](#dir-pagination-controls) +- [Writing A Custom Pagination-Controls Template](#writing-a-custom-pagination-controls-template) +- [Special Repeat Start and End Points](#special-repeat-start-and-end-points) +- [Multiple Pagination Instances on One Page](#multiple-pagination-instances-on-one-page) + - [Demo](#demo-1) +- [Working With Asynchronous Data](#working-with-asynchronous-data) + - [Example Asynchronous Setup](#example-asynchronous-setup) +- [Styling](#styling) +- [Contribution](#contribution) +- [Credits](#credits) + +## Basic Example Let's say you have a collection of items on your controller's `$scope`. Often you want to display them with the `ng-repeat` directive and then paginate the results if there are too many to fit on one page. This is what this @@ -91,6 +110,9 @@ myApp.config(function(paginationTemplateProvider) { ## Directives API +The following attributes form the API for the pagination and pagination-controls directives. Optional attributes are marked as such, +otherwise they are required. + ### `dir-paginate` * **`expression`** Under the hood, this directive delegates to the `ng-repeat` directive, so the syntax for the @@ -137,6 +159,31 @@ one pagination instance per page. See the section below on setting up multiple i Note: you cannot use the `dir-pagination-controls` directive without `dir-paginate`. Attempting to do so will result in an exception. +## Writing A Custom Pagination-Controls Template + +The default template ([dirPagination.tpl.html](dirPagination.tpl.html)) is based on the [Bootstrap pagination markup](http://getbootstrap.com/components/#pagination). If you wish to modify the template or write your own, +there are a few useful values exposed by the directive which you can use: + +- `pages` The array of page numbers, typically used in an `ng-repeat` to generate the individual page links. +- `{{ pagination.current }}` The current page. +- `{{ pagination.last }}` The number of the last page in the collection. +- `{{ range.lower }}` The ordinal number of the first item on the current page. E.g. assuming 10 items per page, when on page 2 this will equal 11. +- `{{ range.upper }}` The ordinal number of the last item on the current page. E.g. assuming 10 items per page, when on page 2 this will equal 20. +- `{{ range.total }}` The total number of items in the collection. + +The three `range` values can be used to generate a label like *"Displaying 16-20 of 53 items"*. + +Here is an example of a custom template which uses the range values along with "previous" and "next" arrow links, but no page links: + +```HTML +
Displaying {{ range.lower }} - {{ range.upper }} of {{ range.total }}
+ + + +``` + +To use a custom template in your app, see the section on [specifying the template](#specifying-the-template). + ## Special Repeat Start and End Points As with the [ngRepeat directive](https://docs.angularjs.org/api/ng/directive/ngRepeat#special-repeat-start-and-end-points), you can use the `-start` and `-end` suffix on the `dir-paginate` directive to diff --git a/src/directives/pagination/dirPagination.js b/src/directives/pagination/dirPagination.js index 52c995a..c121415 100644 --- a/src/directives/pagination/dirPagination.js +++ b/src/directives/pagination/dirPagination.js @@ -204,6 +204,11 @@ last: 1, current: 1 }; + scope.range = { + lower: 1, + upper: 1, + total: 1 + }; scope.$watch(function() { return (paginationService.getCollectionLength(paginationId) + 1) * paginationService.getItemsPerPage(paginationId); @@ -239,6 +244,7 @@ if (isValidPageNumber(num)) { scope.pages = generatePagesArray(num, paginationService.getCollectionLength(paginationId), paginationService.getItemsPerPage(paginationId), paginationRange); scope.pagination.current = num; + updateRangeValues(); // if a callback has been set, then call it with the page number as an argument if (scope.onPageChange) { @@ -255,9 +261,25 @@ scope.pagination.last = scope.pages[scope.pages.length - 1]; if (scope.pagination.last < scope.pagination.current) { scope.setCurrent(scope.pagination.last); + } else { + updateRangeValues(); } } + /** + * This function updates the values (lower, upper, total) of the `scope.range` object, which can be used in the pagination + * template to display the current page range, e.g. "showing 21 - 40 of 144 results"; + */ + function updateRangeValues() { + var currentPage = paginationService.getCurrentPage(paginationId), + itemsPerPage = paginationService.getItemsPerPage(paginationId), + totalItems = paginationService.getCollectionLength(paginationId); + + scope.range.lower = (currentPage - 1) * itemsPerPage + 1; + scope.range.upper = Math.min(currentPage * itemsPerPage, totalItems); + scope.range.total = totalItems; + } + function isValidPageNumber(num) { return (numberRegex.test(num) && (0 < num && num <= scope.pagination.last)); } @@ -351,7 +373,7 @@ }); module.provider('paginationTemplate', function() { - + var templatePath = 'directives/pagination/dirPagination.tpl.html'; this.setPath = function(path) { diff --git a/src/directives/pagination/dirPagination.spec.js b/src/directives/pagination/dirPagination.spec.js index 49b62fe..4f14cce 100644 --- a/src/directives/pagination/dirPagination.spec.js +++ b/src/directives/pagination/dirPagination.spec.js @@ -605,6 +605,67 @@ describe('dirPagination directive', function() { }); + describe('pagination controls template API', function() { + function compile() { + var html = '
  • {{ item }}
' + + ''; + $scope.collection = [1,2,3,4,5,6,7]; + $scope.currentPage = 1; + containingElement.append($compile(html)($scope)); + $scope.$apply(); + } + + it('should provide correct values for current page', function() { + compile(); + + expect(containingElement.find('#tt-pagination-current').html()).toEqual('1'); + $scope.$apply(function() { + $scope.currentPage = 2; + }); + expect(containingElement.find('#tt-pagination-current').html()).toEqual('2'); + }); + + it('should provide correct value for last page', function() { + compile(); + expect(containingElement.find('#tt-pagination-last').html()).toEqual('3'); + }); + + it('should provide correct value for range.lower', function() { + compile(); + expect(containingElement.find('#tt-range-lower').html()).toEqual('1'); + $scope.$apply(function() { + $scope.currentPage = 2; + }); + expect(containingElement.find('#tt-range-lower').html()).toEqual('4'); + $scope.$apply(function() { + $scope.currentPage = 3; + }); + expect(containingElement.find('#tt-range-lower').html()).toEqual('7'); + }); + + it('should provide correct value for range.upper', function() { + compile(); + expect(containingElement.find('#tt-range-upper').html()).toEqual('3'); + $scope.$apply(function() { + $scope.currentPage = 2; + }); + expect(containingElement.find('#tt-range-upper').html()).toEqual('6'); + $scope.$apply(function() { + $scope.currentPage = 3; + }); + expect(containingElement.find('#tt-range-upper').html()).toEqual('7'); + }); + + it('should provide correct value for range.total', function() { + compile(); + expect(containingElement.find('#tt-range-total').html()).toEqual('7'); + $scope.$apply(function() { + $scope.currentPage = 2; + }); + expect(containingElement.find('#tt-range-total').html()).toEqual('7'); + }); + }); + describe('multi element functionality', function() { function compileMultiElement(collection, itemsPerPage, currentPage) { diff --git a/src/directives/pagination/testTemplate.tpl.html b/src/directives/pagination/testTemplate.tpl.html new file mode 100644 index 0000000..5e5b141 --- /dev/null +++ b/src/directives/pagination/testTemplate.tpl.html @@ -0,0 +1,8 @@ + +
+
{{ pagination.current }}
+
{{ pagination.last }}
+
{{ range.lower }}
+
{{ range.upper }}
+
{{ range.total }}
+
From bde42c0d9a85ce6353934c0f9ffde8c7a9f7c3b4 Mon Sep 17 00:00:00 2001 From: Michael Bromley Date: Thu, 18 Dec 2014 14:20:31 +0100 Subject: [PATCH 12/99] dirTagbox: fix https://github.com/michaelbromley/angularUtils/issues/96 Pressing "enter" without selecting an item was returning "undefined" --- src/directives/tagbox/dirTagbox.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/directives/tagbox/dirTagbox.js b/src/directives/tagbox/dirTagbox.js index 3f668e9..ca55f8e 100644 --- a/src/directives/tagbox/dirTagbox.js +++ b/src/directives/tagbox/dirTagbox.js @@ -156,8 +156,14 @@ } function insertSelectedTag(selectedTag) { - var inputVal = input.val(); - var output = inputVal.substring(0, scope.candidate.start) + TOKEN + selectedTag + inputVal.substring(scope.candidate.end); + var output, + inputVal = input.val(); + + if (typeof selectedTag === 'undefined') { + output = inputVal; + } else { + output = inputVal.substring(0, scope.candidate.start) + TOKEN + selectedTag + inputVal.substring(scope.candidate.end); + } scope.$parent.$apply(function() { if (attrs.ngModel) { From 7c10e0abb4ba7bd6687af24c9cdac7c33149fca4 Mon Sep 17 00:00:00 2001 From: Michael Bromley Date: Thu, 18 Dec 2014 14:36:27 +0100 Subject: [PATCH 13/99] dirTagbox: Bump version number --- src/directives/tagbox/dirTagbox.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/directives/tagbox/dirTagbox.js b/src/directives/tagbox/dirTagbox.js index ca55f8e..7ae8090 100644 --- a/src/directives/tagbox/dirTagbox.js +++ b/src/directives/tagbox/dirTagbox.js @@ -1,5 +1,5 @@ /** - * Version 0.1.2 + * Version 0.1.3 * * A directive to enable tagging auto-complete on an input or textarea. * From c6275a699ba580b66a02e7a922fe169ef9b6c61c Mon Sep 17 00:00:00 2001 From: Michael Bromley Date: Thu, 18 Dec 2014 17:16:32 +0100 Subject: [PATCH 14/99] dirPagination: allow expressions as pagiantionID This changes allows you to use expressions in the `pagination-id` attribute, which in turn enables the use of multiple independent pagination instances generated by an ng-repeat. It also simplifies the API by not requiring the pagination id to be specified as a second parameter in the itemsPerPage filter. --- src/directives/pagination/README.md | 43 +++++++++++-- src/directives/pagination/dirPagination.js | 64 +++++++++++++------ .../pagination/dirPagination.spec.js | 59 +++++++++++++++++ 3 files changed, 139 insertions(+), 27 deletions(-) diff --git a/src/directives/pagination/README.md b/src/directives/pagination/README.md index 426705f..648e369 100644 --- a/src/directives/pagination/README.md +++ b/src/directives/pagination/README.md @@ -23,6 +23,7 @@ filtering of the collection. - [Writing A Custom Pagination-Controls Template](#writing-a-custom-pagination-controls-template) - [Special Repeat Start and End Points](#special-repeat-start-and-end-points) - [Multiple Pagination Instances on One Page](#multiple-pagination-instances-on-one-page) + - [Multiple Instances With ngRepeat](#multiple-instances-with-ngrepeat) - [Demo](#demo-1) - [Working With Asynchronous Data](#working-with-asynchronous-data) - [Example Asynchronous Setup](#example-asynchronous-setup) @@ -204,38 +205,68 @@ repeat a series of elements instead of just one parent element: ## Multiple Pagination Instances on One Page -Multiple instances of the directives may be included on a single page by specifying a `pagination-id`. This property **must** be specified in **3** places +Multiple instances of the directives may be included on a single page by specifying a `pagination-id`. This property **must** be specified in **2** places for this to work: 1. Specify the `pagination-id` attribute on the `dir-paginate` directive. -2. Specify the third parameter of the `itemsPerPage` filter. 3. Specify the `pagination-id` attribute on the `dir-paginations-controls` directive. +**Note:** Prior to version 0.5.0, there was an additional requirement to add the ID as a second parameter of the `itemsPerPage` filter. This is now no longer required, as the +directive will add this parameter automatically. Old code that *does* explicitly declare the ID in the filter will still work. + An example of two independent paginations on one page would look like this: ```HTML
    -
  • {{ customer.name }}
  • +
  • {{ customer.name }}
    -
  • {{ customer.name }}
  • +
  • {{ customer.name }}
``` The pagination-ids above are set to "cust" in the first instance and "branch" in the second. The pagination-ids can be anything you like, -the important thing is to make sure the exact same id is used in all 3 places. If the 3 ids don't match, you should see a helpful +the important thing is to make sure the exact same id is used on both the pagination and the controls directives. If the 2 ids don't match, you should see a helpful exception in the console. +### Multiple Instances With ngRepeat + +You can use the pagination-id feature to dynamically create pagination instances, for example inside an `ng-repeat` block. Here is a bare-bones example to +demonstrate how that would work: + +```JavaScript +// in the controller +$scope.lists = [ + { + id: 'list1', + collection: [1, 2, 3, 4, 5] + }, + { + id: 'list2', + collection: ['a', 'b', 'c', 'd', 'e'] + }]; +``` + +```HTML + +
+
    +
  • ID: {{ list.id }}, item: {{ item }}
  • +
+ +
+``` + ### Demo -Here is a working demo featuring two instances on one page: [http://plnkr.co/edit/Pm4L53UYAieF808v8wxL?p=preview](http://plnkr.co/edit/Pm4L53UYAieF808v8wxL?p=preview) +Here is a working demo featuring two instances on one page: [http://plnkr.co/edit/xmjmIId0c9Glh5QH97xz?p=preview](http://plnkr.co/edit/xmjmIId0c9Glh5QH97xz?p=preview) ## Working With Asynchronous Data diff --git a/src/directives/pagination/dirPagination.js b/src/directives/pagination/dirPagination.js index c121415..869f128 100644 --- a/src/directives/pagination/dirPagination.js +++ b/src/directives/pagination/dirPagination.js @@ -20,6 +20,7 @@ * Config */ var moduleName = 'angularUtils.directives.dirPagination'; + var DEFAULT_ID = '__default'; /** * Module @@ -40,15 +41,6 @@ priority: 5000, // This setting is used in conjunction with the later call to $compile() to prevent infinite recursion of compilation compile: function dirPaginationCompileFn(tElement, tAttrs){ - // Add ng-repeat to the dom element - if (tElement[0].hasAttribute('dir-paginate-start') || tElement[0].hasAttribute('data-dir-paginate-start')) { - // using multiElement mode (dir-paginate-start, dir-paginate-end) - tAttrs.$set('ngRepeatStart', tAttrs.dirPaginate); - tElement.eq(tElement.length - 1).attr('ng-repeat-end', true); - } else { - tAttrs.$set('ngRepeat', tAttrs.dirPaginate); - } - var expression = tAttrs.dirPaginate; // regex taken directly from https://github.com/angular/angular.js/blob/master/src/ng/directive/ngRepeat.js#L211 var match = expression.match(/^\s*([\s\S]+?)\s+in\s+([\s\S]+?)(?:\s+track\s+by\s+([\s\S]+?))?\s*$/); @@ -60,10 +52,35 @@ var itemsPerPageFilterRemoved = match[2].replace(filterPattern, ''); var collectionGetter = $parse(itemsPerPageFilterRemoved); - var paginationId = tAttrs.paginationId || '__default'; - paginationService.registerInstance(paginationId); + // If any value is specified for paginationId, we register the un-evaluated expression at this stage for the benefit of any + // dir-pagination-controls directives that may be looking for this ID. + var rawId = tAttrs.paginationId || DEFAULT_ID; + paginationService.registerInstance(rawId); return function dirPaginationLinkFn(scope, element, attrs){ + + // Now that we have access to the `scope` we can interpolate any expression given in the paginationId attribute and + // potentially register a new ID if it evaluates to a different value than the rawId. + var paginationId = $parse(attrs.paginationId)(scope) || attrs.paginationId || DEFAULT_ID; + paginationService.registerInstance(paginationId); + + var repeatExpression; + var idDefinedInFilter = !!expression.match(/(\|\s*itemsPerPage\s*:[^|]*:[^|]*)/); + if (paginationId !== DEFAULT_ID && !idDefinedInFilter) { + repeatExpression = expression.replace(/(\|\s*itemsPerPage\s*:[^|]*)/, "$1 : '" + paginationId + "'"); + } else { + repeatExpression = expression; + } + + // Add ng-repeat to the dom element + if (element[0].hasAttribute('dir-paginate-start') || element[0].hasAttribute('data-dir-paginate-start')) { + // using multiElement mode (dir-paginate-start, dir-paginate-end) + attrs.$set('ngRepeatStart', repeatExpression); + element.eq(element.length - 1).attr('ng-repeat-end', true); + } else { + attrs.$set('ngRepeat', repeatExpression); + } + var compiled = $compile(element, false, 5000); // we manually compile the element again, as we have now added ng-repeat. Priority less than 5000 prevents infinite recursion of compiling dirPaginate var currentPageGetter; @@ -183,21 +200,26 @@ }, scope: { maxSize: '=?', - onPageChange: '&?' + onPageChange: '&?', + paginationId: '=?' }, - link: function(scope, element, attrs) { + link: function dirPaginationControlsLinkFn(scope, element, attrs) { - var paginationId; - paginationId = attrs.paginationId || '__default'; - if (!scope.maxSize) { scope.maxSize = 9; } - scope.directionLinks = angular.isDefined(attrs.directionLinks) ? scope.$parent.$eval(attrs.directionLinks) : true; - scope.boundaryLinks = angular.isDefined(attrs.boundaryLinks) ? scope.$parent.$eval(attrs.boundaryLinks) : false; + // rawId is the un-interpolated value of the pagination-id attribute. This is only important when the corresponding dir-paginate directive has + // not yet been linked (e.g. if it is inside an ng-if block), and in that case it prevents this controls directive from assuming that there is + // no corresponding dir-paginate directive and wrongly throwing an exception. + var rawId = attrs.paginationId || DEFAULT_ID; + var paginationId = scope.paginationId || attrs.paginationId || DEFAULT_ID; - if (!paginationService.isRegistered(paginationId)) { - var idMessage = (paginationId !== '__default') ? ' (id: ' + paginationId + ') ' : ' '; + if (!paginationService.isRegistered(paginationId) && !paginationService.isRegistered(rawId)) { + var idMessage = (paginationId !== DEFAULT_ID) ? ' (id: ' + paginationId + ') ' : ' '; throw 'pagination directive: the pagination controls' + idMessage + 'cannot be used without the corresponding pagination directive.'; } + if (!scope.maxSize) { scope.maxSize = 9; } + scope.directionLinks = angular.isDefined(attrs.directionLinks) ? scope.$parent.$eval(attrs.directionLinks) : true; + scope.boundaryLinks = angular.isDefined(attrs.boundaryLinks) ? scope.$parent.$eval(attrs.boundaryLinks) : false; + var paginationRange = Math.max(scope.maxSize, 5); scope.pages = []; scope.pagination = { @@ -291,7 +313,7 @@ return function(collection, itemsPerPage, paginationId) { if (typeof (paginationId) === 'undefined') { - paginationId = '__default'; + paginationId = DEFAULT_ID; } if (!paginationService.isRegistered(paginationId)) { throw 'pagination directive: the itemsPerPage id argument (id: ' + paginationId + ') does not match a registered pagination-id.'; diff --git a/src/directives/pagination/dirPagination.spec.js b/src/directives/pagination/dirPagination.spec.js index 4f14cce..bdc7746 100644 --- a/src/directives/pagination/dirPagination.spec.js +++ b/src/directives/pagination/dirPagination.spec.js @@ -722,4 +722,63 @@ describe('dirPagination directive', function() { expect(containingElement.find('p').length).toEqual(3); }); }); + + /** + * THis suite tests out the ability to handle dynamically-generated pagination ids. The main use case is when + * there are multiple dir-pagination instances being generated by an ng-repeat, and the pagination id is + * only known at run-time. + */ + describe('dynamic pagination ids', function() { + function compile() { + var html = '
    ' + + '
  • {{ item }}
  • ' + + '
' + + '' + + '
'; + + $scope.lists = [ + { + id: 'list1', + collection: [1, 2, 3, 4, 5] + }, + { + id: 'list2', + collection: ['a', 'b', 'c', 'd', 'e'] + } + ]; + containingElement.append($compile(html)($scope)); + $scope.$apply(); + } + + function getListItems($list) { + return $list.find('li').map(function() { + return $(this).text().trim(); + }).get(); + } + + it('should not throw an exception', function() { + expect(compile).not.toThrow(); + }); + + it('should allow independent pagination', function() { + compile(); + + var $list1 = containingElement.find('ul.list').eq(0); + var $list2 = containingElement.find('ul.list').eq(1); + + expect(getListItems($list1)).toEqual([ '1', '2', '3' ]); + expect(getListItems($list2)).toEqual([ 'a', 'b', 'c' ]); + + // click the "page 2" link on the first set of pagination + var pagination1 = $list1.parent().find('ul.pagination'); + pagination1.children().eq(2).find('a').triggerHandler('click'); + $scope.$apply(); + + // ensure only the first set of pagination changes + expect(getListItems($list1)).toEqual([ '4', '5' ]); + expect(getListItems($list2)).toEqual([ 'a', 'b', 'c' ]); + }); + + + }); }); From dfcf8947592c5614cebf60cdc4e5c07d2f1e5026 Mon Sep 17 00:00:00 2001 From: Michael Bromley Date: Mon, 19 Jan 2015 16:37:23 +0000 Subject: [PATCH 15/99] dirDisqus: make it publishable There was a request for a separate repo for the Disqus module so that it can be managed via Bower. --- Gruntfile.js | 6 ++++++ src/directives/disqus/dirDisqus.js | 23 +++++++++++++++++++++-- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/Gruntfile.js b/Gruntfile.js index 9d39d2b..d0e3701 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -85,6 +85,12 @@ module.exports = function(grunt) { 'src/directives/uiBreadcrumbs/uiBreadcrumbs.tpl.html'], dest: '../angularUtils-dist/angularUtils-uiBreadcrumbs/' + }, + dirDisqus: { + expand: true, + flatten: true, + src: ['src/directives/disqus/dirDisqus.js'], + dest: '../angularUtils-dist/angularUtils-disqus/' } } }); diff --git a/src/directives/disqus/dirDisqus.js b/src/directives/disqus/dirDisqus.js index 535b66f..eafc4a9 100644 --- a/src/directives/disqus/dirDisqus.js +++ b/src/directives/disqus/dirDisqus.js @@ -7,9 +7,26 @@ * Copyright Michael Bromley 2014 * Available under the MIT license. */ -angular.module('angularUtils.directives.dirDisqus', []) - .directive('dirDisqus', ['$window', function($window) { +(function() { + + /** + * Config + */ + var moduleName = 'angularUtils.directives.dirDisqus'; + + /** + * Module + */ + var module; + try { + module = angular.module(moduleName); + } catch(err) { + // named module does not exist, so create one + module = angular.module(moduleName, []); + } + + module.directive('dirDisqus', ['$window', function($window) { return { restrict: 'E', scope: { @@ -66,3 +83,5 @@ angular.module('angularUtils.directives.dirDisqus', []) } }; }]); + +})(); From 7b377c9a54adce4b19c2a8e88b956f454336903c Mon Sep 17 00:00:00 2001 From: Michael Bromley Date: Mon, 19 Jan 2015 16:53:00 +0000 Subject: [PATCH 16/99] dirDisqus: Add Bower instructions to readme In response to https://github.com/michaelbromley/angularUtils/issues/100 --- src/directives/disqus/README.md | 9 +++++++-- src/directives/disqus/dirDisqus.js | 2 -- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/directives/disqus/README.md b/src/directives/disqus/README.md index de9d4cd..500b166 100644 --- a/src/directives/disqus/README.md +++ b/src/directives/disqus/README.md @@ -13,6 +13,12 @@ Setting up as above will ensure that Disqus is able to correctly distinguish bet By default, Angular does not use html5 mode, and also has no hashPrefix set, so you'll have to do one of the above set-up actions in order to use this directive. As far as I know, there is no way to get it to work with the default hash-only (no `!`) urls that Angular uses. +## Installation + +1. Download the file `dirDisqus.js` or use the Bower command `bower install angular-utils-disqus` +2. Include the JavaScript file in your index.html page. +2. Add a reference to the module `angularUtils.directives.dirDisqus` to your app. + ## Usage First, put the directive code in your app, wherever you store your directives. @@ -34,8 +40,7 @@ You can optionally specify the other configuration variables by including the as on the directive's element tag. For more information on the available config vars, see the [Disqus docs](http://help.disqus.com/customer/portal/articles/472098-javascript-configuration-variables). -Note that in the tag, the config attribute names are separated with a hyphen rather -than an underscore (to make it look more HTML-like). +Note that in the tag, the config attribute names are separated with a hyphen rather than an underscore (to make it look more HTML-like). diff --git a/src/directives/disqus/dirDisqus.js b/src/directives/disqus/dirDisqus.js index eafc4a9..034c313 100644 --- a/src/directives/disqus/dirDisqus.js +++ b/src/directives/disqus/dirDisqus.js @@ -1,8 +1,6 @@ /** * A directive to embed a Disqus comments widget on your AngularJS page. * - * For documentation, see the README.md file in this directory - * * Created by Michael on 22/01/14. * Copyright Michael Bromley 2014 * Available under the MIT license. From cbf2a56e8a92a732d5e1b03eed3b4865f7f7fb02 Mon Sep 17 00:00:00 2001 From: Michael Bromley Date: Tue, 20 Jan 2015 10:58:32 +0000 Subject: [PATCH 17/99] uiBreadcrumbs: update ui-router version to 0.2.13 --- bower.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bower.json b/bower.json index 54ccd11..5ce84f3 100644 --- a/bower.json +++ b/bower.json @@ -17,7 +17,7 @@ "tests" ], "dependencies": { - "angular-ui-router": "~0.2.10", + "angular-ui-router": "~0.2.13", "angular": "~1.2.24" }, "devDependencies": { From 0c053d3df37906262e6a802a38c29b244a1164ce Mon Sep 17 00:00:00 2001 From: Michael Bromley Date: Thu, 22 Jan 2015 10:09:50 +0000 Subject: [PATCH 18/99] dirPagination: remove unused $timeout service --- src/directives/pagination/dirPagination.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/directives/pagination/dirPagination.js b/src/directives/pagination/dirPagination.js index 869f128..cc1ee5c 100644 --- a/src/directives/pagination/dirPagination.js +++ b/src/directives/pagination/dirPagination.js @@ -33,7 +33,7 @@ module = angular.module(moduleName, []); } - module.directive('dirPaginate', ['$compile', '$parse', '$timeout', 'paginationService', function($compile, $parse, $timeout, paginationService) { + module.directive('dirPaginate', ['$compile', '$parse', 'paginationService', function($compile, $parse, paginationService) { return { terminal: true, From 76fc70bdaefa086629ebae9297fa753a3f1239fd Mon Sep 17 00:00:00 2001 From: Daniel Nelson Date: Tue, 24 Feb 2015 10:51:37 -0600 Subject: [PATCH 19/99] Support uglification --- src/filters/ordinalDate/ordinalDate.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/filters/ordinalDate/ordinalDate.js b/src/filters/ordinalDate/ordinalDate.js index 4b2c801..553edae 100644 --- a/src/filters/ordinalDate/ordinalDate.js +++ b/src/filters/ordinalDate/ordinalDate.js @@ -1,6 +1,6 @@ angular.module( 'angularUtils.filters.ordinalDate', [] ) - .filter('ordinalDate', function($filter) { + .filter('ordinalDate', ['$filter', function($filter) { var getOrdinalSuffix = function(number) { var suffixes = ["'th'", "'st'", "'nd'", "'rd'"]; @@ -60,4 +60,4 @@ angular.module( 'angularUtils.filters.ordinalDate', [] ) } return $filter('date')(date, format); }; - }); \ No newline at end of file + }]); \ No newline at end of file From f2997ae293b0a936f06d072b50be2d74abdfb140 Mon Sep 17 00:00:00 2001 From: Michael Bromley Date: Mon, 9 Mar 2015 10:40:20 +0100 Subject: [PATCH 20/99] Fix for https://github.com/michaelbromley/angularUtils/issues/112 This prevents the onPageChange callback from firing on page load when the controls appear in the template before the pagination. --- src/directives/pagination/dirPagination.js | 2 +- .../pagination/dirPagination.spec.js | 60 ++++++++++++++++++- 2 files changed, 58 insertions(+), 4 deletions(-) diff --git a/src/directives/pagination/dirPagination.js b/src/directives/pagination/dirPagination.js index cc1ee5c..5aa6efb 100644 --- a/src/directives/pagination/dirPagination.js +++ b/src/directives/pagination/dirPagination.js @@ -243,7 +243,7 @@ scope.$watch(function() { return (paginationService.getItemsPerPage(paginationId)); }, function(current, previous) { - if (current != previous) { + if (current != previous && typeof previous !== 'undefined') { goToPage(scope.pagination.current); } }); diff --git a/src/directives/pagination/dirPagination.spec.js b/src/directives/pagination/dirPagination.spec.js index bdc7746..cf935f2 100644 --- a/src/directives/pagination/dirPagination.spec.js +++ b/src/directives/pagination/dirPagination.spec.js @@ -6,15 +6,17 @@ describe('dirPagination directive', function() { var $compile; var $scope; + var $timeout; var containingElement; var myCollection; beforeEach(module('angularUtils.directives.dirPagination')); beforeEach(module('templates-main')); - beforeEach(inject(function($rootScope, _$compile_) { + beforeEach(inject(function($rootScope, _$compile_, _$timeout_) { $compile = _$compile_; + $timeout = _$timeout_; $scope = $rootScope.$new(); containingElement = angular.element('
'); @@ -343,7 +345,7 @@ describe('dirPagination directive', function() { function compileWithAttributes(attributes) { $scope.collection = myCollection; $scope.currentPage = 1; - html = '
  • {{ item }}
' + + var html = '
  • {{ item }}
' + ''; containingElement.append($compile(html)($scope)); $scope.$apply(); @@ -405,10 +407,36 @@ describe('dirPagination directive', function() { return "The current page is " + currentPage; }; spyOn($scope, 'myCallback').and.callThrough(); - compileWithAttributes(' on-page-change="myCallback(newPageNumber)" '); }); it('should call the callback once when page link clicked', function() { + compileWithAttributes(' on-page-change="myCallback(newPageNumber)" '); + var pagination = containingElement.find('ul.pagination'); + + expect($scope.myCallback.calls.count()).toEqual(0); + pagination.children().eq(2).find('a').triggerHandler('click'); + $scope.$apply(); + expect($scope.myCallback).toHaveBeenCalled(); + expect($scope.myCallback.calls.count()).toEqual(1); + }); + + it('should not call the callback on loading first page, even with controls appearing above the pagination', function() { + function compileWithControlsFirst(attributes) { + $scope.currentPage = 1; + var html = '' + + '' + + '' + + '
{{ item }}
'; + containingElement.append($compile(html)($scope)); + $scope.$apply(); + } + + compileWithControlsFirst(' on-page-change="myCallback(newPageNumber)" '); + + $scope.$apply(function() { + $scope.collection = myCollection; + }); + var pagination = containingElement.find('ul.pagination'); expect($scope.myCallback.calls.count()).toEqual(0); @@ -419,6 +447,7 @@ describe('dirPagination directive', function() { }); it('should pass the current page number to the callback', function() { + compileWithAttributes(' on-page-change="myCallback(newPageNumber)" '); var pagination = containingElement.find('ul.pagination'); pagination.children().eq(2).find('a').triggerHandler('click'); @@ -721,6 +750,31 @@ describe('dirPagination directive', function() { expect(containingElement.find('h1').length).toEqual(3); expect(containingElement.find('p').length).toEqual(3); }); + + /** + * See https://github.com/michaelbromley/angularUtils/issues/92 + */ + xit('should correctly compile an inner ng-repeat', function() { + function compile() { + var html = '
' + + '

{{ item }}

' + + '
yo
  • {{ option }} : {{ item }}
' + + '
'; + $scope.options = ['option1', 'option2', 'option3']; + $scope.collection = myCollection; + $scope.currentPage = 1; + containingElement.append($compile(html)($scope)); + $scope.$apply(); + } + + compile(); + $timeout.flush(); + + console.log(containingElement.html()); + + var options = containingElement.find('.options').eq(0).find('li'); + expect(options.length).toEqual(3); + }); }); /** From 6915137d3953b22b271775c38ed322066a1d2ae1 Mon Sep 17 00:00:00 2001 From: Michael Bromley Date: Wed, 18 Mar 2015 09:04:06 +0100 Subject: [PATCH 21/99] Add note on buggy -start- -end feature --- src/directives/pagination/README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/directives/pagination/README.md b/src/directives/pagination/README.md index 648e369..dbc6bf6 100644 --- a/src/directives/pagination/README.md +++ b/src/directives/pagination/README.md @@ -202,6 +202,10 @@ repeat a series of elements instead of just one parent element: ``` +**Note:** - This feature is currently a little buggy. E.g. attempting to next an ngRepeat in the `
` block above +would currently fail. For simple cases it will work fine, but with complex nexted directives you may experience issues. +I plan to fix this as a priority. See issues https://github.com/michaelbromley/angularUtils/issues/92 & https://github.com/michaelbromley/angularUtils/issues/129 + ## Multiple Pagination Instances on One Page From dcf0a24e56f72e8b9bd89f89c718383d4a724293 Mon Sep 17 00:00:00 2001 From: ozdemirbur Date: Thu, 19 Mar 2015 22:51:22 +0200 Subject: [PATCH 22/99] dirDisqus: added multi-language support --- src/directives/disqus/dirDisqus.js | 8 ++++++-- src/directives/disqus/dirDisqus.spec.js | 8 ++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/directives/disqus/dirDisqus.js b/src/directives/disqus/dirDisqus.js index 034c313..9c09ec5 100644 --- a/src/directives/disqus/dirDisqus.js +++ b/src/directives/disqus/dirDisqus.js @@ -34,9 +34,10 @@ disqus_url: '@disqusUrl', disqus_category_id: '@disqusCategoryId', disqus_disable_mobile: '@disqusDisableMobile', + disqus_config_language : '@disqusConfigLanguage', readyToBind: "@" }, - template: '
comments powered by Disqus', + template: '
', link: function(scope) { // ensure that the disqus_identifier and disqus_url are both set, otherwise we will run in to identifier conflicts when using URLs with "#" in them @@ -60,7 +61,9 @@ $window.disqus_url = scope.disqus_url; $window.disqus_category_id = scope.disqus_category_id; $window.disqus_disable_mobile = scope.disqus_disable_mobile; - + $window.disqus_config = function () { + this.language = scope.disqus_config_language; + }; // get the remote Disqus script and insert it into the DOM, but only if it not already loaded (as that will cause warnings) if (!$window.DISQUS) { var dsq = document.createElement('script'); dsq.type = 'text/javascript'; dsq.async = true; @@ -73,6 +76,7 @@ this.page.identifier = scope.disqus_identifier; this.page.url = scope.disqus_url; this.page.title = scope.disqus_title; + this.language = scope.disqus_config_language; } }); } diff --git a/src/directives/disqus/dirDisqus.spec.js b/src/directives/disqus/dirDisqus.spec.js index 8acc174..e99092e 100644 --- a/src/directives/disqus/dirDisqus.spec.js +++ b/src/directives/disqus/dirDisqus.spec.js @@ -13,6 +13,7 @@ xdescribe('dirDiqus directive', function() { 'disqus-url="{{ post.link }}"' + 'disqus-category-id="{{ post.catId }}"' + 'disqus-disable-mobile="false"' + + 'disqus-config-language="{{ post.lang }}"' + 'ready-to-bind="{{ loaded }}">' + ''; @@ -23,7 +24,8 @@ xdescribe('dirDiqus directive', function() { ID: 123, title: 'test title', link: 'http://www.test.com/', - catId: 999 + catId: 999, + lang: 'en' }; scope.loaded = false; @@ -50,6 +52,7 @@ xdescribe('dirDiqus directive', function() { expect(window.disqus_url).toBeFalsy(); expect(window.disqus_category_id).toBeFalsy(); expect(window.disqus_disable_mobile).toBeFalsy(); + expect(window.disqus_config();window.language).toBeFalsy(); }); it('should activate when ready to bind is true', function() { @@ -62,5 +65,6 @@ xdescribe('dirDiqus directive', function() { expect(window.disqus_url).toEqual('http://www.test.com/'); expect(window.disqus_category_id).toEqual('999'); expect(window.disqus_disable_mobile).toEqual('false'); + expect(window.disqus_config();window.language).toEqual('en'); }); -}); \ No newline at end of file +}); From 3dcc5c5b4a73e8cda87de065328952698d458799 Mon Sep 17 00:00:00 2001 From: ozdemirbur Date: Thu, 19 Mar 2015 23:25:36 +0200 Subject: [PATCH 23/99] dirDisqus: added multi-language support --- src/directives/disqus/dirDisqus.spec.js | 27 +++++++++++++------------ 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/src/directives/disqus/dirDisqus.spec.js b/src/directives/disqus/dirDisqus.spec.js index e99092e..9512337 100644 --- a/src/directives/disqus/dirDisqus.spec.js +++ b/src/directives/disqus/dirDisqus.spec.js @@ -1,4 +1,4 @@ -xdescribe('dirDiqus directive', function() { +xdescribe('dirDisqus directive', function() { var scope, elem, compiled, @@ -8,14 +8,14 @@ xdescribe('dirDiqus directive', function() { beforeEach(function (){ //set our view html. html = '' + - ''; + 'disqus-identifier="{{ post.ID }}"' + + 'disqus-title="{{ post.title }}"' + + 'disqus-url="{{ post.link }}"' + + 'disqus-category-id="{{ post.catId }}"' + + 'disqus-disable-mobile="false"' + + 'disqus-config-language="{{ post.lang }}"' + + 'ready-to-bind="{{ loaded }}">' + + ''; inject(function($compile, $rootScope) { //create a scope and populate it @@ -25,7 +25,7 @@ xdescribe('dirDiqus directive', function() { title: 'test title', link: 'http://www.test.com/', catId: 999, - lang: 'en' + lang: 'en' }; scope.loaded = false; @@ -52,7 +52,7 @@ xdescribe('dirDiqus directive', function() { expect(window.disqus_url).toBeFalsy(); expect(window.disqus_category_id).toBeFalsy(); expect(window.disqus_disable_mobile).toBeFalsy(); - expect(window.disqus_config();window.language).toBeFalsy(); + expect(window.language).toBeFalsy(); }); it('should activate when ready to bind is true', function() { @@ -65,6 +65,7 @@ xdescribe('dirDiqus directive', function() { expect(window.disqus_url).toEqual('http://www.test.com/'); expect(window.disqus_category_id).toEqual('999'); expect(window.disqus_disable_mobile).toEqual('false'); - expect(window.disqus_config();window.language).toEqual('en'); + window.disqus_config(); + expect(window.language).toEqual('en'); }); -}); +}); \ No newline at end of file From e915362d0d81f9a817061bc2ae9817250df8a43d Mon Sep 17 00:00:00 2001 From: Michael Bromley Date: Fri, 20 Mar 2015 09:31:48 +0100 Subject: [PATCH 24/99] dirDisqus: Update docs & tidy up. --- src/directives/disqus/README.md | 18 +++++++++++++++--- src/directives/disqus/dirDisqus.js | 2 +- src/directives/disqus/dirDisqus.spec.js | 5 +++++ 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/src/directives/disqus/README.md b/src/directives/disqus/README.md index 500b166..5f97e08 100644 --- a/src/directives/disqus/README.md +++ b/src/directives/disqus/README.md @@ -36,13 +36,25 @@ The attributes given above are all required. The inclusion of the identifier and If the identifier and URL and not included as attributes, the directive will throw an exception. +## Full API + You can optionally specify the other configuration variables by including the as attributes on the directive's element tag. For more information on the available config vars, see the [Disqus docs](http://help.disqus.com/customer/portal/articles/472098-javascript-configuration-variables). -Note that in the tag, the config attribute names are separated with a hyphen rather than an underscore (to make it look more HTML-like). - +```HTML + + +If using the `disqus-config-language` setting, please see [this Disqus article on multi-lingual websites](https://help.disqus.com/customer/portal/articles/466249-multi-lingual-websites) +for which languages are supported. ## `ready-to-bind` attribute @@ -75,4 +87,4 @@ function myController($scope, $http) { ``` If you omit the `ready-to-bind` attribute, the Disqus widget will be created immediately. This is okay so long as - rely on interpolated data which is not available on page load. \ No newline at end of file + you don't rely on interpolated data which is not available on page load. \ No newline at end of file diff --git a/src/directives/disqus/dirDisqus.js b/src/directives/disqus/dirDisqus.js index 9c09ec5..f5104b6 100644 --- a/src/directives/disqus/dirDisqus.js +++ b/src/directives/disqus/dirDisqus.js @@ -37,7 +37,7 @@ disqus_config_language : '@disqusConfigLanguage', readyToBind: "@" }, - template: '
', + template: '
comments powered by Disqus', link: function(scope) { // ensure that the disqus_identifier and disqus_url are both set, otherwise we will run in to identifier conflicts when using URLs with "#" in them diff --git a/src/directives/disqus/dirDisqus.spec.js b/src/directives/disqus/dirDisqus.spec.js index 9512337..9b9e585 100644 --- a/src/directives/disqus/dirDisqus.spec.js +++ b/src/directives/disqus/dirDisqus.spec.js @@ -1,3 +1,8 @@ +/** + * For some reason, when these tests are run along with all the others in this project, I get a "script error". Running + * them on their own using `ddescribe` works okay. Therefore this test is ignored in general unless specifically testing + * this directive, in which case change `xdescribe` to `ddescribe`. + */ xdescribe('dirDisqus directive', function() { var scope, elem, From 4ca5a9b4e3936b73bb7e6393ede59efe0d82f44f Mon Sep 17 00:00:00 2001 From: Michael Bromley Date: Fri, 20 Mar 2015 09:34:35 +0100 Subject: [PATCH 25/99] Fix formatting in readme --- src/directives/disqus/README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/directives/disqus/README.md b/src/directives/disqus/README.md index 5f97e08..7be369e 100644 --- a/src/directives/disqus/README.md +++ b/src/directives/disqus/README.md @@ -52,6 +52,7 @@ on the directive's element tag. For more information on the available config var disqus-config-language="{{ post.lang }}" ready-to-bind="{{ loaded }}"> +``` If using the `disqus-config-language` setting, please see [this Disqus article on multi-lingual websites](https://help.disqus.com/customer/portal/articles/466249-multi-lingual-websites) for which languages are supported. @@ -87,4 +88,4 @@ function myController($scope, $http) { ``` If you omit the `ready-to-bind` attribute, the Disqus widget will be created immediately. This is okay so long as - you don't rely on interpolated data which is not available on page load. \ No newline at end of file + you don't rely on interpolated data which is not available on page load. From 2eaf17a8eee9df103a20df11abf49e82e181e93e Mon Sep 17 00:00:00 2001 From: Michael Bromley Date: Fri, 20 Mar 2015 19:17:25 +0100 Subject: [PATCH 26/99] dirPagination: Fix https://github.com/michaelbromley/angularUtils/issues/92 Completely new method of handling multi-element usage, allowing behaviour to correctly mirror ng-repeat. Also significant refactoring to improve readability. --- src/directives/pagination/dirPagination.js | 501 +++++++++++------- .../pagination/dirPagination.spec.js | 6 +- 2 files changed, 303 insertions(+), 204 deletions(-) diff --git a/src/directives/pagination/dirPagination.js b/src/directives/pagination/dirPagination.js index 5aa6efb..552d783 100644 --- a/src/directives/pagination/dirPagination.js +++ b/src/directives/pagination/dirPagination.js @@ -33,97 +33,302 @@ module = angular.module(moduleName, []); } - module.directive('dirPaginate', ['$compile', '$parse', 'paginationService', function($compile, $parse, paginationService) { + module + .directive('dirPaginate', ['$compile', '$parse', 'paginationService', dirPaginateDirective]) + .directive('dirPaginateNoCompile', noCompileDirective) + .directive('dirPaginationControls', ['paginationService', 'paginationTemplate', dirPaginationControlsDirective]) + .filter('itemsPerPage', ['paginationService', itemsPerPageFilter]) + .service('paginationService', paginationService) + .provider('paginationTemplate', paginationTemplateProvider); + + function dirPaginateDirective($compile, $parse, paginationService) { return { terminal: true, multiElement: true, - priority: 5000, // This setting is used in conjunction with the later call to $compile() to prevent infinite recursion of compilation - compile: function dirPaginationCompileFn(tElement, tAttrs){ + compile: dirPaginationCompileFn + }; + + function dirPaginationCompileFn(tElement, tAttrs){ + + var expression = tAttrs.dirPaginate; + // regex taken directly from https://github.com/angular/angular.js/blob/master/src/ng/directive/ngRepeat.js#L211 + var match = expression.match(/^\s*([\s\S]+?)\s+in\s+([\s\S]+?)(?:\s+track\s+by\s+([\s\S]+?))?\s*$/); + + var filterPattern = /\|\s*itemsPerPage\s*:[^|]*/; + if (match[2].match(filterPattern) === null) { + throw 'pagination directive: the \'itemsPerPage\' filter must be set.'; + } + var itemsPerPageFilterRemoved = match[2].replace(filterPattern, ''); + var collectionGetter = $parse(itemsPerPageFilterRemoved); + + addNoCompileAttributes(tElement); + + // If any value is specified for paginationId, we register the un-evaluated expression at this stage for the benefit of any + // dir-pagination-controls directives that may be looking for this ID. + var rawId = tAttrs.paginationId || DEFAULT_ID; + paginationService.registerInstance(rawId); + + return function dirPaginationLinkFn(scope, element, attrs){ + + // Now that we have access to the `scope` we can interpolate any expression given in the paginationId attribute and + // potentially register a new ID if it evaluates to a different value than the rawId. + var paginationId = $parse(attrs.paginationId)(scope) || attrs.paginationId || DEFAULT_ID; + paginationService.registerInstance(paginationId); - var expression = tAttrs.dirPaginate; - // regex taken directly from https://github.com/angular/angular.js/blob/master/src/ng/directive/ngRepeat.js#L211 - var match = expression.match(/^\s*([\s\S]+?)\s+in\s+([\s\S]+?)(?:\s+track\s+by\s+([\s\S]+?))?\s*$/); + var repeatExpression = getRepeatExpression(expression, paginationId); + addNgRepeatToElement(element, attrs, repeatExpression); - var filterPattern = /\|\s*itemsPerPage\s*:[^|]*/; - if (match[2].match(filterPattern) === null) { - throw 'pagination directive: the \'itemsPerPage\' filter must be set.'; + removeTemporaryAttributes(element); + var compiled = $compile(element); + + var currentPageGetter = makeCurrentPageGetterFn(scope, attrs, paginationId); + paginationService.setCurrentPageParser(paginationId, currentPageGetter, scope); + + if (typeof attrs.totalItems !== 'undefined') { + paginationService.setAsyncModeTrue(paginationId); + scope.$watch(function() { + return $parse(attrs.totalItems)(scope); + }, function (result) { + if (0 <= result) { + paginationService.setCollectionLength(paginationId, result); + } + }); + } else { + scope.$watchCollection(function() { + return collectionGetter(scope); + }, function(collection) { + if (collection) { + paginationService.setCollectionLength(paginationId, collection.length); + } + }); } - var itemsPerPageFilterRemoved = match[2].replace(filterPattern, ''); - var collectionGetter = $parse(itemsPerPageFilterRemoved); - - // If any value is specified for paginationId, we register the un-evaluated expression at this stage for the benefit of any - // dir-pagination-controls directives that may be looking for this ID. - var rawId = tAttrs.paginationId || DEFAULT_ID; - paginationService.registerInstance(rawId); - - return function dirPaginationLinkFn(scope, element, attrs){ - - // Now that we have access to the `scope` we can interpolate any expression given in the paginationId attribute and - // potentially register a new ID if it evaluates to a different value than the rawId. - var paginationId = $parse(attrs.paginationId)(scope) || attrs.paginationId || DEFAULT_ID; - paginationService.registerInstance(paginationId); - - var repeatExpression; - var idDefinedInFilter = !!expression.match(/(\|\s*itemsPerPage\s*:[^|]*:[^|]*)/); - if (paginationId !== DEFAULT_ID && !idDefinedInFilter) { - repeatExpression = expression.replace(/(\|\s*itemsPerPage\s*:[^|]*)/, "$1 : '" + paginationId + "'"); - } else { - repeatExpression = expression; - } - // Add ng-repeat to the dom element - if (element[0].hasAttribute('dir-paginate-start') || element[0].hasAttribute('data-dir-paginate-start')) { - // using multiElement mode (dir-paginate-start, dir-paginate-end) - attrs.$set('ngRepeatStart', repeatExpression); - element.eq(element.length - 1).attr('ng-repeat-end', true); - } else { - attrs.$set('ngRepeat', repeatExpression); - } + // Delegate to the link function returned by the new compilation of the ng-repeat + compiled(scope); + }; + } + + /** + * If a pagination id has been specified, we need to check that it is present as the second argument passed to + * the itemsPerPage filter. If it is not there, we add it and return the modified expression. + * + * @param expression + * @param paginationId + * @returns {*} + */ + function getRepeatExpression(expression, paginationId) { + var repeatExpression, + idDefinedInFilter = !!expression.match(/(\|\s*itemsPerPage\s*:[^|]*:[^|]*)/); - var compiled = $compile(element, false, 5000); // we manually compile the element again, as we have now added ng-repeat. Priority less than 5000 prevents infinite recursion of compiling dirPaginate + if (paginationId !== DEFAULT_ID && !idDefinedInFilter) { + repeatExpression = expression.replace(/(\|\s*itemsPerPage\s*:[^|]*)/, "$1 : '" + paginationId + "'"); + } else { + repeatExpression = expression; + } - var currentPageGetter; - if (attrs.currentPage) { - currentPageGetter = $parse(attrs.currentPage); - } else { - // if the current-page attribute was not set, we'll make our own - var defaultCurrentPage = paginationId + '__currentPage'; - scope[defaultCurrentPage] = 1; - currentPageGetter = $parse(defaultCurrentPage); - } - paginationService.setCurrentPageParser(paginationId, currentPageGetter, scope); - - if (typeof attrs.totalItems !== 'undefined') { - paginationService.setAsyncModeTrue(paginationId); - scope.$watch(function() { - return $parse(attrs.totalItems)(scope); - }, function (result) { - if (0 <= result) { - paginationService.setCollectionLength(paginationId, result); - } - }); - } else { - scope.$watchCollection(function() { - return collectionGetter(scope); - }, function(collection) { - if (collection) { - paginationService.setCollectionLength(paginationId, collection.length); - } - }); - } + return repeatExpression; + } - // Delegate to the link function returned by the new compilation of the ng-repeat - compiled(scope); - }; + /** + * Adds the ng-repeat directive to the element. In the case of multi-element (-start, -end) it adds the + * appropriate multi-element ng-repeat to the first and last element in the range. + * @param element + * @param attrs + * @param repeatExpression + */ + function addNgRepeatToElement(element, attrs, repeatExpression) { + if (element[0].hasAttribute('dir-paginate-start') || element[0].hasAttribute('data-dir-paginate-start')) { + // using multiElement mode (dir-paginate-start, dir-paginate-end) + attrs.$set('ngRepeatStart', repeatExpression); + element.eq(element.length - 1).attr('ng-repeat-end', true); + } else { + attrs.$set('ngRepeat', repeatExpression); } - }; - }]); + } - module.directive('dirPaginationControls', ['paginationService', 'paginationTemplate', function(paginationService, paginationTemplate) { + /** + * Adds the dir-paginate-no-compile directive to each element in the tElement range. + * @param tElement + */ + function addNoCompileAttributes(tElement) { + angular.forEach(tElement, function(el) { + if (el.nodeType === Node.ELEMENT_NODE) { + angular.element(el).attr('dir-paginate-no-compile', true); + } + }); + } + + /** + * Removes the variations on dir-paginate (data-, -start, -end) and the dir-paginate-no-compile directives. + * @param element + */ + function removeTemporaryAttributes(element) { + angular.forEach(element, function(el) { + if (el.nodeType === Node.ELEMENT_NODE) { + angular.element(el).removeAttr('dir-paginate-no-compile'); + } + }); + element.eq(0).removeAttr('dir-paginate-start').removeAttr('dir-paginate').removeAttr('data-dir-paginate-start').removeAttr('data-dir-paginate'); + element.eq(element.length - 1).removeAttr('dir-paginate-end').removeAttr('data-dir-paginate-end'); + } + + /** + * Creates a getter function for the current-page attribute, using the expression provided or a default value if + * no current-page expression was specified. + * + * @param scope + * @param attrs + * @param paginationId + * @returns {*} + */ + function makeCurrentPageGetterFn(scope, attrs, paginationId) { + var currentPageGetter; + if (attrs.currentPage) { + currentPageGetter = $parse(attrs.currentPage); + } else { + // if the current-page attribute was not set, we'll make our own + var defaultCurrentPage = paginationId + '__currentPage'; + scope[defaultCurrentPage] = 1; + currentPageGetter = $parse(defaultCurrentPage); + } + return currentPageGetter; + } + } + + /** + * This is a helper directive that allows correct compilation when in multi-element mode (ie dir-paginate-start, dir-paginate-end). + * It is dynamically added to all elements in the dir-paginate compile function, and it prevents further compilation of + * any inner directives. It is then removed in the link function, and all inner directives are then manually compiled. + */ + function noCompileDirective() { + return { + priority: 5000, + terminal: true + }; + } + + function dirPaginationControlsDirective(paginationService, paginationTemplate) { var numberRegex = /^\d+$/; + return { + restrict: 'AE', + templateUrl: function(elem, attrs) { + return attrs.templateUrl || paginationTemplate.getPath(); + }, + scope: { + maxSize: '=?', + onPageChange: '&?', + paginationId: '=?' + }, + link: dirPaginationControlsLinkFn + }; + + function dirPaginationControlsLinkFn(scope, element, attrs) { + + // rawId is the un-interpolated value of the pagination-id attribute. This is only important when the corresponding dir-paginate directive has + // not yet been linked (e.g. if it is inside an ng-if block), and in that case it prevents this controls directive from assuming that there is + // no corresponding dir-paginate directive and wrongly throwing an exception. + var rawId = attrs.paginationId || DEFAULT_ID; + var paginationId = scope.paginationId || attrs.paginationId || DEFAULT_ID; + + if (!paginationService.isRegistered(paginationId) && !paginationService.isRegistered(rawId)) { + var idMessage = (paginationId !== DEFAULT_ID) ? ' (id: ' + paginationId + ') ' : ' '; + throw 'pagination directive: the pagination controls' + idMessage + 'cannot be used without the corresponding pagination directive.'; + } + + if (!scope.maxSize) { scope.maxSize = 9; } + scope.directionLinks = angular.isDefined(attrs.directionLinks) ? scope.$parent.$eval(attrs.directionLinks) : true; + scope.boundaryLinks = angular.isDefined(attrs.boundaryLinks) ? scope.$parent.$eval(attrs.boundaryLinks) : false; + + var paginationRange = Math.max(scope.maxSize, 5); + scope.pages = []; + scope.pagination = { + last: 1, + current: 1 + }; + scope.range = { + lower: 1, + upper: 1, + total: 1 + }; + + scope.$watch(function() { + return (paginationService.getCollectionLength(paginationId) + 1) * paginationService.getItemsPerPage(paginationId); + }, function(length) { + if (0 < length) { + generatePagination(); + } + }); + + scope.$watch(function() { + return (paginationService.getItemsPerPage(paginationId)); + }, function(current, previous) { + if (current != previous && typeof previous !== 'undefined') { + goToPage(scope.pagination.current); + } + }); + + scope.$watch(function() { + return paginationService.getCurrentPage(paginationId); + }, function(currentPage, previousPage) { + if (currentPage != previousPage) { + goToPage(currentPage); + } + }); + + scope.setCurrent = function(num) { + if (isValidPageNumber(num)) { + paginationService.setCurrentPage(paginationId, num); + } + }; + + function goToPage(num) { + if (isValidPageNumber(num)) { + scope.pages = generatePagesArray(num, paginationService.getCollectionLength(paginationId), paginationService.getItemsPerPage(paginationId), paginationRange); + scope.pagination.current = num; + updateRangeValues(); + + // if a callback has been set, then call it with the page number as an argument + if (scope.onPageChange) { + scope.onPageChange({ newPageNumber : num }); + } + } + } + + function generatePagination() { + var page = parseInt(paginationService.getCurrentPage(paginationId)) || 1; + + scope.pages = generatePagesArray(page, paginationService.getCollectionLength(paginationId), paginationService.getItemsPerPage(paginationId), paginationRange); + scope.pagination.current = page; + scope.pagination.last = scope.pages[scope.pages.length - 1]; + if (scope.pagination.last < scope.pagination.current) { + scope.setCurrent(scope.pagination.last); + } else { + updateRangeValues(); + } + } + + /** + * This function updates the values (lower, upper, total) of the `scope.range` object, which can be used in the pagination + * template to display the current page range, e.g. "showing 21 - 40 of 144 results"; + */ + function updateRangeValues() { + var currentPage = paginationService.getCurrentPage(paginationId), + itemsPerPage = paginationService.getItemsPerPage(paginationId), + totalItems = paginationService.getCollectionLength(paginationId); + + scope.range.lower = (currentPage - 1) * itemsPerPage + 1; + scope.range.upper = Math.min(currentPage * itemsPerPage, totalItems); + scope.range.total = totalItems; + } + + function isValidPageNumber(num) { + return (numberRegex.test(num) && (0 < num && num <= scope.pagination.last)); + } + } + /** * Generate an array of page numbers (or the '...' string) which is used in an ng-repeat to generate the * links used in pagination @@ -192,124 +397,14 @@ return i; } } + } - return { - restrict: 'AE', - templateUrl: function(elem, attrs) { - return attrs.templateUrl || paginationTemplate.getPath(); - }, - scope: { - maxSize: '=?', - onPageChange: '&?', - paginationId: '=?' - }, - link: function dirPaginationControlsLinkFn(scope, element, attrs) { - - // rawId is the un-interpolated value of the pagination-id attribute. This is only important when the corresponding dir-paginate directive has - // not yet been linked (e.g. if it is inside an ng-if block), and in that case it prevents this controls directive from assuming that there is - // no corresponding dir-paginate directive and wrongly throwing an exception. - var rawId = attrs.paginationId || DEFAULT_ID; - var paginationId = scope.paginationId || attrs.paginationId || DEFAULT_ID; - - if (!paginationService.isRegistered(paginationId) && !paginationService.isRegistered(rawId)) { - var idMessage = (paginationId !== DEFAULT_ID) ? ' (id: ' + paginationId + ') ' : ' '; - throw 'pagination directive: the pagination controls' + idMessage + 'cannot be used without the corresponding pagination directive.'; - } - - if (!scope.maxSize) { scope.maxSize = 9; } - scope.directionLinks = angular.isDefined(attrs.directionLinks) ? scope.$parent.$eval(attrs.directionLinks) : true; - scope.boundaryLinks = angular.isDefined(attrs.boundaryLinks) ? scope.$parent.$eval(attrs.boundaryLinks) : false; - - var paginationRange = Math.max(scope.maxSize, 5); - scope.pages = []; - scope.pagination = { - last: 1, - current: 1 - }; - scope.range = { - lower: 1, - upper: 1, - total: 1 - }; - - scope.$watch(function() { - return (paginationService.getCollectionLength(paginationId) + 1) * paginationService.getItemsPerPage(paginationId); - }, function(length) { - if (0 < length) { - generatePagination(); - } - }); - - scope.$watch(function() { - return (paginationService.getItemsPerPage(paginationId)); - }, function(current, previous) { - if (current != previous && typeof previous !== 'undefined') { - goToPage(scope.pagination.current); - } - }); - - scope.$watch(function() { - return paginationService.getCurrentPage(paginationId); - }, function(currentPage, previousPage) { - if (currentPage != previousPage) { - goToPage(currentPage); - } - }); - - scope.setCurrent = function(num) { - if (isValidPageNumber(num)) { - paginationService.setCurrentPage(paginationId, num); - } - }; - - function goToPage(num) { - if (isValidPageNumber(num)) { - scope.pages = generatePagesArray(num, paginationService.getCollectionLength(paginationId), paginationService.getItemsPerPage(paginationId), paginationRange); - scope.pagination.current = num; - updateRangeValues(); - - // if a callback has been set, then call it with the page number as an argument - if (scope.onPageChange) { - scope.onPageChange({ newPageNumber : num }); - } - } - } - - function generatePagination() { - var page = parseInt(paginationService.getCurrentPage(paginationId)) || 1; - - scope.pages = generatePagesArray(page, paginationService.getCollectionLength(paginationId), paginationService.getItemsPerPage(paginationId), paginationRange); - scope.pagination.current = page; - scope.pagination.last = scope.pages[scope.pages.length - 1]; - if (scope.pagination.last < scope.pagination.current) { - scope.setCurrent(scope.pagination.last); - } else { - updateRangeValues(); - } - } - - /** - * This function updates the values (lower, upper, total) of the `scope.range` object, which can be used in the pagination - * template to display the current page range, e.g. "showing 21 - 40 of 144 results"; - */ - function updateRangeValues() { - var currentPage = paginationService.getCurrentPage(paginationId), - itemsPerPage = paginationService.getItemsPerPage(paginationId), - totalItems = paginationService.getCollectionLength(paginationId); - - scope.range.lower = (currentPage - 1) * itemsPerPage + 1; - scope.range.upper = Math.min(currentPage * itemsPerPage, totalItems); - scope.range.total = totalItems; - } - - function isValidPageNumber(num) { - return (numberRegex.test(num) && (0 < num && num <= scope.pagination.last)); - } - } - }; - }]); - - module.filter('itemsPerPage', ['paginationService', function(paginationService) { + /** + * This filter slices the collection into pages based on the current page number and number of items per page. + * @param paginationService + * @returns {Function} + */ + function itemsPerPageFilter(paginationService) { return function(collection, itemsPerPage, paginationId) { if (typeof (paginationId) === 'undefined') { @@ -335,9 +430,12 @@ return collection; } }; - }]); + } - module.service('paginationService', function() { + /** + * This service allows the various parts of the module to communicate and stay in sync. + */ + function paginationService() { var instances = {}; var lastRegisteredInstance; @@ -392,16 +490,19 @@ this.isAsyncMode = function(instanceId) { return instances[instanceId].asyncMode; }; - }); - - module.provider('paginationTemplate', function() { + } + + /** + * This provider allows global configuration of the template path used by the dir-pagination-controls directive. + */ + function paginationTemplateProvider() { var templatePath = 'directives/pagination/dirPagination.tpl.html'; - + this.setPath = function(path) { templatePath = path; }; - + this.$get = function() { return { getPath: function() { @@ -409,5 +510,5 @@ } }; }; - }); + } })(); \ No newline at end of file diff --git a/src/directives/pagination/dirPagination.spec.js b/src/directives/pagination/dirPagination.spec.js index cf935f2..5b60840 100644 --- a/src/directives/pagination/dirPagination.spec.js +++ b/src/directives/pagination/dirPagination.spec.js @@ -754,7 +754,7 @@ describe('dirPagination directive', function() { /** * See https://github.com/michaelbromley/angularUtils/issues/92 */ - xit('should correctly compile an inner ng-repeat', function() { + it('should correctly compile an inner ng-repeat', function() { function compile() { var html = '
' + '

{{ item }}

' + @@ -768,12 +768,10 @@ describe('dirPagination directive', function() { } compile(); - $timeout.flush(); - - console.log(containingElement.html()); var options = containingElement.find('.options').eq(0).find('li'); expect(options.length).toEqual(3); + expect(options.eq(0).text()).toEqual('option1 : item 1'); }); }); From 33a733df1d0aa8d01872beb905026039b2828139 Mon Sep 17 00:00:00 2001 From: Michael Bromley Date: Fri, 20 Mar 2015 19:48:04 +0100 Subject: [PATCH 27/99] Update readme --- src/directives/pagination/README.md | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/directives/pagination/README.md b/src/directives/pagination/README.md index dbc6bf6..3f4d33e 100644 --- a/src/directives/pagination/README.md +++ b/src/directives/pagination/README.md @@ -202,11 +202,6 @@ repeat a series of elements instead of just one parent element:
``` -**Note:** - This feature is currently a little buggy. E.g. attempting to next an ngRepeat in the `