', {
'class': 'h5p-joubelui-progressbar-background'
}).appendTo(this.$progressbar);
}
JoubelProgressbar.prototype = Object.create(H5P.EventDispatcher.prototype);
JoubelProgressbar.prototype.constructor = JoubelProgressbar;
JoubelProgressbar.prototype.updateAria = function () {
var self = this;
if (this.options.disableAria) {
return;
}
if (!this.$currentStatus) {
this.$currentStatus = $('
', {
'class': 'h5p-joubelui-progressbar-slide-status-text',
'aria-live': 'assertive'
}).appendTo(this.$progressbar);
}
var interpolatedProgressText = self.options.progressText
.replace(':num', self.currentStep)
.replace(':total', self.steps);
this.$currentStatus.html(interpolatedProgressText);
};
/**
* Appends to a container
* @method appendTo
* @param {H5P.jquery} $container
*/
JoubelProgressbar.prototype.appendTo = function ($container) {
this.$progressbar.appendTo($container);
};
/**
* Update progress
* @method setProgress
* @param {number} step
*/
JoubelProgressbar.prototype.setProgress = function (step) {
// Check for valid value:
if (step > this.steps || step < 0) {
return;
}
this.currentStep = step;
this.$background.css({
width: ((this.currentStep/this.steps)*100) + '%'
});
this.updateAria();
};
/**
* Increment progress with 1
* @method next
*/
JoubelProgressbar.prototype.next = function () {
this.setProgress(this.currentStep+1);
};
/**
* Reset progressbar
* @method reset
*/
JoubelProgressbar.prototype.reset = function () {
this.setProgress(0);
};
/**
* Check if last step is reached
* @method isLastStep
* @return {Boolean}
*/
JoubelProgressbar.prototype.isLastStep = function () {
return this.steps === this.currentStep;
};
return JoubelProgressbar;
})(H5P.jQuery);
;
var H5P = H5P || {};
/**
* H5P Joubel UI library.
*
* This is a utility library, which does not implement attach. I.e, it has to bee actively used by
* other libraries
* @module
*/
H5P.JoubelUI = (function ($) {
/**
* The internal object to return
* @class H5P.JoubelUI
* @static
*/
function JoubelUI() {}
/* Public static functions */
/**
* Create a tip icon
* @method H5P.JoubelUI.createTip
* @param {string} text The textual tip
* @param {Object} params Parameters
* @return {H5P.JoubelTip}
*/
JoubelUI.createTip = function (text, params) {
return new H5P.JoubelTip(text, params);
};
/**
* Create message dialog
* @method H5P.JoubelUI.createMessageDialog
* @param {H5P.jQuery} $container The dom container
* @param {string} message The message
* @return {H5P.JoubelMessageDialog}
*/
JoubelUI.createMessageDialog = function ($container, message) {
return new H5P.JoubelMessageDialog($container, message);
};
/**
* Create help text dialog
* @method H5P.JoubelUI.createHelpTextDialog
* @param {string} header The textual header
* @param {string} message The textual message
* @param {string} closeButtonTitle The title for the close button
* @return {H5P.JoubelHelpTextDialog}
*/
JoubelUI.createHelpTextDialog = function (header, message, closeButtonTitle) {
return new H5P.JoubelHelpTextDialog(header, message, closeButtonTitle);
};
/**
* Create progress circle
* @method H5P.JoubelUI.createProgressCircle
* @param {number} number The progress (0 to 100)
* @param {string} progressColor The progress color in hex value
* @param {string} fillColor The fill color in hex value
* @param {string} backgroundColor The background color in hex value
* @return {H5P.JoubelProgressCircle}
*/
JoubelUI.createProgressCircle = function (number, progressColor, fillColor, backgroundColor) {
return new H5P.JoubelProgressCircle(number, progressColor, fillColor, backgroundColor);
};
/**
* Create throbber for loading
* @method H5P.JoubelUI.createThrobber
* @return {H5P.JoubelThrobber}
*/
JoubelUI.createThrobber = function () {
return new H5P.JoubelThrobber();
};
/**
* Create simple rounded button
* @method H5P.JoubelUI.createSimpleRoundedButton
* @param {string} text The button label
* @return {H5P.SimpleRoundedButton}
*/
JoubelUI.createSimpleRoundedButton = function (text) {
return new H5P.SimpleRoundedButton(text);
};
/**
* Create Slider
* @method H5P.JoubelUI.createSlider
* @param {Object} [params] Parameters
* @return {H5P.JoubelSlider}
*/
JoubelUI.createSlider = function (params) {
return new H5P.JoubelSlider(params);
};
/**
* Create Score Bar
* @method H5P.JoubelUI.createScoreBar
* @param {number=} maxScore The maximum score
* @param {string} [label] Makes it easier for readspeakers to identify the scorebar
* @return {H5P.JoubelScoreBar}
*/
JoubelUI.createScoreBar = function (maxScore, label, helpText, scoreExplanationButtonLabel) {
return new H5P.JoubelScoreBar(maxScore, label, helpText, scoreExplanationButtonLabel);
};
/**
* Create Progressbar
* @method H5P.JoubelUI.createProgressbar
* @param {number=} numSteps The total numer of steps
* @param {Object} [options] Additional options
* @param {boolean} [options.disableAria] Disable readspeaker assistance
* @param {string} [options.progressText] A progress text for describing
* current progress out of total progress for readspeakers.
* e.g. "Slide :num of :total"
* @return {H5P.JoubelProgressbar}
*/
JoubelUI.createProgressbar = function (numSteps, options) {
return new H5P.JoubelProgressbar(numSteps, options);
};
/**
* Create standard Joubel button
*
* @method H5P.JoubelUI.createButton
* @param {object} params
* May hold any properties allowed by jQuery. If href is set, an A tag
* is used, if not a button tag is used.
* @return {H5P.jQuery} The jquery element created
*/
JoubelUI.createButton = function(params) {
var type = 'button';
if (params.href) {
type = 'a';
}
else {
params.type = 'button';
}
if (params.class) {
params.class += ' h5p-joubelui-button';
}
else {
params.class = 'h5p-joubelui-button';
}
return $('<' + type + '/>', params);
};
/**
* Fix for iframe scoll bug in IOS. When focusing an element that doesn't have
* focus support by default the iframe will scroll the parent frame so that
* the focused element is out of view. This varies dependening on the elements
* of the parent frame.
*/
if (H5P.isFramed && !H5P.hasiOSiframeScrollFix &&
/iPad|iPhone|iPod/.test(navigator.userAgent)) {
H5P.hasiOSiframeScrollFix = true;
// Keep track of original focus function
var focus = HTMLElement.prototype.focus;
// Override the original focus
HTMLElement.prototype.focus = function () {
// Only focus the element if it supports it natively
if ( (this instanceof HTMLAnchorElement ||
this instanceof HTMLInputElement ||
this instanceof HTMLSelectElement ||
this instanceof HTMLTextAreaElement ||
this instanceof HTMLButtonElement ||
this instanceof HTMLIFrameElement ||
this instanceof HTMLAreaElement) && // HTMLAreaElement isn't supported by Safari yet.
!this.getAttribute('role')) { // Focus breaks if a different role has been set
// In theory this.isContentEditable should be able to recieve focus,
// but it didn't work when tested.
// Trigger the original focus with the proper context
focus.call(this);
}
};
}
return JoubelUI;
})(H5P.jQuery);
;
var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; };
var H5P = H5P || {};
/**
* H5P-Timer
*
* General purpose timer that can be used by other H5P libraries.
*
* @param {H5P.jQuery} $
*/
H5P.Timer = function ($, EventDispatcher) {
/**
* Create a timer.
*
* @constructor
* @param {number} [interval=Timer.DEFAULT_INTERVAL] - The update interval.
*/
function Timer() {
var interval = arguments.length <= 0 || arguments[0] === undefined ? Timer.DEFAULT_INTERVAL : arguments[0];
var self = this;
// time on clock and the time the clock has run
var clockTimeMilliSeconds = 0;
var playingTimeMilliSeconds = 0;
// used to update recurring notifications
var clockUpdateMilliSeconds = 0;
// indicators for total running time of the timer
var firstDate = null;
var startDate = null;
var lastDate = null;
// update loop
var loop = null;
// timer status
var status = Timer.STOPPED;
// indicate counting direction
var mode = Timer.FORWARD;
// notifications
var notifications = [];
// counter for notifications;
var notificationsIdCounter = 0;
// Inheritance
H5P.EventDispatcher.call(self);
// sanitize interval
if (Timer.isInteger(interval)) {
interval = Math.max(interval, 1);
}
else {
interval = Timer.DEFAULT_INTERVAL;
}
/**
* Get the timer status.
*
* @public
* @return {number} The timer status.
*/
self.getStatus = function () {
return status;
};
/**
* Get the timer mode.
*
* @public
* @return {number} The timer mode.
*/
self.getMode = function () {
return mode;
};
/**
* Get the time that's on the clock.
*
* @private
* @return {number} The time on the clock.
*/
var getClockTime = function getClockTime() {
return clockTimeMilliSeconds;
};
/**
* Get the time the timer was playing so far.
*
* @private
* @return {number} The time played.
*/
var getPlayingTime = function getPlayingTime() {
return playingTimeMilliSeconds;
};
/**
* Get the total running time from play() until stop().
*
* @private
* @return {number} The total running time.
*/
var getRunningTime = function getRunningTime() {
if (!firstDate) {
return 0;
}
if (status !== Timer.STOPPED) {
return new Date().getTime() - firstDate.getTime();
}
else {
return !lastDate ? 0 : lastDate.getTime() - firstDate;
}
};
/**
* Get one of the times.
*
* @public
* @param {number} [type=Timer.TYPE_CLOCK] - Type of the time to get.
* @return {number} Clock Time, Playing Time or Running Time.
*/
self.getTime = function () {
var type = arguments.length <= 0 || arguments[0] === undefined ? Timer.TYPE_CLOCK : arguments[0];
if (!Timer.isInteger(type)) {
return;
}
// break will never be reached, but for consistency...
switch (type) {
case Timer.TYPE_CLOCK:
return getClockTime();
break;
case Timer.TYPE_PLAYING:
return getPlayingTime();
break;
case Timer.TYPE_RUNNING:
return getRunningTime();
break;
default:
return getClockTime();
}
};
/**
* Set the clock time.
*
* @public
* @param {number} time - The time in milliseconds.
*/
self.setClockTime = function (time) {
if ($.type(time) === 'string') {
time = Timer.toMilliseconds(time);
}
if (!Timer.isInteger(time)) {
return;
}
// notifications only need an update if changing clock against direction
clockUpdateMilliSeconds = (time - clockTimeMilliSeconds) * mode < 0 ? time - clockTimeMilliSeconds : 0;
clockTimeMilliSeconds = time;
};
/**
* Reset the timer.
*
* @public
*/
self.reset = function () {
if (status !== Timer.STOPPED) {
return;
}
clockTimeMilliSeconds = 0;
playingTimeMilliSeconds = 0;
firstDate = null;
lastDate = null;
loop = null;
notifications = [];
notificationsIdCounter = 0;
self.trigger('reset', {}, {bubbles: true, external: true});
};
/**
* Set timer mode.
*
* @public
* @param {number} mode - The timer mode.
*/
self.setMode = function (direction) {
if (direction !== Timer.FORWARD && direction !== Timer.BACKWARD) {
return;
}
mode = direction;
};
/**
* Start the timer.
*
* @public
*/
self.play = function () {
if (status === Timer.PLAYING) {
return;
}
if (!firstDate) {
firstDate = new Date();
}
startDate = new Date();
status = Timer.PLAYING;
self.trigger('play', {}, {bubbles: true, external: true});
update();
};
/**
* Pause the timer.
*
* @public
*/
self.pause = function () {
if (status !== Timer.PLAYING) {
return;
}
status = Timer.PAUSED;
self.trigger('pause', {}, {bubbles: true, external: true});
};
/**
* Stop the timer.
*
* @public
*/
self.stop = function () {
if (status === Timer.STOPPED) {
return;
}
lastDate = new Date();
status = Timer.STOPPED;
self.trigger('stop', {}, {bubbles: true, external: true});
};
/**
* Update the timer until Timer.STOPPED.
*
* @private
*/
var update = function update() {
var currentMilliSeconds = 0;
// stop because requested
if (status === Timer.STOPPED) {
clearTimeout(loop);
return;
}
//stop because countdown reaches 0
if (mode === Timer.BACKWARD && clockTimeMilliSeconds <= 0) {
self.stop();
return;
}
// update times
if (status === Timer.PLAYING) {
currentMilliSeconds = new Date().getTime() - startDate;
clockTimeMilliSeconds += currentMilliSeconds * mode;
playingTimeMilliSeconds += currentMilliSeconds;
}
startDate = new Date();
checkNotifications();
loop = setTimeout(function () {
update();
}, interval);
};
/**
* Get next notification id.
*
* @private
* @return {number} id - The next id.
*/
var getNextNotificationId = function getNextNotificationId() {
return notificationsIdCounter++;
};
/**
* Set a notification
*
* @public
* @param {Object|String} params - Parameters for the notification.
* @callback callback - Callback function.
* @return {number} ID of the notification.
*/
self.notify = function (params, callback) {
var id = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : getNextNotificationId();
// common default values for the clock timer
// TODO: find a better place for this, maybe a JSON file?
var defaults = {};
defaults['every_tenth_second'] = { "repeat": 100 };
defaults['every_second'] = { "repeat": 1000 };
defaults['every_minute'] = { "repeat": 60000 };
defaults['every_hour'] = { "repeat": 3600000 };
// Sanity check for callback function
if (!callback instanceof Function) {
return;
}
// Get default values
if ($.type(params) === 'string') {
params = defaults[params];
}
if (params !== null && (typeof params === 'undefined' ? 'undefined' : _typeof(params)) === 'object') {
// Sanitize type
if (!params.type) {
params.type = Timer.TYPE_CLOCK;
}
else {
if (!Timer.isInteger(params.type)) {
return;
}
if (params.type < Timer.TYPE_CLOCK || params.type > Timer.TYPE_RUNNING) {
return;
}
}
// Sanitize mode
if (!params.mode) {
params.mode = Timer.NOTIFY_ABSOLUTE;
}
else {
if (!Timer.isInteger(params.mode)) {
return;
}
if (params.mode < Timer.NOTIFY_ABSOLUTE || params.type > Timer.NOTIFY_RELATIVE) {
return;
}
}
// Sanitize calltime
if (!params.calltime) {
params.calltime = params.mode === Timer.NOTIFY_ABSOLUTE ? self.getTime(params.type) : 0;
}
else {
if ($.type(params.calltime) === 'string') {
params.calltime = Timer.toMilliseconds(params.calltime);
}
if (!Timer.isInteger(params.calltime)) {
return;
}
if (params.calltime < 0) {
return;
}
if (params.mode === Timer.NOTIFY_RELATIVE) {
params.calltime = Math.max(params.calltime, interval);
if (params.type === Timer.TYPE_CLOCK) {
// clock could be running backwards
params.calltime *= mode;
}
params.calltime += self.getTime(params.type);
}
}
// Sanitize repeat
if ($.type(params.repeat) === 'string') {
params.repeat = Timer.toMilliseconds(params.repeat);
}
// repeat must be >= interval (ideally multiple of interval)
if (params.repeat !== undefined) {
if (!Timer.isInteger(params.repeat)) {
return;
}
params.repeat = Math.max(params.repeat, interval);
}
}
else {
// neither object nor string
return;
}
// add notification
notifications.push({
'id': id,
'type': params.type,
'calltime': params.calltime,
'repeat': params.repeat,
'callback': callback
});
return id;
};
/**
* Remove a notification.
*
* @public
* @param {number} id - The id of the notification.
*/
self.clearNotification = function (id) {
notifications = $.grep(notifications, function (item) {
return item.id === id;
}, true);
};
/**
* Set a new starting time for notifications.
*
* @private
* @param elements {Object] elements - The notifications to be updated.
* @param deltaMilliSeconds {Number} - The time difference to be set.
*/
var updateNotificationTime = function updateNotificationTime(elements, deltaMilliSeconds) {
if (!Timer.isInteger(deltaMilliSeconds)) {
return;
}
elements.forEach(function (element) {
// remove notification
self.clearNotification(element.id);
//rebuild notification with new data
self.notify({
'type': element.type,
'calltime': self.getTime(element.type) + deltaMilliSeconds,
'repeat': element.repeat
}, element.callback, element.id);
});
};
/**
* Check notifications for necessary callbacks.
*
* @private
*/
var checkNotifications = function checkNotifications() {
var backwards = 1;
var elements = [];
// update recurring clock notifications if clock was changed
if (clockUpdateMilliSeconds !== 0) {
elements = $.grep(notifications, function (item) {
return item.type === Timer.TYPE_CLOCK && item.repeat != undefined;
});
updateNotificationTime(elements, clockUpdateMilliSeconds);
clockUpdateMilliSeconds = 0;
}
// check all notifications for triggering
notifications.forEach(function (element) {
/*
* trigger if notification time is in the past
* which means calltime >= Clock Time if mode is BACKWARD (= -1)
*/
backwards = element.type === Timer.TYPE_CLOCK ? mode : 1;
if (element.calltime * backwards <= self.getTime(element.type) * backwards) {
// notify callback function
element.callback.apply(this);
// remove notification
self.clearNotification(element.id);
// You could use updateNotificationTime() here, but waste some time
// rebuild notification if it should be repeated
if (element.repeat) {
self.notify({
'type': element.type,
'calltime': self.getTime(element.type) + element.repeat * backwards,
'repeat': element.repeat
}, element.callback, element.id);
}
}
});
};
}
// Inheritance
Timer.prototype = Object.create(H5P.EventDispatcher.prototype);
Timer.prototype.constructor = Timer;
/**
* Generate timecode elements from milliseconds.
*
* @private
* @param {number} milliSeconds - The milliseconds.
* @return {Object} The timecode elements.
*/
var toTimecodeElements = function toTimecodeElements(milliSeconds) {
var years = 0;
var month = 0;
var weeks = 0;
var days = 0;
var hours = 0;
var minutes = 0;
var seconds = 0;
var tenthSeconds = 0;
if (!Timer.isInteger(milliSeconds)) {
return;
}
milliSeconds = Math.round(milliSeconds / 100);
tenthSeconds = milliSeconds - Math.floor(milliSeconds / 10) * 10;
seconds = Math.floor(milliSeconds / 10);
minutes = Math.floor(seconds / 60);
hours = Math.floor(minutes / 60);
days = Math.floor(hours / 24);
weeks = Math.floor(days / 7);
month = Math.floor(days / 30.4375); // roughly (30.4375 = mean of 4 years)
years = Math.floor(days / 365); // roughly (no leap years considered)
return {
years: years,
month: month,
weeks: weeks,
days: days,
hours: hours,
minutes: minutes,
seconds: seconds,
tenthSeconds: tenthSeconds
};
};
/**
* Extract humanized time element from time for concatenating.
*
* @public
* @param {number} milliSeconds - The milliSeconds.
* @param {string} element - Time element: hours, minutes, seconds or tenthSeconds.
* @param {boolean} [rounded=false] - If true, element value will be rounded.
* @return {number} The time element.
*/
Timer.extractTimeElement = function (time, element) {
var rounded = arguments.length <= 2 || arguments[2] === undefined ? false : arguments[2];
var timeElements = null;
if ($.type(time) === 'string') {
time = Timer.toMilliseconds(time);
}
if (!Timer.isInteger(time)) {
return;
}
if ($.type(element) !== 'string') {
return;
}
if ($.type(rounded) !== 'boolean') {
return;
}
if (rounded) {
timeElements = {
years: Math.round(time / 31536000000),
month: Math.round(time / 2629800000),
weeks: Math.round(time / 604800000),
days: Math.round(time / 86400000),
hours: Math.round(time / 3600000),
minutes: Math.round(time / 60000),
seconds: Math.round(time / 1000),
tenthSeconds: Math.round(time / 100)
};
}
else {
timeElements = toTimecodeElements(time);
}
return timeElements[element];
};
/**
* Convert time in milliseconds to timecode.
*
* @public
* @param {number} milliSeconds - The time in milliSeconds.
* @return {string} The humanized timecode.
*/
Timer.toTimecode = function (milliSeconds) {
var timecodeElements = null;
var timecode = '';
var minutes = 0;
var seconds = 0;
if (!Timer.isInteger(milliSeconds)) {
return;
}
if (milliSeconds < 0) {
return;
}
timecodeElements = toTimecodeElements(milliSeconds);
minutes = Math.floor(timecodeElements['minutes'] % 60);
seconds = Math.floor(timecodeElements['seconds'] % 60);
// create timecode
if (timecodeElements['hours'] > 0) {
timecode += timecodeElements['hours'] + ':';
}
if (minutes < 10) {
timecode += '0';
}
timecode += minutes + ':';
if (seconds < 10) {
timecode += '0';
}
timecode += seconds + '.';
timecode += timecodeElements['tenthSeconds'];
return timecode;
};
/**
* Convert timecode to milliseconds.
*
* @public
* @param {string} timecode - The timecode.
* @return {number} Milliseconds derived from timecode
*/
Timer.toMilliseconds = function (timecode) {
var head = [];
var tail = '';
var hours = 0;
var minutes = 0;
var seconds = 0;
var tenthSeconds = 0;
if (!Timer.isTimecode(timecode)) {
return;
}
// thx to the regexp we know everything can be converted to a legit integer in range
head = timecode.split('.')[0].split(':');
while (head.length < 3) {
head = ['0'].concat(head);
}
hours = parseInt(head[0]);
minutes = parseInt(head[1]);
seconds = parseInt(head[2]);
tail = timecode.split('.')[1];
if (tail) {
tenthSeconds = Math.round(parseInt(tail) / Math.pow(10, tail.length - 1));
}
return (hours * 36000 + minutes * 600 + seconds * 10 + tenthSeconds) * 100;
};
/**
* Check if a string is a timecode.
*
* @public
* @param {string} value - String to check
* @return {boolean} true, if string is a timecode
*/
Timer.isTimecode = function (value) {
var reg_timecode = /((((((\d+:)?([0-5]))?\d:)?([0-5]))?\d)(\.\d+)?)/;
if ($.type(value) !== 'string') {
return false;
}
return value === value.match(reg_timecode)[0] ? true : false;
};
// Workaround for IE and potentially other browsers within Timer object
Timer.isInteger = Timer.isInteger || function(value) {
return typeof value === "number" && isFinite(value) && Math.floor(value) === value;
};
// Timer states
/** @constant {number} */
Timer.STOPPED = 0;
/** @constant {number} */
Timer.PLAYING = 1;
/** @constant {number} */
Timer.PAUSED = 2;
// Timer directions
/** @constant {number} */
Timer.FORWARD = 1;
/** @constant {number} */
Timer.BACKWARD = -1;
/** @constant {number} */
Timer.DEFAULT_INTERVAL = 10;
// Counter types
/** @constant {number} */
Timer.TYPE_CLOCK = 0;
/** @constant {number} */
Timer.TYPE_PLAYING = 1;
/** @constant {number} */
Timer.TYPE_RUNNING = 2;
// Notification types
/** @constant {number} */
Timer.NOTIFY_ABSOLUTE = 0;
/** @constant {number} */
Timer.NOTIFY_RELATIVE = 1;
return Timer;
}(H5P.jQuery, H5P.EventDispatcher);
;
H5P.FindTheWords = (function ($, UI) {
const ELEMENT_MIN_SIZE = 32; // PX
const ELEMENT_MAX_SIZE = 64; // PX
const MARGIN = 8; //PX
const VOCABULARY_INLINE_WIDTH = 200;// PX
/**
* FindTheWords.
* @class H5P.FindTheWords
* @extends H5P.EventDispatcher
* @param {Object} options
* @param {number} id
* @param {Object} extras
*/
function FindTheWords(options, id, extras) {
/** @alias H5P.FindTheWords# */
this.id = id;
this.extras = extras;
this.numFound = 0;
this.isAttempted = false;
this.isGameStarted = false;
// Only take the unique words
const vocabulary = options.wordList
.split(',')
.map(function (word) {
return word.trim();
})
.filter(function (word, pos, self) {
return self.indexOf(word) === pos;
});
this.options = $.extend(true, {
vocabulary: vocabulary,
height: 5,
width: 5,
fillBlanks: true,
maxAttempts: 5,
l10n: {
wordListHeader: 'Find the words'
}
}, options);
H5P.EventDispatcher.call(this);
this.gridParams = {
height: this.options.height,
width: this.options.width,
orientations: filterOrientations(options.behaviour.orientations),
fillBlanks: this.options.fillBlanks,
maxAttempts: this.options.maxAttempts,
preferOverlap: options.behaviour.preferOverlap,
vocabulary: this.options.vocabulary,
gridActive: true,
fillPool: this.options.behaviour.fillPool
};
this.grid = new FindTheWords.WordGrid(this.gridParams);
this.vocabulary = new FindTheWords.Vocabulary(
this.options.vocabulary,
this.options.behaviour.showVocabulary,
this.options.l10n.wordListHeader
);
this.registerDOMElements();
// responsive functionality
this.on('resize', function () {
const currentSize = this.elementSize;
const currentVocMod = this.isVocModeBlock;
this.calculateElementSize();
this.setVocabularyMode();
if (this.elementSize !== currentSize) {
this.$puzzleContainer.empty();
this.grid.appendTo(this.$puzzleContainer, this.elementSize );
this.grid.drawGrid(MARGIN);
// If there are already marked elements on the grid mark them
if (!this.grid.options.gridActive) {
this.grid.enableGrid();
this.grid.mark(this.vocabulary.getFound());
this.grid.disableGrid();
this.grid.mark(this.vocabulary.getSolved());
}
else {
this.grid.mark(this.vocabulary.getFound());
}
this.registerGridEvents();
}
// vocabulary adjustments on resize
if (this.options.behaviour.showVocabulary) {
if (currentVocMod !== this.isVocModeBlock ) {
this.vocabulary.setMode(this.isVocModeBlock);
if (this.isVocModeBlock) {
this.$puzzleContainer.removeClass('puzzle-inline').addClass('puzzle-block');
}
else {
//initial update has to be done manually
this.$playArea.css({'width': parseInt(this.$gameContainer.width()) + VOCABULARY_INLINE_WIDTH});
this.$puzzleContainer.removeClass('puzzle-block').addClass('puzzle-inline');
}
}
}
// Make the playarea just to fit its content
if (! this.isVocModeBlock) {
this.$playArea.css({'width': parseInt(this.$gameContainer.width()) + 2});
}
else {
this.$playArea.css({'width': parseInt(this.$puzzleContainer.width()) + 2});
}
});
}
FindTheWords.prototype = Object.create(H5P.EventDispatcher.prototype);
FindTheWords.prototype.constructor = FindTheWords;
// private and all prototype function goes there
/**
* filterOrientations - Mapping of directions from semantics to those used by algorithm.
* @param {Object} directions
* @return {Object[]}
*/
const filterOrientations = function (directions) {
return Object.keys(directions).filter(function (key) {
return directions[key];
});
};
/**
* registerDOMElements.
*/
FindTheWords.prototype.registerDOMElements = function () {
const that = this;
this.$playArea = $('
', {
class: 'h5p-play-area'
});
this.$taskDescription = $('
', {
class: 'h5p-task-description',
html: this.options.taskDescription,
tabIndex: 0,
});
// timer part
this.$timer = $('
', {
class: 'time-status',
tabindex: 0,
html: '
' +
this.options.l10n.timeSpent + ':' +
'
0:00'
});
this.timer = new FindTheWords.Timer(this.$timer.find('.h5p-time-spent'));
// counter part
const counterText = that.options.l10n.found
.replace('@found', '
0')
.replace('@totalWords', '
' + this.vocabulary.words.length + '');
this.$counter = $('
', {
class: 'counter-status',
tabindex: 0,
html: '
' + counterText + '
'
});
this.counter = new FindTheWords.Counter(this.$counter.find('.h5p-counter'));
// feedback plus progressBar
this.$feedback = $('
', {
class: 'feedback-element',
tabindex: '0'
});
this.$progressBar = UI.createScoreBar(this.vocabulary.words.length, 'scoreBarLabel');
// buttons section
that.$submitButton = that.createButton('submit', 'check', that.options.l10n.check, that.gameSubmitted);
if (this.options.behaviour.enableShowSolution) {
this.$showSolutionButton = this.createButton('solution', 'eye', this.options.l10n.showSolution, that.showSolutions);
}
if (this.options.behaviour.enableRetry) {
this.$retryButton = this.createButton('retry', 'undo', this.options.l10n.tryAgain, that.resetTask);
}
// container elements
this.$gameContainer = $('
');
this.$puzzleContainer = $('
');
this.$vocabularyContainer = $('
');
this.$footerContainer = $('');
this.$statusContainer = $('
', {
class: 'game-status',
'aria-label': 'game-status',
role: 'group',
tabindex: '0'
});
this.$feedbackContainer = $('
');
this.$buttonContainer = $('
');
};
/**
* createButton - creating all buttons used in this game.
* @param {string} name Buttonname.
* @param {string} icon Fa icon name.
* @param {string} param Button text parameter.
* @param {function} callback Callback function.
* @return {H5P.JoubelUI.Button} Joubel ui button object.
*/
FindTheWords.prototype.createButton = function (name, icon, param, callback) {
const cfunction = callback.bind(this);
return UI.createButton({
title: name,
click: cfunction,
html: '
' + param
});
};
/**
* calculateElementSize - calculate the grid element size according to the container width.
*/
FindTheWords.prototype.calculateElementSize = function () {
const containerWidth = this.$container.width();
const gridCol = this.grid.wordGrid[0].length;
const gridMaxWidth = gridCol * ELEMENT_MAX_SIZE + 2 * MARGIN;
const gridElementStdSize = (containerWidth - 2 * MARGIN) / gridCol;
if (gridMaxWidth < containerWidth) {
this.elementSize = ELEMENT_MAX_SIZE;
}
else if (gridElementStdSize > ELEMENT_MIN_SIZE) {
this.elementSize = gridElementStdSize;
}
else {
this.elementSize = ELEMENT_MAX_SIZE;
}
};
/**
* setVocabularyMode - set vocabulary mode (either inline or block).
*/
FindTheWords.prototype.setVocabularyMode = function () {
const gridCol = this.grid.wordGrid[0].length;
this.isVocModeBlock = (this.$container.width() - (gridCol * this.elementSize + 2 * MARGIN) > VOCABULARY_INLINE_WIDTH) ? false : true;
};
/**
* gameSubmitted - callback function for check button.
*/
FindTheWords.prototype.gameSubmitted = function () {
const totalScore = this.vocabulary.words.length;
const scoreText = this.options.l10n.score
.replace('@score', this.numFound)
.replace('@total', totalScore);
this.timer.stop();
this.$progressBar.setScore(this.numFound);
this.$feedback.html(scoreText);
this.$submitButton = this.$submitButton.detach();
this.grid.disableGrid();
if (totalScore !== this.numFound) {
if (this.options.behaviour.enableShowSolution) {
this.$showSolutionButton.appendTo(this.$buttonContainer);
}
}
if (this.options.behaviour.enableRetry) {
this.$retryButton.appendTo(this.$buttonContainer);
}
this.$feedbackContainer.addClass('feedback-show'); //show feedbackMessage
this.$feedback.focus();
const xAPIEvent = this.createXAPIEventTemplate('answered');
this.addQuestionToXAPI(xAPIEvent);
this.addResponseToXAPI(xAPIEvent);
this.trigger(xAPIEvent);
this.trigger('resize');
};
/**
* showSolutions - call back function for show solution button.
*/
FindTheWords.prototype.showSolutions = function () {
this.grid.disableGrid();
this.grid.mark(this.vocabulary.getNotFound());
this.vocabulary.solveWords();
this.$showSolutionButton.detach();
this.$vocabularyContainer.focus();
this.trigger('resize');
};
/**
* resetTask - resetting the game.
*/
FindTheWords.prototype.resetTask = function () {
this.numFound = 0;
this.timer.reset();
this.counter.reset();
this.$progressBar.reset();
this.$puzzleContainer.empty();
this.vocabulary.reset();
if (this.$showSolutionButton) {
this.$showSolutionButton.detach();
}
this.$retryButton.detach();
this.$feedbackContainer.removeClass('feedback-show');
this.grid = new FindTheWords.WordGrid(this.gridParams);
this.grid.appendTo(this.$puzzleContainer, this.elementSize);
this.grid.drawGrid(MARGIN);
this.grid.enableGrid();
this.registerGridEvents();
this.$submitButton.appendTo(this.$buttonContainer);
this.$puzzleContainer.focus();
this.trigger('resize');
};
/**
* Check whether user is able to play the game.
* @return {boolean}
*/
FindTheWords.prototype.getAnswerGiven = function () {
return this.isAttempted;
};
/**
* getScore - Return the score obtained.
* @return {number}
*/
FindTheWords.prototype.getScore = function () {
return this.numFound;
};
/**
* Turn the maximum possible score that can be obtained.
* @return {number}
*/
FindTheWords.prototype.getMaxScore = function () {
return this.vocabulary.words.length;
};
/**
* getXAPIData - Get xAPI data.
* @see contract at {@link https://h5p.org/documentation/developers/contracts#guides-header-6}
* @return {Object} xApi data statement
*/
FindTheWords.prototype.getXAPIData = function () {
const xAPIEvent = this.createXAPIEventTemplate('answered');
this.addQuestionToXAPI(xAPIEvent);
this.addResponseToXAPI(xAPIEvent);
return {
statement: xAPIEvent.data.statement
};
};
/**
* addQuestionToXAPI - Add the question to the definition part of an xAPIEvent.
* @param {H5P.XAPIEvent} xAPIEvent
*/
FindTheWords.prototype.addQuestionToXAPI = function (xAPIEvent) {
const definition = xAPIEvent.getVerifiedStatementValue(
['object', 'definition']
);
definition.description = {
'en-US': this.options.taskDescription
};
definition.type =
'http://adlnet.gov/expapi/activities/cmi.interaction';
definition.interactionType = 'choice';
definition.correctResponsesPattern = [];
definition.correctResponsesPattern[0] = this.vocabulary.words.join([',']);
};
/**
* Add the response part to an xAPI event.
* @param {H5P.XAPIEvent} xAPIEvent
*/
FindTheWords.prototype.addResponseToXAPI = function (xAPIEvent) {
const maxScore = this.getMaxScore();
const score = this.getScore();
const success = (score === maxScore);
const response = this.vocabulary.getFound().join('[,]');
xAPIEvent.setScoredResult(score, maxScore, this, true, success);
xAPIEvent.data.statement.result.response = response;
};
/**
* registerGridEvents.
*/
FindTheWords.prototype.registerGridEvents = function () {
const that = this;
this.grid.on('drawStart', function () {
if (!that.isGameStarted) {
that.timer.play();
that.triggerXAPI('interacted');
that.isGameStarted = true;
}
});
this.grid.on('drawEnd', function (event) {
that.isAttempted = true;
if (that.vocabulary.checkWord(event.data['markedWord'])) {
that.numFound++;
that.counter.increment();
that.grid.markWord(event.data['wordObject']);
if (that.numFound === that.vocabulary.words.length) {
that.gameSubmitted();
}
}
});
};
/**
* attach - main attach function.
* @param {H5P.jQuery} $container Description.
*/
FindTheWords.prototype.attach = function ($container) {
this.$container = $container.addClass('h5p-find-the-words');
this.triggerXAPI('attempted');
if (this.grid) {
this.calculateElementSize();
this.grid.appendTo(this.$puzzleContainer, this.elementSize );
this.$puzzleContainer.appendTo(this.$gameContainer);
if (this.options.behaviour.showVocabulary) {
this.setVocabularyMode();
this.vocabulary.appendTo(this.$vocabularyContainer, this.isVocModeBlock);
this.$vocabularyContainer.appendTo(this.$gameContainer);
}
}
this.$timer.appendTo(this.$statusContainer);
this.$counter.appendTo(this.$statusContainer);
this.$feedback.appendTo(this.$feedbackContainer);
this.$progressBar.appendTo(this.$feedbackContainer);
this.$submitButton.appendTo(this.$buttonContainer);
//append status and feedback and button containers to footer
this.$statusContainer.appendTo(this.$footerContainer);
this.$feedbackContainer.appendTo(this.$footerContainer);
this.$buttonContainer.appendTo(this.$footerContainer);
//append description , cards and footer to main container.
this.$taskDescription.appendTo(this.$playArea);
this.$gameContainer.appendTo(this.$playArea);
this.$footerContainer.appendTo(this.$playArea);
this.$playArea.appendTo(this.$container);
this.grid.drawGrid(MARGIN);
this.registerGridEvents();
this.trigger('resize');
};
return FindTheWords;
}) (H5P.jQuery, H5P.JoubelUI);
;
(function (FindTheWords, EventDispatcher, $) {
/**
* WordGrid - Handles the word grid part of the game.
* @class H5P.FindTheWords.WordGrid
* @extends H5P.EventDispatcher
* @param {Object} params Description.
*/
FindTheWords.WordGrid = function (params) {
/** @alias H5P.FindTheWords.WordGrid# */
// extending the default parameter set for the grid
this.options = params;
EventDispatcher.call(this);
this.createWordGrid();
};
FindTheWords.WordGrid.prototype = Object.create(EventDispatcher.prototype);
FindTheWords.WordGrid.prototype.constructor = FindTheWords.WordGrid;
// get i th element position based on the current position for different orientations
const orientations = {
horizontal: function (x, y, i) {
return {
x: x + i,
y: y
};
},
horizontalBack: function (x, y, i) {
return {
x: x - i,
y: y
};
},
vertical: function (x, y, i) {
return {
x: x,
y: y + i
};
},
verticalUp: function (x, y, i) {
return {
x: x,
y: y - i
};
},
diagonal: function (x, y, i) {
return {
x: x + i,
y: y + i
};
},
diagonalBack: function (x, y, i) {
return {
x: x - i,
y: y + i
};
},
diagonalUp: function (x, y, i) {
return {
x: x + i,
y: y - i
};
},
diagonalUpBack: function (x, y, i) {
return {
x: x - i,
y: y - i
};
}
};
/*
* Determines if an orientation is possible given the starting square (x,y),
* the height (h) and width (w) of the puzzle, and the length of the word (l).
* Returns true if the word will fit starting at the square provided using
* the specified orientation.
*/
const checkOrientations = {
horizontal: function (x, y, h, w, l) {
return w >= x + l;
},
horizontalBack: function (x, y, h, w, l) {
return x + 1 >= l;
},
vertical: function (x, y, h, w, l) {
return h >= y + l;
},
verticalUp: function (x, y, h, w, l) {
return y + 1 >= l;
},
diagonal: function (x, y, h, w, l) {
return (w >= x + l) && (h >= y + l);
},
diagonalBack: function (x, y, h, w, l) {
return (x + 1 >= l) && (h >= y + l);
},
diagonalUp: function (x, y, h, w, l) {
return (w >= x + l) && (y + 1 >= l);
},
diagonalUpBack: function (x, y, h, w, l) {
return (x + 1 >= l) && (y + 1 >= l);
}
};
/*
* Determines the next possible valid square given the square (x,y) was ]
* invalid and a word lenght of (l). This greatly reduces the number of
* squares that must be checked. Returning {x: x+1, y: y} will always work
* but will not be optimal.
*/
const skipOrientations = {
horizontal: function (x, y) {
return {
x: 0,
y: y + 1
};
},
horizontalBack: function (x, y, l) {
return {
x: l - 1,
y: y
};
},
vertical: function (x, y) {
return {
x: 0,
y: y + 100
};
},
verticalUp: function (x, y, l) {
return {
x: 0,
y: l - 1
};
},
diagonal: function (x, y) {
return {
x: 0,
y: y + 1
};
},
diagonalBack: function (x, y, l) {
return {
x: l - 1,
y: x >= l - 1 ? y + 1 : y
};
},
diagonalUp: function (x, y, l) {
return {
x: 0,
y: y < l - 1 ? l - 1 : y + 1
};
},
diagonalUpBack: function (x, y, l) {
return {
x: l - 1,
y: x >= l - 1 ? y + 1 : y
};
}
};
/**
* calcOverlap - returns the overlap if the word can be fitted with the grid parameters provided.
* @param {string} word Word to be fitted.
* @param {Object[]} wordGrid Grid to which word needs to be fitted.
* @param {number} x Starting x cordinate.
* @param {nuber} y Starting y cordinate.
* @param {function} fnGetSquare Function to get the next grid pos as per the specified direction.
* @return {number} Overlap value if it can be fitted , -1 otherwise.
*/
const calcOverlap = function (word, wordGrid, x, y, fnGetSquare) {
let overlap = 0;
// traverse the squares to determine if the word fits
for (let index = 0 ; index < word.length; index++) {
const next = fnGetSquare(x, y, index);
const square = wordGrid[next.y][next.x];
if (square === word[index]) {
overlap++;
}
else if (square !== '') {
return -1;
}
}
return overlap;
};
/**
* findBestLocations - Find the best possible location for a word in the grid.
* @param {Object[]} wordGrid
* @param {Object} options
* @param {string} word
*/
const findBestLocations = function (wordGrid, options, word) {
const locations = [];
const height = options.height;
const width = options.width;
const wordLength = word.length;
let maxOverlap = 0;
options.orientations.forEach(function (orientation) {
const check = checkOrientations[orientation];
const next = orientations[orientation];
const skipTo = skipOrientations[orientation];
let x = 0;
let y = 0;
while (y < height) {
if (check(x, y, height, width, wordLength)) {
const overlap = calcOverlap(word, wordGrid, x, y, next);
if (overlap >= maxOverlap || (!options.preferOverlap && overlap > -1 )) {
maxOverlap = overlap;
locations.push({
x: x,
y: y,
orientation: orientation,
overlap: overlap
});
}
x++;
if ( x >= width) {
x = 0;
y++;
}
}
else {
const nextPossible = skipTo(x, y, wordLength);
x = nextPossible.x;
y = nextPossible.y;
}
}
});
return locations;
};
/**
* placeWordInGrid - find the best location and place the word.
* @param {Object[]} wordGrid
* @param {Object} options
* @param {string} word
*/
const placeWordInGrid = function (wordGrid, options, word) {
const locations = findBestLocations(wordGrid, options, word);
if (locations.length === 0) {
return false;
}
const selectedLoc = locations[Math.floor(Math.random() * locations.length)];
for (let index = 0; index < word.length; index++) {
const next = orientations[selectedLoc.orientation](selectedLoc.x, selectedLoc.y, index);
wordGrid[next.y][next.x] = word[index];
}
return true;
};
/**
* fillGrid - Create an empty grid and fill it with words.
* @param {Object[]} words Description.
* @param {Object} options Description.
* @return {Object[]|null} Grid array if all words can be fitted, else null.
*/
const fillGrid = function (words, options) {
const wordGrid = [];
for (let i = 0; i < options.height; i++) {
wordGrid[i] = [];
for (let j = 0; j < options.width; j++) {
wordGrid[i][j] = '';
}
}
for (const i in words) {
if (!placeWordInGrid(wordGrid, options, words[i])) {
return null;
}
}
return wordGrid;
};
/**
* fillBlanks - fill the unoccupied spaces with blanks.
* @param {Object[]} wordGrid
* @param {string} fillPool
* @return {Object[]} Resulting word grid.
*/
const fillBlanks = function (wordGrid, fillPool) {
for (let i = 0; i < wordGrid.length; i++) {
for (let j = 0;j < wordGrid[0].length; j++) {
if (!wordGrid[i][j]) {
const randomLetter = Math.floor(Math.random() * fillPool.length);
wordGrid[i][j] = fillPool[randomLetter];
}
}
}
return wordGrid;
};
/**
* calculateCordinates - function to calculate the cordinates & grid postions at which the event occured.
* @param {number} x X-cordinate of the event.
* @param {number} y Y-cordinate of the event.
* @param {number} elementSize Current element size.
* @return {Object[]} [normalized x, normalized y, row ,col].
*/
const calculateCordinates = function (x, y, elementSize) {
const row1 = Math.floor(x / elementSize);
const col1 = Math.floor(y / elementSize);
const x_click = row1 * elementSize + (elementSize / 2);
const y_click = col1 * elementSize + (elementSize / 2);
return [x_click, y_click, row1, col1];
};
/*
* function to process the line drawn to find if it is a valid marking
* in terms of possible grid directions
* returns directional value if it is a valid marking
* else return false
*/
/**
* getValidDirection - process the line drawn to find if it is a valid marking.
* @param {number} x1 Starting x cordinate.
* @param {number} y1 Starting y cordinate.
* @param {number} x2 Ending x cordinate.
* @param {number} y2 Ending y cordinate.
* @return {Object[]|boolean} Direction array if a valid marking, false otherwise.
*/
const getValidDirection = function (x1, y1, x2, y2) {
const dirx = (x2 > x1) ? 1 : ((x2 < x1) ? -1 : 0);
const diry = (y2 > y1) ? 1 : ((y2 < y1) ? -1 : 0);
let y = y1;
let x = x1;
if (dirx !== 0) {
while (x !== x2) {
x = x + dirx;
y = y + diry;
}
}
else {
while (y !== y2) {
y = y + diry;
}
}
if (y2 === y) {
return [dirx, diry];
}
else {
return false;
}
};
// All event handlers are registered here
/**
* mouseDownEventHandler.
* @param {Object} e Event Object.
* @param {HTMLelement} canvas Html5 canvas element.
* @param {number} elementSize Element size.
* @return {Object[]}
*/
const mouseDownEventHandler = function (e, canvas, elementSize) {
const x = e.pageX - $(canvas).offset().left;
const y = e.pageY - $(canvas).offset().top;
return calculateCordinates(x, y, elementSize);
};
/*
* event handler for handling mousemove events
* @private
*/
/**
* mouseMoveEventHandler.
* @param {Object} e Event Object.
* @param {HTMLelement} canvas Html5 Canvas Element.
* @param {Object[]} srcPos Position from which the movement started.
* @param {number} eSize Current element size.
*/
const mouseMoveEventHandler = function (e, canvas, srcPos, eSize) {
const offsetTop = ($(canvas).offset().top > eSize * 0.75) ? Math.floor(eSize * 0.75) : $(canvas).offset().top;
const desX = e.pageX - $(canvas).offset().left;
const desY = e.pageY - Math.abs(offsetTop);
const context = canvas.getContext('2d');
// Draw the current marking
context.clearRect(0, 0, context.canvas.width, context.canvas.height);
context.fillStyle = 'rgba(107,177,125,0.3)';
context.beginPath();
context.lineCap = 'round';
context.moveTo(srcPos[0] - (eSize / 8), srcPos[1] + (offsetTop / 8));
context.strokeStyle = 'rgba(107,177,125,0.4)';
context.lineWidth = Math.floor(eSize / 2);
context.lineTo(desX - (eSize / 8), desY + (offsetTop / 8));
context.stroke();
context.closePath();
};
/*
* event handler for handling mouseup events
* @private
*/
/**
* mouseUpEventHandler.
* @param {Object} e Event Object.
* @param {HTMLelement} canvas Html5 Canvas Element.
* @param {number} elementSize Current element size.
* @param {Object[]} clickStart Starting Event location.
* @return {Object} return staring,ending and direction of the current marking.
*/
const mouseUpEventHandler = function (e, canvas, elementSize, clickStart) {
let wordObject = {};
const offsetTop = ($(canvas).offset().top > elementSize * 0.75) ? Math.floor(elementSize * 0.75) * (-1) : $(canvas).offset().top;
const x = e.pageX - $(canvas).offset().left;
const y = e.pageY - Math.abs(offsetTop);
const clickEnd = calculateCordinates(x, y, elementSize);
const context = canvas.getContext('2d');
if ((Math.abs(clickEnd[0] - x) < 20) && (Math.abs(clickEnd[1] - y) < 15)) {
// Drag ended within permissible range
wordObject = {
'start': clickStart,
'end': clickEnd,
'dir': getValidDirection(clickStart[2], clickStart[3], clickEnd[2], clickEnd[3])
};
}
// Clear if there any markings started
context.closePath();
context.clearRect(0, 0, canvas.width, canvas.height);
return wordObject;
};
/**
* touchHandler - Mapping touchevents to corresponding mouse events.
* @param {Object} event Description.
*/
const touchHandler = function (event) {
const touches = event.changedTouches;
const first = touches[0];
const simulatedEvent = document.createEvent('MouseEvent');
let type = '';
switch (event.type) {
case 'touchstart':
type = 'mousedown';
break;
case 'touchmove':
type = 'mousemove';
break;
case 'touchend':
type = 'mouseup';
break;
default:
return;
}
// Created and fire a simulated mouse event
simulatedEvent.initMouseEvent(type, true, true, window, 1,
first.screenX, first.screenY,
first.clientX, first.clientY, false,
false, false, false, 0 /*left*/, null);
first.target.dispatchEvent(simulatedEvent);
event.preventDefault();
};
FindTheWords.WordGrid.prototype.createWordGrid = function () {
let wordGrid = null ;
let attempts = 0;
// sorting the words by length speedup the word fitting algorithm
const wordList = this.options.vocabulary.slice(0).sort(function (a, b) {
return (a.length < b.length);
});
while (!wordGrid) {
while (!wordGrid && attempts++ < this.options.maxAttempts) {
wordGrid = fillGrid(wordList, this.options);
}
// if grid cannot be formed in the current dimensions
if (!wordGrid) {
this.options.height++;
this.options.width++;
attempts = 0;
}
}
// fill in empty spaces with random letters
if (this.options.fillBlanks) {
wordGrid = fillBlanks(wordGrid, this.options.fillPool);
}
// set the output puzzle
this.wordGrid = wordGrid;
};
/**
* markWord - mark the word on the output canvas (permanent).
* @param {Object} wordParams
*/
FindTheWords.WordGrid.prototype.markWord = function (wordParams) {
const dirKey = wordParams['directionKey'];
const clickStart = wordParams['start'];
const clickEnd = wordParams['end'];
const context = this.$outputCanvas[0].getContext('2d');
const offsetTop = (this.$container.offset().top > this.elementSize * 0.75) ? Math.floor(this.elementSize * 0.75) * (-1) : this.$container.offset().top;
const topRadius = Math.floor(this.elementSize / 8);
const bottomRadius = Math.abs(Math.floor(offsetTop / 8));
const lineWidth = Math.floor(this.elementSize / 4);
let startingAngle;
// set the drawing property values
context.lineWidth = 2;
context.strokeStyle = 'rgba(107,177,125,0.9)';
context.fillStyle = 'rgba(107,177,125,0.3)';
if (!this.options.gridActive) {
context.strokeStyle = 'rgba(51, 102, 255,0.9)';
context.fillStyle = 'rgba(51, 102, 255,0.1)';
context.setLineDash([8, 4]);
}
// find the arc starting angle depending on the direction
switch (dirKey) {
case 'horizontal': {
startingAngle = (Math.PI / 2);
break;
}
case 'horizontalBack': {
startingAngle = -(Math.PI / 2);
break;
}
case 'diagonal': {
startingAngle = 3 * (Math.PI / 4);
break;
}
case 'diagonalBack': {
startingAngle = 5 * (Math.PI / 4);
break;
}
case 'diagonalUp': {
startingAngle = (Math.PI / 4);
break;
}
case 'diagonalUpBack': {
startingAngle = -(Math.PI / 4);
break;
}
case 'vertical': {
startingAngle = (Math.PI);
break;
}
case 'verticalUp': {
startingAngle = 0;
break;
}
}
// start drawing
context.beginPath();
context.arc(clickStart[0] - topRadius, clickStart[1] + bottomRadius, lineWidth, startingAngle, startingAngle + (Math.PI));
context.arc(clickEnd[0] - topRadius, clickEnd[1] + bottomRadius, lineWidth, startingAngle + (Math.PI), startingAngle + (2 * Math.PI));
context.closePath();
context.stroke();
context.fill();
};
/**
* mark - mark the words if they are not found.
* @param {Object[]} wordList
*/
FindTheWords.WordGrid.prototype.mark = function (wordList) {
const words = wordList;
const that = this;
const options = {
height: this.wordGrid.length,
width: this.wordGrid[0].length,
orientations: this.options.orientations,
preferOverlap: this.options.preferOverlap
};
const found = [];
const notFound = [];
words.forEach(function (word) {
const locations = findBestLocations(that.wordGrid, options, word);
if (locations.length > 0 && locations[0].overlap === word.length) {
locations[0].word = word;
found.push(locations[0]);
}
else {
notFound.push(word);
}
});
this.markSolution(found);
};
/**
* markSolution.
* @param {Object[]} solutions
*/
FindTheWords.WordGrid.prototype.markSolution = function (solutions) {
const that = this;
solutions.forEach(function (solution) {
const next = orientations[solution.orientation];
const word = solution.word;
const startX = solution.x;
const startY = solution.y;
const endPos = next(startX, startY, word.length - 1);
const clickStartX = startX * that.elementSize + (that.elementSize / 2);
const clickStartY = startY * that.elementSize + (that.elementSize / 2);
const clickEndX = endPos.x * that.elementSize + (that.elementSize / 2);
const clickEndY = endPos.y * that.elementSize + (that.elementSize / 2);
const wordParams = {
'start': [clickStartX, clickStartY, startX, startY],
'end': [clickEndX, clickEndY, endPos.x, endPos.y],
'directionKey': solution.orientation
};
that.markWord(wordParams);
});
};
/**
* disableGrid.
*/
FindTheWords.WordGrid.prototype.disableGrid = function () {
this.options.gridActive = false;
};
/**
* enableGrid.
*/
FindTheWords.WordGrid.prototype.enableGrid = function () {
this.options.gridActive = true;
};
/**
* appendTo - Placing the container for drawing the grid.
* @param {H5P.jQuery} $container
* @param {number} elementSize
*/
FindTheWords.WordGrid.prototype.appendTo = function ($container, elementSize) {
this.$container = $container;
this.canvasWidth = elementSize * this.wordGrid[0].length;
this.canvasHeight = elementSize * this.wordGrid.length;
this.elementSize = elementSize;
$container.css('height', this.canvasHeight);
$container.css('width', this.canvasWidth);
};
/**
* drawGrid - draw the letter on the canvas element provided.
* @param {number} margin Description.
*/
FindTheWords.WordGrid.prototype.drawGrid = function (margin) {
const that = this;
const marginResp = (Math.floor(that.elementSize / 8) < margin) ? (Math.floor(that.elementSize / 8)) : margin;
const offsetTop = (that.$container.offset().top > that.elementSize * 0.75) ? Math.floor(that.elementSize * 0.75) : that.$container.offset().top;
this.$gridCanvas = $('
').appendTo(that.$container);
this.$outputCanvas = $('
').appendTo(that.$container);
this.$drawingCanvas = $('
').appendTo(that.$container);
const ctx1 = this.$gridCanvas[0].getContext('2d');
const offset = that.$container.offset();
ctx1.clearRect(offset.left, offset.top, that.canvasWidth, that.canvasHeight);
ctx1.font = (that.elementSize / 3 ) + 'px sans-serif';
that.wordGrid.forEach(function (row, index1) {
row.forEach(function (element, index2) {
ctx1.fillText(element.toUpperCase(), index2 * that.elementSize + 2 * marginResp, index1 * that.elementSize + (offsetTop) );
});
});
let clickStart = [];
let isDragged = false;
let clickMode = false;
this.$container[0].addEventListener('keydown', function () {
//TODO: need to implement for a11y
}, false);
this.$drawingCanvas[0].addEventListener('touchstart', function (event) {
touchHandler(event);
}, false);
this.$drawingCanvas[0].addEventListener('touchmove', function (event) {
touchHandler(event);
}, false);
this.$drawingCanvas[0].addEventListener('touchend', function (event) {
touchHandler(event);
}, false);
this.$drawingCanvas.on('mousedown', function (event) {
if (that.options.gridActive) {
if (!clickMode) {
that.enableDrawing = true;
clickStart = mouseDownEventHandler(event, this, that.elementSize);
that.trigger('drawStart');
}
}
});
this.$drawingCanvas.on('mouseup', function (event) {
if (that.enableDrawing) {
if (isDragged || clickMode) {
if (clickMode) {
clickMode = false;
}
let markedWord = '';
const wordObject = mouseUpEventHandler(event, this, that.elementSize, clickStart);
const dict = {
'horizontal' : [1, 0],
'horizontalBack' : [-1, 0],
'diagonal' : [1, 1],
'diagonalBack' : [-1, 1],
'diagonalUp' : [1, -1],
'diagonalUpBack' : [-1, -1],
'vertical' : [0, 1],
'verticalUp' : [0, -1]
};
if (!$.isEmptyObject(wordObject) && wordObject['dir'] !== false) {
const dir = wordObject['dir'];
let y1 = wordObject['start'][3];
let x1 = wordObject['start'][2];
let x2 = wordObject['end'][2];
const y2 = wordObject['end'][3];
do {
markedWord += that.wordGrid[y1][x1];
x1 = x1 + dir[0];
y1 = y1 + dir[1];
} while (!((y1 === y2) && (x1 === x2)));
markedWord += that.wordGrid[y2][x2];
for (const key in dict) {
if (dict[key][0] === dir[0] && dict[key][1] === dir[1]) {
wordObject['directionKey'] = key;
break;
}
}
}
that.enableDrawing = false;
isDragged = false;
that.trigger('drawEnd', {'markedWord': markedWord, 'wordObject': wordObject});
}
else if (!clickMode) {
clickMode = true;
const offsetTop = (that.$container.offset().top > that.elementSize * 0.75) ? Math.floor(that.elementSize * 0.75) : that.$container.offset().top;
const context = that.$drawingCanvas[0].getContext('2d');
//drawing the dot on initial click
context.clearRect(0, 0, context.canvas.width, context.canvas.height);
context.lineWidth = Math.floor(that.elementSize / 2);
context.strokeStyle = 'rgba(107,177,125,0.9)';
context.fillStyle = 'rgba(107,177,125,0.3)';
context.beginPath();
context.arc(clickStart[0] - (that.elementSize / 8), clickStart[1] + Math.floor(offsetTop / 8), that.elementSize / 4, 0, 2 * Math.PI);
context.fill();
context.closePath();
}
}
});
this.$drawingCanvas.on('mousemove', function (event) {
if (that.enableDrawing ) {
isDragged = true;
mouseMoveEventHandler(event, this, clickStart, that.elementSize);
}
});
};
return FindTheWords.WordGrid;
}) (H5P.FindTheWords, H5P.EventDispatcher, H5P.jQuery);
;
(function (FindTheWords, EventDispatcher, $) {
/**
* Vocabulary - Handles the vocabulary part.
* @class H5P.FindTheWords.Vocabulary
* @param {Object} params
* @param {boolean} showVocabulary
*/
FindTheWords.Vocabulary = function (params, showVocabulary, header) {
/** @alias H5P.FindTheWords.Vocabulary# */
this.words = params;
this.header = header;
this.showVocabulary = showVocabulary;
this.wordsFound = [];
this.wordsNotFound = [];
this.wordsSolved = [];
};
FindTheWords.Vocabulary.prototype = Object.create(EventDispatcher.prototype);
FindTheWords.Vocabulary.prototype.constructor = FindTheWords.Vocabulary;
/**
* appendTo - appending vocabulary to the play area.
* @param {H5P.jQuery} $container
* @param {string} isModeBlock Either in inline/block mode.
*/
FindTheWords.Vocabulary.prototype.appendTo = function ($container, isModeBlock) {
let output = '
' +
this.header + '
';
this.words.forEach(function (element) {
const identifierName = element.replace(/ /g, '');
output += '\
' + element + '
';
});
output += '
';
$container.html(output);
$container.addClass('vocabulary-container');
this.$container = $container;
this.setMode(isModeBlock);
};
/**
* setMode - set the vocabularies.
* @param {string} mode
*/
FindTheWords.Vocabulary.prototype.setMode = function (isModeBlock) {
this.$container
.toggleClass('vocabulary-block-container', isModeBlock)
.toggleClass('vocabulary-inline-container', !isModeBlock);
};
/**
* checkWord - if the marked word belongs to the vocabulary as not found.
* @param {string} word
*/
FindTheWords.Vocabulary.prototype.checkWord = function (word) {
const reverse = word.split('').reverse().join('');
const originalWord = (this.words.indexOf(word) !== -1) ? word : ( this.words.indexOf(reverse) !== -1) ? reverse : null;
if (!originalWord || this.wordsFound.indexOf(originalWord) !== -1) {
return false;
}
this.wordsFound.push(originalWord);
if (this.showVocabulary) {
const idName = originalWord.replace(/ /g, '');
this.$container.find('#' + idName).addClass('word-found').attr('aria-label', idName + ' found');
}
return true;
};
/**
* reset - reset the vocabulary upon game resetting.
*/
FindTheWords.Vocabulary.prototype.reset = function () {
this.wordsFound = [];
this.wordsNotFound = this.words;
if (this.showVocabulary) {
this.$container.find('.word').each(function () {
$(this).removeClass('word-found').removeClass('word-solved').attr('aria-label', $(this).attr('id') + ' not found');
});
}
};
/**
* solveWords - changes on vocabulary upon showing the solution.
*/
FindTheWords.Vocabulary.prototype.solveWords = function () {
const that = this;
that.wordsSolved = that.wordsNotFound;
if (that.showVocabulary) {
that.wordsNotFound.forEach(function (word) {
const idName = word.replace(/ /g, '');
that.$container.find('#' + idName).addClass('word-solved').attr('aria-label', idName + ' solved');
});
}
};
/**
* getNotFound - return the list of words that are not found yet.
* @return {Object[]}
*/
FindTheWords.Vocabulary.prototype.getNotFound = function () {
const that = this;
this.wordsNotFound = this.words.filter(function (word) {
return (that.wordsFound.indexOf(word) === -1);
});
return this.wordsNotFound;
};
/**
* getFound - returns the words found so far.
* @return {Object[]}
*/
FindTheWords.Vocabulary.prototype.getFound = function () {
const that = this;
return this.words.filter(function (word) {
return (that.wordsFound.indexOf(word) !== -1);
});
};
/**
* getSolved - get the words solved by the game by show solution feature.
* @return {Object[]}
*/
FindTheWords.Vocabulary.prototype.getSolved = function () {
const that = this;
return this.words.filter(function (word) {
return (that.wordsSolved.indexOf(word) !== -1);
});
};
return FindTheWords.Vocabulary;
}) (H5P.FindTheWords, H5P.EventDispatcher, H5P.jQuery);
;
(function (FindTheWords, Timer) {
/**
* FindTheWords.Timer - Adapter between Find the words and H5P.Timer.
* @class H5P.FindTheWords.Timer
* @extends H5P.Timer
* @param {H5P.jQuery} $element
*/
FindTheWords.Timer = function ($element) {
/** @alias H5P.FindTheWords.Timer# */
const that = this;
// Initialize event inheritance
Timer.call(that, 100);
/** @private {string} */
const naturalState = '0:00';
/**
* update - Set up callback for time updates.
* Formats time stamp for humans.
*
* @private
*/
const update = function () {
const time = that.getTime();
const minutes = Timer.extractTimeElement(time, 'minutes');
let seconds = Timer.extractTimeElement(time, 'seconds') % 60;
if (seconds < 10) {
seconds = '0' + seconds;
}
$element.text(minutes + ':' + seconds);
};
// Setup default behavior
that.notify('every_tenth_second', update);
that.on('reset', function () {
$element.text(naturalState);
that.notify('every_tenth_second', update);
});
};
// Inheritance
FindTheWords.Timer.prototype = Object.create(Timer.prototype);
FindTheWords.Timer.prototype.constructor = FindTheWords.Timer;
}) (H5P.FindTheWords, H5P.Timer);
;
(function (FindTheWords) {
/**
* Keeps track of the number of times the game is submitted.
* @class H5P.FindTheWords.Counter
* @param {H5P.jQuery} $container
*/
FindTheWords.Counter = function ($container) {
/** @alias H5P.FindTheWords.Counter# */
var self = this;
var current = 0;
/**
* @private
*/
var update = function () {
$container[0].innerText = current;
};
/**
* Increment the counter.
*/
self.increment = function () {
current++;
update();
};
/**
* Revert counter back to its natural state
*/
self.reset = function () {
current = 0;
update();
};
};
})(H5P.FindTheWords);
;