AUI.add('aui-rating', function(A) {
/**
* The Rating Utility - The Star Rating creates a non-obstrusive star rating
* control, could be based on a set of radio input boxes.
*
* @module aui-rating
*/
var L = A.Lang,
isBoolean = L.isBoolean,
isNumber = L.isNumber,
isString = L.isString,
isNodeList = function(v) {
return (v instanceof A.NodeList);
},
isNode = function(v) {
return (v instanceof A.Node);
},
ANCHOR = 'a',
BLANK = '',
BOUNDING_BOX = 'boundingBox',
CAN_RESET = 'canReset',
CLEARFIX = 'clearfix',
CONTENT_BOX = 'contentBox',
DEFAULT_SELECTED = 'defaultSelected',
DISABLED = 'disabled',
DOT = '.',
ELEMENT = 'element',
ELEMENTS = 'elements',
EMPTY_STR = '',
HELPER = 'helper',
HOVER = 'hover',
HREF = 'href',
HREF_JAVASCRIPT = 'javascript:;',
ID = 'id',
INPUT = 'input',
INPUT_NAME = 'inputName',
LABEL = 'label',
LABEL_NODE = 'labelNode',
NAME = 'name',
NODE_NAME = 'nodeName',
OFF = 'off',
ON = 'on',
RATING = 'rating',
RATING_ELEMENT = 'ratingElement',
SELECTED_INDEX = 'selectedIndex',
SHOW_TITLE = 'showTitle',
SIZE = 'size',
TITLE = 'title',
VALUE = 'value',
EV_RATING_ITEM_CLICK = 'itemClick',
EV_RATING_ITEM_SELECT = 'itemSelect',
EV_RATING_ITEM_OUT = 'itemOut',
EV_RATING_ITEM_OVER = 'itemOver',
getCN = A.ClassNameManager.getClassName,
CSS_CLEAR_FIX = getCN(HELPER, CLEARFIX),
CSS_RATING_LABEL_EL = getCN(RATING, LABEL, ELEMENT),
CSS_RATING_EL = getCN(RATING, ELEMENT),
CSS_RATING_EL_HOVER = getCN(RATING, ELEMENT, HOVER),
CSS_RATING_EL_OFF = getCN(RATING, ELEMENT, OFF),
CSS_RATING_EL_ON = getCN(RATING, ELEMENT, ON),
TPL_LABEL = '<div class="'+CSS_RATING_LABEL_EL+'"></div>',
TPL_RATING_EL = '<a href="'+HREF_JAVASCRIPT+'"></a>',
TPL_RATING_EL_DISABLED = '<span></span>';
/**
* <p><img src="assets/images/aui-rating/main.png"/></p>
*
* A base class for Rating, providing:
* <ul>
* <li>A non-obstrusive star rating control</li>
* <li>Could be based on a set of radio input boxes</li>
* </ul>
*
* Quick Example:<br/>
*
* <pre><code>var instance = new A.Rating({
* boundingBox: '#rating',
* defaultSelected: 3,
* disabled: false,
* label: 'Label'
* }).render();
* </code></pre>
*
* Check the list of <a href="Rating.html#configattributes">Configuration Attributes</a> available for
* Rating.
*
* @param config {Object} Object literal specifying widget configuration properties.
*
* @class Rating
* @constructor
* @extends Component
*/
var Rating = A.Component.create(
{
/**
* Static property provides a string to identify the class.
*
* @property Rating.NAME
* @type String
* @static
*/
NAME: 'rating',
/**
* Static property used to define the default attribute
* configuration for the Rating.
*
* @property Rating.ATTRS
* @type Object
* @static
*/
ATTRS: {
/**
* Whether the Rating is disabled or not. Disabled Ratings don't allow
* hover or click, just display selected stars.
*
* @attribute disabled
* @default false
* @type boolean
*/
disabled: {
value: false,
validator: isBoolean
},
/**
* If <code>true</code> could be reseted (i.e., have no values
* selected).
*
* @attribute canReset
* @default true
* @type boolean
*/
canReset: {
value: true,
validator: isBoolean
},
/**
* The number of selected starts when the Rating render.
*
* @attribute defaultSelected
* @default 0
* @writeOnce
* @type Number
*/
defaultSelected: {
value: 0,
writeOnce: true,
validator: isNumber
},
/**
* <a href="NodeList.html">NodeList</a> of elements used on the
* Rating. Each element is one Star.
*
* @attribute elements
* @writeOnce
* @readOnly
* @type NodeList
*/
elements: {
validator: isNodeList
},
/**
* Hidden input to handle the selected value. This hidden input
* replace the radio elements and keep the same name.
*
* @attribute hiddenInput
* @type Node
*/
hiddenInput: {
validator: isNode
},
/**
* Name of the <a
* href="Rating.html#config_hiddenInput">hiddenInput</a> element. If
* not specified will use the name of the replaced radio.
*
* @attribute inputName
* @default ''
* @type String
*/
inputName: {
value: BLANK,
validator: isString
},
/**
* Label to be displayed with the Rating elements.
*
* @attribute label
* @default ''
* @type String
*/
label: {
value: BLANK,
validator: isString
},
/**
* DOM Node to display the text of the StarRating. If not
* specified try to query using HTML_PARSER an element inside
* boundingBox which matches <code>aui-rating-label-element</code>.
*
* @attribute labelNode
* @default Generated div element.
* @type String
*/
labelNode: {
valueFn: function() {
return A.Node.create(TPL_LABEL);
},
validator: isNode
},
ratingElement: {
valueFn: function() {
var instance = this;
var ratingElement = A.Node.create(
instance.get(DISABLED) ? TPL_RATING_EL_DISABLED : TPL_RATING_EL
);
return ratingElement.addClass(CSS_RATING_EL);
}
},
/**
* Stores the index of the selected element.
*
* @attribute selectedIndex
* @default -1
* @type Number
*/
selectedIndex: {
value: -1,
validator: isNumber
},
/**
* If <code>true</code> will extract the value of the
* <code>title</code> attribute on the radio, and use it on the
* generated Rating elements.
*
* @attribute showTitle
* @default true
* @type boolean
*/
showTitle: {
value: true,
validator: isBoolean
},
/**
* Number of Rating elements to be displayed.
*
* @attribute size
* @default 5
* @type Number
*/
size: {
value: 5,
validator: function(v) {
return isNumber(v) && (v > 0);
}
},
/**
* If set, will be used when there is no DOM <code>title</code> on the
* radio elements.
*
* @attribute title
* @default null
* @type String
*/
title: null,
/**
* Stores the value of the current selected Rating element.
*
* @attribute value
* @default null
* @type String
*/
value: null
},
/**
* Object hash, defining how attribute values are to be parsed from
* markup contained in the widget's content box.
*
* @property StarRating.HTML_PARSER
* @type Object
* @static
*/
HTML_PARSER: {
elements: function(srcNode) {
return srcNode.all(DOT+CSS_RATING_EL);
},
label: function(srcNode) {
var labelNode = srcNode.one(DOT+CSS_RATING_LABEL_EL);
if (labelNode) {
return labelNode.html();
}
},
labelNode: DOT+CSS_RATING_LABEL_EL
},
prototype: {
/**
* Construction logic executed during Rating instantiation. Lifecycle.
*
* @method initializer
* @protected
*/
initializer: function(){
var instance = this;
instance.inputElementsData = {};
instance.after('labelChange', this._afterSetLabel);
},
/**
* Create the DOM structure for the Rating. Lifecycle.
*
* @method renderUI
* @protected
*/
renderUI: function () {
var instance = this;
var contentBox = instance.get(CONTENT_BOX);
contentBox.addClass(CSS_CLEAR_FIX);
instance._parseInputElements();
instance._renderLabel();
instance._renderElements();
},
/**
* Bind the events on the Rating UI. Lifecycle.
*
* @method bindUI
* @protected
*/
bindUI: function () {
var instance = this;
instance._createEvents();
instance.on('click', instance._handleClickEvent);
instance.on('mouseover', instance._handleMouseOverEvent);
instance.on('mouseout', instance._handleMouseOutEvent);
},
/**
* Sync the Rating UI. Lifecycle.
*
* @method syncUI
* @protected
*/
syncUI: function(){
var instance = this;
instance._syncElements();
instance._syncLabelUI();
},
/**
* Clear all selected starts to the default state.
*
* @method clearSelection
*/
clearSelection: function() {
var instance = this;
instance.get(ELEMENTS).each(function(node) {
node.removeClass(CSS_RATING_EL_ON);
node.removeClass(CSS_RATING_EL_HOVER);
});
},
/**
* Selects the <code>index</code> Rating element.
*
* @method select
* @param {Number} index Index to be selected
*/
select: function(index) {
var instance = this;
var oldIndex = instance.get(SELECTED_INDEX);
var canReset = instance.get(CAN_RESET);
// clear selection when the selected element is clicked
if (canReset && (oldIndex == index)) {
index = -1;
}
instance.set(SELECTED_INDEX, index);
var selectedIndex = instance.get(SELECTED_INDEX);
var data = instance._getInputData(selectedIndex);
var title = (TITLE in data) ? data.title : BLANK;
var value = (VALUE in data) ? data.value : selectedIndex;
instance.fillTo(selectedIndex);
instance.set(TITLE, title);
instance.set(VALUE, value);
var hiddenInput = instance.get('hiddenInput');
hiddenInput.setAttribute(TITLE, title);
hiddenInput.setAttribute(VALUE, value);
},
/**
* Add the <code>className</code> on the the <code>index</code> element
* and all the previous Rating elements.
*
* @method fillTo
* @param {Number} index Index to be selected
* @param {String} className Class name to be applied when fill the Rating elements
*/
fillTo: function(index, className) {
var instance = this;
instance.clearSelection();
if (index >= 0) {
instance.get(ELEMENTS).some(function(node, i) {
node.addClass(className || CSS_RATING_EL_ON);
// stop loop when return true
return (i == Math.floor(index));
});
}
},
/**
* Finds the index of the <code>elem</code>.
*
* @method indexOf
* @param {Node} elem Rating element
* @return {Number}
*/
indexOf: function(elem) {
var instance = this;
return instance.get(ELEMENTS).indexOf(elem);
},
/**
* Check if the Rating element can fire the custom events. Disabled
* elements won't fire nothing.
*
* @method _canFireCustomEvent
* @param {EventFacade} event
* @protected
* @return {Boolean}
*/
_canFireCustomEvent: function(event) {
var instance = this;
var domTarget = event.domEvent.target;
// checks if the widget is not disabled and if the dom event is firing with a item as target
// do not fire custom events for other elements into the boundingBox
return !instance.get(DISABLED) && domTarget.hasClass(CSS_RATING_EL);
},
/**
* Create rating elements based on the <code>size</code>
* attribute. It's only invoked when the HTML_PARSER does not find
* nothing.
*
* @method _createElements
* @protected
* @return {NodeList}
*/
_createElements: function() {
var instance = this;
var elements = [];
var ratingElement = instance.get(RATING_ELEMENT);
for (var i = 0, size = this.get(SIZE); i < size; i++) {
elements.push(
ratingElement.clone()
);
}
return new A.NodeList(elements);
},
/**
* Create the custom events.
*
* @method _createEvents
* @protected
*/
_createEvents: function() {
var instance = this;
// create publish function for kweight optimization
var publish = function(name, fn) {
instance.publish(name, {
defaultFn: fn,
queuable: false,
emitFacade: true,
bubbles: true
});
};
/**
* Handles the itemClick event.
*
* @event itemClick
* @preventable _defRatingItemClickFn
* @param {Event.Facade} event The itemClick event.
* @type {Event.Custom}
*/
publish(
EV_RATING_ITEM_CLICK,
this._defRatingItemClickFn
);
/**
* Handles the itemSelect event.
*
* @event itemSelect
* @preventable _defRatingItemSelectFn
* @param {Event.Facade} event The itemSelect event.
* @type {Event.Custom}
*/
publish(
EV_RATING_ITEM_SELECT,
this._defRatingItemSelectFn
);
/**
* Handles the itemOver event.
*
* @event itemSelect
* @preventable _defRatingItemOverFn
* @param {Event.Facade} event The itemOver event.
* @type {Event.Custom}
*/
publish(
EV_RATING_ITEM_OVER,
this._defRatingItemOverFn
);
/**
* Handles the itemOut event.
*
* @event itemOut
* @preventable _defRatingItemOutFn
* @param {Event.Facade} event The itemOut event.
* @type {Event.Custom}
*/
publish(
EV_RATING_ITEM_OUT,
this._defRatingItemOutFn
);
},
/**
* Fires the itemClick event.
*
* @method _defRatingItemClickFn
* @param {EventFacade} event itemClick event facade
* @protected
*/
_defRatingItemClickFn: function(event) {
var instance = this;
var domEvent = event.domEvent;
instance.fire(EV_RATING_ITEM_SELECT, {
delegateEvent: event,
domEvent: domEvent,
ratingItem: domEvent.target
});
},
/**
* Fires the itemSelect event.
*
* @method _defRatingItemSelectFn
* @param {EventFacade} event itemSelect event facade
* @protected
*/
_defRatingItemSelectFn: function(event) {
var instance = this;
var domTarget = event.domEvent.target;
instance.select(
instance.indexOf(domTarget)
);
},
/**
* Fires the itemOut event.
*
* @method _defRatingItemOutFn
* @param {EventFacade} event itemOut event facade
* @protected
*/
_defRatingItemOutFn: function(event) {
var instance = this;
instance.fillTo(
instance.get(SELECTED_INDEX)
);
},
/**
* Fires the itemOver event.
*
* @method _defRatingItemOverFn
* @param {EventFacade} event itemOver event facade
* @protected
*/
_defRatingItemOverFn: function(event) {
var instance = this;
var index = instance.indexOf(event.domEvent.target);
instance.fillTo(index, CSS_RATING_EL_HOVER);
},
/**
* Parse the HTML radio elements from the markup to be Rating elements.
*
* @method _parseInputElements
* @protected
*/
_parseInputElements: function() {
var instance = this;
var boundingBox = instance.get(BOUNDING_BOX);
var inputs = boundingBox.all(INPUT);
var size = inputs.size();
var inputName = instance.get(INPUT_NAME);
var hiddenInput = A.Node.create('<input type="hidden" />');
if (size > 0) {
inputName = inputName || inputs.item(0).getAttribute(NAME);
instance.set(SIZE, size);
var labels = boundingBox.all('label');
inputs.each(function(node, index) {
var id = node.get(ID);
var label = EMPTY_STR;
if (id) {
// for a11y parse the <label> elments information
// checking if the node has a <label>
var labelEl = labels.filter('[for="'+id+'"]');
if (labelEl.size()) {
// if there is, extract the content of the label to use as content of the anchors...
label = labelEl.item(0).html();
}
}
instance.inputElementsData[index] = {
content: label,
value: node.getAttribute(VALUE) || index,
title: node.getAttribute(TITLE)
};
});
labels.remove(true);
inputs.remove(true);
}
if (inputName) {
hiddenInput.setAttribute(NAME, inputName);
boundingBox.appendChild(hiddenInput);
}
instance.set('hiddenInput', hiddenInput);
},
/**
* Render the Rating label.
*
* @method _renderLabel
* @protected
*/
_renderLabel: function() {
var instance = this;
instance.get(CONTENT_BOX).append(
instance.get(LABEL_NODE)
);
},
/**
* Render the Rating elements.
*
* @method _renderElements
* @protected
*/
_renderElements: function(elements) {
var instance = this;
var contentBox = instance.get(CONTENT_BOX);
// if not found any elements from the HTML_PARSER create them based on the size attribute
if (!instance.get(ELEMENTS).size()) {
instance.set(
ELEMENTS,
instance._createElements()
);
}
instance.get(ELEMENTS).each(
function(element, i) {
var data = instance._getInputData(i);
var content = data.content;
// try to use the pulled title data from the dom, otherwise use the TITLE attr, in the last case use the content
var title = data.title || instance.get(TITLE) || content;
// setting the content
if (content || title) {
// if there is no content use the title as content
element.html(content || title);
}
// setting the title
if (title && instance.get(SHOW_TITLE)) {
element.setAttribute(TITLE, title);
}
if (!element.attr(HREF) && (element.get(NODE_NAME).toLowerCase() == ANCHOR)) {
element.setAttribute(HREF, HREF_JAVASCRIPT);
}
contentBox.appendChild(element);
}
);
},
/**
* Sync the Rating elements.
*
* @method _syncElements
* @protected
*/
_syncElements: function() {
var instance = this;
var selectedIndex = instance.get(DEFAULT_SELECTED) - 1;
instance.set(SELECTED_INDEX, selectedIndex);
instance.select();
},
/**
* Sync the Rating label UI.
*
* @method _syncLabelUI
* @protected
*/
_syncLabelUI: function() {
var instance = this;
var labelText = instance.get(LABEL);
instance.get(LABEL_NODE).html(labelText);
},
/**
* Get the <code>index</code> element input data stored on <a
* href="Rating.html#property_inputElementsData">inputElementsData</a>.
*
* @method _getInputData
* @protected
*/
_getInputData: function(index) {
var instance = this;
return instance.inputElementsData[index] || {};
},
/**
* Fires the click event.
*
* @method _handleClickEvent
* @param {EventFacade} event click event facade
* @protected
*/
_handleClickEvent: function(event) {
var instance = this;
if (instance._canFireCustomEvent(event)) {
instance.fire(EV_RATING_ITEM_CLICK, {
delegateEvent: event,
domEvent: event.domEvent
});
}
},
/**
* Fires the mouseOut event.
*
* @method _handleMouseOutEvent
* @param {EventFacade} event mouseOut event facade
* @protected
*/
_handleMouseOutEvent: function(event) {
var instance = this;
if (instance._canFireCustomEvent(event)) {
instance.fire(EV_RATING_ITEM_OUT, {
delegateEvent: event,
domEvent: event.domEvent
});
}
},
/**
* Fires the mouseOver event.
*
* @method _handleMouseOverEvent
* @param {EventFacade} event mouseOver event facade
* @protected
*/
_handleMouseOverEvent: function(event) {
var instance = this;
if (instance._canFireCustomEvent(event)) {
instance.fire(EV_RATING_ITEM_OVER, {
delegateEvent: event,
domEvent: event.domEvent
});
}
},
/**
* Fires after the value of the
* <a href="Rating.html#config_label">label</a> attribute change.
*
* @method _afterSetLabel
* @param {EventFacade} event
* @protected
*/
_afterSetLabel: function(event) {
this._syncLabelUI();
}
}
}
);
/*
* ThumbRating
*/
var DOWN = 'down',
THUMB = 'thumb',
THUMB_RATING = 'ThumbRating',
UP = 'up',
CSS_RATING_THUMB_DOWN = getCN(RATING, THUMB, DOWN),
CSS_RATING_THUMB_UP = getCN(RATING, THUMB, UP);
/**
* <p><img src="assets/images/aui-rating/thumb-rating.png"/></p>
*
* A base class for ThumbRating, providing:
* <ul>
* <li>A non-obstrusive star rating control using Thumb up and Thumb down icons</li>
* <li>Could be based on a set of radio input boxes</li>
* </ul>
*
* Quick Example:<br/>
*
* <pre><code>var instance = new A.ThumbRating({
* boundingBox: '#rating',
* defaultSelected: 3,
* disabled: false,
* label: 'Label'
* }).render();
* </code></pre>
*
* Check the list of <a href="ThumbRating.html#configattributes">Configuration Attributes</a> available for
* ThumbRating.
*
* @param config {Object} Object literal specifying widget configuration properties.
*
* @class ThumbRating
* @constructor
* @extends Rating
*/
var ThumbRating = A.Component.create(
{
/**
* Static property provides a string to identify the class.
*
* @property ThumbRating.NAME
* @type String
* @static
*/
NAME: THUMB_RATING,
/**
* Static property used to define the default attribute
* configuration for the ThumbRating.
*
* @property ThumbRating.ATTRS
* @type Object
* @static
*/
ATTRS: {
/**
* The size on ThumbRating is always 2 (i.e., thumb up and thumb down).
*
* @attribute size
* @default 2
* @readOnly
* @type Number
*/
size: {
value: 2,
readOnly: true
}
},
EXTENDS: Rating,
prototype: {
/**
* Create the DOM structure for the ThumbRating. Lifecycle.
*
* @method renderUI
* @protected
*/
renderUI: function() {
var instance = this;
ThumbRating.superclass.renderUI.apply(this, arguments);
var elements = instance.get(ELEMENTS);
elements.addClass(CSS_RATING_EL_OFF);
elements.item(0).addClass(CSS_RATING_THUMB_UP);
elements.item(1).addClass(CSS_RATING_THUMB_DOWN);
},
/**
* Add the <code>className</code> on the the <code>index</code> element
* and all the previous Rating elements.
*
* @method fillTo
* @param {Number} index Index to be selected
* @param {String} className Class name to be applied when fill the Rating elements
*/
fillTo: function(index, className) {
this.clearSelection();
if (index >= 0) {
this.get(ELEMENTS).item(index).addClass(className || CSS_RATING_EL_ON);
}
},
/**
* Empty method, no logic needed on this method on ThumbRating.
*
* @method _syncElements
* @protected
*/
_syncElements: function() {}
}
}
);
A.Rating = Rating;
A.StarRating = Rating;
A.ThumbRating = ThumbRating;
}, '@VERSION@' ,{skinnable:true, requires:['aui-base']});