From: Vincent Vanwaelscappel Date: Fri, 18 Aug 2023 15:59:07 +0000 (+0200) Subject: wip #6182 @3 X-Git-Url: http://git.cubedesigners.com/?a=commitdiff_plain;h=02f46fe915da2379650b1f8ad3d60f2cc667b728;p=fluidbook-toolbox-quiz.git wip #6182 @3 --- 02f46fe915da2379650b1f8ad3d60f2cc667b728 diff --cc .editorconfig index 0000000,0000000..2436a50 new file mode 100644 --- /dev/null +++ b/.editorconfig @@@ -1,0 -1,0 +1,20 @@@ ++root = true ++ ++[*] ++charset = utf-8 ++end_of_line = lf ++insert_final_newline = true ++indent_style = space ++indent_size = 4 ++trim_trailing_whitespace = true ++ ++[*.sh] ++charset = utf-8 ++end_of_line = lf ++insert_final_newline = true ++ ++[*.md] ++trim_trailing_whitespace = false ++ ++[*.{yml,yaml}] ++indent_size = 2 diff --cc js/quiz.accessibility.js index 17a49fd,0000000..bf34596 mode 100644,000000..100644 --- a/js/quiz.accessibility.js +++ b/js/quiz.accessibility.js @@@ -1,28 -1,0 +1,28 @@@ +function QuizAccessibility(quiz) { + this.quiz = quiz; + this.initShortcuts(); +} + + +QuizAccessibility.prototype = { + initShortcuts: function () { + let $this = this; + $(document).on('keyup', function (e) { - console.log(e); ++ + let key = e.key.toLocaleUpperCase(); + if (key === ' ') { + key = 'Space'; + } + $('[aria-keyshortcuts="' + key + '"]').each(function () { - if ($this.quiz.utils.isVisible($(this))) { - console.log('click ',$(this)); ++ if ($this.quiz.utils.isEnabled($(this))) { ++ console.log('click ', $(this)); + $(this).get(0).click(); + return true; + } + }); + }); + }, +} + +export default QuizAccessibility; + diff --cc js/quiz.animations.js index bd83cba,e71b64a..97bc6b7 --- a/js/quiz.animations.js +++ b/js/quiz.animations.js @@@ -1,12 -1,12 +1,26 @@@ ++import gsap from "gsap"; ++ const lottie = require("lottie-web"); const $ = require("cash-dom"); -- function QuizAnimations(quiz) { this.quiz = quiz; ++ this.initEvents(); } QuizAnimations.prototype = { ++ ++ initEvents:function(){ ++ // Animate buttons on mouse down and up ++ $(document).on("mousedown", ".btn", function () { ++ gsap.to($(this), {scale: .95, duration: .2, ease: "back.inOut"}); ++ }).on("mouseup", ".btn", function () { ++ gsap.to($(this), {scale: 1, duration: .2}); ++ }); ++ }, ++ ++ ++ // Load the animation "name" in container load: function (name, container, replace) { let json = this.quiz.data.animations[name]; diff --cc js/quiz.js index fc6ae4f,10dafa2..3f74281 --- a/js/quiz.js +++ b/js/quiz.js @@@ -57,38 -61,91 +57,9 @@@ Quiz.prototype = }); this.quizResize(); - //animer le texte d'intro - let title = new SplitType("#welcome h2", { types: 'words, chars' }) - let text = new SplitType("#welcome p", { types: 'words, chars' }) - gsap.from(title.words, { - opacity: 0, - y: 20, - duration: 1, - stagger: 0.05, - onStart: () => { - $(title.elements).removeClass("none") - } - }) - gsap.to(text.words, { - opacity: 1, - y: 0, - duration: 1, - ease: "power1.inOut", - stagger: { - amount: 0.2 - }, - onStart: () => { - $(text.elements).removeClass("none") - } - }) - - // Préparer les réponses du joueur dans l'objet this.responses - // à chaque fois que le formulaire change de valeur - $(document).on("change", ".active-screen form", function(e) { - $this.responses = [] - $(this).find("input:checked").each(function() { - $this.responses.push(parseInt($(this).val())) - }); - }) - - // Passer à la page suivante - // Valider les réponses - $(document).on("click", ".next .action", function () { - let responses = $this.responses - $this.next(responses); - $this.responses = [] - }) - key('space', function (e){ - e.preventDefault() - $this.next(); - }) - - // Réinitialiser les réponses - $(document).on("click", ".btn.reset", function() { - $this.resetForm() - }) - key('r', function (e){ - e.preventDefault() - $this.resetForm() - }) - - if(key.isPressed('R')) { - alert('r') - } - - $(document).on("mousedown", ".btn", function() { - gsap.to($(this), { scale: .95, duration: .2, ease: "back.inOut" }); - }).on("mouseup", ".btn", function() { - gsap.to($(this), { scale: 1, duration: .2 }); - }) - - // - $(document).on("click", ".toggle-answers-review", function() { - $(".score-answers-review_container").toggleClass("active") - }) - - // - this.activeNav() - }, - - start: function() { - const $this = this - gsap.timeline().to("#welcome-screen", { - opacity: 0, - onComplete: function() { - let responses = $this.responses - $this.next(responses); - $("#welcome-screen").removeClass("next active-screen").addClass("none").next(".container-screen").removeClass("none").addClass("next active-screen") - $this.activeNav(1) - } - }) + // Afficher l'écran d'introduction et lancer l'animation + this.screens.intro.show(); - - // Préparer les réponses du joueur dans l'objet this.responses - // à chaque fois que le formulaire change de valeur - $(document).on("change", ".active-screen form", function (e) { - $this.responses = [] - $(this).find("input:checked").each(function () { - $this.responses.push(parseInt($(this).val())) - }); - }) - - // Passer à la page suivante - // Valider les réponses - $(document).on("click", ".next .action", function () { - let responses = $this.responses - $this.next(responses); - $this.responses = [] - }) - - // Réinitialiser les réponses - $(document).on("click", ".btn.reset", function () { - $this.resetForm() - }) - - $(document).on("mousedown", ".btn", function () { - gsap.to($(this), {scale: .95, duration: .2, ease: "back.inOut"}); - }).on("mouseup", ".btn", function () { - gsap.to($(this), {scale: 1, duration: .2}); - }) - + this.progressbar.update(); }, updateIcons: function () { diff --cc js/quiz.question.js index 8c90f4d,f659d07..97c7c0d --- a/js/quiz.question.js +++ b/js/quiz.question.js @@@ -1,35 -1,35 +1,47 @@@ --var QuizQuestion = function(quiz) { ++var QuizQuestion = function (quiz) { this.quiz = quiz; this.init(); } QuizQuestion.prototype = { -- init: function(){ ++ init: function () { }, -- getAll: function() { -- return this.quiz.data.questions ++ getAll: function () { ++ return this.quiz.data.questions }, -- byPosition: function(pos) { ++ byPosition: function (pos) { return this.quiz.data.questions[parseInt(pos)] }, -- currentPosition: function() { -- return $(document).find(".active-screen").data("position"); ++ /** ++ * ++ * @returns {number} ++ */ ++ currentPosition: function () { ++ let screenName = this.quiz.screens.getActiveScreen().data('screen'); ++ if (screenName.indexOf('q-') === 0) { ++ return parseInt(screenName.substring(2)); ++ } ++ return -1; }, -- current: function() { ++ current: function () { let currentPosition = this.currentPosition() return this.byPosition(currentPosition) }, -- last: function(position) { -- return this.quiz.data.questions.length === parseInt(position) ++ last: function (position) { ++ return this.count() === parseInt(position) }, -- getFormData: function(responses) { ++ count: function () { ++ return this.quiz.data.questions.length; ++ }, ++ ++ getFormData: function (responses) { // }, } diff --cc js/quiz.screen.intro.js index 195be85,0000000..b468f2d mode 100644,000000..100644 --- a/js/quiz.screen.intro.js +++ b/js/quiz.screen.intro.js @@@ -1,39 -1,0 +1,38 @@@ +import gsap from "gsap"; +import SplitType from 'split-type'; + +function QuizScreenIntro(screens) { + this.quiz = screens.quiz; + this.screens = screens; +} + +QuizScreenIntro.prototype = { + show: function () { + let $this = this; + + this.screens.showScreen('welcome', function () { + $this.animate(); + $("#start").on("click", function () { - $this.screens.showScreen('q0'); ++ $this.screens.showScreen('q-0'); + return false; + }); + }); + }, + + animate: function () { - console.log('animate'); + //animer le texte d'intro + let title = new SplitType("#welcome h2", {types: 'words, chars'}) + let text = new SplitType("#welcome p", {types: 'words, chars'}) + gsap.from(title.words, { + opacity: 0, y: 20, duration: 1, stagger: 0.05 + }) + gsap.to(text.words, { + opacity: 1, y: 0, duration: 1, ease: "power1.inOut", stagger: { + amount: 0.2 + } + }) + }, + +} + - export default QuizScreenIntro; ++export default QuizScreenIntro; diff --cc js/quiz.screens.js index 6daca92,0000000..72f5955 mode 100644,000000..100644 --- a/js/quiz.screens.js +++ b/js/quiz.screens.js @@@ -1,152 -1,0 +1,253 @@@ +import gsap from "gsap"; + +import QuizScreenIntro from './quiz.screen.intro'; +import QuizScreenOutro from "./quiz.screen.outro"; + +function QuizScreens(quiz) { + this.quiz = quiz; ++ this.activeScreen = null; + this.intro = new QuizScreenIntro(this); + this.outro = new QuizScreenOutro(this); ++ this.initEvents(); +} + +QuizScreens.prototype = { ++ ++ initEvents: function () { ++ const $this = this; ++ // Réinitialiser les réponses ++ $(document).on("click", ".btn.reset", function () { ++ $this.resetForm(); ++ }); ++ ++ // Préparer les réponses du joueur dans l'objet this.responses ++ // à chaque fois que le formulaire change de valeur ++ $(document).on("change", ".active-screen form:not(.disabled)", function (e) { ++ $this.updateUserAnswers(); ++ }); ++ ++ // Cliquer sur le bouton suivant ++ $(document).on("click", ".next .action", function () { ++ if ($(this).hasClass('validate')) { ++ let review = $this.sendUserAnswers(); ++ // Si la revue instantanée est activée, on affiche les résultats ++ if (this.quiz.data.instantReview) { ++ $this.instantReview(review); ++ } else { ++ // Sinon, on passe directement à la question suivante ++ $this.nextQuestion(); ++ } ++ } else { ++ // Bouton continuer, on était dans la revue instantanée, on passe à la question suivante ++ $this.nextQuestion(); ++ } ++ }); ++ }, ++ ++ instantReview: function (review) { ++ let form = this.getCurrentForm(); ++ let activeScreen = this.getActiveScreen(); ++ // Disable form, we don't want the user be able to click on items ++ $(form).addClass('disabled'); ++ ++ // Remove validate button and show continue button ++ $(activeScreen).find('.btn.validate').addClass('none'); ++ $(activeScreen).find('.btn.continue').removeClass('none'); ++ ++ for (let k in results) { ++ let answerResult = results[k]; ++ ++ let n = (parseInt(k) + 1); ++ let icon = getSpriteIcon("quiz-ok"); ++ let $el = form.fid(".list-item:nth-of-type(" + n + ") label"); ++ $el.addClass(answerResult); ++ if (answerResult === "nok") { ++ icon = getSpriteIcon("quiz-wrong"); ++ } ++ if (answerResult !== "neutral") { ++ $el.find(".access").addClass(answerResult).html(icon); ++ } else { ++ $el.find(".access").remove(); ++ } ++ } ++ }, ++ ++ nextQuestion: function () { ++ let nextQuestionIndex = this.quiz.question.currentPosition() + 1; ++ let nextScreen; ++ if (nextQuestionIndex >= this.quiz.question.count()) { ++ nextScreen = 'review'; ++ } else { ++ nextScreen = 'q-' + nextQuestionIndex; ++ } ++ this.showScreen(nextScreen); ++ }, ++ ++ sendUserAnswers: function () { ++ return this.quiz.score.setAnswer(this.quiz.question.currentPosition(), this.currentQuestionAnswers) ++ }, ++ ++ updateUserAnswers: function () { ++ const $this = this; ++ this.currentQuestionAnswers = [] ++ $(".active-screen form").find("input:checked").each(function () { ++ $this.currentQuestionAnswers.push(parseInt($(this).val())); ++ }); ++ }, ++ + /** + * - * @param screen ++ * @param screen string ++ * @param callback function + */ + showScreen: function (screen, callback) { + const $this = this; + let screenToShow = $('[data-screen="' + screen + '"]'); + this.hideCurrentScreen(function () { + screenToShow.removeClass("none").addClass("next active-screen"); - callback(); ++ if (callback !== undefined) { ++ callback(); ++ } ++ $this.activeScreen = screenToShow; + }); + }, + + next: function (responses) { + // on arrête l'animation si le joueur passe à la question suivante + this.stopAnimationValidation() + + let status = quiz.score.questionStatus + let currentPosition = quiz.score.lastAnsweredQuestion + 1 + + const $el = $(".active-screen .btn.action") + + this.quiz.score.updateScore() + // if form exist and responses are validated + // dont miss to add this second condition + if ($(".active-screen form").length > 0) { + if (status.length > 0) { - if (status[this.question.currentPosition()].ok === "not answered") { ++ if (status[this.quiz.question.currentPosition()].ok === "not answered") { + this.validateResponse(responses); + this.updateBtnValidation("validated") + return false + } + } + } + + $el.parents(".container-screen").addClass("none").removeClass("next active-screen").next(".container-screen").removeClass("none").addClass("next active-screen") + this.resetForm() + // on incrémente de 1 la position actuelle de la question (qui commence à l'index zero) + // pour récupérer le premier enfant de la nav + this.quiz.progressbar.update((parseInt(this.quiz.question.currentPosition()) + 1)) + this.updateBtnValidation() + // si c'est la dernière question a été validée alors on affiche le résultat au prochain screen + if (this.quiz.question.last(currentPosition)) { + this.outro.show(); + } + }, + ++ getCurrentForm: function () { ++ return $(this.activeScreen).find('form'); ++ }, + + resetForm: () => { - $("form").find("input").prop("checked", false) ++ this.getCurrentForm().find("input").prop("checked", false) + }, + + updateBtnValidation: function (status) { + let $btnAction = $(".footer-question .action"), validationText = $btnAction.data("validation-text"), + continueText = $btnAction.data("continue-text") + + $btnAction.find('.text').text(status === "validated" ? continueText : validationText) + }, + + animationValidation: function (status) { + let selector = $("#anim") + let text = status === "NOK" ? "Not quite" : "Perfect" + let $this = this + this.quiz.animations.load(status, selector, {'\\$text': text}); + selector.addClass("active") + this.timeoutAnimation = setTimeout(function (e) { + $this.stopAnimationValidation() + }, 10000) + }, + + stopAnimationValidation: function () { + $("#anim").removeClass("active").empty() + if (this.timeoutAnimation) { + clearTimeout(this.timeoutAnimation); + } + }, + ++ getActiveScreen() { ++ return this.activeScreen; ++ }, ++ + + validateResponse: function (responses) { - const form = $(".active-screen form") - const activeScreen = $(".active-screen") - const position = activeScreen.data("position") ++ const form = this.getCurrentForm(); ++ const questionIndex = this.quiz.question.currentPosition(); + + if (form) { + if (form.length > 0) { - let validated = quiz.score.setAnswer(position, responses); ++ let validated = quiz.score.setAnswer(questionIndex, responses); + if (validated.ok === "ok") { + this.animationValidation("OK") + } else { + this.animationValidation("NOK") + } - this.resultAfterValidation(validated.status[position].answers) ++ this.resultAfterValidation(validated.status[questionIndex].answers) + } + return false + } + }, + + - resultAfterValidation: function (datas) { - for (let k in datas) { ++ resultAfterValidation: function (results) { ++ for (let k in results) { ++ let answerResult = results[k]; ++ + let n = (parseInt(k) + 1) + let icon = getSpriteIcon("quiz-ok") + let $el = $(".active-screen .question-multiple .list-item:nth-of-type(" + n + ") label") - $el.addClass(datas[k]) - if (datas[k] === "nok") { ++ $el.addClass(answerResult) ++ if (answerResult === "nok") { + icon = getSpriteIcon("quiz-wrong") + } - if (datas[k] !== "neutral") { - $el.find(".access").addClass(datas[k]).html(icon) ++ if (answerResult !== "neutral") { ++ $el.find(".access").addClass(answerResult).html(icon) ++ } else { ++ $el.find(".access").remove(); + } + } + }, + + + /** + * Si un écran est affiché, on le masque puis on exécute le callback. Sinon, on exécute immédiatement le callback + * @param callback + */ + hideCurrentScreen: function (callback) { + let cb = function (screen) { + if (screen.length > 0) { + $(screen).removeClass("next active-screen").addClass("none"); + } - callback(); ++ if (callback !== undefined) { ++ callback(); ++ } + }; + + let currentScreen = $('.container-screen:not(.none)'); + if (currentScreen.length > 0) { + gsap.timeline().to(currentScreen, { + autoAlpha: 0, onComplete: function () { + cb(currentScreen); + } + }) + } else { + cb(currentScreen); + } + }, +}; + - export default QuizScreens; ++export default QuizScreens; diff --cc js/quiz.utils.js index 4e28901,0000000..a0abb1e mode 100644,000000..100644 --- a/js/quiz.utils.js +++ b/js/quiz.utils.js @@@ -1,15 -1,0 +1,27 @@@ +function QuizUtils() { + +} + +QuizUtils.prototype = { + isVisible(e) { + if ($(e).length == 0) { + return false; + } + let elt = $(e).get(0); + return elt.offsetWidth || elt.offsetHeight || elt.getClientRects().length; ++ }, ++ isEnabled: function (e) { ++ if (!this.isVisible(e)) { ++ return false; ++ } ++ if ($(e).css('pointer-events') === 'none') { ++ return false; ++ } ++ if ($(e).hasClass('disabled') || $(e).closest('.disabled').length > 0) { ++ return false; ++ } ++ return true; + } +} + +export default QuizUtils; diff --cc style/103-question-multiple.sass index fed8e2e,7dc8c3a..8f61c6c --- a/style/103-question-multiple.sass +++ b/style/103-question-multiple.sass @@@ -1,21 -1,29 +1,29 @@@ .screen.question-multiple - top: 45px - +above(992px) - top: 93px + top: 93px ++ ++ form ++ &.disabled ++ label ++ pointer-events: none ++ .list display: grid + grid-template-columns: repeat(2, 1fr) grid-gap: 16px - +below($screenSizeMobile) - grid-gap: 8px - +breakpoint(md) - grid-template-columns: repeat(2, 1fr) ++ &-item label width: 100% height: 58px @extend .radius -- +opacity(.80,background-color,$neutral-color) -- +flex-config(space-between,false,false,center) ++ +opacity(.80, background-color, $neutral-color) ++ +flex-config(space-between, false, false, center) padding: 0 16px cursor: pointer border: 2px solid rgba($texts-color, .24) position: relative overflow: hidden - +below($screenSizeMobile) - height: 56px - +font-size(14) ++ &:after content: "" width: 100% @@@ -24,17 -32,17 +32,21 @@@ top: 0 left: 0 transition: background-color .1s ease-out ++ &.ok:after, &.missed:after // -- +opacity(.24,background-color,$ok-color) ++ +opacity(.24, background-color, $ok-color) ++ * z-index: 1 -- input:checked+label ++ input:checked + label border-color: $texts-color transition: border-color .1s ease-out ++ &:after +opacity(.16) ++ &.nok:after background: transparent diff --cc views/footer.blade.php index 6aee4f1,bcadcba..dc75cc1 --- a/views/footer.blade.php +++ b/views/footer.blade.php @@@ -5,16 -5,15 +5,21 @@@ R @endisset - - -- {{$text}} ++ ++ Validate answer space @isset($time) @endisset ++ @if($data['instantReview']) ++ ++ Continue ++ space ++ ++ @endif @isset($info) - + More infos F1 diff --cc views/header_question.blade.php index dda7f1f,dda7f1f..5909879 --- a/views/header_question.blade.php +++ b/views/header_question.blade.php @@@ -5,7 -5,7 +5,7 @@@ @endfor --

{{$data['question']}}

++

{{$question['question']}}

@if($data['type'] === "draganddrop")

Use arrow keys to move the cards to the corresponding zone

@endif diff --cc views/index.blade.php index 1a501d7,88b61a0..c4f77a5 --- a/views/index.blade.php +++ b/views/index.blade.php @@@ -21,7 -21,7 +21,7 @@@ @include('screens.intro', ['data'=> $data]) @endif @foreach($data->questions as $key => $question) -- @include('screens.question_'.$question['type'], ['theme' => $data->theme, 'data'=> $question, 'max' => $totalQuestion, 'position' => $key, 'alphabet' => $alphabet]) ++ @include('screens.question_'.$question['type'], ['theme' => $data->theme,'data'=>$data, 'question'=> $question, 'max' => $totalQuestion, 'position' => $key, 'alphabet' => $alphabet]) @endforeach @include('screens.outro', ['data'=> $data])
diff --cc views/screens/question_draganddrop.blade.php index a0e6549,469b4bc..7d1645f --- a/views/screens/question_draganddrop.blade.php +++ b/views/screens/question_draganddrop.blade.php @@@ -1,4 -1,4 +1,4 @@@ -
-
++
diff --cc views/screens/question_multiple.blade.php index f133911,34f66cc..958ff39 --- a/views/screens/question_multiple.blade.php +++ b/views/screens/question_multiple.blade.php @@@ -1,12 -1,12 +1,12 @@@ -
-
-- @include('header_question', ['data' => $data, 'max' => $max, 'position' => $position]) ++
++ @include('header_question', ['question' =>$question, 'max' => $max, 'position' => $position])
    -- @foreach($data['answers'] as $key => $answer) ++ @foreach($question['answers'] as $key => $answer)
  • -
- - +
-- @include('footer', ['data' => $data, 'reset' => true, 'text' => 'Validate answer', 'info' => true]) ++ @include('footer', ['question' =>$question, 'data' => $data, 'reset' => true, 'info' => true])