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

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);