diff --git a/.gitignore b/.gitignore
index e2a8d56..e099741 100644
--- a/.gitignore
+++ b/.gitignore
@@ -218,3 +218,11 @@ pip-log.txt
.idea/
node_modules/
vendor/
+
+
+#############
+## Sublime Text
+#############
+
+*.sublime-project
+*.sublime-workspace
diff --git a/ISSUE_TEMPLATE.md b/ISSUE_TEMPLATE.md
new file mode 100644
index 0000000..37c5a1a
--- /dev/null
+++ b/ISSUE_TEMPLATE.md
@@ -0,0 +1,28 @@
+Hi, thanks for contributing!
+
+This project is maintained in my spare time, so in order to help me address your issue as quickly as
+possible, please provide as much of the following information as you can:
+
+- Title: Please indicate the module in question, e.g. "dirPagination: X is broken when Y"
+- If reporting on dirPagination, please include the version you are using (can be found in the package.json / bower.json file)
+- If you are able to reproduce your issue on Plunker, this will vastly increase the chances of a rapid response from me.
+
+-- Michael
+
+=======
+(Delete the above. Fill in the rest as applicable)
+
+**Description of issue**:
+
+**Steps to reproduce**:
+
+**Expected result**:
+
+**Actual result**:
+
+**Demo**: (for dirPagination, fork and modify this Plunk: http://plnkr.co/edit/b37IdFFJUokaeSummETX?p=preview)
+
+Any relevant code:
+```
+
+```
diff --git a/README.md b/README.md
index b8338d9..ac0d61e 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,10 @@
# Angular Utilities
+### No longer maintained
+(20/04/2017) - I am no longer actively maintaining this project. I no longer use AngularJS in my own projects and do not have the time to dedicate to maintiaining this project as well as my other active open source projects. Thank you for your understanding.
+
+---
+
I am working on a large-scale AngularJS-based project and I'll be extracting
any useful re-usable components that I make and putting them here.
@@ -33,4 +38,4 @@ This code is made available under the MIT license, so feel free to use any of th
## License
-MIT
\ No newline at end of file
+MIT
diff --git a/bower.json b/bower.json
index 5ce84f3..f7567f1 100644
--- a/bower.json
+++ b/bower.json
@@ -18,9 +18,9 @@
],
"dependencies": {
"angular-ui-router": "~0.2.13",
- "angular": "~1.2.24"
+ "angular": "~1.4.0"
},
"devDependencies": {
- "angular-mocks": "~1.2.24"
+ "angular-mocks": "~1.4.0"
}
}
diff --git a/package.json b/package.json
index 8f281ff..4c78ee3 100644
--- a/package.json
+++ b/package.json
@@ -6,6 +6,7 @@
"author": "Michael Bromley",
"license": "MIT",
"devDependencies": {
+ "karma": "~0.12.0",
"bower": "^1.3.1",
"grunt": "~0.4.2",
"grunt-contrib-copy": "^0.5.0",
diff --git a/src/directives/disqus/README.md b/src/directives/disqus/README.md
index 500b166..e386a83 100644
--- a/src/directives/disqus/README.md
+++ b/src/directives/disqus/README.md
@@ -1,5 +1,11 @@
# Disqus Directive
+### No longer maintained
+(20/04/2017) - I am no longer actively maintaining this project. I no longer use AngularJS in my own projects and do not have the time to dedicate to maintiaining this project as well as my other active open source projects. Thank you for your understanding.
+
+---
+
+
A directive to embed a Disqus comments widget on your AngularJS page.
## Prerequisites
@@ -15,7 +21,9 @@ 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`
+1. Download the file `dirDisqus.js` or:
+ * via Bower: `bower install angular-utils-disqus`
+ * via npm: `npm 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.
@@ -25,54 +33,54 @@ First, put the directive code in your app, wherever you store your directives.
Wherever you want the Disqus comments to appear, add the following to your template:
-```html
-
-
+```
+
+```
+
+And in your controller:
+
+```
+$scope.disqusConfig = {
+ disqus_shortname: 'Your disqus shortname',
+ disqus_identifier: 'Comments identifier',
+ disqus_url: 'Comments url'
+};
```
The attributes given above are all required. The inclusion of the identifier and URL ensure that identifier conflicts will not occur. See http://help.disqus.com/customer/portal/articles/662547-why-are-the-same-comments-showing-up-on-multiple-pages-
-If the identifier and URL and not included as attributes, the directive will throw an exception.
+If the identifier and URL and not included as attributes, the directive will not appear.
+
+## 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).
-
-
-
-## `ready-to-bind` attribute
+```
+$scope.disqusConfig = {
+ disqus_shortname: 'Your disqus shortname',
+ disqus_identifier: 'Comments identifier',
+ disqus_url: 'Comments url',
+ disqus_title: 'Comments title',
+ disqus_category_id: 'Comments category id }}',
+ disqus_disable_mobile: 'false',
+ disqus_config_language: 'Comments language',
+ disqus_remote_auth_s3: 'remote_auth_s3',
+ disqus_api_key: 'public_api_key',
+ disqus_on_ready: ready()
+};
+```
-If you are loading the page asynchronously, the model data (`$scope.article` in the above example) used to populate the config variables above
-will probably not be defined at the time the page is loaded. This will result in your config settings
-being all undefined. To get around this, you can specify a scope variable that should be set to (or evaluate to) `false`
-until your data is loaded, at which point you can set it to `true`. The directive watches this property and once it changes
-to `true`, any config attributes which are bound to your model should be available and used to load up the Disqus widget.
+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.
-For example:
+## `disqus-remote-auth-s3 and disqus-api-key` attributes for SSO
+If using the `disqus-remote-auth-s3 and disqus-api-key` setting, please see [Integrating Single Sign-On](https://help.disqus.com/customer/portal/articles/236206#sso-script)
+to know how to generate a remote_auth_s3 and public_api_key.
-```JavaScript
-// simple example of controller loading async data
-function myController($scope, $http) {
- $scope.contentLoaded = false;
+Note: Single Sign-on (SSO) allows users to sign into a site and be able to use Disqus Comments without having to re-authenticate Disqus. SSO will create a site-specific user profile on Disqus, in a way that will prevent conflict with existing Disqus users.
- $http.get('api/article/1').then(function(result) {
- $scope.article = result.article;
- $scope.contentLoaded = true; // this tells the directive that it should load the Disqus widget now
- })
-}
-```
-```html
-// in your view code
-
-
-```
+## `disqus-on-ready` attribute
- 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
+If Disqus is rendered, `disqus-on-ready` function will be called. Callback is registered to disqus by similar technique as explained in [this post](https://help.disqus.com/customer/portal/articles/466258-capturing-disqus-commenting-activity-via-callbacks).
diff --git a/src/directives/disqus/dirDisqus.js b/src/directives/disqus/dirDisqus.js
index 034c313..ff0f182 100644
--- a/src/directives/disqus/dirDisqus.js
+++ b/src/directives/disqus/dirDisqus.js
@@ -1,12 +1,13 @@
-/**
+/**
* A directive to embed a Disqus comments widget on your AngularJS page.
*
* Created by Michael on 22/01/14.
+ * Modified by Serkan "coni2k" Holat on 24/02/16.
* Copyright Michael Bromley 2014
* Available under the MIT license.
*/
-(function() {
+(function () {
/**
* Config
@@ -19,67 +20,69 @@
var module;
try {
module = angular.module(moduleName);
- } catch(err) {
+ } catch (err) {
// named module does not exist, so create one
module = angular.module(moduleName, []);
}
- module.directive('dirDisqus', ['$window', function($window) {
+ module.directive('dirDisqus', ['$window', function ($window) {
return {
restrict: 'E',
scope: {
- disqus_shortname: '@disqusShortname',
- disqus_identifier: '@disqusIdentifier',
- disqus_title: '@disqusTitle',
- disqus_url: '@disqusUrl',
- disqus_category_id: '@disqusCategoryId',
- disqus_disable_mobile: '@disqusDisableMobile',
- readyToBind: "@"
+ config: '='
},
- template: '
comments powered by ',
- link: function(scope) {
+ 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
- // see http://help.disqus.com/customer/portal/articles/662547-why-are-the-same-comments-showing-up-on-multiple-pages-
- if (typeof scope.disqus_identifier === 'undefined' || typeof scope.disqus_url === 'undefined') {
- throw "Please ensure that the `disqus-identifier` and `disqus-url` attributes are both set.";
- }
+ scope.$watch('config', configChanged, true);
- scope.$watch("readyToBind", function(isReady) {
+ function configChanged() {
- // If the directive has been called without the 'ready-to-bind' attribute, we
- // set the default to "true" so that Disqus will be loaded straight away.
- if ( !angular.isDefined( isReady ) ) {
- isReady = "true";
+ // 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
+ // see http://help.disqus.com/customer/portal/articles/662547-why-are-the-same-comments-showing-up-on-multiple-pages-
+ if (!scope.config.disqus_shortname ||
+ !scope.config.disqus_identifier ||
+ !scope.config.disqus_url) {
+ return;
}
- if (scope.$eval(isReady)) {
- // put the config variables into separate global vars so that the Disqus script can see them
- $window.disqus_shortname = scope.disqus_shortname;
- $window.disqus_identifier = scope.disqus_identifier;
- $window.disqus_title = scope.disqus_title;
- $window.disqus_url = scope.disqus_url;
- $window.disqus_category_id = scope.disqus_category_id;
- $window.disqus_disable_mobile = scope.disqus_disable_mobile;
- // 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;
- dsq.src = '//' + scope.disqus_shortname + '.disqus.com/embed.js';
- (document.getElementsByTagName('head')[0] || document.getElementsByTagName('body')[0]).appendChild(dsq);
- } else {
- $window.DISQUS.reset({
- reload: true,
- config: function () {
- this.page.identifier = scope.disqus_identifier;
- this.page.url = scope.disqus_url;
- this.page.title = scope.disqus_title;
- }
- });
+ $window.disqus_shortname = scope.config.disqus_shortname;
+ $window.disqus_identifier = scope.config.disqus_identifier;
+ $window.disqus_url = scope.config.disqus_url;
+ $window.disqus_title = scope.config.disqus_title;
+ $window.disqus_category_id = scope.config.disqus_category_id;
+ $window.disqus_disable_mobile = scope.config.disqus_disable_mobile;
+ $window.disqus_config = function () {
+ this.language = scope.config.disqus_config_language;
+ this.page.remote_auth_s3 = scope.config.disqus_remote_auth_s3;
+ this.page.api_key = scope.config.disqus_api_key;
+ if (scope.config.disqus_on_ready) {
+ this.callbacks.onReady = [function () {
+ scope.config.disqus_on_ready();
+ }];
}
+ };
+
+ // 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;
+ dsq.src = '//' + scope.config.disqus_shortname + '.disqus.com/embed.js';
+ (document.getElementsByTagName('head')[0] || document.getElementsByTagName('body')[0]).appendChild(dsq);
+ } else {
+ $window.DISQUS.reset({
+ reload: true,
+ config: function () {
+ this.page.identifier = scope.config.disqus_identifier;
+ this.page.url = scope.config.disqus_url;
+ this.page.title = scope.config.disqus_title;
+ this.language = scope.config.disqus_config_language;
+ this.page.remote_auth_s3 = scope.config.disqus_remote_auth_s3;
+ this.page.api_key = scope.config.disqus_api_key;
+ }
+ });
}
- });
+ }
}
};
}]);
-
})();
diff --git a/src/directives/disqus/dirDisqus.spec.js b/src/directives/disqus/dirDisqus.spec.js
index 8acc174..b6ce512 100644
--- a/src/directives/disqus/dirDisqus.spec.js
+++ b/src/directives/disqus/dirDisqus.spec.js
@@ -1,4 +1,9 @@
-xdescribe('dirDiqus directive', function() {
+/**
+ * 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,
compiled,
@@ -8,13 +13,15 @@ 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 }}"' +
+ 'disqus-on-ready="ready()"' +
+ 'ready-to-bind="{{ loaded }}">' +
+ '';
inject(function($compile, $rootScope) {
//create a scope and populate it
@@ -23,9 +30,14 @@ xdescribe('dirDiqus directive', function() {
ID: 123,
title: 'test title',
link: 'http://www.test.com/',
- catId: 999
+ catId: 999,
+ lang: 'en'
};
scope.loaded = false;
+ scope.readyCalled = false;
+ scope.ready = function() {
+ scope.readyCalled = true;
+ };
//get the jqLite or jQuery element
elem = angular.element(html);
@@ -34,14 +46,20 @@ xdescribe('dirDiqus directive', function() {
compiled = $compile(elem);
//run the compiled view.
- compiled(scope);
+ var element = compiled(scope);
+
+ var div = document.createElement("div");
+ div.innerHTML = element.html();
- //call digest on the scope!
- scope.$digest();
+ // Just add disqus to document - it is needed to work embed.js properly
+ document.getElementsByTagName('body')[0].appendChild(div);
});
});
it('should not do anything when ready to bind is false', function() {
+ //call digest on the scope!
+ scope.$digest();
+
expect(elem.find("#disqus_thread")).toBeTruthy();
expect($("script[src='http://shortname.disqus.com/embed.js']").length).toEqual(0);
expect(window.disqus_shortname).toBeFalsy();
@@ -50,6 +68,8 @@ xdescribe('dirDiqus directive', function() {
expect(window.disqus_url).toBeFalsy();
expect(window.disqus_category_id).toBeFalsy();
expect(window.disqus_disable_mobile).toBeFalsy();
+ expect(scope.readyCalled).toBeFalsy();
+ expect(window.language).toBeFalsy();
});
it('should activate when ready to bind is true', function() {
@@ -62,5 +82,16 @@ 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');
+
+ window.page = {};
+ window.callbacks = {};
+ window.disqus_config();
+
+ expect(window.language).toEqual('en');
+ expect(window.callbacks.onReady).toBeDefined();
+ expect(window.callbacks.onReady.length).toEqual(1);
+ window.callbacks.onReady[0]();
+ expect(scope.readyCalled).toBeTruthy();
+
});
});
\ No newline at end of file
diff --git a/src/directives/pagination/README.md b/src/directives/pagination/README.md
index 648e369..4fd209f 100644
--- a/src/directives/pagination/README.md
+++ b/src/directives/pagination/README.md
@@ -1,11 +1,18 @@
# Pagination Directive
+### No longer maintained
+(20/04/2017) - I am no longer actively maintaining this project. I no longer use AngularJS in my own projects and do not have the time to dedicate to maintiaining this project as well as my other active open source projects. Thank you for your understanding.
+
+---
+
## Another one?
Yes, there are quite a few pagination solutions for Angular out there already, but what I wanted to do was make
something that would be truly plug-n-play - no need to do any set-up or logic in your controller. Just add
an attribute, drop in your navigation wherever you like, and boom - instant, full-featured pagination.
+(**Looking for the Angular2 version? [Right here!](https://github.com/michaelbromley/ng2-pagination)**)
+
## Demo
[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
@@ -16,7 +23,7 @@ filtering of the collection.
- [Basic Example](#basic-example)
- [Installation](#installation)
- [Usage](#usage)
- - [Specifying The Template](#specifying-the-template)
+ - [Specifying The Template](#customising--specifying-the-template)
- [Directives API](#directives-api)
- [dir-paginate](#dir-paginate)
- [dir-pagination-controls](#dir-pagination-controls)
@@ -28,7 +35,9 @@ filtering of the collection.
- [Working With Asynchronous Data](#working-with-asynchronous-data)
- [Example Asynchronous Setup](#example-asynchronous-setup)
- [Styling](#styling)
+- [FAQ](#frequently-asked-questions)
- [Contribution](#contribution)
+- [Changelog](#changelog)
- [Credits](#credits)
## Basic Example
@@ -55,7 +64,11 @@ You can install with Bower:
`bower install angular-utils-pagination`
-Alternatively just download the files `dirPagination.js` and `dirPagination.tpl.html`. Using bower has the advantage of making version management easier.
+or npm:
+
+`npm install angular-utils-pagination`
+
+Alternatively just download the files `dirPagination.js` and `dirPagination.tpl.html`. Using Bower or npm has the advantage of making version management easier.
## Usage
@@ -87,15 +100,25 @@ And finally include the pagination itself.
[boundary-links=""]
[on-page-change=""]
[pagination-id=""]
- [template-url=""]>
+ [template-url=""]
+ [auto-hide=""]>
```
-### Specifying The Template
+### Customising & Specifying The Template
-There are two ways to specify the template of the pagination controls directive:
+By default, the pagination controls will use a built-in template which uses the exact same markup as is found in the
+dirPagination.tpl.html file (which conforms to Bootstrap's pagination markup). Therefore, it is not necessary to specify a template.
-**1. Use the `paginationTemplateProvider` in your app's config block to set a global template for your app:**
+However, you may not want to use the default embedded template - for example if you use a another CSS framework that
+expects pagination lists to have a particular structure different from the default.
+
+If you plan to use a custom template, take a look at the default as demonstrated in dirPagination.tpl.html to get
+an idea of how it interacts with the directive.
+
+There are three ways to specify the template of the pagination controls directive:
+
+**1. Use the `paginationTemplateProvider` in your app's config block to set a global templateUrl for your app:**
```JavaScript
myApp.config(function(paginationTemplateProvider) {
@@ -103,12 +126,33 @@ myApp.config(function(paginationTemplateProvider) {
});
```
-**2. Use the `template-url` attribute on each pagination controls directive:**
+**2. Use the `paginationTemplateProvider` in your app's config block to set a global template string for your app:**
+
+```JavaScript
+myApp.config(function(paginationTemplateProvider) {
+ paginationTemplateProvider.setString('...
');
+
+ // or with e.g. Webpack you might do
+ paginationTemplateProvider.setString(require('/path/to/customPagination.tpl.html'));
+});
+```
+
+**3. Use the `template-url` attribute on each pagination controls directive:**
```HTML
```
+#### Template Priority
+
+If you use more than one method for specifying the template, the actual template to use will be decided based on
+the following order of precedence (highest priority first):
+
+1. `paginationTemplate.getString()`
+2. `template-url`
+3. `paginationTemplate.getPath()`
+4. (default built-in template)
+
## Directives API
The following attributes form the API for the pagination and pagination-controls directives. Optional attributes are marked as such,
@@ -126,7 +170,7 @@ The optional third argument `paginationId` is used when you need more than one i
on setting up multiple instances.
* **`current-page`** (optional) Specify a property on your controller's $scope that will be bound to the current
-page of the pagination. If this is not specified, the directive will automatically create a property named `__currentPage` and use
+page of the pagination. If this is not specified, the directive will automatically create a property named `__default__currentPage` and use
that instead.
* **`pagination-id`** (optional) Used to group together the dir-paginate directive with a corresponding dir-pagination-controls when you need more than
@@ -149,13 +193,15 @@ pagination.
pagination.
* **`on-page-change`** (optional, default = null) Specify a callback method to run each time one of the pagination links is clicked. The method will be passed the
-argument `newPageNumber`, which is an integer equal to the page number that has just been navigated to. **Note** you must use that exact argument name in your view,
-i.e. ``, and the method you specify must be defined on your controller $scope.
+optional arguments `newPageNumber` and `oldPageNumber`, which are integers equal to the page number that has just been navigated to, and the one just left, respectively. **Note** you must use that exact argument name in your view,
+i.e. ``, and the method you specify must be defined on your controller $scope.
* **`pagination-id`** (optional) Used to group together the dir-pagination-controls with a corresponding dir-paginate when you need more than
one pagination instance per page. See the section below on setting up multiple instances.
-* **`template-url`** (optional, default = `directives/pagination/dirPagination.tpl.html`) Specifies the template to use.
+* **`template-url`** (optional, default = `directives/pagination/dirPagination.tpl.html`) Specifies the template to use.
+
+* **`auto-hide`** (optional, default = true) Specify whether the dir-pagination-controls should be hidden when there's not enough elements to paginate over.
Note: you cannot use the `dir-pagination-controls` directive without `dir-paginate`. Attempting to do so will result in an
exception.
@@ -202,7 +248,6 @@ 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 **2** places
@@ -232,7 +277,7 @@ An example of two independent paginations on one page would look like this:
```
-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 pagination-ids above are set to "cust" in the first instance and "branch" in the second. The pagination-ids can be any [valid JavaScript identifier](https://mathiasbynens.be/notes/javascript-identifiers) (i.e. no hyphens, cannot begin with a number etc. [further discussion here](http://www.michaelbromley.co.uk/blog/410/a-note-on-angular-expressions-and-javascript-identifiers)),
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.
@@ -331,7 +376,7 @@ potential advantage of being triggered whenever the current-page changes, rather
```HTML
-
+
| {{ user.name }} |
{{ user.email }} |
@@ -346,14 +391,40 @@ potential advantage of being triggered whenever the current-page changes, rather
I've based the pagination navigation on the Bootstrap 3 component, so if you use Bootstrap in your project,
you'll get some nice styling for free. If you don't use Bootstrap, it's simple to style the links with css.
+## Frequently Asked Questions
+
+### Why does my sort / filter only affect the current page?
+This is a common problem and is usually due to the `itemsPerPage` filter not being at the end of the expression. For example, consider the following:
+
+```HTML
+...
+```
+
+In this case, the collection is first truncated to 10 items by the `itemsPerPage` filter, and then *those 10 items only* are filtered. The solution is to ensure the `itemsPerPage` filter comes after any sorting / filtering:
+
+```HTML
+...
+```
+
+### What is the `paginationService` and why is it not documented?
+
+The [`paginationService`](https://github.com/michaelbromley/angularUtils/blob/6055d260be44c0ba221a8c9bea015ac97e836a10/src/directives/pagination/dirPagination.js#L466-L521) is used internally to facilitate communication between the instances of the `dir-pagination` and `dir-pagination-controls` directives. Due to the way Angular's dependency injection system works, the service will be exposed in your app, meaning you can inject it directly into your controllers etc.
+
+However, since the `paginationService` is intended as an internal service, I cannot make any guarantees about the API, so it is dangerous to rely on using it directly in your code. Therefore I am not documenting it currently, so as not to encourage its general use. If you have a case that you feel can only be solved by direct use of this API, please open an issue and we can discuss it.
+
## Contribution
Pull requests are welcome. If you are adding a new feature or fixing an as-yet-untested use case, please consider
-writing unit tests to cover your change(s). All unit tests are contained in the `dirPagination.spec.js` file, and
+writing unit tests to cover your change(s). All unit tests are contained in the `dirPagination.spec.js` file, and
Karma is set up if you run `grunt watch` as you make changes.
At a minimum, make sure that all the tests still pass. Thanks!
+## Changelog
+
+Please see the [releases page of the package repo](https://github.com/michaelbromley/angularUtils-pagination/releases) for details
+of each released version.
+
## Credits
I did quite a bit of research before I figured I needed to make my own directive, and I picked up a lot of good ideas
@@ -369,3 +440,7 @@ from their pagination directive.
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.
+
+## License
+
+MIT
diff --git a/src/directives/pagination/dirPagination.js b/src/directives/pagination/dirPagination.js
index 5aa6efb..d5103ff 100644
--- a/src/directives/pagination/dirPagination.js
+++ b/src/directives/pagination/dirPagination.js
@@ -25,104 +25,380 @@
/**
* Module
*/
- var module;
- try {
- module = angular.module(moduleName);
- } catch(err) {
- // named module does not exist, so create one
- module = angular.module(moduleName, []);
- }
+ angular.module(moduleName, [])
+ .directive('dirPaginate', ['$compile', '$parse', 'paginationService', dirPaginateDirective])
+ .directive('dirPaginateNoCompile', noCompileDirective)
+ .directive('dirPaginationControls', ['paginationService', 'paginationTemplate', dirPaginationControlsDirective])
+ .filter('itemsPerPage', ['paginationService', itemsPerPageFilter])
+ .service('paginationService', paginationService)
+ .provider('paginationTemplate', paginationTemplateProvider)
+ .run(['$templateCache',dirPaginationControlsTemplateInstaller]);
- module.directive('dirPaginate', ['$compile', '$parse', 'paginationService', function($compile, $parse, paginationService) {
+ 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){
+ priority: 100,
+ compile: dirPaginationCompileFn
+ };
+
+ function dirPaginationCompileFn(tElement, tAttrs){
+
+ var expression = tAttrs.dirPaginate;
+ // regex taken directly from https://github.com/angular/angular.js/blob/v1.4.x/src/ng/directive/ngRepeat.js#L339
+ var match = expression.match(/^\s*([\s\S]+?)\s+in\s+([\s\S]+?)(?:\s+as\s+([\s\S]+?))?(?:\s+track\s+by\s+([\s\S]+?))?\s*$/);
+
+ var filterPattern = /\|\s*itemsPerPage\s*:\s*(.*\(\s*\w*\)|([^\)]*?(?=\s+as\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);
- 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*$/);
+ // 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);
- var filterPattern = /\|\s*itemsPerPage\s*:[^|]*/;
- if (match[2].match(filterPattern) === null) {
- throw 'pagination directive: the \'itemsPerPage\' filter must be set.';
+ 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;
+
+ // (TODO: this seems sound, but I'm reverting as many bug reports followed it's introduction in 0.11.0.
+ // Needs more investigation.)
+ // In case rawId != paginationId we deregister using rawId for the sake of general cleanliness
+ // before registering using paginationId
+ // paginationService.deregisterInstance(rawId);
+ paginationService.registerInstance(paginationId);
+
+ var repeatExpression = getRepeatExpression(expression, paginationId);
+ addNgRepeatToElement(element, attrs, repeatExpression);
+
+ 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 {
+ paginationService.setAsyncModeFalse(paginationId);
+ scope.$watchCollection(function() {
+ return collectionGetter(scope);
+ }, function(collection) {
+ if (collection) {
+ var collectionLength = (collection instanceof Array) ? collection.length : Object.keys(collection).length;
+ paginationService.setCollectionLength(paginationId, collectionLength);
+ }
+ });
}
- 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);
+ // Delegate to the link function returned by the new compilation of the ng-repeat
+ compiled(scope);
+
+ // (TODO: Reverting this due to many bug reports in v 0.11.0. Needs investigation as the
+ // principle is sound)
+ // When the scope is destroyed, we make sure to remove the reference to it in paginationService
+ // so that it can be properly garbage collected
+ // scope.$on('$destroy', function destroyDirPagination() {
+ // paginationService.deregisterInstance(paginationId);
+ // });
+ };
+ }
- return function dirPaginationLinkFn(scope, element, attrs){
+ /**
+ * 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*:[^|]*:[^|]*)/);
- // 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);
+ if (paginationId !== DEFAULT_ID && !idDefinedInFilter) {
+ repeatExpression = expression.replace(/(\|\s*itemsPerPage\s*:\s*[^|\s]*)/, "$1 : '" + paginationId + "'");
+ } else {
+ repeatExpression = expression;
+ }
- 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;
- }
+ return repeatExpression;
+ }
- // 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);
- }
+ /**
+ * 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);
+ }
+ }
- 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
+ /**
+ * 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 === 1) {
+ angular.element(el).attr('dir-paginate-no-compile', true);
+ }
+ });
+ }
- 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);
- }
+ /**
+ * 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 === 1) {
+ 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.
+ // Replace any non-alphanumeric characters which might confuse
+ // the $parse service and give unexpected results.
+ // See https://github.com/michaelbromley/angularUtils/issues/233
+ // Adding the '_' as a prefix resolves an issue where paginationId might be have a digit as its first char
+ // See https://github.com/michaelbromley/angularUtils/issues/400
+ var defaultCurrentPage = '_' + (paginationId + '__currentPage').replace(/\W/g, '_');
+ 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 dirPaginationControlsTemplateInstaller($templateCache) {
+ $templateCache.put('angularUtils.directives.dirPagination.template', '');
+ }
+
+ function dirPaginationControlsDirective(paginationService, paginationTemplate) {
+
+ var numberRegex = /^\d+$/;
+
+ var DDO = {
+ restrict: 'AE',
+ scope: {
+ maxSize: '=?',
+ onPageChange: '&?',
+ paginationId: '=?',
+ autoHide: '=?'
+ },
+ link: dirPaginationControlsLinkFn
+ };
+
+ // We need to check the paginationTemplate service to see whether a template path or
+ // string has been specified, and add the `template` or `templateUrl` property to
+ // the DDO as appropriate. The order of priority to decide which template to use is
+ // (highest priority first):
+ // 1. paginationTemplate.getString()
+ // 2. attrs.templateUrl
+ // 3. paginationTemplate.getPath()
+ var templateString = paginationTemplate.getString();
+ if (templateString !== undefined) {
+ DDO.template = templateString;
+ } else {
+ DDO.templateUrl = function(elem, attrs) {
+ return attrs.templateUrl || paginationTemplate.getPath();
+ };
+ }
+ return DDO;
+
+ 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 + ') ' : ' ';
+ if (window.console) {
+ console.warn('Pagination directive: the pagination controls' + idMessage + 'cannot be used without the corresponding pagination directive, which was not found at link time.');
+ }
+ }
+
+ if (!scope.maxSize) { scope.maxSize = 9; }
+ scope.autoHide = scope.autoHide === undefined ? true : scope.autoHide;
+ 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('maxSize', function(val) {
+ if (val) {
+ paginationRange = Math.max(scope.maxSize, 5);
+ generatePagination();
+ }
+ });
+
+ scope.$watch(function() {
+ if (paginationService.isRegistered(paginationId)) {
+ return (paginationService.getCollectionLength(paginationId) + 1) * paginationService.getItemsPerPage(paginationId);
+ }
+ }, function(length) {
+ if (0 < length) {
+ generatePagination();
+ }
+ });
+
+ scope.$watch(function() {
+ if (paginationService.isRegistered(paginationId)) {
+ return (paginationService.getItemsPerPage(paginationId));
+ }
+ }, function(current, previous) {
+ if (current != previous && typeof previous !== 'undefined') {
+ goToPage(scope.pagination.current);
+ }
+ });
+
+ scope.$watch(function() {
+ if (paginationService.isRegistered(paginationId)) {
+ return paginationService.getCurrentPage(paginationId);
+ }
+ }, function(currentPage, previousPage) {
+ if (currentPage != previousPage) {
+ goToPage(currentPage);
+ }
+ });
+
+ scope.setCurrent = function(num) {
+ if (paginationService.isRegistered(paginationId) && isValidPageNumber(num)) {
+ num = parseInt(num, 10);
+ paginationService.setCurrentPage(paginationId, num);
+ }
+ };
+
+ /**
+ * Custom "track by" function which allows for duplicate "..." entries on long lists,
+ * yet fixes the problem of wrongly-highlighted links which happens when using
+ * "track by $index" - see https://github.com/michaelbromley/angularUtils/issues/153
+ * @param id
+ * @param index
+ * @returns {string}
+ */
+ scope.tracker = function(id, index) {
+ return id + '_' + index;
+ };
+
+ function goToPage(num) {
+ if (paginationService.isRegistered(paginationId) && isValidPageNumber(num)) {
+ var oldPageNumber = scope.pagination.current;
+
+ 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 the first argument
+ // and the previous page number as a second argument
+ if (scope.onPageChange) {
+ scope.onPageChange({
+ newPageNumber : num,
+ oldPageNumber : oldPageNumber
});
}
+ }
+ }
- // Delegate to the link function returned by the new compilation of the ng-repeat
- compiled(scope);
- };
+ function generatePagination() {
+ if (paginationService.isRegistered(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);
+ } else {
+ updateRangeValues();
+ }
+ }
}
- };
- }]);
- module.directive('dirPaginationControls', ['paginationService', 'paginationTemplate', function(paginationService, paginationTemplate) {
+ /**
+ * 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() {
+ if (paginationService.isRegistered(paginationId)) {
+ var currentPage = paginationService.getCurrentPage(paginationId),
+ itemsPerPage = paginationService.getItemsPerPage(paginationId),
+ totalItems = paginationService.getCollectionLength(paginationId);
- var numberRegex = /^\d+$/;
+ 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
@@ -192,124 +468,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') {
@@ -320,7 +486,7 @@
}
var end;
var start;
- if (collection instanceof Array) {
+ if (angular.isObject(collection)) {
itemsPerPage = parseInt(itemsPerPage) || 9999999999;
if (paginationService.isAsyncMode(paginationId)) {
start = 0;
@@ -330,14 +496,47 @@
end = start + itemsPerPage;
paginationService.setItemsPerPage(paginationId, itemsPerPage);
- return collection.slice(start, end);
+ if (collection instanceof Array) {
+ // the array just needs to be sliced
+ return collection.slice(start, end);
+ } else {
+ // in the case of an object, we need to get an array of keys, slice that, then map back to
+ // the original object.
+ var slicedObject = {};
+ angular.forEach(keys(collection).slice(start, end), function(key) {
+ slicedObject[key] = collection[key];
+ });
+ return slicedObject;
+ }
} else {
return collection;
}
};
- }]);
+ }
- module.service('paginationService', function() {
+ /**
+ * Shim for the Object.keys() method which does not exist in IE < 9
+ * @param obj
+ * @returns {Array}
+ */
+ function keys(obj) {
+ if (!Object.keys) {
+ var objKeys = [];
+ for (var i in obj) {
+ if (obj.hasOwnProperty(i)) {
+ objKeys.push(i);
+ }
+ }
+ return objKeys;
+ } else {
+ return Object.keys(obj);
+ }
+ }
+
+ /**
+ * This service allows the various parts of the module to communicate and stay in sync.
+ */
+ function paginationService() {
var instances = {};
var lastRegisteredInstance;
@@ -351,6 +550,10 @@
}
};
+ this.deregisterInstance = function(instanceId) {
+ delete instances[instanceId];
+ };
+
this.isRegistered = function(instanceId) {
return (typeof instances[instanceId] !== 'undefined');
};
@@ -389,25 +592,50 @@
instances[instanceId].asyncMode = true;
};
+ this.setAsyncModeFalse = function(instanceId) {
+ instances[instanceId].asyncMode = false;
+ };
+
this.isAsyncMode = function(instanceId) {
return instances[instanceId].asyncMode;
};
- });
-
- module.provider('paginationTemplate', function() {
+ }
- var templatePath = 'directives/pagination/dirPagination.tpl.html';
-
+ /**
+ * This provider allows global configuration of the template path used by the dir-pagination-controls directive.
+ */
+ function paginationTemplateProvider() {
+
+ var templatePath = 'angularUtils.directives.dirPagination.template';
+ var templateString;
+
+ /**
+ * Set a templateUrl to be used by all instances of
+ * @param {String} path
+ */
this.setPath = function(path) {
templatePath = path;
};
-
+
+ /**
+ * Set a string of HTML to be used as a template by all instances
+ * of . If both a path *and* a string have been set,
+ * the string takes precedence.
+ * @param {String} str
+ */
+ this.setString = function(str) {
+ templateString = str;
+ };
+
this.$get = function() {
return {
getPath: function() {
return templatePath;
+ },
+ getString: function() {
+ return templateString;
}
};
};
- });
-})();
\ No newline at end of file
+ }
+})();
diff --git a/src/directives/pagination/dirPagination.spec.js b/src/directives/pagination/dirPagination.spec.js
index cf935f2..07a7e45 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.
*/
@@ -9,10 +9,18 @@ describe('dirPagination directive', function() {
var $timeout;
var containingElement;
var myCollection;
+ var myObjectCollection;
beforeEach(module('angularUtils.directives.dirPagination'));
beforeEach(module('templates-main'));
+ // used to test the paginationTemplateProvider (see end of file)
+ var templateProvider;
+ angular.module('customTemplateTestApp', []);
+ beforeEach(module('customTemplateTestApp', function(paginationTemplateProvider) {
+ templateProvider = paginationTemplateProvider;
+ }));
+
beforeEach(inject(function($rootScope, _$compile_, _$timeout_) {
$compile = _$compile_;
@@ -64,14 +72,6 @@ describe('dirPagination directive', function() {
expect(compile).toThrow("pagination directive: the 'itemsPerPage' filter must be set.");
});
- it('should allow a space after itemsPerPage and before the colon', function() {
- function compile() {
- var customExpression = "item in collection | itemsPerPage : 10";
- compileElement(myCollection, 5, 1, customExpression);
- }
- expect(compile).not.toThrow();
- });
-
it('should repeat the items like ng-repeat', function() {
compileElement(myCollection);
var listItems = getListItems();
@@ -124,9 +124,9 @@ describe('dirPagination directive', function() {
it('should work inside a transcluded directive (ng-if)', function() {
$scope.collection = myCollection;
var html = '';
+ ' ' +
+ '' +
+ '';
containingElement.append($compile(html)($scope));
$scope.$apply();
@@ -170,6 +170,196 @@ describe('dirPagination directive', function() {
expect(listItems).toEqual(['item 1', 'item 2']);
});
+ // see https://github.com/michaelbromley/angularUtils/issues/233
+ function testPaginationId(paginationIdString) {
+ $scope.collection = myCollection;
+ var html = ' ' +
+ '';
+ containingElement.append($compile(html)($scope));
+ $scope.$apply();
+
+ var listItems = getListItems();
+ expect(listItems.length).toEqual(2);
+ expect(listItems).toEqual(['item 1', 'item 2']);
+ }
+
+ it('should work when the pagination-id evaluates to a string containing a period.', function() {
+ testPaginationId('some.string');
+ });
+
+ it('should work when the pagination-id evaluates to a string containing a hyphen.', function() {
+ testPaginationId('some-string');
+ });
+
+ });
+
+ describe('paginating over an object', function() {
+ beforeEach(function() {
+ myObjectCollection = {};
+ for(var i = 1; i <= 100; i++) {
+ myObjectCollection['key_' + i] = 'item ' + i;
+ }
+ });
+
+ it('should not throw an exception when the collection is an object', function() {
+ function compile() {
+ compileElement(myObjectCollection, 10, 1, "(key, item) in collection | itemsPerPage: itemsPerPage");
+ }
+ expect(compile).not.toThrow();
+ });
+
+ it('should correctly paginate with simple syntax', function() {
+ compileElement(myObjectCollection, 5, 1, "item in collection | itemsPerPage: itemsPerPage");
+ expect(getListItems()).toEqual(['item 1', 'item 2', 'item 3', 'item 4', 'item 5']);
+ });
+
+ it('should correctly paginate with (key, value) syntax', function() {
+ compileElement(myObjectCollection, 5, 1, "(key, item) in collection | itemsPerPage: itemsPerPage");
+ expect(getListItems()).toEqual(['item 1', 'item 2', 'item 3', 'item 4', 'item 5']);
+ });
+
+ it('should show the correct items for the currentPage', function() {
+ compileElement(myObjectCollection, 5, 4, "(key, item) in collection | itemsPerPage: itemsPerPage");
+ expect(getListItems()).toEqual(['item 16', 'item 17', 'item 18', 'item 19', 'item 20']);
+ });
+
+ it('should display the correct pagination links', function() {
+ compileElement(myObjectCollection, 20, 1, "item in collection | itemsPerPage: itemsPerPage");
+ var paginationLinks = getPageLinksArray();
+
+ expect(paginationLinks).toEqual(['‹','1', '2', '3', '4', '5', '›']);
+ });
+
+ });
+
+ describe('valid expressions', function() {
+
+ beforeEach(function() {
+ $scope.getPageSize = function() {
+ return 10;
+ };
+ });
+
+ it('should allow a space after itemsPerPage and before the colon', function() {
+ function compile() {
+ compileElement(myCollection, 5, 1, "item in collection | itemsPerPage : 10");
+ }
+ expect(compile).not.toThrow();
+ });
+
+ it('should allow parentheses around the itemsPerPage filter', function() {
+ function compile() {
+ compileElement(myCollection, 5, 1, "item in filtered = (collection | filter: '1' | itemsPerPage: itemsPerPage)");
+ }
+
+ expect(compile).not.toThrow();
+ expect(getListItems()).toEqual(['item 1', 'item 10', 'item 11', 'item 12', 'item 13']);
+ expect($scope.filtered.length).toEqual(5);
+ });
+
+ it('should allow the itemsPerPage to be a scope method 1', function() {
+ function compile() {
+ compileElement(myCollection, 5, 1, "item in collection | itemsPerPage: getPageSize()");
+ }
+ expect(compile).not.toThrow();
+ expect(getListItems().length).toEqual(10);
+ });
+
+ it('should allow the itemsPerPage to be a scope method 2', function() {
+ function compile() {
+ compileElement(myCollection, 5, 1, "item in collection | itemsPerPage: getPageSize(myvar)");
+ }
+ expect(compile).not.toThrow();
+ expect(getListItems().length).toEqual(10);
+ });
+
+ it('should allow the itemsPerPage to be a scope method 3', function() {
+ function compile() {
+ compileElement(myCollection, 5, 1, "item in filtered = (collection | itemsPerPage: getPageSize(_myvar))");
+ }
+ expect(compile).not.toThrow();
+ expect(getListItems().length).toEqual(10);
+ });
+
+ it('should allow alias syntax', function() {
+ function compile() {
+ compileElement(myCollection, 5, 1, "item in collection | itemsPerPage: 10 as myAlias");
+ }
+ expect(compile).not.toThrow();
+ expect(getListItems().length).toEqual(10);
+ expect($scope.myAlias.length).toEqual(10);
+ });
+
+ it('should allow alias syntax 2', function() {
+ function compile() {
+ compileElement(myCollection, 5, 1, "item in collection | itemsPerPage: getPageSize() as myAlias");
+ }
+ expect(compile).not.toThrow();
+ expect(getListItems().length).toEqual(10);
+ expect($scope.myAlias.length).toEqual(10);
+ });
+
+ it('should allow alias syntax 3', function() {
+ function compile() {
+ compileElement(myCollection, 5, 1, "item in alias1 = (collection | itemsPerPage: getPageSize()) as alias2");
+ }
+ expect(compile).not.toThrow();
+ expect(getListItems().length).toEqual(10);
+ expect($scope.alias1.length).toEqual(10);
+ expect($scope.alias2.length).toEqual(10);
+ });
+
+ it('should allow dot and bracket notation', function() {
+ function compile() {
+ $scope.foo = {
+ name: 'myCollection',
+ myCollection: myCollection,
+ perPage: 10
+ };
+ compileElement(myCollection, 5, 1, "item in foo[foo.name] | itemsPerPage:foo.perPage");
+ }
+ expect(compile).not.toThrow();
+ expect(getListItems().length).toEqual(10);
+ });
+
+ // see https://github.com/michaelbromley/angularUtils/issues/241
+ // the actual issue was caused by the work "sharedTasksFilters", which was
+ // being matched by the part of the regex that looks for "as" alias syntax.
+ it('should allow deeply nested, long-winded object for itemsPerPage', function() {
+ function compile() {
+ $scope.eventsCtrl = { eventsFilters: { sharedTasksFilters: { eventsPerPage: 10 } } };
+ compileElement(myCollection, 5, 1, "item in collection | itemsPerPage:eventsCtrl.eventsFilters.sharedTasksFilters.eventsPerPage");
+ }
+ expect(compile).not.toThrow();
+ expect(getListItems().length).toEqual(10);
+ });
+
+
+ it('should allow track by syntax 1', function() {
+ function compile() {
+ compileElement(myCollection, 5, 1, "item in collection | itemsPerPage: 10 track by $index");
+ }
+ expect(compile).not.toThrow();
+ expect(getListItems().length).toEqual(10);
+ });
+
+ it('should allow track by syntax 2', function() {
+ function compile() {
+ compileElement(myCollection, 5, 1, "item in collection | itemsPerPage: 10 track by item");
+ }
+ expect(compile).not.toThrow();
+ expect(getListItems().length).toEqual(10);
+ });
+
+ it('should allow track by with dot in itemsPerPage', function() {
+ function compile() {
+ $scope.foo = { perPage : 10 };
+ compileElement(myCollection, 5, 1, "item in collection | itemsPerPage: foo.perPage track by item");
+ }
+ expect(compile).not.toThrow();
+ expect(getListItems().length).toEqual(10);
+ });
+
});
describe('if currentPage attribute is not set', function() {
@@ -200,14 +390,21 @@ describe('dirPagination directive', function() {
describe('pagination controls', function() {
- it('should throw an exception if the dir-paginate directive has not been set up', function() {
+ beforeEach(function(){
+ spyOn(console, 'warn');
+ });
+
+ it('should throw a warning if the dir-paginate directive has not been set up', function() {
+
function compile() {
var html = '';
containingElement.append($compile(html)($scope));
$scope.$apply();
}
- expect(compile).toThrow("pagination directive: the pagination controls cannot be used without the corresponding pagination directive.");
+ compile();
+
+ expect(console.warn).toHaveBeenCalledWith('Pagination directive: the pagination controls cannot be used without the corresponding pagination directive, which was not found at link time.');
});
it('should not display pagination if all rows fit on one page', function() {
@@ -324,9 +521,9 @@ describe('dirPagination directive', function() {
$scope.collection = myCollection;
$scope.showList = false;
html = '' +
- '';
+ ' ' +
+ '' +
+ '';
containingElement.append($compile(html)($scope));
$scope.$apply();
}
@@ -360,6 +557,23 @@ describe('dirPagination directive', function() {
expect(pageLinks).toEqual(['‹','1', '2', '3', '...', '10', '›']);
});
+ it('should alter links array when value of max-size changes', function() {
+ $scope.maxSize = 5;
+ compileWithAttributes(' max-size="maxSize" ');
+
+ var pageLinks = getPageLinksArray();
+
+ expect(pageLinks).toEqual(['‹','1', '2', '3', '...', '10', '›']);
+
+ $scope.$apply(function() {
+ $scope.maxSize = 9;
+ });
+
+ pageLinks = getPageLinksArray();
+
+ expect(pageLinks).toEqual(['‹','1', '2', '3', '4', '5', '6', '7', '...', '10', '›']);
+ });
+
it('should impose a minimum max-size of 5', function() {
compileWithAttributes(' max-size="2" ');
@@ -406,7 +620,7 @@ describe('dirPagination directive', function() {
$scope.myCallback = function(currentPage) {
return "The current page is " + currentPage;
};
- spyOn($scope, 'myCallback').and.callThrough();
+ spyOn($scope, 'myCallback');
});
it('should call the callback once when page link clicked', function() {
@@ -425,7 +639,7 @@ describe('dirPagination directive', function() {
$scope.currentPage = 1;
var html = '' +
'' +
- '| {{ item }} |
' +
+ '| {{ item }} |
' +
'
';
containingElement.append($compile(html)($scope));
$scope.$apply();
@@ -454,6 +668,15 @@ describe('dirPagination directive', function() {
$scope.$apply();
expect($scope.myCallback).toHaveBeenCalledWith(2);
});
+
+ it('should pass the previous page number to the callback', function() {
+ compileWithAttributes(' on-page-change="myCallback(oldPageNumber)" ');
+ var pagination = containingElement.find('ul.pagination');
+
+ pagination.children().eq(3).find('a').triggerHandler('click');
+ $scope.$apply();
+ expect($scope.myCallback).toHaveBeenCalledWith(1);
+ });
});
describe('total-items attribute', function() {
@@ -478,6 +701,56 @@ describe('dirPagination directive', function() {
expect(listItems.length).toEqual(100);
});
});
+
+ describe('auto-hide attribute', function () {
+ function compileWithAttributesAndItemsPerPage(attributes, itemsPerPage) {
+ $scope.collection = myCollection;
+ $scope.currentPage = 1;
+ var html = ' ' +
+ '';
+ containingElement.append($compile(html)($scope));
+ $scope.$apply();
+ }
+
+ it('when not set, should not generate pagination controls, with not enough items to paginate over', function () {
+ compileWithAttributesAndItemsPerPage('', 100);
+ var pagination = containingElement.find('ul.pagination');
+ expect(pagination.length).toEqual(0);
+ });
+
+ it('when not set, should generate pagination controls, with enough items to paginate over', function () {
+ compileWithAttributesAndItemsPerPage('', 99);
+ var pagination = containingElement.find('ul.pagination');
+ expect(pagination.length).toEqual(1);
+ });
+
+ it('when set to false, should generate pagination controls, with not enough items to paginate over', function () {
+ compileWithAttributesAndItemsPerPage('auto-hide="false"', 100);
+ var pagination = containingElement.find('ul.pagination');
+ expect(pagination.length).toEqual(1);
+ var pageLinks = containingElement.find('ul.pagination li.disabled');
+ expect(pageLinks.length).toEqual(3);
+ });
+
+ it('when set to false, should generate pagination controls, with enough items to paginate over', function () {
+ compileWithAttributesAndItemsPerPage('auto-hide="false"', 99);
+ var pagination = containingElement.find('ul.pagination');
+ expect(pagination.length).toEqual(1);
+ });
+
+ it('when set to true, should generate pagination controls, with enough items to paginate over', function () {
+ compileWithAttributesAndItemsPerPage('auto-hide="true"', 100);
+ var pagination = containingElement.find('ul.pagination');
+ expect(pagination.length).toEqual(0);
+ });
+
+ it('when set to true, should generate pagination controls, with not enough items to paginate over', function () {
+ compileWithAttributesAndItemsPerPage('auto-hide="true"', 99);
+ var pagination = containingElement.find('ul.pagination');
+ expect(pagination.length).toEqual(1);
+ });
+ });
});
});
@@ -493,6 +766,7 @@ describe('dirPagination directive', function() {
collection1.push('c1:' + i);
collection2.push('c2:' + i);
}
+ spyOn(console, 'warn');
});
/**
@@ -518,7 +792,7 @@ describe('dirPagination directive', function() {
$scope.collection[paginationId] = collection;
$scope.itemsPerPage[paginationId] = itemsPerPage;
$scope.currentPage[paginationId] = currentPage || 1;
- expression = customExpression || "item in collection." + paginationId + " | itemsPerPage: itemsPerPage." + paginationId + ": '" + paginationId + "'";
+ expression = customExpression || "item in collection." + paginationId + " | itemsPerPage: itemsPerPage." + paginationId;
html = ' ' +
'';
containingElement.append($compile(html)($scope));
@@ -604,7 +878,7 @@ describe('dirPagination directive', function() {
expect(getMultiListItems("c2")).toEqual(['c2:6', 'c2:7']);
});
- it('should throw an exception if a non-existant paginationId is set in the pagination-controls', function() {
+ it('should print a warning if a non-existant paginationId is set in the pagination-controls', function() {
$scope.collection = [1,2,3,4,5];
function compile() {
@@ -614,8 +888,8 @@ describe('dirPagination directive', function() {
containingElement.append($compile(html)($scope));
$scope.$apply();
}
-
- expect(compile).toThrow("pagination directive: the pagination controls (id: id2) cannot be used without the corresponding pagination directive.");
+ compile();
+ expect(console.warn).toHaveBeenCalledWith('Pagination directive: the pagination controls (id: id2) cannot be used without the corresponding pagination directive, which was not found at link time.');
});
it('should throw an exception if a non-existant paginationId is set in the itemsPerPage filter', function() {
@@ -632,6 +906,49 @@ describe('dirPagination directive', function() {
expect(compile).toThrow("pagination directive: the itemsPerPage id argument (id: id2) does not match a registered pagination-id.");
});
+ describe('valid expressions with pagination id', function() {
+
+ it('should allow track by syntax', function() {
+ function compile() {
+ compileMultipleInstance(collection1, 10, 1, "c1", "item in collection.c1 | itemsPerPage: itemsPerPage.c1 track by $index");
+ }
+ expect(compile).not.toThrow();
+ expect(getListItems().length).toEqual(10);
+ });
+
+ it('should allow track by with other filter syntax', function() {
+ function compile() {
+ compileMultipleInstance(collection1, 10, 1, "c1", "item in collection.c1 | orderBy: reverse | itemsPerPage: itemsPerPage.c1 track by $index");
+ }
+ expect(compile).not.toThrow();
+ expect(getListItems().length).toEqual(10);
+ });
+ });
+
+ describe('dymanic pagination-id', function() {
+
+ function compileWithDynamicId(paginationId, customExpression) {
+ var html;
+ html = ' ' +
+ '';
+ containingElement.append($compile(html)($scope));
+ $scope.$apply();
+ }
+
+ it('should allow object reference', function() {
+ function compile() {
+ $scope.myId = {
+ foo: 'foo'
+ };
+ $scope.collection = myCollection;
+ compileWithDynamicId('myId.foo', 'item in collection | itemsPerPage: 10');
+ }
+
+ expect(compile).not.toThrow();
+ expect(getListItems().length).toEqual(10);
+ });
+ });
+
});
describe('pagination controls template API', function() {
@@ -703,10 +1020,10 @@ describe('dirPagination directive', function() {
$scope.itemsPerPage = itemsPerPage || 10;
$scope.currentPage = currentPage || 1;
html = '' +
- '
header
' +
- '
{{ item }}
' +
- '
footer
' +
- '
';
+ 'header
' +
+ '{{ item }}
' +
+ 'footer
' +
+ ' ';
containingElement.append($compile(html)($scope));
$scope.$apply();
}
@@ -754,7 +1071,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 +1085,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');
});
});
@@ -818,7 +1133,7 @@ describe('dirPagination directive', function() {
compile();
var $list1 = containingElement.find('ul.list').eq(0);
- var $list2 = containingElement.find('ul.list').eq(1);
+ var $list2 = containingElement.find('ul.list').eq(1);
expect(getListItems($list1)).toEqual([ '1', '2', '3' ]);
expect(getListItems($list2)).toEqual([ 'a', 'b', 'c' ]);
@@ -833,6 +1148,52 @@ describe('dirPagination directive', function() {
expect(getListItems($list2)).toEqual([ 'a', 'b', 'c' ]);
});
+ });
+
+ describe('paginationTemplateProvider', function() {
+
+ beforeEach(inject(function($templateCache) {
+ $templateCache.put('setPath_template', '
Test Template{{ pages.length }}
');
+ $templateCache.put('templateUrl_template', '
Test TemplateUrl Template{{ pages.length }}
');
+ }));
+
+ it('should use the custom template specified by setPath()', function() {
+ templateProvider.setPath('setPath_template');
+ compileElement(myCollection, 10);
+
+ expect(containingElement.find('.set-path-template').html()).toContain('Test Template');
+ });
+
+ it('should use the custom template specified by setString()', function() {
+ templateProvider.setString('
Test Template String{{ pages.length }}
');
+ compileElement(myCollection, 10);
+
+ expect(containingElement.find('.set-string-template').html()).toContain('Test Template String');
+ });
+
+ it('should prioritize setString() if both path and string have been set', function() {
+ templateProvider.setString('
Test Template String{{ pages.length }}
');
+ templateProvider.setPath('setPath_template');
+ compileElement(myCollection, 10);
+
+ expect(containingElement.find('.set-path-template').html()).toBeUndefined();
+ expect(containingElement.find('.set-string-template').html()).toContain('Test Template String');
+ });
+
+ it('should prioritize setString() over path and template-url attribute.', function() {
+ templateProvider.setString('
Test Template String{{ pages.length }}
');
+ templateProvider.setPath('setPath_template');
+
+ var html = '
' +
+ '
';
+ containingElement.append($compile(html)($scope));
+ $scope.$apply();
+
+ expect(containingElement.find('.set-path-template').html()).toBeUndefined();
+ expect(containingElement.find('.template-url-template').html()).toBeUndefined();
+ expect(containingElement.find('.set-string-template').html()).toContain('Test Template String');
+ });
});
+
});
diff --git a/src/directives/pagination/dirPagination.tpl.html b/src/directives/pagination/dirPagination.tpl.html
index 558aa20..db98d4c 100644
--- a/src/directives/pagination/dirPagination.tpl.html
+++ b/src/directives/pagination/dirPagination.tpl.html
@@ -1,11 +1,11 @@
-