--- /dev/null
--- /dev/null
++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
--- /dev/null
- console.log(e);
+function QuizAccessibility(quiz) {
+ this.quiz = quiz;
+ this.initShortcuts();
+}
+
+
+QuizAccessibility.prototype = {
+ initShortcuts: function () {
+ let $this = this;
+ $(document).on('keyup', function (e) {
- if ($this.quiz.utils.isVisible($(this))) {
- console.log('click ',$(this));
++
+ let key = e.key.toLocaleUpperCase();
+ if (key === ' ') {
+ key = 'Space';
+ }
+ $('[aria-keyshortcuts="' + key + '"]').each(function () {
++ if ($this.quiz.utils.isEnabled($(this))) {
++ console.log('click ', $(this));
+ $(this).get(0).click();
+ return true;
+ }
+ });
+ });
+ },
+}
+
+export default QuizAccessibility;
+
++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];
});
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 () {
--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) {
//
},
}
--- /dev/null
- $this.screens.showScreen('q0');
+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 () {
- console.log('animate');
++ $this.screens.showScreen('q-0');
+ return false;
+ });
+ });
+ },
+
+ animate: function () {
- export default QuizScreenIntro;
+ //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;
--- /dev/null
- * @param screen
+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()));
++ });
++ },
++
+ /**
+ *
- callback();
++ * @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");
- if (status[this.question.currentPosition()].ok === "not answered") {
++ 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) {
- $("form").find("input").prop("checked", false)
++ 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: () => {
- const form = $(".active-screen form")
- const activeScreen = $(".active-screen")
- const position = activeScreen.data("position")
++ 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) {
- let validated = quiz.score.setAnswer(position, responses);
++ const form = this.getCurrentForm();
++ const questionIndex = this.quiz.question.currentPosition();
+
+ if (form) {
+ if (form.length > 0) {
- this.resultAfterValidation(validated.status[position].answers)
++ let validated = quiz.score.setAnswer(questionIndex, responses);
+ if (validated.ok === "ok") {
+ this.animationValidation("OK")
+ } else {
+ this.animationValidation("NOK")
+ }
- resultAfterValidation: function (datas) {
- for (let k in datas) {
++ this.resultAfterValidation(validated.status[questionIndex].answers)
+ }
+ return false
+ }
+ },
+
+
- $el.addClass(datas[k])
- if (datas[k] === "nok") {
++ 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")
- if (datas[k] !== "neutral") {
- $el.find(".access").addClass(datas[k]).html(icon)
++ $el.addClass(answerResult)
++ if (answerResult === "nok") {
+ icon = getSpriteIcon("quiz-wrong")
+ }
- callback();
++ 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");
+ }
- export default QuizScreens;
++ 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;
--- /dev/null
+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;
.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%
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
<span class="access">R</span>
</a>
@endisset
- <a class="btn primary action" data-validation-text="Validate answer" data-continue-text="Continue"
- aria-keyshortcuts="Space">
- <a class="btn primary action" data-validation-text="Validate answer" data-continue-text="Continue">
-- <span class="text">{{$text}}</span>
++ <a class="btn primary action validate" aria-keyshortcuts="Space">
++ <span class="text">Validate answer</span>
<span class="access space">space</span>
@isset($time)
<span data-icon="running-man"></span>
@endisset
</a>
++ @if($data['instantReview'])
++ <a class="btn primary action none continue" aria-keyshortcuts="Space">
++ <span class="text">Continue</span>
++ <span class="access space">space</span>
++ </a>
++ @endif
@isset($info)
- <a class="btn secondary none">
+ <a class="btn secondary none" aria-keyshortcuts="F1">
More infos
<span class="access infos">F1</span>
</a>
<span class="progress-item"></span>
@endfor
</div>
-- <h1 id="titleQuestion">{{$data['question']}}</h1>
++ <h1 id="titleQuestion">{{$question['question']}}</h1>
@if($data['type'] === "draganddrop")
<h2 class="subtitle">Use arrow keys to move the cards to the corresponding zone</h2>
@endif
@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])
<div id="anim"></div>
- <div class="container-screen none question-draganddrop" data-screen="q{{$position}}">
-<div class="container-screen none question-draganddrop" data-position="{{$position}}">
++<div class="container-screen none question-draganddrop" data-screen="q-{{$position}}">
<div class="screen-image zone-1">
<img src="{{$theme->draganddropArea1Image}}" />
<div class="zone-content">
- <div class="container-screen none" data-screen="q{{$position}}">
-<div class="container-screen none" data-position="{{$position}}">
-- @include('header_question', ['data' => $data, 'max' => $max, 'position' => $position])
++<div class="container-screen none" data-screen="q-{{$position}}">
++ @include('header_question', ['question' =>$question, 'max' => $max, 'position' => $position])
<div class="screen question-multiple">
<form id="form-{{$position}}">
<ul class="list">
-- @foreach($data['answers'] as $key => $answer)
++ @foreach($question['answers'] as $key => $answer)
<li class="list-item">
- <input type="{{ $data['type'] === "multiple" ? 'checkbox' : 'radio' }}"
- name="{{ $data['type'] === "multiple" ? 'answer-'.$position.$key : 'answer' }}"
+ <input type="{{ $data['multiple'] ? 'checkbox' : 'radio' }}"
+ name="{{ 'answer' }}"
id="question-{{$position.$key}}" class="none" value="{{$key}}">
<label for="question-{{$position.$key}}" aria-keyshortcuts="{{$alphabet[$key]}}">
<span class="relative">{{$answer['answer']}}</span>
</form>
</div>
<div class="screen-image">
- <img src="{{$theme->standardImage}}"/>
- <img class="mobile question-multiple" src="{{$theme->standardImageMobile}}"/>
+ <img src="{{$theme->standardImage}}">
</div>
-- @include('footer', ['data' => $data, 'reset' => true, 'text' => 'Validate answer', 'info' => true])
++ @include('footer', ['question' =>$question, 'data' => $data, 'reset' => true, 'info' => true])
</div>