Source: deliteful/list/Renderer.js

/** @module deliteful/list/Renderer */
define([
	"dcl/dcl",
	"requirejs-dplugins/jquery!attributes/classes",
	"delite/register",
	"delite/Widget"
], function (dcl, $, register, Widget) {

	/**
	 * The base class for a widget that render an item or its category inside a deliteful/list/List widget.
	 * 
	 * This base class provide all the infrastructure that a deliteful/list/List widget
	 * expect from a renderer, including keyboard navigation support.
	 * 
	 * Focusability and Keyboard navigation order for a renderer instance is defined using
	 * the navindex attribute on the rendered nodes:
	 * - no navindex attribute value means that the node is not focusable
	 * - a navindex attribute means that the node is focusable. When navigating
	 *  the renderer instance using arrow keys, the traversal order is the following:
	 *  - the nodes with the lowest navindex value comes first
	 *  - if two nodes have the same navindex value, the one that is before the other one in the DOM
	 *  comes first.
	 * @class module:deliteful/list/Renderer
	 * @augments module:delite/Widget
	 */
	return dcl([Widget], /** @lends module:deliteful/list/Renderer# */ {

		/**
		 * The list item to render.
		 * @member {Object}
		 * @default {}
		 */
		item: {}, // must be initialized to an empty object because it is expected by the template

		/**
		 * Contains all the renderer nodes that can be focused, in the same order
		 * that they are to be focused during keyboard navigation with the left and right arrow
		 * keys.
		 * @member {Node[]}
		 * @private
		 */
		_focusableChildren: null,

		//////////// PROTECTED METHODS ///////////////////////////////////////

		render: dcl.after(function () {
			if (!this.renderNode) {
				throw new Error("render must define a renderNode property on the Renderer."
						+ " Example using attach-point in a template: "
						+ "<template><div attach-point='renderNode'></div></template>");
			}
			this.renderNode.tabIndex = -1;
			$(this.renderNode).addClass("d-list-cell");
			this.updateFocusableChildren();
		}),

		// Interface from List to Renderer to navigate fields

		/**
		 * Retrieves the first focusable child.
		 * @returns {Element}
		 * @protected
		 */
		getFirst: function () {
			if (this._focusableChildren && this._focusableChildren.length) {
				return this._focusableChildren[0];
			} else {
				return null;
			}
		},

		/**
		 * Retrieves the last focusable child.
		 * @returns {Element}
		 * @protected
		 */
		getLast: function () {
			if (this._focusableChildren && this._focusableChildren.length) {
				return this._focusableChildren[this._focusableChildren.length - 1];
			} else {
				return null;
			}
		},

		/**
		 * Retrieves the next focusable child after another child.
		 * @param {Element} child the child from which to retrieve the next focusable child
		 * @returns {Element}
		 * @protected
		 */
		getNext: function (child) {
			return this.getNextFocusableChild(child, 1);
		},

		/**
		 * Retrieves the previous focusable child before another child.
		 * @param {Element} child the child from which to retrieve the previous focusable child
		 * @returns {Element}
		 * @protected
		 */
		getPrev: function (child) {
			return this.getNextFocusableChild(child, -1);
		},

		/**
		 * This method update the list of children of the renderer that can
		 * be focused during keyboard navigation.
		 * If the list of navigable children of the renderer is updated after the
		 * render step has been executed, this method must be
		 * called to take into account the new list.
		 * If the list of navigable children is defined during the render
		 * step, there is no need to call this method.
		 */
		updateFocusableChildren: function () {
			if (this._focusableChildren) {
				for (var i = 0; i < this._focusableChildren.length; i++) {
					delete this._focusableChildren[i].tabIndex;
				}
			}
			// parse the renderNode content to retrieve the ordered list of focusable children
			var nodes = Array.prototype.slice.call(this.renderNode.querySelectorAll("[navindex]"), 0);
			this._focusableChildren = nodes.slice(0).sort(function (a, b) {
				var navindexA = parseInt(a.getAttribute("navindex"), 10);
				var navindexB = parseInt(b.getAttribute("navindex"), 10);
				if (navindexA === navindexB) {
					return nodes.indexOf(a) - nodes.indexOf(b);
				} else {
					return navindexA - navindexB;
					
				}
			});
			// update the focusable children nodes
			for (i = 0; i < this._focusableChildren.length; i++) {
				var node = this._focusableChildren[i];
				node.tabIndex = -1;
				if (!node.id) {
					node.id = this.id + "-cell-" + i;
				}
			}
		},

		/**
		 * Get the next renderer child that can be focused using arrow keys.
		 * @param {Element} fromChild The child from which the next focusable child is requested
		 * @param {number} dir The direction, from fromChild, of the next child: 1 for the child that
		 * comes after in the focusable order, -1 for the child that comes before.
		 * @returns {Element} The next focusable child if there is one.
		 * @protected
		 */
		getNextFocusableChild: function (fromChild, dir) {
			if (this._focusableChildren && fromChild !== this) {
				// retrieve the position of the from node
				var fromChildIndex = fromChild ? this._focusableChildren.indexOf(fromChild) : -1;
				var nextChildIndex = fromChildIndex + dir;
				if (nextChildIndex >= 0 && nextChildIndex < this._focusableChildren.length) {
					return this._focusableChildren[nextChildIndex]; // Widget
				} else {
					return null;
				}
			}
		}

	});

});