2015-10-19 11:17:38 +02:00
|
|
|
(function (MemoryGame, EventDispatcher, $) {
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Controls all the operations for each card.
|
|
|
|
*
|
|
|
|
* @class H5P.MemoryGame.Card
|
2017-02-13 14:42:34 +01:00
|
|
|
* @extends H5P.EventDispatcher
|
2017-01-20 11:58:08 +01:00
|
|
|
* @param {Object} image
|
|
|
|
* @param {number} id
|
2017-06-20 16:47:07 +02:00
|
|
|
* @param {string} alt
|
2017-09-25 14:38:14 +02:00
|
|
|
* @param {Object} l10n Localization
|
2017-01-20 11:58:08 +01:00
|
|
|
* @param {string} [description]
|
2017-05-04 14:20:20 +02:00
|
|
|
* @param {Object} [styles]
|
2015-10-19 11:17:38 +02:00
|
|
|
*/
|
2018-08-23 01:44:41 +02:00
|
|
|
MemoryGame.Card = function (image, id, alt, l10n, description, styles, audio) {
|
2017-02-13 14:42:34 +01:00
|
|
|
/** @alias H5P.MemoryGame.Card# */
|
2015-10-19 11:17:38 +02:00
|
|
|
var self = this;
|
|
|
|
|
|
|
|
// Initialize event inheritance
|
|
|
|
EventDispatcher.call(self);
|
|
|
|
|
2018-08-23 01:44:41 +02:00
|
|
|
var path, width, height, $card, $wrapper, removedState, flippedState, audioPlayer;
|
2015-10-19 11:17:38 +02:00
|
|
|
|
2017-09-22 16:54:08 +02:00
|
|
|
alt = alt || 'Missing description'; // Default for old games
|
|
|
|
|
2018-08-23 01:44:41 +02:00
|
|
|
if (image && image.path) {
|
|
|
|
path = H5P.getPath(image.path, id);
|
|
|
|
|
|
|
|
if (image.width !== undefined && image.height !== undefined) {
|
|
|
|
if (image.width > image.height) {
|
|
|
|
width = '100%';
|
|
|
|
height = 'auto';
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
height = '100%';
|
|
|
|
width = 'auto';
|
|
|
|
}
|
2015-10-19 11:17:38 +02:00
|
|
|
}
|
|
|
|
else {
|
2018-08-23 01:44:41 +02:00
|
|
|
width = height = '100%';
|
2015-10-19 11:17:38 +02:00
|
|
|
}
|
|
|
|
}
|
2018-08-23 01:44:41 +02:00
|
|
|
|
|
|
|
if (audio) {
|
|
|
|
// Check if browser supports audio.
|
|
|
|
audioPlayer = document.createElement('audio');
|
|
|
|
if (audioPlayer.canPlayType !== undefined) {
|
|
|
|
// Add supported source files.
|
|
|
|
for (var i = 0; i < audio.length; i++) {
|
|
|
|
if (audioPlayer.canPlayType(audio[i].mime)) {
|
|
|
|
var source = document.createElement('source');
|
|
|
|
source.src = H5P.getPath(audio[i].path, id);
|
|
|
|
source.type = audio[i].mime;
|
|
|
|
audioPlayer.appendChild(source);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!audioPlayer.children.length) {
|
|
|
|
audioPlayer = null; // Not supported
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
audioPlayer.controls = false;
|
|
|
|
audioPlayer.preload = 'auto';
|
|
|
|
|
|
|
|
var handlePlaying = function () {
|
|
|
|
if ($card) {
|
|
|
|
$card.addClass('h5p-memory-audio-playing');
|
2019-01-02 11:40:27 +01:00
|
|
|
self.trigger('audioplay');
|
2018-08-23 01:44:41 +02:00
|
|
|
}
|
|
|
|
};
|
|
|
|
var handleStopping = function () {
|
|
|
|
if ($card) {
|
|
|
|
$card.removeClass('h5p-memory-audio-playing');
|
2019-01-02 11:40:27 +01:00
|
|
|
self.trigger('audiostop');
|
2018-08-23 01:44:41 +02:00
|
|
|
}
|
|
|
|
};
|
|
|
|
audioPlayer.addEventListener('play', handlePlaying);
|
|
|
|
audioPlayer.addEventListener('ended', handleStopping);
|
|
|
|
audioPlayer.addEventListener('pause', handleStopping);
|
|
|
|
}
|
2015-10-19 11:17:38 +02:00
|
|
|
}
|
|
|
|
|
2017-09-22 16:54:08 +02:00
|
|
|
/**
|
|
|
|
* Update the cards label to make it accessible to users with a readspeaker
|
|
|
|
*
|
|
|
|
* @param {boolean} isMatched The card has been matched
|
|
|
|
* @param {boolean} announce Announce the current state of the card
|
|
|
|
* @param {boolean} reset Go back to the default label
|
|
|
|
*/
|
|
|
|
self.updateLabel = function (isMatched, announce, reset) {
|
|
|
|
|
|
|
|
// Determine new label from input params
|
2017-09-25 14:38:14 +02:00
|
|
|
var label = (reset ? l10n.cardUnturned : alt);
|
2017-09-22 16:54:08 +02:00
|
|
|
if (isMatched) {
|
2017-09-25 14:38:14 +02:00
|
|
|
label = l10n.cardMatched + ' ' + label;
|
2017-09-22 16:54:08 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// Update the card's label
|
2017-09-25 14:38:14 +02:00
|
|
|
$wrapper.attr('aria-label', l10n.cardPrefix.replace('%num', $wrapper.index() + 1) + ' ' + label);
|
2017-09-22 16:54:08 +02:00
|
|
|
|
|
|
|
// Update disabled property
|
|
|
|
$wrapper.attr('aria-disabled', reset ? null : 'true');
|
|
|
|
|
|
|
|
// Announce the label change
|
|
|
|
if (announce) {
|
|
|
|
$wrapper.blur().focus(); // Announce card label
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2015-10-19 11:17:38 +02:00
|
|
|
/**
|
|
|
|
* Flip card.
|
|
|
|
*/
|
|
|
|
self.flip = function () {
|
2017-09-22 16:54:08 +02:00
|
|
|
if (flippedState) {
|
|
|
|
$wrapper.blur().focus(); // Announce card label again
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2015-10-19 11:17:38 +02:00
|
|
|
$card.addClass('h5p-flipped');
|
|
|
|
self.trigger('flip');
|
2017-06-20 16:47:07 +02:00
|
|
|
flippedState = true;
|
2019-01-02 11:40:27 +01:00
|
|
|
|
|
|
|
if (audioPlayer) {
|
|
|
|
audioPlayer.play();
|
|
|
|
}
|
2015-10-19 11:17:38 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Flip card back.
|
|
|
|
*/
|
|
|
|
self.flipBack = function () {
|
2019-01-02 11:40:27 +01:00
|
|
|
self.stopAudio();
|
2017-09-22 16:54:08 +02:00
|
|
|
self.updateLabel(null, null, true); // Reset card label
|
2015-10-19 11:17:38 +02:00
|
|
|
$card.removeClass('h5p-flipped');
|
2017-06-20 16:47:07 +02:00
|
|
|
flippedState = false;
|
2015-10-19 11:17:38 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Remove.
|
|
|
|
*/
|
2017-10-17 16:16:46 +02:00
|
|
|
self.remove = function () {
|
2015-10-19 11:17:38 +02:00
|
|
|
$card.addClass('h5p-matched');
|
2017-06-20 16:47:07 +02:00
|
|
|
removedState = true;
|
2015-10-19 11:17:38 +02:00
|
|
|
};
|
|
|
|
|
2017-02-13 14:42:34 +01:00
|
|
|
/**
|
|
|
|
* Reset card to natural state
|
|
|
|
*/
|
|
|
|
self.reset = function () {
|
2019-01-02 11:40:27 +01:00
|
|
|
self.stopAudio();
|
2017-09-22 16:54:08 +02:00
|
|
|
self.updateLabel(null, null, true); // Reset card label
|
|
|
|
flippedState = false;
|
|
|
|
removedState = false;
|
2017-02-13 14:42:34 +01:00
|
|
|
$card[0].classList.remove('h5p-flipped', 'h5p-matched');
|
|
|
|
};
|
|
|
|
|
2015-10-19 11:17:38 +02:00
|
|
|
/**
|
|
|
|
* Get card description.
|
|
|
|
*
|
|
|
|
* @returns {string}
|
|
|
|
*/
|
|
|
|
self.getDescription = function () {
|
2017-01-20 11:58:08 +01:00
|
|
|
return description;
|
2015-10-19 11:17:38 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get image clone.
|
|
|
|
*
|
|
|
|
* @returns {H5P.jQuery}
|
|
|
|
*/
|
|
|
|
self.getImage = function () {
|
|
|
|
return $card.find('img').clone();
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Append card to the given container.
|
|
|
|
*
|
|
|
|
* @param {H5P.jQuery} $container
|
|
|
|
*/
|
|
|
|
self.appendTo = function ($container) {
|
2017-06-20 16:47:07 +02:00
|
|
|
$wrapper = $('<li class="h5p-memory-wrap" tabindex="-1" role="button"><div class="h5p-memory-card">' +
|
2017-06-20 10:58:13 +02:00
|
|
|
'<div class="h5p-front"' + (styles && styles.front ? styles.front : '') + '>' + (styles && styles.backImage ? '' : '<span></span>') + '</div>' +
|
2017-05-04 14:20:20 +02:00
|
|
|
'<div class="h5p-back"' + (styles && styles.back ? styles.back : '') + '>' +
|
2018-08-23 01:44:41 +02:00
|
|
|
(path ? '<img src="' + path + '" alt="' + alt + '" style="width:' + width + ';height:' + height + '"/>' + (audioPlayer ? '<div class="h5p-memory-audio-button"></div>' : '') : '<i class="h5p-memory-audio-instead-of-image">') +
|
2015-10-19 11:17:38 +02:00
|
|
|
'</div>' +
|
2017-01-23 14:12:33 +01:00
|
|
|
'</div></li>')
|
2015-10-19 11:17:38 +02:00
|
|
|
.appendTo($container)
|
2017-10-17 16:16:46 +02:00
|
|
|
.on('keydown', function (event) {
|
2017-06-20 16:47:07 +02:00
|
|
|
switch (event.which) {
|
|
|
|
case 13: // Enter
|
|
|
|
case 32: // Space
|
2017-09-22 16:54:08 +02:00
|
|
|
self.flip();
|
|
|
|
event.preventDefault();
|
2017-06-20 16:47:07 +02:00
|
|
|
return;
|
|
|
|
case 39: // Right
|
|
|
|
case 40: // Down
|
|
|
|
// Move focus forward
|
|
|
|
self.trigger('next');
|
|
|
|
event.preventDefault();
|
|
|
|
return;
|
|
|
|
case 37: // Left
|
|
|
|
case 38: // Up
|
|
|
|
// Move focus back
|
|
|
|
self.trigger('prev');
|
|
|
|
event.preventDefault();
|
|
|
|
return;
|
2017-09-25 14:38:14 +02:00
|
|
|
case 35:
|
|
|
|
// Move to last card
|
|
|
|
self.trigger('last');
|
|
|
|
event.preventDefault();
|
|
|
|
return;
|
|
|
|
case 36:
|
|
|
|
// Move to first card
|
|
|
|
self.trigger('first');
|
|
|
|
event.preventDefault();
|
|
|
|
return;
|
2017-06-20 16:47:07 +02:00
|
|
|
}
|
|
|
|
});
|
2017-09-25 14:38:14 +02:00
|
|
|
|
|
|
|
$wrapper.attr('aria-label', l10n.cardPrefix.replace('%num', $wrapper.index() + 1) + ' ' + l10n.cardUnturned);
|
2017-06-20 16:47:07 +02:00
|
|
|
$card = $wrapper.children('.h5p-memory-card')
|
|
|
|
.children('.h5p-front')
|
|
|
|
.click(function () {
|
|
|
|
self.flip();
|
|
|
|
})
|
|
|
|
.end();
|
2018-08-23 01:44:41 +02:00
|
|
|
|
|
|
|
if (audioPlayer) {
|
|
|
|
$card.children('.h5p-back')
|
|
|
|
.click(function () {
|
|
|
|
if ($card.hasClass('h5p-memory-audio-playing')) {
|
2019-01-02 11:40:27 +01:00
|
|
|
self.stopAudio();
|
2018-08-23 01:44:41 +02:00
|
|
|
}
|
|
|
|
else {
|
|
|
|
audioPlayer.play();
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
2017-02-13 14:42:34 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
2017-09-22 16:54:08 +02:00
|
|
|
* Re-append to parent container.
|
2017-02-13 14:42:34 +01:00
|
|
|
*/
|
|
|
|
self.reAppend = function () {
|
2017-06-20 16:47:07 +02:00
|
|
|
var parent = $wrapper[0].parentElement;
|
|
|
|
parent.appendChild($wrapper[0]);
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
2017-09-22 16:54:08 +02:00
|
|
|
* Make the card accessible when tabbing
|
2017-06-20 16:47:07 +02:00
|
|
|
*/
|
|
|
|
self.makeTabbable = function () {
|
|
|
|
if ($wrapper) {
|
|
|
|
$wrapper.attr('tabindex', '0');
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
2017-09-22 16:54:08 +02:00
|
|
|
* Prevent tabbing to the card
|
2017-06-20 16:47:07 +02:00
|
|
|
*/
|
|
|
|
self.makeUntabbable = function () {
|
|
|
|
if ($wrapper) {
|
|
|
|
$wrapper.attr('tabindex', '-1');
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
2017-09-22 16:54:08 +02:00
|
|
|
* Make card tabbable and move focus to it
|
2017-06-20 16:47:07 +02:00
|
|
|
*/
|
|
|
|
self.setFocus = function () {
|
|
|
|
self.makeTabbable();
|
|
|
|
if ($wrapper) {
|
|
|
|
$wrapper.focus();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
2017-09-22 16:54:08 +02:00
|
|
|
* Check if the card has been removed from the game, i.e. if has
|
|
|
|
* been matched.
|
2017-06-20 16:47:07 +02:00
|
|
|
*/
|
2017-09-22 16:54:08 +02:00
|
|
|
self.isRemoved = function () {
|
|
|
|
return removedState;
|
2017-02-13 14:42:34 +01:00
|
|
|
};
|
2019-01-02 11:40:27 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Stop any audio track that might be playing.
|
|
|
|
*/
|
|
|
|
self.stopAudio = function () {
|
|
|
|
if (audioPlayer) {
|
|
|
|
audioPlayer.pause();
|
|
|
|
audioPlayer.currentTime = 0;
|
|
|
|
}
|
|
|
|
};
|
2015-10-19 11:17:38 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
// Extends the event dispatcher
|
|
|
|
MemoryGame.Card.prototype = Object.create(EventDispatcher.prototype);
|
|
|
|
MemoryGame.Card.prototype.constructor = MemoryGame.Card;
|
|
|
|
|
2016-05-19 09:40:22 +02:00
|
|
|
/**
|
|
|
|
* Check to see if the given object corresponds with the semantics for
|
|
|
|
* a memory game card.
|
|
|
|
*
|
|
|
|
* @param {object} params
|
|
|
|
* @returns {boolean}
|
|
|
|
*/
|
|
|
|
MemoryGame.Card.isValid = function (params) {
|
|
|
|
return (params !== undefined &&
|
2018-08-23 01:44:41 +02:00
|
|
|
(params.image !== undefined &&
|
|
|
|
params.image.path !== undefined) ||
|
|
|
|
params.audio);
|
2016-05-19 09:40:22 +02:00
|
|
|
};
|
|
|
|
|
2017-01-20 11:58:08 +01:00
|
|
|
/**
|
|
|
|
* Checks to see if the card parameters should create cards with different
|
|
|
|
* images.
|
|
|
|
*
|
|
|
|
* @param {object} params
|
|
|
|
* @returns {boolean}
|
|
|
|
*/
|
|
|
|
MemoryGame.Card.hasTwoImages = function (params) {
|
|
|
|
return (params !== undefined &&
|
2018-08-23 01:44:41 +02:00
|
|
|
(params.match !== undefined &&
|
|
|
|
params.match.path !== undefined) ||
|
|
|
|
params.matchAudio);
|
2017-01-20 11:58:08 +01:00
|
|
|
};
|
|
|
|
|
2017-05-04 14:20:20 +02:00
|
|
|
/**
|
|
|
|
* Determines the theme for how the cards should look
|
|
|
|
*
|
|
|
|
* @param {string} color The base color selected
|
|
|
|
* @param {number} invertShades Factor used to invert shades in case of bad contrast
|
|
|
|
*/
|
|
|
|
MemoryGame.Card.determineStyles = function (color, invertShades, backImage) {
|
|
|
|
var styles = {
|
|
|
|
front: '',
|
|
|
|
back: '',
|
|
|
|
backImage: !!backImage
|
|
|
|
};
|
|
|
|
|
|
|
|
// Create color theme
|
|
|
|
if (color) {
|
|
|
|
var frontColor = shade(color, 43.75 * invertShades);
|
|
|
|
var backColor = shade(color, 56.25 * invertShades);
|
|
|
|
|
|
|
|
styles.front += 'color:' + color + ';' +
|
2017-05-08 15:14:18 +02:00
|
|
|
'background-color:' + frontColor + ';' +
|
|
|
|
'border-color:' + frontColor +';';
|
2017-05-04 14:20:20 +02:00
|
|
|
styles.back += 'color:' + color + ';' +
|
2017-05-08 15:14:18 +02:00
|
|
|
'background-color:' + backColor + ';' +
|
|
|
|
'border-color:' + frontColor +';';
|
2017-05-04 14:20:20 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// Add back image for card
|
|
|
|
if (backImage) {
|
|
|
|
var backgroundImage = 'background-image:url(' + backImage + ')';
|
|
|
|
|
|
|
|
styles.front += backgroundImage;
|
|
|
|
styles.back += backgroundImage;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Prep style attribute
|
|
|
|
if (styles.front) {
|
|
|
|
styles.front = ' style="' + styles.front + '"';
|
|
|
|
}
|
|
|
|
if (styles.back) {
|
|
|
|
styles.back = ' style="' + styles.back + '"';
|
|
|
|
}
|
|
|
|
|
|
|
|
return styles;
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Convert hex color into shade depending on given percent
|
|
|
|
*
|
|
|
|
* @private
|
|
|
|
* @param {string} color
|
|
|
|
* @param {number} percent
|
|
|
|
* @return {string} new color
|
|
|
|
*/
|
|
|
|
var shade = function (color, percent) {
|
|
|
|
var newColor = '#';
|
|
|
|
|
|
|
|
// Determine if we should lighten or darken
|
|
|
|
var max = (percent < 0 ? 0 : 255);
|
|
|
|
|
|
|
|
// Always stay positive
|
|
|
|
if (percent < 0) {
|
|
|
|
percent *= -1;
|
|
|
|
}
|
|
|
|
percent /= 100;
|
|
|
|
|
|
|
|
for (var i = 1; i < 6; i += 2) {
|
|
|
|
// Grab channel and convert from hex to dec
|
|
|
|
var channel = parseInt(color.substr(i, 2), 16);
|
|
|
|
|
|
|
|
// Calculate new shade and convert back to hex
|
|
|
|
channel = (Math.round((max - channel) * percent) + channel).toString(16);
|
|
|
|
|
|
|
|
// Make sure to always use two digits
|
|
|
|
newColor += (channel.length < 2 ? '0' + channel : channel);
|
|
|
|
}
|
|
|
|
|
|
|
|
return newColor;
|
|
|
|
};
|
|
|
|
|
2015-10-19 11:17:38 +02:00
|
|
|
})(H5P.MemoryGame, H5P.EventDispatcher, H5P.jQuery);
|