summaryrefslogtreecommitdiff
path: root/addons/website_slides/static/src
diff options
context:
space:
mode:
authorqmo-odoo <qmo@odoo.com>2019-02-22 09:47:39 +0000
committerjem-odoo <jem@openerp.com>2019-03-12 15:31:40 +0000
commitfd78a40836dad044bfb8c0ab30fcb37fc8ecf9af (patch)
treeb784b1227767b66a0fb041cd85a8d96989ebc8ea /addons/website_slides/static/src
parentc9f832d9f032b6af51807b387b325b5c2b8aad00 (diff)
[REF] website_slides: revamp fullscreen widget
This commit makes the 'Fullscreen' page a complete odoo widgets. We decided to split it into some sub widget: - Fullscreen: will be the master piece, handle communication of components, displaying slide content, setting slide as completed, handling server RPC calls, .... - Sidebar: responsible for slide navigation, based on the table of content. (prev, next, keyboard naviguation, ...) - VideoPlayer: display content for Youtube video and handling player event to auto set as completed, ... The DOM of this page is still rendered serverside. DOM was cleaned in order to be less complex and less deep. CSS style was also reviewed to decrease line of code. Fullscreen is now completely using Quiz widget with events triggered. Task-1942852
Diffstat (limited to 'addons/website_slides/static/src')
-rw-r--r--addons/website_slides/static/src/js/slides_course_fullscreen_player.js765
-rw-r--r--addons/website_slides/static/src/js/slides_course_quiz.js18
-rw-r--r--addons/website_slides/static/src/scss/slides_slide_fullscreen.scss265
-rw-r--r--addons/website_slides/static/src/xml/slide_quiz.xml2
-rw-r--r--addons/website_slides/static/src/xml/website_slides_fullscreen.xml59
5 files changed, 522 insertions, 587 deletions
diff --git a/addons/website_slides/static/src/js/slides_course_fullscreen_player.js b/addons/website_slides/static/src/js/slides_course_fullscreen_player.js
index b7165e92d4d..f8c34ee222f 100644
--- a/addons/website_slides/static/src/js/slides_course_fullscreen_player.js
+++ b/addons/website_slides/static/src/js/slides_course_fullscreen_player.js
@@ -7,414 +7,561 @@ odoo.define('website_slides.fullscreen', function (require) {
var core = require('web.core');
var QWeb = core.qweb;
- var tag = document.createElement('script');
+ var Quiz = require('website_slides.quiz');
- var QuizWidget = require('website_slides.quiz');
- tag.src = "https://www.youtube.com/iframe_api";
- var firstScriptTag = document.getElementsByTagName('script')[0];
- firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
+ /**
+ * Helper: Get the slide dict matching the given criteria
+ *
+ * @private
+ * @param {Array<Object>} slideList List of dict reprensenting a slide
+ * @param {Object} matcher (see https://underscorejs.org/#matcher)
+ */
+ var findSlide = function (slideList, matcher) {
+ var slideMatch = _.matcher(matcher);
+ return _.find(slideList, slideMatch);
+ };
+ /**
+ * This widget is responsible of display Youtube Player
+ *
+ * The widget will trigger an event `change_slide` when the video is at
+ * its end, and `slide_completed` when the player is at 30 sec before the
+ * end of the video (30 sec before is considered as completed).
+ */
+ var VideoPlayer = Widget.extend({
+ template: 'website.slides.fullscreen.video',
+ youtubeUrl: 'https://www.youtube.com/iframe_api',
- var Fullscreen = Widget.extend({
- custom_events: {
- quiz_next_slide: '_goToNextSlide',
- quiz_completed: '_onQuizCompleted'
- },
- /**
- * @override
- * @param {Object} el
- * @param {Object} data holding channelId and optionally upload and publish control parameters
- */
- init: function (el, courseId, slideId, userId) {
- this.courseID = parseInt(courseId, 10);
- this.slideID = parseInt(slideId, 10);
- this.userID = parseInt(userId, 10);
- this.course = undefined;
- this.slide = undefined;
- this.slides = [];
- this.nextSlide = undefined;
- this.previousSlide = undefined;
- this.url = undefined;
- this.urlToSmallScreen = undefined;
- this.activetab = undefined;
- this.player = undefined;
- this.goToQuiz = false;
- this.slideTitle = undefined;
- return this._super.apply(this,arguments);
+ init: function (parent, slide) {
+ this.slide = slide;
+ return this._super.apply(this, arguments);
},
start: function (){
var self = this;
- this.url = window.location.pathname;
- this.urlToSmallScreen = this.url.replace('/fullscreen','');
- this._getSlides();
- this._renderPlayer();
- this._bindListEvents();
- $(document).keydown(this._onKeyDown.bind(this));
- return this._super.apply(this, arguments);
+ return $.when(this._super.apply(this, arguments), this._loadYoutubeAPI()).then(function() {
+ self._setupYoutubePlayer();
+ });
},
- //--------------------------------------------------------------------------
- // Private
- //--------------------------------------------------------------------------
- /**
- * @private
- * Renders the player accordingly to the current slide
- */
- _renderPlayer: function (){
- var self = this;
-
+ _loadYoutubeAPI: function () {
var def = $.Deferred();
- if (this.slide.slide_type === 'webpage') {
- this._fetchHtmlContent().then(function () {
- self._renderWebpage();
- def.resolve();
- });
- } else {
- if (this.slide.slide_type !== 'quiz') {
- // no RPC, get slide data from existing DOM
- var embed_url = $(this.slide.embed_code).attr('src');
- if (this.slide.slide_type === "video"){
- embed_url = "https://" + embed_url + "&rel=0&autoplay=1&enablejsapi=1&origin=" + window.location.origin;
- }
- $('.o_wslides_fs_player').html(QWeb.render('website.slides.fullscreen', {
- slide: self.slide,
- nextSlide: self.nextSlide,
- questions: self.slide.quiz ? self.slide.quiz.questions: '',
- embed_url: embed_url,
- question_count: self.slide.quiz ? self.slide.quiz.questions.length : '',
- letters: self.slide.quiz ? self.letters : '',
- showMiniQuiz: self.goToQuiz
- }));
+ if ($(document).find('script[src="' + this.youtubeUrl + '"]').length === 0) {
+ var $youtubeElement = $('<script/>', {src: this.youtubeUrl});
+ $(document.head).append($youtubeElement);
- if(this.slide.slide_type === "video"){
- this._renderYoutubeIframe();
- }
+ // function called when the Youtube asset is loaded
+ // see https://developers.google.com/youtube/iframe_api_reference#Requirements
+ onYouTubeIframeAPIReady = function () {
def.resolve();
- }
- }
-
- if (this.slide.slide_type === "quiz" || this.goToQuiz) {
- this._renderQuiz().then(function() {
- def.resolve();
- });
- }
+ };
- return def.then(function() {
- self._renderTitle();
- });
- },
- _renderYoutubeIframe: function (){
- var self = this;
- /**
- * Due to issues of synchronization between the youtube api script and the widget's instanciation.
- */
- try {
- self._setupYoutubePlayer();
- }
- catch (error) {
- onYouTubeIframeAPIReady = function (){
- var self = this;
- self._setupYoutubePlayer();
- }.bind(this);
+ } else {
+ def.resolve();
}
- },
- _renderWebpage: function (){
- var self = this;
- $(self.slide.htmlContent).appendTo('.o_wslides_fs_webpage_content');
- },
-
- _renderQuiz: function (){
- var quizSlideData = _.extend(this.slide, {
- completed: this.slide.done,
- });
- var Quiz = new QuizWidget(this, quizSlideData);
- return Quiz.appendTo('.o_wslides_fs_player');
- },
-
- _renderTitle: function (){
- var self = this;
- $('.o_wslides_fs_slide_title').empty().html(QWeb.render('website.course.fullscreen.title', {
- slide: self.slide,
- miniQuiz: self.goToQuiz
- }));
+ return def;
},
/**
- * @private
* Links the youtube api to the iframe present in the template
+ *
+ * @private
*/
_setupYoutubePlayer: function (){
- var self = this;
- self.player = new YT.Player('youtube-player', {
+ this.player = new YT.Player('youtube-player' + this.slide.id, {
host: 'https://www.youtube.com',
- playerVars: {'autoplay': 1, 'origin': window.location.origin},
- autoplay: 1,
+ playerVars: {
+ 'autoplay': 1,
+ 'origin': window.location.origin
+ },
events: {
- 'onReady': self._onPlayerReady,
- 'onStateChange': this._onPlayerStateChange.bind(self)
+ 'onStateChange': this._onPlayerStateChange.bind(this)
}
});
},
/**
- * @param {*} event
* Specific method of the youtube api.
* Whenever the player starts playing, a setinterval is created.
* This setinterval is used to check te user's progress in the video.
- * Once the user reaches a particular time in the video, the slide will be considered as completed if the video doesn't have a mini-quiz.
- * This method also allows to automatically go to the next slide (or the quiz associated to the current video) once the video is over
+ * Once the user reaches a particular time in the video, the slide will be considered as completed
+ * if the video doesn't have a mini-quiz.
+ * This method also allows to automatically go to the next slide (or the quiz associated to the current
+ * video) once the video is over
+ *
+ * @private
+ * @param {*} event
*/
_onPlayerStateChange: function (event){
var self = this;
- var tid;
clearInterval(self.tid);
- if (event.data === YT.PlayerState.PLAYING && !self.slide.done) {
+ if (event.data === YT.PlayerState.PLAYING && !self.slide.completed) {
self.tid = setInterval(function (){
if (event.target.getCurrentTime){
var currentTime = event.target.getCurrentTime();
var totalTime = event.target.getDuration();
if (totalTime && currentTime > totalTime - 30){
clearInterval(self.tid);
- if (!self.slide.hasQuiz && !self.slide.done){
- self.slide.done = true;
- self._setSlideStateAsDone();
+ if (!self.slide.hasQuestion && !self.slide.completed){
+ self.trigger_up('slide_completed', self.slide);
}
}
}
}, 1000);
}
if (event.data === YT.PlayerState.ENDED){
- self.player = undefined;
- self._goToNextSlide();
+ this.player = undefined;
+ if (this.slide.hasNext) {
+ this.trigger_up('slide_go_next');
+ }
}
},
+ });
+
+
+ /**
+ * This widget is responsible of navigation for one slide to another:
+ * - by clicking on any slide list entry
+ * - by mouse click (next / prev)
+ * - by recieving the order to go to prev/next slide (`goPrevious` and `goNext` public methods)
+ *
+ * The widget will trigger an event `change_slide` with
+ * the `slideId` and `isMiniQuiz` as data.
+ */
+ var Sidebar = Widget.extend({
+ events: {
+ "click .o_wslides_fs_sidebar_list_item": '_onClickTab',
+ },
+ init: function (parent, slideList, defaultSlide) {
+ var result = this._super.apply(this, arguments);
+ this.slideEntries = slideList;
+ this.set('slideEntry', defaultSlide);
+ return result;
+ },
+ start: function (){
+ var self = this;
+ this.on('change:slideEntry', this, this._onChangeCurrentSlide);
+ return this._super.apply(this, arguments).then(function (){
+ $(document).keydown(self._onKeyDown.bind(self));
+ });
+ },
+ destroy: function () {
+ $(document).unbind('keydown', this._onKeyDown.bind(this));
+ return this._super.apply(this, arguments);
+ },
+ //--------------------------------------------------------------------------
+ // Public
+ //--------------------------------------------------------------------------
/**
- * @private
- * Creates slides objects from every slide-list-cells attributes
+ * Change the current slide with the next one (if there is one).
+ *
+ * @public
*/
- _getSlides: function (){
- var self = this;
- var slides = $('.o_wslides_fs_sidebar_slide_tab');
- for (var i = 0; i < slides.length;i++){
- var slide = $(slides[i]);
- self.slides.push(slide.data());
- this._getActiveSlide();
+ goNext: function () {
+ var currentIndex = this._getCurrentIndex();
+ if (currentIndex < this.slideEntries.length-1) {
+ this.set('slideEntry', this.slideEntries[currentIndex+1]);
+ }
+ },
+ /**
+ * Change the current slide with the previous one (if there is one).
+ *
+ * @public
+ */
+ goPrevious: function () {
+ var currentIndex = this._getCurrentIndex();
+ if (currentIndex >= 1) {
+ this.set('slideEntry', this.slideEntries[currentIndex-1]);
}
},
+ /**
+ * Greens up the bullet when the slide is completed
+ *
+ * @public
+ * @param {*} slide_id
+ */
+ setSlideCompleted: function (slideId) {
+ var $elem = this.$('.fa-circle-thin[data-slide-id="'+slideId+'"]');
+ $elem.removeClass('fa-circle-thin').addClass('fa-check-circle text-success');
+ },
+ /**
+ * Updates the progressbar whenever a lesson is completed
+ *
+ * @public
+ * @param {*} channelCompletion
+ */
+ updateProgressbar: function (channelCompletion) {
+ var completion = Math.min(100, channelCompletion);
+ this.$('.progress-bar').css('width', completion + "%" );
+ this.$('.o_wslides_progress_percentage').text(completion);
+ },
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
/**
- * @private
+ * Get the index of the current slide entry (slide and/or quiz)
*/
- _fetchHtmlContent: function(){
- var self = this;
- return self._rpc({
- route: 'slides/slide/get_html_content',
- params: {
- 'slide_id': self.slide.id
- }
- }).then(function (data){
- if (data.html_content) {
- self.slide.htmlContent = data.html_content;
- }
+ _getCurrentIndex: function () {
+ var slide = this.get('slideEntry');
+ var currentIndex = _.findIndex(this.slideEntries, function (entry) {
+ return entry.id === slide.id && entry.isQuiz === slide.isQuiz;
});
+ return currentIndex;
},
+ //--------------------------------------------------------------------------
+ // Handler
+ //--------------------------------------------------------------------------
/**
+ * Handler called whenever the user clicks on a sub-quiz which is linked to a slide.
+ * This does NOT handle the case of a slide of type "quiz".
+ * By going through this handler, the widget will be able to determine that it has to render
+ * the associated quiz and not the main content.
+ *
* @private
- * Once the completion conditions are filled,
- * sends a json request to the backend to set the relation between the slide and the user as being completed
+ * @param {*} ev
*/
- _setSlideStateAsDone: function (){
- var self = this;
- return self._rpc({
- route: '/slides/slide/set_completed',
- params: {
- slide_id: self.slide.id,
- }
- }).then(function (data) {
- if (! data.error) {
- $('#check-'+self.slide.id).replaceWith($('<i class="check-done o_wslides_slide_completed fa fa-check-circle"></i>'));
- self.slide.done = true;
- clearInterval(self.tid);
- self.channelCompletion = data.channel_completion;
- self._updateProgressbar();
- }
+ _onClickMiniQuiz: function (ev){
+ var slideID = parseInt($(ev.currentTarget).data().slide_id);
+ this.set('slideEntry',{
+ slideID: slideID,
+ isMiniQuiz: true
});
+ this.trigger_up('change_slide', this.get('slideEntry'));
},
- _updateProgressbar: function () {
- var completion = _.min([this.channelCompletion, 100]);
- this.$('.o_wslides_fs_sidebar_progressbar .progress-bar').css('width', completion + "%" );
- this.$('.o_wslides_progress_percentage').text(completion);
+ /**
+ * Handler called when the user clicks on a normal slide tab
+ *
+ * @private
+ * @param {*} ev
+ */
+ _onClickTab: function (ev) {
+ var $elem = $(ev.currentTarget);
+ var isQuiz = $elem.data('isQuiz');
+ var slideID = parseInt($elem.data('id'));
+ var slide = findSlide(this.slideEntries, {id: slideID, isQuiz: isQuiz});
+ this.set('slideEntry', slide);
},
/**
+ * Actively changes the active tab in the sidebar so that it corresponds
+ * the slide currently displayed
+ *
* @private
- * Creates an array of letters to be used in the quiz with a foreach
*/
- _generateQuizLetters: function (){
- var letters = [];
- for (var i = 65; i < 91; i++){
- letters.push(String.fromCharCode(i));
+ _onChangeCurrentSlide: function () {
+ var slide = this.get('slideEntry');
+ this.$('.o_wslides_fs_sidebar_list_item.active').removeClass('active');
+ var selector = '.o_wslides_fs_sidebar_list_item[data-id='+slide.id+']';
+ if (slide.isQuiz) {
+ selector += '[data-is-quiz="1"]';
+ } else {
+ selector += '[data-is-quiz!="1"]';
}
- return letters;
+ this.$(selector).addClass('active');
+ this.trigger_up('change_slide', this.get('slideEntry'));
},
- _goToNextSlide: function (){
- var self = this;
- clearInterval(self.tid);
- self.player = undefined;
- self.goToQuiz = self.slide.hasQuiz && !self.goToQuiz && self.slide.slide_type !== 'quiz';
- if (self.nextSlide && !self.goToQuiz){
- self.slide = self.nextSlide;
- self.index++;
- self._setActiveTab();
- self._renderPlayer();
- self._setPreviousAndNextSlides();
- self._updateUrl();
- history.pushState(null,'',self.url);
+
+ /**
+ * Binds left and right arrow to allow the user to navigate between slides
+ *
+ * @param {*} ev
+ * @private
+ */
+ _onKeyDown: function (ev){
+ switch (ev.key){
+ case "ArrowLeft":
+ this.goPrevious();
+ break;
+ case "ArrowRight":
+ this.goNext();
+ break;
}
- else if (self.nextSlide){
- self._renderPlayer();
+ },
+ });
+
+
+ /**
+ * This widget's purpose is to show content of a course, naviguating through contents
+ * and correclty display it. It also handle slide completion, course progress, ...
+ *
+ * This widget is rendered sever side, and attached to the existing DOM.
+ */
+ var Fullscreen = Widget.extend({
+ events: {
+ "click .o_wslides_fs_toggle_sidebar": '_onClickToggleSidebar',
+ },
+ custom_events: {
+ 'change_slide': '_onChangeSlideRequest',
+ 'toggle_sidebar': '_onToggleSidebar',
+ 'slide_completed': '_onSlideCompleted',
+ 'quiz_completed': '_onSlideCompleted',
+ 'slide_go_next': '_onSlideGoToNext',
+ },
+ /**
+ * @override
+ * @param {Object} el
+ * @param {Object} slides Contains the list of all slides of the course
+ * @param {integer} defaultSlideId Contains the ID of the slide requested by the user
+ */
+ init: function (el, slides, defaultSlideId){
+ var result = this._super.apply(this,arguments);
+ this.initialSlideID = defaultSlideId;
+ this.slides = this._preprocessSlideData(slides);
+
+ var slide;
+ if (defaultSlideId) {
+ slide = findSlide(this.slides, {id: defaultSlideId});
+ } else {
+ slide = this.slides[0];
}
+
+ this.set('slide', slide);
+
+ this.sidebar = new Sidebar(this, this.slides, slide);
+ return result;
},
- _goToPreviousSlide: function (){
+ /**
+ * @override
+ */
+ start: function (){
var self = this;
- clearInterval(self.tid);
- self.goToQuiz = false;
- self.player = undefined;
- if (self.previousSlide){
- self.slide = self.previousSlide;
- self.index--;
- self._setActiveTab();
- self._renderPlayer();
- self._setPreviousAndNextSlides();
- self._updateUrl();
- history.pushState(null,'',self.url);
- }
+ this.on('change:slide', this, this._onChangeSlide);
+ return this._super.apply(this, arguments).then(function () {
+ self._onChangeSlide(); // trigger manually once DOM ready, since slide content is not rendered server side
+ });
+ },
+ /**
+ * Extended to attach sub widget to sub DOM. This might be experimental but
+ * seems working fine.
+ *
+ * @override
+ */
+ attachTo: function (){
+ var defs = [this._super.apply(this, arguments)];
+ defs.push(this.sidebar.attachTo(this.$('.o_wslides_fs_sidebar')));
+ return $.when.apply($, defs);
},
- _setPreviousAndNextSlides: function (){
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+ /**
+ * Fetches content with an rpc call for slides of type "webpage"
+ *
+ * @private
+ */
+ _fetchHtmlContent: function (){
var self = this;
- self.previousSlide = self.index > 0 ? self.slides[self.index-1] : undefined;
- self.nextSlide = self.index < (self.slides.length - 1) ? self.slides[self.index+1] : undefined;
+ var currentSlide = this.get('slide');
+ return self._rpc({
+ route:"/slides/slide/get_html_content",
+ params: {
+ 'slide_id': currentSlide.id
+ }
+ }).then(function (data){
+ if (data.html_content) {
+ currentSlide.htmlContent = data.html_content;
+ }
+ });
+ },
+ /**
+ * Fetches slide content depending on its type.
+ * If the slide doesn't need to fetch any content, return a resolved deferred
+ *
+ * @private
+ */
+ _fetchSlideContent: function (){
+ var slide = this.get('slide');
+ if (slide.type === 'webpage') {
+ return this._fetchHtmlContent();
+ }
+ return $.when();
+ },
+ /**
+ * Extend the slide data list to add informations about rendering method, and other
+ * specific values according to their slide_type.
+ */
+ _preprocessSlideData: function (slidesDataList) {
+ slidesDataList.forEach(function (slideData, index) {
+ // compute hasNext slide
+ slideData.hasNext = index < slidesDataList.length-1;
+ // compute embed url
+ if (slideData.type === 'video') {
+ slideData.embedCode = $(slideData.embedCode).attr('src'); // embedCode containts an iframe tag, where src attribute is the url (youtube or embed document from odoo)
+ slideData.embedUrl = "https://" + slideData.embedCode + "&rel=0&autoplay=1&enablejsapi=1&origin=" + window.location.origin;
+ } else if (slideData.type === 'infographic') {
+ slideData.embedUrl = _.str.sprintf('/web/image/slide.slide/%s/image', slideData.id);
+ } else if (_.contains(['document', 'presentation'], slideData.type)) {
+ slideData.embedUrl = $(slideData.embedCode).attr('src');
+ }
+ // fill empty property to allow searching on it with _.filter(list, matcher)
+ slideData.isQuiz = !!slideData.isQuiz;
+ slideData.hasQuestion = !!slideData.hasQuestion;
+ // technical settings for the Fullscreen to work
+ slideData._autoSetDone = _.contains(['infographic', 'presentation', 'document', 'webpage'], slideData.type) && !slideData.hasQuestion;
+ });
+ return slidesDataList;
},
/**
* Changes the url whenever the user changes slides.
- * This allows the user to refresh the page and stay at the right video
+ * This allows the user to refresh the page and stay on the right slide
+ *
+ * @private
*/
- _updateUrl: function (){
- var self = this;
- var url = window.location.pathname.split('/');
- url[url.length-1] = self.slide.slug;
- url = url.join('/');
- self.url = url;
- self.urlToSmallScreen = self.url;
- self.url += "?fullscreen=1";
- $('.o_wslides_small_screen').attr('href', self.urlToSmallScreen);
+ _pushUrlState: function (){
+ var urlParts = window.location.pathname.split('/');
+ urlParts[urlParts.length-1] = this.get('slide').slug;
+ var url = _.str.sprintf('%s?fullscreen=1', urlParts.join('/'));
+ history.pushState(null, '', url);
},
/**
- * Whenever the user changes slide, change the active tab
+ * Render the current slide content using specific mecanism according to slide type:
+ * - simply append content (for webpage)
+ * - template rendering (for image, document, ....)
+ * - using a sub widget (quiz and video)
+ *
+ * @private
+ * @returns Deferred
*/
- _setActiveTab: function (){
- var self = this;
- self.activeTab.removeClass('active');
- $('li.active').removeClass('active');
- $('li[data-slide-id='+self.slide.id+']').addClass('active');
- self.activeTab = $('.o_wslides_fs_sidebar_slide_tab[index="'+self.index+'"]');
- self.activeTab.addClass('active');
+ _renderSlide: function () {
+ var slide = this.get('slide');
+ var $content = this.$('.o_wslides_fs_content');
+ $content.empty();
+ $content.removeClass('bg-white'); // webpage case
+
+ // display quiz slide, or quiz attached to a slide
+ if (slide.type === 'quiz' || slide.isQuiz) {
+ var QuizWidget = new Quiz(this, slide);
+ return QuizWidget.appendTo($content);
+ }
+
+ // render slide content
+ if (_.contains(['document', 'presentation', 'infographic'], slide.type)) {
+ $content.html(QWeb.render('website.slides.fullscreen.content', {widget: this}));
+ } else if (slide.type === 'video') {
+ this.videoPlayer = new VideoPlayer(this, slide);
+ return this.videoPlayer.appendTo($content);
+ } else if (slide.type === 'webpage'){
+ $(slide.htmlContent).appendTo($content);
+ $content.addClass('bg-white');
+ }
+ return $.when();
},
/**
- * The first time the user gets on the player,
- * get the slide that is represented by the active tab in the sidebar
+ * Once the completion conditions are filled,
+ * rpc call to set the the relation between the slide and the user as "completed"
+ *
+ * @private
+ * @param {Integer} slideId: the id of slide to set as completed
*/
- _getActiveSlide: function (){
+ _setCompleted: function (slideId){
var self = this;
- self.activeTab = $('.o_wslides_fs_sidebar_slide_tab.active');
- self.index = parseInt(self.activeTab.attr('index'), 10);
- self.slide = self.slides[self.index];
- self._setPreviousAndNextSlides();
+ var slide = findSlide(this.slides, {id: slideId});
+ if (!slide.completed) { // no useless RPC call
+ return this._rpc({
+ route: '/slides/slide/set_completed',
+ params: {
+ slide_id: slide.id,
+ }
+ }).then(function (data){
+ slide.completed = true;
+ self.sidebar.setSlideCompleted(slide.id);
+ self.sidebar.updateProgressbar(data.channel_completion);
+ });
+ }
+ return $.when();
},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
- _onListCellClick: function (ev){
- var self = this;
- clearInterval(self.tid);
- self.player = undefined;
- var target = $(ev.currentTarget);
- self.goToQuiz = false;
- if (target[0] !== self.activeTab[0]){
- self.activeTab.removeClass('active');
- target.addClass('active');
- self.index = parseInt(target.attr('index'));
- self._getActiveSlide();
- self._renderPlayer();
- $('li.active').removeClass('active');
- $('li[data-slide-id='+self.slide.id+']').addClass('active');
- self._setPreviousAndNextSlides();
- self._updateUrl();
- history.pushState(null,'',self.url);
- }
- },
- _sidebarToggle: function (ev){
- ev.preventDefault();
- $(ev.currentTarget).toggleClass('active');
- $('.o_wslides_fs_sidebar').toggleClass('o_wslides_fs_sidebar_hidden');
- $('.o_wslides_fs_player').toggleClass('o_wslides_fs_player_no_sidebar');
- },
- _onMiniQuizClick: function (ev){
- var self = this;
- self.index = parseInt($(ev.currentTarget).attr('index')) || 0;
- self.slide = self.slides[self.index];
- self.goToQuiz = true;
- self._setPreviousAndNextSlides();
- self._renderPlayer();
- self._setActiveTab();
- self._updateUrl();
- history.pushState(null,'' ,self.url);
- },
- _onQuizCompleted: function (ev) {
- this._setSlideStateAsDone();
- },
- /**
- * @private
- * Binds events related to the list
- */
- _bindListEvents: function (){
+ /**
+ * Method called whenever the user changes slides.
+ * When the current slide is changed, it will autoimatically update the widget
+ * and allow it to fetch the content if needs be, render it, update the url and set the
+ * slide as "completed" if its type makes it necessary
+ *
+ * @private
+ */
+ _onChangeSlide: function (){
var self = this;
- $('.o_wslides_fs_sidebar_slide_tab').each(function () {
- $(this).click(self._onListCellClick.bind(self));
- });
- $('.o_wslides_fs_slide_quiz ').each(function (){
- $(this).click(self._onMiniQuizClick.bind(self));
+ var slide = this.get('slide');
+ return this._fetchSlideContent().then(function() { // update title and render content
+ self.$('.o_wslides_fs_slide_title').html(QWeb.render('website.slides.fullscreen.title', {widget: self}));
+ return self._renderSlide();
+ }).then(function() {
+ if (slide._autoSetDone) {
+ self._setCompleted(slide.id);
+ }
+ self._pushUrlState();
});
- $('.o_wslides_fs_toggle_sidebar').click(function (ev){
- self._sidebarToggle(ev);
+ },
+ /**
+ * Changes current slide when receiving custom event `change_slide` with
+ * its id and if it's its quizz or not we need to display.
+ *
+ * @private
+ */
+ _onChangeSlideRequest: function (ev){
+ var slideData = ev.data;
+ var newSlide = findSlide(this.slides, {
+ id: slideData.id,
+ isQuiz: slideData.isQuiz || false,
});
+ this.set('slide', newSlide);
},
- _onKeyDown: function (ev){
- var self = this;
- switch (ev.key){
- case "ArrowRight":
- self._goToNextSlide();
- break;
- case "ArrowLeft":
- self._goToPreviousSlide();
- break;
- }
+ /**
+ * Triggered when sub widget business is done and that slide
+ * can now be marked as done.
+ *
+ * @private
+ */
+ _onSlideCompleted: function (ev) {
+ var slideId = ev.data.id;
+ this._setCompleted(slideId);
+ },
+ /**
+ * Go the next slide
+ *
+ * @private
+ */
+ _onSlideGoToNext: function (ev) {
+ this.sidebar.goNext();
+ },
+ /**
+ * Show or Hide the sidebar
+ *
+ * @private
+ */
+ _onClickToggleSidebar: function (ev){
+ ev.preventDefault();
+ this.$('.o_wslides_fs_sidebar').toggleClass('d-none');
+ this.$('.o_wslides_fs_content').toggleClass('col-10').toggleClass('col-12');
+ this.$('.o_wslides_fs_toggle_sidebar').toggleClass('active');
},
});
sAnimations.registry.websiteSlidesFullscreenPlayer = Widget.extend({
selector: '.o_wslides_fs_main',
xmlDependencies: ['/website_slides/static/src/xml/website_slides_fullscreen.xml'],
- init: function (el){
- this._super.apply(this, arguments);
- },
start: function (){
var defs = [this._super.apply(this, arguments)];
- var userId = this.$el.data('userId');
- var courseId = this.$el.data('courseId');
- var slideId = this.$el.data('slideId');
- var fullscreen = new Fullscreen(this, courseId, slideId, userId);
- defs.push(fullscreen.attachTo(this.$el));
+ var fullscreen = new Fullscreen(this, this._getSlides(), this._getCurrentSlideID());
+ defs.push(fullscreen.attachTo(".o_wslides_fs_main"));
return $.when.apply($, defs);
- }
+ },
+ _getCurrentSlideID: function (){
+ return parseInt(this.$('.o_wslides_fs_sidebar_list_item.active').data('id'));
+ },
+ /**
+ * @private
+ * Creates slides objects from every slide-list-cells attributes
+ */
+ _getSlides: function (){
+ var $slides = this.$('.o_wslides_fs_sidebar_list_item');
+ var slideList = [];
+ $slides.each(function () {
+ var slideData = $(this).data();
+ slideList.push(slideData);
+ });
+ return slideList;
+ },
});
return Fullscreen;
-}); \ No newline at end of file
+});
diff --git a/addons/website_slides/static/src/js/slides_course_quiz.js b/addons/website_slides/static/src/js/slides_course_quiz.js
index 319ca629bf2..c969e38623b 100644
--- a/addons/website_slides/static/src/js/slides_course_quiz.js
+++ b/addons/website_slides/static/src/js/slides_course_quiz.js
@@ -15,9 +15,8 @@ odoo.define('website_slides.quiz', function (require) {
* used client side (Fullscreen).
*
* Triggered events are :
- * - quiz_next_slide: need to go to the next slide, when quiz is done. Event data contains the current slide id.
- * - quiz_completed: when the quiz is passed and completed by the user. Event data contains completion
- * percentage and current slide id.
+ * - slide_go_next: need to go to the next slide, when quiz is done. Event data contains the current slide id.
+ * - quiz_completed: when the quiz is passed and completed by the user. Event data contains current slide data.
*/
var Quiz = Widget.extend({
template: 'slide.slide.quiz',
@@ -88,6 +87,8 @@ odoo.define('website_slides.quiz', function (require) {
}
else if (alert_code === 'slide_quiz_done') {
message = _t('This quiz is already done. Retaking it is not possible.');
+ } else if (alert_code === 'public_user') {
+ message = _t("You must be logged to submit the quiz.");
}
this.$('.o_wslides_js_lesson_quiz_error span').html(message);
this.$('.o_wslides_js_lesson_quiz_error').removeClass('d-none');
@@ -116,7 +117,6 @@ odoo.define('website_slides.quiz', function (require) {
_renderAnswers: function () {
var self = this;
this.$('input[type=radio]').each(function () {
- console.log($(this));
$(this).prop('disabled', self.slide.readonly);
});
},
@@ -184,9 +184,7 @@ odoo.define('website_slides.quiz', function (require) {
* @param {Integer} completion
*/
_setCompleted: function () {
- this.trigger_up('quiz_completed', {
- 'slideId': this.slide.id,
- });
+ this.trigger_up('quiz_completed', this.slide);
},
/*
* Submit the given answer, and display the result
@@ -241,9 +239,7 @@ odoo.define('website_slides.quiz', function (require) {
*/
_onClickNext: function (ev) {
if (this.slide.hasNext) {
- this.trigger_up('quiz_next_slide', {
- 'slideId': this.slide.id,
- });
+ this.trigger_up('slide_go_next');
}
},
/**
@@ -272,7 +268,7 @@ odoo.define('website_slides.quiz', function (require) {
sAnimations.registry.websiteSlidesQuizNoFullscreen = sAnimations.Class.extend({
selector: '.o_wslides_js_lesson_quiz',
custom_events: {
- quiz_next_slide: '_onQuizNextSlide',
+ slide_go_next: '_onQuizNextSlide',
quiz_completed: '_onQuizCompleted',
},
diff --git a/addons/website_slides/static/src/scss/slides_slide_fullscreen.scss b/addons/website_slides/static/src/scss/slides_slide_fullscreen.scss
index 08ef8bc299b..3868ad873cb 100644
--- a/addons/website_slides/static/src/scss/slides_slide_fullscreen.scss
+++ b/addons/website_slides/static/src/scss/slides_slide_fullscreen.scss
@@ -1,3 +1,9 @@
+$slides-bg-fullscreen: #313A44;
+$slides-bg-fullscreen-dark: darken($slides-bg-fullscreen,5%);
+$slides-bg-fullscreen-darker: darken($slides-bg-fullscreen,10%);
+
+$slides-text: #8A8F93;
+
.o_wslides_fs_main {
position: absolute;
top: 0;
@@ -5,267 +11,60 @@
width: 100%;
height: 100%;
z-index: 2000;
+ color: $slides-text;
+
.o_wslides_slide_fs_header {
- align-items: center;
- background-color: #313A44;
- color: #AEB0B2;
- display: flex;
+ background-color: $slides-bg-fullscreen;
height: 50px;
- justify-content: space-between;
- width: 100%;
-
- .o_wslides_fs_slide_title {
- height: 100%;
- display: flex;
- align-items: center;
- padding: 0 20px;
- color: #D5D7D9;
- font-weight: bold;
-
- i.fa-flag-checkered{
- color: #D39B4B;
- margin-right: 10px;
- }
- }
-
- div {
- display: flex;
- height: 100%;
-
- a {
- align-items: center;
- background-color: #1F242C;
- color: inherit;
- display: flex;
- font-size: 0.8rem;
- font-weight: bold;
- height: 100%;
- justify-content: center;
- padding: 0 20px;
- text-decoration: none !important;
- text-transform: uppercase;
-
- i {
- align-items: center;
- display: flex;
- height: 100%;
- margin: 0;
- margin-right: 5px;
- }
- }
-
- a:hover,
- a.active{
- background-color: #313A44;
- color: #fff;
- }
- }
}
.o_wslides_fs_container {
- display: flex;
- align-items: stretch;
height: calc(100% - 50px);
- position: relative;
- background-color: #313A44;
- }
-
- .o_wslides_fs_sidebar_hidden{
- transform: translateX(-100%);
+ background-color: $slides-bg-fullscreen;
}
- .o_wslides_fs_sidebar{
- width: 400px;
- min-width: 400px;
- background-color: #313A44;
- color: #fff;
+ .o_wslides_fs_sidebar {
transition: all .2s ease-in;
overflow: auto;
- a {
- text-decoration: none !important;
- color: inherit;
- }
-
- ul {
- margin: 0;
- padding: 0;
- list-style: none;
- }
-
- li {
- padding-left: 20px;
- }
-
.o_wslides_fs_sidebar_header {
- height: 100px;
- padding: 20px 0;
- display: flex;
- flex-direction: column;
-
- .o_wslides_slide_fs_progress_box{
- display: flex;
- align-items: center;
- font-size: .9rem;
- .o_wslides_fs_sidebar_progressbar{
- height: 5px;
- border-radius: 10px;
- width: 70%;
- background-color: rgba(0,0,0,.3);
- margin-right: 40px;
- overflow: hidden;
- }
- .progress-bar {
- background-color: $primary;
- height: 100%;
- width: 0;
- transition: all 1.5s ease-in;
- }
- span {
- color: #BABDC0;
- }
- }
- a {
- font-size: 1.5rem;
- font-weight: bold;
+ color: $slides-text;
+ .progress {
+ height: 5px;
}
}
+
.o_wslides_fs_sidebar_section {
- margin-bottom: 20px;
- background-color: #283038;
- padding: 10px 0;
- > a > li {
- color: #656B72;
- font-size: .8rem;
- text-transform: uppercase;
- font-weight: bold;
- padding: 5px 0 5px 15px;
+ background-color: $slides-bg-fullscreen-dark;
+ border: none;
+ border-radius: 0;
+
+ .card-body {
+ color: $slides-text;
}
}
- .o_wslides_fs_sidebar_section_slides{
- padding: 0;
- margin-bottom: 15px;
- li {
- margin: 0;
- padding: 0;
- border-left: 2px solid transparent;
- display: flex;
- a {
- flex: 1;
- }
- > span{
- width: 15px;
- padding: 0;
- margin: 0;
- margin-left: 10px;
- display: flex;
- flex-direction: column;
- align-items: center;
- i {
- z-index: 100;
- padding: 0;
- margin: 0;
- }
- i.o_wslides_slide_completed{
- color: #00A04A;
- border-radius: 50%;
- overflow: hidden;
- background-color: #fff;
- height: 12px;
- width: 12px;
- display: flex;
- justify-content: center;
- align-items: center;
- border: 1px solid #00A04A;
- }
- .o_wslides_fs_full_line{
- width: 1px;
- height: 100%;
- background-color: #787D82;
- }
- }
- }
- .o_wslides_top_line,
- .o_wslides_bottom_line{
- width: 1px;
- display: flex;
- flex-grow: 1;
- background-color: #787D82;
- }
- li:first-child .o_wslides_top_line{
- background-color: transparent;
- }
- li:last-child .o_wslides_bottom_line{
- background-color: transparent;
- }
+
+ .o_wslides_fs_sidebar_list{
li:hover,
li.active {
- border-left: 2px solid $primary;
cursor: pointer;
- background-color: #1D2228;
+ background-color: $slides-bg-fullscreen-darker;
}
- .o_wslides_fs_sidebar_slide_tab {
- padding:5px 0 5px 15px;
- font-size: .8rem;
- color: #8A8F93;
- font-weight: bold;
- flex: 1;
+ li {
+ color: $slides-text;
}
- .o_wslides_fs_sidebar_slide_tab.active{
+ li.active{
+ border-left: 2px solid $primary;
color: #fff;
}
- .o_wslides_fs_slide_link {
- color: #098586;
- font-size: .8rem;
- margin-left: 15px;
- flex: 1;
- i {
- margin-right: 10px;
- }
- }
- .o_wslides_fs_slide_quiz {
- font-size: .8rem;
- margin-left: 15px;
- color: #8A8F93;
- flex: 1;
- i {
- color: #D39B4B;
- margin-right: 10px;
- }
- }
}
-
-
}
- .o_wslides_fs_player {
- display: flex;
- align-items: stretch;
- max-width: calc(100% - 400px);
- height: 100%;
- background-color: #283038;
- transition: all .2s ease-in;
- position: absolute;
- top: 0;
- right: 0;
- height: 100%;
- width: 80%;
-
- iframe {
- height: 100%;
- width: 100%;
- }
+ .o_wslides_fs_content {
+ background-color: $slides-bg-fullscreen-dark;
}
- .o_wslides_fs_webpage {
- background-color: #fff;
- width: 100%;
- height: 100%;
- overflow-y: scroll;
- padding: 20px 0;
- }
-
- .o_wslides_fs_player_no_sidebar {
+ .o_wslides_fs_content_no_sidebar {
max-width: 100%;
width: 100%;
}
@@ -273,10 +72,8 @@
// quizz
.o_wslides_fs_quiz_container {
background-color: $gray-200;
- height: 100%;
overflow: scroll;
overflow-x: hidden;
- width: 100%;
.o_wslides_js_lesson_quiz_question {
li {
diff --git a/addons/website_slides/static/src/xml/slide_quiz.xml b/addons/website_slides/static/src/xml/slide_quiz.xml
index f512985a3af..0af500914c5 100644
--- a/addons/website_slides/static/src/xml/slide_quiz.xml
+++ b/addons/website_slides/static/src/xml/slide_quiz.xml
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="slide.slide.quiz">
- <div class="o_wslides_fs_quiz_container">
+ <div class="o_wslides_fs_quiz_container h-100 w-100">
<div class="mx-5">
<div t-foreach="widget.quiz.questions" t-as="question"
class="o_wslides_js_lesson_quiz_question mt-3" t-att-data-question-id="question.id" t-att-data-title="question.question">
diff --git a/addons/website_slides/static/src/xml/website_slides_fullscreen.xml b/addons/website_slides/static/src/xml/website_slides_fullscreen.xml
index b364f486420..683dc45752d 100644
--- a/addons/website_slides/static/src/xml/website_slides_fullscreen.xml
+++ b/addons/website_slides/static/src/xml/website_slides_fullscreen.xml
@@ -1,41 +1,36 @@
<templates id="template" xml:space="preserve">
- <t t-name="website.slides.fullscreen">
- <div t-if="slide.slide_type == 'video' &amp;&amp; !showMiniQuiz" class="player embed-responsive embed-responsive-16by9 embed-responsive-item">
- <iframe id="youtube-player" t-att-src="embed_url" allowFullScreen="true" frameborder="0" enablejsapi="1" autoplay="1" allow="autoplay"></iframe>
- </div>
- <div t-if="['presentation', 'document'].indexOf(slide.slide_type) !== -1 &amp;&amp; !showMiniQuiz" class="embed-responsive embed-responsive-4by3 embed-responsive-item ">
- <iframe t-att-src="embed_url" class="o_wslides_iframe_viewer" allowFullScreen="true" height="315" width="420" frameborder="0"></iframe>
- </div>
- <div t-if="slide.slide_type == 'infographic' &amp;&amp; !showMiniQuiz" style="width:100%;height:100%;text-align:center;">
- <img t-attf-src="/web/image/slide.slide/#{slide.id}/image" class="img-fluid" style="height: 100%" alt="Slide image"/>
- </div>
- <div class="o_wslides_fs_content"/>
- </t>
- <t t-name="website.course.player.infos">
- <div class="text-box">
- <h1><t t-esc="slide.name"/></h1>
- <a t-att-href="url" class="oe_js_show_comments">Show comments</a>
- </div>
- <div>
- <button class="oe_js_slide_full_screen btn btn-secondary">Full Screen</button>
- <button t-att-current_slide_id="slide.id" class="btn btn-primary oe_js_slide_completed">Mark as completed<i class="fa fa-check-circle ml-1"></i></button>
- </div>
+ <t t-name="website.slides.fullscreen.title">
+ <t t-if="!widget.get('slide').isQuiz">
+ <i t-if="widget.get('slide').type == 'document'" class="fa fa-file-pdf-o mr-2"></i>
+ <i t-if="widget.get('slide').type == 'infographic'" class="fa fa-file-picture-o mr-2"></i>
+ <i t-if="widget.get('slide').type == 'video'" class="fa fa-play-circle mr-2"></i>
+ <i t-if="widget.get('slide').type == 'webpage'" class="fa fa-file-text mr-2"></i>
+ <i t-if="widget.get('slide').type == 'quiz'" class="fa fa-question-circle mr-2 text"></i>
+ <span class="o_wslides_fs_slide_title_span" t-esc="widget.get('slide').name"/>
+ </t>
+ <t t-else="">
+ <span><i class="fa fa-flag-checkered"></i>Quiz: <span t-esc="widget.get('slide').name"/></span>
+ </t>
</t>
- <t t-name="website.course.fullscreen.title">
- <t t-if="!miniQuiz">
- <i t-if="slide.slide_type == 'document'" class="fa fa-file-pdf-o mr-2"></i>
- <i t-if="slide.slide_type == 'infographic'" class="fa fa-file-picture-o mr-2"></i>
- <i t-if="slide.slide_type == 'video'" class="fa fa-play-circle mr-2"></i>
- <i t-if="slide.slide_type == 'link'" class="fa fa-file-code-o mr-2"></i>
- <i t-if="slide.slide_type == 'webpage'" class="fa fa-file-text mr-2"></i>
- <i t-if="slide.slide_type == 'quiz'" class="fa fa-question-circle mr-2 text"></i>
- <span class="o_wslides_fs_slide_title_span" t-esc="slide.name"/>
+ <t t-name="website.slides.fullscreen.content">
+ <t t-if="_.contains(['document', 'presentation'], widget.get('slide').type)">
+ <div class="embed-responsive embed-responsive-4by3 embed-responsive-item h-100">
+ <iframe t-att-src="widget.get('slide').embedUrl" class="o_wslides_iframe_viewer h-100 w-100" allowFullScreen="true" frameborder="0"></iframe>
+ </div>
</t>
- <t t-else="">
- <span><i class="fa fa-flag-checkered"></i>Quiz: <span t-esc="slide.name"/></span>
+ <t t-if="widget.get('slide').type === 'infographic'">
+ <div class="w-100 h-100 text-center">
+ <img t-att-src="'/web/image/slide.slide/'+ widget.get('slide').id +'/image'" class="img-fluid" style="height: 100%" alt="Slide image"/>
+ </div>
</t>
</t>
+ <t t-name="website.slides.fullscreen.video">
+ <div class="player embed-responsive embed-responsive-16by9 embed-responsive-item h-100">
+ <iframe t-att-id="'youtube-player' + widget.slide.id" t-att-src="widget.slide.embedUrl" allowFullScreen="true" frameborder="0" enablejsapi="1" autoplay="1" allow="autoplay"></iframe>
+ </div>
+ </t>
+
</templates>