Skip to content

Commit

Permalink
Signal timeout warning visually
Browse files Browse the repository at this point in the history
  • Loading branch information
otacke committed Jul 15, 2023
1 parent 5c660e8 commit 2dd74d8
Show file tree
Hide file tree
Showing 9 changed files with 191 additions and 61 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Util from '@services/util';
import FocusTrap from '@services/focus-trap';
import Util from '@services/util';
import TimerDisplay from './timer-display';
import './exercise-screen.scss';

/** Class representing an exercise screen */
Expand Down Expand Up @@ -67,10 +68,8 @@ export default class ExerciseScreen {
this.headlineText.classList.add('h5p-game-map-exercise-headline-text');
headline.append(this.headlineText);

this.headlineTimer = document.createElement('div');
this.headlineTimer.classList.add('h5p-game-map-exercise-headline-timer');
this.headlineTimer.classList.add('display-none');
headline.append(this.headlineTimer);
this.timerDisplay = new TimerDisplay();
headline.append(this.timerDisplay.getDOM());

// H5P instance
this.h5pInstance = document.createElement('div');
Expand Down Expand Up @@ -100,10 +99,12 @@ export default class ExerciseScreen {
show(params = {}) {
this.dom.classList.remove('display-none');

this.headlineTimer.classList.toggle(
'display-none',
params.isShowingSolutions || (this.headlineTimer.innerText ?? '') === ''
);
if (params.isShowingSolutions) {
this.timerDisplay.hide();
}
else {
this.timerDisplay.show();
}

// Wait to allow DOM to progress
window.requestAnimationFrame(() => {
Expand Down Expand Up @@ -203,28 +204,12 @@ export default class ExerciseScreen {
/**
* Set time.
* @param {number} timeMs Time to display on timer.
* @param {object} [options] Options.
* @param {boolean} [options.timeoutWarning] If true, timeout warning state.
*/
setTime(timeMs) {
if (timeMs === null || timeMs === '') {
this.headlineTimer.innerText = '';
this.headlineTimer.classList.add('display-none');
return;
}

if (typeof timeMs !== 'number') {
return;
}

const date = new Date(0);
date.setSeconds(Math.round(Math.max(0, timeMs / 1000)));

this.headlineTimer.innerText = date
.toISOString()
.split('T')[1]
.split('.')[0]
.replace(/^[0:]+/, '') || '0';

this.headlineTimer.classList.remove('display-none');
setTime(timeMs, options = {}) {
this.timerDisplay.setTime(timeMs);
this.timerDisplay.setTimeoutWarning(options.timeoutWarning);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,22 +119,6 @@
text-overflow: ellipsis;
white-space: nowrap;
}

.h5p-game-map-exercise-headline-timer {
font-size: 1.25rem;
font-weight: bold;
justify-self: flex-end;

&::before {
content: "\f017";
font-family: "H5PFontAwesome4", sans-serif;
margin-right: 0.5rem;
}

&.display-none {
display: none;
}
}
}

.h5p-game-map-exercise-instance-container {
Expand Down
105 changes: 105 additions & 0 deletions src/scripts/components/exercise-screen/timer-display.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import './timer-display.scss';

/** Class representing a timer on screen */
export default class TimerDisplay {
/**
* @class
*/
constructor() {
this.handleNotifyingEnded = this.handleNotifyingEnded.bind(this);

this.dom = document.createElement('div');
this.dom.classList.add('h5p-game-map-exercise-headline-timer');

this.hide();
}

/**
* Get DOM.
* @returns {HTMLElement} DOM.
*/
getDOM() {
return this.dom;
}

/**
* Show.
*/
show() {
if ((this.dom.innerText || '') === '') {
return;
}

this.dom.classList.remove('display-none');
}

/**
* Hide.
*/
hide() {
this.dom.classList.add('display-none');

this.handleNotifyingEnded();
}

/**
* Set time.
* @param {number} timeMs Time in milliseconds.
*/
setTime(timeMs) {
if (timeMs === null || timeMs === '') {
this.dom.innerText = '';
this.hide();
return;
}

if (typeof timeMs !== 'number') {
return;
}

const date = new Date(0);
date.setSeconds(Math.round(Math.max(0, timeMs / 1000)));

this.dom.innerText = date
.toISOString()
.split('T')[1]
.split('.')[0]
.replace(/^[0:]+/, '') || '0';

this.show();
}

/**
* Set timeout warning.
* @param {boolean} state If true, set warning state. Else hide.
*/
setTimeoutWarning(state) {
if (!this.isTimeoutwarning && state) {
this.notify(); // Only notify if not yet notified.
}
this.isTimeoutwarning = state;

this.dom.classList.toggle('timeout-warning', state);
}

/**
* Notify about timeout warning.
*/
notify() {
this.dom.addEventListener(
'animationend', this.handleNotifyingEnded
);

this.dom.classList.add('notify-animation');
}

/**
* Handle notification ended.
*/
handleNotifyingEnded() {
this.dom.removeEventListener(
'animationend', this.handleNotifyingEnded
);
this.dom.classList.remove('notify-animation');
}
}
39 changes: 39 additions & 0 deletions src/scripts/components/exercise-screen/timer-display.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
.h5p-game-map-exercise-headline-timer {
font-size: 1.25rem;
font-weight: bold;
justify-self: flex-end;

&.timeout-warning {
color: #bb0000;
}

&::before {
content: "\f017";
font-family: "H5PFontAwesome4", sans-serif;
margin-right: 0.5rem;
}

&.display-none {
display: none;
}

&.notify-animation {
animation: pulse;
animation-duration: 1s;
animation-timing-function: ease-in-out;
}

@keyframes pulse {
0% {
transform: scale3d(1, 1, 1);
}

50% {
transform: scale3d(1.25, 1.25, 1.25);
}

100% {
transform: scale3d(1, 1, 1);
}
}
}
6 changes: 4 additions & 2 deletions src/scripts/components/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -383,13 +383,15 @@ export default class Main {
* Handle timer ticked.
* @param {number} id Id of exercise that had a timer tick.
* @param {number} remainingTime Remaining time in ms.
* @param {object} [options] Options.
* @param {boolean} [options.timeoutWarning] If true, timeout warning state.
*/
handleTimerTicked(id, remainingTime) {
handleTimerTicked(id, remainingTime, options = {}) {
if (!id || id !== this.openExerciseId) {
return;
}

this.exerciseScreen.setTime(remainingTime);
this.exerciseScreen.setTime(remainingTime, options);
}

/**
Expand Down
6 changes: 4 additions & 2 deletions src/scripts/components/mixins/main-handlers-exercise.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,11 @@ export default class MainHandlersExercise {
* Handle exercise timer ticked.
* @param {number} id Id of exercise that had a timer tick.
* @param {number} remainingTime Remaining time in ms.
* @param {object} [options] Options.
* @param {boolean} [options.timeoutWarning] If true, timeout warning state.
*/
handleExerciseTimerTicked(id, remainingTime) {
this.handleTimerTicked(id, remainingTime);
handleExerciseTimerTicked(id, remainingTime, options) {
this.handleTimerTicked(id, remainingTime, options);
}

/**
Expand Down
6 changes: 3 additions & 3 deletions src/scripts/components/mixins/main-initialization.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import EndScreen from '@components/media-screen/end-screen';
import Map from '@components/map/map';
import Toolbar from '@components/toolbar/toolbar';
import Exercises from '@models/exercises';
import ExerciseScreen from '@components/exercise/exercise-screen';
import ExerciseScreen from '@components/exercise-screen/exercise-screen';
import ConfirmationDialog from '@components/confirmation-dialog/confirmation-dialog';

/**
Expand Down Expand Up @@ -301,8 +301,8 @@ export default class MainInitialization {
onScoreChanged: (id, scoreParams) => {
this.handleExerciseScoreChanged(id, scoreParams);
},
onTimerTicked: (id, remainingTime) => {
this.handleExerciseTimerTicked(id, remainingTime);
onTimerTicked: (id, remainingTime, options) => {
this.handleExerciseTimerTicked(id, remainingTime, options);
},
onTimeoutWarning: (id) => {
this.handleExerciseTimeoutWarning(id);
Expand Down
25 changes: 19 additions & 6 deletions src/scripts/models/exercise.js
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,17 @@ export default class Exercise {
return this.remainingTime;
}

/**
* Determine whether exercise is in timeout warning state.
* @returns {boolean} True, if exercise is in timeout warning state.
*/
isTimeoutWarning() {
return (
typeof this.params.time.timeoutWarning === 'number' &&
this.remainingTime <= this.params.time?.timeoutWarning * 1000
);
}

/**
* Make it easy to bubble events from child to parent.
* @param {object} origin Origin of event.
Expand Down Expand Up @@ -448,9 +459,14 @@ export default class Exercise {
},
onTick: () => {
this.remainingTime = this.timer.getTime();
this.callbacks.onTimerTicked(this.remainingTime);
const isTimeoutWarning = this.isTimeoutWarning();

this.callbacks.onTimerTicked(
this.remainingTime,
{ timeoutWarning: isTimeoutWarning }
);

if (this.params.time.timeoutWarning) {
if (!this.hasPlayedTimeoutWarning && isTimeoutWarning) {
this.handleTimeoutWarning();
}
}
Expand Down Expand Up @@ -497,10 +513,7 @@ export default class Exercise {
* Handle timeout warning.
*/
handleTimeoutWarning() {
if (
this.remainingTime <= this.params.time?.timeoutWarning * 1000 &&
!this.hasPlayedTimeoutWarning
) {
if (!this.hasPlayedTimeoutWarning && this.isTimeoutWarning()) {
this.hasPlayedTimeoutWarning = true;
this.callbacks.onTimeoutWarning();
}
Expand Down
4 changes: 2 additions & 2 deletions src/scripts/models/exercises.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@ export default class Exercises {
onScoreChanged: (scoreParams) => {
this.callbacks.onScoreChanged(element.id, scoreParams);
},
onTimerTicked: (remainingTime) => {
this.callbacks.onTimerTicked(element.id, remainingTime);
onTimerTicked: (remainingTime, options) => {
this.callbacks.onTimerTicked(element.id, remainingTime, options);
},
onTimeoutWarning: () => {
this.callbacks.onTimeoutWarning(element.id);
Expand Down

0 comments on commit 2dd74d8

Please sign in to comment.