diff --git a/.gitignore b/.gitignore
index 61b81d1..620c5d0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,4 +2,7 @@
node_modules
bower_components
test.htm
-.vs
\ No newline at end of file
+
+# Editors
+.vs
+.vscode
\ No newline at end of file
diff --git a/gulpfile.js b/gulpfile.js
index 9aa57d5..6d5a8ea 100644
--- a/gulpfile.js
+++ b/gulpfile.js
@@ -8,11 +8,15 @@ var uglify = require('gulp-uglify');
var plumber = require('gulp-plumber');
var rename = require('gulp-rename');
var sourcemaps = require('gulp-sourcemaps');
+var typescript = require('gulp-tsc');
+gulp.task('compile', function(){
+ gulp.src(['src/*.ts'])
+ .pipe(typescript({ target: 'es5', module: 'umd' }))
+ .pipe(gulp.dest('scripts/'))
+});
-
-
-gulp.task('default', function () {
- return gulp.src('scripts/jquery.rateit.js')
+gulp.task('minify', function () {
+ return gulp.src(['scripts/rateit.js','scripts/jquery.rateit.js'])
.pipe(sourcemaps.init())
.pipe(plumber())
.pipe(uglify({ preserveComments: 'license' }))
@@ -20,3 +24,5 @@ gulp.task('default', function () {
.pipe(sourcemaps.write(''))
.pipe(gulp.dest('scripts')) // save .min.js
});
+
+gulp.task('default', ['compile', 'minify']);
\ No newline at end of file
diff --git a/package.json b/package.json
index c9ba78a..5890b5e 100644
--- a/package.json
+++ b/package.json
@@ -24,10 +24,12 @@
"jquery": ">=1.6"
},
"devDependencies": {
- "gulp": "~3.9.1",
+ "gulp": "^3.9.1",
"gulp-plumber": "~1.1.0",
- "gulp-uglify": "~2.0.0",
"gulp-rename": "~1.2.2",
- "gulp-sourcemaps": "~2.1.1"
+ "gulp-sourcemaps": "~2.1.1",
+ "gulp-tsc": "~1.3.2",
+ "gulp-uglify": "~2.0.0",
+ "typescript": "~2.2.2"
}
}
diff --git a/scripts/rateit.js b/scripts/rateit.js
new file mode 100644
index 0000000..79adb4a
--- /dev/null
+++ b/scripts/rateit.js
@@ -0,0 +1,438 @@
+(function (factory) {
+ if (typeof module === "object" && typeof module.exports === "object") {
+ var v = factory(require, exports);
+ if (v !== undefined) module.exports = v;
+ }
+ else if (typeof define === "function" && define.amd) {
+ define(["require", "exports"], factory);
+ }
+})(function (require, exports) {
+ "use strict";
+ Object.defineProperty(exports, "__esModule", { value: true });
+ var rateit = /** @class */ (function () {
+ function rateit() {
+ this.index = 0;
+ this.defaultOptions = {
+ min: 0,
+ max: 5,
+ step: 0.5,
+ mode: 'bg',
+ icon: '★',
+ starwidth: 16,
+ starheight: 16,
+ readonly: false,
+ resetable: true,
+ ispreset: false
+ };
+ }
+ Object.defineProperty(rateit.prototype, "resetLabel", {
+ get: function () { return 'reset rating'; },
+ enumerable: false,
+ configurable: true
+ });
+ ;
+ Object.defineProperty(rateit.prototype, "ratingLabel", {
+ get: function () { return 'rating'; },
+ enumerable: false,
+ configurable: true
+ });
+ ;
+ rateit.prototype.enable = function (selector, options) {
+ var _this = this;
+ options = options || this.defaultOptions;
+ selector = selector || '.rateit';
+ var items = document.querySelectorAll(selector);
+ var _loop_1 = function () {
+ var item = items[i];
+ //let dataSet = new DataSetWrapper(item);
+ var dataset = rateit.getDataSetWrapper(item);
+ if (dataset.getString('init')) {
+ this_1.redraw(item);
+ return "continue";
+ }
+ item.classList.add('rateit');
+ var ltr = window.getComputedStyle(item).direction === 'ltr';
+ dataset.set('mode', dataset.getString('mode') || options.mode);
+ dataset.set('icon', dataset.getString('icon') || options.icon);
+ dataset.set('min', isNaN(dataset.getNumber('min')) ? options.min : dataset.getNumber('min')); //since 0 is a valid entry, we check for isNaN
+ dataset.set('max', isNaN(dataset.getNumber('max')) ? options.max : dataset.getNumber('max')); //since 0 is a valid entry, we check for isNaN
+ dataset.set('step', dataset.getNumber('step') || options.step);
+ dataset.set('resetable', dataset.getString('resetable') !== undefined ? dataset.getBoolean('resetable') : options.resetable);
+ dataset.set('readonly', dataset.getString('readonly') !== undefined ? dataset.getBoolean('readonly') : options.readonly);
+ dataset.set('ispreset', dataset.getString('ispreset') !== undefined ? dataset.getBoolean('ispreset') : options.ispreset);
+ dataset.set('backingfld', dataset.getString('backingfld') || options.backingfld);
+ dataset.set('starwidth', dataset.getNumber('starwidth') || options.starwidth);
+ dataset.set('starheight', dataset.getNumber('starheight') || options.starheight);
+ dataset.set('value', Math.max(dataset.getNumber('min'), Math.min(dataset.getNumber('max'), (!isNaN(dataset.getNumber('value')) ? dataset.getNumber('value') : options.value == null ? options.min : options.value))));
+ if (dataset.getString('backingfld')) {
+ //if we have a backing field, hide it, override defaults if range or select.
+ var el = document.querySelector(dataset.getString('backingfld'));
+ var fld = el;
+ fld.style.display = 'none';
+ if (fld.disabled || fld.readOnly) {
+ dataset.set('readonly', true);
+ }
+ if (el.nodeName === 'INPUT') {
+ if (fld.type === 'range' || fld.type === 'text') { //in browsers not support the range type, it defaults to text
+ dataset.set('min', parseInt(fld.getAttribute('min')) || dataset.getNumber('min')); //if we would have done fld.min it wouldn't have worked in browsers not supporting the range type.
+ dataset.set('max', parseInt(fld.getAttribute('max')) || dataset.getNumber('max'));
+ dataset.set('step', parseInt(fld.getAttribute('step')) || dataset.getNumber('step'));
+ }
+ }
+ if (el.nodeName == 'SELECT' && el.options.length > 1) {
+ var select = el;
+ // If backing field is a select box with valuesrc option set to "index", use the indexes of its options; otherwise, use the values.
+ if (select.getAttribute('data-rateit-valuesrc') === 'index') {
+ dataset.set('min', (!isNaN(dataset.getNumber('min')) ? dataset.getNumber('min') : 0));
+ dataset.set('max', select.length - 1);
+ dataset.set('step', 1);
+ }
+ else {
+ dataset.set('min', (!isNaN(dataset.getNumber('min')) ? dataset.getNumber('min') : 0));
+ dataset.set('max', Number(select.options.item(select.length - 1).value));
+ dataset.set('step', Number(select.options.item(1).value) - Number(select.options.item(0).value));
+ }
+ //see if we have a option that as explicity been selected
+ selectedOption = select.querySelector('option[selected]');
+ if (selectedOption != null) {
+ // If backing field is a select box with valuesrc option set to "index", use the index of selected option; otherwise, use the value.
+ if (select.getAttribute('data-rateit-valuesrc') === 'index') {
+ dataset.set('value', selectedOption.index);
+ }
+ else {
+ dataset.set('value', selectedOption.value);
+ }
+ }
+ }
+ else {
+ //if it is not a select box, we can get's it's value using the val function.
+ //If it is a selectbox, we always get a value (the first one of the list), even if it was not explicity set.
+ dataset.set('value', fld.value);
+ }
+ }
+ //Create the necessary tags. For ARIA purposes we need to give the items an ID. So we use an internal index to create unique ids
+ var element = item.nodeName === 'DIV' ? 'div' : 'span';
+ this_1.index++;
+ html = "<").concat(element, " id=\"rateit-range-").concat(this_1.index, "\" class=\"rateit-range\" tabindex=\"0\" role=\"slider\" aria-label=\"").concat(this_1.ratingLabel, "\" aria-owns=\"rateit-reset-").concat(this_1.index, "\" aria-valuemin=\"").concat(dataset.getNumber('min'), "\" aria-valuemax=\"").concat(dataset.getNumber('max'), "\" aria-valuenow=\"").concat(dataset.getNumber('value'), "\"><").concat(element, " class=\"rateit-empty\">").concat(element, "><").concat(element, " class=\"rateit-selected\">").concat(element, "><").concat(element, " class=\"rateit-hover\">").concat(element, ">").concat(element, ">");
+ item.insertAdjacentHTML('beforeend', html);
+ if (!ltr) {
+ item.querySelector('.rateit-reset').style.cssFloat = 'right';
+ item.querySelector('.rateit-selected').classList.add('rateit-selected-rtl');
+ item.querySelector('.rateit-hover').classList.add('rateit-hover-rtl');
+ }
+ //setup the reset button
+ var resetbtn = item.querySelector('.rateit-reset');
+ resetbtn.addEventListener('click', function (ev) {
+ resetbtn.blur();
+ var beforeResetEvent = new Event('beforereset');
+ //if the beforereset event was cancelled then do not continue resetting
+ if (!item.dispatchEvent(beforeResetEvent)) {
+ return false;
+ }
+ var initialOptions = JSON.parse(dataset.getString('init'));
+ if (initialOptions.ispreset) {
+ _this.options(item, { ispreset: true, value: initialOptions.value });
+ }
+ else {
+ _this.value(item, null);
+ }
+ item.dispatchEvent(new Event('reset'));
+ return false;
+ }, false);
+ //setup other events
+ var range = item.querySelector('.rateit-range');
+ if (!dataset.getBoolean('readonly')) {
+ range.addEventListener('touchmove', rateit.touchHandler, false);
+ range.addEventListener('touchend', rateit.touchHandler, false);
+ range.addEventListener('mousemove', function (e) {
+ var score = _this.calcRawScore(item, e);
+ _this.setHover(item, score);
+ });
+ //when the mouse leaves the range, we have to hide the hover stars, and show the current value.
+ range.addEventListener('mouseleave', function (e) {
+ var rateitHover = (range.querySelector('.rateit-hover'));
+ rateitHover.style.display = 'none';
+ rateitHover.style.width = '0';
+ rateitHover.dataset['width'] = '';
+ item.dispatchEvent(new CustomEvent('hover'));
+ item.dispatchEvent(new CustomEvent('over'));
+ (range.querySelector('.rateit-selected')).style.display = 'block';
+ });
+ //when we click on the range, we have to set the value, hide the hover.
+ range.addEventListener('mouseup', function (e) {
+ var score = _this.calcRawScore(item, e);
+ var value = (score * dataset.getNumber('step')) + dataset.getNumber('min');
+ _this.value(item, value, true);
+ range.blur();
+ });
+ //support key nav
+ range.addEventListener('keyup', function (e) {
+ if (e.which == 38 || e.which == (ltr ? 39 : 37)) {
+ var value = Math.min(dataset.getNumber('value') + dataset.getNumber('step'), dataset.getNumber('max'));
+ _this.value(item, value, true);
+ }
+ if (e.which == 40 || e.which == (ltr ? 37 : 39)) {
+ var value = Math.max(dataset.getNumber('value') - dataset.getNumber('step'), dataset.getNumber('min'));
+ _this.value(item, value, true);
+ }
+ });
+ }
+ dataset.set('init', JSON.stringify(this_1.options(item))); //cheap way to create a clone
+ this_1.redraw(item);
+ };
+ var this_1 = this, selectedOption, html;
+ for (var i = 0; i < items.length; i++) {
+ _loop_1();
+ }
+ };
+ rateit.prototype.options = function (selectorOrElement, options) {
+ var element = rateit.getElementFromSelectorOrElement(selectorOrElement);
+ var dataset = rateit.getDataSetWrapper(element);
+ if (typeof (options) === 'undefined') {
+ var numberProps = ['value', 'min', 'max', 'step', 'starwidth', 'starheight'];
+ var booleanProps = ['readonly', 'ispreset', 'resetable'];
+ var opts = {};
+ for (var property in element.dataset) {
+ if (element.dataset.hasOwnProperty(property) && property !== 'rateitInit') {
+ var key = property.substring('rateit'.length).toLowerCase();
+ var raw = element.dataset[property];
+ if (numberProps.indexOf(key) !== -1) {
+ opts[key] = Number(raw);
+ }
+ else if (booleanProps.indexOf(key) !== -1) {
+ opts[key] = raw === 'true';
+ }
+ else {
+ opts[key] = raw;
+ }
+ }
+ }
+ return opts;
+ }
+ if (options.hasOwnProperty('value')) {
+ //if we also got the value passed in, call the value method, since that takes also care of setting the correct value in the backingfield.
+ this.value(element, options.value, false);
+ }
+ for (var property in options) {
+ if (options.hasOwnProperty(property) && property !== 'init') {
+ dataset.set(property, options[property]);
+ }
+ }
+ this.redraw(element);
+ };
+ rateit.prototype.value = function (selectorOrElement, value, raiseEvents) {
+ var element = rateit.getElementFromSelectorOrElement(selectorOrElement);
+ var dataset = rateit.getDataSetWrapper(element);
+ if (typeof (value) === 'undefined')
+ return dataset.getNumber('value');
+ if (value === null) {
+ value = dataset.getNumber('min');
+ }
+ var range = element.querySelector('.rateit-range');
+ var rangeComputedStyle = window.getComputedStyle(range);
+ var presetclass = "rateit-preset".concat((rangeComputedStyle.direction === 'ltr') ? '' : '-rtl');
+ if (raiseEvents === true) {
+ var event_1 = new CustomEvent('beforerated', { detail: { value: value } }); // has to be CustomEvent
+ if (!element.dispatchEvent(event_1)) {
+ return false;
+ }
+ }
+ dataset.set('value', value);
+ if (dataset.getString('backingfld') != null) {
+ // If backing field is a select box with valuesrc option set to "index", update its selectedIndex property; otherwise, update its value.
+ var backingFld = document.querySelector(dataset.getString('backingfld'));
+ if (backingFld.nodeName == 'SELECT' && backingFld.getAttribute('data-rateit-valuesrc') === 'index') {
+ backingFld.selectedIndex = value;
+ }
+ else {
+ backingFld.value = value.toString();
+ }
+ if (raiseEvents === true) {
+ var evt = new Event('change', { bubbles: true, cancelable: false });
+ backingFld.dispatchEvent(evt);
+ }
+ }
+ if (dataset.getBoolean('ispreset')) { //if it was a preset value, unset that.
+ (range.querySelector('.rateit-selected')).classList.remove(presetclass);
+ dataset.set('ispreset', false);
+ }
+ (range.querySelector('.rateit-hover')).style.display = 'none';
+ var rateItSelected = (range.querySelector('.rateit-selected'));
+ rateItSelected.style.width = "".concat(value * dataset.getNumber('starwidth') - (dataset.getNumber('min') * dataset.getNumber('starwidth')), "px");
+ rateItSelected.style.display = 'block';
+ if (raiseEvents === true) {
+ element.dispatchEvent(new CustomEvent('hover'));
+ element.dispatchEvent(new CustomEvent('over'));
+ element.dispatchEvent(new CustomEvent('rated', { detail: { value: value } }));
+ }
+ return true;
+ };
+ rateit.prototype.revert = function (selectorOrElement) {
+ var element = rateit.getElementFromSelectorOrElement(selectorOrElement);
+ var dataset = rateit.getDataSetWrapper(element);
+ this.options(selectorOrElement, JSON.parse(dataset.getString('init')));
+ };
+ rateit.prototype.setHover = function (element, score) {
+ var dataset = rateit.getDataSetWrapper(element);
+ var range = element.querySelector('.rateit-range');
+ var w = score * dataset.getNumber('starwidth') * dataset.getNumber('step');
+ var h = range.querySelector('.rateit-hover');
+ if (h.dataset['width'] != w.toString()) {
+ (range.querySelector('.rateit-selected')).style.display = 'none';
+ h.style.width = "".concat(w, "px");
+ h.style.display = 'block';
+ h.dataset['width'] = w.toString();
+ var value = [(score * dataset.getNumber('step')) + dataset.getNumber('min')];
+ var ev = new CustomEvent('hover', {
+ detail: {
+ value: (score * dataset.getNumber('step')) + dataset.getNumber('min')
+ }
+ });
+ element.dispatchEvent(new CustomEvent('hover', { detail: { value: value } }));
+ element.dispatchEvent(new CustomEvent('over', { detail: { value: value } }));
+ }
+ };
+ rateit.prototype.calcRawScore = function (el, ev) {
+ var pageX = (ev.type.substring(0, 5) === 'touch')
+ ? ev.changedTouches[0].pageX
+ : ev.pageX;
+ var dataset = rateit.getDataSetWrapper(el);
+ var range = el.querySelector('.rateit-range');
+ var rangeComputedStyle = window.getComputedStyle(range);
+ var offsetLeft = range.getBoundingClientRect().left + document.body.scrollLeft;
+ var rangeWidth = parseInt(rangeComputedStyle.width);
+ var offsetx = pageX - offsetLeft;
+ if (rangeComputedStyle.direction === 'rtl') {
+ offsetx = parseInt(rangeComputedStyle.width) - offsetx;
+ }
+ ;
+ if (offsetx > rangeWidth) {
+ offsetx = rangeWidth;
+ }
+ if (offsetx < 0) {
+ offsetx = 0;
+ }
+ return Math.ceil(offsetx / dataset.getNumber('starwidth') * (1 / dataset.getNumber('step')));
+ };
+ rateit.prototype.redraw = function (element) {
+ var dataset = rateit.getDataSetWrapper(element);
+ var isfont = dataset.getString('mode') === 'font';
+ var range = element.querySelector('.rateit-range');
+ var rangeComputedStyle = window.getComputedStyle(range);
+ var ltr = rangeComputedStyle.direction === 'ltr';
+ if (isfont) {
+ element.classList.add('rateit-font');
+ element.classList.remove('rateit-bg');
+ //fill the ranges with the icons
+ var icon = dataset.getString('icon');
+ var stars = dataset.getNumber('max') - dataset.getNumber('min');
+ var txt_1 = '';
+ for (var i = 0; i < stars; i++) {
+ txt_1 += icon;
+ }
+ Array.prototype.forEach.call(range.children, function (item) {
+ item.innerText = txt_1;
+ });
+ dataset.set('starwidth', parseInt(rangeComputedStyle.width) / (dataset.getNumber('max') - dataset.getNumber('min')));
+ range.style.width = '';
+ range.style.height = '';
+ Array.prototype.forEach.call(element.querySelectorAll('.rateit-selected, .rateit-hover'), function (item) {
+ item.style.height = '';
+ });
+ }
+ else {
+ element.classList.add('rateit-bg');
+ element.classList.remove('rateit-font');
+ Array.prototype.forEach.call(range.children, function (item) {
+ item.innerText = '';
+ });
+ //set the range element to fit all the stars.
+ range.style.width = "".concat(dataset.getNumber('starwidth') * (dataset.getNumber('max') - dataset.getNumber('min')), "px");
+ range.style.height = "".concat(dataset.getNumber('starheight'), "px");
+ Array.prototype.forEach.call(element.querySelectorAll('.rateit-selected, .rateit-hover'), function (item) {
+ item.style.height = "".concat(dataset.getNumber('starheight'), "px");
+ });
+ }
+ //add/remove the preset class
+ var presetclass = "rateit-preset".concat((ltr) ? '' : '-rtl');
+ if (dataset.getBoolean('ispreset')) {
+ element.querySelector('.rateit-selected').classList.add(presetclass);
+ }
+ else {
+ element.querySelector('.rateit-selected').classList.remove(presetclass);
+ }
+ //set the value if we have it.
+ if (dataset.getNumber('value') != null) {
+ var score = (dataset.getNumber('value') - dataset.getNumber('min')) * dataset.getNumber('starwidth');
+ (element.querySelector('.rateit-selected')).style.width = "".concat(score, "px");
+ }
+ //sets the hover element based on the score.
+ var resetbtn = element.querySelector('.rateit-reset');
+ if (!dataset.getBoolean('readonly')) {
+ //if we are not read only, add all the events
+ //if we have a reset button, set the event handler.
+ if (!dataset.getBoolean('resetable')) {
+ resetbtn.style.display = 'none';
+ }
+ if (dataset.getBoolean('resetable')) {
+ resetbtn.style.display = 'block';
+ }
+ }
+ else {
+ resetbtn.style.display = 'none';
+ }
+ range.setAttribute('aria-readonly', dataset.getString('readonly'));
+ };
+ rateit.getElementFromSelectorOrElement = function (selectorOrElement) {
+ if (typeof (selectorOrElement) === 'string') {
+ return document.querySelector(selectorOrElement);
+ }
+ else {
+ return selectorOrElement;
+ }
+ };
+ //touch converter http://ross.posterous.com/2008/08/19/iphone-touch-events-in-javascript/
+ rateit.touchHandler = function (event) {
+ var touches = event.changedTouches, first = touches[0], type = "";
+ switch (event.type) {
+ case "touchmove":
+ type = "mousemove";
+ break;
+ case "touchend":
+ type = "mouseup";
+ break;
+ default: return;
+ }
+ var simulatedEvent = document.createEvent("MouseEvent");
+ simulatedEvent.initMouseEvent(type, true, true, window, 1, first.screenX, first.screenY, first.clientX, first.clientY, false, false, false, false, 0 /*left*/, null);
+ first.target.dispatchEvent(simulatedEvent);
+ event.preventDefault();
+ };
+ rateit.getDataSetWrapper = function (item) {
+ var createKey = function (key) {
+ return 'rateit' + key.charAt(0).toUpperCase() + key.substr(1);
+ };
+ return {
+ getNumber: function (key) { return Number(item.dataset[createKey(key)]); },
+ getBoolean: function (key) { return item.dataset[createKey(key)] === 'true'; },
+ getString: function (key) { return item.dataset[createKey(key)]; },
+ set: function (key, value) {
+ if (value == null) {
+ delete item.dataset[createKey(key)]; //might not work in safari http://stackoverflow.com/questions/9200712/how-to-remove-data-attributes-using-html5-dataset
+ }
+ else {
+ item.dataset[createKey(key)] = value.toString();
+ }
+ }
+ };
+ };
+ return rateit;
+ }());
+ exports.default = rateit;
+ if (typeof document !== 'undefined') {
+ document.addEventListener('DOMContentLoaded', function () {
+ var _rateit = new rateit();
+ _rateit.enable();
+ });
+ }
+});
diff --git a/src/rateit.ts b/src/rateit.ts
new file mode 100644
index 0000000..f7b90bd
--- /dev/null
+++ b/src/rateit.ts
@@ -0,0 +1,539 @@
+export type RateItMode = "bg" | "font";
+type RateItItemDataTypes = "init" | "value" | "min" | "max" | "step" | "backingfld" | "readonly" | "ispreset" | "resetable" | "starwidth" | "starheight" | "mode" | "icon";
+
+export interface RateItOptions {
+ value?: number;
+ min?: number;
+ max?: number;
+ step?: number;
+ backingfld?: string;
+ readonly?: boolean;
+ ispreset?: boolean;
+ resetable?: boolean;
+ starwidth?: number;
+ starheight?: number;
+ mode?: RateItMode;
+ icon?: string;
+ [key: string]: any;
+}
+
+export default class rateit {
+
+ private index: number = 0;
+
+ private defaultOptions: RateItOptions = {
+ min: 0,
+ max: 5,
+ step: 0.5,
+ mode: 'bg',
+ icon: '★',
+ starwidth: 16,
+ starheight: 16,
+ readonly: false,
+ resetable: true,
+ ispreset: false
+
+ };
+
+ public get resetLabel(): string { return 'reset rating' };
+
+ public get ratingLabel(): string { return 'rating' };
+
+ public enable(): void
+ public enable(selector: string): void
+ public enable(selector: string, options: RateItOptions): void
+ public enable(selector?: string, options?: RateItOptions): void {
+ options = options || this.defaultOptions;
+ selector = selector || '.rateit';
+
+ let items = document.querySelectorAll(selector);
+
+ for (var i = 0; i < items.length; i++) {
+ let item = items[i];
+ //let dataSet = new DataSetWrapper(item);
+ let dataset: DataSetWrapper = rateit.getDataSetWrapper(item);
+
+ if (dataset.getString('init')) {
+ this.redraw(item);
+ continue;
+ }
+
+ item.classList.add('rateit');
+
+ let ltr = window.getComputedStyle(item).direction === 'ltr';
+
+ dataset.set('mode', dataset.getString('mode') || options.mode);
+ dataset.set('icon', dataset.getString('icon') || options.icon);
+ dataset.set('min', isNaN(dataset.getNumber('min')) ? options.min : dataset.getNumber('min')); //since 0 is a valid entry, we check for isNaN
+ dataset.set('max', isNaN(dataset.getNumber('max')) ? options.max : dataset.getNumber('max'));//since 0 is a valid entry, we check for isNaN
+ dataset.set('step', dataset.getNumber('step') || options.step);
+ dataset.set('resetable', dataset.getString('resetable') !== undefined ? dataset.getBoolean('resetable') : options.resetable);
+ dataset.set('readonly', dataset.getString('readonly') !== undefined ? dataset.getBoolean('readonly') : options.readonly);
+ dataset.set('ispreset', dataset.getString('ispreset') !== undefined ? dataset.getBoolean('ispreset') : options.ispreset);
+ dataset.set('backingfld', dataset.getString('backingfld') || options.backingfld);
+ dataset.set('starwidth', dataset.getNumber('starwidth') || options.starwidth);
+ dataset.set('starheight', dataset.getNumber('starheight') || options.starheight);
+ dataset.set('value', Math.max(dataset.getNumber('min'), Math.min(dataset.getNumber('max'), (!isNaN(dataset.getNumber('value')) ? dataset.getNumber('value') : options.value == null ? options.min : options.value))));
+
+ if (dataset.getString('backingfld')) {
+ //if we have a backing field, hide it, override defaults if range or select.
+ const el = document.querySelector(dataset.getString('backingfld'));
+ const fld = el;
+ fld.style.display = 'none';
+ if (fld.disabled || fld.readOnly) {
+ dataset.set('readonly', true);
+ }
+
+ if (el.nodeName === 'INPUT') {
+ if (fld.type === 'range' || fld.type === 'text') { //in browsers not support the range type, it defaults to text
+
+ dataset.set('min', parseInt(fld.getAttribute('min')) || dataset.getNumber('min')); //if we would have done fld.min it wouldn't have worked in browsers not supporting the range type.
+ dataset.set('max', parseInt(fld.getAttribute('max')) || dataset.getNumber('max'));
+ dataset.set('step', parseInt(fld.getAttribute('step')) || dataset.getNumber('step'));
+ }
+ }
+
+ if (el.nodeName == 'SELECT' && (el).options.length > 1) {
+ let select = el;
+ // If backing field is a select box with valuesrc option set to "index", use the indexes of its options; otherwise, use the values.
+ if (select.getAttribute('data-rateit-valuesrc') === 'index') {
+ dataset.set('min', (!isNaN(dataset.getNumber('min')) ? dataset.getNumber('min') : 0));
+ dataset.set('max', select.length - 1);
+ dataset.set('step', 1);
+ }
+ else {
+ dataset.set('min', (!isNaN(dataset.getNumber('min')) ? dataset.getNumber('min') : 0));
+ dataset.set('max', Number(select.options.item(select.length - 1).value));
+ dataset.set('step', Number(select.options.item(1).value) - Number(select.options.item(0).value));
+ }
+ //see if we have a option that as explicity been selected
+ var selectedOption = select.querySelector('option[selected]');
+ if (selectedOption != null) {
+ // If backing field is a select box with valuesrc option set to "index", use the index of selected option; otherwise, use the value.
+ if (select.getAttribute('data-rateit-valuesrc') === 'index') {
+ dataset.set('value', selectedOption.index);
+ }
+ else {
+ dataset.set('value', selectedOption.value);
+ }
+ }
+ }
+ else {
+ //if it is not a select box, we can get's it's value using the val function.
+ //If it is a selectbox, we always get a value (the first one of the list), even if it was not explicity set.
+ dataset.set('value', fld.value);
+ }
+ }
+
+ //Create the necessary tags. For ARIA purposes we need to give the items an ID. So we use an internal index to create unique ids
+ let element = item.nodeName === 'DIV' ? 'div' : 'span';
+ this.index++;
+
+ var html = `<${element} id="rateit-range-${this.index}" class="rateit-range" tabindex="0" role="slider" aria-label="${this.ratingLabel}" aria-owns="rateit-reset-${this.index}" aria-valuemin="${dataset.getNumber('min')}" aria-valuemax="${dataset.getNumber('max')}" aria-valuenow="${dataset.getNumber('value')}"><${element} class="rateit-empty">${element}><${element} class="rateit-selected">${element}><${element} class="rateit-hover">${element}>${element}>`;
+ item.insertAdjacentHTML('beforeend', html);
+
+ if (!ltr) {
+ (item.querySelector('.rateit-reset')).style.cssFloat = 'right';
+ (item.querySelector('.rateit-selected')).classList.add('rateit-selected-rtl');
+ (item.querySelector('.rateit-hover')).classList.add('rateit-hover-rtl');
+ }
+
+ //setup the reset button
+ const resetbtn = item.querySelector('.rateit-reset');
+ resetbtn.addEventListener('click', (ev) => {
+
+ resetbtn.blur();
+ const beforeResetEvent = new Event('beforereset');
+
+ //if the beforereset event was cancelled then do not continue resetting
+ if (!item.dispatchEvent(beforeResetEvent)) {
+ return false;
+ }
+ const initialOptions = JSON.parse(dataset.getString('init'));
+ if (initialOptions.ispreset) {
+ this.options(item, { ispreset: true, value: initialOptions.value });
+ }
+ else {
+ this.value(item, null);
+ }
+
+ item.dispatchEvent(new Event('reset'));
+
+
+
+ return false;
+ }, false);
+
+ //setup other events
+ const range = item.querySelector('.rateit-range');
+
+ if (!dataset.getBoolean('readonly')) {
+ range.addEventListener('touchmove', rateit.touchHandler, false);
+ range.addEventListener('touchend', rateit.touchHandler, false);
+ range.addEventListener('mousemove', (e: MouseEvent) => {
+ var score = this.calcRawScore(item, e);
+ this.setHover(item, score);
+ });
+
+ //when the mouse leaves the range, we have to hide the hover stars, and show the current value.
+ range.addEventListener('mouseleave', (e: MouseEvent) => {
+ const rateitHover = ((range.querySelector('.rateit-hover')));
+ rateitHover.style.display = 'none';
+ rateitHover.style.width = '0';
+ rateitHover.dataset['width'] = '';
+ item.dispatchEvent(new CustomEvent('hover'));
+ item.dispatchEvent(new CustomEvent('over'));
+
+ ((range.querySelector('.rateit-selected'))).style.display = 'block';
+ });
+ //when we click on the range, we have to set the value, hide the hover.
+ range.addEventListener('mouseup', (e: MouseEvent) => {
+ var score = this.calcRawScore(item, e);
+ var value = (score * dataset.getNumber('step')) + dataset.getNumber('min');
+ this.value(item, value, true);
+ range.blur();
+ });
+
+ //support key nav
+ range.addEventListener('keyup', (e: KeyboardEvent) => {
+ if (e.which == 38 || e.which == (ltr ? 39 : 37)) {
+ const value = Math.min(dataset.getNumber('value') + dataset.getNumber('step'), dataset.getNumber('max'));
+ this.value(item, value, true);
+ }
+ if (e.which == 40 || e.which == (ltr ? 37 : 39)) {
+ const value = Math.max(dataset.getNumber('value') - dataset.getNumber('step'), dataset.getNumber('min'));
+ this.value(item, value, true);
+ }
+ });
+ }
+
+
+ dataset.set('init', JSON.stringify(this.options(item)));//cheap way to create a clone
+
+ this.redraw(item);
+ }
+ }
+
+ public options(selector: string, options: RateItOptions): void
+ public options(element: HTMLElement, options: RateItOptions): void
+ public options(selector: string): RateItOptions
+ public options(element: HTMLElement): RateItOptions
+ public options(selectorOrElement: any, options?: RateItOptions): any {
+ const element: HTMLElement = rateit.getElementFromSelectorOrElement(selectorOrElement);
+ const dataset = rateit.getDataSetWrapper(element);
+ if (typeof (options) === 'undefined') {
+ const numberProps = ['value', 'min', 'max', 'step', 'starwidth', 'starheight'];
+ const booleanProps = ['readonly', 'ispreset', 'resetable'];
+ let opts: any = {};
+ for (var property in element.dataset) {
+ if (element.dataset.hasOwnProperty(property) && property !== 'rateitInit') {
+ const key = property.substring('rateit'.length).toLowerCase();
+ const raw = element.dataset[property];
+ if (numberProps.indexOf(key) !== -1) {
+ opts[key] = Number(raw);
+ } else if (booleanProps.indexOf(key) !== -1) {
+ opts[key] = raw === 'true';
+ } else {
+ opts[key] = raw;
+ }
+ }
+ }
+ return opts;
+ }
+
+ if (options.hasOwnProperty('value'))
+ {
+ //if we also got the value passed in, call the value method, since that takes also care of setting the correct value in the backingfield.
+ this.value(element, options.value, false);
+ }
+
+ for (var property in options) {
+ if (options.hasOwnProperty(property) && property !== 'init') {
+ dataset.set(property, options[property]);
+ }
+ }
+
+ this.redraw(element);
+
+ }
+
+
+ public value(element: HTMLElement): Number
+ public value(selector: HTMLElement): Number
+ public value(element: HTMLElement, value: number | null): boolean
+ public value(selector: string, value: number | null): boolean
+ public value(element: HTMLElement, value: number | null, raiseEvents: boolean): boolean
+ public value(selector: string, value: number | null, raiseEvents: boolean): boolean
+ public value(selectorOrElement: any, value?: number | null, raiseEvents?: boolean): any {
+ const element: HTMLElement = rateit.getElementFromSelectorOrElement(selectorOrElement);
+ const dataset: DataSetWrapper = rateit.getDataSetWrapper(element);
+
+ if (typeof (value) === 'undefined')
+ return dataset.getNumber('value');
+
+ if (value === null) {
+ value = dataset.getNumber('min');
+ }
+
+ const range = element.querySelector('.rateit-range');
+ const rangeComputedStyle = window.getComputedStyle(range);
+ const presetclass = `rateit-preset${(rangeComputedStyle.direction === 'ltr') ? '' : '-rtl'}`;
+
+ if (raiseEvents === true) {
+ const event = new CustomEvent('beforerated', { detail: { value: value } }); // has to be CustomEvent
+
+ if (!element.dispatchEvent(event)) {
+ return false;
+ }
+ }
+
+ dataset.set('value', value);
+
+ if (dataset.getString('backingfld') != null) {
+ // If backing field is a select box with valuesrc option set to "index", update its selectedIndex property; otherwise, update its value.
+ const backingFld = document.querySelector(dataset.getString('backingfld'));
+
+ if (backingFld.nodeName == 'SELECT' && backingFld.getAttribute('data-rateit-valuesrc') === 'index') {
+ (backingFld).selectedIndex = value;
+ }
+ else {
+ (backingFld).value = value.toString();
+
+ }
+
+ if (raiseEvents === true) {
+ const evt = new Event('change', { bubbles: true, cancelable: false });
+ backingFld.dispatchEvent(evt);
+ }
+
+ }
+ if (dataset.getBoolean('ispreset')) { //if it was a preset value, unset that.
+ ((range.querySelector('.rateit-selected'))).classList.remove(presetclass);
+ dataset.set('ispreset', false);
+ }
+
+
+ ((range.querySelector('.rateit-hover'))).style.display = 'none';
+ const rateItSelected = ((range.querySelector('.rateit-selected')));
+ rateItSelected.style.width = `${value * dataset.getNumber('starwidth') - (dataset.getNumber('min') * dataset.getNumber('starwidth'))}px`;
+ rateItSelected.style.display = 'block';
+
+ if (raiseEvents === true) {
+ element.dispatchEvent(new CustomEvent('hover'));
+ element.dispatchEvent(new CustomEvent('over'));
+ element.dispatchEvent(new CustomEvent('rated', { detail: { value: value } }));
+ }
+
+ return true;
+ }
+
+
+ public revert(element: HTMLElement): void
+ public revert(selector: string): void
+ public revert(selectorOrElement: any): void {
+ const element: HTMLElement = rateit.getElementFromSelectorOrElement(selectorOrElement);
+ const dataset: DataSetWrapper = rateit.getDataSetWrapper(element);
+
+ this.options(selectorOrElement, JSON.parse(dataset.getString('init')));
+ }
+
+
+ private setHover(element: HTMLElement, score: number): void {
+ const dataset = rateit.getDataSetWrapper(element);
+ const range = element.querySelector('.rateit-range');
+
+ var w = score * dataset.getNumber('starwidth') * dataset.getNumber('step');
+ var h = range.querySelector('.rateit-hover');
+ if (h.dataset['width'] != w.toString()) {
+ ((range.querySelector('.rateit-selected'))).style.display = 'none';
+ h.style.width = `${w}px`;
+ h.style.display = 'block';
+ h.dataset['width'] = w.toString();
+ const value = [(score * dataset.getNumber('step')) + dataset.getNumber('min')];
+ var ev = new CustomEvent('hover', {
+ detail: {
+ value: (score * dataset.getNumber('step')) + dataset.getNumber('min')
+ }
+ })
+ element.dispatchEvent(new CustomEvent('hover', { detail: { value: value } }));
+ element.dispatchEvent(new CustomEvent('over', { detail: { value: value } }));
+ }
+ }
+
+ private calcRawScore(el: HTMLElement, ev: UIEvent): number {
+ const pageX: number = (ev.type.substring(0, 5) === 'touch')
+ ? (ev).changedTouches[0].pageX
+ : (ev).pageX;
+ const dataset = rateit.getDataSetWrapper(el);
+ const range = el.querySelector('.rateit-range');
+ const rangeComputedStyle = window.getComputedStyle(range);
+
+ const offsetLeft = range.getBoundingClientRect().left + document.body.scrollLeft;
+ const rangeWidth = parseInt(rangeComputedStyle.width);
+ let offsetx = pageX - offsetLeft;
+ if (rangeComputedStyle.direction === 'rtl') { offsetx = parseInt(rangeComputedStyle.width) - offsetx };
+ if (offsetx > rangeWidth) { offsetx = rangeWidth; }
+ if (offsetx < 0) { offsetx = 0; }
+
+ return Math.ceil(offsetx / dataset.getNumber('starwidth') * (1 / dataset.getNumber('step')));
+ }
+
+
+ private redraw(element: HTMLElement) {
+ const dataset = rateit.getDataSetWrapper(element);
+
+ const isfont = dataset.getString('mode') === 'font';
+ const range = element.querySelector('.rateit-range');
+ let rangeComputedStyle = window.getComputedStyle(range);
+ const ltr = rangeComputedStyle.direction === 'ltr';
+
+
+ if (isfont) {
+
+ element.classList.add('rateit-font');
+ element.classList.remove('rateit-bg');
+
+ //fill the ranges with the icons
+ const icon = dataset.getString('icon');
+ const stars = dataset.getNumber('max') - dataset.getNumber('min');
+
+ let txt = '';
+ for (let i = 0; i < stars; i++) {
+ txt += icon;
+ }
+
+ Array.prototype.forEach.call(range.children, function (item: HTMLElement) {
+ item.innerText = txt;
+ });
+
+ dataset.set('starwidth', parseInt(rangeComputedStyle.width) / (dataset.getNumber('max') - dataset.getNumber('min')));
+
+ range.style.width = '';
+ range.style.height = '';
+
+ Array.prototype.forEach.call(element.querySelectorAll('.rateit-selected, .rateit-hover'), function (item: HTMLElement) {
+ item.style.height = '';
+ });
+ }
+ else {
+
+
+ element.classList.add('rateit-bg');
+ element.classList.remove('rateit-font');
+
+ Array.prototype.forEach.call(range.children, function (item: HTMLElement) {
+ item.innerText = '';
+ });
+
+ //set the range element to fit all the stars.
+ range.style.width = `${dataset.getNumber('starwidth') * (dataset.getNumber('max') - dataset.getNumber('min'))}px`;
+ range.style.height = `${dataset.getNumber('starheight')}px`;
+
+ Array.prototype.forEach.call(element.querySelectorAll('.rateit-selected, .rateit-hover'), function (item: HTMLElement) {
+ item.style.height = `${dataset.getNumber('starheight')}px`;
+ });
+
+ }
+
+ //add/remove the preset class
+ const presetclass = `rateit-preset${(ltr) ? '' : '-rtl'}`;
+ if (dataset.getBoolean('ispreset')) {
+ element.querySelector('.rateit-selected').classList.add(presetclass);
+ }
+ else {
+ element.querySelector('.rateit-selected').classList.remove(presetclass);
+ }
+
+ //set the value if we have it.
+ if (dataset.getNumber('value') != null) {
+ var score = (dataset.getNumber('value') - dataset.getNumber('min')) * dataset.getNumber('starwidth');
+ ((element.querySelector('.rateit-selected'))).style.width = `${score}px`;
+ }
+
+
+ //sets the hover element based on the score.
+ const resetbtn = element.querySelector('.rateit-reset');
+ if (!dataset.getBoolean('readonly')) {
+ //if we are not read only, add all the events
+
+ //if we have a reset button, set the event handler.
+ if (!dataset.getBoolean('resetable')) {
+ resetbtn.style.display = 'none';
+ }
+
+ if (dataset.getBoolean('resetable')) {
+ resetbtn.style.display = 'block';
+ }
+ }
+ else {
+ resetbtn.style.display = 'none';
+ }
+
+ range.setAttribute('aria-readonly', dataset.getString('readonly'));
+
+ }
+
+ private static getElementFromSelectorOrElement(selectorOrElement: any): HTMLElement {
+ if (typeof (selectorOrElement) === 'string') {
+ return document.querySelector(selectorOrElement);
+ } else {
+ return selectorOrElement;
+ }
+ }
+
+ //touch converter http://ross.posterous.com/2008/08/19/iphone-touch-events-in-javascript/
+ private static touchHandler(event: TouchEvent): void {
+
+ var touches = event.changedTouches,
+ first = touches[0],
+ type = "";
+ switch (event.type) {
+ case "touchmove": type = "mousemove"; break;
+ case "touchend": type = "mouseup"; break;
+ default: return;
+ }
+
+ var simulatedEvent = document.createEvent("MouseEvent");
+ simulatedEvent.initMouseEvent(type, true, true, window, 1,
+ first.screenX, first.screenY,
+ first.clientX, first.clientY, false,
+ false, false, false, 0/*left*/, null);
+
+ first.target.dispatchEvent(simulatedEvent);
+ event.preventDefault();
+ }
+
+private static getDataSetWrapper(item: HTMLElement): DataSetWrapper {
+ const createKey = function (key: string) {
+ return 'rateit' + key.charAt(0).toUpperCase() + key.substr(1);
+ };
+ return {
+ getNumber: (key: RateItItemDataTypes): number => Number(item.dataset[createKey(key)]),
+ getBoolean: (key: RateItItemDataTypes): boolean => item.dataset[createKey(key)] === 'true',
+ getString: (key: RateItItemDataTypes): string => item.dataset[createKey(key)],
+ set: (key: RateItItemDataTypes, value: any) => {
+ if (value == null) {
+ delete item.dataset[createKey(key)]; //might not work in safari http://stackoverflow.com/questions/9200712/how-to-remove-data-attributes-using-html5-dataset
+ }
+ else {
+ item.dataset[createKey(key)] = value.toString();
+ }
+ }
+ };
+}
+}
+
+interface DataSetWrapper {
+
+ getNumber(key: RateItItemDataTypes): number;
+ getBoolean(key: RateItItemDataTypes): boolean;
+ getString(key: RateItItemDataTypes): string;
+ set(key: RateItItemDataTypes, value: any): void;
+}
+
+if (typeof document !== 'undefined') {
+ document.addEventListener('DOMContentLoaded', function () {
+ var _rateit = new rateit();
+ _rateit.enable();
+ });
+}
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..200d20d
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,16 @@
+{
+ "compilerOptions": {
+ "module": "umd",
+
+ "noImplicitAny": true,
+ "removeComments": true,
+ "preserveConstEnums": true,
+ "target": "es5",
+ "sourceMap": true
+
+ },
+ "compileOnSave": true,
+ "files": [
+ "./src/rateit.ts"
+ ]
+}
\ No newline at end of file