var youtubeApiReadyPromise = $.Deferred();
/**
* Callback for Youtube iframe API
* @callback
*/
function onYouTubeIframeAPIReady() {
youtubeApiReadyPromise.resolve();
}
/**
* @requires $ jQuery
* @requires $.fn.block blockUI
* @requires _ Underscore.js
* @requires YT YouTube iframe API
*/
(function (ns, youtubeApiReadyPromise) {
var debug = false;
/**
* @alias app.training.Training
* @constructor
*/
ns.Training = function (options, slideState) {
new Training(options, slideState);
};
/**
* @constructor
*/
function Training(options, slideState) {
this.construct(options, slideState);
}
var proto = Training.prototype;
proto.construct = function (options, slideState) {
this.options = $.extend(true, {
containerId: 'training-test',
slideContainerId: 'training-slide',
playerId: 'slide-player',
requestUrl: '#',
slideId: null,
slideIndex: 0,
slideCount: 0,
parentType: null,
slideLockTimeout: 2000,
blockUI: {
css: { border: 'none', backgroundColor: 'transparent' },
overlayCSS: { backgroundColor: '#ffffff' },
message: '
Loading...
',
focusInput: false
}
}, options);
this.container = $('#' + this.options.containerId);
this.slideContainer = $('#' + this.options.slideContainerId);
this.btnPrev = this.container.find('[data-control="prev"]');
this.btnNext = this.container.find('[data-control="next"]');
this.btnComplete = this.container.find('[data-control="complete"]');
this.isLoading = false;
/**
* Slide runtime data
* @type {Slide}
*/
this.slide = new Slide();
this.initTest();
this.initSlide(slideState);
youtubeApiReadyPromise.then(_.bind(this.initSlideVideo, this));
return this;
};
proto.startLoading = function () {
if (this.isLoading) {
debug && console.warn('Training.startLoading: already loading');
return false;
}
this.isLoading = true;
this.container.block(this.options.blockUI);
return true;
};
proto.stopLoading = function () {
this.isLoading = false;
this.container.unblock();
return true;
};
proto.refreshCore = function () {
this.refreshNav();
return this;
};
proto.checkLocks = function (triggerDone) {
debug && console.log('Training.checkLocks');
triggerDone = (null == triggerDone) ? true : triggerDone;
var allDone = true;
for (var id in this.slide.locks) {
if (this.slide.locks.hasOwnProperty(id) && true !== this.slide.locks[id].removed) {
allDone = false;
break;
}
}
if (allDone && triggerDone) {
this.doneSlide();
}
return allDone;
};
proto.doneSlide = function () {
debug && console.log('Training.doneSlide');
var alreadyDone = false;
if (this.slide.getFinished()) {
alreadyDone = true;
} else {
this.slide.setFinished(true);
}
this.unlockButtons(alreadyDone);
};
proto.unlockButtons = function (alreadyDone) {
if (this.options.slideIndex == this.options.slideCount - 1 && !this.options.parentType) {
return this;
}
this.refreshNav();
return this;
};
proto.showLocks = function () {
debug && console.log('Training.showLocks');
var allVisible = true, target, messages = [], blinkTargets = [];
for (var id in this.slide.locks) {
if (!this.slide.locks.hasOwnProperty(id) || true === this.slide.locks[id].removed) {
continue;
}
target = $(this.slide.locks[id].target);
if (target.length && target.is(':visible')) {
blinkTargets.push(target.get(0));
} else {
allVisible = false;
}
if (this.slide.locks[id].data) {
var node;
if (this.slide.locks[id].data.relatedNode
&& (node = $(this.slide.locks[id].data.relatedNode, this.container))
) {
blinkTargets.push(node);
}
if (this.slide.locks[id].data.message) {
messages.push(this.slide.locks[id].data.message);
}
}
}
messages.push('To move to the next slide you should look through all the page content and answer the questions, if any.');
alert(messages.join('\n'));
$(blinkTargets).blink();
return this;
};
proto.addLock = function (id, target, data) {
if (id in this.slide.locks || this.slide.getFinished()) {
return this;
}
debug && console.log('Training.addLock', id, target);
var lock = new LockItem(id, target, data);
this.slide.locks[id] = lock;
return lock;
};
/**
* @param {String} id
* @param {int} [delay]
*/
proto.removeLock = function (id, delay) {
delay = delay || 0;
if (id in this.slide.locks && true !== this.slide.locks[id].removed) {
if (0 == delay) {
debug && console.log('Training.removeLock', id, 'delay: ' + delay);
this.slide.locks[id].removed = true;
this.checkLocks();
} else {
setTimeout(_.bind(function () {
if (id in this.slide.locks) {
debug && console.log('Training.removeLock', id, 'delay: ' + delay);
this.slide.locks[id].removed = true;
this.checkLocks();
}
}, this), delay);
}
}
return this;
};
proto.initTest = function () {
this.btnPrev.on('click', _.bind(this.onSlideNavClick, this, 'prev'));
this.btnNext.on('click', _.bind(this.onSlideNavClick, this, 'next'));
this.btnComplete.on('click', _.bind(this.onSlideNavClick, this, 'next'));
};
/**
* @param {Object=} state
*/
proto.initSlide = function (state) {
window.scrollTo(0, 0);
this.slide = new Slide(state);
this.initSlideQuestions();
this.initSlideVideo();
this.checkLocks(true);
this.refreshCore();
return this;
};
proto.onSlideNavClick = function (direction, e) {
if (this.isLoading) {
return this;
}
if (direction !== 'prev' && !this.checkLocks(false)) {
this.showLocks();
return this;
}
debug && console.log('Training.onSlideNavClick', direction);
var newIdx = (direction === 'prev') ? +this.options.slideIndex - 1 : +this.options.slideIndex + 1;
if (newIdx >= 0 && newIdx < this.options.slideCount || (newIdx === this.options.slideCount && this.options.parentType)) {
this.loadSlide(newIdx);
} else {
debug && console.warn('Training.onSlideNavClick', 'slide index out of bounds', newIdx);
}
};
proto.refreshNav = function () {
if (null === this.options.parentType) {
return this;
}
this.btnPrev.toggle(this.options.slideIndex > 0).removeClass('disabled');
this.btnNext.toggleClass('disabled', !this.slide.getFinished() || this.options.slideIndex >= this.options.slideCount).show();
if (this.options.slideIndex == this.options.slideCount - 1) {
this.btnNext.hide();
this.btnComplete.toggleClass('disabled', !this.slide.getFinished()).show();
} else {
this.btnNext.show();
this.btnComplete.toggleClass('disabled', true).hide();
}
return this;
};
proto.loadSlide = function (idx) {
debug && console.log('Training.loadSlide', idx);
if (!this.startLoading() || +idx === +this.options.slideIndex) {
return this;
}
if (idx < 0 && idx >= this.options.slideCount && !(idx == this.options.slideCount && this.options.parentType)) {
debug && console.warn('Training.loadSlide', 'slide index out of bounds', idx);
return this;
}
$.ajax({
url: this.options.requestUrl || window.location.href,
data: {
action: 'slide',
slideIndex: idx,
oldIndex: this.options.slideIndex,
oldState: JSON.stringify(this.storeSlideState())
},
type: 'post',
dataType: 'json'
})
.done(_.bind(this.onSlideLoadDone, this))
.fail(_.bind(this.onSlideLoadFail, this));
return this;
};
/**
* Store current slide state (questions)
*/
proto.storeSlideState = function () {
var data = this.slide.options;
this.slideContainer.find('[data-question]').each(function (idx, node) {
data.question = data.question || {};
var $node = $(node);
var questionId = $node.data('question');
var answerId = $node.find(':checked').val();
data.question[questionId] = answerId;
});
return data;
};
proto.onSlideLoadDone = function (response) {
/**
* @param {Object} response
* @param {String} response.mode ex. 'slide'|'result'
* @param {Number} response.slideIndex
* @param {Object} response.slideState
* @param {String} response.slideHtml
*/
debug && console.log('Training.onSlideLoadDone', response);
this.stopLoading();
if (null == response || null == response.mode) {
debug && console.warn('Training.onSlideLoadDone', 'wrong response');
return;
}
if (response.mode === 'slide') {
this.options.slideId = +response.slideId;
this.options.slideIndex = +response.slideIndex;
this.slideContainer.html(response.slideHtml);
this.initSlide(response.slideState);
} else if (response.mode === 'result') {
this.options.slideIndex = this.options.slideCount;
this.container.html(response.html);
this.initResultPage();
} else {
debug && console.warn('Training.onSlideLoadDone', 'Wrong response param .mode', response.mode);
}
};
proto.onSlideLoadFail = function (response) {
debug && console.log('Training.onSlideLoadFail', response);
this.stopLoading();
};
proto.initResultPage = function () {
window.scrollTo(0, 0);
};
proto.initSlideQuestions = function () {
if (this.slide.getFinished()) {
return this;
}
var that = this;
var questions = this.slideContainer.find('[data-question]');
if (questions.length) {
questions.each(function (idx, node) {
var $node = $(node);
var lockId = 'question-' + idx;
if ($node.data('slideQuestionInitialized')) {
return;
}
that.addLock(lockId, $node);
$node.one('click', 'input', _.bind(that.removeLock, that, lockId, 0));
$node.data('slideQuestionInitialized', true);
});
}
return this;
};
proto.initSlideVideo = function () {
var $node = $('#' + this.options.playerId);
var videoId;
if (!$node.length || !(videoId = $node.data('videoId')) || $node.data('slideVideoInitialized')) {
return;
}
this.addLock('slide', this.slideContainer);
this.removeLock('slide', this.options.slideLockTimeout);
if (null == YT || null == YT.Player) {
return;
}
this.slide.video = {
player: null,
duration: null,
playDuration: 0,
playStart: null
};
$node.data('slideVideoInitialized', true);
this.player = new YT.Player(this.options.playerId, {
height: '390',
width: '640',
videoId: videoId,
playerVars: {hl: 'en', rel: 0, showinfo: 0, modestbranding: 1, disablekb: 0, controls: 1},
events: {
'onReady': _.bind(this.onPlayerReady, this),
'onStateChange': _.bind(this.onPlayerStateChange, this),
'onError': _.bind(this.onPlayerError, this)
}
});
return this;
};
proto.onPlayerReady = function (e) {
debug && console.log('Training.onPlayerReady', e);
if (this.slide.getFinished()) {
this.slideContainer.find('[data-video-progress]').css('width', '100%');
} else {
this.addLock('video', $('#' + this.options.playerId));
this.slide.video.player = e.target;
}
};
/**
* YouTube Player state values:
* -1 (unstarted)
* 0 YT.PlayerState.ENDED (ended)
* 1 YT.PlayerState.PLAYING (playing)
* 2 YT.PlayerState.PAUSED (paused)
* 3 YT.PlayerState.BU./seFFERING (buffering)
* 5 YT.PlayerState.CUED (video cued).
*
* @param {Object} e
* @param {Number} e.data player state
*/
proto.onPlayerStateChange = function (e) {
var state = e.data;
debug && console.log('Training.onPlayerStateChange', e, state);
if (!this.slide.getFinished()) {
if (state == YT.PlayerState.PLAYING) {
this.slide.video.playStart = (new Date()).getTime();
} else {
if (this.slide.video.playStart) {
this.slide.video.playDuration += ((new Date()).getTime() - this.slide.video.playStart) / 1000;
this.slide.video.playStart = null;
}
}
this.playerInitProgress();
}
};
/**
* YouTube Player Error codes:
* 2 – The request contains an invalid parameter value.
* 5 – The requested content cannot be played in an HTML5 player or another error related to the HTML5 player has occurred.
* 100 – The video requested was not found. This error occurs when a video has been removed (for any reason) or has been marked as private.
* 101 – The owner of the requested video does not allow it to be played in embedded players.
* 150 – This error is the same as 101. It's just a 101 error in disguise!
*
* @param {Object} e
* @param {Number} e.data error code
*/
proto.onPlayerError = function (e) {
debug && console.warn('Training.onPlayerError', e, e.data);
};
proto.playerInitProgress = function () {
setTimeout($.proxy(this.onPlayProgress, this), 1000);
};
proto.onPlayProgress = function () {
if (null == this.slide.video) {
return;
}
var progress;
if (null == this.slide.video.duration) {
this.slide.video.duration = this.slide.video.player.getDuration();
}
var playDuration = this.slide.video.playDuration;
if (this.slide.video.playStart) {
playDuration += ((new Date()).getTime() - this.slide.video.playStart) / 1000;
}
progress = playDuration / this.slide.video.duration;
if (progress < 0) {
progress = 0;
} else if (progress > 1) {
progress = 1;
}
if (progress >= 0.95) {
progress = 1;
this.removeLock('video');
}
this.slideContainer.find('[data-video-progress]').css('width', Math.round(progress * 100) + '%');
if (progress < 1) {
this.playerInitProgress();
}
};
/**
* @constructor
* @param {Object=} options
* @param {Boolean} options.finished
*/
function Slide(options) {
this.construct(options);
}
Slide.prototype.construct = function (options) {
this.options = $.extend({}, options || {});
this.locks = {};
};
Slide.prototype.getFinished = function () {
return !!this.options.finished;
};
Slide.prototype.setFinished = function (flag) {
this.options.finished = !!flag;
return this;
};
/**
*
* @param {String} id
* @param {HTMLElement|jQuery} target
* @param {*} data
* @constructor
*/
function LockItem(id, target, data) {
this.id = id;
this.target = target;
this.data = data;
}
})(qs.defineNS('app.training'), youtubeApiReadyPromise);
(function($) {
$.fn.blink = function(options) {
var target = $(this), i = 0;
options = $.extend({frequency: 750, count: 1}, options);
var doBlink = function () {
if (i++ >= options.count) {
target.data('blinking', false);
return;
}
target.data('blinking', true);
target.animate({opacity: 0.25}, 300, function () {
target.animate({opacity: 1}, 300, function () {
target.css('opacity', '');
});
});
setTimeout(doBlink, options.frequency);
};
if (!target.data('blinking')) {
target.data('blinking', true);
doBlink();
}
return this;
};
})(jQuery);