%PDF- %PDF-
| Direktori : /opt/bitnami/apps/moodle/moodledata/filedir/7b/2a/ |
| Current File : /opt/bitnami/apps/moodle/moodledata/filedir/7b/2a/7b2ae7cc8a8ad7558c2c716160d8e44a15e1e33e |
H5P.Question = (function ($, EventDispatcher, JoubelUI) {
/**
* Extending this class make it alot easier to create tasks for other
* content types.
*
* @class H5P.Question
* @extends H5P.EventDispatcher
* @param {string} type
*/
function Question(type) {
var self = this;
// Inheritance
EventDispatcher.call(self);
// Register default section order
self.order = ['video', 'image', 'introduction', 'content', 'feedback', 'buttons', 'read'];
// Keep track of registered sections
var sections = {};
// Buttons
var buttons = {};
var buttonOrder = [];
// Wrapper when attached
var $wrapper;
// ScoreBar
var scoreBar;
// Keep track of the feedback's visual status.
var showFeedback;
// Keep track of which buttons are scheduled for hiding.
var buttonsToHide = [];
// Keep track of which buttons are scheduled for showing.
var buttonsToShow = [];
// Keep track of the hiding and showing of buttons.
var toggleButtonsTimer;
var toggleButtonsTransitionTimer;
var buttonTruncationTimer;
// Keeps track of initialization of question
var initialized = false;
/**
* @type {Object} behaviour Behaviour of Question
* @property {Boolean} behaviour.disableFeedback Set to true to disable feedback section
*/
var behaviour = {
disableFeedback: false,
disableReadSpeaker: false
};
// Keeps track of thumb state
var imageThumb = true;
// Keeps track of image transitions
var imageTransitionTimer;
// Keep track of whether sections is transitioning.
var sectionsIsTransitioning = false;
// Keep track of auto play state
var disableAutoPlay = false;
// Feedback transition timer
var feedbackTransitionTimer;
// Used when reading messages to the user
var $read, readText;
/**
* Register section with given content.
*
* @private
* @param {string} section ID of the section
* @param {(string|H5P.jQuery)} [content]
*/
var register = function (section, content) {
sections[section] = {};
var $e = sections[section].$element = $('<div/>', {
'class': 'h5p-question-' + section,
});
if (content) {
$e[content instanceof $ ? 'append' : 'html'](content);
}
};
/**
* Update registered section with content.
*
* @private
* @param {string} section ID of the section
* @param {(string|H5P.jQuery)} content
*/
var update = function (section, content) {
if (content instanceof $) {
sections[section].$element.html('').append(content);
}
else {
sections[section].$element.html(content);
}
};
/**
* Insert element with given ID into the DOM.
*
* @private
* @param {array|Array|string[]} order
* List with ordered element IDs
* @param {string} id
* ID of the element to be inserted
* @param {Object} elements
* Maps ID to the elements
* @param {H5P.jQuery} $container
* Parent container of the elements
*/
var insert = function (order, id, elements, $container) {
// Try to find an element id should be after
for (var i = 0; i < order.length; i++) {
if (order[i] === id) {
// Found our pos
while (i > 0 &&
(elements[order[i - 1]] === undefined ||
!elements[order[i - 1]].isVisible)) {
i--;
}
if (i === 0) {
// We are on top.
elements[id].$element.prependTo($container);
}
else {
// Add after element
elements[id].$element.insertAfter(elements[order[i - 1]].$element);
}
elements[id].isVisible = true;
break;
}
}
};
/**
* Set element max height, used for animations.
*
* @param {H5P.jQuery} $element
*/
var setElementHeight = function ($element) {
if (!$element.is(':visible')) {
// No animation
$element.css('max-height', 'none');
return;
}
// Get natural element height
var $tmp = $element.clone()
.css({
'position': 'absolute',
'max-height': 'none'
}).appendTo($element.parent());
// Apply height to element
var h = Math.round($tmp.get(0).getBoundingClientRect().height);
var fontSize = parseFloat($element.css('fontSize'));
var relativeH = h / fontSize;
$element.css('max-height', relativeH + 'em');
$tmp.remove();
if (h > 0 && sections.buttons && sections.buttons.$element === $element) {
// Make sure buttons section is visible
sections.buttons.$element.addClass('h5p-question-visible');
// Resize buttons after resizing button section
setTimeout(function () {
resizeButtons();
}, 150);
}
return h;
};
/**
* Does the actual job of hiding the buttons scheduled for hiding.
*
* @private
* @param {boolean} [relocateFocus] Find a new button to focus
*/
var hideButtons = function (relocateFocus) {
for (var i = 0; i < buttonsToHide.length; i++) {
hideButton(buttonsToHide[i].id);
}
buttonsToHide = [];
if (relocateFocus) {
self.focusButton();
}
};
/**
* Does the actual hiding.
* @private
* @param {string} buttonId
*/
var hideButton = function (buttonId) {
// Using detach() vs hide() makes it harder to cheat.
buttons[buttonId].$element.detach();
buttons[buttonId].isVisible = false;
};
/**
* Shows the buttons on the next tick. This is to avoid buttons flickering
* If they're both added and removed on the same tick.
*
* @private
*/
var toggleButtons = function () {
// Clear transition timer, reevaluate if buttons will be detached
clearTimeout(toggleButtonsTransitionTimer);
// Show buttons
for (var i = 0; i < buttonsToShow.length; i++) {
insert(buttonOrder, buttonsToShow[i].id, buttons, sections.buttons.$element);
buttons[buttonsToShow[i].id].isVisible = true;
}
buttonsToShow = [];
// Hide buttons
var numToHide = 0;
var relocateFocus = false;
for (var j = 0; j < buttonsToHide.length; j++) {
var button = buttons[buttonsToHide[j].id];
if (button.isVisible) {
numToHide += 1;
}
if (button.$element.is(':focus')) {
// Move focus to the first visible button.
relocateFocus = true;
}
}
if (sections.buttons && numToHide === sections.buttons.$element.children().length) {
// All buttons are going to be hidden. Hide container using transition.
sections.buttons.$element.removeClass('h5p-question-visible');
sections.buttons.$element.css('max-height', '');
sectionsIsTransitioning = true;
// Wait for animations before detaching buttons
toggleButtonsTransitionTimer = setTimeout(function () {
hideButtons(relocateFocus);
sectionsIsTransitioning = false;
}, 150);
}
else {
hideButtons(relocateFocus);
// Show button section
if (!sections.buttons.$element.is(':empty')) {
sections.buttons.$element.addClass('h5p-question-visible');
setElementHeight(sections.buttons.$element);
// Trigger resize after animation
toggleButtonsTransitionTimer = setTimeout(function () {
self.trigger('resize');
}, 150);
}
}
// Resize buttons to fit container
resizeButtons();
toggleButtonsTimer = undefined;
};
/**
* Allows for scaling of the question image.
*/
var scaleImage = function () {
var $imgSection = sections.image.$element;
clearTimeout(imageTransitionTimer);
// Add this here to avoid initial transition of the image making
// content overflow. Alternatively we need to trigger a resize.
$imgSection.addClass('animatable');
if (imageThumb) {
// Expand image
$(this).attr('aria-expanded', true);
$imgSection.addClass('h5p-question-image-fill-width');
imageThumb = false;
imageTransitionTimer = setTimeout(function () {
self.trigger('resize');
}, 600);
}
else {
// Scale down image
$(this).attr('aria-expanded', false);
$imgSection.removeClass('h5p-question-image-fill-width');
imageThumb = true;
imageTransitionTimer = setTimeout(function () {
self.trigger('resize');
}, 600);
}
};
/**
* Get scrollable ancestor of element
*
* @private
* @param {H5P.jQuery} $element
* @param {Number} [currDepth=0] Current recursive calls to ancestor, stop at maxDepth
* @param {Number} [maxDepth=5] Maximum depth for finding ancestor.
* @returns {H5P.jQuery} Parent element that is scrollable
*/
var findScrollableAncestor = function ($element, currDepth, maxDepth) {
if (!currDepth) {
currDepth = 0;
}
if (!maxDepth) {
maxDepth = 5;
}
// Check validation of element or if we have reached document root
if (!$element || !($element instanceof $) || document === $element.get(0) || currDepth >= maxDepth) {
return;
}
if ($element.css('overflow-y') === 'auto') {
return $element;
}
else {
return findScrollableAncestor($element.parent(), currDepth + 1, maxDepth);
}
};
/**
* Scroll to bottom of Question.
*
* @private
*/
var scrollToBottom = function () {
if (!$wrapper || ($wrapper.hasClass('h5p-standalone') && !H5P.isFullscreen)) {
return; // No scroll
}
var scrollableAncestor = findScrollableAncestor($wrapper);
// Scroll to bottom of scrollable ancestor
if (scrollableAncestor) {
scrollableAncestor.animate({
scrollTop: $wrapper.css('height')
}, "slow");
}
};
/**
* Resize buttons to fit container width
*
* @private
*/
var resizeButtons = function () {
if (!buttons || !sections.buttons) {
return;
}
// Clear button truncation timer if within a button truncation function
if (buttonTruncationTimer) {
clearTimeout(buttonTruncationTimer);
}
// Allow button section to attach before getting width
buttonTruncationTimer = setTimeout(function () {
// A static margin is added as buffer for smoother transitions
var buttonsWidth = 0;
for (var i in buttons) {
var $element = buttons[i].$element;
if (buttons[i].isVisible) {
//Calculate exact button width
var buttonInstanceWidth = $element.get(0).offsetWidth +
parseFloat($element.css('margin-left')) +
parseFloat($element.css('margin-right'));
buttonsWidth += Math.ceil(buttonInstanceWidth) + 1;
}
}
// Button section reduced by 1 pixel for cross-broswer consistency.
var buttonSectionWidth = Math.floor(sections.buttons.$element.get(0).offsetWidth) - 1;
// Remove button labels if width of buttons are too wide
if (buttonsWidth >= buttonSectionWidth) {
removeButtonLabels(buttonsWidth, buttonSectionWidth);
}
else {
restoreButtonLabels(buttonsWidth, buttonSectionWidth);
}
buttonTruncationTimer = undefined;
}, 0);
};
/**
* Remove button labels until they use less than max width.
*
* @private
* @param {Number} buttonsWidth Total width of all buttons
* @param {Number} maxButtonsWidth Max width allowed for buttons
*/
var removeButtonLabels = function (buttonsWidth, maxButtonsWidth) {
// Reverse traversal
for (var i = buttonOrder.length - 1; i >= 0; i--) {
var buttonId = buttonOrder[i];
if (!buttons[buttonId].isTruncated && buttons[buttonId].isVisible) {
var $button = buttons[buttonId].$element;
var $tmp = $button.clone()
.css({
'position': 'absolute',
'white-space': 'nowrap',
'max-width': 'none'
})
.addClass('truncated')
.html('')
.appendTo($button.parent());
// Calculate new total width of buttons
buttonsWidth = buttonsWidth - $button.outerWidth(true) + $tmp.outerWidth(true);
// Remove label
$button.html('');
$button.addClass('truncated');
buttons[buttonId].isTruncated = true;
$tmp.remove();
if (buttonsWidth < maxButtonsWidth) {
// Buttons are small enough.
return;
}
}
}
};
/**
* Restore button labels until it fills maximum possible width without exceeding the max width.
*
* @private
* @param {Number} buttonsWidth Total width of all buttons
* @param {Number} maxButtonsWidth Max width allowed for buttons
*/
var restoreButtonLabels = function (buttonsWidth, maxButtonsWidth) {
for (var i = 0; i < buttonOrder.length; i++) {
var buttonId = buttonOrder[i];
if (buttons[buttonId].isTruncated && buttons[buttonId].isVisible) {
// Check if adding label exceeds allowed width
var $button = buttons[buttonId].$element;
var $tmp = $button.clone()
.css({
'position': 'absolute',
'white-space': 'nowrap',
'max-width': 'none'
}).removeClass('truncated')
.html(buttons[buttonId].text)
.appendTo($button.parent());
// Make sure clone was successfull
if(!$button.length || !$tmp.length) {
return;
}
var oldButtonSize = Math.floor($button.get(0).offsetWidth) - 1;
var newButtonSize = Math.ceil($tmp.get(0).offsetWidth) + 1;
// Calculate new total width of buttons with a static pixel for consistency cross-browser
buttonsWidth = buttonsWidth - Math.floor(oldButtonSize) + Math.ceil(newButtonSize) + 1;
$tmp.remove();
if (buttonsWidth >= maxButtonsWidth) {
return;
}
// Restore label
$button.html(buttons[buttonId].text);
$button.removeClass('truncated');
buttons[buttonId].isTruncated = false;
}
}
};
/**
* Helper function for finding index of keyValue in array
*
* @param {String} keyValue Value to be found
* @param {String} key In key
* @param {Array} array In array
* @returns {number}
*/
var existsInArray = function (keyValue, key, array) {
var i;
for (i = 0; i < array.length; i++) {
if (array[i][key] === keyValue) {
return i;
}
}
return -1;
};
/**
* Set behaviour for question.
*
* @param {Object} options An object containing behaviour that will be extended by Question
*/
self.setBehaviour = function (options) {
$.extend(behaviour, options);
};
/**
* A video to display above the task.
*
* @param {object} params
*/
self.setVideo = function (params) {
sections.video = {
$element: $('<div/>', {
'class': 'h5p-question-video'
})
};
if (disableAutoPlay) {
params.params.autoplay = false;
}
// Never fit to wrapper
params.params.fit = false;
sections.video.instance = H5P.newRunnable(params, self.contentId, sections.video.$element, true);
var fromVideo = false; // Hack to avoid never ending loop
sections.video.instance.on('resize', function () {
fromVideo = true;
self.trigger('resize');
fromVideo = false;
});
self.on('resize', function () {
if (!fromVideo) {
sections.video.instance.trigger('resize');
}
});
return self;
};
/**
* Will stop any playback going on in the task.
*/
self.pause = function () {
if (sections.video && sections.video.isVisible) {
sections.video.instance.pause();
}
};
/**
* Start playback of video
*/
self.play = function () {
if (sections.video && sections.video.isVisible) {
sections.video.instance.play();
}
};
/**
* Disable auto play, useful in editors.
*/
self.disableAutoPlay = function () {
disableAutoPlay = true;
};
/**
* Add task image.
*
* @param {string} path Relative
* @param {Object} [options] Options object
* @param {string} [options.alt] Text representation
* @param {Boolean} [options.disableImageZooming] Set as true to disable image zooming
*/
self.setImage = function (path, options) {
options = options ? options : {};
sections.image = {};
// Image container
sections.image.$element = $('<div/>', {
'class': 'h5p-question-image h5p-question-image-fill-width'
});
// Inner wrap
var $imgWrap = $('<div/>', {
'class': 'h5p-question-image-wrap',
appendTo: sections.image.$element
});
// Image element
var $img = $('<img/>', {
src: H5P.getPath(path, self.contentId),
alt: (options.alt === undefined ? '' : options.alt),
on: {
load: function () {
self.trigger('imageLoaded', this);
self.trigger('resize');
}
},
appendTo: $imgWrap
});
// Disable image zooming
if (options.disableImageZooming) {
$img.css('maxHeight', 'none');
// Make sure we are using the correct amount of width at all times
var determineImgWidth = function () {
// Remove margins if natural image width is bigger than section width
var imageSectionWidth = sections.image.$element.get(0).getBoundingClientRect().width;
// Do not transition, for instant measurements
$imgWrap.css({
'-webkit-transition': 'none',
'transition': 'none'
});
// Margin as translateX on both sides of image.
var diffX = 2 * ($imgWrap.get(0).getBoundingClientRect().left -
sections.image.$element.get(0).getBoundingClientRect().left);
if ($img.get(0).naturalWidth >= imageSectionWidth - diffX) {
sections.image.$element.addClass('h5p-question-image-fill-width');
}
else { // Use margin for small res images
sections.image.$element.removeClass('h5p-question-image-fill-width');
}
// Reset transition rules
$imgWrap.css({
'-webkit-transition': '',
'transition': ''
});
};
// Determine image width
if ($img.is(':visible')) {
determineImgWidth();
}
else {
$img.load(function () {
determineImgWidth();
});
}
// Skip adding zoom functionality
return;
}
var sizeDetermined = false;
var determineSize = function () {
if (sizeDetermined || !$img.is(':visible')) {
return; // Try again next time.
}
$imgWrap.addClass('h5p-question-image-scalable')
.attr('aria-expanded', false)
.attr('role', 'button')
.attr('tabIndex', '0')
.on('click', function (event) {
if (event.which === 1) {
scaleImage.apply(this); // Left mouse button click
}
}).on('keypress', function (event) {
if (event.which === 32) {
scaleImage.apply(this); // Space bar pressed
}
});
sections.image.$element.removeClass('h5p-question-image-fill-width');
sizeDetermined = true; // Prevent any futher events
};
self.on('resize', determineSize);
return self;
};
/**
* Add the introduction section.
*
* @param {(string|H5P.jQuery)} content
*/
self.setIntroduction = function (content) {
register('introduction', content);
return self;
};
/**
* Add the content section.
*
* @param {(string|H5P.jQuery)} content
* @param {Object} [options]
* @param {string} [options.class]
*/
self.setContent = function (content, options) {
register('content', content);
if (options && options.class) {
sections.content.$element.addClass(options.class);
}
return self;
};
/**
* Force readspeaker to read text. Useful when you have to use
* setTimeout for animations.
*/
self.read = function (content) {
if (!$read) {
return; // Not ready yet
}
if (readText) {
// Combine texts if called multiple times
readText += (readText.substr(-1, 1) === '.' ? ' ' : '. ') + content
}
else {
readText = content;
}
// Set text
$read.html(readText);
setTimeout(function () {
// Stop combining when done reading
readText = null;
$read.html('');
}, 100);
};
/**
* Read feedback
*/
self.readFeedback = function () {
var invalidFeedback =
behaviour.disableReadSpeaker ||
!showFeedback ||
!sections.feedback ||
!sections.feedback.$element;
if (invalidFeedback) {
return;
}
var $feedbackText = $('.h5p-question-feedback-content-text', sections.feedback.$element);
if ($feedbackText && $feedbackText.html() && $feedbackText.html().length) {
self.read($feedbackText.html());
}
};
/**
* Set feedback message.
* Setting the message to blank or undefined will hide it again.
*
* @param {string} content
* @param {number} score The score
* @param {number} maxScore The maximum score for this question
* @param {string} [scoreBarLabel] Makes it easier for readspeakers to identify the scorebar
* @param {string} [helpText] Help text that describes the score inside a tip icon
*/
self.setFeedback = function (content, score, maxScore, scoreBarLabel, helpText) {
// Feedback is disabled
if (behaviour.disableFeedback) {
return self;
}
clearTimeout(feedbackTransitionTimer);
if (content) {
var $feedback = $('<div>', {
'class': 'h5p-question-feedback-container'
});
if (scoreBar === undefined) {
scoreBar = JoubelUI.createScoreBar(maxScore, scoreBarLabel);
}
scoreBar.appendTo($feedback);
scoreBar.setScore(score);
var $feedbackContent = $('<div>', {
'class': 'h5p-question-feedback-content'
}).appendTo($feedback);
// Feedback text
$('<div>', {
'class': 'h5p-question-feedback-content-text',
'html': content
}).appendTo($feedbackContent);
if (helpText) {
JoubelUI.createTip(helpText, {helpIcon: true})
.appendTo($feedbackContent);
}
// Feedback for readspeakers
if (!behaviour.disableReadSpeaker) {
self.read(content);
}
showFeedback = true;
if (sections.feedback) {
// Update section
update('feedback', $feedback);
}
else {
// Create section
register('feedback', $feedback);
if (initialized && $wrapper) {
insert(self.order, 'feedback', sections, $wrapper);
}
}
// Show feedback section
feedbackTransitionTimer = setTimeout(function () {
sections.feedback.$element.addClass('h5p-question-visible');
setElementHeight(sections.feedback.$element);
sectionsIsTransitioning = true;
// Scroll to bottom after showing feedback
scrollToBottom();
// Trigger resize after animation
feedbackTransitionTimer = setTimeout(function () {
sectionsIsTransitioning = false;
self.trigger('resize');
}, 150);
}, 0);
}
else if (sections.feedback && showFeedback) {
showFeedback = false;
// Hide feedback section
sections.feedback.$element.removeClass('h5p-question-visible');
sections.feedback.$element.css('max-height', '');
sectionsIsTransitioning = true;
// Detach after transition
feedbackTransitionTimer = setTimeout(function () {
// Avoiding Transition.onTransitionEnd since it will register multiple events, and there's no way to cancel it if the transition changes back to "show" while the animation is happening.
if (!showFeedback) {
sections.feedback.$element.children().detach();
// Trigger resize after animation
self.trigger('resize');
}
sectionsIsTransitioning = false;
scoreBar.setScore(0);
}, 150);
}
return self;
};
/**
* Set feedback content (no animation).
*
* @param {string} content
* @param {boolean} [extendContent] True will extend content, instead of replacing it
*/
self.updateFeedbackContent = function (content, extendContent) {
if (sections.feedback && sections.feedback.$element) {
if (extendContent) {
content = $('.h5p-question-feedback-content', sections.feedback.$element).html() + ' ' + content;
}
// Update feedback content html
$('.h5p-question-feedback-content', sections.feedback.$element).html(content);
}
return self;
};
/**
* Checks to see if button is registered.
*
* @param {string} id
* @returns {boolean}
*/
self.hasButton = function (id) {
return (buttons[id] !== undefined);
};
/**
* @typedef {Object} ConfirmationDialog
* @property {boolean} [enable] Must be true to show confirmation dialog
* @property {Object} [instance] Instance that uses confirmation dialog
* @property {jQuery} [$parentElement] Append to this element.
* @property {Object} [l10n] Translatable fields
* @property {string} [l10n.header] Header text
* @property {string} [l10n.body] Body text
* @property {string} [l10n.cancelLabel]
* @property {string} [l10n.confirmLabel]
*/
/**
* Register buttons for the task.
*
* @param {string} id
* @param {string} text label
* @param {function} clicked
* @param {boolean} [visible=true]
* @param {Object} [options] Options for button
* @param {Object} [extras] Extra options
* @param {ConfirmationDialog} [extras.confirmationDialog] Confirmation dialog
*/
self.addButton = function (id, text, clicked, visible, options, extras) {
if (buttons[id]) {
return self; // Already registered
}
if (sections.buttons === undefined) {
// We have buttons, register wrapper
register('buttons');
if (initialized) {
insert(self.order, 'buttons', sections, $wrapper);
}
}
extras = extras || {};
extras.confirmationDialog = extras.confirmationDialog || {};
options = options || {};
var confirmationDialog =
self.addConfirmationDialogToButton(extras.confirmationDialog, clicked);
/**
* Handle button clicks through both mouse and keyboard
* @private
*/
var handleButtonClick = function () {
if (extras.confirmationDialog.enable && confirmationDialog) {
// Show popups section if used
if (!extras.confirmationDialog.$parentElement) {
sections.popups.$element.removeClass('hidden');
}
confirmationDialog.show($e.position().top);
}
else {
clicked();
}
};
buttons[id] = {
isTruncated: false,
text: text
};
var $e = buttons[id].$element = JoubelUI.createButton($.extend({
'class': 'h5p-question-' + id,
html: text,
on: {
click: function (event) {
handleButtonClick();
if (options.href !== undefined) {
event.preventDefault();
}
},
keydown: function (event) {
switch (event.which) {
case 13: // Enter
case 32: // Space
handleButtonClick();
event.preventDefault();
}
}
}
}, options));
buttonOrder.push(id);
if (visible === undefined || visible) {
// Button should be visible
$e.appendTo(sections.buttons.$element);
buttons[id].isVisible = true;
sections.buttons.$element.addClass('h5p-question-visible');
}
return self;
};
/**
* Add confirmation dialog to button
* @param {ConfirmationDialog} options
* A confirmation dialog that will be shown before click handler of button
* is triggered
* @param {function} clicked
* Click handler of button
* @return {H5P.ConfirmationDialog|undefined}
* Confirmation dialog if enabled
*/
self.addConfirmationDialogToButton = function (options, clicked) {
options = options || {};
if (!options.enable) {
return;
}
// Confirmation dialog
var confirmationDialog = new H5P.ConfirmationDialog({
instance: options.instance,
headerText: options.l10n.header,
dialogText: options.l10n.body,
cancelText: options.l10n.cancelLabel,
confirmText: options.l10n.confirmLabel
});
// Determine parent element
if (options.$parentElement) {
confirmationDialog.appendTo(options.$parentElement.get(0));
}
else {
// Create popup section and append to that
if (sections.popups === undefined) {
register('popups');
if (initialized) {
insert(self.order, 'popups', sections, $wrapper);
}
sections.popups.$element.addClass('hidden');
self.order.push('popups');
}
confirmationDialog.appendTo(sections.popups.$element.get(0));
}
// Add event listeners
confirmationDialog.on('confirmed', function () {
if (!options.$parentElement) {
sections.popups.$element.addClass('hidden');
}
clicked();
// Trigger to content type
self.trigger('confirmed');
});
confirmationDialog.on('canceled', function () {
if (!options.$parentElement) {
sections.popups.$element.addClass('hidden');
}
// Trigger to content type
self.trigger('canceled');
});
return confirmationDialog;
};
/**
* Show registered button with given identifier.
*
* @param {string} id
* @param {Number} [priority]
*/
self.showButton = function (id, priority) {
if (buttons[id] === undefined) {
return self;
}
priority = priority || 0;
// Skip if already being shown
var indexToShow = existsInArray(id, 'id', buttonsToShow);
if (indexToShow !== -1) {
// Update priority
if (buttonsToShow[indexToShow].priority < priority) {
buttonsToShow[indexToShow].priority = priority;
}
return self;
}
// Check if button is going to be hidden on next tick
var exists = existsInArray(id, 'id', buttonsToHide);
if (exists !== -1) {
// Skip hiding if higher priority
if (buttonsToHide[exists].priority <= priority) {
buttonsToHide.splice(exists, 1);
buttonsToShow.push({id: id, priority: priority});
}
} // If button is not shown
else if (!buttons[id].$element.is(':visible')) {
// Show button on next tick
buttonsToShow.push({id: id, priority: priority});
}
if (!toggleButtonsTimer) {
toggleButtonsTimer = setTimeout(toggleButtons, 0);
}
return self;
};
/**
* Hide registered button with given identifier.
*
* @param {string} id
* @param {number} [priority]
*/
self.hideButton = function (id, priority) {
if (buttons[id] === undefined) {
return self;
}
priority = priority || 0;
// Skip if already being hidden
var indexToHide = existsInArray(id, 'id', buttonsToHide);
if (indexToHide !== -1) {
// Update priority
if (buttonsToHide[indexToHide].priority < priority) {
buttonsToHide[indexToHide].priority = priority;
}
return self;
}
// Check if buttons is going to be shown on next tick
var exists = existsInArray(id, 'id', buttonsToShow);
if (exists !== -1) {
// Skip showing if higher priority
if (buttonsToShow[exists].priority <= priority) {
buttonsToShow.splice(exists, 1);
buttonsToHide.push({id: id, priority: priority});
}
}
else if (!buttons[id].$element.is(':visible')) {
// Make sure it is detached in case the container is hidden.
hideButton(id);
}
else {
// Hide button on next tick.
buttonsToHide.push({id: id, priority: priority});
}
if (!toggleButtonsTimer) {
toggleButtonsTimer = setTimeout(toggleButtons, 0);
}
return self;
};
/**
* Set focus to the given button. If no button is given the first visible
* button gets focused. This is useful if you lose focus.
*
* @param {string} [id]
*/
self.focusButton = function (id) {
if (id === undefined) {
// Find first button that is visible.
for (var i = 0; i < buttonOrder.length; i++) {
var button = buttons[buttonOrder[i]];
if (button && button.isVisible) {
// Give that button focus
button.$element.focus();
break;
}
}
}
else if (buttons[id] && buttons[id].$element.is(':visible')) {
// Set focus to requested button
buttons[id].$element.focus();
}
return self;
};
/**
* Toggle readspeaker functionality
* @param {boolean} [disable] True to disable, false to enable.
*/
self.toggleReadSpeaker = function (disable) {
behaviour.disableReadSpeaker = disable || !behaviour.disableReadSpeaker;
};
/**
* Set new element for section.
*
* @param {String} id
* @param {H5P.jQuery} $element
*/
self.insertSectionAtElement = function (id, $element) {
if (sections[id] === undefined) {
register(id);
}
sections[id].parent = $element;
// Insert section if question is not initialized
if (!initialized) {
insert([id], id, sections, $element);
}
return self;
};
/**
* Attach content to given container.
*
* @param {H5P.jQuery} $container
*/
self.attach = function ($container) {
if (self.isRoot()) {
self.setActivityStarted();
}
// The first time we attach we also create our DOM elements.
if ($wrapper === undefined) {
if (self.registerDomElements !== undefined &&
(self.registerDomElements instanceof Function ||
typeof self.registerDomElements === 'function')) {
// Give the question type a chance to register before attaching
self.registerDomElements();
}
// Create section for reading messages
$read = $('<div/>', {
'aria-live': 'polite',
'class': 'h5p-hidden-read'
});
register('read', $read);
self.trigger('registerDomElements');
}
// Prepare container
$wrapper = $container;
$container.html('')
.addClass('h5p-question h5p-' + type);
// Add sections in given order
var $sections = [];
for (var i = 0; i < self.order.length; i++) {
var section = self.order[i];
if (sections[section]) {
if (sections[section].parent) {
// Section has a different parent
sections[section].$element.appendTo(sections[section].parent);
}
else {
$sections.push(sections[section].$element);
}
sections[section].isVisible = true;
}
}
// Only append once to DOM for optimal performance
$container.append($sections);
// Let others react to dom changes
self.trigger('domChanged', {
'$target': $container,
'library': self.libraryInfo.machineName,
'contentId': self.contentId,
'key': 'newLibrary'
}, {'bubbles': true, 'external': true});
// ??
initialized = true;
return self;
};
/**
* Detach all sections from their parents
*/
self.detachSections = function () {
// Deinit Question
initialized = false;
// Detach sections
for (var section in sections) {
sections[section].$element.detach();
}
return self;
};
// Listen for resize
self.on('resize', function () {
// Allow elements to attach and set their height before resizing
if (!sectionsIsTransitioning && sections.feedback && showFeedback) {
// Resize feedback to fit
setElementHeight(sections.feedback.$element);
}
resizeButtons();
});
}
// Inheritance
Question.prototype = Object.create(EventDispatcher.prototype);
Question.prototype.constructor = Question;
return Question;
})(H5P.jQuery, H5P.EventDispatcher, H5P.JoubelUI);