/** @module deliteful/StarRating */
define([
"dcl/dcl",
"dpointer/events",
"requirejs-dplugins/jquery!attributes/classes",
"delite/register",
"delite/FormValueWidget",
"requirejs-dplugins/i18n!./StarRating/nls/StarRating",
"delite/theme!./StarRating/themes/{{theme}}/StarRating.css"
], function (dcl, pointer, $,
register, FormValueWidget, messages) {
/**
* A widget that displays a rating, usually with stars, and that allows setting a different rating value
* by touching the stars.
* Its custom element tag is `d-star-rating`.
*
* @class module:deliteful/StarRating
* @augments module:delite/FormValueWidget
*/
return register("d-star-rating", [HTMLElement, FormValueWidget], /** @lends module:deliteful/StarRating# */ {
/**
* The name of the CSS class of this widget.
* @member {string}
* @default "d-star-rating"
*/
baseClass: "d-star-rating",
/**
* The maximum rating, that is also the number of stars to show.
* @member {number}
* @default 5
*/
max: 5,
/**
* The current value of the Rating.
* @member {number}
* @default 0
*/
value: 0,
/**
* Indicates whether the user is allowed to edit half values (0.5, 1.5, ...) or not.
* Ignored if readOnly is set to false.
* @member {boolean}
* @default false
*/
editHalfValues: false,
/**
* Indicates whether the user is allowed to set the value to zero or not.
* @member {boolean}
* @default true
*/
allowZero: true,
/* internal properties */
/*=====
_hoveredValue: null,
_startHandles: null,
_keyDownHandle: null,
=====*/
_hovering: false,
_otherEventsHandles: [],
render: function () {
this.focusNode = this.ownerDocument.createElement("div");
this.appendChild(this.focusNode);
pointer.setTouchAction(this, "none");
// init WAI-ARIA attributes
this.focusNode.setAttribute("role", "slider");
this.focusNode.setAttribute("aria-valuemin", 0);
this.valueNode.style.display = "none";
if (!this.valueNode.parentNode) {
this.appendChild(this.valueNode);
}
},
/* jshint maxcomplexity: 13 */
refreshRendering: function (props) {
if ("disabled" in props) {
$(this).toggleClass(this.baseClass + "-disabled", this.disabled);
}
if ("max" in props) {
this.focusNode.setAttribute("aria-valuemax", this.max);
}
if ("max" in props || "value" in props) {
this._refreshStarsRendering();
}
if ("value" in props) {
this.focusNode.setAttribute("aria-valuenow", this.value);
this.focusNode.setAttribute("aria-valuetext",
messages["aria-valuetext"].replace("${value}", this.value));
this.valueNode.value = this.value;
}
if ("readOnly" in props || "disabled" in props) {
this._refreshEditionEventHandlers();
}
if ("readOnly" in props || "disabled" in props || "allowZero" in props) {
this._updateZeroArea();
}
},
/* jshint maxcomplexity: 10 */
_refreshStarsRendering: function () {
var createChildren = this.focusNode.children.length - 1 !== 2 * this.max;
if (createChildren) {
this.focusNode.innerHTML = "";
}
this._updateStars(this.value, createChildren);
},
_refreshEditionEventHandlers: function () {
var passive = this.disabled || this.readOnly;
if (!passive && !this._keyDownHandle) {
this._keyDownHandle = this.on("keydown", this._keyDownHandler.bind(this));
} else if (passive && this._keyDownHandle) {
this._keyDownHandle.remove();
this._keyDownHandle = null;
}
if (!passive && !this._startHandles) {
this._startHandles = [this.on("pointerover", this._pointerOverHandler.bind(this)),
this.on("pointerdown", this._wireHandlers.bind(this))];
} else if (passive && this._startHandles) {
while (this._startHandles.length) {
this._startHandles.pop().remove();
}
this._startHandles = null;
}
},
_removeEventsHandlers: function () {
while (this._otherEventsHandles.length) {
this._otherEventsHandles.pop().remove();
}
},
_wireHandlers: function () {
if (!this._otherEventsHandles.length) {
this._otherEventsHandles.push(this.on("pointerup", this._pointerUpHandler.bind(this)));
this._otherEventsHandles.push(this.on("pointerleave", this._pointerLeaveHandler.bind(this)));
this._otherEventsHandles.push(this.on("pointercancel", this._pointerLeaveHandler.bind(this)));
}
},
_pointerOverHandler: function (/*Event*/ event) {
this._wireHandlers();
if (!this._hovering && event.pointerType === "mouse") {
this._hovering = true;
$(this).addClass(this.baseClass + "-hovered");
}
var newValue = event.target.value;
if (newValue !== undefined) {
if (this._hovering) {
if (newValue !== this._hoveredValue) {
$(this).addClass(this.baseClass + "-hovered");
this._updateStars(newValue, false);
this._hoveredValue = newValue;
}
} else {
// Set the previous value here, as this handler is called before _onFocus
this._previousOnChangeValue = this.value;
this.handleOnChange(newValue);
}
}
},
_pointerUpHandler: function (/*Event*/ event) {
var value = event.target.value;
if (value !== undefined) {
this.handleOnChange(value);
}
if (!this._hovering) {
this._removeEventsHandlers();
} else {
$(this).removeClass(this.baseClass + "-hovered");
}
},
_pointerLeaveHandler: function () {
if (this._hovering) {
this._hovering = false;
this._hoveredValue = null;
$(this).removeClass(this.baseClass + "-hovered");
this._updateStars(this.value, false);
}
this._removeEventsHandlers();
},
_keyDownHandler: function (/*Event*/ event) {
var incrementArrow = this.effectiveDir === "ltr" ? "ArrowRight" : "ArrowLeft",
decrementArrow = this.effectiveDir === "ltr" ? "ArrowLeft" : "ArrowRight";
switch (event.key) {
case incrementArrow:
case "ArrowUp":
case "Add":
event.preventDefault();
this._incrementValue();
break;
case decrementArrow:
case "ArrowDown":
case "Subtract":
event.preventDefault();
this._decrementValue();
break;
}
},
_incrementValue: function () {
if (this.value < this.max) {
this.value = this.value + (this.editHalfValues ? 0.5 : 1);
}
},
_decrementValue: function () {
if (this.value > (this.allowZero ? 0 : (this.editHalfValues ? 0.5 : 1))) {
this.value = this.value - (this.editHalfValues ? 0.5 : 1);
}
},
_updateStars: function (/*Number*/value, /*Boolean*/create) {
var stars = this.focusNode.querySelectorAll("div");
if (create) {
this._zeroSettingArea = this.ownerDocument.createElement("div");
this._zeroSettingArea.className = this.baseClass + "-zero";
this._zeroSettingArea.value = 0;
this.focusNode.appendChild(this._zeroSettingArea);
this._updateZeroArea();
}
for (var i = 0; i < 2 * this.max; i++) {
var starClass = this.baseClass + (i % 2 ? "-end " : "-start ");
if ((i + 1) * 0.5 <= value) {
starClass += this.baseClass + "-full";
} else {
starClass += this.baseClass + "-empty";
}
if (create) {
var parent = this.ownerDocument.createElement("div");
parent.value = this.editHalfValues ? (i + 1) / 2 : Math.ceil((i + 1) / 2);
this.focusNode.appendChild(parent);
} else {
parent = stars[i + 1];
}
parent.className = this.baseClass + "-star-icon " + starClass;
}
},
_updateZeroArea: function () {
if (this.readOnly || !this.allowZero) {
$(this._zeroSettingArea).addClass("d-hidden");
delete this.focusNode.value;
} else {
$(this._zeroSettingArea).removeClass("d-hidden");
// _zeroSettingArea might not fill the whole widget height
// so pointer events can land in the underlying focus node
this.focusNode.value = 0;
}
}
});
});