Alloy UI

aui-paginator  1.0.1

 
Filters
AUI.add('aui-paginator', function(A) {
/**
 * The Paginator Utility - The Paginator widget provides a set of controls to navigate through paged data.
 *
 * @module aui-paginator
 */

var L = A.Lang,
	isArray = L.isArray,
	isBoolean = L.isBoolean,
	isFunction = L.isFunction,
	isNumber = L.isNumber,
	isObject = L.isObject,
	isString = L.isString,

	ALWAYS_VISIBLE = 'alwaysVisible',
	BOUNDING_BOX = 'boundingBox',
	CONTAINER = 'container',
	CONTAINERS = 'containers',
	CONTENT = 'content',
	CURRENT = 'current',
	DOT = '.',
	FIRST = 'first',
	FIRST_PAGE_LINK = 'firstPageLink',
	FIRST_PAGE_LINK_LABEL = 'firstPageLinkLabel',
	LAST = 'last',
	LAST_PAGE_LINK = 'lastPageLink',
	LAST_PAGE_LINK_LABEL = 'lastPageLinkLabel',
	LINK = 'link',
	MAX_PAGE_LINKS = 'maxPageLinks',
	NEXT = 'next',
	NEXT_PAGE_LINK = 'nextPageLink',
	NEXT_PAGE_LINK_LABEL = 'nextPageLinkLabel',
	PAGE = 'page',
	PAGE_CONTAINER_TEMPLATE = 'pageContainerTemplate',
	PAGE_LINK_CONTENT = 'pageLinkContent',
	PAGE_LINK_TEMPLATE = 'pageLinkTemplate',
	PAGE_REPORT_EL = 'pageReportEl',
	PAGE_REPORT_LABEL_TEMPLATE = 'pageReportLabelTemplate',
	PAGINATOR = 'paginator',
	PER = 'per',
	PREV = 'prev',
	PREV_PAGE_LINK = 'prevPageLink',
	PREV_PAGE_LINK_LABEL = 'prevPageLinkLabel',
	REPORT = 'report',
	ROWS = 'rows',
	ROWS_PER_PAGE = 'rowsPerPage',
	ROWS_PER_PAGE_EL = 'rowsPerPageEl',
	ROWS_PER_PAGE_OPTIONS = 'rowsPerPageOptions',
	SELECT = 'select',
	SPACE = ' ',
	STATE = 'state',
	TEMPLATE = 'template',
	TOTAL = 'total',
	TOTAL_EL = 'totalEl',
	TOTAL_LABEL = 'totalLabel',
	TOTAL_PAGES = 'totalPages',

	concat = function() {
		return Array.prototype.slice.call(arguments).join(SPACE);
	},

	isNodeList = function(v) {
		return (v instanceof A.NodeList);
	},

	num = function(n) {
		return parseInt(n, 10) || 0;
	},

	getCN = A.ClassNameManager.getClassName,

	CSS_PAGINATOR = getCN(PAGINATOR),
	CSS_PAGINATOR_CONTAINER = getCN(PAGINATOR, CONTAINER),
	CSS_PAGINATOR_CONTENT = getCN(PAGINATOR, CONTENT),
	CSS_PAGINATOR_CURRENT_PAGE = getCN(PAGINATOR, CURRENT, PAGE),
	CSS_PAGINATOR_FIRST_LINK = getCN(PAGINATOR, FIRST, LINK),
	CSS_PAGINATOR_LAST_LINK = getCN(PAGINATOR, LAST, LINK),
	CSS_PAGINATOR_LINK = getCN(PAGINATOR, LINK),
	CSS_PAGINATOR_NEXT_LINK = getCN(PAGINATOR, NEXT, LINK),
	CSS_PAGINATOR_PAGE_CONTAINER = getCN(PAGINATOR, PAGE, CONTAINER),
	CSS_PAGINATOR_PAGE_LINK = getCN(PAGINATOR, PAGE, LINK),
	CSS_PAGINATOR_PAGE_REPORT = getCN(PAGINATOR, CURRENT, PAGE, REPORT),
	CSS_PAGINATOR_PREV_LINK = getCN(PAGINATOR, PREV, LINK),
	CSS_PAGINATOR_ROWS_PER_PAGE = getCN(PAGINATOR, ROWS, PER, PAGE),
	CSS_PAGINATOR_TOTAL = getCN(PAGINATOR, TOTAL),

	TOTAL_LABEL_TPL = '(Total {total})',
	PAGE_REPORT_LABEL_TPL = '({page} of {totalPages})',
	DEFAULT_OUTPUT_TPL = '{FirstPageLink} {PrevPageLink} {PageLinks} {NextPageLink} {LastPageLink} {CurrentPageReport} {Total} {RowsPerPageSelect}',

	GT_TPL = '>',
	LT_TPL = '<',
	FIRST_LINK_TPL = '<a href="#" class="'+concat(CSS_PAGINATOR_LINK, CSS_PAGINATOR_FIRST_LINK)+'"></a>',
	LAST_LINK_TPL = '<a href="#" class="'+concat(CSS_PAGINATOR_LINK, CSS_PAGINATOR_LAST_LINK)+'"></a>',
	NEXT_LINK_TPL = '<a href="#" class="'+concat(CSS_PAGINATOR_LINK, CSS_PAGINATOR_NEXT_LINK)+'"></a>',
	PAGE_CONTAINER_TPL = '<span></span>',
	PAGE_LINK_TPL = '<a href="#"></a>',
	PAGE_REPORT_TPL = '<span class="'+concat(CSS_PAGINATOR_PAGE_REPORT)+'"></span>',
	PREV_LINK_TPL = '<a href="#" class="'+concat(CSS_PAGINATOR_LINK, CSS_PAGINATOR_PREV_LINK)+'"></a>',
	ROWS_PER_PAGE_TPL = '<select class="'+CSS_PAGINATOR_ROWS_PER_PAGE+'"></select>',
	TOTAL_TPL = '<span class="'+concat(CSS_PAGINATOR_TOTAL)+'"></span>';

/**
 * <p><img src="assets/images/aui-paginator/main.png"/></p>
 *
 * A base class for Paginator, providing:
 * <ul>
 *    <li>Widget Lifecycle (initializer, renderUI, bindUI, syncUI, destructor)</li>
 *    <li>Set of controls to navigate through paged data</li>
 * </ul>
 *
 * Quick Example:<br/>
 *
 * <pre><code>var instance = new A.Paginator({
 *	containers: '.paginatorA',
 *	total: 10,
 *	maxPageLinks: 10,
 *	rowsPerPage: 1,
 *	rowsPerPageOptions: [ 1, 3, 5, 7 ]
 * }).render();
 * </code></pre>
 *
 * Check the list of <a href="Paginator.html#configattributes">Configuration Attributes</a> available for
 * Paginator.
 *
 * @param config {Object} Object literal specifying widget configuration properties.
 *
 * @class Paginator
 * @constructor
 * @extends Base
 */
var Paginator = A.Component.create(
	{
		/**
		 * Static property provides a string to identify the class.
		 *
		 * @property Paginator.NAME
		 * @type String
		 * @static
		 */
		NAME: PAGINATOR,

		/**
		 * Static property used to define the default attribute
		 * configuration for the Paginator.
		 *
		 * @property Paginator.ATTRS
		 * @type Object
		 * @static
		 */
		ATTRS: {
			/**
			 * If true the Paginator will be always visible, even when the number
	         * of pages is 0. To hide the paginator controls automatically when
	         * there is no pages to display use <code>false</code>.
			 *
			 * @attribute alwaysVisible
			 * @default true
			 * @type boolean
			 */
			alwaysVisible: {
				value: true,
				validator: isBoolean
			},

			/**
			 * The Paginator controls UI could be displayed in more than one
	         * container (i.e., in the header and footer of a list). Pass a
	         * <a href="NodeList.html">NodeList</a> or a selector to grab the
	         * containers.
			 *
			 * @attribute containers
			 * @default null
			 * @type Node | String
			 */
			containers: {
				writeOnce: true,
				setter: function(v) {
					var instance = this;

					if (isNodeList(v)) {
						return v;
					}
					else if (isString(v)) {
						return A.all(v);
					}

					return new A.NodeList([v]);
				}
			},

			/**
			 * The <a href="Node.html">Node</a> or template to be used as the
	         * first link element.
			 *
			 * @attribute firstPageLink
			 * @default Generated anchor element.
			 * @type Node | String
			 */
			firstPageLink: {
				setter: A.one,
				valueFn: function() {
					var label = this.get(FIRST_PAGE_LINK_LABEL);

					return A.Node.create(FIRST_LINK_TPL).html(label);
				}
			},

			/**
			 * The label used as content of the
	         * <a href="Paginator.html#config_firstPageLink">firstPageLink</a> element.
			 *
			 * @attribute firstPageLinkLabel
			 * @default 'first'
			 * @type String
			 */
			firstPageLinkLabel: {
				value: FIRST,
				validator: isString
			},

			/**
			 * The <a href="Node.html">Node</a> or template to be used as the
	         * last link element.
			 *
			 * @attribute lastPageLink
			 * @default Generated anchor element.
			 * @type Node | String
			 */
			lastPageLink: {
				setter: A.one,
				valueFn: function() {
					var label = this.get(LAST_PAGE_LINK_LABEL);

					return A.Node.create(LAST_LINK_TPL).html(label);
				}
			},

			/**
			 * The label used as content of the
	         * <a href="Paginator.html#config_lastPageLink">lastPageLink</a> element.
			 *
			 * @attribute lastPageLinkLabel
			 * @default 'last'
			 * @type String
			 */
			lastPageLinkLabel: {
				value: LAST,
				validator: isString
			},

			/**
			 * The max number of page links to be displayed. If lower than the
	         * total number of pages they are still navigable using next and prev
	         * links.
			 *
			 * @attribute maxPageLinks
			 * @default 10
			 * @type Number
			 */
			maxPageLinks: {
				value: 10,
				getter: function(v) {
					var totalPages = this.get(TOTAL_PAGES);

					// maxPageLinks cannot be bigger than totalPages
					return Math.min(totalPages, v);
				},
				validator: isNumber
			},

			/**
			 * The <a href="Node.html">Node</a> or template to be used as the
	         * next link element.
			 *
			 * @attribute nextPageLink
			 * @default Generated anchor element.
			 * @type Node | String
			 */
			nextPageLink: {
				setter: A.one,
				valueFn: function() {
					var label = this.get(NEXT_PAGE_LINK_LABEL);

					return A.Node.create(NEXT_LINK_TPL).html(label);
				}
			},

			/**
			 * The label used as content of the
	         * <a href="Paginator.html#config_nextPageLink">nextPageLink</a> element.
			 *
			 * @attribute nextPageLinkLabel
			 * @default 'next &gt;'
			 * @type String
			 */
			nextPageLinkLabel: {
				value: concat(NEXT, GT_TPL),
				validator: isString
			},

			/**
			 * Page to display on initial paint.
			 *
			 * @attribute page
			 * @default 1
			 * @type Number
			 */
			page: {
				setter: function(v) {
					return num(v);
				},
				value: 1
			},

			/**
			 * HTML Template for the page links container.
			 *
			 * @attribute pageContainerTemplate
			 * @default Generated span HTML element.
			 * @type String
			 */
			pageContainerTemplate: {
				getter: function(v) {
					return A.Node.create(v).addClass(CSS_PAGINATOR_PAGE_CONTAINER);
				},
				value: PAGE_CONTAINER_TPL,
				validator: isString
			},

			/**
			 * <p>Function which set the content of the each page element. The passed
	         * function receive as arguments the reference for the page element
	         * node, the page number and the index of the page element.</p>
	         *
	         * Example:
	         *
	         * <pre><code>function(pageEl, pageNumber, index) {
			 *	 pageEl.html(pageNumber);
			 *	}</code></pre>
			 *
			 * @attribute pageLinkContent
			 * @default Basic function to set the html of the page element with the page number.
			 * @type function
			 */
			pageLinkContent: {
				value: function(pageEl, pageNumber, index) {
					pageEl.html(pageNumber);
				},
				validator: isFunction
			},

			/**
			 * HTML Template for the link elements.
			 *
			 * @attribute pageLinkTemplate
			 * @default Generated anchor HTML element.
			 * @type String
			 */
			pageLinkTemplate: {
				getter: function(v) {
					var node = A.Node.create(v);

					return node.addClass(
						concat(CSS_PAGINATOR_LINK, CSS_PAGINATOR_PAGE_LINK)
					);
				},
				value: PAGE_LINK_TPL,
				validator: isString
			},

			/**
			 * Node element to display the page report (i.e., (1 of 100)).
			 *
			 * @attribute pageReportEl
			 * @default Generated span HTML element.
			 * @type String
			 */
			pageReportEl: {
				setter: A.one,
				valueFn: function() {
					var label = this.get(PAGE_REPORT_LABEL_TEMPLATE);

					return A.Node.create(PAGE_REPORT_TPL).html(label);
				}
			},

			/**
			 * Template for the
	         * <a href="Paginator.html#config_pageReportEl">pageReportEl</a> content.
	         * Note the placeholders for the page {page} and the total pages
	         * {totalPages}.
			 *
			 * @attribute pageReportLabelTemplate
			 * @default '({page} of {totalPages})'
			 * @type String
			 */
			pageReportLabelTemplate: {
				getter: function() {
					var instance = this;

					return A.substitute(PAGE_REPORT_LABEL_TPL, {
						page: instance.get(PAGE),
						totalPages: instance.get(TOTAL_PAGES)
					});
				},
				validator: isString
			},

			/**
			 * The <a href="Node.html">Node</a> or template to be used as the
	         * prev link element.
			 *
			 * @attribute prevPageLink
			 * @default Generated anchor element.
			 * @type Node | String
			 */
			prevPageLink: {
				setter: A.one,
				valueFn: function() {
					var label = this.get(PREV_PAGE_LINK_LABEL);

					return A.Node.create(PREV_LINK_TPL).html(label);
				}
			},

			/**
			 * The label used as content of the
	         * <a href="Paginator.html#config_prevPageLink">prevPageLink</a> element.
			 *
			 * @attribute prevPageLinkLabel
			 * @default '&lt; prev'
			 * @type String
			 */
			prevPageLinkLabel: {
				value: concat(LT_TPL, PREV),
				validator: isString
			},

			/**
			 * Array to be displayed on the generated HTML Select element with the
	         * <a href="Paginator.html#config_rowsPerPage">rowsPerPage</a>
	         * information. (i.e., [1,3,5,7], will display these values on the
	         * select)
			 *
			 * @attribute rowsPerPageOptions
			 * @default []
			 * @type Array
			 */
			rowsPerPageOptions: {
				value: {},
				validator: isArray
			},

			/**
			 * Number of records constituting a "page".
			 *
			 * @attribute rowsPerPage
			 * @default 1
			 * @type Number
			 */
			rowsPerPage: {
				setter: function(v) {
					return num(v);
				},
				value: 1
			},

			/**
			 * Node element to display the
	         * <a href="Paginator.html#config_rowsPerPage">rowsPerPage</a>.
			 *
			 * @attribute rowsPerPageEl
			 * @default Generated select HTML element.
			 * @type Node | String
			 */
			rowsPerPageEl: {
				setter: A.one,
				valueFn: function() {
					return A.Node.create(ROWS_PER_PAGE_TPL);
				}
			},

			/**
			 * Generates information to the <code>changeRequest</code> event. See
	         * <a href="Paginator.html#method_changeRequest">changeRequest</a>.
			 *
			 * @attribute state
			 * @default {}
			 * @type Object
			 */
			state: {
				setter: function(v) {
					return this._setState(v);
				},
				getter: function(v) {
					return this._getState(v);
				},
				value: {},
				validator: isObject
			},

			/**
			 * Template used to render controls. The string will be used as
	         * innerHTML on all specified container nodes. Bracketed keys (e.g.
	         * {pageLinks}) in the string will be replaced with an instance of the
	         * so named ui component.
			 *
			 * @attribute template
			 * @default '{FirstPageLink} {PrevPageLink} {PageLinks} {NextPageLink} {LastPageLink} {CurrentPageReport} {Total} {RowsPerPageSelect}'
			 * @type String
			 */
			template: {
				getter: function(v) {
					return this._getTemplate(v);
				},
				writeOnce: true,
				value: DEFAULT_OUTPUT_TPL,
				validator: isString
			},

			/**
			 * Total number of records to paginate through.
			 *
			 * @attribute total
			 * @default 0
			 * @type Number
			 */
			total: {
				setter: function(v) {
					return this._setTotal(v);
				},
				value: 0,
				validator: isNumber
			},

			/**
			 * Node element to display the total information.
			 *
			 * @attribute totalEl
			 * @default Generated span HTML element.
			 * @type String
			 */
			totalEl: {
				setter: A.one,
				valueFn: function() {
					var label = this.get(TOTAL_LABEL);

					return A.Node.create(TOTAL_TPL).html(label);
				}
			},

			/**
			 * The label markup to the total information.
			 *
			 * @attribute totalLabel
			 * @default '(Total {total})'
			 * @type String
			 */
			totalLabel: {
				getter: function() {
					var instance = this;

					return A.substitute(TOTAL_LABEL_TPL, {
						total: instance.get(TOTAL)
					});
				},
				validator: isString
			},

			/**
			 * Number of pages. Calculated based on the
	         * <a href="Paginator.html#config_total">total</a> divided by the
	         * <a href="Paginator.html#config_rowsPerPage">rowsPerPage</a>.
			 *
			 * @attribute totalPages
			 * @default 0
			 * @type Number
			 */
			totalPages: {
				readOnly: true,
				getter: function(v) {
					return Math.ceil(
						this.get(TOTAL) / this.get(ROWS_PER_PAGE)
					);
				}
			}
		},

		prototype: {
			/**
			 * Store the last state object used on the <a href="Paginator.html#method_changeRequest">changeRequest</a> event.
			 *
			 * @property lastState
			 * @type Object | null
			 * @protected
			 */
			lastState: null,

			/**
			 * Cached template after <a href="YUI.html#method_substitute">YUI
		     * substitute</a> were applied.
			 *
			 * @property templatesCache
			 * @type String
			 * @protected
			 */
			templatesCache: null,

			/**
			 * Create the DOM structure for the Paginator. Lifecycle.
			 *
			 * @method renderUI
			 * @protected
			 */
			renderUI: function() {
				var instance = this;
				var containers = instance.get(CONTAINERS);

				containers.unselectable();

				instance._renderRowsPerPageOptions();

				instance._renderTemplateUI();

				containers.addClass(CSS_PAGINATOR_CONTAINER);
			},

			/**
			 * Bind the events on the Paginator UI. Lifecycle.
			 *
			 * @method bindUI
			 * @protected
			 */
			bindUI: function() {
				var instance = this;

				instance._delegateDOM();

				instance.publish('changeRequest');
				instance.after('stateChange', A.bind(instance._afterSetState, instance));
				instance.before('stateChange', A.bind(instance._beforeSetState, instance));

				// invoke _renderTemplateUI to recreate the template markup
				instance.after('maxPageLinksChange', A.bind(instance._renderTemplateUI, instance));
				instance.after('rowsPerPageChange', A.bind(instance._renderTemplateUI, instance));
				instance.after('totalChange', A.bind(instance._renderTemplateUI, instance));
			},

			/**
			 * Sync the Paginator UI. Lifecycle.
			 *
			 * @method syncUI
			 * @protected
			 */
			syncUI: function() {
				var instance = this;

				// fire changeRequest to the first state
				instance.changeRequest();
			},

			/**
			 * Descructor lifecycle implementation for the Paginator class.
			 * Purges events attached to the node (and all child nodes).
			 *
			 * @method destructor
			 * @protected
			 */
			destructor: function() {
				var instance = this;

				instance.get(CONTAINERS).remove(true);
			},

			/**
			 * Sync the Paginator links UI.
			 *
			 * @method _syncPageLinksUI
			 * @protected
			 */
			_syncPageLinksUI: function() {
				var instance = this;
				var containers = instance.get(CONTAINERS);
				var page = instance.get(PAGE);

				// creating range to show on the pageLinks, keep the current page on center
				var range = instance.calculateRange(page);

				// loop all containers
				containers.each(function(node) {
					var index = 0;
					var pageNumber = range.start;
					// query all anchor that represents the page links
					var pageLinks = node.all(DOT+CSS_PAGINATOR_PAGE_LINK);

					if (pageLinks) {
						pageLinks.removeClass(CSS_PAGINATOR_CURRENT_PAGE);

						// loop all pages from range.start to range.end
						while (pageNumber <= range.end) {
							// get the anchor pageEl and set the label to be the number of the current page
							var pageEl = pageLinks.item(index);

							// invoke the handler to set the page link content
							instance.get(PAGE_LINK_CONTENT).apply(instance, [pageEl, pageNumber, index]);

							// uset an attribute page on the anchor to retrieve later when _onClickPageLinkEl fires
							pageEl.setAttribute(PAGE, pageNumber);

							if (pageNumber == page) {
								// search for the current page and addClass saying it's the current
								pageEl.addClass(CSS_PAGINATOR_CURRENT_PAGE);
							}

							index++;
							pageNumber++;
						}
					}
				});
			},

			/**
			 * Sync the Paginator page report UI.
			 *
			 * @method _syncPageLinksUI
			 * @protected
			 */
			_syncPageReportUI: function(event) {
				var instance = this;
				var containers = instance.get(CONTAINERS);

				containers.each(function(node) {
					// search for the respective pageReportEl
					var pageReportEl = node.one(DOT+CSS_PAGINATOR_PAGE_REPORT);

					if (pageReportEl) {
						// update its label with the page report template, eg. (1 of 100)
						pageReportEl.html(
							instance.get(PAGE_REPORT_LABEL_TEMPLATE)
						);
					}
				});
			},

			/**
			 * Create a range to display on the pageLinks, keep the current page on
		     * center.
			 *
			 * @method calculateRange
			 * @param {Type} name description
			 * @return {Object} Object containing the start and end information.
			 */
			calculateRange: function(page) {
				var instance = this;
				var totalPages = instance.get(TOTAL_PAGES);
				var maxPageLinks = instance.get(MAX_PAGE_LINKS);
				// calculates the center page link index
				var offset = Math.ceil(maxPageLinks/2);

				// find the start range to show on the page links
				// Math.min pick the minimum value when the page number is near totalPages value
				// this fixes when the offset is small and generates less than [maxPageLinks] page links
				var start = Math.min(
					// Math.max(x, 1) doesn't allow negative or zero values
					Math.max(page - offset, 1), (totalPages - maxPageLinks + 1)
				);

				// find the end range, the range has always maxPageLinks size
				// so, (start + maxPageLinks - 1) try to find the end range
				// Math.min with totalPages doesn't allow values bigger than totalPages
				var end = Math.min(start + maxPageLinks - 1, totalPages);

				return {
					start: start,
					end: end
				};
			},

			/**
			 * Fires <a href="Paginator.html#event_changeRequest">changeRequest</a>
		     * event. This is the most important event because it's responsible to
		     * update the UI, invoked <code>.setState(newState)</code> to update the
		     * UI.
			 *
			 * @method changeRequest
			 */
			changeRequest: function() {
				var instance = this;
				var state = instance.get(STATE);

				instance.fire('changeRequest', { state: state });
			},

			/**
			 * Loop through all
		     * <a href="Paginator.html#config_containers">containers</a> and execute the
		     * passed callback.
			 *
			 * @method eachContainer
			 * @param {function} fn Callback
			 */
			eachContainer: function(fn) {
				var instance = this;

				instance.get(CONTAINERS).each(function(node) {
					if (node) {
						fn.apply(instance, arguments);
					}
				});
			},

			/**
			 * Check if there is a next page.
			 *
			 * @method hasNextPage
			 * @return {boolean}
			 */
			hasNextPage: function() {
				var instance = this;

				return instance.hasPage(
					instance.get(PAGE) + 1
				);
			},

			/**
			 * Check if the <code>page</code> exists.
			 *
			 * @method hasPage
			 * @param {Number} page
			 * @return {boolean}
			 */
			hasPage: function(page) {
				var instance = this;
				var totalPages = instance.get(TOTAL_PAGES);

				return ( (page > 0) && (page <= totalPages) );
			},

			/**
			 * Check if there is a previous page.
			 *
			 * @method hasPrevPage
			 * @return {boolean}
			 */
			hasPrevPage: function() {
				var instance = this;

				return instance.hasPage(
					instance.get(PAGE) - 1
				);
			},

			/**
			 * Render rows per page options.
			 *
			 * @method _renderRowsPerPageOptions
			 * @protected
			 */
			_renderRowsPerPageOptions: function() {
				var instance = this;
				var i = 0;
				var rowsPerPageEl = instance.get(ROWS_PER_PAGE_EL);
				var rowsPerPageOptions = instance.get(ROWS_PER_PAGE_OPTIONS);

				A.each(rowsPerPageOptions, function(value) {
					rowsPerPageEl.getDOM().options[i++] = new Option(value, value);
				});
			},

			/**
			 * Render the UI controls based on the
		     * <a href="Paginator.html#config_template">template</a>.
			 *
			 * @method _renderTemplateUI
			 * @protected
			 */
			_renderTemplateUI: function() {
				var instance = this;
				var containers = instance.get(CONTAINERS);

				instance.templatesCache = null;

				// loading HTML template on the containers
				containers.html(
					instance.get(TEMPLATE)
				);

				// sync pageLinks
				instance._syncPageLinksUI();

				// sync page report, eg. (1 of 100)
				instance._syncPageReportUI();

				// bind the DOM events after _renderTemplateUI
				instance._bindDOMEvents();
			},

			/**
			 * Public setter for <a href="Paginator.html#config_state">state</a>.
			 *
			 * @method setState
			 * @param {Object} v New state object.
			 */
			setState: function(v) {
				var instance = this;

				instance.set(STATE, v);
			},

			/**
			 * Private getter for <a href="Paginator.html#config_state">state</a>.
			 *
			 * @method _getState
			 * @param {Object} v Current state object.
			 * @protected
			 * @return {Object} State object.
			 */
			_getState: function(v) {
				var instance = this;

				return {
					before: instance.lastState,
					paginator: instance,
					page: instance.get(PAGE),
					total: instance.get(TOTAL),
					totalPages: instance.get(TOTAL_PAGES),
					rowsPerPage: instance.get(ROWS_PER_PAGE)
				};
			},

			/**
			 * Getter for <a href="Paginator.html#config_template">template</a>.
			 *
			 * @method _getTemplate
			 * @param {String} v Current template.
			 * @protected
			 * @return {String} Current template.
			 */
			_getTemplate: function(v) {
				var instance = this;

				var outer = function(key) {
					return instance.get(key).outerHTML();
				};

				// if template is not cached...
				if (!instance.templatesCache) {
					var page = 0;
					var totalPages = instance.get(TOTAL_PAGES);
					var maxPageLinks = instance.get(MAX_PAGE_LINKS);
					var pageContainer = instance.get(PAGE_CONTAINER_TEMPLATE);

					// crate the anchor to be the page links
					while (page++ < maxPageLinks) {
						pageContainer.append(
							instance.get(PAGE_LINK_TEMPLATE)
						);
					}

					// substitute the {keys} on the templates with the real outerHTML templates
					instance.templatesCache = A.substitute(v,
						{
							CurrentPageReport: outer(PAGE_REPORT_EL),
							FirstPageLink: outer(FIRST_PAGE_LINK),
							LastPageLink: outer(LAST_PAGE_LINK),
							NextPageLink: outer(NEXT_PAGE_LINK),
							PageLinks: pageContainer.outerHTML(),
							PrevPageLink: outer(PREV_PAGE_LINK),
							RowsPerPageSelect: outer(ROWS_PER_PAGE_EL),
							Total: outer(TOTAL_EL)
						}
					);
				}

				return instance.templatesCache;
			},

			/**
			 * Private setter for <a href="Paginator.html#config_state">state</a>.
			 *
			 * @method _setState
			 * @param {Object} v New state object.
			 * @protected
			 * @return {Object}
			 */
			_setState: function(v) {
				var instance = this;

				A.each(v, function(value, key) {
					instance.set(key, value);
				});

				return v;
			},

			/**
			 * Setter for <a href="Paginator.html#config_total">total</a>.
			 *
			 * @method _setTotal
			 * @param {Number} v
			 * @protected
			 * @return {Number}
			 */
			_setTotal: function(v) {
				var instance = this;
				var alwaysVisible = instance.get(ALWAYS_VISIBLE);
				var containers = instance.get(CONTAINERS);

				// if !alwaysVisible and there is nothing to show, hide it
				if (!alwaysVisible && (v === 0)) {
					containers.hide();
				}
				else {
					containers.show();
				}

				return v;
			},

			/**
			 * Fires after the value of the
		     * <a href="Paginator.html#config_state">state</a> attribute change.
			 *
			 * @method _afterSetState
			 * @param {EventFacade} event
			 * @protected
			 */
			_afterSetState: function(event) {
				var instance = this;

				instance._syncPageLinksUI();
				instance._syncPageReportUI();
			},

			/**
			 * Fires before the value of the
			 * <a href="Paginator.html#config_state">state</a> attribute change.
			 *
			 * @method _beforeSetState
			 * @param {EventFacade} event
			 * @protected
			 */
			_beforeSetState: function(event) {
				var instance = this;

				instance.lastState = event.prevVal;
			},

			/**
			 * Click event handler for the
		     * <a href="Paginator.html#config_firstLinkEl">firstLinkEl</a>.
			 *
			 * @method _onClickFirstLinkEl
		     * @param {EventFacade} event
			 * @protected
			 */
			_onClickFirstLinkEl: function(event) {
				var instance = this;

				instance.set(PAGE, 1);

				instance.changeRequest();

				event.halt();
			},

			/**
			 * Click event handler for the
		     * <a href="Paginator.html#config_prevLinkEl">prevLinkEl</a>.
			 *
			 * @method _onClickPrevLinkEl
		     * @param {EventFacade} event
			 * @protected
			 */
			_onClickPrevLinkEl: function(event) {
				var instance = this;

				if (instance.hasPrevPage()) {
					var page = instance.get(PAGE);

					instance.set(PAGE, page - 1);

					instance.changeRequest();
				}

				event.halt();
			},

			/**
			 * Click event handler for the
		     * <a href="Paginator.html#config_pageLinkEl">pageLinkEl</a>.
			 *
			 * @method _onClickPageLinkEl
		     * @param {EventFacade} event
			 * @protected
			 */
			_onClickPageLinkEl: function(event) {
				var instance = this;
				var pageNumber = event.currentTarget.attr(PAGE);

				instance.set(PAGE, pageNumber);

				instance.changeRequest();

				event.halt();
			},

			/**
			 * Click event handler for the
		     * <a href="Paginator.html#config_nextLinkEl">nextLinkEl</a>.
			 *
			 * @method _onClickNextLinkEl
		     * @param {EventFacade} event
			 * @protected
			 */
			_onClickNextLinkEl: function(event) {
				var instance = this;

				if (instance.hasNextPage()) {
					var page = instance.get(PAGE);

					instance.set(PAGE, page + 1);

					instance.changeRequest();
				}

				event.halt();
			},

			/**
			 * Click event handler for the
		     * <a href="Paginator.html#config_lastLinkEl">lastLinkEl</a>.
			 *
			 * @method _onClickLastLinkEl
		     * @param {EventFacade} event
			 * @protected
			 */
			_onClickLastLinkEl: function(event) {
				var instance = this;
				var totalPages = instance.get(TOTAL_PAGES);

				instance.set(PAGE, totalPages);

				instance.changeRequest();

				event.halt();
			},

			/**
			 * Bind DOM events on the Paginator UI.
			 *
			 * @method _bindDOMEvents
			 * @protected
			 */
			_bindDOMEvents: function() {
				var instance = this;

				// loop all containers...
				instance.eachContainer(function(node) {
					// search for selects rows per page elements
					var rowsPerPageEl = node.one(DOT+CSS_PAGINATOR_ROWS_PER_PAGE);

					if (rowsPerPageEl) {
						// update the value with the current rowsPerPage
						rowsPerPageEl.val(
							instance.get(ROWS_PER_PAGE)
						);

						// detach change event
						rowsPerPageEl.detach('change');

						// bind change event to update the rowsPerPage
						rowsPerPageEl.on('change', function(event) {
							var rowsPerPage = instance.get(ROWS_PER_PAGE);

							try {
								// prevent IE error when first access .val() on A.Node when wraps a SELECT
								rowsPerPage = event.target.val();
							}
							catch(e) {}

							// reset the page before render the pageLinks again
							instance.set(PAGE, 1);

							// set rowsPerPage, this will render the UI again
							instance.set(ROWS_PER_PAGE, rowsPerPage);

							instance.changeRequest();
						});
					}
				});
			},

			/**
			 * Delegate DOM events on the Paginator UI.
			 *
			 * @method _delegateDOM
			 * @protected
			 */
			_delegateDOM: function() {
				var instance = this;

				instance.eachContainer(function(node, i) {
					node.delegate('click', A.bind(instance._onClickFirstLinkEl, instance), DOT+CSS_PAGINATOR_FIRST_LINK);
					node.delegate('click', A.bind(instance._onClickPrevLinkEl, instance), DOT+CSS_PAGINATOR_PREV_LINK);
					node.delegate('click', A.bind(instance._onClickPageLinkEl, instance), DOT+CSS_PAGINATOR_PAGE_LINK);
					node.delegate('click', A.bind(instance._onClickNextLinkEl, instance), DOT+CSS_PAGINATOR_NEXT_LINK);
					node.delegate('click', A.bind(instance._onClickLastLinkEl, instance), DOT+CSS_PAGINATOR_LAST_LINK);
				});
			}
		}
	}
);

A.Paginator = Paginator;

}, '@VERSION@' ,{skinnable:true, requires:['aui-base','substitute']});