" ).appendTo( tooltip ),
id = tooltip.uniqueId().attr( "id" );
this._addClass( content, "ui-tooltip-content" );
this._addClass( tooltip, "ui-tooltip", "ui-widget ui-widget-content" );
tooltip.appendTo( this._appendTo( element ) );
return this.tooltips[ id ] = {
element: element,
tooltip: tooltip
};
},
_find: function( target ) {
var id = target.data( "ui-tooltip-id" );
return id ? this.tooltips[ id ] : null;
},
_removeTooltip: function( tooltip ) {
// Clear the interval for delayed tracking tooltips
clearInterval( this.delayedShow );
tooltip.remove();
delete this.tooltips[ tooltip.attr( "id" ) ];
},
_appendTo: function( target ) {
var element = target.closest( ".ui-front, dialog" );
if ( !element.length ) {
element = this.document[ 0 ].body;
}
return element;
},
_destroy: function() {
var that = this;
// Close open tooltips
$.each( this.tooltips, function( id, tooltipData ) {
// Delegate to close method to handle common cleanup
var event = $.Event( "blur" ),
element = tooltipData.element;
event.target = event.currentTarget = element[ 0 ];
that.close( event, true );
// Remove immediately; destroying an open tooltip doesn't use the
// hide animation
$( "#" + id ).remove();
// Restore the title
if ( element.data( "ui-tooltip-title" ) ) {
// If the title attribute has changed since open(), don't restore
if ( !element.attr( "title" ) ) {
element.attr( "title", element.data( "ui-tooltip-title" ) );
}
element.removeData( "ui-tooltip-title" );
}
} );
this.liveRegion.remove();
}
});
// DEPRECATED
// TODO: Switch return back to widget declaration at top of file when this is removed
if ( $.uiBackCompat !== false ) {
// Backcompat for tooltipClass option
$.widget( "ui.tooltip", $.ui.tooltip, {
options: {
tooltipClass: null
},
_tooltip: function() {
var tooltipData = this._superApply( arguments );
if ( this.options.tooltipClass ) {
tooltipData.tooltip.addClass( this.options.tooltipClass );
}
return tooltipData;
}
} );
}
var widgetsTooltip = $.ui.tooltip;
});
/*!
* jQuery UI Touch Punch 0.2.2
*
* Copyright 2011, Dave Furfero
* Dual licensed under the MIT or GPL Version 2 licenses.
*
* Depends:
* jquery.ui.widget.js
* jquery.ui.mouse.js
*/
(function ($) {
// Detect touch support
$.support.touch = ('ontouchend' in document || navigator.maxTouchPoints > 0);
// Ignore browsers without touch support
if (!$.support.touch) {
return;
}
var mouseProto = $.ui.mouse.prototype,
_mouseInit = mouseProto._mouseInit,
touchHandled;
/**
* Simulate a mouse event based on a corresponding touch event
* @param {Object} event A touch event
* @param {String} simulatedType The corresponding mouse event
*/
function simulateMouseEvent (event, simulatedType) {
// Ignore multi-touch events
if (event.originalEvent.touches.length > 1) {
return;
}
event.preventDefault();
var touch = event.originalEvent.changedTouches[0],
simulatedEvent = document.createEvent('MouseEvents');
// Initialize the simulated mouse event using the touch event's coordinates
simulatedEvent.initMouseEvent(
simulatedType, // type
true, // bubbles
true, // cancelable
window, // view
1, // detail
touch.screenX, // screenX
touch.screenY, // screenY
touch.clientX, // clientX
touch.clientY, // clientY
false, // ctrlKey
false, // altKey
false, // shiftKey
false, // metaKey
0, // button
null // relatedTarget
);
// Dispatch the simulated event to the target element
event.target.dispatchEvent(simulatedEvent);
}
/**
* Handle the jQuery UI widget's touchstart events
* @param {Object} event The widget element's touchstart event
*/
mouseProto._touchStart = function (event) {
var self = this;
// Ignore the event if another widget is already being handled
if (touchHandled || !self._mouseCapture(event.originalEvent.changedTouches[0])) {
return;
}
// Set the flag to prevent other widgets from inheriting the touch event
touchHandled = true;
// Track movement to determine if interaction was a click
self._touchMoved = false;
// Simulate the mouseover event
simulateMouseEvent(event, 'mouseover');
// Simulate the mousemove event
simulateMouseEvent(event, 'mousemove');
// Simulate the mousedown event
simulateMouseEvent(event, 'mousedown');
};
/**
* Handle the jQuery UI widget's touchmove events
* @param {Object} event The document's touchmove event
*/
mouseProto._touchMove = function (event) {
// Ignore event if not handled
if (!touchHandled) {
return;
}
// Interaction was not a click
this._touchMoved = true;
// Simulate the mousemove event
simulateMouseEvent(event, 'mousemove');
};
/**
* Handle the jQuery UI widget's touchend events
* @param {Object} event The document's touchend event
*/
mouseProto._touchEnd = function (event) {
// Ignore event if not handled
if (!touchHandled) {
return;
}
// Simulate the mouseup event
simulateMouseEvent(event, 'mouseup');
// Simulate the mouseout event
simulateMouseEvent(event, 'mouseout');
// If the touch interaction did not move, it should trigger a click
if (!this._touchMoved) {
// Simulate the click event
simulateMouseEvent(event, 'click');
}
// Unset the flag to allow other widgets to inherit the touch event
touchHandled = false;
};
/**
* A duck punch of the $.ui.mouse _mouseInit method to support touch events.
* This method extends the widget with bound touch event handlers that
* translate touch events to mouse events and pass them to the widget's
* original mouse event handling methods.
*/
mouseProto._mouseInit = function () {
var self = this;
// Delegate the touch handlers to the widget's element
self.element
.bind('touchstart', $.proxy(self, '_touchStart'))
.bind('touchmove', $.proxy(self, '_touchMove'))
.bind('touchend', $.proxy(self, '_touchEnd'));
// Call the original $.ui.mouse init method
_mouseInit.call(self);
};
})(jQuery);
(function (jQuery) {
// This is a hack to make ckeditor work inside modal dialogs. Since ckeditor dialogs are placed on body and not in the ui.dialog's DOM. See http://bugs.jqueryui.com/ticket/9087
jQuery.widget("ui.dialog", jQuery.ui.dialog, {
_allowInteraction: function (event) {
return true;
}
});
jQuery.ui.dialog.prototype._focusTabbable = function () {};
})(jQuery);
jQuery = oldJQuery;
;
(()=>{var e={97:(e,t,r)=>{"use strict";r.d(t,{A:()=>Y});var n=function(e){var t=e.length;return function r(){var n=Array.prototype.slice.call(arguments,0);return n.length>=t?e.apply(null,n):function(){var e=Array.prototype.slice.call(arguments,0);return r.apply(null,n.concat(e))}}},o=function(e,t){return t.substr(0,1)===e},a=function(e,t){return t.substr(-1)===e},i=n((function(e,t){return o(e,t)&&(t=t.slice(1)),a(e,t)&&(t=t.slice(0,-1)),t}));const s={curry:n,cleanCharacter:i,startsWith:o,endsWith:a,shuffle:function(e){for(var t=e.length;t>0;){var r=Math.floor(Math.random()*t),n=e[--t];e[t]=e[r],e[r]=n}return e},createElementWithTextPart:function(e){var t=document.createElement("span");return t.innerHTML=e,t}};var l=function(e){return e.split(/(\*.*?\*)/).filter((function(e){return e.length>0}))},c=function(e){var t=e.match(/(:([^\\*]+))/g),r=e.match(/(\\\+([^\\*:]+))/g),n=e.match(/(\\\-([^\\*:]+))/g),o=s.cleanCharacter("*",e);return t&&(o=o.replace(t,""),t=(t=t[0].replace(":","")).replace(/\s+$/,"")),r&&(o=o.replace(r,""),r=(r=r[0].replace("\\+","")).replace(/\s+$/,"")),n&&(o=o.replace(n,""),n=(n=n[0].replace("\\-","")).replace(/\s+$/,"")),{tip:t,correctFeedback:r,incorrectFeedback:n,text:o=o.replace(/\s+$/,"")}},h=h||{};h.DragText=h.DragText||{},h.DragText.StopWatch=function(){function e(){this.duration=0}return e.prototype.start=function(){return this.startTime=Date.now(),this},e.prototype.stop=function(){return this.duration=this.duration+Date.now()-this.startTime,this.passedTime()},e.prototype.reset=function(){this.duration=0,this.startTime=Date.now()},e.prototype.passedTime=function(){return Math.round(this.duration/10)/100},e}();const p=h.DragText.StopWatch;var d=function(e){return e.stopPropagation()};H5P.TextDraggable=function(e){var t="h5p-drag-dropped";function r(t,r,n){H5P.EventDispatcher.call(this);var o=this;o.text=t,o.insideDropzone=null,o.$draggable=e(r),o.$ariaLabel=o.$draggable.find(".h5p-hidden-read"),o.index=n,o.initialIndex=n,o.shortFormat=o.text,o.shortFormat.length>20&&!o.shortFormat.match(/\\\(.+\\\)|\\\[.+\\\]|\$\$.+\$\$/)&&(o.shortFormat=o.shortFormat.slice(0,17)+"..."),o.$draggable.on("touchstart",d),o.$draggable.on("touchmove",d),o.$draggable.on("touchend",d)}return r.prototype=Object.create(H5P.EventDispatcher.prototype),r.prototype.constructor=r,r.prototype.getIndex=function(){return this.index},r.prototype.setIndex=function(e){return this.index=e,this},r.prototype.getInitialIndex=function(){return this.initialIndex},r.prototype.hasInitialIndex=function(e){return this.initialIndex===e},r.prototype.appendDraggableTo=function(e){var t=this.$draggable[0]===document.activeElement;this.$draggable.detach().css({left:0,top:0}).appendTo(e),t&&this.$draggable.focus()},r.prototype.revertDraggableTo=function(e){var t=this.$draggable.offset().left-e.offset().left,r=this.$draggable.offset().top-e.offset().top;this.$draggable.detach().prependTo(e).css({left:t,top:r}).animate({left:0,top:0})},r.prototype.toggleDroppedFeedback=function(e){e?this.$draggable.addClass(t):this.$draggable.removeClass(t)},r.prototype.disableDraggable=function(){this.$draggable.draggable({disabled:!0})},r.prototype.enableDraggable=function(){this.$draggable.draggable({disabled:!1})},r.prototype.getDraggableElement=function(){return this.$draggable},r.prototype.updateAriaLabel=function(e){this.$ariaLabel.html(e)},r.prototype.updateAriaDescription=function(e){this.$draggable.attr("aria-description",e)},r.prototype.getElement=function(){return this.$draggable.get(0)},r.prototype.removeFromZone=function(){var e=this.insideDropzone;return null!==this.insideDropzone&&(this.insideDropzone.removeFeedback(),this.insideDropzone.removeDraggable()),this.toggleDroppedFeedback(!1),this.removeShortFormat(),this.updateAriaDescription(""),this.insideDropzone=null,e},r.prototype.addToZone=function(e){null!==this.insideDropzone&&this.insideDropzone.removeDraggable(),this.toggleDroppedFeedback(!0),this.insideDropzone=e,this.setShortFormat(),this.trigger("addedToZone")},r.prototype.getAnswerText=function(){return this.text},r.prototype.setShortFormat=function(){this.$draggable.html(this.shortFormat)},r.prototype.getShortFormat=function(){return this.shortFormat},r.prototype.removeShortFormat=function(){this.$draggable.html(this.text)},r.prototype.getInsideDropzone=function(){return this.insideDropzone},r.prototype.isInsideDropZone=function(){return!!this.insideDropzone},r}(H5P.jQuery);const g=H5P.TextDraggable;H5P.TextDroppable=function(e){var t="h5p-drag-correct-feedback",r="h5p-drag-wrong-feedback",n="h5p-drag-draggable-correct",o="h5p-drag-draggable-wrong";function a(t,r,n,o,a,i,s,l){var c=this;c.text=t,c.tip=r,c.correctFeedback=n,c.incorrectFeedback=o,c.index=s,c.params=l,c.containedDraggable=null,c.$dropzone=e(a),c.$dropzoneContainer=e(i),c.tip&&(c.$tip=H5P.JoubelUI.createTip(c.tip,{tipLabel:c.params.tipLabel,tabcontrol:!0}),c.$dropzoneContainer.append(c.$tip),c.$dropzone.focus((function(){return c.$tip.attr("tabindex","0")})),c.$dropzone.blur((function(){return c.removeTipTabIndexIfNoFocus()})),c.$tip.blur((function(){return c.removeTipTabIndexIfNoFocus()}))),c.$incorrectText=e("
",{html:c.params.incorrectText+" "+c.params.correctAnswer,class:"correct-answer"}),c.$correctText=e("
",{html:c.params.correctText,class:"correct-answer"}),c.$showSolution=e("
",{class:"h5p-drag-show-solution-container"}).appendTo(c.$dropzoneContainer).hide()}return a.prototype.removeTipTabIndexIfNoFocus=function(){var e=this;setTimeout((function(){e.$dropzone.is(":focus")||e.$tip.is(":focus")||e.$tip.attr("tabindex","-1")}),0)},a.prototype.showSolution=function(){var e=null!==this.containedDraggable&&this.containedDraggable.getAnswerText()===this.text;e||this.$showSolution.html(this.text),this.$showSolution.prepend(e?this.$correctText:this.$incorrectText),this.$showSolution.toggleClass("incorrect",!e),this.$showSolution.show()},a.prototype.hideSolution=function(){this.$showSolution.html(""),this.$showSolution.hide()},a.prototype.getElement=function(){return this.$dropzone.get(0)},a.prototype.appendDroppableTo=function(e){this.$dropzoneContainer.appendTo(e)},a.prototype.appendInsideDroppableTo=function(e){if(null!==this.containedDraggable)return this.containedDraggable.revertDraggableTo(e),this.containedDraggable},a.prototype.setDraggable=function(e){var t=this;t.containedDraggable!==e&&(null!==t.containedDraggable&&t.containedDraggable.removeFromZone(),t.containedDraggable=e,e.addToZone(t))},a.prototype.hasDraggable=function(){return!!this.containedDraggable},a.prototype.removeDraggable=function(){null!==this.containedDraggable&&(this.containedDraggable=null)},a.prototype.isCorrect=function(){return null!==this.containedDraggable&&this.containedDraggable.getAnswerText()===this.text},a.prototype.addFeedback=function(){this.isCorrect()?(this.$dropzone.removeClass(r).addClass(t),this.containedDraggable.getDraggableElement().removeClass(o).addClass(n)):null===this.containedDraggable?this.$dropzone.removeClass(r).removeClass(t):(this.$dropzone.removeClass(t).addClass(r),null!==this.containedDraggable&&this.containedDraggable.getDraggableElement().addClass(o).removeClass(n))},a.prototype.removeFeedback=function(){this.$dropzone.removeClass(r).removeClass(t),null!==this.containedDraggable&&this.containedDraggable.getDraggableElement().removeClass(o).removeClass(n)},a.prototype.hasFeedback=function(){return this.$dropzone.hasClass(r)||this.$dropzone.hasClass(t)},a.prototype.setShortFormat=function(){null!==this.containedDraggable&&this.containedDraggable.setShortFormat()},a.prototype.disableDropzoneAndContainedDraggable=function(){null!==this.containedDraggable&&this.containedDraggable.disableDraggable(),this.$dropzone.droppable({disabled:!0})},a.prototype.enableDropzone=function(){this.$dropzone.droppable({disabled:!1})},a.prototype.removeShortFormat=function(){null!==this.containedDraggable&&this.containedDraggable.removeShortFormat()},a.prototype.getDropzone=function(){return this.$dropzone},a.prototype.getIndex=function(){return this.index},a}(H5P.jQuery);const u=H5P.TextDroppable,b=function(e){const t=e.length;return function r(){const n=Array.prototype.slice.call(arguments,0);return n.length>=t?e.apply(null,n):function(){const e=Array.prototype.slice.call(arguments,0);return r.apply(null,n.concat(e))}}},m=(...e)=>e.reduce(((e,t)=>(...r)=>e(t(...r)))),f=b((function(e,t){t.forEach(e)})),v=(b((function(e,t){return t.map(e)})),b((function(e,t){return t.filter(e)}))),D=b((function(e,t){return t.some(e)})),y=b((function(e,t){return-1!=t.indexOf(e)})),E=b((function(e,t){return v((t=>!y(t,e)),t)})),x=b(((e,t)=>t.getAttribute(e))),w=b(((e,t,r)=>r.setAttribute(e,t))),T=b(((e,t)=>t.removeAttribute(e))),C=b(((e,t)=>t.hasAttribute(e))),A=b(((e,t,r)=>r.getAttribute(e)===t)),k=(b(((e,t)=>{const r=x(e,t);w(e,("true"!==r).toString(),t)})),b(((e,t)=>e.appendChild(t))),b(((e,t)=>t.querySelector(e))),b(((e,t)=>{return r=t.querySelectorAll(e),Array.prototype.slice.call(r);var r})),b(((e,t)=>e.removeChild(t))),b(((e,t)=>t.classList.contains(e))),b(((e,t)=>t.classList.add(e)))),F=b(((e,t)=>t.classList.remove(e))),$=k("hidden"),S=F("hidden"),I=(b(((e,t)=>(e?S:$)(t))),b(((e,t,r)=>{r.classList[t?"add":"remove"](e)})),T("tabindex")),P=(f(I),w("tabindex","0")),z=w("tabindex","-1"),B=C("tabindex");class L{constructor(e){Object.assign(this,{listeners:{},on:function(e,t,r){const n={listener:t,scope:r};return this.listeners[e]=this.listeners[e]||[],this.listeners[e].push(n),this},fire:function(e,t){return(this.listeners[e]||[]).every((function(e){return!1!==e.listener.call(e.scope||this,t)}))},propagate:function(e,t){let r=this;e.forEach((e=>t.on(e,(t=>r.fire(e,t)))))}}),this.plugins=e||[],this.elements=[],this.negativeTabIndexAllowed=!1,this.on("nextElement",this.nextElement,this),this.on("previousElement",this.previousElement,this),this.on("firstElement",this.firstElement,this),this.on("lastElement",this.lastElement,this),this.initPlugins()}addElement(e){this.elements.push(e),this.firesEvent("addElement",e),1===this.elements.length&&this.setTabbable(e)}insertElementAt(e,t){this.elements.splice(t,0,e),this.firesEvent("addElement",e),1===this.elements.length&&this.setTabbable(e)}removeElement(e){this.elements=E([e],this.elements),B(e)&&(this.setUntabbable(e),this.elements[0]&&this.setTabbable(this.elements[0])),this.firesEvent("removeElement",e)}count(){return this.elements.length}firesEvent(e,t){const r=this.elements.indexOf(t);return this.fire(e,{element:t,index:r,elements:this.elements,oldElement:this.tabbableElement})}nextElement({index:e}){const t=e===this.elements.length-1,r=this.elements[t?0:e+1];this.setTabbable(r),r.focus()}firstElement(){const e=this.elements[0];this.setTabbable(e),e.focus()}lastElement(){const e=this.elements[this.elements.length-1];this.setTabbable(e),e.focus()}setTabbableByIndex(e){const t=this.elements[e];t&&this.setTabbable(t)}setTabbable(e){f(this.setUntabbable.bind(this),this.elements),P(e),this.tabbableElement=e}setUntabbable(e){e!==document.activeElement&&(this.negativeTabIndexAllowed?z(e):I(e))}previousElement({index:e}){const t=0===e,r=this.elements[t?this.elements.length-1:e-1];this.setTabbable(r),r.focus()}useNegativeTabIndex(){this.negativeTabIndexAllowed=!0,this.elements.forEach((e=>{e.hasAttribute("tabindex")||z(e)}))}initPlugins(){this.plugins.forEach((function(e){void 0!==e.init&&e.init(this)}),this)}}const H="aria-grabbed",_=w(H),Z=A(H,"true"),W=v(C(H)),R=m(f(w(H,"false")),W),O=m(D(Z),W);class N{init(e){this.controls=e,this.controls.on("select",this.select,this)}addElement(e){_("false",e),this.controls.addElement(e)}setAllGrabbedToFalse(){R(this.controls.elements)}hasAnyGrabbed(){return O(this.controls.elements)}select({element:e}){const t=Z(e);this.setAllGrabbedToFalse(),t||_("true",e)}}const j="aria-dropeffect",K=w(j,"none"),M=w(j,"move"),X=v(C(j)),Q=m(f(M),X),U=m(f(K),X);class V{init(e){this.controls=e}setAllToMove(){Q(this.controls.elements)}setAllToNone(){U(this.controls.elements)}}V.DropEffect={COPY:"copy",MOVE:"move",EXECUTE:"execute",POPUP:"popup",NONE:"none"};class G{constructor(){this.selectability=!0}init(e){this.boundHandleKeyDown=this.handleKeyDown.bind(this),this.controls=e,this.controls.on("addElement",this.listenForKeyDown,this),this.controls.on("removeElement",this.removeKeyDownListener,this)}listenForKeyDown({element:e}){e.addEventListener("keydown",this.boundHandleKeyDown)}removeKeyDownListener({element:e}){e.removeEventListener("keydown",this.boundHandleKeyDown)}handleKeyDown(e){switch(e.which){case 27:this.close(e.target),e.preventDefault(),e.stopPropagation();break;case 35:this.lastElement(e.target),e.preventDefault(),e.stopPropagation();break;case 36:this.firstElement(e.target),e.preventDefault(),e.stopPropagation();break;case 13:case 32:this.select(e.target),e.preventDefault(),e.stopPropagation();break;case 37:case 38:this.hasChromevoxModifiers(e)||(this.previousElement(e.target),e.preventDefault(),e.stopPropagation());break;case 39:case 40:this.hasChromevoxModifiers(e)||(this.nextElement(e.target),e.preventDefault(),e.stopPropagation())}}hasChromevoxModifiers(e){return e.shiftKey||e.ctrlKey}previousElement(e){!1!==this.controls.firesEvent("beforePreviousElement",e)&&(this.controls.firesEvent("previousElement",e),this.controls.firesEvent("afterPreviousElement",e))}nextElement(e){!1!==this.controls.firesEvent("beforeNextElement",e)&&(this.controls.firesEvent("nextElement",e),this.controls.firesEvent("afterNextElement",e))}select(e){this.selectability&&!1!==this.controls.firesEvent("before-select",e)&&(this.controls.firesEvent("select",e),this.controls.firesEvent("after-select",e))}firstElement(e){!1!==this.controls.firesEvent("beforeFirstElement",e)&&(this.controls.firesEvent("firstElement",e),this.controls.firesEvent("afterFirstElement",e))}lastElement(e){!1!==this.controls.firesEvent("beforeLastElement",e)&&(this.controls.firesEvent("lastElement",e),this.controls.firesEvent("afterLastElement",e))}disableSelectability(){this.selectability=!1}enableSelectability(){this.selectability=!0}close(e){!1!==this.controls.firesEvent("before-close",e)&&(this.controls.firesEvent("close",e),this.controls.firesEvent("after-close",e))}}class q{constructor(){this.selectability=!0,this.handleClickBound=this.handleClick.bind(this),this.handleDragBound=this.handleDrag.bind(this)}init(e){this.controls=e,this.controls.on("addElement",this.listenForKeyDown,this),this.controls.on("removeElement",this.unlistenForKeyDown,this)}listenForKeyDown({element:e}){e.addEventListener("click",this.handleClickBound),e.addEventListener("drag",this.handleClickBound)}unlistenForKeyDown({element:e}){e.removeEventListener("click",this.handleClickBound),e.removeEventListener("drag",this.handleDragBound)}handleClick(e){this.controls.firesEvent("select",e.currentTarget)}handleDrag(e){this.controls.firesEvent("drag",e.currentTarget)}disableSelectability(){this.selectability=!1}enableSelectability(){this.selectability=!0}}H5P.DragText=function(e,t,r){var n="h5p-drag-wide-screen",o="h5p-drag-draggable-wide-screen";function a(r,n,o){var a=this;this.$=e(this),this.contentId=n,this.contentData=o,t.call(this,"drag-text"),this.params=e.extend(!0,{media:{},taskDescription:"Set in adjectives in the following sentence",textField:"This is a *nice*, *flexible* content type, which allows you to highlight all the *wonderful* words in this *exciting* sentence.\nThis is another line of *fantastic* text.",distractors:"",overallFeedback:[],checkAnswer:"Check",submitAnswer:"Submit",tryAgain:"Retry",behaviour:{enableRetry:!0,enableSolutionsButton:!0,enableCheckButton:!0,instantFeedback:!1},showSolution:"Show solution",dropZoneIndex:"Drop Zone @index.",empty:"Empty.",contains:"Drop Zone @index contains draggable @draggable.",ariaDraggableIndex:"@index of @count.",tipLabel:"Show tip",correctText:"Correct!",incorrectText:"Incorrect!",resetDropTitle:"Reset drop",resetDropDescription:"Are you sure you want to reset this drop zone?",grabbed:"Draggable is grabbed.",cancelledDragging:"Cancelled dragging.",correctAnswer:"Correct answer:",scoreBarLabel:"You got :num out of :total points",a11yCheck:"Check the answers. The responses will be marked as correct, incorrect, or unanswered.",a11yShowSolution:"Show the solution. The task will be marked with its correct solution.",a11yRetry:"Retry the task. Reset all responses and start the task over again."},r),this.contentData=o,void 0!==this.contentData&&void 0!==this.contentData.previousState&&void 0!==this.contentData.previousState.length&&(this.previousState=this.contentData.previousState),this.answered=!1,this.textFieldHtml=this.params.textField.replace(/(\r\n|\n|\r)/gm,"
"),this.distractorsHtml=this.params.distractors.replace(/(\r\n|\n|\r)/gm,"
"),this.introductionId="h5p-drag-text-"+n+"-introduction",this.selectedElement=void 0,this.ariaDragControls=new N,this.ariaDropControls=new V,this.dragControls=new L([new G,new q,this.ariaDragControls]),this.dragControls.useNegativeTabIndex(),this.dropControls=new L([new G,new q,this.ariaDropControls]),this.dropControls.useNegativeTabIndex(),this.dragControls.on("before-select",(function(e){return!a.isElementDisabled(e.element)})),this.dragControls.on("select",this.keyboardDraggableSelected,this),this.dropControls.on("select",this.keyboardDroppableSelected,this),this.on("start",this.addAllDroppablesToControls,this),this.on("revert",this.removeControlsFromEmptyDropZones,this),this.on("stop",(function(e){e.data.target||a.removeControlsFromDropZonesIfAllEmpty()}),this),this.on("drop",this.removeControlsFromEmptyDropZones,this),this.on("start",(function(e){var t=e.data.element,r=a.getDraggableByElement(t);a.toggleDropEffect(),t.setAttribute("aria-grabbed","true"),a.setDraggableAriaLabel(r)})),this.on("stop",(function(e){var t=e.data.element,r=a.getDraggableByElement(t);a.toggleDropEffect(),t.setAttribute("aria-grabbed","false"),a.setDraggableAriaLabel(r)})),this.on("drop",this.ariaDropControls.setAllToNone,this.ariaDropControls),this.on("drop",(function(e){this.dragControls.removeElement(e.data.element)}),this),this.on("revert",(function(e){this.dragControls.insertElementAt(e.data.element,0)}),this),this.on("drop",this.updateDroppableElement,this),this.on("revert",this.updateDroppableElement,this),this.initDragText(),this.stopWatch=new p,this.stopWatch.start(),this.on("resize",this.resize,this),this.on("revert",this.toggleDraggablesContainer,this),this.on("drop",this.toggleDraggablesContainer,this),this.on("stop",(function(e){e.data.target||a.read(a.params.cancelledDragging)})),this.params.behaviour.instantFeedback&&this.on("revert",(function(){return a.instantFeedbackEvaluation()}))}return a.prototype=Object.create(t.prototype),a.prototype.constructor=a,a.prototype.updateDroppableElement=function(e){var t=e.data.target,r=e.data.element,n=this.getDroppableByElement(t);t&&this.setDroppableLabel(t,r.textContent,n.getIndex())},a.prototype.removeControlsFromDropZonesIfAllEmpty=function(){this.anyDropZoneHasDraggable()||this.removeAllDroppablesFromControls()},a.prototype.removeControlsFromEmptyDropZones=function(){var e=this;this.droppables.filter((function(e){return!e.hasDraggable()})).map((function(e){return e.getElement()})).forEach((function(t){e.dropControls.removeElement(t)}))},a.prototype.addAllDroppablesToControls=function(){var e=this;this.dropControls.count()>0&&this.removeAllDroppablesFromControls(),this.droppables.map((function(e){return e.getElement()})).forEach((function(t){return e.dropControls.addElement(t)}))},a.prototype.removeAllDroppablesFromControls=function(){var e=this;this.droppables.map((function(e){return e.getElement()})).forEach((function(t){return e.dropControls.removeElement(t)}))},a.prototype.anyDropZoneHasDraggable=function(){return this.droppables.some((function(e){return e.hasDraggable()}))},a.prototype.setDroppableLabel=function(e,t,r){this.params.dropZoneIndex.replace("@index",r.toString());var n=e.classList.contains("h5p-drag-correct-feedback"),o=e.classList.contains("h5p-drag-wrong-feedback"),a=n||o,i=e.childNodes.length>0;if(e){var s;if(a){var l,c=this.getDroppableByElement(e);l=n?c.correctFeedback?c.correctFeedback:this.params.correctText:c.incorrectFeedback?c.incorrectFeedback:this.params.incorrectText,s="".concat(this.params.contains.replace("@index",r.toString()).replace("@draggable",t)," ").concat(l,"."),c&&c.containedDraggable&&c.containedDraggable.updateAriaDescription(n?this.params.correctText:this.params.incorrectText)}else s="".concat(i?this.params.contains.replace("@index",r.toString()).replace("@draggable",t):this.params.empty.replace("@index",r.toString()));e.setAttribute("aria-label",s)}},a.prototype.registerDomElements=function(){var t=this.params.media;if(t&&t.type&&t.type.library){var r=(t=t.type).library.split(" ")[0];"H5P.Image"===r?t.params.file&&this.setImage(t.params.file.path,{disableImageZooming:this.params.media.disableImageZooming||!1,alt:t.params.alt,title:t.params.title,expandImage:t.params.expandImage,minimizeImage:t.params.minimizeImage}):"H5P.Video"===r?t.params.sources&&this.setVideo(t):"H5P.Audio"===r&&t.params.files&&this.setAudio(t)}this.$introduction=e('
'+this.params.taskDescription+"
"),this.setIntroduction(this.$introduction),this.$introduction.parent().attr("tabindex","-1"),this.setContent(this.$inner),this.addButtons()},a.prototype.initDragText=function(){return this.$inner=e("
",{"aria-describedby":this.introductionId,class:"h5p-drag-inner"}),this.addTaskTo(this.$inner),this.setH5PUserState(),this.$inner},a.prototype.resize=function(){this.changeLayoutToFitWidth()},a.prototype.changeLayoutToFitWidth=function(){var e=this;if(e.addDropzoneWidth(),e.$inner.width()/parseFloat(e.$inner.css("font-size"),10)>43&&e.widestDraggable<=e.$inner.width()/3){e.$draggables.addClass(n);var t=document.activeElement;e.$wordContainer.detach().appendTo(e.$taskContainer),t!==document.activeElement&&t.focus(),e.draggables.forEach((function(e){e.getDraggableElement().addClass(o)})),e.$wordContainer.css({"margin-right":e.$draggables.width()})}else e.$wordContainer.css({"margin-right":0}),e.$draggables.removeClass(n),e.$draggables.detach().appendTo(e.$taskContainer),e.draggables.forEach((function(e){e.getDraggableElement().removeClass(o)}))},a.prototype.addButtons=function(){var e=this;e.params.behaviour.enableCheckButton&&e.addButton("check-answer",e.params.checkAnswer,(function(){e.answered=!0,e.removeAllElementsFromDragControl(),e.showEvaluation()?(e.hideButton("show-solution"),e.hideButton("try-again"),e.hideButton("check-answer")):(e.params.behaviour.enableRetry&&e.showButton("try-again"),e.params.behaviour.enableSolutionsButton&&e.showButton("show-solution"),e.hideButton("check-answer"),e.disableDraggables()),e.$introduction.parent().focus()}),!e.params.behaviour.instantFeedback,{"aria-label":e.params.a11yCheck},{contentData:e.contentData,textIfSubmitting:e.params.submitAnswer}),e.addButton("show-solution",e.params.showSolution,(function(){e.droppables.forEach((function(e){e.showSolution()})),e.draggables.forEach((function(t){return e.setDraggableAriaLabel(t)})),e.disableDraggables(),e.removeAllDroppablesFromControls(),e.hideButton("show-solution")}),e.initShowShowSolutionButton||!1,{"aria-label":e.params.a11yShowSolution}),e.addButton("try-again",e.params.tryAgain,(function(){e.resetTask(),e.read(e.params.taskDescription)}),e.initShowTryAgainButton||!1,{"aria-label":e.params.a11yRetry})},a.prototype.removeAllElementsFromDragControl=function(){var e=this;this.dragControls.elements.forEach((function(t){return e.dragControls.removeElement(t)}))},a.prototype.keyboardDraggableSelected=function(e){var t=this.selectedElement,r=void 0!==this.selectedElement,n=this.selectedElement===e.element;r&&(this.selectedElement=void 0,this.trigger("stop",{element:t})),r&&n||this.isElementDisabled(e.element)||(this.selectedElement=e.element,this.trigger("start",{element:e.element}),this.focusOnFirstEmptyDropZone())},a.prototype.focusOnFirstEmptyDropZone=function(){var e=this.droppables.filter((function(e){return!e.hasDraggable()}))[0].getElement();this.dropControls.setTabbable(e),e.focus()},a.prototype.isElementDisabled=function(e){return"true"===e.getAttribute("aria-disabled")},a.prototype.keyboardDroppableSelected=function(e){var t=this,r=e.element,n=t.getDroppableByElement(r),o=t.getDraggableByElement(this.selectedElement),a=this.params.behaviour.instantFeedback&&n&&n.isCorrect(),i=!this.params.behaviour.instantFeedback&&n.hasFeedback();if(o&&n&&!a){var s=t.selectedElement;t.drop(o,n),t.selectedElement=void 0,this.trigger("stop",{element:s,target:n.getElement()})}else if(n&&n.hasDraggable()&&!i&&!a){var l=r.querySelector("[aria-grabbed]");this.createConfirmResetDialog((function(){t.revert(t.getDraggableByElement(l))})).show()}},a.prototype.toggleDraggablesContainer=function(){var e=0===this.$draggables.children().length;this.$draggables.toggleClass("hide",e)},a.prototype.createConfirmResetDialog=function(e,t){var n=new r({headerText:this.params.resetDropTitle,dialogText:this.params.resetDropDescription});return n.appendTo(document.body),n.on("confirmed",e,t||this),n},a.prototype.showDropzoneFeedback=function(){var e=this;this.droppables.forEach((function(t){t.addFeedback();var r=t.containedDraggable;t&&r&&(e.setDroppableLabel(t.getElement(),r.getElement().textContent,t.getIndex()),e.setDraggableAriaLabel(r))}))},a.prototype.showExplanation=function(){var e=[];this.droppables.forEach((function(t){var r=t.containedDraggable;t&&r&&(t.isCorrect()&&t.correctFeedback&&e.push({correct:r.text,text:t.correctFeedback}),!t.isCorrect()&&t.incorrectFeedback&&e.push({correct:t.text,wrong:r.text,text:t.incorrectFeedback}))})),0!==e.length&&this.setExplanation(e,this.params.feedbackHeader)},a.prototype.showEvaluation=function(e){this.hideEvaluation(),this.showDropzoneFeedback(),this.showExplanation();var t=this.calculateScore(),r=this.droppables.length;if(!e){var n=this.createXAPIEventTemplate("answered");this.addQuestionToXAPI(n),this.addResponseToXAPI(n),this.trigger(n)}var o=H5P.Question.determineOverallFeedback(this.params.overallFeedback,t/r).replace(/@score/g,t.toString()).replace(/@total/g,r.toString());return t===r&&(this.hideButton("check-answer"),this.hideButton("show-solution"),this.hideButton("try-again"),this.disableDraggables()),this.trigger("resize"),this.setFeedback(o,t,r,this.params.scoreBarLabel),t===r},a.prototype.calculateScore=function(){return this.droppables.reduce((function(e,t){return e+(t.isCorrect()?1:0)}),0)},a.prototype.hideEvaluation=function(){this.removeFeedback(),this.trigger("resize")},a.prototype.hideExplanation=function(){this.setExplanation(),this.trigger("resize")},a.prototype.hideAllSolutions=function(){this.droppables.forEach((function(e){e.hideSolution()})),this.trigger("resize")},a.prototype.addTaskTo=function(t){var r=this;r.widest=0,r.widestDraggable=0,r.droppables=[],r.draggables=[],r.$taskContainer=e("
",{class:"h5p-drag-task"}),r.$draggables=e("
",{class:"h5p-drag-draggables-container"}),r.$wordContainer=e("
",{class:"h5p-drag-droppable-words"}),l(r.textFieldHtml).forEach((function(e){if(r.isAnswerPart(e)){var t=c(e);r.createDraggable(t.text),r.createDroppable(t.text,t.tip,t.correctFeedback,t.incorrectFeedback)}else{var n=s.createElementWithTextPart(e);r.$wordContainer.append(n)}})),l(r.distractorsHtml).forEach((function(e){""!==e.trim()&&"*"===e.substring(0,1)&&"*"===e.substring(e.length-1,e.length)&&(e=c(e),r.createDraggable(e.text))})),r.shuffleAndAddDraggables(r.$draggables),r.$draggables.appendTo(r.$taskContainer),r.$wordContainer.appendTo(r.$taskContainer),r.$taskContainer.appendTo(t),r.addDropzoneWidth()},a.prototype.isAnswerPart=function(e){return s.startsWith("*",e)&&s.endsWith("*",e)},a.prototype.addDropzoneWidth=function(){var e=this,t=0,r=0,n=parseInt(this.$inner.css("font-size"),10),o=3*n,a=n;this.draggables.forEach((function(e){var n=e.getDraggableElement(),o=n.clone().css({position:"absolute","white-space":"nowrap",width:"auto",padding:0,margin:0}).html(e.getAnswerText()).appendTo(n.parent()),i=o.outerWidth();r=i>r?i:r,o.text().length>=20&&(o.html(e.getShortFormat()),i=o.width()),i+a>t&&(t=i+a),o.remove()})),t
",{html:"".concat(t," "),role:"button","aria-grabbed":"false",tabindex:"-1"}).draggable({revert:function(e){return e||r.revert(o),!1},drag:r.propagateDragEvent("drag",r),start:r.propagateDragEvent("start",r),stop:function(e){r.trigger("stop",{element:o.getElement(),target:e.target})},containment:r.$taskContainer}).append(e("",{class:"h5p-hidden-read"})),o=new g(t,n,r.draggables.length);return o.on("addedToZone",(function(){r.triggerXAPI("interacted")})),r.draggables.push(o),o},a.prototype.createDroppable=function(t,r,n,o){var a=this,i=this.draggables.length,s=e("
",{class:"h5p-drag-dropzone-container"}),l=e("
",{"aria-dropeffect":"none","aria-label":this.params.dropZoneIndex.replace("@index",i.toString())+" "+this.params.empty.replace("@index",i.toString()),tabindex:"-1"}).appendTo(s).droppable({tolerance:"pointer",drop:function(e,t){var r=a.getDraggableByElement(t.draggable[0]),n=a.getDroppableByElement(e.target);r&&n&&a.drop(r,n)}}),c=new u(t,r,n,o,l,s,i,a.params);return c.appendDroppableTo(a.$wordContainer),a.droppables.push(c),c},a.prototype.propagateDragEvent=s.curry((function(e,t,r){t.trigger(e,{element:r.target})})),a.prototype.revert=function(e){var t=e.removeFromZone(),r=t?t.getElement():void 0;e.revertDraggableTo(this.$draggables),this.setDraggableAriaLabel(e),this.trigger("revert",{element:e.getElement(),target:r}),this.trigger("resize")},a.prototype.drop=function(e,t){var r=this;r.answered=!0,e.removeFromZone();var n=t.appendInsideDroppableTo(this.$draggables);n&&r.trigger("revert",{element:n.getElement(),target:t.getElement()}),t.setDraggable(e),e.appendDraggableTo(t.getDropzone()),r.params.behaviour.instantFeedback&&(t.addFeedback(),r.instantFeedbackEvaluation(),r.params.behaviour.enableRetry&&!t.isCorrect()||t.disableDropzoneAndContainedDraggable()),this.trigger("drop",{element:e.getElement(),target:t.getElement()}),this.trigger("resize")},a.prototype.shuffleAndAddDraggables=function(e){var t=this;return s.shuffle(this.draggables).map((function(e,t){return e.setIndex(t)})).map((function(r){return t.addDraggableToContainer(e,r)})).map((function(e){return t.setDraggableAriaLabel(e)})).map((function(e){return t.addDraggableToControls(t.dragControls,e)}))},a.prototype.setDraggableAriaLabel=function(e){return e.updateAriaLabel(this.params.ariaDraggableIndex.replace("@index",(e.getIndex()+1).toString()).replace("@count",this.draggables.length.toString())),e},a.prototype.isGrabbed=function(e){return"true"===e.getAttribute("aria-grabbed")},a.prototype.addDraggableToContainer=function(e,t){return t.appendDraggableTo(e),t},a.prototype.addDraggableToControls=function(e,t){return e.addElement(t.getElement()),t},a.prototype.instantFeedbackEvaluation=function(){var e=this;e.isAllAnswersFilled()?(e.params.behaviour.enableSolutionsButton&&e.showButton("show-solution"),e.params.behaviour.enableRetry&&e.showButton("try-again"),e.showEvaluation()):(e.hideButton("try-again"),e.hideButton("show-solution"),e.hideEvaluation())},a.prototype.isAllAnswersFilled=function(){return this.droppables.every((function(e){return e.hasDraggable()}))},a.prototype.enableAllDropzonesAndDraggables=function(){this.enableDraggables(),this.droppables.forEach((function(e){e.enableDropzone()}))},a.prototype.disableDraggables=function(){this.draggables.forEach((function(e){e.disableDraggable()}))},a.prototype.enableDraggables=function(){this.draggables.forEach((function(e){e.enableDraggable()}))},a.prototype.getAnswerGiven=function(){return this.answered},a.prototype.getScore=function(){return this.calculateScore()},a.prototype.getMaxScore=function(){return this.droppables.length},a.prototype.getTitle=function(){return H5P.createTitle(this.contentData&&this.contentData.metadata&&this.contentData.metadata.title?this.contentData.metadata.title:"Drag the Words")},a.prototype.toggleDropEffect=function(){var e=void 0!==this.selectedElement;this.ariaDropControls[e?"setAllToMove":"setAllToNone"]()},a.prototype.getDraggableByElement=function(e){return this.draggables.filter((function(t){return t.$draggable.get(0)===e}),this)[0]},a.prototype.getDroppableByElement=function(e){return this.droppables.filter((function(t){return t.$dropzone.get(0)===e}),this)[0]},a.prototype.showSolutions=function(){this.showEvaluation(!0),this.droppables.forEach((function(e){e.addFeedback(),e.showSolution()})),this.removeAllDroppablesFromControls(),this.disableDraggables(),this.hideButton("try-again"),this.hideButton("show-solution"),this.hideButton("check-answer"),this.trigger("resize")},a.prototype.resetTask=function(){var e=this;e.answered=!1,e.resetDraggables(),e.hideEvaluation(),e.hideExplanation(),e.enableAllDropzonesAndDraggables(),e.hideButton("try-again"),e.hideButton("show-solution"),e.params.behaviour.instantFeedback||e.showButton("check-answer"),e.hideAllSolutions(),e.stopWatch.reset(),this.trigger("resize")},a.prototype.resetDraggables=function(){s.shuffle(this.draggables).forEach(this.revert,this)},a.prototype.getCurrentState=function(){var e=this;if(void 0!==this.draggables)return this.draggables.filter((function(e){return null!==e.getInsideDropzone()})).map((function(t){return{draggable:t.getInitialIndex(),droppable:e.droppables.indexOf(t.getInsideDropzone())}}))},a.prototype.setH5PUserState=function(){var e=this,t=this;void 0!==this.previousState&&(this.previousState.forEach((function(r){if(!t.isValidIndex(r.draggable)||!t.isValidIndex(r.droppable))throw new Error("Stored user state is invalid");var n=e.getDraggableByInitialIndex(r.draggable),o=t.droppables[r.droppable];t.drop(n,o),t.params.behaviour.instantFeedback&&(null!==o&&o.addFeedback(),o.isCorrect()&&o.disableDropzoneAndContainedDraggable())})),t.params.behaviour.instantFeedback&&t.isAllAnswersFilled()&&!t.showEvaluation()&&(t.params.behaviour.enableSolutionsButton&&(t.initShowShowSolutionButton=!0),t.params.behaviour.enableRetry&&(t.initShowTryAgainButton=!0)))},a.prototype.isValidIndex=function(e){return!isNaN(e)&&e=0},a.prototype.getDraggableByInitialIndex=function(e){return this.draggables.filter((function(t){return t.hasInitialIndex(e)}))[0]},a.prototype.getXAPIData=function(){var e=this.createXAPIEventTemplate("answered");return this.addQuestionToXAPI(e),this.addResponseToXAPI(e),{statement:e.data.statement}},a.prototype.addQuestionToXAPI=function(t){var r=t.getVerifiedStatementValue(["object","definition"]);e.extend(r,this.getxAPIDefinition())},a.prototype.getxAPIDefinition=function(){var e={interactionType:"fill-in",type:"http://adlnet.gov/expapi/activities/cmi.interaction"},t=this.textFieldHtml.replaceAll(/_{10,}/gi,"_________"),r=this.params.taskDescription.replaceAll(/_{10,}/gi,"_________")+" ";return e.description={"en-US":r+this.replaceSolutionsWithBlanks(t)},e.correctResponsesPattern=[this.getSolutionsFromQuestion(t)],e},a.prototype.addResponseToXAPI=function(e){var t,r=this,n=r.getScore(),o=r.droppables.length;e.setScoredResult(n,o,r);var a={min:0,raw:n,max:o,scaled:Math.round(n/o*1e4)/1e4};r.stopWatch&&(t="PT"+r.stopWatch.stop()+"S"),e.data.statement.result={response:r.getXAPIResponse(),score:a,duration:t,completion:!0}},a.prototype.getXAPIResponse=function(){return this.droppables.map((function(e){return e.hasDraggable()?e.containedDraggable.text:""})).join("[,]")},a.prototype.replaceSolutionsWithBlanks=function(e){var t=this;return l(e).map((function(e){return t.isAnswerPart(e)?"__________":e})).join("")},a.prototype.getSolutionsFromQuestion=function(e){return l(e).filter(this.isAnswerPart).map((function(e){return c(e)})).map((function(e){return e.text})).join("[,]")},a.prototype.parseText=function(e){return l(e)},a}(H5P.jQuery,H5P.Question,H5P.ConfirmationDialog),H5P.DragText.parseText=function(e){return l(e).map((function(e){return function(e){return s.startsWith("*",e)&&s.endsWith("*",e)}(e)?{type:"answer",correct:c(e).text}:{type:"text",content:e}}))};const Y=H5P.DragText},477:(e,t,r)=>{"use strict";r.r(t)}},t={};function r(n){var o=t[n];if(void 0!==o)return o.exports;var a=t[n]={exports:{}};return e[n](a,a.exports,r),a.exports}r.d=(e,t)=>{for(var n in t)r.o(t,n)&&!r.o(e,n)&&Object.defineProperty(e,n,{enumerable:!0,get:t[n]})},r.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),r.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},r(477),H5P=H5P||{},H5P.DragText=r(97).A})();;
/**
* @class
* @classdesc Keyboard navigation for accessibility support
* @extends H5P.EventDispatcher
*/
H5P.KeyboardNav = (function (EventDispatcher) {
/**
* Construct a new KeyboardNav
* @constructor
*/
function KeyboardNav() {
EventDispatcher.call(this);
/** @member {boolean} */
this.selectability = true;
/** @member {HTMLElement[]|EventTarget[]} */
this.elements = [];
}
KeyboardNav.prototype = Object.create(EventDispatcher.prototype);
KeyboardNav.prototype.constructor = KeyboardNav;
/**
* Adds a new element to navigation
*
* @param {HTMLElement} el The element
* @public
*/
KeyboardNav.prototype.addElement = function(el){
const keyDown = this.handleKeyDown.bind(this);
const onClick = this.onClick.bind(this);
el.addEventListener('keydown', keyDown);
el.addEventListener('click', onClick);
// add to array to navigate over
this.elements.push({
el: el,
keyDown: keyDown,
onClick: onClick,
});
if(this.elements.length === 1){ // if first
this.setTabbableAt(0);
}
};
/**
* Select the previous element in the list. Select the last element,
* if the current element is the first element in the list.
*
* @param {Number} index The index of currently selected element
* @public
* @fires KeyboardNav#previousOption
*/
KeyboardNav.prototype.previousOption = function (index) {
var isFirstElement = index === 0;
if (isFirstElement) {
return;
}
this.focusOnElementAt(isFirstElement ? (this.elements.length - 1) : (index - 1));
/**
* Previous option event
*
* @event KeyboardNav#previousOption
* @type KeyboardNavigationEventData
*/
this.trigger('previousOption', this.createEventPayload(index));
};
/**
* Select the next element in the list. Select the first element,
* if the current element is the first element in the list.
*
* @param {Number} index The index of the currently selected element
* @public
* @fires KeyboardNav#previousOption
*/
KeyboardNav.prototype.nextOption = function (index) {
var isLastElement = index === this.elements.length - 1;
if (isLastElement) {
return;
}
this.focusOnElementAt(isLastElement ? 0 : (index + 1));
/**
* Previous option event
*
* @event KeyboardNav#nextOption
* @type KeyboardNavigationEventData
*/
this.trigger('nextOption', this.createEventPayload(index));
};
/**
* Focus on an element by index
*
* @param {Number} index The index of the element to focus on
* @public
*/
KeyboardNav.prototype.focusOnElementAt = function (index) {
this.setTabbableAt(index);
this.getElements()[index].focus();
};
/**
* Disable possibility to select a word trough click and space or enter
*
* @public
*/
KeyboardNav.prototype.disableSelectability = function () {
this.elements.forEach(function (el) {
el.el.removeEventListener('keydown', el.keyDown);
el.el.removeEventListener('click', el.onClick);
}.bind(this));
this.selectability = false;
};
/**
* Enable possibility to select a word trough click and space or enter
*
* @public
*/
KeyboardNav.prototype.enableSelectability = function () {
this.elements.forEach(function (el) {
el.el.addEventListener('keydown', el.keyDown);
el.el.addEventListener('click', el.onClick);
}.bind(this));
this.selectability = true;
};
/**
* Sets tabbable on a single element in the list, by index
* Also removes tabbable from all other elements in the list
*
* @param {Number} index The index of the element to set tabbale on
* @public
*/
KeyboardNav.prototype.setTabbableAt = function (index) {
this.removeAllTabbable();
this.getElements()[index].setAttribute('tabindex', '0');
};
/**
* Remove tabbable from all entries
*
* @public
*/
KeyboardNav.prototype.removeAllTabbable = function () {
this.elements.forEach(function(el){
el.el.removeAttribute('tabindex');
});
};
/**
* Toggles 'aria-selected' on an element, if selectability == true
*
* @param {EventTarget|HTMLElement} el The element to select/unselect
* @private
* @fires KeyboardNav#select
*/
KeyboardNav.prototype.toggleSelect = function(el){
if(this.selectability) {
// toggle selection
el.setAttribute('aria-selected', !isElementSelected(el));
// focus current
el.setAttribute('tabindex', '0');
el.focus();
var index = this.getElements().indexOf(el);
/**
* Previous option event
*
* @event KeyboardNav#select
* @type KeyboardNavigationEventData
*/
this.trigger('select', this.createEventPayload(index));
}
};
/**
* Handles key down
*
* @param {KeyboardEvent} event Keyboard event
* @private
*/
KeyboardNav.prototype.handleKeyDown = function(event){
var index;
switch (event.which) {
case 13: // Enter
case 32: // Space
// Select
this.toggleSelect(event.target);
event.preventDefault();
break;
case 37: // Left Arrow
case 38: // Up Arrow
// Go to previous Option
index = this.getElements().indexOf(event.currentTarget);
this.previousOption(index);
event.preventDefault();
break;
case 39: // Right Arrow
case 40: // Down Arrow
// Go to next Option
index = this.getElements().indexOf(event.currentTarget);
this.nextOption(index);
event.preventDefault();
break;
}
};
/**
* Get only elements from elements array
* @returns {Array}
*/
KeyboardNav.prototype.getElements = function () {
return this.elements.map(function (el) {
return el.el;
});
};
/**
* Handles element click. Toggles 'aria-selected' on element
*
* @param {MouseEvent} event Mouse click event
* @private
*/
KeyboardNav.prototype.onClick = function(event){
this.toggleSelect(event.currentTarget);
};
/**
* Creates a paylod for event that is fired
*
* @param {Number} index
* @return {KeyboardNavigationEventData}
*/
KeyboardNav.prototype.createEventPayload = function(index){
/**
* Data that is passed along as the event parameter
*
* @typedef {Object} KeyboardNavigationEventData
* @property {HTMLElement} element
* @property {number} index
* @property {boolean} selected
*/
return {
element: this.getElements()[index],
index: index,
selected: isElementSelected(this.getElements()[index])
};
};
/**
* Sets aria-selected="true" on an element
*
* @param {HTMLElement} el The element to set selected
* @return {boolean}
*/
var isElementSelected = function(el){
return el.getAttribute('aria-selected') === 'true';
};
return KeyboardNav;
})(H5P.EventDispatcher);
;
H5P.MarkTheWords = H5P.MarkTheWords || {};
/**
* Mark the words XapiGenerator
*/
H5P.MarkTheWords.XapiGenerator = (function ($) {
/**
* Xapi statements Generator
* @param {H5P.MarkTheWords} markTheWords
* @constructor
*/
function XapiGenerator(markTheWords) {
/**
* Generate answered event
* @return {H5P.XAPIEvent}
*/
this.generateAnsweredEvent = function () {
var xAPIEvent = markTheWords.createXAPIEventTemplate('answered');
// Extend definition
var objectDefinition = createDefinition(markTheWords);
$.extend(true, xAPIEvent.getVerifiedStatementValue(['object', 'definition']), objectDefinition);
// Set score
xAPIEvent.setScoredResult(markTheWords.getScore(),
markTheWords.getMaxScore(),
markTheWords,
true,
markTheWords.getScore() === markTheWords.getMaxScore()
);
// Extend user result
var userResult = {
response: getUserSelections(markTheWords)
};
$.extend(xAPIEvent.getVerifiedStatementValue(['result']), userResult);
return xAPIEvent;
};
}
/**
* Create object definition for question
*
* @param {H5P.MarkTheWords} markTheWords
* @return {Object} Object definition
*/
function createDefinition(markTheWords) {
var definition = {};
definition.description = {
'en-US': markTheWords.params.taskDescription,
};
definition.type = 'http://adlnet.gov/expapi/activities/cmi.interaction';
definition.interactionType = 'choice';
definition.correctResponsesPattern = [getCorrectResponsesPattern(markTheWords)];
definition.choices = getChoices(markTheWords);
definition.extensions = {
'https://h5p.org/x-api/line-breaks': markTheWords.getIndexesOfLineBreaks()
};
return definition;
}
/**
* Get all choices that it is possible to choose between
*
* @param {H5P.MarkTheWords} markTheWords
* @return {Array}
*/
function getChoices(markTheWords) {
return markTheWords.selectableWords.map(function (word, index) {
var text = word.getText();
if (text.charAt(0) === '*' && text.charAt(text.length - 1) === '*') {
text = text.substr(1, text.length - 2);
}
return {
id: index.toString(),
description: {
'en-US': $('' + text + '
').text()
}
};
});
}
/**
* Get selected words as a user response pattern
*
* @param {H5P.MarkTheWords} markTheWords
* @return {string}
*/
function getUserSelections(markTheWords) {
return markTheWords.selectableWords
.reduce(function (prev, word, index) {
if (word.isSelected()) {
prev.push(index);
}
return prev;
}, []).join('[,]');
}
/**
* Get correct response pattern from correct words
*
* @param {H5P.MarkTheWords} markTheWords
* @return {string}
*/
function getCorrectResponsesPattern(markTheWords) {
return markTheWords.selectableWords
.reduce(function (prev, word, index) {
if (word.isAnswer()) {
prev.push(index);
}
return prev;
}, []).join('[,]');
}
return XapiGenerator;
})(H5P.jQuery);
;
H5P.MarkTheWords = H5P.MarkTheWords || {};
H5P.MarkTheWords.Word = (function () {
/**
* @constant
*
* @type {string}
*/
Word.ID_MARK_MISSED = "h5p-description-missed";
/**
* @constant
*
* @type {string}
*/
Word.ID_MARK_CORRECT = "h5p-description-correct";
/**
* @constant
*
* @type {string}
*/
Word.ID_MARK_INCORRECT = "h5p-description-incorrect";
/**
* Class for keeping track of selectable words.
*
* @class
* @param {jQuery} $word
*/
function Word($word, params) {
var self = this;
self.params = params;
H5P.EventDispatcher.call(self);
var input = $word.text();
var handledInput = input;
// Check if word is an answer
var isAnswer = checkForAnswer();
// Remove single asterisk and escape double asterisks.
handleAsterisks();
if (isAnswer) {
$word.text(handledInput);
}
const ariaText = document.createElement('span');
ariaText.classList.add('hidden-but-read');
$word[0].appendChild(ariaText);
/**
* Checks if the word is an answer by checking the first, second to last and last character of the word.
*
* @private
* @return {Boolean} Returns true if the word is an answer.
*/
function checkForAnswer() {
// Check last and next to last character, in case of punctuations.
var wordString = removeDoubleAsterisks(input);
if (wordString.charAt(0) === ('*') && wordString.length > 2) {
if (wordString.charAt(wordString.length - 1) === ('*')) {
handledInput = input.slice(1, input.length - 1);
return true;
}
// If punctuation, add the punctuation to the end of the word.
else if(wordString.charAt(wordString.length - 2) === ('*')) {
handledInput = input.slice(1, input.length - 2);
return true;
}
return false;
}
return false;
}
/**
* Removes double asterisks from string, used to handle input.
*
* @private
* @param {String} wordString The string which will be handled.
* @return {String} Returns a string without double asterisks.
*/
function removeDoubleAsterisks(wordString) {
var asteriskIndex = wordString.indexOf('*');
var slicedWord = wordString;
while (asteriskIndex !== -1) {
if (wordString.indexOf('*', asteriskIndex + 1) === asteriskIndex + 1) {
slicedWord = wordString.slice(0, asteriskIndex) + wordString.slice(asteriskIndex + 2, input.length);
}
asteriskIndex = wordString.indexOf('*', asteriskIndex + 1);
}
return slicedWord;
}
/**
* Escape double asterisks ** = *, and remove single asterisk.
*
* @private
*/
function handleAsterisks() {
var asteriskIndex = handledInput.indexOf('*');
while (asteriskIndex !== -1) {
handledInput = handledInput.slice(0, asteriskIndex) + handledInput.slice(asteriskIndex + 1, handledInput.length);
asteriskIndex = handledInput.indexOf('*', asteriskIndex + 1);
}
}
/**
* Removes any score points added to the marked word.
*/
self.clearScorePoint = function () {
const scorePoint = $word[0].querySelector('div');
if (scorePoint) {
scorePoint.parentNode.removeChild(scorePoint);
}
};
/**
* Get Word as a string
*
* @return {string} Word as text
*/
this.getText = function () {
return input;
};
/**
* Clears all marks from the word.
*
* @public
*/
this.markClear = function () {
$word
.attr('aria-selected', false)
.removeAttr('aria-describedby');
ariaText.innerHTML = '';
this.clearScorePoint();
};
/**
* Check if the word is correctly marked and style it accordingly.
* Reveal result
*
* @public
* @param {H5P.Question.ScorePoints} scorePoints
*/
this.markCheck = function (scorePoints) {
if (this.isSelected()) {
$word.attr('aria-describedby', isAnswer ? Word.ID_MARK_CORRECT : Word.ID_MARK_INCORRECT);
ariaText.innerHTML = isAnswer
? self.params.correctAnswer
: self.params.incorrectAnswer;
if (scorePoints) {
$word[0].appendChild(scorePoints.getElement(isAnswer));
}
}
else if (isAnswer) {
$word.attr('aria-describedby', Word.ID_MARK_MISSED);
ariaText.innerHTML = self.params.missedAnswer;
}
};
/**
* Checks if the word is marked correctly.
*
* @public
* @returns {Boolean} True if the marking is correct.
*/
this.isCorrect = function () {
return (isAnswer && this.isSelected());
};
/**
* Checks if the word is marked wrong.
*
* @public
* @returns {Boolean} True if the marking is wrong.
*/
this.isWrong = function () {
return (!isAnswer && this.isSelected());
};
/**
* Checks if the word is correct, but has not been marked.
*
* @public
* @returns {Boolean} True if the marking is missed.
*/
this.isMissed = function () {
return (isAnswer && !this.isSelected());
};
/**
* Checks if the word is an answer.
*
* @public
* @returns {Boolean} True if the word is an answer.
*/
this.isAnswer = function () {
return isAnswer;
};
/**
* Checks if the word is selected.
*
* @public
* @returns {Boolean} True if the word is selected.
*/
this.isSelected = function () {
return $word.attr('aria-selected') === 'true';
};
/**
* Sets that the Word is selected
*
* @public
*/
this.setSelected = function () {
$word.attr('aria-selected', 'true');
};
}
Word.prototype = Object.create(H5P.EventDispatcher.prototype);
Word.prototype.constructor = Word;
return Word;
})();
;
/*global H5P*/
/**
* Mark The Words module
* @external {jQuery} $ H5P.jQuery
*/
H5P.MarkTheWords = (function ($, Question, Word, KeyboardNav, XapiGenerator) {
/**
* Initialize module.
*
* @class H5P.MarkTheWords
* @extends H5P.Question
* @param {Object} params Behavior settings
* @param {Number} contentId Content identification
* @param {Object} contentData Object containing task specific content data
*
* @returns {Object} MarkTheWords Mark the words instance
*/
function MarkTheWords(params, contentId, contentData) {
this.contentId = contentId;
this.contentData = contentData;
this.introductionId = 'mark-the-words-introduction-' + contentId;
Question.call(this, 'mark-the-words');
// Set default behavior.
this.params = $.extend(true, {
media: {},
taskDescription: "",
textField: "This is a *nice*, *flexible* content type.",
overallFeedback: [],
behaviour: {
enableRetry: true,
enableSolutionsButton: true,
enableCheckButton: true,
showScorePoints: true
},
checkAnswerButton: "Check",
submitAnswerButton: "Submit",
tryAgainButton: "Retry",
showSolutionButton: "Show solution",
correctAnswer: "Correct!",
incorrectAnswer: "Incorrect!",
missedAnswer: "Answer not found!",
displaySolutionDescription: "Task is updated to contain the solution.",
scoreBarLabel: 'You got :num out of :total points',
a11yFullTextLabel: 'Full readable text',
a11yClickableTextLabel: 'Full text where words can be marked',
a11ySolutionModeHeader: 'Solution mode',
a11yCheckingHeader: 'Checking mode',
a11yCheck: 'Check the answers. The responses will be marked as correct, incorrect, or unanswered.',
a11yShowSolution: 'Show the solution. The task will be marked with its correct solution.',
a11yRetry: 'Retry the task. Reset all responses and start the task over again.',
}, params);
this.contentData = contentData;
if (this.contentData !== undefined && this.contentData.previousState !== undefined) {
this.previousState = this.contentData.previousState;
}
this.keyboardNavigators = [];
this.initMarkTheWords();
this.XapiGenerator = new XapiGenerator(this);
}
MarkTheWords.prototype = Object.create(H5P.EventDispatcher.prototype);
MarkTheWords.prototype.constructor = MarkTheWords;
/**
* Initialize Mark The Words task
*/
MarkTheWords.prototype.initMarkTheWords = function () {
this.$inner = $('
');
this.addTaskTo(this.$inner);
// Set user state
this.setH5PUserState();
};
/**
* Recursive function that creates html for the words
* @method createHtmlForWords
* @param {Array} nodes Array of dom nodes
* @return {string}
*/
MarkTheWords.prototype.createHtmlForWords = function (nodes) {
var self = this;
var html = '';
for (var i = 0; i < nodes.length; i++) {
var node = nodes[i];
if (node instanceof Text) {
var text = $(node).text();
var selectableStrings = text.replace(/( |\r\n|\n|\r)/g, ' ')
.match(/ \*[^\* ]+\* |[^\s]+/g);
if (selectableStrings) {
selectableStrings.forEach(function (entry) {
entry = entry.trim();
// Words
if (html) {
// Add space before
html += ' ';
}
// Remove prefix punctuations from word
var prefix = entry.match(/^[\[\({⟨¿¡“"«„]+/);
var start = 0;
if (prefix !== null) {
start = prefix[0].length;
html += prefix;
}
// Remove suffix punctuations from word
var suffix = entry.match(/[",….:;?!\]\)}⟩»”]+$/);
var end = entry.length - start;
if (suffix !== null) {
end -= suffix[0].length;
}
// Word
entry = entry.substr(start, end);
if (entry.length) {
html += '' + self.escapeHTML(entry) + ' ';
}
if (suffix !== null) {
html += suffix;
}
});
}
else if ((selectableStrings !== null) && text.length) {
html += '' + this.escapeHTML(text) + ' ';
}
}
else {
if (node.nodeName === 'BR') {
html += ' ';
}
else {
var attributes = ' ';
for (var j = 0; j < node.attributes.length; j++) {
attributes +=node.attributes[j].name + '="' + node.attributes[j].nodeValue + '" ';
}
html += '<' + node.nodeName + attributes + '>';
html += self.createHtmlForWords(node.childNodes);
html += '' + node.nodeName + '>';
}
}
}
return html;
};
/**
* Escapes HTML
*
* @param html
* @returns {jQuery}
*/
MarkTheWords.prototype.escapeHTML = function (html) {
return $('').text(html).html();
};
/**
* Search for the last children in every paragraph and
* return their indexes in an array
*
* @returns {Array}
*/
MarkTheWords.prototype.getIndexesOfLineBreaks = function () {
var indexes = [];
var selectables = this.$wordContainer.find('span.h5p-word-selectable');
selectables.each(function (index, selectable) {
if ($(selectable).next().is('br')) {
indexes.push(index);
}
if ($(selectable).parent('p') && !$(selectable).parent().is(':last-child') && $(selectable).is(':last-child')) {
indexes.push(index);
}
});
return indexes;
};
/**
* Handle task and add it to container.
* @param {jQuery} $container The object which our task will attach to.
*/
MarkTheWords.prototype.addTaskTo = function ($container) {
var self = this;
self.selectableWords = [];
self.answers = 0;
// Wrapper
var $wordContainer = $('
', {
'class': 'h5p-word-selectable-words',
'aria-labelledby': self.introductionId,
'aria-multiselectable': 'true',
'role': 'listbox',
html: self.createHtmlForWords($.parseHTML(self.params.textField))
});
let isNewParagraph = true;
$wordContainer.find('[role="option"], br').each(function () {
if ($(this).is('br')) {
isNewParagraph = true;
return;
}
if (isNewParagraph) {
// Add keyboard navigation helper
self.currentKeyboardNavigator = new KeyboardNav();
// on word clicked
self.currentKeyboardNavigator.on('select', function () {
self.isAnswered = true;
self.triggerXAPI('interacted');
});
self.keyboardNavigators.push(self.currentKeyboardNavigator);
isNewParagraph = false;
}
self.currentKeyboardNavigator.addElement(this);
// Add keyboard navigation to this element
var selectableWord = new Word($(this), self.params);
if (selectableWord.isAnswer()) {
self.answers += 1;
}
self.selectableWords.push(selectableWord);
});
self.blankIsCorrect = (self.answers === 0);
if (self.blankIsCorrect) {
self.answers = 1;
}
// A11y full readable text
const $ariaTextWrapper = $('
', {
'class': 'hidden-but-read',
}).appendTo($container);
$('
', {
html: self.params.a11yFullTextLabel,
}).appendTo($ariaTextWrapper);
// Add space after each paragraph to read the sentences better
const ariaText = $('
', {
'html': $wordContainer.html().replace('', ' '),
}).text();
$('
', {
text: ariaText,
}).appendTo($ariaTextWrapper);
// A11y clickable list label
this.$a11yClickableTextLabel = $('
', {
'class': 'hidden-but-read',
html: self.params.a11yClickableTextLabel,
tabIndex: '-1',
}).appendTo($container);
$wordContainer.appendTo($container);
self.$wordContainer = $wordContainer;
};
/**
* Add check solution and retry buttons.
*/
MarkTheWords.prototype.addButtons = function () {
var self = this;
self.$buttonContainer = $('
', {
'class': 'h5p-button-bar'
});
if (this.params.behaviour.enableCheckButton) {
this.addButton('check-answer', this.params.checkAnswerButton, function () {
self.isAnswered = true;
var answers = self.calculateScore();
self.feedbackSelectedWords();
if (!self.showEvaluation(answers)) {
// Only show if a correct answer was not found.
if (self.params.behaviour.enableSolutionsButton && (answers.correct < self.answers)) {
self.showButton('show-solution');
}
if (self.params.behaviour.enableRetry) {
self.showButton('try-again');
}
}
// Set focus to start of text
self.$a11yClickableTextLabel.html(self.params.a11yCheckingHeader + ' - ' + self.params.a11yClickableTextLabel);
self.$a11yClickableTextLabel.focus();
self.hideButton('check-answer');
self.trigger(self.XapiGenerator.generateAnsweredEvent());
self.toggleSelectable(true);
}, true, {
'aria-label': this.params.a11yCheck,
}, {
contentData: this.contentData,
textIfSubmitting: this.params.submitAnswerButton,
});
}
this.addButton('try-again', this.params.tryAgainButton, this.resetTask.bind(this), false, {
'aria-label': this.params.a11yRetry,
});
this.addButton('show-solution', this.params.showSolutionButton, function () {
self.setAllMarks();
self.$a11yClickableTextLabel.html(self.params.a11ySolutionModeHeader + ' - ' + self.params.a11yClickableTextLabel);
self.$a11yClickableTextLabel.focus();
if (self.params.behaviour.enableRetry) {
self.showButton('try-again');
}
self.hideButton('check-answer');
self.hideButton('show-solution');
self.read(self.params.displaySolutionDescription);
self.toggleSelectable(true);
}, false, {
'aria-label': this.params.a11yShowSolution,
});
};
/**
* Toggle whether words can be selected
* @param {Boolean} disable
*/
MarkTheWords.prototype.toggleSelectable = function (disable) {
this.keyboardNavigators.forEach(function (navigator) {
if (disable) {
navigator.disableSelectability();
navigator.removeAllTabbable();
}
else {
navigator.enableSelectability();
navigator.setTabbableAt((0));
}
});
if (disable) {
this.$wordContainer.removeAttr('aria-multiselectable').removeAttr('role');
}
else {
this.$wordContainer.attr('aria-multiselectable', 'true')
.attr('role', 'listbox');
}
};
/**
* Get Xapi Data.
*
* @see used in contracts {@link https://h5p.org/documentation/developers/contracts#guides-header-6}
* @return {Object}
*/
MarkTheWords.prototype.getXAPIData = function () {
return {
statement: this.XapiGenerator.generateAnsweredEvent().data.statement
};
};
/**
* Mark the words as correct, wrong or missed.
*
* @fires MarkTheWords#resize
*/
MarkTheWords.prototype.setAllMarks = function () {
this.selectableWords.forEach(function (entry) {
entry.markCheck();
entry.clearScorePoint();
});
/**
* Resize event
*
* @event MarkTheWords#resize
*/
this.trigger('resize');
};
/**
* Mark the selected words as correct or wrong.
*
* @fires MarkTheWords#resize
*/
MarkTheWords.prototype.feedbackSelectedWords = function () {
var self = this;
var scorePoints;
if (self.params.behaviour.showScorePoints) {
scorePoints = new H5P.Question.ScorePoints();
}
this.selectableWords.forEach(function (entry) {
if (entry.isSelected()) {
entry.markCheck(scorePoints);
}
});
this.$wordContainer.addClass('h5p-disable-hover');
this.trigger('resize');
};
/**
* Evaluate task and display score text for word markings.
*
* @fires MarkTheWords#resize
* @return {Boolean} Returns true if maxScore was achieved.
*/
MarkTheWords.prototype.showEvaluation = function (answers) {
this.hideEvaluation();
var score = answers.score;
//replace editor variables with values, uses regexp to replace all instances.
var scoreText = H5P.Question.determineOverallFeedback(this.params.overallFeedback, score / this.answers).replace(/@score/g, score.toString())
.replace(/@total/g, this.answers.toString())
.replace(/@correct/g, answers.correct.toString())
.replace(/@wrong/g, answers.wrong.toString())
.replace(/@missed/g, answers.missed.toString());
this.setFeedback(scoreText, score, this.answers, this.params.scoreBarLabel);
this.trigger('resize');
return score === this.answers;
};
/**
* Clear the evaluation text.
*
* @fires MarkTheWords#resize
*/
MarkTheWords.prototype.hideEvaluation = function () {
this.removeFeedback();
this.trigger('resize');
};
/**
* Calculate the score.
*
* @return {Answers}
*/
MarkTheWords.prototype.calculateScore = function () {
var self = this;
/**
* @typedef {Object} Answers
* @property {number} correct The number of correct answers
* @property {number} wrong The number of wrong answers
* @property {number} missed The number of answers the user missed
* @property {number} score The calculated score
*/
var initial = {
correct: 0,
wrong: 0,
missed: 0,
score: 0
};
// iterate over words, and calculate score
var answers = self.selectableWords.reduce(function (result, word) {
if (word.isCorrect()) {
result.correct++;
}
else if (word.isWrong()) {
result.wrong++;
}
else if (word.isMissed()) {
result.missed++;
}
return result;
}, initial);
// if no wrong answers, and black is correct
if (answers.wrong === 0 && self.blankIsCorrect) {
answers.correct = 1;
}
// no negative score
answers.score = Math.max(answers.correct - answers.wrong, 0);
return answers;
};
/**
* Clear styling on marked words.
*
* @fires MarkTheWords#resize
*/
MarkTheWords.prototype.clearAllMarks = function () {
this.selectableWords.forEach(function (entry) {
entry.markClear();
});
this.$wordContainer.removeClass('h5p-disable-hover');
this.trigger('resize');
};
/**
* Returns true if task is checked or a word has been clicked
*
* @see {@link https://h5p.org/documentation/developers/contracts|Needed for contracts.}
* @returns {Boolean} Always returns true.
*/
MarkTheWords.prototype.getAnswerGiven = function () {
return this.blankIsCorrect ? true : this.isAnswered;
};
/**
* Counts the score, which is correct answers subtracted by wrong answers.
*
* @see {@link https://h5p.org/documentation/developers/contracts|Needed for contracts.}
* @returns {Number} score The amount of points achieved.
*/
MarkTheWords.prototype.getScore = function () {
return this.calculateScore().score;
};
/**
* Gets max score for this task.
*
* @see {@link https://h5p.org/documentation/developers/contracts|Needed for contracts.}
* @returns {Number} maxScore The maximum amount of points achievable.
*/
MarkTheWords.prototype.getMaxScore = function () {
return this.answers;
};
/**
* Get title
* @returns {string}
*/
MarkTheWords.prototype.getTitle = function () {
return H5P.createTitle((this.contentData && this.contentData.metadata && this.contentData.metadata.title) ? this.contentData.metadata.title : 'Mark the Words');
};
/**
* Display the evaluation of the task, with proper markings.
*
* @fires MarkTheWords#resize
* @see {@link https://h5p.org/documentation/developers/contracts|Needed for contracts.}
*/
MarkTheWords.prototype.showSolutions = function () {
var answers = this.calculateScore();
this.showEvaluation(answers);
this.setAllMarks();
this.read(this.params.displaySolutionDescription);
this.hideButton('try-again');
this.hideButton('show-solution');
this.hideButton('check-answer');
this.$a11yClickableTextLabel.html(this.params.a11ySolutionModeHeader + ' - ' + this.params.a11yClickableTextLabel);
this.toggleSelectable(true);
this.trigger('resize');
};
/**
* Resets the task back to its' initial state.
*
* @fires MarkTheWords#resize
* @see {@link https://h5p.org/documentation/developers/contracts|Needed for contracts.}
*/
MarkTheWords.prototype.resetTask = function () {
this.isAnswered = false;
this.clearAllMarks();
this.hideEvaluation();
this.hideButton('try-again');
this.hideButton('show-solution');
this.showButton('check-answer');
this.$a11yClickableTextLabel.html(this.params.a11yClickableTextLabel);
this.toggleSelectable(false);
this.trigger('resize');
};
/**
* Returns an object containing the selected words
*
* @public
* @returns {object} containing indexes of selected words
*/
MarkTheWords.prototype.getCurrentState = function () {
var selectedWordsIndexes = [];
if (this.selectableWords === undefined) {
return undefined;
}
this.selectableWords.forEach(function (selectableWord, swIndex) {
if (selectableWord.isSelected()) {
selectedWordsIndexes.push(swIndex);
}
});
return selectedWordsIndexes;
};
/**
* Sets answers to current user state
*/
MarkTheWords.prototype.setH5PUserState = function () {
var self = this;
// Do nothing if user state is undefined
if (this.previousState === undefined || this.previousState.length === undefined) {
return;
}
// Select words from user state
this.previousState.forEach(function (answeredWordIndex) {
if (isNaN(answeredWordIndex) || answeredWordIndex >= self.selectableWords.length || answeredWordIndex < 0) {
throw new Error('Stored user state is invalid');
}
self.isAnswered = true;
self.selectableWords[answeredWordIndex].setSelected();
});
};
/**
* Register dom elements
*
* @see {@link https://github.com/h5p/h5p-question/blob/1558b6144333a431dd71e61c7021d0126b18e252/scripts/question.js#L1236|Called from H5P.Question}
*/
MarkTheWords.prototype.registerDomElements = function () {
// Register optional media
let media = this.params.media;
if (media && media.type && media.type.library) {
media = media.type;
const type = media.library.split(' ')[0];
if (type === 'H5P.Image') {
if (media.params.file) {
// Register task image
this.setImage(media.params.file.path, {
disableImageZooming: this.params.media.disableImageZooming || false,
alt: media.params.alt,
title: media.params.title,
expandImage: media.params.expandImage,
minimizeImage: media.params.minimizeImage
});
}
}
else if (type === 'H5P.Video') {
if (media.params.sources) {
// Register task video
this.setVideo(media);
}
}
else if (type === 'H5P.Audio') {
if (media.params.files) {
// Register task audio
this.setAudio(media);
}
}
}
// wrap introduction in div with id
var introduction = '
' + this.params.taskDescription + '
';
// Register description
this.setIntroduction(introduction);
// creates aria descriptions for correct/incorrect/missed
this.createDescriptionsDom().appendTo(this.$inner);
// Register content
this.setContent(this.$inner, {
'class': 'h5p-word'
});
// Register buttons
this.addButtons();
};
/**
* Creates dom with description to be used with aria-describedby
* @return {jQuery}
*/
MarkTheWords.prototype.createDescriptionsDom = function () {
var self = this;
var $el = $('
');
$('
' + self.params.correctAnswer + '
').appendTo($el);
$('
' + self.params.incorrectAnswer + '
').appendTo($el);
$('
' + self.params.missedAnswer + '
').appendTo($el);
return $el;
};
return MarkTheWords;
}(H5P.jQuery, H5P.Question, H5P.MarkTheWords.Word, H5P.KeyboardNav, H5P.MarkTheWords.XapiGenerator));
/**
* Static utility method for parsing H5P.MarkTheWords content item questions
* into format useful for generating reports.
*
* Example input: "
I like *pizza* and *burgers*.
"
*
* Produces the following:
* [
* {
* type: 'text',
* content: 'I like '
* },
* {
* type: 'answer',
* correct: 'pizza',
* },
* {
* type: 'text',
* content: ' and ',
* },
* {
* type: 'answer',
* correct: 'burgers'
* },
* {
* type: 'text',
* content: '.'
* }
* ]
*
* @param {string} question MarkTheWords textField (html)
*/
H5P.MarkTheWords.parseText = function (question) {
/**
* Separate all words surrounded by a space and an asterisk and any other
* sequence of non-whitespace characters from str into an array.
*
* @param {string} str
* @returns {string[]} array of all words in the given string
*/
function getWords(str) {
return str.match(/ \*[^\*]+\* |[^\s]+/g);
}
/**
* Replace each HTML tag in str with the provided value and return the resulting string
*
* Regexp expression explained:
* < - first character is '<'
* [^>]* - followed by zero or more occurences of any character except '>'
* > - last character is '>'
**/
function replaceHtmlTags(str, value) {
return str.replace(/<[^>]*>/g, value);
}
function startsAndEndsWith(char, str) {
return str.startsWith(char) && str.endsWith(char);
}
function removeLeadingPunctuation(str) {
return str.replace(/^[\[\({⟨¿¡“"«„]+/, '');
}
function removeTrailingPunctuation(str) {
return str.replace(/[",….:;?!\]\)}⟩»”]+$/, '');
}
/**
* Escape double asterisks ** = *, and remove single asterisk.
* @param {string} str
*/
function handleAsterisks(str) {
var asteriskIndex = str.indexOf('*');
while (asteriskIndex !== -1) {
str = str.slice(0, asteriskIndex) + str.slice(asteriskIndex + 1, str.length);
asteriskIndex = str.indexOf('*', asteriskIndex + 1);
}
return str;
}
/**
* Decode HTML entities (e.g. ) from the given string using the DOM API
* @param {string} str
*/
function decodeHtmlEntities(str) {
const el = document.createElement('textarea');
el.innerHTML = str;
return el.value;
}
const wordsWithAsterisksNotRemovedYet = getWords(replaceHtmlTags(decodeHtmlEntities(question), ' '))
.map(function (w) {
return w.trim();
})
.map(function (w) {
return removeLeadingPunctuation(w);
})
.map(function (w) {
return removeTrailingPunctuation(w);
});
const allSelectableWords = wordsWithAsterisksNotRemovedYet.map(function (w) {
return handleAsterisks(w);
});
const correctWordIndexes = [];
const correctWords = wordsWithAsterisksNotRemovedYet
.filter(function (w, i) {
if (startsAndEndsWith('*', w)) {
correctWordIndexes.push(i);
return true;
}
return false;
})
.map(function (w) {
return handleAsterisks(w);
});
const printableQuestion = replaceHtmlTags(decodeHtmlEntities(question), ' ')
.replace('\xa0', '\x20');
return {
alternatives: allSelectableWords,
correctWords: correctWords,
correctWordIndexes: correctWordIndexes,
textWithPlaceholders: printableQuestion.match(/[^\s]+/g)
.reduce(function (textWithPlaceholders, word, index) {
word = removeTrailingPunctuation(
removeLeadingPunctuation(word));
return textWithPlaceholders.replace(word, '%' + index);
}, printableQuestion)
};
};
;
var H5P = H5P || {};
H5P.Summary = H5P.Summary || {};
H5P.Summary.StopWatch = (function () {
/**
* @class {H5P.Summary.StopWatch}
* @constructor
*/
function StopWatch() {
/**
* @property {number} duration in ms
*/
this.duration = 0;
}
/**
* Starts the stop watch
*
* @public
* @return {H5P.Summary.StopWatch}
*/
StopWatch.prototype.start = function(){
/**
* @property {number}
*/
this.startTime = Date.now();
return this;
};
/**
* Stops the stopwatch, and returns the duration in seconds.
*
* @public
* @return {number}
*/
StopWatch.prototype.stop = function(){
this.duration = this.duration + Date.now() - this.startTime;
return this.passedTime();
};
/**
* Sets the duration to 0
*
* @public
*/
StopWatch.prototype.reset = function(){
this.duration = 0;
};
/**
* Returns the passed time in seconds
*
* @public
* @return {number}
*/
StopWatch.prototype.passedTime = function(){
return Math.round(this.duration / 10) / 100;
};
return StopWatch;
})();
;
var H5P = H5P || {};
H5P.Summary = H5P.Summary || {};
H5P.Summary.XApiEventBuilder = (function ($, EventDispatcher) {
/**
* @typedef {object} LocalizedString
* @property {string} en-US
*/
/**
* @class {H5P.Summary.XApiEventDefinitionBuilder}
* @constructor
*/
function XApiEventDefinitionBuilder(){
EventDispatcher.call(this);
/**
* @property {object} attributes
* @property {string} attributes.name
* @property {string} attributes.description
* @property {string} attributes.interactionType
* @property {string} attributes.correctResponsesPattern
* @property {object} attributes.optional
*/
this.attributes = {};
}
XApiEventDefinitionBuilder.prototype = Object.create(EventDispatcher.prototype);
XApiEventDefinitionBuilder.prototype.constructor = XApiEventDefinitionBuilder;
/**
* Sets name
* @param {string} name
* @return {XApiEventDefinitionBuilder}
*/
XApiEventDefinitionBuilder.prototype.name = function (name) {
this.attributes.name = name;
return this;
};
/**
* Question text and any additional information to generate the report.
* @param {string} description
* @return {XApiEventDefinitionBuilder}
*/
XApiEventDefinitionBuilder.prototype.description = function (description) {
this.attributes.description = description;
return this;
};
/**
* Type of the interaction.
* @param {string} interactionType
* @see {@link https://github.com/adlnet/xAPI-Spec/blob/master/xAPI-Data.md#interaction-types|xAPI Spec}
* @return {XApiEventDefinitionBuilder}
*/
XApiEventDefinitionBuilder.prototype.interactionType = function (interactionType) {
this.attributes.interactionType = interactionType;
return this;
};
/**
* A pattern for determining the correct answers of the interaction
* @param {string[]} correctResponsesPattern
* @see {@link https://github.com/adlnet/xAPI-Spec/blob/master/xAPI-Data.md#response-patterns|xAPI Spec}
* @return {XApiEventDefinitionBuilder}
*/
XApiEventDefinitionBuilder.prototype.correctResponsesPattern = function (correctResponsesPattern) {
this.attributes.correctResponsesPattern = correctResponsesPattern;
return this;
};
/**
* Sets optional attributes
* @param {object} optional Can have one of the following configuration objects: choices, scale, source, target, steps
* @return {XApiEventDefinitionBuilder}
*/
XApiEventDefinitionBuilder.prototype.optional = function (optional) {
this.attributes.optional = optional;
return this;
};
/**
* @return {object}
*/
XApiEventDefinitionBuilder.prototype.build = function () {
var definition = {};
// sets attributes
setAttribute(definition, 'name', localizeToEnUS(this.attributes.name));
setAttribute(definition, 'description', localizeToEnUS(this.attributes.description));
setAttribute(definition, 'interactionType', this.attributes.interactionType);
setAttribute(definition, 'correctResponsesPattern', this.attributes.correctResponsesPattern);
setAttribute(definition, 'type', 'http://adlnet.gov/expapi/activities/cmi.interaction');
// adds the optional object to the definition
if(this.attributes.optional){
$.extend(definition, this.attributes.optional);
}
return definition;
};
// -----------------------------------------------------
/**
*
* @constructor
*/
function XApiEventResultBuilder(){
EventDispatcher.call(this);
/**
* @property {object} attributes
* @property {string} attributes.completion
* @property {boolean} attributes.success
* @property {boolean} attributes.response
* @property {number} attributes.rawScore
* @property {number} attributes.maxScore
*/
this.attributes = {};
}
XApiEventResultBuilder.prototype = Object.create(EventDispatcher.prototype);
XApiEventResultBuilder.prototype.constructor = XApiEventResultBuilder;
/**
* @param {boolean} completion
* @return {XApiEventResultBuilder}
*/
XApiEventResultBuilder.prototype.completion = function (completion) {
this.attributes.completion = completion;
return this;
};
/**
* @param {boolean} success
* @return {XApiEventResultBuilder}
*/
XApiEventResultBuilder.prototype.success = function (success) {
this.attributes.success = success;
return this;
};
/**
* @param {number} duration The duraction in seconds
* @return {XApiEventResultBuilder}
*/
XApiEventResultBuilder.prototype.duration = function (duration) {
this.attributes.duration = duration;
return this;
};
/**
* Sets response
* @param {string|string[]} response
* @return {XApiEventResultBuilder}
*/
XApiEventResultBuilder.prototype.response = function (response) {
this.attributes.response = (typeof response === 'string') ? response : response.join('[,]');
return this;
};
/**
* Sets the score, and max score
* @param {number} score
* @param {number} maxScore
* @return {XApiEventResultBuilder}
*/
XApiEventResultBuilder.prototype.score = function (score, maxScore) {
this.attributes.rawScore = score;
this.attributes.maxScore = maxScore;
return this;
};
/**
* Builds the result object
* @return {object}
*/
XApiEventResultBuilder.prototype.build = function () {
var result = {};
setAttribute(result, 'response', this.attributes.response);
setAttribute(result, 'completion', this.attributes.completion);
setAttribute(result, 'success', this.attributes.success);
if(isDefined(this.attributes.duration)){
setAttribute(result, 'duration','PT' + this.attributes.duration + 'S');
}
// sets score
if (isDefined(this.attributes.rawScore)) {
result.score = {};
setAttribute(result.score, 'raw', this.attributes.rawScore);
if (isDefined(this.attributes.maxScore) && this.attributes.maxScore > 0) {
setAttribute(result.score, 'min', 0);
setAttribute(result.score, 'max', this.attributes.maxScore);
setAttribute(result.score, 'min', 0);
setAttribute(result.score, 'scaled', Math.round(this.attributes.rawScore / this.attributes.maxScore * 10000) / 10000);
}
}
return result;
};
// -----------------------------------------------------
/**
* @class {H5P.Summary.XApiEventBuilder}
*/
function XApiEventBuilder() {
EventDispatcher.call(this);
/**
* @property {object} attributes
* @property {string} attributes.contentId
* @property {string} attributes.subContentId
*/
this.attributes = {};
}
XApiEventBuilder.prototype = Object.create(EventDispatcher.prototype);
XApiEventBuilder.prototype.constructor = XApiEventBuilder;
/**
* @param {object} verb
*
* @public
* @return {H5P.Summary.XApiEventBuilder}
*/
XApiEventBuilder.prototype.verb = function (verb) {
this.attributes.verb = verb;
return this;
};
/**
* @param {string} name
* @param {string} mbox
* @param {string} objectType
*
* @public
* @return {H5P.Summary.XApiEventBuilder}
*/
XApiEventBuilder.prototype.actor = function (name, mbox, objectType) {
this.attributes.actor = {
name: name,
mbox: mbox,
objectType: objectType
};
return this;
};
/**
* Sets contentId
* @param {string} contentId
* @param {string} [subContentId]
* @return {H5P.Summary.XApiEventBuilder}
*/
XApiEventBuilder.prototype.contentId = function (contentId, subContentId) {
this.attributes.contentId = contentId;
this.attributes.subContentId = subContentId;
return this;
};
/**
* Sets parent in context
* @param {string} parentContentId
* @param {string} [parentSubContentId]
* @return {H5P.Summary.XApiEventBuilder}
*/
XApiEventBuilder.prototype.context = function (parentContentId, parentSubContentId) {
this.attributes.parentContentId = parentContentId;
this.attributes.parentSubContentId = parentSubContentId;
return this;
};
/**
* @param {object} result
*
* @public
* @return {H5P.Summary.XApiEventBuilder}
*/
XApiEventBuilder.prototype.result = function (result) {
this.attributes.result = result;
return this;
};
/**
* @param {object} objectDefinition
*
* @public
* @return {H5P.Summary.XApiEventBuilder}
*/
XApiEventBuilder.prototype.objectDefinition = function (objectDefinition) {
this.attributes.objectDefinition = objectDefinition;
return this;
};
/**
* Returns the buildt event
* @public
* @return {H5P.XAPIEvent}
*/
XApiEventBuilder.prototype.build = function(){
var event = new H5P.XAPIEvent();
event.setActor();
event.setVerb(this.attributes.verb);
// sets context
if(this.attributes.parentContentId || this.attributes.parentSubContentId){
event.data.statement.context = {
'contextActivities': {
'parent': [
{
'id': getContentXAPIId(this.attributes.parentContentId, this.attributes.parentSubContentId),
'objectType': "Activity"
}
]
}
};
if (H5P.xApiSessionId) {
event.data.statement.context.extensions = {
'https://h5p.com/xapi/session-id': H5P.xApiSessionId
};
}
}
event.data.statement.object = {
'id': getContentXAPIId(this.attributes.contentId, this.attributes.subContentId),
'objectType': 'Activity'
};
setAttribute(event.data, 'actor', this.attributes.actor);
setAttribute(event.data.statement, 'result', this.attributes.result);
setAttribute(event.data.statement.object, 'definition', this.attributes.objectDefinition);
// sets h5p specific attributes
if(event.data.statement.object.definition && (this.attributes.contentId || this.attributes.subContentId)) {
var extensions = event.data.statement.object.definition.extensions = {};
setAttribute(extensions, 'http://h5p.org/x-api/h5p-local-content-id', this.attributes.contentId);
setAttribute(extensions, 'http://h5p.org/x-api/h5p-subContentId', this.attributes.subContentId);
}
return event;
};
/**
* Creates a Localized String object for en-US
*
* @param str
* @return {LocalizedString}
*/
var localizeToEnUS = function(str){
if(str != undefined){
return {
'en-US': cleanString(str)
};
}
};
/**
* Generates an id for the content
* @param {string} contentId
* @param {string} [subContentId]
*
* @see {@link https://github.com/h5p/h5p-php-library/blob/master/js/h5p-x-api-event.js#L240-L249}
* @return {string}
*/
var getContentXAPIId = function (contentId, subContentId) {
const cid = 'cid-' + contentId;
if (contentId && H5PIntegration && H5PIntegration.contents && H5PIntegration.contents[cid]) {
var id = H5PIntegration.contents[cid].url;
if (subContentId) {
id += '?subContentId=' + subContentId;
}
return id;
}
};
/**
* Removes html elements from string
*
* @param {string} str
* @return {string}
*/
var cleanString = function (str) {
return $('
' + str + '
').text().trim();
};
var isDefined = function(val){
return typeof val !== 'undefined';
};
function setAttribute(obj, key, value, required){
if(isDefined(value)){
obj[key] = value;
} else if (required) {
console.error("xApiEventBuilder: No value for [" + key + "] in", obj);
}
}
/**
* Creates a new XApiEventBuilder
*
* @public
* @static
* @return {H5P.Summary.XApiEventBuilder}
*/
XApiEventBuilder.create = function(){
return new XApiEventBuilder();
};
/**
* Creates a new XApiEventDefinitionBuilder
*
* @public
* @static
* @return {XApiEventDefinitionBuilder}
*/
XApiEventBuilder.createDefinition = function(){
return new XApiEventDefinitionBuilder();
};
/**
* Creates a new XApiEventDefinitionBuilder
*
* @public
* @static
* @return {XApiEventResultBuilder}
*/
XApiEventBuilder.createResult = function(){
return new XApiEventResultBuilder();
};
/**
* Returns choice to be used with 'cmi.interaction' for Activity of type 'choice'
*
* @param {string} id
* @param {string} description
*
* @public
* @static
* @see {@link https://github.com/adlnet/xAPI-Spec/blob/master/xAPI-Data.md#choice|xAPI-Spec}
* @return {object}
*/
XApiEventBuilder.createChoice = function(id, description){
return {
id: id,
description: localizeToEnUS(description)
};
};
/**
* Takes an array of correct ids, and joins them to a 'correct response pattern'
*
* @param {string[]} ids
*
* @public
* @static
* @see {@link https://github.com/adlnet/xAPI-Spec/blob/master/xAPI-Data.md#choice|xAPI-Spec}
* @return {string}
*/
XApiEventBuilder.createCorrectResponsePattern = function(ids){
return ids.join('[,]');
};
/**
* Interaction types
*
* @readonly
* @enum {String}
*/
XApiEventBuilder.interactionTypes = {
CHOICE: 'choice',
COMPOUND: 'compound',
FILL_IN: 'fill-in',
MATCHING: 'matching',
TRUE_FALSE: 'true-false'
};
/**
* Verbs
*
* @readonly
* @enum {String}
*/
XApiEventBuilder.verbs = {
ANSWERED: 'answered'
};
return XApiEventBuilder;
})(H5P.jQuery, H5P.EventDispatcher);
;
H5P.Summary = (function ($, Question, XApiEventBuilder, StopWatch) {
var summaryId = 0;
function Summary(options, contentId, contentData) {
if (!(this instanceof H5P.Summary)) {
return new H5P.Summary(options, contentId);
}
this.id = this.contentId = contentId;
this.contentData = contentData;
this.summaryId = summaryId;
Question.call(this, 'summary');
this.offset = 0;
this.score = 0;
this.progress = 0;
this.answers = [];
this.answer = [];
this.errorCounts = [];
summaryId += 1;
/**
* The key is panel index, returns an array of the answer indexes the user tried.
*
* @property {number[][]}
*/
this.userResponses = [];
/**
* The first key is panel index, and the second key is data-bit, value is index in panel
*
* @property {number[][]}
*/
this.dataBitMap = [];
// Remove empty summary to avoid JS-errors
if (options.summaries) {
options.summaries = options.summaries.filter(function (element) {
return element.summary !== undefined;
});
}
if (contentData && contentData.previousState !== undefined &&
contentData.previousState.progress !== undefined &&
contentData.previousState.answers) {
this.progress = contentData.previousState.progress || this.progress;
this.answers = contentData.previousState.answers || this.answers;
var currentProgress = this.progress;
// Do not count score screen as an error
if (this.progress >= options.summaries.length) {
currentProgress = options.summaries.length - 1;
}
for (var i = 0; i <= currentProgress; i++) {
if (this.errorCounts[i] === undefined) {
this.errorCounts[i] = 0;
}
if (this.answers[i]) {
this.score += this.answers[i].length;
this.errorCounts[i]++;
}
}
}
var that = this;
/**
* @property {StopWatch[]} Stop watches for tracking duration of slides
*/
this.stopWatches = [];
this.startStopWatch(this.progress);
this.options = H5P.jQuery.extend({}, {
overallFeedback: [],
resultLabel: "Your result:",
intro: "Choose the correct statement.",
solvedLabel: "Solved:",
scoreLabel: "Wrong answers:",
labelCorrect: "Correct.",
labelIncorrect: 'Incorrect! Please try again.',
labelCorrectAnswers: "List of correct answers.",
alternativeIncorrectLabel: 'Incorrect',
postUserStatistics: (H5P.postUserStatistics === true),
tipButtonLabel: 'Show tip',
scoreBarLabel: 'You got :num out of :total points',
progressText: 'Progress :num of :total'
}, options);
this.summaries = that.options.summaries;
// Prevent the score bar from interrupting the progress counter
this.setBehaviour({disableReadSpeaker: true});
// Required questiontype contract function
this.showSolutions = function() {
// intentionally left blank, no solution view exists
};
// Required questiontype contract function
this.getMaxScore = function() {
return this.summaries ? this.summaries.length : 0;
};
this.getScore = function() {
var self = this;
// count single correct answers
return self.summaries ? self.summaries.reduce(function(result, panel, index){
var userResponse = self.userResponses[index] || [];
return result + (self.correctOnFirstTry(userResponse) ? 1 : 0);
}, 0) : 0;
};
this.getTitle = function() {
return H5P.createTitle((this.contentData && this.contentData.metadata && this.contentData.metadata.title) ? this.contentData.metadata.title: 'Summary');
};
this.getCurrentState = function () {
return {
progress: this.progress || null,
answers: this.answers
};
};
}
Summary.prototype = Object.create(Question.prototype);
Summary.prototype.constructor = Summary;
/**
* Registers DOM elements before they are attached.
* Called from H5P.Question.
*/
Summary.prototype.registerDomElements = function () {
// Register task content area
this.setContent(this.createQuestion());
};
// Function for attaching the multichoice to a DOM element.
Summary.prototype.createQuestion = function() {
var that = this;
var id = 0; // element counter
// variable to capture currently focused option.
var currentFocusedOption;
var elements = [];
var $ = H5P.jQuery;
this.$myDom = $('
', {
'class': 'summary-content'
});
this.$answerAnnouncer = $('
', {
'class': 'hidden-but-read',
'aria-live': 'assertive',
appendTo: this.$myDom,
});
if (that.summaries === undefined || that.summaries.length === 0) {
return;
}
// Create array objects
for (var panelIndex = 0; panelIndex < that.summaries.length; panelIndex++) {
if (!(that.summaries[panelIndex].summary && that.summaries[panelIndex].summary.length)) {
continue;
}
elements[panelIndex] = {
tip: that.summaries[panelIndex].tip,
summaries: []
};
for (var summaryIndex = 0; summaryIndex < that.summaries[panelIndex].summary.length; summaryIndex++) {
var isAnswer = (summaryIndex === 0);
that.answer[id] = isAnswer; // First claim is correct
// create mapping from data-bit to index in panel
that.dataBitMap[panelIndex] = this.dataBitMap[panelIndex] || [];
that.dataBitMap[panelIndex][id] = summaryIndex;
// checks the answer and updates the user response array
if(that.answers[panelIndex] && (that.answers[panelIndex].indexOf(id) !== -1)){
this.storeUserResponse(panelIndex, summaryIndex);
}
// adds to elements
elements[panelIndex].summaries[summaryIndex] = {
id: id++,
text: that.summaries[panelIndex].summary[summaryIndex]
};
}
// if we have progressed passed this point, the success pattern must also be saved
if(panelIndex < that.progress){
this.storeUserResponse(panelIndex, 0);
}
// Randomize elements
for (var k = elements[panelIndex].summaries.length - 1; k > 0; k--) {
var j = Math.floor(Math.random() * (k + 1));
var temp = elements[panelIndex].summaries[k];
elements[panelIndex].summaries[k] = elements[panelIndex].summaries[j];
elements[panelIndex].summaries[j] = temp;
}
}
// Create content panels
var $summary_container = $('
');
var $summary_list = $('
');
var $evaluation = $('
');
var $evaluation_content = $('
' + that.options.intro + '
');
var $score = $('
');
var $options = $('
');
var $progress = $('
');
var $progressNumeric = $('
');
var options_padding = parseInt($options.css('paddingLeft'));
// content div added for readspeaker that indicates list of correct answers.
var $answersListHeading = $('
' + that.options.labelCorrectAnswers + '
');
$score
.html(that.options.scoreLabel + ' ' + this.score)
.toggleClass('visible', this.score > 0);
// Insert content
// aria-hidden = true added for readspeaker to avoid reading empty answers list.
$summary_container.attr("aria-hidden", "true");
$summary_container.html($answersListHeading);
$summary_container.append($summary_list);
this.$myDom.append($summary_container);
this.$myDom.append($evaluation);
this.$myDom.append($options);
$evaluation.append($evaluation_content);
$evaluation.append($evaluation);
$evaluation.append($progress);
$evaluation.append($progressNumeric);
$evaluation.append($score);
/**
* Handle selected alternative
*
* @param {jQuery} $el Selected element
* @param {boolean} [setFocus] Set focus on first element of next panel.
* Used when alt was selected with keyboard.
*/
var selectedAlt = function ($el, setFocus) {
var nodeId = Number($el.attr('data-bit'));
var panelId = Number($el.parent().data('panel'));
var isRadioClicked = $el.attr('aria-checked');
if(isRadioClicked == 'true') return;
if (that.errorCounts[panelId] === undefined) {
that.errorCounts[panelId] = 0;
}
that.storeUserResponse(panelId, nodeId);
// Correct answer?
if (that.answer[nodeId]) {
that.announceAnswer(true);
that.stopStopWatch(panelId);
that.progress++;
var position = $el.position();
var summary = $summary_list.position();
var $answer = $('
' + $el.html() + ' ');
$progressNumeric.html(that.options.solvedLabel + ' ' + (panelId + 1) + '/' + that.summaries.length);
var interpolatedProgressText = that.options.progressText
.replace(':num', panelId + 1)
.replace(':total', that.summaries.length);
$progress.html(interpolatedProgressText);
$el.attr("aria-checked", "true");
// Insert correct claim into summary list
$summary_list.append($answer);
$summary_container.addClass('has-results');
// change aria-hidden property as when correct answer is added inside list at top
$summary_container.attr("aria-hidden", "false");
that.adjustTargetHeight($summary_container, $summary_list, $answer);
// Move into position over clicked element
$answer.css({display: 'block', width: $el.css('width'), height: $el.css('height')});
$answer.css({position: 'absolute', top: position.top, left: position.left});
$answer.css({backgroundColor: '#9dd8bb', border: ''});
setTimeout(function () {
$answer.css({backgroundColor: ''});
}, 1);
//$answer.animate({backgroundColor: '#eee'}, 'slow');
var panel = parseInt($el.parent().attr('data-panel'));
var $curr_panel = $('.h5p-panel:eq(' + panel + ')', that.$myDom);
var $next_panel = $('.h5p-panel:eq(' + (panel + 1) + ')', that.$myDom);
var finished = ($next_panel.length === 0);
// Disable panel while waiting for animation
$curr_panel.addClass('panel-disabled');
// Update tip:
$evaluation_content.find('.joubel-tip-container').remove();
if (elements[that.progress] !== undefined &&
elements[that.progress].tip !== undefined &&
elements[that.progress].tip.trim().length > 0) {
$evaluation_content.append(H5P.JoubelUI.createTip(elements[that.progress].tip, {
tipLabel: that.options.tipButtonLabel
}));
}
$answer.animate(
{
top: summary.top + that.offset,
left: '-=' + options_padding + 'px',
width: '+=' + (options_padding * 2) + 'px'
},
{
complete: function() {
// Remove position (becomes inline);
$(this).css('position', '').css({
width: '',
height: '',
top: '',
left: ''
});
$summary_container.css('height', '');
// Calculate offset for next summary item
var tpadding = parseInt($answer.css('paddingTop')) * 2;
var tmargin = parseInt($answer.css('marginBottom'));
var theight = parseInt($answer.css('height'));
that.offset += theight + tpadding + tmargin + 1;
// Fade out current panel
$curr_panel.fadeOut('fast', function () {
$curr_panel.parent().css('height', 'auto');
// Show next panel if present
if (!finished) {
// start next timer
that.startStopWatch(that.progress);
$next_panel.fadeIn('fast');
// Focus first element of next panel
if (setFocus) {
$next_panel.children().get(0).focus();
}
} else {
// Hide intermediate evaluation
$evaluation_content.html(that.options.resultLabel);
that.doFinalEvaluation();
}
that.trigger('resize');
});
}
}
);
}
else {
that.announceAnswer(false);
// Remove event handler (prevent repeated clicks) and mouseover effect
$el.off('click');
$el.addClass('summary-failed');
const label = that.options.alternativeIncorrectLabel + '. '
+ $el.text();
$el.attr('aria-label', label);
$el.removeClass('summary-claim-unclicked');
$el.attr("aria-checked", "true");
$evaluation.children('.summary-score').toggleClass('visible', true);
$score.html(that.options.scoreLabel + ' ' + (++that.score));
that.errorCounts[panelId]++;
if (that.answers[panelId] === undefined) {
that.answers[panelId] = [];
}
that.answers[panelId].push(nodeId);
}
that.trigger('resize');
that.triggerXAPI('interacted');
// Trigger answered xAPI event on first try for the current
// statement group
if (that.userResponses[panelId].length === 1) {
that.trigger(that.createXApiAnsweredEvent(
that.summaries[panelId],
that.userResponses[panelId] || [],
panelId,
that.timePassedInStopWatch(panelId)));
}
// Trigger overall answered xAPI event when finished
if (finished) {
that.triggerXAPIScored(that.getScore(), that.getMaxScore(), 'answered');
}
};
// Initialize the visible and invisible progress counters
$progressNumeric.html(that.options.solvedLabel + ' ' + this.progress + '/' + that.summaries.length);
var interpolatedProgressText = that.options.progressText
.replace(':num', that.progress)
.replace(':total', that.summaries.length);
$progress.html(interpolatedProgressText);
// Add elements to content
for (var i = 0; i < elements.length; i++) {
var element = elements[i];
if (i < that.progress) { // i is panelId
for (var j = 0; j < element.summaries.length; j++) {
var sum = element.summaries[j];
if (that.answer[sum.id]) {
$summary_list.append('
' + sum.text + ' ');
$summary_container.addClass('has-results');
break;
}
}
// Cannot use continue; due to id/animation system
}
// added aria-labelledby property for readspeaker to read, when first option receive focus
var $page = $('
');
// Create initial tip for first summary-list if tip is available
if (i==0 && element.tip !== undefined && element.tip.trim().length > 0) {
$evaluation_content.append(H5P.JoubelUI.createTip(element.tip, {
tipLabel: that.options.tipButtonLabel
}));
}
for (var j = 0; j < element.summaries.length; j++) {
var summaryLineClass = 'summary-claim-unclicked';
// If progress is at current task
if (that.progress === i && that.answers[that.progress]) {
// Check if there are any previous wrong answers.
for (var k = 0; k < that.answers[that.progress].length; k++) {
if (that.answers[that.progress][k] === element.summaries[j].id) {
summaryLineClass = 'summary-failed';
break;
}
}
}
var $node = $('' +
'
' +
element.summaries[j].text +
' ');
// added tabindex = 0 for the first option to avoid accessing rest of the options via TAB
(j == 0) ? $node.attr("tabindex", "0") : $node.attr("tabindex", "-1");
$node.on('focus', function() {
var ind = $(this).attr('data-name');
setFocusIndex(ind);
});
// function captures the index of currently focused option
var setFocusIndex = function(idx) {
currentFocusedOption = idx;
};
// Do not add click event for failed nodes
if (summaryLineClass === 'summary-failed') {
$page.append($node);
continue;
}
$node.click(function() {
selectedAlt($(this));
}).keydown(function (e) {
switch (e.which) {
case 13: // Enter
case 32: // Space
selectedAlt($(this), true);
e.preventDefault();
break;
case 37: // Left Arrow
case 38: // Up Arrow
// Go to previous Option
that.gotoPreviousOption(that, currentFocusedOption);
e.preventDefault();
break;
case 39: // Right Arrow
case 40: // Down Arrow
// Go to next Option
that.gotoNextOption(that, currentFocusedOption);
e.preventDefault();
break;
}
});
$page.append($node);
}
$options.append($page);
}
if (that.progress === elements.length) {
$evaluation_content.html(that.options.resultLabel);
that.doFinalEvaluation();
}
else {
// Show first panel
$('.h5p-panel:eq(' + (that.progress) + ')', that.$myDom).css({display: 'block'});
if (that.progress) {
that.offset = ($('.summary-claim-unclicked:visible:first', that.$myDom).outerHeight() * that.errorCounts.length);
}
}
that.trigger('resize');
return this.$myDom;
};
/**
* Announce if answered alternative was correct or wrong
* @param isCorrect
*/
Summary.prototype.announceAnswer = function (isCorrect) {
const announcement = isCorrect
? this.options.labelCorrect
: this.options.labelIncorrect;
this.$answerAnnouncer.html(announcement);
// Remove text so it can't be navigated to and read at a later point
setTimeout(function () {
this.$answerAnnouncer.html('');
}.bind(this), 100);
};
/**
* Returns true if answers have been given
*
* @return {boolean}
*/
Summary.prototype.getAnswerGiven = function () {
return this.errorCounts.length > 0;
};
/**
* Handles moving the focus from the current option to the previous option and changes tabindex accorindgly
*
*/
Summary.prototype.gotoPreviousOption = function (that, currentFocusedOption) {
this.currentFocusedOption = currentFocusedOption;
var totOptions = that.summaries[that.progress].summary.length;
var prevRadioEle = $("ul[data-panel="+that.progress+"] li[role='radio']", this.$myDom);
//prevRadioEle.removeAttr("tabindex");
prevRadioEle.attr("tabindex", "-1");
this.currentFocusedOption--;
if(this.currentFocusedOption < 0) {
var num = totOptions - 1;
prevRadioEle.eq(num).attr("tabindex", "0");
prevRadioEle.eq(num).focus();
}
else {
prevRadioEle.eq(this.currentFocusedOption).attr("tabindex", "0");
prevRadioEle.eq(this.currentFocusedOption).focus();
}
};
/**
* Handles moving the focus from the current option to the next option and changes tabindex accorindgly
*
*/
Summary.prototype.gotoNextOption = function (that, currentFocusedOption) {
this.currentFocusedOption = currentFocusedOption;
var totOptions = that.summaries[that.progress].summary.length;
var nextRadioEle = $("ul[data-panel="+that.progress+"] li[role='radio']", this.$myDom);
//nextRadioEle.removeAttr("tabindex");
nextRadioEle.attr("tabindex", "-1");
this.currentFocusedOption++;
if(this.currentFocusedOption == totOptions) {
nextRadioEle.eq(0).attr("tabindex", "0");
nextRadioEle.eq(0).focus();
}
else {
nextRadioEle.eq(this.currentFocusedOption).attr("tabindex", "0");
nextRadioEle.eq(this.currentFocusedOption).focus();
}
};
/**
* Calculate final score and display feedback.
*
* @param container
* @param options_panel
* @param list
* @param score
*/
Summary.prototype.doFinalEvaluation = function () {
var that = this;
var errorCount = this.countErrors();
var maxScore = that.summaries.length;
var score = maxScore - errorCount;
// Calculate percentage
var percent = 100 - (errorCount / that.errorCounts.length * 100);
// Show final evaluation
var summary = H5P.Question.determineOverallFeedback(that.options.overallFeedback, percent / 100)
.replace('@score', score)
.replace('@total', maxScore)
.replace('@percent', Math.round(percent));
$(".summary-evaluation-content", this.$myDom).removeAttr("tabindex");
var scoreBarLabel = that.options.scoreBarLabel.replace(':num', score).replace(':total', maxScore);
this.setFeedback(summary, score, maxScore, scoreBarLabel);
// Only read out the score after the progress is read
setTimeout(function() {
that.setBehaviour({disableReadSpeaker: false});
that.readFeedback();
that.read(scoreBarLabel);
}, 3000);
that.trigger('resize');
};
/**
* Resets the complete task back to its' initial state.
* Used for contracts.
*/
Summary.prototype.resetTask = function () {
this.offset = 0;
this.score = 0;
this.progress = 0;
this.answers = [];
this.answer = [];
this.errorCounts = [];
this.userResponses = [];
this.dataBitMap = [];
if (this.$myDom) {
const contentWrapper = this.$myDom[0].parentNode;
contentWrapper.innerHTML = '';
this.createQuestion();
contentWrapper.appendChild(this.$myDom[0]);
this.removeFeedback();
}
};
/**
* Adjust height of container.
*
* @param container
* @param elements
* @param el
*/
Summary.prototype.adjustTargetHeight = function (container, elements, el) {
var new_height = parseInt(elements.outerHeight()) + parseInt(el.outerHeight()) + parseInt(el.css('marginBottom')) + parseInt(el.css('marginTop'));
if (new_height > parseInt(container.css('height'))) {
container.animate({height: new_height});
}
};
/**
* Count amount of wrong answers
*
* @returns {number}
*/
Summary.prototype.countErrors = function() {
var error_count = 0;
// Count boards without errors
for (var i = 0; i < this.summaries.length; i++) {
if (this.errorCounts[i] === undefined) {
error_count++;
}
else {
error_count += this.errorCounts[i] ? 1 : 0;
}
}
return error_count;
};
/**
* Returns the choices array for xApi statements
*
* @param {String[]} answers
*
* @return {{ choices: []}}
*/
Summary.prototype.getXApiChoices = function (answers) {
var choices = answers.map(function(answer, index){
return XApiEventBuilder.createChoice(index.toString(), answer);
});
return {
choices: choices
};
};
/**
* Saves the user response
*
* @param {number} questionIndex
* @param {number} answerIndex
*/
Summary.prototype.storeUserResponse = function (questionIndex, answerIndex) {
var self = this;
if(self.userResponses[questionIndex] === undefined){
self.userResponses[questionIndex] = [];
}
self.userResponses[questionIndex].push(this.dataBitMap[questionIndex][answerIndex]);
};
/**
* Starts a stopwatch for indexed slide
*
* @param {number} index
*/
Summary.prototype.startStopWatch = function (index) {
this.stopWatches[index] = this.stopWatches[index] || new StopWatch();
this.stopWatches[index].start();
};
/**
* Stops a stopwatch for indexed slide
*
* @param {number} [index]
*/
Summary.prototype.stopStopWatch = function (index) {
if(this.stopWatches[index]){
this.stopWatches[index].stop();
}
};
/**
* Returns the passed time in seconds of a stopwatch on an indexed slide,
* or 0 if not existing
*
* @param {number} index
* @return {number}
*/
Summary.prototype.timePassedInStopWatch = function (index) {
if(this.stopWatches[index] !== undefined){
return this.stopWatches[index].passedTime();
}
else {
// if not created, return no passed time,
return 0;
}
};
/**
* Returns the time the user has spent on all questions so far
*
* @return {number}
*/
Summary.prototype.getTotalPassedTime = function () {
return this.stopWatches
.filter(function(watch){
return watch !== undefined;
})
.reduce(function(sum, watch){
return sum + watch.passedTime();
}, 0);
};
/**
* Creates an xAPI answered event for a single statement list
*
* @param {object} panel
* @param {number[]} userAnswer
* @param {number} panelIndex
* @param {number} duration
*
* @return {H5P.XAPIEvent}
*/
Summary.prototype.createXApiAnsweredEvent = function (panel, userAnswer, panelIndex, duration) {
var self = this;
// creates the definition object
var definition = XApiEventBuilder.createDefinition()
.name('Summary statement')
.description(self.options.intro)
.interactionType(XApiEventBuilder.interactionTypes.CHOICE)
.correctResponsesPattern(['0'])
.optional(self.getXApiChoices(panel.summary))
.build();
// create the result object
var result = XApiEventBuilder.createResult()
.response(userAnswer.join('[,]'))
.duration(duration)
.score((self.correctOnFirstTry(userAnswer) ? 1 : 0), 1)
.build();
return XApiEventBuilder.create()
.verb(XApiEventBuilder.verbs.ANSWERED)
.objectDefinition(definition)
.context(self.contentId, self.subContentId)
.contentId(self.contentId, panel.subContentId)
.result(result)
.build();
};
Summary.prototype.correctOnFirstTry = function(userAnswer){
return (userAnswer.length === 1) && userAnswer[0] === 0;
};
/**
* Retrieves the xAPI data necessary for generating result reports.
*
* @return {object}
*/
Summary.prototype.getXAPIData = function(){
var self = this;
// create array with userAnswer
var children = self.summaries.map(function(panel, index) {
var userResponse = self.userResponses[index] || [];
var duration = self.timePassedInStopWatch(index);
var event = self.createXApiAnsweredEvent(panel, userResponse, index, duration);
return {
statement: event.data.statement
};
});
var result = XApiEventBuilder.createResult()
.score(self.getScore(), self.getMaxScore())
.duration(self.getTotalPassedTime())
.build();
// creates the definition object
var definition = XApiEventBuilder.createDefinition()
.interactionType(XApiEventBuilder.interactionTypes.COMPOUND)
.name(self.getTitle())
.description(self.options.intro)
.build();
var xAPIEvent = XApiEventBuilder.create()
.verb(XApiEventBuilder.verbs.ANSWERED)
.contentId(self.contentId, self.subContentId)
.context(self.getParentAttribute('contentId'), self.getParentAttribute('subContentId'))
.objectDefinition(definition)
.result(result)
.build();
return {
statement: xAPIEvent.data.statement,
children: children
};
};
/**
* Returns an attribute from this.parent if it exists
*
* @param {string} attributeName
* @return {*|undefined}
*/
Summary.prototype.getParentAttribute = function (attributeName) {
var self = this;
if(self.parent !== undefined){
return self.parent[attributeName];
}
};
return Summary;
})(H5P.jQuery, H5P.Question, H5P.Summary.XApiEventBuilder, H5P.Summary.StopWatch);
;
var H5P = H5P || {};
/**
* A class that easily helps your create awesome drag and drop.
*
* @param {H5P.DragNBar} DnB
* @param {jQuery} $container
* @returns {undefined}
*/
H5P.DragNDrop = function (dnb, $container) {
H5P.EventDispatcher.call(this);
this.dnb = dnb;
this.$container = $container;
this.scrollLeft = 0;
this.scrollTop = 0;
};
// Inherit support for events
H5P.DragNDrop.prototype = Object.create(H5P.EventDispatcher.prototype);
H5P.DragNDrop.prototype.constructor = H5P.DragNDrop;
/**
* Set the current element
*
* @method setElement
* @param {j@uery} $element
*/
H5P.DragNDrop.prototype.setElement = function ($element) {
this.$element = $element;
};
/**
* Start tracking the mouse.
*
* @param {jQuery} $element
* @param {Number} x Start X coordinate
* @param {Number} y Start Y coordinate
* @returns {undefined}
*/
H5P.DragNDrop.prototype.press = function ($element, x, y) {
var that = this;
var eventData = {
instance: this
};
H5P.$window
.mousemove(eventData, H5P.DragNDrop.moveHandler)
.bind('mouseup', eventData, H5P.DragNDrop.release);
H5P.$body
// With user-select: none uncommented, after moving a drag and drop element, if I hover over something that changes transparancy on hover IE10 on WIN7 crashes
// TODO: Add user-select and -ms-user-select later if IE10 stops bugging
.css({'-moz-user-select': 'none', '-webkit-user-select': 'none'/*, 'user-select': 'none', '-ms-user-select': 'none'*/})
.attr('unselectable', 'on')[0]
.onselectstart = H5P.$body[0].ondragstart = function () {
return false;
};
that.containerOffset = $element.offsetParent().offset();
this.$element = $element;
this.moving = false;
this.startX = x;
this.startY = y;
this.containerWidth = this.$container[0].getBoundingClientRect().width;
this.containerHeight = this.$container[0].getBoundingClientRect().height;
this.marginX = parseInt($element.css('marginLeft')) + parseInt($element.css('marginRight'));
this.marginY = parseInt($element.css('marginTop')) + parseInt($element.css('marginBottom'));
var offset = $element.offset();
this.adjust = {
x: x - offset.left + this.marginX,
y: y - offset.top - this.marginY
};
if (that.dnb && that.dnb.newElement) {
this.move(x, y);
}
};
/**
* Handles mouse move events.
*
* @param {Event} event
*/
H5P.DragNDrop.moveHandler = function (event) {
event.stopPropagation();
event.data.instance.move(event.pageX, event.pageY);
};
/**
* Handles mouse movements.
*
* @param {number} x
* @param {number} y
*/
H5P.DragNDrop.prototype.move = function (x, y) {
if (!this.moving) {
if (this.startMovingCallback !== undefined && !this.startMovingCallback(x, y)) {
return;
}
// Start moving
this.moving = true;
this.$element.addClass('h5p-moving');
}
let angle = 0;
let setAngle = false;
// Finding angle on element in the css-transform
if(!setAngle) {
const styleElement = window.getComputedStyle(this.$element[0]);
const matrix = styleElement.getPropertyValue("transform");
if (matrix !== "none") {
const values = matrix.split("(")[1].split(")")[0].split(",");
const a = values[0];
const b = values[1];
angle = Math.round(Math.atan2(b, a) * (180 / Math.PI));
}
setAngle = true;
}
// Finding corner positions to ensure the element is never outside the container borders
// *************************************************************************************
const theElement = this.$element[0];
let left;
let top;
let width;
let height;
// When scaling the element by dragging on the 'dots', the transform-value is changing, not left and top, as it is when 'moving'/'dragging' the element.
// So we find the values 'translate x and y' and add them to left and top.
const transformCSSTranslateXYArray = theElement.style.transform.split("px");
let transformCSSTranslateX = 0;
let transformCSSTranslateY = 0;
if(transformCSSTranslateXYArray[0] != "") {
transformCSSTranslateX = (parseInt(transformCSSTranslateXYArray[0].match(/-?\d+/g)));
transformCSSTranslateY = (parseInt(transformCSSTranslateXYArray[1].match(/-?\d+/g)));
}
if(theElement.style.left.includes("%")) {
left = this.containerWidth * parseInt(theElement.style.left) / 100 + transformCSSTranslateX;
} else {
left = parseInt(theElement.style.left) + transformCSSTranslateX;
}
if(theElement.style.top.includes("%")) {
top = this.containerHeight * parseInt(theElement.style.top) / 100 + transformCSSTranslateY;
} else {
top = parseInt(theElement.style.top) + transformCSSTranslateY;
}
if(theElement.style.width.includes("%")) {
width = this.containerWidth * parseInt(theElement.style.width) / 100;
} else if(theElement.style.width.includes("em")) {
width = parseFloat(theElement.style.width) * 16;
}
else {
width = parseInt(theElement.style.width)
}
if(theElement.style.height.includes("%")) {
height = this.containerHeight * parseInt(theElement.style.height) / 100;
} else if(theElement.style.width.includes("em")) {
height = parseFloat(theElement.style.height) * 16;
}
else {
height = parseInt(theElement.style.height)
}
let origin = [left + 0.5 * width, top + 0.5 * height];
const topRightCorner0DegreesPos = [origin[0] + 0.5 * width, origin[1] - 0.5 * height];
const topLeftCorner0DegreesPos = [origin[0] - 0.5 * width, origin[1] - 0.5 * height];
const angleTopRight0Degrees = Math.atan2(origin[1] - topRightCorner0DegreesPos[1], topRightCorner0DegreesPos[0] - origin[0]) * 180 / Math.PI;
const angleTopLeft0Degrees = Math.atan2(origin[1] - topLeftCorner0DegreesPos[1], topLeftCorner0DegreesPos[0] - origin[0]) * 180 / Math.PI;
const angleBottomRight0Degrees = -angleTopRight0Degrees;
const angleBottomLeft0Degrees = -angleTopLeft0Degrees;
const hypToCorners = Math.sqrt(Math.pow((width/2),2) + Math.pow((height/2),2));
const newPosTopRightCorner = this.findNewPoint(origin[0], origin[1], (angleTopRight0Degrees - angle), hypToCorners);
const newPosTopleftCorner = this.findNewPoint(origin[0], origin[1], (angleTopLeft0Degrees - angle), hypToCorners);
const newPosBottomRightCorner = this.findNewPoint(origin[0], origin[1], (angleBottomRight0Degrees - angle), hypToCorners);
const newPosBottomLeftCorner = this.findNewPoint(origin[0], origin[1], (angleBottomLeft0Degrees - angle), hypToCorners);
const rightmostPoint = Math.max(newPosTopRightCorner[0], newPosTopleftCorner[0], newPosBottomLeftCorner[0], newPosBottomRightCorner[0]);
const rightmostPointAdjust = rightmostPoint - (left + width);
const leftmostPoint = Math.min(newPosTopRightCorner[0], newPosTopleftCorner[0], newPosBottomLeftCorner[0], newPosBottomRightCorner[0]);
const leftmostPointAdjust = left - leftmostPoint;
const topmostPoint = Math.min(newPosTopRightCorner[1], newPosTopleftCorner[1], newPosBottomLeftCorner[1], newPosBottomRightCorner[1]);
const topmostPointAdjust = top - topmostPoint;
const bottommostPoint = Math.max(newPosTopRightCorner[1], newPosTopleftCorner[1], newPosBottomLeftCorner[1], newPosBottomRightCorner[1]);
const bottommostPointAdjust = bottommostPoint - (top + height);
// Done finding corner positions
// ***********************************************************************************
x -= this.adjust.x;
y -= this.adjust.y;
// Adding leftmostPointAdjust and topmostPointAdjust to prevent rotated elements jumping when dragged.
var posX = x - this.containerOffset.left + this.scrollLeft + leftmostPointAdjust;
var posY = y - this.containerOffset.top + this.scrollTop + topmostPointAdjust;
if (this.snap !== undefined) {
posX = Math.round(posX / this.snap) * this.snap;
posY = Math.round(posY / this.snap) * this.snap;
}
// Do not move outside of minimum values.
// Adjusted values are added when the element is rotated.
if (this.min !== undefined) {
if ((posX - leftmostPointAdjust) < this.min.x) {
posX = this.min.x + leftmostPointAdjust;
x = this.min.x + this.containerOffset.left - this.scrollLeft;
}
if (posY - topmostPointAdjust < this.min.y) {
posY = this.min.y + topmostPointAdjust;
y = this.min.y + this.containerOffset.top - this.scrollTop;
}
}
if (this.dnb && this.dnb.newElement && posY >= 0) {
this.min.y = 0;
}
// Do not move outside of maximum values.
if (this.max !== undefined) {
if ((posX + width + rightmostPointAdjust) > (this.max.x + width)) {
posX = this.max.x - rightmostPointAdjust;
x = this.max.x + this.containerOffset.left - this.scrollLeft;
}
if (posY + height + bottommostPointAdjust > (this.max.y + height)) {
posY = this.max.y - bottommostPointAdjust;
y = this.max.y + this.containerOffset.top - this.scrollTop;
}
}
// Show transform panel if element has moved
var startX = this.startX - this.adjust.x - this.containerOffset.left + this.scrollLeft;
var startY = this.startY - this.adjust.y - this.containerOffset.top + this.scrollTop;
if (!this.snap && (posX !== startX || posY !== startY)) {
this.trigger('showTransformPanel');
}
else if (this.snap) {
var xChanged = (Math.round(posX / this.snap) * this.snap) !==
(Math.round(startX / this.snap) * this.snap);
var yChanged = (Math.round(posY / this.snap) * this.snap) !==
(Math.round(startY / this.snap) * this.snap);
if (xChanged || yChanged) {
this.trigger('showTransformPanel');
}
}
// Moving the element to the calculated position
this.$element.css({left: posX - transformCSSTranslateX, top: posY - transformCSSTranslateY});
if (this.dnb) {
this.dnb.updateCoordinates();
}
if (this.moveCallback) {
this.moveCallback(x, y, this.$element);
}
};
// Find position relative to origin by knowing origin, angle and distance
H5P.DragNDrop.prototype.findNewPoint = function (originX, originY, angle, distance) {
let result = [];
result.push(Math.cos(angle * Math.PI / 180) * distance + originX);
result.push(-Math.sin(angle * Math.PI / 180) * distance + originY);
return result;
}
/**
* Stop tracking the mouse.
*
* @param {Object} event
* @returns {undefined}
*/
H5P.DragNDrop.release = function (event) {
var that = event.data.instance;
H5P.$window
.unbind('mousemove', H5P.DragNDrop.moveHandler)
.unbind('mouseup', H5P.DragNDrop.release);
H5P.$body
.css({'-moz-user-select': '', '-webkit-user-select': ''/*, 'user-select': '', '-ms-user-select': ''*/})
.removeAttr('unselectable')[0]
.onselectstart = H5P.$body[0].ondragstart = null;
if (that.releaseCallback !== undefined) {
that.releaseCallback();
}
if (that.moving) {
that.$element.removeClass('h5p-moving');
if (that.stopMovingCallback !== undefined) {
that.stopMovingCallback(event);
}
}
// trigger to hide the transform panel unless it was activated
// through the context menu
that.trigger('hideTransformPanel');
};
;
/*global H5P*/
H5P.DragNResize = (function ($, EventDispatcher) {
/**
* Constructor!
*
* @class H5P.DragNResize
* @param {H5P.jQuery} $container
*/
function C($container) {
var self = this;
this.$container = $container;
self.disabledModifiers = false;
this.minSize = H5P.DragNResize.MIN_SIZE;
EventDispatcher.call(this);
// Override settings for snapping to grid, and locking aspect ratio.
H5P.$body.keydown(function (event) {
if (self.disabledModifiers) {
return;
}
if (event.keyCode === 17) {
// Ctrl
self.revertSnap = true;
}
else if (event.keyCode === 16) {
// Shift
self.revertLock = true;
}
}).keyup(function (event) {
if (self.disabledModifiers) {
return;
}
if (event.keyCode === 17) {
// Ctrl
self.revertSnap = false;
}
else if (event.keyCode === 16) {
// Shift
self.revertLock = false;
}
});
}
// Inheritance
C.prototype = Object.create(EventDispatcher.prototype);
C.prototype.constructor = C;
/**
* Gives the given element a resize handle.
*
* @param {H5P.jQuery} $element
* @param {Object} [options]
* @param {boolean} [options.lock]
* @param {boolean} [options.cornerLock]
* @param {string} [options.directionLock]
* @param {number} [options.minSize]
*/
C.prototype.add = function ($element, options) {
var that = this;
// Array with position of handles
var cornerPositions = ['nw', 'ne', 'sw', 'se'];
var horizontalEdgePositions = ['w', 'e'];
var verticalEdgePositions = ['n', 's'];
var addResizeHandle = function (position, corner) {
$('
', {
'class': 'h5p-dragnresize-handle ' + position
}).mousedown(function (event) {
that.lock = (options && (options.lock || corner && options.cornerLock));
if (options.cornerLock) {
that.isImage = true;
}
that.$element = $element;
that.press(event.clientX, event.clientY, position, options.minSize);
}).data('position', position)
.appendTo($element);
};
if (!options.directionLock) {
cornerPositions.forEach(function (pos) {
addResizeHandle(pos, true);
});
}
// Add edge handles
if (!options || !options.lock) {
if (options.directionLock != "vertical") {
horizontalEdgePositions.forEach(function (pos) {
addResizeHandle(pos);
});
}
if (options.directionLock != "horizontal") {
verticalEdgePositions.forEach(function (pos) {
addResizeHandle(pos);
});
}
}
if (options.minSize) {
this.minSize = options.minSize;
}
else {
this.minSize = H5P.DragNResize.MIN_SIZE;
}
};
/**
* Get paddings for the element
*/
C.prototype.getElementPaddings = function () {
return {
horizontal: Number(this.$element.css('padding-left').replace("px", "")) + Number(this.$element.css('padding-right').replace("px", "")),
vertical: Number(this.$element.css('padding-top').replace("px", "")) + Number(this.$element.css('padding-bottom').replace("px", ""))
};
};
/**
* Get borders for the element
* @returns {{horizontal: number, vertical: number}}
*/
C.prototype.getElementBorders = function () {
return {
horizontal: Number(this.$element.css('border-left-width').replace('px', '')) + Number(this.$element.css('border-right-width').replace('px', '')),
vertical: Number(this.$element.css('border-top-width').replace('px', '')) + Number(this.$element.css('border-bottom-width').replace('px', ''))
};
};
C.prototype.setContainerEm = function (containerEm) {
this.containerEm = containerEm;
};
/**
* Start resizing
*
* @param {number} x
* @param {number} y
* @param {String} [direction] Direction of resize
* @param {number} minSize
*/
C.prototype.press = function (x, y, direction, minSize) {
this.active = true;
var eventData = {
instance: this,
direction: direction
};
this.minSize = (minSize ? minSize : H5P.DragNResize.MIN_SIZE);
H5P.$window
.bind('mouseup', eventData, C.release)
.mousemove(eventData, C.move);
H5P.$body
.css({
'-moz-user-select': 'none',
'-webkit-user-select': 'none',
'user-select': 'none',
'-ms-user-select': 'none'
})
.attr('unselectable', 'on')[0]
.onselectstart = H5P.$body[0].ondragstart = function () {
return false;
};
this.startX = x;
this.startY = y;
this.padding = this.getElementPaddings();
this.borders = this.getElementBorders();
this.startWidth = this.$element.outerWidth();
this.startHeight = this.$element.outerHeight();
this.ratio = (this.startWidth / this.startHeight);
var position = this.$element.position();
this.left = position.left;
this.top = position.top;
this.containerWidth = this.$container.width();
this.containerHeight = this.$container.height();
// Set default values
this.newLeft = this.left;
this.newTop = this.top;
this.newWidth = this.startWidth;
this.newHeight = this.startHeight;
this.trigger('startResizing', eventData);
// Show transform panel
this.trigger('showTransformPanel');
};
/**
* Resize events
*
* @param {Event} event
*/
C.move = function (event) {
var direction = (event.data.direction ? event.data.direction : 'se');
var that = event.data.instance;
var moveW = (direction === 'nw' || direction === 'sw' || direction === 'w');
var moveN = (direction === 'nw' || direction === 'ne' || direction === 'n');
var moveDiagonally = (direction === 'nw' || direction === 'ne' || direction === 'sw' || direction === 'se');
var movesHorizontal = (direction === 'w' || direction === 'e');
var movesVertical = (direction === 'n' || direction === 's');
var deltaX = that.startX - event.clientX;
var deltaY = that.startY - event.clientY;
that.minLeft = that.left + that.startWidth - that.minSize;
that.minTop = that.top + that.startHeight - that.minSize;
// Moving west
if (moveW) {
that.newLeft = that.left - deltaX;
that.newWidth = that.startWidth + deltaX;
// Check edge cases
if (that.newLeft < 0) {
that.newLeft = 0;
that.newWidth = that.left + that.startWidth;
}
else if (that.newLeft > that.minLeft) {
that.newLeft = that.minLeft;
that.newWidth = that.left - that.minLeft + that.startWidth;
}
// Snap west side
if (that.snap && !that.revertSnap) {
that.newLeft = Math.round(that.newLeft / that.snap) * that.snap;
// Make sure element does not snap east
if (that.newLeft > that.minLeft) {
that.newLeft = Math.floor(that.minLeft / that.snap) * that.snap;
}
that.newWidth = (that.left - that.newLeft) + that.startWidth;
}
}
else if (!movesVertical) {
that.newWidth = that.startWidth - deltaX;
// Snap width
if (that.snap && !that.revertSnap) {
that.newWidth = Math.round(that.newWidth / that.snap) * that.snap;
}
if (that.left + that.newWidth > that.containerWidth) {
that.newWidth = that.containerWidth - that.left;
}
}
// Moving north
if (moveN) {
that.newTop = that.top - deltaY;
that.newHeight = that.startHeight + deltaY;
// Check edge cases
if (that.newTop < 0) {
that.newTop = 0;
that.newHeight = that.top + that.startHeight;
}
else if (that.newTop > that.minTop) {
that.newTop = that.minTop;
that.newHeight = that.top - that.minTop + that.startHeight;
}
// Snap north
if (that.snap && !that.revertSnap) {
that.newTop = Math.round(that.newTop / that.snap) * that.snap;
// Make sure element does not snap south
if (that.newTop > that.minTop) {
that.newTop = Math.floor(that.minTop / that.snap) * that.snap;
}
that.newHeight = (that.top - that.newTop) + that.startHeight;
}
}
else if (!movesHorizontal) {
that.newHeight = that.startHeight - deltaY;
// Snap height
if (that.snap && !that.revertSnap) {
that.newHeight = Math.round(that.newHeight / that.snap) * that.snap;
}
if (that.top + that.newHeight > that.containerHeight) {
that.newHeight = that.containerHeight - that.top;
}
}
// Set min size
if (that.newWidth <= that.minSize) {
that.newWidth = that.minSize;
}
if (that.newHeight <= that.minSize) {
that.newHeight = that.minSize;
}
// Apply ratio lock for elements except images, they have a their own specific for corner cases
var lock = (that.revertLock ? !that.lock : that.lock);
if (lock && (moveDiagonally || !that.isImage)) {
that.lockDimensions(moveW, moveN, movesVertical, movesHorizontal);
}
// Reduce size by padding and borders
that.finalWidth = that.newWidth;
that.finalHeight = that.newHeight;
if (that.$element.css('boxSizing') !== 'border-box') {
that.finalWidth -= (that.padding.horizontal + that.borders.horizontal);
that.finalHeight -= (that.padding.vertical + that.borders.vertical);
}
that.$element.css({
width: (that.finalWidth / that.containerEm) + 'em',
height: (that.finalHeight / that.containerEm) + 'em',
left: ((that.newLeft / that.containerWidth) * 100) + '%',
top: ((that.newTop / that.containerHeight) * 100) + '%'
});
that.trigger('moveResizing');
};
/**
* Changes element values depending on moving direction of the element
* @param isMovingWest
* @param isMovingNorth
* @param movesVertical
* @param movesHorizontal
*/
C.prototype.lockDimensions = function (isMovingWest, isMovingNorth, movesVertical, movesHorizontal) {
var self = this;
// Cap movement at top
var lockTop = function (isMovingNorth) {
if (!isMovingNorth) {
return;
}
self.newTop = self.top - (self.newHeight - self.startHeight);
// Edge case
if (self.newTop <= 0) {
self.newTop = 0;
}
};
// Expand to longest edge
if (movesVertical) {
this.newWidth = this.newHeight * this.ratio;
// Make sure locked ratio does not cause size to go below min size
if (this.newWidth < this.minSize) {
this.newWidth = this.minSize;
this.newHeight = this.minSize / this.ratio;
}
}
else if (movesHorizontal) {
this.newHeight = this.newWidth / this.ratio;
// Make sure locked ratio does not cause size to go below min size
if (this.newHeight < this.minSize) {
this.newHeight = this.minSize;
this.newWidth = this.minSize * this.ratio;
}
}
else if (this.newWidth / this.startWidth > this.newHeight / this.startHeight) {
// Expand to width
this.newHeight = this.newWidth / this.ratio;
}
else {
// Expand to height
this.newWidth = this.newHeight * this.ratio;
}
// Change top to match new height
if (isMovingNorth) {
lockTop(isMovingNorth);
if (self.newTop <= 0) {
self.newHeight = self.top + self.startHeight;
self.newWidth = self.newHeight * self.ratio;
}
}
else {
// Too high
if (this.top + this.newHeight > this.containerHeight) {
this.newHeight = this.containerHeight - this.top;
this.newWidth = this.newHeight * this.ratio;
}
}
// Change left to match new width
if (isMovingWest) {
this.newLeft = this.left - (this.newWidth - this.startWidth);
// Edge case
if (this.newLeft <= 0) {
this.newLeft = 0;
this.newWidth = this.left + this.startWidth;
this.newHeight = this.newWidth / this.ratio;
}
}
else {
// Too wide
if (this.left + this.newWidth > this.containerWidth) {
this.newWidth = this.containerWidth - this.left;
this.newHeight = this.newWidth / this.ratio;
}
}
// Need to re-lock top in case height changed
lockTop(isMovingNorth);
};
/**
* Stop resizing
*
* @param {Event} event
*/
C.release = function (event) {
var that = event.data.instance;
that.active = false;
H5P.$window
.unbind('mouseup', C.release)
.unbind('mousemove', C.move);
H5P.$body
.css({
'-moz-user-select': '',
'-webkit-user-select': '',
'user-select': '',
'-ms-user-select': ''
})
.removeAttr('unselectable')[0]
.onselectstart = H5P.$body[0].ondragstart = null;
if (that.newWidth !== that.startWidth ||
that.newHeight !== that.startHeight) {
// Stopped resizing send width and height in Ems
that.trigger('stoppedResizing', {
left: that.newLeft,
top: that.newTop,
width: that.finalWidth / that.containerEm,
height: that.finalHeight / that.containerEm
});
}
// Refocus element after resizing it. Apply timeout since focus is lost at the end of mouse event.
setTimeout(function () {
that.$element.focus();
}, 0);
// trigger to hide the transform panel unless it was activated
// through the context menu
that.trigger('hideTransformPanel');
};
/**
* Toggle modifiers when we are not interacting with drag objects.
* @param {boolean} [enable]
*/
C.prototype.toggleModifiers = function (enable) {
this.disabledModifiers = enable === undefined ? !this.disabledModifiers : !enable;
};
C.MIN_SIZE = 24;
return C;
})(H5P.jQuery, H5P.EventDispatcher);
;
H5P.DragNBar = (function (EventDispatcher) {
var nextInstanceIndex = 0;
/**
* Constructor. Initializes the drag and drop menu bar.
*
* @class
* @param {Array} buttons
* @param {H5P.jQuery} $container
* @param {H5P.jQuery} $dialogContainer
* @param {object} [options] Collection of options
* @param {boolean} [options.disableEditor=false] Determines if DragNBar should be displayed in view or editor mode
* @param {boolean} [options.enableCopyPaste=true] Determines if copy & paste is supported
* @param {H5P.jQuery} [options.$blurHandlers] When clicking these element(s) dnb focus will be lost
* @param {object} [options.libraries] Libraries to check against for paste notification
*/
function DragNBar(buttons, $container, $dialogContainer, options) {
var self = this;
EventDispatcher.call(this);
this.overflowThreshold = 13; // How many buttons to display before we add the more button.
this.buttons = buttons;
this.$container = $container;
this.$dialogContainer = $dialogContainer;
this.dnd = new H5P.DragNDrop(this, $container);
this.dnd.snap = 10;
this.newElement = false;
this.enabled = true;
var defaultOptions = {
disableEditor: false,
enableCopyPaste: true
};
options = H5P.jQuery.extend(defaultOptions, options);
this.enableCopyPaste = options.enableCopyPaste;
this.isEditor = !options.disableEditor;
this.$blurHandlers = options.$blurHandlers ? options.$blurHandlers : undefined;
this.libraries = options.libraries;
this.instanceIndex = nextInstanceIndex++;
/**
* Keeps track of created DragNBar elements
* @type {Array}
*/
this.elements = [];
// Create a popup dialog
this.dialog = new H5P.DragNBarDialog($dialogContainer, $container);
// Fix for forcing redraw on $container, to avoid "artifcats" on safari
this.$container.addClass('hardware-accelerated');
if (this.isEditor) {
this.transformButtonActive = false;
this.initEditor();
this.initClickListeners();
H5P.$window.resize(function () {
self.resize();
});
}
/**
* Add button group.
*
* @private
* @param {object[]} Buttons.
* @param {H5P.jQuery} $button Button to add button group to.
* @param {object} [options] Options.
* @param {string} [options.title] Title for the group.
*/
this.addButtonGroup = function (buttons, $button, options) {
const $buttonGroup = H5P.jQuery('
');
// Add optional title to the group
if (options && options.title && options.title !=='') {
H5P.jQuery('
' + options.title + '
')
.appendTo($buttonGroup);
}
// Container for buttons
const $buttonGroupButtons = H5P.jQuery('
')
.appendTo($buttonGroup);
// Add buttons
buttons.forEach(function (button) {
self.addButton(button, $buttonGroupButtons);
});
$buttonGroup.insertAfter($button.parent());
return $buttonGroup;
};
}
// Inherit support for events
DragNBar.prototype = Object.create(EventDispatcher.prototype);
DragNBar.prototype.constructor = DragNBar;
return DragNBar;
})(H5P.EventDispatcher);
/**
* Initializes editor functionality of DragNBar
*/
H5P.DragNBar.prototype.initEditor = function () {
var that = this;
this.dnr = new H5P.DragNResize(this.$container);
this.dnr.snap = 10;
// Update coordinates when element is resized
this.dnr.on('moveResizing', function () {
var offset = that.$element.offset();
var position = that.$element.position();
that.updateCoordinates(offset.left, offset.top, position.left, position.top);
});
// Set pressed to not lose focus at the end of resize
this.dnr.on('stoppedResizing', function () {
that.pressed = true;
// Delete pressed after dnbelement has been refocused so it will lose focus on single click.
setTimeout(function () {
delete that.pressed;
}, 10);
});
/**
* Show transform panel listeners
*/
this.dnr.on('showTransformPanel', function () {
TransformPanel(true);
});
this.dnd.on('showTransformPanel', function () {
TransformPanel(true);
});
/**
* Hide transform panel listeners
*/
this.dnr.on('hideTransformPanel', function () {
if (!that.transformButtonActive) {
TransformPanel(false);
}
});
this.dnd.on('hideTransformPanel', function () {
if (!that.transformButtonActive) {
TransformPanel(false);
}
});
/**
* Trigger a context menu transform to either show or hide
* the transform panel.
*
* @param {boolean} show
*/
function TransformPanel(show) {
if (that.focusedElement) {
that.focusedElement.contextMenu.trigger('contextMenuTransform', {showTransformPanel: show});
}
}
this.dnd.startMovingCallback = function () {
that.dnd.min = {x: 0, y: 0};
that.dnd.max = {
x: that.$container.width() - that.$element.outerWidth(),
y: that.$container.height() - that.$element.outerHeight()
};
if (that.newElement) {
that.dnd.adjust.x = 10;
that.dnd.adjust.y = 10;
that.dnd.min.y -= that.$list.height();
}
return true;
};
this.dnd.stopMovingCallback = function () {
var pos = {};
if (that.newElement) {
that.$container.css('overflow', '');
if (Math.round(parseFloat(that.$element.css('top'))) < 0) {
// Try to center element, but avoid overlapping
pos.x = (that.dnd.max.x / 2);
pos.y = (that.dnd.max.y / 2);
that.avoidOverlapping(pos, that.$element);
}
}
if (pos.x === undefined || pos.y === undefined ) {
pos.x = Math.round(parseFloat(that.$element.css('left')));
pos.y = Math.round(parseFloat(that.$element.css('top')));
}
that.stopMoving(pos.x, pos.y);
that.newElement = false;
delete that.dnd.min;
delete that.dnd.max;
};
};
/**
* Tries to position the given element close to the requested coordinates.
* Element can be skipped to check if spot is available.
*
* @param {object} pos
* @param {number} pos.x
* @param {number} pos.y
* @param {(H5P.jQuery|Object)} element object with width&height if ran before insertion.
*/
H5P.DragNBar.prototype.avoidOverlapping = function (pos, $element) {
// Determine size of element
var size = $element;
if (size instanceof H5P.jQuery) {
size = window.getComputedStyle(size[0]);
size = {
width: parseFloat(size.width),
height: parseFloat(size.height)
};
}
else {
$element = undefined;
}
// Determine how much they can be manuvered
var containerStyle = window.getComputedStyle(this.$container[0]);
var manX = parseFloat(containerStyle.width) - size.width;
var manY = parseFloat(containerStyle.height) - size.height;
var limit = 16;
var attempts = 0;
while (attempts < limit && this.elementOverlaps(pos.x, pos.y, $element)) {
// Try to place randomly inside container
if (manX > 0) {
pos.x = Math.floor(Math.random() * manX);
}
if (manY > 0) {
pos.y = Math.floor(Math.random() * manY);
}
attempts++;
}
};
/**
* Determine if moving the given element to its new position will cause it to
* cover another element. This can make new or pasted elements difficult to see.
* Element can be skipped to check if spot is available.
*
* @param {number} x
* @param {number} y
* @param {H5P.jQuery} [$element]
* @returns {boolean}
*/
H5P.DragNBar.prototype.elementOverlaps = function (x, y, $element) {
var self = this;
// Use snap grid
x = Math.round(x / 10);
y = Math.round(y / 10);
for (var i = 0; i < self.elements.length; i++) {
var element = self.elements[i];
if ($element !== undefined && element.$element === $element) {
continue;
}
if (x === Math.round(parseFloat(element.$element.css('left')) / 10) &&
y === Math.round(parseFloat(element.$element.css('top')) / 10)) {
return true; // Stop loop
}
}
return false;
};
// Key coordinates
var SHIFT = 16;
var CTRL = 17;
var DELETE = 46;
var BACKSPACE = 8;
var C = 67;
var V = 86;
var LEFT = 37;
var UP = 38;
var RIGHT = 39;
var DOWN = 40;
// Keep track of key state
var ctrlDown = false;
// How many pixels to move
var snapAmount = 1;
/**
* Handle keydown events for the entire frame
*/
H5P.DragNBar.keydownHandler = function (event) {
var self = event.data.instance;
var activeElement = document.activeElement;
// Don't care about keys if parent editor is not in focus
// This means all editors using drag-n-bar need to set a tabindex
// (it's not done inside this library)
if (self.$dialogContainer.find(activeElement).length === 0 && self.$dialogContainer.get(0) !== activeElement) {
return;
}
if (event.which === CTRL) {
ctrlDown = true;
if (self.dnd.snap !== undefined) {
// Disable snapping
delete self.dnd.snap;
}
}
if (event.which === SHIFT) {
snapAmount = self.dnd.snap;
}
if (event.which === LEFT && self.focusedElement) {
if (activeElement.contentEditable === 'true' || activeElement.value !== undefined) {
return;
}
event.preventDefault();
self.moveWithKeys(-snapAmount, 0);
}
else if (event.which === UP && self.focusedElement) {
if (activeElement.contentEditable === 'true' || activeElement.value !== undefined) {
return;
}
event.preventDefault();
self.moveWithKeys(0, -snapAmount);
}
else if (event.which === RIGHT && self.focusedElement) {
if (activeElement.contentEditable === 'true' || activeElement.value !== undefined) {
return;
}
event.preventDefault();
self.moveWithKeys(snapAmount, 0);
}
else if (event.which === DOWN && self.focusedElement) {
if (activeElement.contentEditable === 'true' || activeElement.value !== undefined) {
return;
}
event.preventDefault();
self.moveWithKeys(0, snapAmount);
}
else if (event.which === C && ctrlDown && self.focusedElement && self.$container.is(':visible')) {
self.copyHandler(event);
}
else if (event.which === V && ctrlDown && window.localStorage && self.$container.is(':visible')) {
self.pasteHandler(event);
}
else if ((event.which === DELETE || event.which === BACKSPACE) && self.focusedElement && self.$container.is(':visible') && activeElement.tagName.toLowerCase() !== 'input') {
if (self.pressed === undefined) {
self.focusedElement.contextMenu.trigger('contextMenuRemove');
event.preventDefault(); // Prevent browser navigating back
}
}
};
/**
* Copy object.
* @param {Event} event - Event to check for copyable content.
*/
H5P.DragNBar.prototype.copyHandler = function (event) {
if (!this.enableCopyPaste) {
return;
}
var self = event === undefined ? this : event.data.instance;
// Copy element params to clipboard
var elementSize = window.getComputedStyle(self.focusedElement.$element[0]);
var width = parseFloat(elementSize.width);
var height = parseFloat(elementSize.height) / width;
width = width / (parseFloat(window.getComputedStyle(self.$container[0]).width) / 100);
height *= width;
self.focusedElement.toClipboard(width, height);
H5P.externalDispatcher.trigger('datainclipboard', {reset: false});
};
/**
* Paste object.
* @param {Event} event - Event to check for pastable content.
*/
H5P.DragNBar.prototype.pasteHandler = function (event) {
var self = event === undefined ? this : event.data.instance;
var activeElement = document.activeElement;
// Don't paste if parent editor is not in focus
if (!this.enableCopyPaste || self.preventPaste || self.dialog.isOpen() ||
activeElement.contentEditable === 'true' || activeElement.value !== undefined) {
return;
}
if (self.$pasteButton.hasClass('disabled')) {
// Inform user why pasting is not possible
const pasteCheck = H5PEditor.canPastePlus(H5P.getClipboard(), this.libraries);
if (pasteCheck.canPaste !== true) {
if (pasteCheck.reason === 'pasteTooOld' || pasteCheck.reason === 'pasteTooNew') {
self.confirmPasteError(pasteCheck.description, 0, function () {});
}
else {
H5PEditor.attachToastTo(
self.$pasteButton.get(0),
pasteCheck.description,
{position: {horizontal: 'center', vertical: 'above', noOverflowX: true}}
);
}
return;
}
}
var clipboardData = localStorage.getItem('h5pClipboard');
if (clipboardData) {
// Parse
try {
clipboardData = JSON.parse(clipboardData);
}
catch (err) {
console.error('Unable to parse JSON from clipboard.', err);
return;
}
// Update file URLs
H5P.DragNBar.updateFileUrls(clipboardData.specific, function (path) {
var isTmpFile = (path.substr(-4,4) === '#tmp');
if (!isTmpFile && clipboardData.contentId) {
// Comes from existing content
let prefix;
if (H5PEditor.contentId) {
// .. to existing content
prefix = '../' + clipboardData.contentId + '/';
}
else {
// .. to new content
prefix = (H5PEditor.contentRelUrl ? H5PEditor.contentRelUrl : '../content/') + clipboardData.contentId + '/';
}
return path.substr(0, prefix.length) === prefix ? path : prefix + path;
}
return path; // Will automatically be looked for in tmp folder
});
if (clipboardData.generic) {
// Use reference instead of key
clipboardData.generic = clipboardData.specific[clipboardData.generic];
// Avoid multiple content with same ID
delete clipboardData.generic.subContentId;
}
self.trigger('paste', clipboardData);
}
};
/**
* Set state of paste button.
* @param {boolean} canPaste - If true, button will be enabled
*/
H5P.DragNBar.prototype.setCanPaste = function (canPaste) {
canPaste = canPaste || false;
if (this.$pasteButton) {
this.$pasteButton.toggleClass('disabled', !canPaste);
}
};
/**
* Confirm replace if there is content selected
*
* @param {number} top Offset
* @param {function} next Next callback
*/
H5P.DragNBar.prototype.confirmPasteError = function (message, top, next) {
// Confirm changing library
var confirmReplace = new H5P.ConfirmationDialog({
headerText: H5PEditor.t('core', 'pasteError'),
dialogText: message,
cancelText: ' ',
confirmText: H5PEditor.t('core', 'ok')
}).appendTo(document.body);
confirmReplace.on('confirmed', next);
confirmReplace.show(top);
};
/**
* Handle keypress events for the entire frame
*/
H5P.DragNBar.keypressHandler = function (event) {
var self = event.data.instance;
if (event.which === BACKSPACE && self.focusedElement && self.$container.is(':visible') && document.activeElement.tagName.toLowerCase() !== 'input') {
event.preventDefault(); // Prevent browser navigating back
}
};
/**
* Handle keyup events for the entire frame
*/
H5P.DragNBar.keyupHandler = function (event) {
var self = event.data.instance;
if (event.which === CTRL) {
// Update key state
ctrlDown = false;
// Enable snapping
self.dnd.snap = 10;
}
if (event.which === SHIFT) {
snapAmount = 1;
}
if (self.focusedElement && (event.which === LEFT || event.which === UP || event.which === RIGHT || event.which === DOWN)) {
// Store position of element after moving
var position = self.getElementSizeNPosition();
self.stopMoving(Math.round(position.left), Math.round(position.top));
}
};
/**
* Handle click events for the entire frame
*/
H5P.DragNBar.clickHandler = function (event) {
var self = event.data.instance;
// Remove pressed on click
delete self.pressed;
};
/**
* Initialize click listeners
*/
H5P.DragNBar.prototype.initClickListeners = function () {
var self = this;
var index = self.instanceIndex;
// Register event listeners
var eventData = {
instance: self
};
H5P.$body.on('keydown.dnb' + index, eventData, H5P.DragNBar.keydownHandler)
.on('keypress.dnb' + index, eventData, H5P.DragNBar.keypressHandler)
.on('keyup.dnb' + index, eventData, H5P.DragNBar.keyupHandler)
.on('click.dnb' + index, eventData, H5P.DragNBar.clickHandler);
// Set blur handler element if option has been specified
var $blurHandlers = this.$container;
if (this.$blurHandlers) {
$blurHandlers = this.$blurHandlers;
}
function handleBlur() {
// Remove coordinates picker if we didn't press an element.
if (self.pressed !== undefined) {
delete self.pressed;
}
else {
self.blurAll();
if (self.focusedElement !== undefined) {
delete self.focusedElement;
}
}
}
$blurHandlers
.keydown(function (e) {
if (e.which === 9) { // pressed tab
handleBlur();
}
})
.click(handleBlur);
};
/**
* Update file URLs. Useful when copying between different contents.
*
* @param {object} params Reference
* @param {function} handler Modifies the path to work when pasted
*/
H5P.DragNBar.updateFileUrls = function (params, handler) {
for (var prop in params) {
if (params.hasOwnProperty(prop) && params[prop] instanceof Object) {
var obj = params[prop];
if (obj.path !== undefined && obj.mime !== undefined) {
obj.path = handler(obj.path);
}
else {
H5P.DragNBar.updateFileUrls(obj, handler);
}
}
}
};
/**
* Attaches the menu bar to the given wrapper.
*
* @param {jQuery} $wrapper
* @returns {undefined}
*/
H5P.DragNBar.prototype.attach = function ($wrapper) {
var self = this;
$wrapper.html('');
$wrapper.addClass('h5peditor-dragnbar');
var $list = H5P.jQuery('
').appendTo($wrapper);
this.$list = $list;
for (var i = 0; i < this.buttons.length; i++) {
var button = this.buttons[i];
if (i === this.overflowThreshold) {
const $buttonMore = H5P.jQuery('
');
$list = $buttonMore
.appendTo($list)
.click(function (e) {
$list.stop().slideToggle(300);
e.preventDefault();
})
.children(':first')
.next();
// Close "more" on click somewhere else
H5P.jQuery(document).click(function (event) {
if (!H5P.jQuery(event.target).is($buttonMore.find('.h5p-dragnbar-more-button')) && $list.css('display') !== 'none') {
$list.stop().slideToggle(300);
}
});
}
this.addButton(button, $list);
}
if (this.enableCopyPaste) {
// Paste button
this.$pasteButton = H5P.jQuery(
'
' +
' ' +
' '
);
H5P.jQuery('
', {
'class': 'h5p-dragnbar-tooltip',
'text': H5PEditor.t('H5P.DragNBar', 'paste')
}).appendTo(this.$pasteButton);
this.$pasteButton.find('.h5p-dragnbar-paste-button').click(function (event) {
event.preventDefault(); // Avoid anchor click making window scroll
self.pasteHandler();
});
if (this.buttons.length > this.overflowThreshold) {
this.$pasteButton.insertAfter($list.parent());
}
else {
this.$pasteButton.appendTo($list);
}
}
this.containTooltips();
};
/**
* Add button.
*
* @param {type} button
* @param {Function} button.createElement Function for creating element
* @param {type} $list
* @returns {undefined}
*/
H5P.DragNBar.prototype.addButton = function (button, $list) {
var that = this;
const hasTitle = (button.title && button.title !== '');
const ariaLabel = hasTitle ? ' aria-label="' + button.title + '"' : '';
var $button = H5P.jQuery(
'' +
' ' +
' '
).appendTo($list);
// Prevent empty tooltips (would show on Firefox)
if (hasTitle) {
H5P.jQuery(' ', {
'class': 'h5p-dragnbar-tooltip',
'text': button.title
}).appendTo($button);
}
let $buttonGroup;
if (button.type === 'group') {
// Create dropdown button group
$buttonGroup = this.addButtonGroup(button.buttons, $button, {title: button.titleGroup});
$buttonGroup.addClass('h5peditor-dragnbar-gone');
// Close group on click somewhere else
H5P.jQuery(document).click(function (event) {
const hitButton = H5P.jQuery(event.target).is($button); // Closing handled by button itself
const hitButtonGroup = H5P.jQuery(event.target).closest('.h5p-dragnbar-button-group').length === 1;
if (!hitButton && !hitButtonGroup) {
$buttonGroup.toggleClass('h5peditor-dragnbar-gone', true);
$button.find('.h5p-dragnbar-tooltip').toggleClass('h5peditor-dragnbar-gone', false);
}
});
}
$button
.hover(function () {
that.containTooltips();
})
.children()
.click(function () {
return false;
}).mousedown(function (event) {
if (event.which !== 1 || !that.enabled) {
return;
}
// Switch between normal button and dropdown button group
if (button.type === 'group') {
if ($buttonGroup !== undefined) {
// Set position here, because content types might add buttons out of order
const offset = parseFloat($button.closest('.h5p-dragnbar').css('padding-left'));
const position = $button.position().left - $buttonGroup.position().left - offset;
if (position > 0) {
$buttonGroup.css('left', position);
}
// Show dropdown and hide buttons tooltip
$buttonGroup.toggleClass('h5peditor-dragnbar-gone');
$button.find('.h5p-dragnbar-tooltip').toggleClass('h5peditor-dragnbar-gone');
}
}
else {
that.newElement = true;
that.pressed = true;
var createdElement = button.createElement();
that.$element = createdElement;
that.$container.css('overflow', 'visible');
// y = 0 will make sure this press is regarded as outside of canvas to place element correctly
that.dnd.press(that.$element, event.pageX, 0);
that.focus(that.$element);
}
});
};
/**
* Contain tooltips.
*
* @returns {undefined}
*/
H5P.DragNBar.prototype.containTooltips = function () {
var that = this;
var containerWidth = that.$container.outerWidth();
this.$list.find('.h5p-dragnbar-tooltip').each(function () {
// Get correct offset even if element is a child
var width = H5P.jQuery(this).outerWidth();
var parentWidth = H5P.jQuery(this).parents('.h5p-dragnbar-li').last().outerWidth();
// Center the tooltip
H5P.jQuery(this).css('left', -(width / 2) + (parentWidth / 2) + 'px');
var offsetLeft = H5P.jQuery(this).position().left += H5P.jQuery(this).parents('.h5p-dragnbar-li').last().position().left;
// If outside left edge
if (offsetLeft <= 0) {
H5P.jQuery(this).css('left', 0);
}
// If outside right edge
if (offsetLeft + width > containerWidth) {
H5P.jQuery(this).css('left', -(width - parentWidth));
}
});
};
/**
* Change container.
*
* @param {jQuery} $container
* @returns {undefined}
*/
H5P.DragNBar.prototype.setContainer = function ($container) {
this.$container = $container;
if (this.dnd) {
this.dnd.$container = $container;
}
if (this.dnr) {
this.dnr.$container = $container;
}
};
/**
* Handler for when the dragging stops. Makes sure the element is inside its container.
*
* @param {Number} left
* @param {Number} top
* @returns {undefined}
*/
H5P.DragNBar.prototype.stopMoving = function (left, top) {
// Calculate percentage
top = top / (this.$container.height() / 100);
left = left / (this.$container.width() / 100);
this.$element.css({top: top + '%', left: left + '%'});
// Give others the result
if (this.stopMovingCallback !== undefined) {
this.stopMovingCallback(left, top);
}
};
/**
* @typedef SizeNPosition
* @type Object
* @property {number} width Outer width of the element
* @property {number} height Outer height of the element
* @property {number} left The X Coordinate
* @property {number} top The Y Coordinate
* @property {number} containerWidth Inner width of the container
* @property {number} containerHeight Inner height of the container
*/
/**
*
* Only works when element is inside this.$container. This is assumed and no
* are done.
*
* @param {H5P.jQuery} [$element] Defaults to focused element.
* @throws 'No element given' if $element is missing
* @return {SizeNPosition}
*/
H5P.DragNBar.prototype.getElementSizeNPosition = function ($element) {
$element = $element || this.focusedElement.$element;
if (!$element || !$element.length) {
throw 'No element given';
}
// Always use outer size for element
var size = $element[0].getBoundingClientRect();
// Always use position relative to container for element
var position = window.getComputedStyle($element[0]);
// We include container inner size as well
var containerSize = window.getComputedStyle(this.$container[0]);
// Start preparing return value
var sizeNPosition = {
width: parseFloat(size.width),
height: parseFloat(size.height),
left: parseFloat(position.left),
top: parseFloat(position.top),
containerWidth: parseFloat(containerSize.width),
containerHeight: parseFloat(containerSize.height)
};
if (position.left.substr(-1, 1) === '%' || position.top.substr(-1, 1) === '%') {
// Some browsers(Safari) gets percentage value instead of pixel value.
// Container inner size must be used to calculate such values.
sizeNPosition.left *= (sizeNPosition.containerWidth / 100);
sizeNPosition.top *= (sizeNPosition.containerHeight / 100);
}
return sizeNPosition;
};
/**
* Makes it possible to move dnb elements by adding to it's x and y
*
* @param {number} x Amount to move on x-axis.
* @param {number} y Amount to move on y-axis.
*/
H5P.DragNBar.prototype.moveWithKeys = function (x, y) {
/**
* Ensure that the given value is within the given boundaries.
*
* @private
* @param {number} value
* @param {number} min
* @param {number} max
* @return {number}
*/
var withinBoundaries = function (value, min, max) {
if (value < min) {
value = min;
}
if (value > max) {
value = max;
}
return value;
};
// Get size and position of current elemet in focus
var sizeNPosition = this.getElementSizeNPosition();
// Change position
sizeNPosition.left += x;
sizeNPosition.top += y;
// Check that values are within boundaries
sizeNPosition.left = withinBoundaries(sizeNPosition.left, 0, sizeNPosition.containerWidth - sizeNPosition.width);
sizeNPosition.top = withinBoundaries(sizeNPosition.top, 0, sizeNPosition.containerHeight - sizeNPosition.height);
// Determine new position style
this.$element.css({
left: sizeNPosition.left + 'px',
top: sizeNPosition.top + 'px',
});
this.dnd.trigger('showTransformPanel');
// Update position of context menu
this.updateCoordinates(sizeNPosition.left, sizeNPosition.top, sizeNPosition.left, sizeNPosition.top);
};
/**
* Makes it possible to focus and move the element around.
* Must be inside $container.
*
* @param {H5P.jQuery} $element
* @param {Object} [options]
* @param {H5P.DragNBarElement} [options.dnbElement] Register new element with dnbelement
* @param {boolean} [options.disableResize] Resize disabled
* @param {boolean} [options.lock] Lock ratio during resize
* @param {string} [clipboardData]
* @returns {H5P.DragNBarElement} Reference to added dnbelement
*/
H5P.DragNBar.prototype.add = function ($element, clipboardData, options) {
var self = this;
options = options || {};
if (this.isEditor && !options.disableResize) {
this.dnr.add($element, options);
}
var newElement = null;
// Check if element already exist
if (options.dnbElement) {
// Set element as added element
options.dnbElement.setElement($element);
newElement = options.dnbElement;
}
else {
options.element = $element;
options.disableCopy = !this.enableCopyPaste;
newElement = new H5P.DragNBarElement(this, clipboardData, options);
this.elements.push(newElement);
}
$element.addClass('h5p-dragnbar-element');
if (this.isEditor) {
if (newElement.contextMenu) {
newElement.contextMenu.on('contextMenuCopy', function () {
self.copyHandler();
});
}
if ($element.attr('tabindex') === undefined) {
// Make it possible to tab between elements.
$element.attr('tabindex', '0');
}
$element.mousedown(function (event) {
if (event.which !== 1) {
return;
}
self.pressed = true;
self.focus($element);
if (self.dnr.active !== true) { // Moving can be stopped if the mousedown is doing something else
self.dnd.press($element, event.pageX, event.pageY);
}
});
}
$element.focus(function () {
self.focus($element);
});
return newElement;
};
/**
* Remove given element in the UI.
*
* @param {H5P.DragNBarElement} dnbElement
*/
H5P.DragNBar.prototype.removeElement = function (dnbElement) {
dnbElement.removeElement();
};
/**
* Select the given element in the UI.
*
* @param {jQuery} $element
* @returns {undefined}
*/
H5P.DragNBar.prototype.focus = function ($element) {
var self = this;
// Blur last focused
if (this.focusedElement && this.focusedElement.$element !== $element) {
this.focusedElement.blur();
this.focusedElement.hideContextMenu();
}
if (!$element.is(':visible')) {
return; // Do not focus invisible items (fixes FF refocus issue)
}
// Keep track of the element we have in focus
self.$element = $element;
this.dnd.setElement($element);
// Show and update coordinates picker
this.focusedElement = this.getDragNBarElement($element);
if (this.focusedElement) {
this.focusedElement.showContextMenu();
this.focusedElement.focus();
self.updateCoordinates();
}
// Wait for potential recreation of element
setTimeout(function () {
self.updateCoordinates();
if (self.focusedElement && self.focusedElement.contextMenu && self.focusedElement.contextMenu.canResize) {
self.focusedElement.contextMenu.updateDimensions();
}
}, 0);
};
/**
* Get dnbElement from $element
* @param {jQuery} $element
* @returns {H5P.DragNBarElement} dnbElement with matching $element
*/
H5P.DragNBar.prototype.getDragNBarElement = function ($element) {
var foundElement;
// Find object with matching element
this.elements.forEach(function (element) {
if (element.getElement().is($element)) {
foundElement = element;
}
});
return foundElement;
};
/**
* Deselect all elements in the UI.
*
* @returns {undefined}
*/
H5P.DragNBar.prototype.blurAll = function () {
this.elements.forEach(function (element) {
element.blur();
});
delete this.focusedElement;
};
/**
* Resize DnB, make sure context menu is positioned correctly.
*/
H5P.DragNBar.prototype.resize = function () {
var self = this;
this.updateCoordinates();
if (self.focusedElement) {
self.focusedElement.resizeContextMenu(self.$element.offset().left - self.$element.parent().offset().left);
}
};
/**
* Update the coordinates of context menu.
*
* @param {Number} [left]
* @param {Number} [top]
* @param {Number} [x]
* @param {Number} [y]
* @returns {undefined}
*/
H5P.DragNBar.prototype.updateCoordinates = function (left, top, x, y) {
if (!this.focusedElement) {
return;
}
var containerPosition = this.$container.position();
if (left && top && x && y) {
left = x + containerPosition.left;
top = y + containerPosition.top;
this.focusedElement.updateCoordinates(left, top, x, y);
}
else {
var position = this.$element.position();
this.focusedElement.updateCoordinates(position.left + containerPosition.left, position.top + containerPosition.top, position.left, position.top);
}
};
/**
* Creates element data to store in the clipboard.
*
* @param {string} from Source of the element
* @param {object} params Element options
* @param {string} [generic] Which part of the parameters can be used by other libraries
* @returns {string} JSON
*/
H5P.DragNBar.clipboardify = function (from, params, generic) {
var clipboardData = {
from: from,
specific: params
};
if (H5PEditor.contentId) {
clipboardData.contentId = H5PEditor.contentId;
}
// Add the generic part
if (params[generic]) {
clipboardData.generic = generic;
}
return clipboardData;
};
/**
* Make sure the given element is inside the container.
*
* @param {SizeNPosition} sizeNPosition For the element
* @returns {SizeNPosition} Only the properties which require change
*/
H5P.DragNBar.fitElementInside = function (sizeNPosition) {
var style = {};
if (sizeNPosition.left < 0) {
// Element sticks out of the left side
style.left = sizeNPosition.left = 0;
}
if (sizeNPosition.width + sizeNPosition.left > sizeNPosition.containerWidth) {
// Element sticks out of the right side
style.left = sizeNPosition.containerWidth - sizeNPosition.width;
if (style.left < 0) {
// Element is wider than the container
style.left = 0;
style.width = sizeNPosition.containerWidth;
}
}
if (sizeNPosition.top < 0) {
// Element sticks out of the top side
style.top = sizeNPosition.top = 0;
}
if (sizeNPosition.height + sizeNPosition.top > sizeNPosition.containerHeight) {
// Element sticks out of the bottom side
style.top = sizeNPosition.containerHeight - sizeNPosition.height;
if (style.top < 0) {
// Element is higher than the container
style.top = 0;
style.height = sizeNPosition.containerHeight;
}
}
return style;
};
/**
* Clean up any event listeners
*/
H5P.DragNBar.prototype.remove = function () {
var index = this.instanceIndex;
H5P.$body.off('keydown.dnb' + index, H5P.DragNBar.keydownHandler)
.off('keypress.dnb' + index, H5P.DragNBar.keypressHandler)
.off('keyup.dnb' + index, H5P.DragNBar.keyupHandler)
.off('click.dnb' + index, H5P.DragNBar.clickHandler);
};
/**
* Toggle dragging on/off.
* When off can not start dragging in any new elements until turned on.
*/
H5P.DragNBar.prototype.toggleDrag = function (enabled = true) {
if (enabled === undefined) {
this.enabled = !this.enabled;
}
else {
this.enabled = enabled;
}
};
/*global H5P*/
/**
* Create context menu
*/
H5P.DragNBarContextMenu = (function ($, EventDispatcher) {
/**
* Constructor for context menu
* @class
* @param {jQuery} $container Parent container
* @param {H5P.DragNBarElement} DragNBarElement
* @param {boolean} [hasCoordinates] Decides if coordinates will be displayed
* @param {boolean} [disableResize] No input for dimensions
* @param {boolean} [disableCopy] Disable copy button
* @param {'vertical' | 'horizontal'} [directionLock] Which way to lock resizing
*/
function ContextMenu($container, DragNBarElement, hasCoordinates, disableResize, disableCopy, directionLock) {
var self = this;
EventDispatcher.call(this);
/**
* Keeps track of DragNBar object
*
* @type {H5P.DragNBar}
*/
this.dnb = DragNBarElement.dnb;
this.directionLock = directionLock;
/**
* Keeps track of DnBElement object
*
* @type {H5P.DragNBarElement}
*/
this.dnbElement = DragNBarElement;
/**
* Keeps track of context menu container
*
* @type {H5P.jQuery}
*/
this.$contextMenu = $('', {
'class': 'h5p-dragnbar-context-menu'
});
/**
* Keeps track of buttons container
*
* @type {H5P.jQuery}
*/
this.$buttons = $('
', {
'class': 'h5p-context-menu-buttons'
});
/**
* Keeps track of transform panel
*
* @type {H5P.jQuery}
*/
this.$transformPanel = $('
', {
'class': 'h5p-transform-panel hide'
});
/**
* Keeps track of context menu parent
*
* @type {jQuery}
*/
this.$parent = $container;
/**
* Keeps track of whether the context menu should display coordinates
* @type {Boolean}
*/
this.hasCoordinates = (hasCoordinates !== undefined ? hasCoordinates : true);
/**
* Determines if the dimensions can be changed.
* @type {boolean}
*/
this.canResize = !disableResize;
/**
* Determines if the transform panel is showing.
* @type {boolean}
*/
this.showingTransformPanel = false;
/**
* Button containing button name and event name that will be fired.
* @typedef {Object} ContextMenuButton
* @property {String} name Machine readable
* @property {String} label Human readable
*/
/**
* Keeps track of button objects
* @type {ContextMenuButton[]}
*/
this.buttons = [
{name: 'Edit', label: H5PEditor.t('H5P.DragNBar', 'editLabel')},
{name: 'BringToFront', label: H5PEditor.t('H5P.DragNBar', 'bringToFrontLabel')},
{name: 'SendToBack', label: H5PEditor.t('H5P.DragNBar', 'sendToBackLabel')},
{name: 'Remove', label: H5PEditor.t('H5P.DragNBar', 'removeLabel')}
];
if (!disableCopy) {
this.buttons.splice(1, 0, {name: 'Copy', label: H5PEditor.t('H5P.DragNBar', 'copyLabel')});
}
/**
* Register transform listener
*
* @param {event} [e] event
* @param {Object} [e.data] event data
* @param {Boolean} [e.data.showTransformPanel] Show transform panel
*/
self.on('contextMenuTransform', function (e) {
if (e && e.data.showTransformPanel !== undefined) {
// Use event data
self.showingTransformPanel = e.data.showTransformPanel;
}
else {
// Toggle showing panel
self.showingTransformPanel = !self.showingTransformPanel;
}
// Toggle sticky transform panel
if (e.data.button === 'Transform') {
if (self.dnb.transformButtonActive) {
self.dnb.transformButtonActive = false;
}
else {
self.dnb.transformButtonActive = true;
}
}
// Remove sticky transform panel when focus is lost
if (e.data.showTransformPanel == false) {
self.dnb.transformButtonActive = false;
}
// Toggle buttons bar and transform panel
self.toggleButtonsBar(!self.showingTransformPanel);
self.toggleTransformPanel(self.showingTransformPanel);
self.$transformButtonWrapper.toggleClass('active', self.showingTransformPanel);
// Realign context menu
self.dnb.updateCoordinates();
});
this.updateContextMenu();
}
// Inherit event dispatcher
ContextMenu.prototype = Object.create(EventDispatcher.prototype);
ContextMenu.prototype.constructor = ContextMenu;
/**
* Create coordinates in context menu
*/
ContextMenu.prototype.addCoordinates = function () {
// Coordinates disabled or exists
if (!this.hasCoordinates || this.$coordinates) {
return;
}
var self = this;
// Add coordinates picker
this.$coordinates = $(
'
' +
'
' + H5PEditor.t('H5P.DragNBar', 'positionLabel') + '
' +
'
' +
' ' +
'
' +
'
, ' +
'
' +
' ' +
'
' +
'
'
).mousedown(function () {
self.dnb.pressed = true;
}).appendTo(this.$transformPanel);
this.$x = this.$coordinates.find('.h5p-dragnbar-x');
this.$y = this.$coordinates.find('.h5p-dragnbar-y');
this.$x.add(this.$y).on('change keydown', function (event) {
if (event.type === 'change' || event.which === 13) {
// Get input
var x = Number(self.$x.val());
var y = Number(self.$y.val());
if (!isNaN(x) && !isNaN(y)) {
// Do not move outside of container
var min = {x: 0 , y: 0};
var max = {
x: self.dnb.$container.width() - self.dnbElement.getElement().outerWidth(),
y: self.dnb.$container.height() - self.dnbElement.getElement().outerHeight()
};
// Check min values
if (x < 0) {
x = min.x;
}
if (y < 0) {
y = min.y;
}
// Check max values
if (x > max.x) {
x = max.x;
}
if (y > max.y) {
y = max.y;
}
// Update and store location
self.dnb.stopMoving(x, y);
if (event.which === 13) {
// Pressed enter, mark number for easy edit
setTimeout(function () {
event.target.focus();
event.target.setSelectionRange(0, event.target.value.length);
}, 0);
}
// Update context menu position
self.dnb.updateCoordinates();
}
}
}).click(function (event) {
// Select coordinates numbers for easy edit
event.target.focus();
event.target.setSelectionRange(0, event.target.value.length);
});
};
/**
* Update the coordinates picker.
*
* @param {Number} left Left pos of context menu
* @param {Number} top Top pos of context menu
* @param {Number} x X value in coordinates
* @param {Number} y Y value in coordinates
*/
ContextMenu.prototype.updateCoordinates = function (left, top, x, y) {
// Move it
this.$contextMenu.css({
left: left,
top: top
});
// Set pos
if (this.hasCoordinates) {
this.$x.val(Math.round(x));
this.$y.val(Math.round(y));
}
};
/**
* Create coordinates in context menu
*/
ContextMenu.prototype.addDimensions = function () {
var self = this;
self.$dimensions = $('
', {
'class': 'h5p-dragnbar-dimensions'
});
// Add label
$('
', {
'class': 'h5p-dragnbar-label',
appendTo: self.$dimensions,
text: H5PEditor.t('H5P.DragNBar', 'sizeLabel')
});
var updateDimensions = function (type) {
var target = parseFloat(this.value);
if (isNaN(target)) {
return;
}
// Get element
var $element = self.dnbElement.getElement();
// Determine min&max values
var min = H5P.DragNResize.MIN_SIZE;
var containerSize = parseFloat(window.getComputedStyle(self.dnb.$container[0])[type]);
var elementStyle = window.getComputedStyle($element[0]);
var max = containerSize - parseFloat(elementStyle[type === 'width' ? 'left' : 'top']);
if (target < min) {
target = min;
}
if (target > max) {
target = max;
}
// Set input field value
self['$' + type].val(Math.round(target));
// Remove any height padding before updating element
var padding = $element[0].getBoundingClientRect()[type] - parseFloat(elementStyle[type]);
target -= padding;
$element.css(type, (target / (containerSize / 100)) + '%');
var eventData = {};
eventData[type] = target / self.dnb.dnr.containerEm;
self.dnb.dnr.trigger('stoppedResizing', eventData);
};
// Add input for width
self.$width = self.getNewInput('width', H5PEditor.t('H5P.DragNBar', 'widthLabel'), self.$dimensions, updateDimensions, self.directionLock === 'vertical');
$('
', {
'class': 'h5p-dragnbar-dimensions-separator',
text: '×',
appendTo: self.$dimensions
});
self.$height = self.getNewInput('height', H5PEditor.t('H5P.DragNBar', 'heightLabel'), self.$dimensions, updateDimensions, self.directionLock === 'horizontal');
self.dnb.dnr.on('moveResizing', function () {
self.updateDimensions();
});
self.$dimensions.appendTo(self.$transformPanel);
};
/**
* Add transform functionality
*
* @param [enableTransform]
*/
ContextMenu.prototype.addTransform = function (enableTransform) {
var self = this;
var transformButtonObject = {name: 'Transform', label: H5PEditor.t('H5P.DragNBar', 'transformLabel')};
var $transformButtonWrapper = $('
', {
'class': 'h5p-transform-button-wrapper'
});
// Attach button
if (enableTransform) {
self.createButton(transformButtonObject)
.appendTo($transformButtonWrapper);
}
self.$transformButtonWrapper = $transformButtonWrapper;
return $transformButtonWrapper;
};
/**
* Updates the values in the input fields for width and height.
*/
ContextMenu.prototype.updateDimensions = function () {
var self = this;
var $element = self.dnbElement.getElement();
var elementSize = window.getComputedStyle($element[0]);
// Re-add any padding removed while updating size
var paddingX = $element[0].getBoundingClientRect()['width'] - parseFloat(elementSize['width']);
var paddingY = $element[0].getBoundingClientRect()['height'] - parseFloat(elementSize['height']);
self.$width.val(Math.round(parseFloat(elementSize.width) + paddingX));
self.$height.val(Math.round(parseFloat(elementSize.height) + paddingY));
};
/**
* Creates a new input field for modifying an element property.
*
* @param {string} type
* @param {string} label
* @param {H5P.jQuery} $container
* @param {function} handler
* @param {boolean} disabled
* @returns {H5P.jQuery}
*/
ContextMenu.prototype.getNewInput = function (type, label, $container, handler, disabled) {
// Wrap input element with label (implicit labeling)
var $wrapper = $('
', {
'class': 'h5p-dragnbar-input h5p-dragnbar-' + type,
'aria-label': label,
appendTo: $container
});
// Create input field
var $input = $('
', {
maxLength: 5,
disabled: disabled === true,
on: {
change: function () {
handler.call(this, type);
},
keydown: function (event) {
if (event.which === 13) { // Enter key
handler.call(this, type);
$input.focus().select();
}
else if (event.which === 38 || event.which === 40) { // Up key
// Increase or decrease the number by using the arrows keys
var currentValue = parseFloat($input.val());
if (!isNaN(currentValue)) {
$input.val(currentValue + (event.which === 38 ? 1 : -1));
handler.call(this, type);
}
}
},
keyup: function (event) {
if (event.which === 38 || event.which === 40) { // Up or Down key
$input.select(); // Select again
}
},
click: function () {
$input.select();
}
},
appendTo: $wrapper
});
return $input;
};
/**
* Create button and add it to buttons bar
* @param {object} button
*/
ContextMenu.prototype.addToMenu = function (button) {
var self = this;
self.createButton(button).appendTo(this.$buttons);
};
/**
* Create button
*
* @param button
* @param {string} button.name
* @param {string} button.label
*
* @returns {H5P.jQuery}
*/
ContextMenu.prototype.createButton = function (button) {
var self = this;
var $newButton = $('
', {
'class': 'h5p-dragnbar-context-menu-button ' + button.name.toLowerCase(),
'role': 'button',
'tabindex': 0,
'aria-label': button.label
}).click(function () {
self.dnb.pressed = true;
self.trigger('contextMenu' + button.name, {button: button.name});
}).keydown(function (e) {
var keyPressed = e.which;
// 32 - space
if (keyPressed === 32) {
$(this).click();
}
});
return $newButton;
};
/**
* Remove button from context menu
* @param {String} buttonName
*/
ContextMenu.prototype.removeFromMenu = function (buttonName) {
var $removeButton = this.$buttons.children('.h5p-context-menu-button-' + buttonName);
$removeButton.remove();
};
/**
* Update context menu with current buttons. Useful when having added or removed buttons.
*/
ContextMenu.prototype.updateContextMenu = function () {
var self = this;
// Clear context menu
this.$buttons.children().remove();
// Check if transform button should be enabled
var enableTransform = false;
// Add coordinates
if (this.hasCoordinates) {
this.addCoordinates();
enableTransform = true;
}
// Add dimensions
if (this.canResize) {
this.addDimensions();
enableTransform = true;
}
// Add menu elements
this.buttons.forEach(function (button) {
self.addToMenu(button);
});
// Add transform button
this.addTransform(enableTransform)
.appendTo(this.$contextMenu);
this.$buttons.appendTo(this.$contextMenu);
this.$transformPanel.appendTo(this.$contextMenu);
};
/**
* Add button and update context menu.
* @param {String} name
* @param {String} label
*/
ContextMenu.prototype.addButton = function (name, label) {
this.buttons.push({name:name, label:label});
this.updateContextMenu();
};
/**
* Remove button from context menu
* @param {string} name
*/
ContextMenu.prototype.removeButton = function (name) {
var self = this;
// Check if button exists
self.buttons.forEach(function (button, index) {
if (button.name === name) {
self.buttons.splice(index, 1);
return;
}
});
this.updateContextMenu();
};
/**
* Toggle buttons visibility
*
* @param [showButtons] Show buttons
*/
ContextMenu.prototype.toggleButtonsBar = function (showButtons) {
var self = this;
if (showButtons !== undefined) {
self.$buttons.toggleClass('hide', !showButtons);
}
else {
self.$buttons.toggleClass('hide');
}
};
/**
* Toggle transform panel visibility.
*
* @param [showTransformPanel] Show transform panel
*/
ContextMenu.prototype.toggleTransformPanel = function (showTransformPanel) {
var self = this;
if (showTransformPanel !== undefined) {
self.$transformPanel.toggleClass('hide', !showTransformPanel);
}
else {
self.$transformPanel.toggleClass('hide');
}
};
/**
* Toggle if coordinates should show
* @param {Boolean} [enableCoordinates] Enable coordinates
*/
ContextMenu.prototype.toggleCoordinates = function (enableCoordinates) {
if (enableCoordinates === undefined) {
this.hasCoordinates = !this.hasCoordinates;
}
else {
this.hasCoordinates = !!enableCoordinates;
}
this.updateContextMenu();
};
/**
* Attach context menu to body.
*/
ContextMenu.prototype.attach = function () {
this.$contextMenu.appendTo(this.$parent);
};
/**
* Detach context menu from DOM.
*/
ContextMenu.prototype.detach = function () {
this.$contextMenu.detach();
};
return ContextMenu;
})(H5P.jQuery, H5P.EventDispatcher);
;
/*global H5P*/
H5P.DragNBarDialog = (function ($, EventDispatcher) {
/**
* Controls the dialog in the interactive video.
*
* @class
* @param {H5P.jQuery} $container for dialog
* @param {H5P.jQuery} $videoWrapper needed for positioning of dialog
*/
function Dialog($container, $videoWrapper) {
var KEY_CODE_ESC = 27;
var KEY_CODE_ENTER = 13;
var KEY_CODE_SPACE = 32;
var self = this;
var titleId = 'dialog-title-' + H5P.createUUID();
// Initialize event inheritance
EventDispatcher.call(self);
/**
* Stops propagating an event
*
* @param {Event} event
*/
var stopEventPropagation = function (event) {
// k is used to stop and start an interactive video
if (event.which === 75) {
event.stopPropagation();
}
};
// Create DOM elements for dialog
var $wrapper = $('
', {
'class': 'h5p-dialog-wrapper h5p-ie-transparent-background h5p-hidden',
on: {
click: function () {
if (!self.disableOverlay) {
self.close();
}
},
keyup: stopEventPropagation,
keydown: stopEventPropagation
}
});
var $dialog = $('
', {
'class': 'h5p-dialog h5p-big',
'aria-labelledby': titleId,
on: {
click: function (event) {
event.stopPropagation();
},
keydown: function (event) {
var isClosable = $close.is(':visible');
if (event.which === KEY_CODE_ESC && isClosable) {
self.close();
}
}
}
}).appendTo($wrapper);
// Create title bar
var $titleBar = $('
', {
'class': 'h5p-dialog-titlebar',
appendTo: $dialog
});
var $title = $('
', {
'class': 'h5p-dialog-title',
id: titleId,
appendTo: $titleBar
});
var $close = $('
', {
'role': 'button',
'class': 'h5p-dialog-close',
tabindex: '0',
title: H5P.t('close'),
'aria-label': H5P.t('close'),
on: {
click: function (event) {
if (event.which === 1) {
self.close();
}
},
keypress: function (event) {
if (event.which === KEY_CODE_SPACE || event.which === KEY_CODE_ENTER) {
self.close();
event.preventDefault();
}
}
},
appendTo: $titleBar
});
// Used instead of close
var $customButtons;
// Create inner DOM elements for dialog
var $inner = $('
', {
'class': 'h5p-dialog-inner'
}).appendTo($dialog);
// Add all to DOM
$wrapper.appendTo($container);
/**
* Reset the dialog's positioning
*
* @private
*/
var resetPosition = function () {
// Reset positioning
$dialog.css({
left: '',
top: '',
height: '',
width: '',
fontSize: '',
bottom: ''
});
$inner.css({
width: '',
height: '',
overflow: ''
});
};
/**
* Display overlay.
*
* @private
* @param {function} next callback
*/
var showOverlay = function (next) {
$wrapper.show();
setTimeout(function () {
// Remove class on next tick to ensure css animation
$wrapper.removeClass('h5p-hidden');
if (next) {
next();
}
}, 0);
};
/**
* Close overlay.
*
* @private
* @param {function} next callback
*/
var hideOverlay = function (next) {
$wrapper.addClass('h5p-hidden');
setTimeout(function () {
// Hide when animation is done
$wrapper.hide();
if (next) {
next();
}
}, 200);
};
/**
* Opens a new dialog. Displays the given element.
*
* @param {H5P.jQuery} $element
* @param {string} [title] Label for the dialog
* @param {string} [classes] For styling
* @param {H5P.jQuery} [$buttons] Use custom buttons for dialog
*/
self.open = function ($element, title, classes, $buttons) {
showOverlay();
$inner.children().detach().end().append($element);
// Reset positioning
resetPosition();
$dialog.addClass('h5p-big');
$title.attr('class', 'h5p-dialog-title' + (classes ? ' ' + classes : ''));
// Add label
if (!title) {
title = '';
}
$title.html(title);
// Clean up after previous custom buttons
if ($customButtons) {
$customButtons.remove();
$close.show();
}
// Add new custom buttons
if ($buttons) {
$customButtons = $buttons;
// Hide default close button
$close.hide();
// Add custom buttons
$buttons.appendTo($titleBar);
}
self.resize();
self.trigger('open');
$dialog.one('transitionend', function() {
// Find visible enabled inputs:
var $inputs = $inner.find('input:visible:not(:disabled)');
var $tabbables = $inner.find('[tabindex]');
// Prioritize the focusing of inputs before other elements
if ($inputs.length) {
$inputs.get(0).focus();
}
// If other tabbables exist like h5p-text, focus on them
else if ($tabbables.length) {
$tabbables.get(0).focus();
}
});
};
self.resize = function () {
if (!$dialog.hasClass('h5p-big')) {
return;
}
var fontSize = toNum($inner.css('fontSize'));
var titleBarHeight = ($titleBar.outerHeight() / fontSize);
// Same as height
var maxHeight = $container.height();
// minus dialog margins
maxHeight -= Number($dialog.css('top').replace('px', '')) * 2;
$inner.css({
width: '100%',
maxHeight: ((maxHeight / fontSize) - titleBarHeight) + 'em',
marginTop: titleBarHeight + 'em'
});
$dialog.css({
bottom: 'auto',
maxHeight: ''
});
};
/**
* Adds a name to the dialog for identifying what it contains.
*
* @param {string} machineName Name of library inside dialog.
*/
self.addLibraryClass = function (machineName) {
$dialog.attr('data-lib', machineName);
};
/**
* Toggle class on the dialog Dom element
* @method toggleClass
* @param {String} cls Classname
* @param {Boolean} toggle
*/
self.toggleClass = function (cls, toggle) {
$dialog.toggleClass(cls, toggle);
};
self.isOpen = function () {
return $wrapper.is(':visible');
};
/**
* Reposition the currently open dialog relative to the given button.
*
* @param {H5P.jQuery} $button
* @param {Object} [size] Sets a size for the dialog, useful for images.
* @param {string|boolean} [type] Type of dialog. Possible values are
* 'medium' and 'big'. It also supports an older version of the function,
* i.e: type = true means 'medium'
*/
self.position = function ($button, size, type) {
// Still support old version of this function
if (type === true) {
type = 'medium';
}
resetPosition();
$dialog.removeClass('h5p-big h5p-medium');
var titleBarHeight = Number($inner[0].style.marginTop.replace('em', ''));
// Use a fixed size
if (size) {
var fontSizeRatio = 16 / toNum($container.css('fontSize'));
// Fixed width
if (size.width) {
size.width = (size.width * fontSizeRatio);
$dialog.css('width', size.width + 'em');
}
// Fixed height
if (size.height) {
size.height = (size.height * fontSizeRatio) + titleBarHeight;
$dialog.css('height', size.height + 'em');
$inner.css({
width: 'auto',
overflow: 'hidden'
});
}
}
if (type === 'medium') {
$dialog.addClass('h5p-medium');
}
if (type === 'big') {
$dialog.addClass('h5p-big');
$dialog.addClass('h5p-stretch');
}
var buttonWidth = $button.outerWidth(true);
var buttonPosition = $button.position();
var containerWidth = $container.width();
var containerHeight = $container.height();
// Position dialog horizontally
var left = buttonPosition.left;
var dialogWidth = $dialog.outerWidth(true);
if (type === 'medium' && dialogWidth > containerWidth) {
// If dialog is too big to fit within the container, display as h5p-big instead.
// Only medium dialogs can become big
$dialog.addClass('h5p-big');
return;
}
if (buttonPosition.left > (containerWidth / 2) - (buttonWidth / 2)) {
// Show on left
left -= dialogWidth - buttonWidth;
}
// Make sure the dialog is within the video on the right.
if ((left + dialogWidth) > containerWidth) {
left = containerWidth - dialogWidth;
}
var marginLeft = parseInt($videoWrapper.css('marginLeft'));
if (isNaN(marginLeft)) {
marginLeft = 0;
}
// And finally, make sure we're within bounds on the left hand side too...
if (left < marginLeft) {
left = marginLeft;
}
// Position dialog vertically
var marginTop = parseInt($videoWrapper.css('marginTop'));
if (isNaN(marginTop)) {
marginTop = 0;
}
// Set dialog size for dialogs which aren't stretched
if (type !== 'big') {
var top = (type === 'medium' ? 0 : (buttonPosition.top + marginTop));
var totalHeight = top + $dialog.outerHeight(true);
if (totalHeight > containerHeight) {
top -= totalHeight - containerHeight;
}
var maxHeight = $container.height() - top + $dialog.height() - $dialog.outerHeight(true);
var fontSize = toNum($container.css('fontSize'));
$dialog.css({
top: (top / (containerHeight / 100)) + '%',
left: (left / (containerWidth / 100)) + '%',
width: (window.getComputedStyle($dialog[0]).width / fontSize) + 'em',
maxHeight: (maxHeight / fontSize) + 'em'
});
$inner.css('maxHeight', ((maxHeight - $titleBar.outerHeight(true)) / fontSize) + 'em');
}
};
/**
* Find max available space inside dialog when positioning relative to
* given button.
*
* @param {H5P.jQuery} $button
* @param {Boolean} fullScreen True if dialog fills whole parent
* @returns {Object} Attrs: width, height
*/
self.getMaxSize = function ($button, fullScreen) {
var buttonWidth = $button.outerWidth(true);
var buttonPosition = $button.position();
var containerWidth = $container.width();
var max = {};
max.height = Number($inner.css('maxHeight').replace('px', ''));
// If border, extract that:
max.height -= Number($inner.css('border-width').replace('px', '')) * 2;
if (fullScreen) {
max.width = containerWidth;
}
else {
if (buttonPosition.left > (containerWidth / 2) - (buttonWidth / 2)) {
// Space to the left of the button minus margin
max.width = buttonPosition.left;
}
else {
// Space to the right of the button minus margin
max.width = (containerWidth - buttonPosition.left - buttonWidth);
}
}
// Use em
var fontSize = toNum($container.css('fontSize'));
max.width = (max.width / fontSize) * (fontSize / 16);
max.height = (max.height / fontSize) * (fontSize / 16);
return max;
};
/**
* Scroll to given position in current dialog.
*
* @param {number} to Scroll position
* @param {number} ms Time the animation takes.
*/
self.scroll = function (to, ms) {
$inner.stop().animate({
scrollTop: to
}, ms);
};
/**
* Close the currently open dialog.
*/
self.close = function (closeInstant) {
$wrapper.addClass('h5p-hidden');
if (closeInstant) {
$wrapper.hide();
self.disableOverlay = false;
$close.show();
}
else {
setTimeout(function () {
$wrapper.hide();
self.disableOverlay = false;
$close.show();
}, 201);
}
self.trigger('close');
// Let others reach to the hiding of this dialog
self.trigger('domHidden', {
'$dom': $wrapper,
'key': 'dialogClosed'
}, {'bubbles': true, 'external': true});
};
/**
* Open overlay only.
*/
self.openOverlay = function () {
self.disableOverlay = true;
$dialog.hide();
showOverlay();
};
/**
* Close overlay only.
*/
self.closeOverlay = function () {
$wrapper.addClass('h5p-hidden');
hideOverlay(function () {
$dialog.show();
self.disableOverlay = false;
});
};
/**
* Removes the close button from the current dialog.
*/
self.hideCloseButton = function () {
$close.hide();
};
/**
* Get width of dialog
* @returns {Number} Width of dialog
*/
self.getDialogWidth = function () {
return $dialog.width();
};
/**
* Reset dialog width
*/
self.removeStaticWidth = function () {
$dialog.css('width', '');
};
}
// Extends the event dispatcher
Dialog.prototype = Object.create(EventDispatcher.prototype);
Dialog.prototype.constructor = Dialog;
/**
* Converts css px value to number.
*
* @private
* @param {string} num
* @returns {Number}
*/
var toNum = function (num) {
return Number(num.replace('px',''));
};
return Dialog;
})(H5P.jQuery, H5P.EventDispatcher);
;
/*global H5P*/
/**
* Create Drag N Bar Element. Connects a DragNBar element to a context menu
*/
H5P.DragNBarElement = (function ($, ContextMenu, EventDispatcher) {
/**
* Constructor DragNBarElement
*
* @class
* @param {H5P.DragNBar} dragNBar Parent dragNBar toolbar
* @param {object} [clipboardData]
* @param {Object} [options] Button object that the element is created from
* @param {Boolean} [options.disableContextMenu] Decides if element should have editor functionality
* @param {Function} [options.createElement] Function for creating element from button
* @param {Function} [options.disableCopy] Copy button disabled or enabled?
* @param {boolean} [options.hasCoordinates] Decides if element will display coordinates
* @param {H5P.jQuery} [options.element] Element
*/
function DragNBarElement(dragNBar, clipboardData, options) {
var self = this;
EventDispatcher.call(this);
this.dnb = dragNBar;
this.options = options || {};
if (!this.options.disableContextMenu) {
this.contextMenu = new ContextMenu(this.dnb.$dialogContainer, this, this.options.hasCoordinates, this.options.disableResize, this.options.disableCopy, this.options.directionLock);
}
this.focused = false;
if (this.options.createElement) {
this.$element = this.options.createElement().appendTo(dragNBar.$container);
this.focus();
}
else {
this.$element = this.options.element;
}
// Let dnb know element has been pressed
if (this.$element) {
if (this.dnb.isEditor) {
this.$element.mousedown(function () {
self.dnb.pressed = true;
});
}
// Run custom focus function on element focus
this.$element.focus(function () {
self.focus();
});
}
/**
* Store element paramets in the local storage.
*/
self.toClipboard = function (width, height) {
if (clipboardData && localStorage) {
clipboardData.width = width;
clipboardData.height = height;
H5P.setClipboard(clipboardData);
}
};
}
// Inheritance
DragNBarElement.prototype = Object.create(EventDispatcher.prototype);
DragNBarElement.prototype.constructor = DragNBarElement;
/**
* Add button to context menu.
*
* @param {string} name
* @param {string} label
*/
DragNBarElement.prototype.addButton = function (name, label) {
this.contextMenu.addToMenu({name:name, label:label});
};
/**
* Get element
* @returns {H5P.jQuery}
*/
DragNBarElement.prototype.getElement = function () {
return this.$element;
};
/**
* Set element
* @param {H5P.jQuery} $element
*/
DragNBarElement.prototype.setElement = function ($element) {
var self = this;
this.$element = $element;
// Register custom focus function on new element focus
this.$element.focus(function () {
self.focus();
});
};
/**
* Show context menu
*/
DragNBarElement.prototype.showContextMenu = function () {
if (this.contextMenu) {
this.contextMenu.attach();
}
};
/**
* Hide context menu
*/
DragNBarElement.prototype.hideContextMenu = function () {
if (this.contextMenu) {
this.contextMenu.detach();
}
};
/**
* Update coordinates in context menu to current location
*
* @param {Number} left Left position of context menu
* @param {Number} top Top position of context menu
* @param {Number} x X coordinate of context menu
* @param {Number} y Y coordinate of context menu
*/
DragNBarElement.prototype.updateCoordinates = function (left, top, x, y) {
if (this.contextMenu) {
this.contextMenu.updateCoordinates(left, top, x, y);
this.resizeContextMenu(x);
}
};
/**
* Float context menu left if width exceeds parent container.
*
* @param {Number} [left] Left position of context menu.
*/
DragNBarElement.prototype.resizeContextMenu = function (left) {
if (this.options.disableContextMenu) {
return;
}
// Need to take into account the left padding of the contextmenu's parent
var paddingLeft = Number(this.contextMenu.$parent.css('padding-left').replace('px', ''));
left = (left || this.$element.position().left) + paddingLeft;
var containerWidth = this.dnb.$container.width();
var $cm = this.contextMenu.$contextMenu;
// Measure full outer width
$cm.css({
position: 'absolute',
left: 0
});
var contextMenuWidth = $cm.outerWidth(true);
// Reset to default
$cm.css({
position: '',
left: left
});
var isTooWide = left + contextMenuWidth >= containerWidth;
if (isTooWide) {
var newLeft = left - contextMenuWidth;
this.contextMenu.$contextMenu.css('left', newLeft + 'px');
this.contextMenu.$contextMenu.addClass('left-aligned');
}
else {
this.contextMenu.$contextMenu.removeClass('left-aligned');
}
};
/**
* Blur element and hide context menu.
*/
DragNBarElement.prototype.blur = function () {
if (this.$element) {
this.$element.removeClass('focused');
this.focused = false;
if (!this.options.disableContextMenu) {
// Hide transform panel
this.contextMenu.trigger('contextMenuTransform', {showTransformPanel: false});
}
}
this.hideContextMenu();
};
/**
* Focus element
*/
DragNBarElement.prototype.focus = function () {
this.$element.addClass('focused');
this.focused = true;
if (this.contextMenu) {
this.resizeContextMenu(this.$element.position().left);
}
};
/**
* Remove element and hide context menu
*/
DragNBarElement.prototype.removeElement = function () {
this.$element.detach();
this.hideContextMenu();
};
return DragNBarElement;
})(H5P.jQuery, H5P.DragNBarContextMenu, H5P.EventDispatcher);
;
(function (DragNBar, EventDispatcher) {
/**
* Allows different forms to be places on top of each other instead of
* in a dialog.
*
* @class H5P.DragNBar.FormManager
* @extends H5P.EventDispatcher
* @param {*} parent
* @param {Object} l10n
*/
DragNBar.FormManager = function (parent, l10n, customIconClass) {
/** @alias H5P.DragNBar.FormManager# */
var self = this;
// Initialize event inheritance
EventDispatcher.call(self);
const formTargets = [self];
let head, footer, subForm, titles, handleTransitionend, proceedButton, breadcrumbButton, alwaysShowButtons;
/**
* Initialize the FormManager.
* Create frame breadcrumbs, and fullscreen button.
*
* @private
*/
const initialize = function () {
self.isMainLibrary = !(parent instanceof H5PEditor.Library)
// Locate target container
self.formContainer = (self.isMainLibrary ? parent.$form : parent.$libraryWrapper)[0];
self.formContainer.classList.add('form-manager');
self.formContainer.classList.add('root-form');
head = document.createElement('div');
head.classList.add('form-manager-head');
footer = document.createElement('div');
footer.classList.add('form-manager-head');
footer.classList.add('form-manager-footer');
self.footer = footer;
// Create button to toggle preivous menu on narrow layouts
breadcrumbButton = createButton('breadcrumb-menu', l10n.expandBreadcrumbButtonLabel, self.toggleBreadcrumbMenu);
breadcrumbButton.classList.add('form-manager-disabled');
head.appendChild(breadcrumbButton);
// Create breadcrumb menu to use when the layout is too narrow for the regular breadcrumb
self.formBreadcrumbMenu = document.createElement('div');
self.formBreadcrumbMenu.classList.add('form-manager-breadcrumb-menulist');
head.appendChild(self.formBreadcrumbMenu);
// Create breadcrumb wrapper
self.formBreadcrumb = document.createElement('div');
self.formBreadcrumb.classList.add('form-manager-breadcrumb');
head.appendChild(self.formBreadcrumb);
// Create the first part of the breadcrumb
const titles = createTitles(parent);
titles.breadcrumb.classList.add('form-manager-comein');
self.formBreadcrumb.appendChild(titles.breadcrumb);
self.formBreadcrumbMenu.appendChild(titles.menu);
// Create 'Proceed to save' button
proceedButton = createButton('proceed', H5PEditor.t('core', 'proceedButtonLabel'), function () {
if (manager.exitSemiFullscreen) {
// Trigger semi-fullscreen exit
manager.exitSemiFullscreen();
manager.exitSemiFullscreen = null;
}
});
hideElement(proceedButton);
head.appendChild(proceedButton);
// Create a container for the action buttons
self.formButtons = document.createElement('div');
self.formButtons.classList.add('form-manager-buttons');
self.footerFormButtons = document.createElement('div');
self.footerFormButtons.classList.add('form-manager-buttons');
hideElement(self.footerFormButtons);
hideElement(self.formButtons); // Buttons are hidden by default
footer.appendChild(self.footerFormButtons);
head.appendChild(self.formButtons);
// Create 'Delete' button
self.formButtons.appendChild(createButton('delete', l10n.deleteButtonLabel, function () {
const e = new H5P.Event('formremove');
e.data = formTargets.length;
formTargets[formTargets.length - 1].trigger(e);
if (!e.preventRemove && formTargets.length > 1) {
closeForm();
}
}));
// Create 'Done' button
self.formButtons.appendChild(createButton('done', l10n.doneButtonLabel, function () {
formTargets[formTargets.length - 1].trigger('formdone', formTargets.length);
if (formTargets.length > 1) {
closeForm();
}
}));
// Footer form buttons
self.footerFormButtons.appendChild(createButton('done', l10n.doneButtonLabel, function () {
formTargets[formTargets.length - 1].trigger('formdone', formTargets.length);
if (formTargets.length > 1) {
closeForm();
}
}));
self.footerFormButtons.appendChild(createButton('delete', l10n.deleteButtonLabel, function () {
const e = new H5P.Event('formremove');
e.data = formTargets.length;
formTargets[formTargets.length - 1].trigger(e);
if (!e.preventRemove && formTargets.length > 1) {
closeForm();
}
}));
// Check if we should add the fullscreen button
if (self.isMainLibrary && H5PEditor.semiFullscreen !== undefined) {
// Create and insert fullscreen button into header
const fullscreenButton = createButton('fullscreen', '', function () {
if (manager.exitSemiFullscreen) {
// Trigger semi-fullscreen exit
manager.exitSemiFullscreen();
}
else {
// Trigger semi-fullscreen enter
manager.exitSemiFullscreen = H5PEditor.semiFullscreen([manager.formContainer], function () {
if (!subForm) {
showElement(proceedButton);
}
toggleFullscreenButtonState(fullscreenButton, true);
self.trigger('formentersemifullscreen');
}, function () {
manager.exitSemiFullscreen = null;
if (!subForm) {
hideElement(proceedButton);
}
toggleFullscreenButtonState(fullscreenButton);
self.trigger('formexitsemifullscreen');
});
}
});
toggleFullscreenButtonState(fullscreenButton);
head.appendChild(fullscreenButton);
}
window.addEventListener('resize', self.updateFormResponsiveness);
// Always clean up on remove
self.on('remove', function () {
window.removeEventListener('resize', self.updateFormResponsiveness);
});
const overlay = document.createElement('div');
overlay.classList.add('form-mananger-overlay');
self.formContainer.insertBefore(overlay, self.formContainer.firstChild);
// Insert everything in the top of the form DOM
self.formContainer.insertBefore(head, self.formContainer.firstChild);
hideElement(footer);
self.formContainer.appendChild(manager.footer);
// Always clean up on remove
self.on('validate', function () {
if (parent.metadata && (!parent.metadata.title || !H5P.trim(parent.metadata.title))) {
// We are trying to save the form without a title
self.closeFormUntil(0);
}
});
};
/**
* Helper for creating buttons.
*
* @private
* @param {string} id
* @param {string} text
* @param {function} clickHandler
* @return {Element}
*/
const createButton = function (id, text, clickHandler) {
const button = document.createElement('button');
button.setAttribute('type', 'button');
button.classList.add('form-manager-button');
button.classList.add('form-manager-' + id);
button.setAttribute('aria-label', text);
button.addEventListener('click', clickHandler);
// Create special inner filler to avoid focus from pointer devices.
const content = document.createElement('span');
content.classList.add('form-manager-button-inner');
content.innerText = text
content.tabIndex = -1;
button.appendChild(content);
return button;
};
/**
* Create two titles, one for the breadcrumb and for the expanded
* breadcrumb menu used for narrow layouts.
*
* @private
* @param {H5PEditor.Library} libraryField
* @return {Element[]}
*/
const createTitles = function (libraryField, customTitle, customIconId) {
const library = (libraryField.params && libraryField.params.library) ? libraryField.params.library : (libraryField.currentLibrary ? libraryField.currentLibrary : undefined);
// Create breadcrumb section.
const title = document.createElement('div');
title.classList.add('form-manager-title');
// Create breadcrumb section.
const menuTitle = document.createElement('div');
menuTitle.classList.add('form-manager-menutitle');
menuTitle.tabIndex = '0';
menuTitle.addEventListener('click', function () {
handleBreadcrumbClick.call(title);
});
menuTitle.addEventListener('keypress', function (e) {
handleBreadcrumbKeypress.call(title, e);
});
// For limiting the length of the menu title
const menuTitleText = document.createElement('span');
menuTitleText.classList.add('form-manager-menutitle-text');
menuTitle.appendChild(menuTitleText);
// Create a tooltip that can be display the whole text on hover
const menuTitleTooltip = document.createElement('span');
menuTitleTooltip.classList.add('form-manager-tooltip');
menuTitle.appendChild(menuTitleTooltip);
// Create a text wrapper so we can limit max-width on the text
const textWrapper = document.createElement('span');
textWrapper.classList.add('truncatable-text');
textWrapper.tabIndex = -1;
title.appendChild(textWrapper);
// Create a tooltip that can display the whole text on hover
const tooltip = document.createElement('span');
tooltip.classList.add('form-manager-tooltip');
title.appendChild(tooltip);
/**
* @private
* @param {string} title WARNING: This is Text do not use as HTML.
*/
const setTitle = function (title) {
textWrapper.innerText = menuTitleText.innerText = tooltip.innerText = menuTitleTooltip.innerText = title;
};
/**
* @private
* @return {string} WARNING: This is Text do not use as HTML.
*/
const getTitle = function () {
if (customTitle) {
return customTitle;
}
else if (libraryField.params && libraryField.params.metadata && libraryField.params.metadata.title &&
libraryField.params.metadata.title.substr(0, 8) !== 'Untitled' ||
libraryField.metadata && libraryField.metadata.title &&
libraryField.metadata.title.substr(0, 8) !== 'Untitled') {
return getText(libraryField.metadata ? libraryField.metadata.title : libraryField.params.metadata.title);
}
else {
if (libraryField.$select !== undefined && libraryField.$select.children(':selected').text() !== '-') {
return libraryField.$select.children(':selected').text();
}
else {
return H5PEditor.libraryCache[library].title;
}
}
};
// Set correct starting title
setTitle(getTitle());
/**
* Help listen for title changes after library has been fully loaded
* @private
*/
const listenForTitleChanges = function () {
if (libraryField.metadataForm) {
libraryField.metadataForm.on('titlechange', function (e) {
// Handle changes to the metadata title
setTitle(getTitle());
manager.updateFormResponsiveness();
});
}
if (textWrapper.innerText === 'Loading...') {
// Correct title was not set initally, try again after library load
setTitle(getTitle());
manager.updateFormResponsiveness();
}
};
// Listen for title updates
if (libraryField.metadataForm === undefined && libraryField.change) {
libraryField.change(listenForTitleChanges);
}
else {
listenForTitleChanges();
}
const iconId = customIconId ? customIconId : library.split(' ')[0].split('.')[1].toLowerCase();
title.classList.add('form-manager-icon-' + iconId);
menuTitle.classList.add('form-manager-icon-' + iconId);
if (customIconClass) {
title.classList.add('form-manager-' + customIconClass);
menuTitle.classList.add('form-manager-' + customIconClass);
}
return {
breadcrumb: title,
menu: menuTitle
};
};
/**
* Look through all parent ancestors to see if a manager already exists.
*
* @private
* @param {*} parent
* @return {DragNBar.FormManager}
*/
const findExistingManager = function (parent) {
if (parent instanceof DragNBar.FormManager) {
return parent.getFormManager(); // Found our parent manager
}
if (parent.parent) {
// Looks deeper
return findExistingManager(parent.parent);
}
else {
return self; // Use our self
}
};
/**
* Help hide an element.
*
* @param {Element} element
* @private
*/
const hideElement = function (element) {
// Make sure element is hidden while still retaining its width without
// expanding the container's height. This is due to some editors resizing
// if their container changes size which leads to some funny transitions.
// Also, having invisible height causes resize loops.
element.classList.add('form-manager-hidden');
element.setAttribute('aria-hidden', true);
};
/**
* Help show a hidden element again
*
* @param {Element} element
* @private
*/
const showElement = function (element) {
element.classList.remove('form-manager-hidden');
element.removeAttribute('aria-hidden');
};
/**
* Update fuillscreen button's attributes dependent on fullscreen or not
*
* @private
* @param {Element} element The fullscreen button element
* @param {boolean} isInFullscreen
*/
const toggleFullscreenButtonState = function (element, isInFullscreen) {
if (isInFullscreen) {
// We are entering fullscreen mode
element.setAttribute('aria-label', H5PEditor.t('core', 'exitFullscreenButtonLabel'));
element.classList.add('form-manager-exit');
}
else {
// We are exiting fullscreen mode
element.setAttribute('aria-label', H5PEditor.t('core', 'enterFullscreenButtonLabel'));
element.classList.remove('form-manager-exit');
}
};
/**
* Closes the current form.
*
* @private
*/
const closeForm = function () {
const activeManager = formTargets.pop();
// Close any open CKEditors
if (H5PEditor.Html) {
H5PEditor.Html.removeWysiwyg();
}
// Let everyone know we're closing
activeManager.trigger('formclose');
// Locate open form and remove it from the manager
const activeSubForm = activeManager.popForm();
if (handleTransitionend) {
// Cancel callback for form if not fully opened.
activeSubForm.removeEventListener('transitionend', handleTransitionend);
handleTransitionend = null;
}
// Find last part of breadcrumb and remove it from the manager
const titles = activeManager.popTitles();
// Remove menu title
manager.formBreadcrumbMenu.removeChild(titles.menu);
// The previous breadcrumb must no longer be clickable
const previousBreadcrumb = titles.breadcrumb.previousSibling;
previousBreadcrumb.removeEventListener('click', handleBreadcrumbClick);
previousBreadcrumb.removeEventListener('keypress', handleBreadcrumbKeypress);
previousBreadcrumb.classList.remove('clickable');
previousBreadcrumb.removeAttribute('tabindex');
const headHeight = manager.getFormHeadHeight();
// Freeze container height to avoid jumping while showing elements
manager.formContainer.style.height = (activeSubForm.getBoundingClientRect().height + headHeight) + 'px';
// Make underlay visible again
if (activeSubForm.previousSibling.classList.contains('form-manager-form')) {
// This is not our last sub-form
showElement(activeSubForm.previousSibling);
}
else {
// Show bottom form
for (let i = 1; i < manager.formContainer.children.length - 1; i++) {
showElement(manager.formContainer.children[i]);
}
// No need for the buttons any more
if (!alwaysShowButtons) {
hideElement(manager.formButtons);
manager.formButtons.classList.remove('form-manager-comein');
// Hide footer
manager.footerFormButtons.classList.remove('form-manager-comein');
hideElement(manager.footerFormButtons);
hideElement(manager.footer);
}
manager.formContainer.classList.add('root-form');
}
// Animation fix for fullscreen max-width limit.
activeSubForm.style.marginLeft = window.getComputedStyle(activeSubForm).marginLeft
// Make the sub-form animatable
activeSubForm.classList.add('form-manager-movable');
// Resume natural container height
manager.formContainer.style.height = '';
// Set sub-form height to cover container
activeSubForm.style.height = (manager.formContainer.getBoundingClientRect().height - headHeight) + 'px';
// Clean up when the final transition animation is finished
onlyOnce(activeSubForm, 'transitionend', function () {
// Remove from DOM
manager.formContainer.removeChild(activeSubForm);
});
// Start the animation
activeSubForm.classList.remove('form-manager-slidein');
if (titles.breadcrumb.offsetWidth === 0) {
// Remove last breadcrumb section in case it's hidden
manager.formBreadcrumb.removeChild(titles.breadcrumb);
}
else {
onlyOnce(titles.breadcrumb, 'transitionend', function () {
// Remove last breadcrumb section
manager.formBreadcrumb.removeChild(titles.breadcrumb);
});
// Start the animation
titles.breadcrumb.classList.remove('form-manager-comein');
}
if (!subForm) {
if (proceedButton && manager.exitSemiFullscreen) {
// We are in fullscreen and closing sub-form, show proceed button
showElement(proceedButton);
}
if (breadcrumbButton) {
breadcrumbButton.classList.add('form-manager-disabled');
}
}
if (self.formContainer.classList.contains('mobile-menu-open')) {
self.toggleBreadcrumbMenu();
}
// Scroll parent manager header into view
manager.formButtons.scrollIntoView();
};
/**
* The breadcrumb click handler figures out how many forms to close.
*
* @private
*/
const handleBreadcrumbClick = function () {
for (let i = 0; i < manager.formBreadcrumb.children.length; i++) {
if (manager.formBreadcrumb.children[i] === this) {
manager.closeFormUntil(i);
break;
}
}
};
/**
* The breadcrumb click handler figures out how many forms to close.
*
* @private
*/
const handleBreadcrumbKeypress = function (e) {
if (e.which === 13 || e.which === 32) {
handleBreadcrumbClick.call(this);
}
};
/**
* Close all forms until the given index.
*
* @param {number} index
*/
self.closeFormUntil = function (index) {
while (formTargets.length - 1 !== index) {
formTargets[formTargets.length - 1].trigger('formdone');
closeForm();
}
};
/**
* Retrieve the current form element and remove it from the manager.
*
* @return {Element}
*/
self.popForm = function () {
const sF = subForm;
subForm = null;
return sF;
};
/**
* Retrieve the current title element and remove it from the manager.
*
* @return {Element}
*/
self.popTitles = function () {
const t = titles;
titles = null;
return t;
};
/**
* Retrieve the active manager.
*
* @return {DragNBar.FormManager}
*/
self.getFormManager = function () {
return manager;
};
/**
* Set the form manager to be used for the next button clicks.
*
* @param {DragNBar.FormManager} target
*/
self.addFormTarget = function (target) {
formTargets.push(target);
};
/**
* Create a new sub-form and shows it.
*
* @param {H5PEditor.Library} libraryField
* @param {Element} formElement
*/
self.openForm = function (libraryField, formElement, customClass, customTitle, customIconId) {
if (subForm) {
return; // Prevent opening more than one sub-form at a time per editor.
}
// Tell manager that we should be receiving the next buttons events
manager.formContainer.classList.remove('root-form');
manager.addFormTarget(self);
// Create the new sub-form
subForm = document.createElement('div');
subForm.classList.add('form-manager-form');
subForm.classList.add('form-manager-movable');
if (customClass) {
subForm.classList.add(customClass);
}
subForm.appendChild(formElement);
// Ensure same height as container
subForm.style.height = (manager.formContainer.getBoundingClientRect().height - manager.getFormHeadHeight()) + 'px';
// Insert into DOM
manager.formContainer.appendChild(subForm);
// Make last part of breadcrumb clickable
const lastBreadcrumb = manager.formBreadcrumb.lastChild;
lastBreadcrumb.addEventListener('click', handleBreadcrumbClick);
lastBreadcrumb.addEventListener('keypress', handleBreadcrumbKeypress);
lastBreadcrumb.classList.add('clickable');
lastBreadcrumb.tabIndex = '0';
// Add breadcrumb section
titles = createTitles(libraryField, customTitle, customIconId);
manager.formBreadcrumb.appendChild(titles.breadcrumb);
manager.formBreadcrumbMenu.insertBefore(titles.menu, manager.formBreadcrumbMenu.firstChild);
// Show our buttons
showElement(manager.formButtons);
showElement(manager.footerFormButtons);
showElement(manager.footer);
// Ensure footer is at the bottom of the form
manager.formContainer.appendChild(manager.footer);
// When transition animation is done and the form is fully open...
handleTransitionend = onlyOnce(subForm, 'transitionend', function () {
handleTransitionend = null;
// Hide everything except first, second, last child and footer
for (let i = 2; i < manager.formContainer.children.length - 1; i++) {
const child = manager.formContainer.children[i];
const skipHiding = child === subForm
|| child.classList.contains('sp-container')
|| child.classList.contains('form-manager-footer');
if (!skipHiding) {
hideElement(manager.formContainer.children[i]);
}
}
// Resume natural height
subForm.style.height = '';
subForm.style.marginLeft = '';
subForm.classList.remove('form-manager-movable');
self.trigger('formopened');
});
// Start animation on the next tick
setTimeout(function () {
// Animation fix for fullscreen max-width limit.
subForm.style.marginLeft = (parseFloat(window.getComputedStyle(manager.formContainer.children[manager.formContainer.children.length - 2]).marginLeft) - 20) + 'px';
subForm.classList.add('form-manager-slidein');
titles.breadcrumb.classList.add('form-manager-comein');
manager.formButtons.classList.add('form-manager-comein');
manager.footerFormButtons.classList.add('form-manager-comein');
manager.updateFormResponsiveness();
}, 0);
if (proceedButton && manager.exitSemiFullscreen) {
// We are in fullscreen and opening sub-form, hide Proceed button
hideElement(proceedButton);
}
if (breadcrumbButton) {
breadcrumbButton.classList.remove('form-manager-disabled');
}
};
/**
* Check if the sub-form is fully opened. (animation finished)
*
* @return {boolean}
*/
self.isFormOpen = function () {
return subForm && !handleTransitionend;
};
/**
* Determine the overall height of the form head section.
*
* @return {number}
*/
self.getFormHeadHeight = function () {
return (alwaysShowButtons ? 0 : head.getBoundingClientRect().height);
};
/**
* Toggle the breadcrumb menu.
*/
self.toggleBreadcrumbMenu = function () {
if (self.formContainer.classList.contains('mobile-menu-open')) {
// Close breadcrumb menu
self.formContainer.classList.remove('mobile-menu-open');
breadcrumbButton.children[0].innerText = l10n.expandBreadcrumbButtonLabel;
breadcrumbButton.setAttribute('aria-label', l10n.expandBreadcrumbButtonLabel);
self.formBreadcrumbMenu.classList.remove('form-manager-comein');
}
else {
// Open breadcrumb menu
self.formContainer.classList.add('mobile-menu-open');
breadcrumbButton.children[0].innerText = l10n.collapseBreadcrumbButtonLabel;
breadcrumbButton.setAttribute('aria-label', l10n.collapseBreadcrumbButtonLabel);
self.formBreadcrumbMenu.classList.add('form-manager-comein');
}
};
/**
* Resize form header elements to fit better inside narrow forms.
*/
self.updateFormResponsiveness = function () {
if (head.classList.contains('mobile-view-large')) {
head.classList.remove('mobile-view-large');
}
if (self.formContainer.classList.contains('mobile-view-small')) {
self.formContainer.classList.remove('mobile-view-small');
}
if (head.offsetWidth < 481) {
self.formContainer.classList.add('mobile-view-small');
}
/**
* Enable tooltips where we have text-ellipsis.
*
* @private
* @param {Element} element
*/
const updateActiveTooltips = function (element) {
let tooltipActive;
for (let i = 0; i < element.children.length; i++) {
const breadcrumbTitle = element.children[i];
if (breadcrumbTitle.firstChild.offsetWidth && breadcrumbTitle.firstChild.scrollWidth > breadcrumbTitle.firstChild.offsetWidth + 1) {
breadcrumbTitle.classList.add('form-mananger-tooltip-active');
tooltipActive = true;
}
else {
breadcrumbTitle.classList.remove('form-mananger-tooltip-active');
}
}
return tooltipActive;
};
if (updateActiveTooltips(self.formBreadcrumb)) {
head.classList.add('mobile-view-large');
// Check again since we made buttons smaller
updateActiveTooltips(self.formBreadcrumb)
}
updateActiveTooltips(self.formBreadcrumbMenu);
};
/**
* Keep the buttons visible even though the last sub-form is closed.
*
* @param {Boolean} state
*/
self.setAlwaysShowButtons = function (state) {
alwaysShowButtons = state;
if (alwaysShowButtons) {
// Show our buttons
showElement(manager.formButtons);
manager.formButtons.classList.add('form-manager-comein');
}
};
// Figure out which manager to use.
const manager = findExistingManager(parent);
if (manager === self) {
initialize(); // We are the first of our kind
}
};
DragNBar.FormManager.prototype = Object.create(EventDispatcher.prototype);
DragNBar.FormManager.prototype.constructor = DragNBar.FormManager;
/**
* Help convert any HTML into text.
*
* @param {string} value
* @return {string}
*/
const getText = function (value) {
const textNode = H5PEditor.$.parseHTML(value);
if (textNode !== null) {
return textNode[0].nodeValue;
}
return value;
};
/**
* Help make sure that an event handler is only triggered once.
*
* @private
* @param {Element} element
* @param {string} eventName
* @param {function} handler
* @return {function} Callback in case of manual cancellation
*/
const onlyOnce = function (element, eventName, handler) {
const callback = function () {
// Make sure we're only called once.
element.removeEventListener(eventName, callback);
// Trigger the real handler
handler.apply(this, arguments);
};
element.addEventListener(eventName, callback);
return callback;
};
})(H5P.DragNBar, H5P.EventDispatcher);
;
/** @namespace H5P */
H5P.VideoVimeo = (function ($) {
let numInstances = 0;
/**
* Vimeo video player for H5P.
*
* @class
* @param {Array} sources Video files to use
* @param {Object} options Settings for the player
* @param {Object} l10n Localization strings
*/
function VimeoPlayer(sources, options, l10n) {
const self = this;
let player;
// Since all the methods of the Vimeo Player SDK are promise-based, we keep
// track of all relevant state variables so that we can implement the
// H5P.Video API where all methods return synchronously.
let buffered = 0;
let currentQuality;
let currentTextTrack;
let currentTime = 0;
let duration = 0;
let isMuted = 0;
let volume = 0;
let playbackRate = 1;
let qualities = [];
let loadingFailedTimeout;
let failedLoading = false;
let ratio = 9/16;
let isLoaded = false;
const LOADING_TIMEOUT_IN_SECONDS = 8;
const id = `h5p-vimeo-${++numInstances}`;
const $wrapper = $('
');
const $placeholder = $('
', {
id: id,
html: `
`
}).appendTo($wrapper);
/**
* Create a new player with the Vimeo Player SDK.
*
* @private
*/
const createVimeoPlayer = async () => {
if (!$placeholder.is(':visible') || player !== undefined) {
return;
}
// Since the SDK is loaded asynchronously below, explicitly set player to
// null (unlike undefined) which indicates that creation has begun. This
// allows the guard statement above to be hit if this function is called
// more than once.
player = null;
const Vimeo = await loadVimeoPlayerSDK();
const MIN_WIDTH = 200;
const width = Math.max($wrapper.width(), MIN_WIDTH);
const canHasControls = options.controls || self.pressToPlay;
const embedOptions = {
url: sources[0].path,
controls: canHasControls,
responsive: true,
dnt: true,
// Hardcoded autoplay to false to avoid playing videos on init
autoplay: false,
loop: options.loop ? true : false,
playsinline: true,
quality: 'auto',
width: width,
muted: false,
keyboard: canHasControls,
};
// Create a new player
player = new Vimeo.Player(id, embedOptions);
registerVimeoPlayerEventListeneners(player);
// Failsafe timeout to handle failed loading of videos.
// This seems to happen for private videos even though the SDK docs
// suggests to catch PrivacyError when attempting play()
loadingFailedTimeout = setTimeout(() => {
failedLoading = true;
removeLoadingIndicator();
$wrapper.html(`
${l10n.vimeoLoadingError}
`);
$wrapper.css({
width: null,
height: null
});
self.trigger('resize');
self.trigger('error', l10n.vimeoLoadingError);
}, LOADING_TIMEOUT_IN_SECONDS * 1000);
}
const removeLoadingIndicator = () => {
$placeholder.find('div.h5p-video-loading').remove();
};
/**
* Register event listeners on the given Vimeo player.
*
* @private
* @param {Vimeo.Player} player
*/
const registerVimeoPlayerEventListeneners = (player) => {
let isFirstPlay, tracks;
player.on('loaded', async () => {
isFirstPlay = true;
isLoaded = true;
clearTimeout(loadingFailedTimeout);
const videoDetails = await getVimeoVideoMetadata(player);
tracks = videoDetails.tracks.options;
currentTextTrack = tracks.current;
duration = videoDetails.duration;
qualities = videoDetails.qualities;
currentQuality = 'auto';
try {
ratio = videoDetails.dimensions.height / videoDetails.dimensions.width;
}
catch (e) { /* Intentionally ignore this, and fallback on the default ratio */ }
removeLoadingIndicator();
if (options.startAt) {
// Vimeo.Player doesn't have an option for setting start time upon
// instantiation, so we instead perform an initial seek here.
currentTime = await self.seek(options.startAt);
}
self.trigger('ready');
self.trigger('loaded');
self.trigger('qualityChange', currentQuality);
self.trigger('resize');
});
player.on('play', () => {
if (isFirstPlay) {
isFirstPlay = false;
if (tracks.length) {
self.trigger('captions', tracks);
}
}
});
// Handle playback state changes.
player.on('playing', () => self.trigger('stateChange', H5P.Video.PLAYING));
player.on('pause', () => self.trigger('stateChange', H5P.Video.PAUSED));
player.on('ended', () => self.trigger('stateChange', H5P.Video.ENDED));
// Track the percentage of video that has finished loading (buffered).
player.on('progress', (data) => {
buffered = data.percent * 100;
});
// Track the current time. The update frequency may be browser-dependent,
// according to the official docs:
// https://developer.vimeo.com/player/sdk/reference#timeupdate
player.on('timeupdate', (time) => {
currentTime = time.seconds;
});
};
/**
* Get metadata about the video loaded in the given Vimeo player.
*
* Example resolved value:
*
* ```
* {
* "duration": 39,
* "qualities": [
* {
* "name": "auto",
* "label": "Auto"
* },
* {
* "name": "1080p",
* "label": "1080p"
* },
* {
* "name": "720p",
* "label": "720p"
* }
* ],
* "dimensions": {
* "width": 1920,
* "height": 1080
* },
* "tracks": {
* "current": {
* "label": "English",
* "value": "en"
* },
* "options": [
* {
* "label": "English",
* "value": "en"
* },
* {
* "label": "Norsk bokmål",
* "value": "nb"
* }
* ]
* }
* }
* ```
*
* @private
* @param {Vimeo.Player} player
* @returns {Promise}
*/
const getVimeoVideoMetadata = (player) => {
// Create an object for easy lookup of relevant metadata
const massageVideoMetadata = (data) => {
const duration = data[0];
const qualities = data[1].map(q => ({
name: q.id,
label: q.label
}));
const tracks = data[2].reduce((tracks, current) => {
const h5pVideoTrack = new H5P.Video.LabelValue(current.label, current.language);
tracks.options.push(h5pVideoTrack);
if (current.mode === 'showing') {
tracks.current = h5pVideoTrack;
}
return tracks;
}, { current: undefined, options: [] });
const dimensions = { width: data[3], height: data[4] };
return {
duration,
qualities,
tracks,
dimensions
};
};
return Promise.all([
player.getDuration(),
player.getQualities(),
player.getTextTracks(),
player.getVideoWidth(),
player.getVideoHeight(),
]).then(data => massageVideoMetadata(data));
}
try {
if (document.featurePolicy.allowsFeature('autoplay') === false) {
self.pressToPlay = true;
}
}
catch (err) {}
/**
* Appends the video player to the DOM.
*
* @public
* @param {jQuery} $container
*/
self.appendTo = ($container) => {
$container.addClass('h5p-vimeo').append($wrapper);
createVimeoPlayer();
};
/**
* Get list of available qualities.
*
* @public
* @returns {Array}
*/
self.getQualities = () => {
return qualities;
};
/**
* Get the current quality.
*
* @returns {String} Current quality identifier
*/
self.getQuality = () => {
return currentQuality;
};
/**
* Set the playback quality.
*
* @public
* @param {String} quality
*/
self.setQuality = async (quality) => {
currentQuality = await player.setQuality(quality);
self.trigger('qualityChange', currentQuality);
};
/**
* Start the video.
*
* @public
*/
self.play = async () => {
if (!player) {
self.on('ready', self.play);
return;
}
try {
await player.play();
}
catch (error) {
switch (error.name) {
case 'PasswordError': // The video is password-protected
self.trigger('error', l10n.vimeoPasswordError);
break;
case 'PrivacyError': // The video is private
self.trigger('error', l10n.vimeoPrivacyError);
break;
default:
self.trigger('error', l10n.unknownError);
break;
}
}
};
/**
* Pause the video.
*
* @public
*/
self.pause = () => {
if (player) {
player.pause();
}
};
/**
* Seek video to given time.
*
* @public
* @param {Number} time
*/
self.seek = async (time) => {
if (!player) {
return;
}
currentTime = time;
await player.setCurrentTime(time);
};
/**
* @public
* @returns {Number} Seconds elapsed since beginning of video
*/
self.getCurrentTime = () => {
return currentTime;
};
/**
* @public
* @returns {Number} Video duration in seconds
*/
self.getDuration = () => {
return duration;
};
/**
* Get percentage of video that is buffered.
*
* @public
* @returns {Number} Between 0 and 100
*/
self.getBuffered = () => {
return buffered;
};
/**
* Mute the video.
*
* @public
*/
self.mute = async () => {
isMuted = await player.setMuted(true);
};
/**
* Unmute the video.
*
* @public
*/
self.unMute = async () => {
isMuted = await player.setMuted(false);
};
/**
* Whether the video is muted.
*
* @public
* @returns {Boolean} True if the video is muted, false otherwise
*/
self.isMuted = () => {
return isMuted;
};
/**
* Whether the video is loaded.
*
* @public
* @returns {Boolean} True if the video is muted, false otherwise
*/
self.isLoaded = () => {
return isLoaded;
};
/**
* Get the video player's current sound volume.
*
* @public
* @returns {Number} Between 0 and 100.
*/
self.getVolume = () => {
return volume;
};
/**
* Set the video player's sound volume.
*
* @public
* @param {Number} level
*/
self.setVolume = async (level) => {
volume = await player.setVolume(level);
};
/**
* Get list of available playback rates.
*
* @public
* @returns {Array} Available playback rates
*/
self.getPlaybackRates = () => {
return [0.5, 1, 1.5, 2];
};
/**
* Get the current playback rate.
*
* @public
* @returns {Number} e.g. 0.5, 1, 1.5 or 2
*/
self.getPlaybackRate = () => {
return playbackRate;
};
/**
* Set the current playback rate.
*
* @public
* @param {Number} rate Must be one of available rates from getPlaybackRates
*/
self.setPlaybackRate = async (rate) => {
playbackRate = await player.setPlaybackRate(rate);
self.trigger('playbackRateChange', rate);
};
/**
* Set current captions track.
*
* @public
* @param {H5P.Video.LabelValue} track Captions to display
*/
self.setCaptionsTrack = (track) => {
if (!track) {
return player.disableTextTrack().then(() => {
currentTextTrack = null;
});
}
player.enableTextTrack(track.value).then(() => {
currentTextTrack = track;
});
};
/**
* Get current captions track.
*
* @public
* @returns {H5P.Video.LabelValue}
*/
self.getCaptionsTrack = () => {
return currentTextTrack;
};
self.on('resize', () => {
if (failedLoading || !$wrapper.is(':visible')) {
return;
}
if (player === undefined) {
// Player isn't created yet. Try again.
createVimeoPlayer();
return;
}
// Use as much space as possible
$wrapper.css({
width: '100%',
height: 'auto'
});
const width = $wrapper[0].clientWidth;
const height = options.fit ? $wrapper[0].clientHeight : (width * (ratio));
// Validate height before setting
if (height > 0) {
// Set size
$wrapper.css({
width: width + 'px',
height: height + 'px'
});
}
});
}
/**
* Check to see if we can play any of the given sources.
*
* @public
* @static
* @param {Array} sources
* @returns {Boolean}
*/
VimeoPlayer.canPlay = (sources) => {
return getId(sources[0].path);
};
/**
* Find id of Vimeo video from given URL.
*
* @private
* @param {String} url
* @returns {String} Vimeo video ID
*/
const getId = (url) => {
// https://stackoverflow.com/a/11660798
const matches = url.match(/^.*(vimeo\.com\/)((channels\/[A-z]+\/)|(groups\/[A-z]+\/videos\/))?([0-9]+)/);
if (matches && matches[5]) {
return matches[5];
}
};
/**
* Load the Vimeo Player SDK asynchronously.
*
* @private
* @returns {Promise} Vimeo Player SDK object
*/
const loadVimeoPlayerSDK = async () => {
if (window.Vimeo) {
return await Promise.resolve(window.Vimeo);
}
return await new Promise((resolve, reject) => {
const tag = document.createElement('script');
tag.src = 'https://player.vimeo.com/api/player.js';
tag.onload = () => resolve(window.Vimeo);
tag.onerror = reject;
document.querySelector('script').before(tag);
});
};
return VimeoPlayer;
})(H5P.jQuery);
// Register video handler
H5P.videoHandlers = H5P.videoHandlers || [];
H5P.videoHandlers.push(H5P.VideoVimeo);
;
/** @namespace H5P */
H5P.VideoYouTube = (function ($) {
/**
* YouTube video player for H5P.
*
* @class
* @param {Array} sources Video files to use
* @param {Object} options Settings for the player
* @param {Object} l10n Localization strings
*/
function YouTube(sources, options, l10n) {
var self = this;
var player;
var playbackRate = 1;
var id = 'h5p-youtube-' + numInstances;
numInstances++;
var ratio = 9/16;
var $wrapper = $('
');
var $placeholder = $('
', {
text: l10n.loading,
html: `
`
}).appendTo($wrapper);
// Optional placeholder
// var $placeholder = $('
VIDEO ').appendTo($wrapper);
/**
* Use the YouTube API to create a new player
*
* @private
*/
var create = function () {
if (!$placeholder.is(':visible') || player !== undefined) {
return;
}
if (window.YT === undefined) {
// Load API first
loadAPI(create);
return;
}
if (YT.Player === undefined) {
return;
}
var width = $wrapper.width();
if (width < 200) {
width = 200;
}
var loadCaptionsModule = true;
var videoId = getId(sources[0].path);
player = new YT.Player(id, {
width: width,
height: width * (9/16),
videoId: videoId,
playerVars: {
origin: ORIGIN,
// Hardcoded autoplay to false to avoid playing videos on init
autoplay: 0,
controls: options.controls ? 1 : 0,
disablekb: options.controls ? 0 : 1,
fs: 0,
loop: options.loop ? 1 : 0,
playlist: options.loop ? videoId : undefined,
rel: 0,
showinfo: 0,
iv_load_policy: 3,
wmode: "opaque",
start: Math.floor(options.startAt),
playsinline: 1
},
events: {
onReady: function () {
self.trigger('ready');
self.trigger('loaded');
if (!options.autoplay) {
self.toPause = true;
}
if (options.deactivateSound) {
self.mute();
}
},
onApiChange: function () {
if (loadCaptionsModule) {
loadCaptionsModule = false;
// Always load captions
player.loadModule('captions');
}
var trackList;
try {
// Grab tracklist from player
trackList = player.getOption('captions', 'tracklist');
}
catch (err) {}
if (trackList && trackList.length) {
// Format track list into valid track options
var trackOptions = [];
for (var i = 0; i < trackList.length; i++) {
trackOptions.push(new H5P.Video.LabelValue(trackList[i].displayName, trackList[i].languageCode));
}
// Captions are ready for loading
self.trigger('captions', trackOptions);
}
},
onStateChange: function (state) {
if (state.data > -1 && state.data < 4) {
if (self.toPause) {
// if video buffering, was likely paused already - skip
if (state.data === H5P.Video.BUFFERING) {
delete self.toPause;
}
else {
self.pause();
}
}
// Fix for keeping playback rate in IE11
if (H5P.Video.IE11_PLAYBACK_RATE_FIX && state.data === H5P.Video.PLAYING && playbackRate !== 1) {
// YT doesn't know that IE11 changed the rate so it must be reset before it's set to the correct value
player.setPlaybackRate(1);
player.setPlaybackRate(playbackRate);
}
// End IE11 fix
self.trigger('stateChange', state.data);
}
},
onPlaybackQualityChange: function (quality) {
self.trigger('qualityChange', quality.data);
},
onPlaybackRateChange: function (playbackRate) {
self.trigger('playbackRateChange', playbackRate.data);
},
onError: function (error) {
var message;
switch (error.data) {
case 2:
message = l10n.invalidYtId;
break;
case 100:
message = l10n.unknownYtId;
break;
case 101:
case 150:
message = l10n.restrictedYt;
break;
default:
message = l10n.unknownError + ' ' + error.data;
break;
}
self.trigger('error', message);
}
}
});
player.g.style = "position:absolute;top:0;left:0;width:100%;height:100%;";
};
/**
* Indicates if the video must be clicked for it to start playing.
* For instance YouTube videos on iPad must be pressed to start playing.
*
* @public
*/
if (navigator.userAgent.match(/iPad/i)) {
self.pressToPlay = true;
}
else {
try {
if (document.featurePolicy.allowsFeature('autoplay') === false) {
self.pressToPlay = true;
}
}
catch (err) {}
}
/**
* Appends the video player to the DOM.
*
* @public
* @param {jQuery} $container
*/
self.appendTo = function ($container) {
$container.addClass('h5p-youtube').append($wrapper);
create();
};
/**
* Get list of available qualities. Not available until after play.
*
* @public
* @returns {Array}
*/
self.getQualities = function () {
if (!player || !player.getAvailableQualityLevels) {
return;
}
var qualities = player.getAvailableQualityLevels();
if (!qualities.length) {
return; // No qualities
}
// Add labels
for (var i = 0; i < qualities.length; i++) {
var quality = qualities[i];
var label = (LABELS[quality] !== undefined ? LABELS[quality] : 'Unknown'); // TODO: l10n
qualities[i] = {
name: quality,
label: LABELS[quality]
};
}
return qualities;
};
/**
* Get current playback quality. Not available until after play.
*
* @public
* @returns {String}
*/
self.getQuality = function () {
if (!player || !player.getPlaybackQuality) {
return;
}
var quality = player.getPlaybackQuality();
return quality === 'unknown' ? undefined : quality;
};
/**
* Set current playback quality. Not available until after play.
* Listen to event "qualityChange" to check if successful.
*
* @public
* @params {String} [quality]
*/
self.setQuality = function (quality) {
if (!player || !player.setPlaybackQuality) {
return;
}
player.setPlaybackQuality(quality);
};
/**
* Start the video.
*
* @public
*/
self.play = function () {
if (!player || !player.playVideo) {
self.on('ready', self.play);
return;
}
player.playVideo();
};
/**
* Pause the video.
*
* @public
*/
self.pause = function () {
delete self.toPause;
self.off('ready', self.play);
if (!player || !player.pauseVideo) {
return;
}
player.pauseVideo();
};
/**
* Seek video to given time.
*
* @public
* @param {Number} time
*/
self.seek = function (time) {
if (!player || !player.seekTo) {
return;
}
player.seekTo(time, true);
};
/**
* Recreate player with initial time
*
* @public
* @param {Number} time
*/
self.resetPlayback = function (time) {
options.startAt = time;
if (player) {
if (player.getPlayerState() === H5P.Video.PLAYING) {
player.pauseVideo();
self.trigger('stateChange', H5P.Video.PAUSED);
}
player.destroy();
player = undefined;
}
create();
}
/**
* Get elapsed time since video beginning.
*
* @public
* @returns {Number}
*/
self.getCurrentTime = function () {
if (!player || !player.getCurrentTime) {
return;
}
return player.getCurrentTime();
};
/**
* Get total video duration time.
*
* @public
* @returns {Number}
*/
self.getDuration = function () {
if (!player || !player.getDuration) {
return;
}
return player.getDuration();
};
/**
* Get percentage of video that is buffered.
*
* @public
* @returns {Number} Between 0 and 100
*/
self.getBuffered = function () {
if (!player || !player.getVideoLoadedFraction) {
return;
}
return player.getVideoLoadedFraction() * 100;
};
/**
* Turn off video sound.
*
* @public
*/
self.mute = function () {
if (!player || !player.mute) {
return;
}
player.mute();
};
/**
* Turn on video sound.
*
* @public
*/
self.unMute = function () {
if (!player || !player.unMute) {
return;
}
player.unMute();
};
/**
* Check if video sound is turned on or off.
*
* @public
* @returns {Boolean}
*/
self.isMuted = function () {
if (!player || !player.isMuted) {
return;
}
return player.isMuted();
};
/**
* Check if video is loaded and ready to play.
*
* @public
* @returns {Boolean}
*/
self.isLoaded = function () {
if (!player || !player.getPlayerState) {
return;
}
return player.getPlayerState() === 5;
};
/**
* Return the video sound level.
*
* @public
* @returns {Number} Between 0 and 100.
*/
self.getVolume = function () {
if (!player || !player.getVolume) {
return;
}
return player.getVolume();
};
/**
* Set video sound level.
*
* @public
* @param {Number} level Between 0 and 100.
*/
self.setVolume = function (level) {
if (!player || !player.setVolume) {
return;
}
player.setVolume(level);
};
/**
* Get list of available playback rates.
*
* @public
* @returns {Array} available playback rates
*/
self.getPlaybackRates = function () {
if (!player || !player.getAvailablePlaybackRates) {
return;
}
var playbackRates = player.getAvailablePlaybackRates();
if (!playbackRates.length) {
return; // No rates, but the array should contain at least 1
}
return playbackRates;
};
/**
* Get current playback rate.
*
* @public
* @returns {Number} such as 0.25, 0.5, 1, 1.25, 1.5 and 2
*/
self.getPlaybackRate = function () {
if (!player || !player.getPlaybackRate) {
return;
}
return player.getPlaybackRate();
};
/**
* Set current playback rate.
* Listen to event "playbackRateChange" to check if successful.
*
* @public
* @params {Number} suggested rate that may be rounded to supported values
*/
self.setPlaybackRate = function (newPlaybackRate) {
if (!player || !player.setPlaybackRate) {
return;
}
playbackRate = Number(newPlaybackRate);
player.setPlaybackRate(playbackRate);
};
/**
* Set current captions track.
*
* @param {H5P.Video.LabelValue} Captions track to show during playback
*/
self.setCaptionsTrack = function (track) {
player.setOption('captions', 'track', track ? {languageCode: track.value} : {});
};
/**
* Figure out which captions track is currently used.
*
* @return {H5P.Video.LabelValue} Captions track
*/
self.getCaptionsTrack = function () {
var track = player.getOption('captions', 'track');
return (track.languageCode ? new H5P.Video.LabelValue(track.displayName, track.languageCode) : null);
};
// Respond to resize events by setting the YT player size.
self.on('resize', function () {
if (!$wrapper.is(':visible')) {
return;
}
if (!player) {
// Player isn't created yet. Try again.
create();
return;
}
// Use as much space as possible
$wrapper.css({
width: '100%',
height: 'auto'
});
var width = $wrapper[0].clientWidth;
var height = options.fit ? $wrapper[0].clientHeight : (width * (9/16));
// Validate height before setting
if (height > 0) {
// Set size
$wrapper.css({
width: width + 'px',
height: height + 'px'
});
player.setSize(width, height);
}
});
}
/**
* Check to see if we can play any of the given sources.
*
* @public
* @static
* @param {Array} sources
* @returns {Boolean}
*/
YouTube.canPlay = function (sources) {
return getId(sources[0].path);
};
/**
* Find id of YouTube video from given URL.
*
* @private
* @param {String} url
* @returns {String} YouTube video identifier
*/
var getId = function (url) {
// Has some false positives, but should cover all regular URLs that people can find
var matches = url.match(/(?:(?:youtube.com\/(?:attribution_link\?(?:\S+))?(?:v\/|embed\/|watch\/|(?:user\/(?:\S+)\/)?watch(?:\S+)v\=))|(?:youtu.be\/|y2u.be\/))([A-Za-z0-9_-]{11})/i);
if (matches && matches[1]) {
return matches[1];
}
};
/**
* Load the IFrame Player API asynchronously.
*/
var loadAPI = function (loaded) {
if (window.onYouTubeIframeAPIReady !== undefined) {
// Someone else is loading, hook in
var original = window.onYouTubeIframeAPIReady;
window.onYouTubeIframeAPIReady = function (id) {
loaded(id);
original(id);
};
}
else {
// Load the API our self
var tag = document.createElement('script');
tag.src = "https://www.youtube.com/iframe_api";
var firstScriptTag = document.getElementsByTagName('script')[0];
firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
window.onYouTubeIframeAPIReady = loaded;
}
};
/** @constant {Object} */
var LABELS = {
highres: '2160p', // Old API support
hd2160: '2160p', // (New API)
hd1440: '1440p',
hd1080: '1080p',
hd720: '720p',
large: '480p',
medium: '360p',
small: '240p',
tiny: '144p',
auto: 'Auto'
};
/** @private */
var numInstances = 0;
// Extract the current origin (used for security)
var ORIGIN = window.location.href.match(/http[s]?:\/\/[^\/]+/);
ORIGIN = !ORIGIN || ORIGIN[0] === undefined ? undefined : ORIGIN[0];
// ORIGIN = undefined is needed to support fetching file from device local storage
return YouTube;
})(H5P.jQuery);
// Register video handler
H5P.videoHandlers = H5P.videoHandlers || [];
H5P.videoHandlers.push(H5P.VideoYouTube);
;
/** @namespace H5P */
H5P.VideoPanopto = (function ($) {
/**
* Panopto video player for H5P.
*
* @class
* @param {Array} sources Video files to use
* @param {Object} options Settings for the player
* @param {Object} l10n Localization strings
*/
function Panopto(sources, options, l10n) {
var self = this;
self.volume = 100;
self.toSeek = undefined;
var player;
var playbackRate = 1;
let canHasAutoplay;
var id = 'h5p-panopto-' + numInstances;
numInstances++;
let isLoaded = false;
let isPlayerReady = false;
var $wrapper = $('
');
var $placeholder = $('
', {
id: id,
html: '
' + l10n.loading + '
'
}).appendTo($wrapper);
// Determine autoplay/play.
try {
if (document.featurePolicy.allowsFeature('autoplay') !== false) {
canHasAutoplay = true;
}
}
catch (err) {}
/**
* Use the Panopto API to create a new player
*
* @private
*/
var create = function () {
if (!$placeholder.is(':visible') || player !== undefined) {
return;
}
if (window.EmbedApi === undefined) {
// Load API first
loadAPI(create);
return;
}
var width = $wrapper.width();
if (width < 200) {
width = 200;
}
const videoId = getId(sources[0].path);
player = new EmbedApi(id, {
width: width,
height: width * (9/16),
serverName: videoId[0],
sessionId: videoId[1],
videoParams: { // Optional
interactivity: 'none',
showtitle: false,
autohide: true,
offerviewer: false,
autoplay: false,
showbrand: false,
start: 0,
hideoverlay: !options.controls,
},
events: {
onIframeReady: function () {
isPlayerReady = true;
$placeholder.children(0).text('');
if (options.autoplay && canHasAutoplay) {
player.loadVideo();
isLoaded = true;
}
self.trigger('containerLoaded');
self.trigger('resize'); // Avoid black iframe if loading is slow
},
onReady: function () {
self.videoLoaded = true;
self.trigger('loaded');
if (typeof self.oldTime === 'number') {
self.seek(self.oldTime);
}
else if (typeof self.startAt === 'number' && self.startAt > 0) {
self.seek(self.startAt);
}
if (player.hasCaptions()) {
const captions = [];
const captionTracks = player.getCaptionTracks();
for (trackIndex in captionTracks) {
captions.push(new H5P.Video.LabelValue(captionTracks[trackIndex], trackIndex));
}
// Select active track
currentTrack = player.getSelectedCaptionTrack();
currentTrack = captions[currentTrack] ? captions[currentTrack] : null;
self.trigger('captions', captions);
}
},
onStateChange: function (state) {
if ([H5P.Video.PLAYING, H5P.Video.PAUSED].includes(state) && typeof self.seekToTime === 'number') {
player.seekTo(self.seekToTime);
delete self.seekToTime;
}
// since panopto has different load sequence in IV, need additional condition here
if (self.WAS_RESET) {
self.WAS_RESET = false;
}
// TODO: Playback rate fix for IE11?
if (state > -1 && state < 4) {
self.trigger('stateChange', state);
}
},
onPlaybackRateChange: function () {
self.trigger('playbackRateChange', self.getPlaybackRate());
},
onError: function (error) {
if (error === ApiError.PlayWithSoundNotAllowed) {
// pause and allow user to handle playing
self.pause();
self.unMute(); // because player is automuted on this error
}
else {
self.trigger('error', l10n.unknownError);
}
},
onLoginShown: function () {
$placeholder.children().first().remove(); // Remove loading message
self.trigger('loaded'); // Resize parent
}
}
});
};
/**
* Indicates if the video must be clicked for it to start playing.
* This is always true for Panopto since all videos auto play.
*
* @public
*/
self.pressToPlay = true;
/**
* Appends the video player to the DOM.
*
* @public
* @param {jQuery} $container
*/
self.appendTo = function ($container) {
$container.addClass('h5p-panopto').append($wrapper);
create();
};
/**
* Get list of available qualities. Not available until after play.
*
* @public
* @returns {Array}
*/
self.getQualities = function () {
// Not available for Panopto
};
/**
* Get current playback quality. Not available until after play.
*
* @public
* @returns {String}
*/
self.getQuality = function () {
// Not available for Panopto
};
/**
* Set current playback quality. Not available until after play.
* Listen to event "qualityChange" to check if successful.
*
* @public
* @params {String} [quality]
*/
self.setQuality = function (quality) {
// Not available for Panopto
};
/**
* Start the video.
*
* @public
*/
self.play = function () {
if (!player || !player.playVideo || !isPlayerReady) {
return;
}
if (isLoaded || self.videoLoaded) {
player.playVideo();
}
else {
player.loadVideo(); // Loads and starts playing
isLoaded = true;
}
};
/**
* Pause the video.
*
* @public
*/
self.pause = function () {
if (!player || !player.pauseVideo) {
return;
}
try {
player.pauseVideo();
}
catch (err) {
// Swallow Panopto throwing an error. This has been seen in the authoring
// tool if Panopto has been used inside Iv inside CP
}
};
/**
* Seek video to given time.
*
* @public
* @param {Number} time
*/
self.seek = function (time) {
if (!player || !player.seekTo || !self.videoLoaded) {
return;
}
if (!player.isReady) {
self.seekToTime = time;
return;
}
player.seekTo(time);
if (self.WAS_RESET) {
// need to check just to be sure, since state === 1 is unusable
delete self.seekToTime;
self.WAS_RESET = false;
}
};
/**
* Recreate player with initial time
*
* @public
* @param {Number} time
*/
self.resetPlayback = function (time) {
if (player && player.isReady && self.videoLoaded) {
self.seek(time);
self.pause();
}
else {
self.seekToTime = time;
}
}
/**
* Get elapsed time since video beginning.
*
* @public
* @returns {Number}
*/
self.getCurrentTime = function () {
if (!player || !player.getCurrentTime) {
return;
}
return player.getCurrentTime();
};
/**
* Get total video duration time.
*
* @public
* @returns {Number}
*/
self.getDuration = function () {
if (!player || !player.getDuration) {
return;
}
return player.getDuration();
};
/**
* Get percentage of video that is buffered.
*
* @public
* @returns {Number} Between 0 and 100
*/
self.getBuffered = function () {
// Not available for Panopto
};
/**
* Turn off video sound.
*
* @public
*/
self.mute = function () {
if (!player || !player.muteVideo) {
return;
}
player.muteVideo();
};
/**
* Turn on video sound.
*
* @public
*/
self.unMute = function () {
if (!player || !player.unmuteVideo) {
return;
}
player.unmuteVideo();
// The volume is set to 0 when the browser prevents autoplay,
// causing there to be no sound despite unmuting
self.setVolume(self.volume);
};
/**
* Check if video sound is turned on or off.
*
* @public
* @returns {Boolean}
*/
self.isMuted = function () {
if (!player || !player.isMuted) {
return;
}
return player.isMuted();
};
/**
* Check video is loaded and ready to play
*
* @public
* @returns {Boolean}
*/
self.isLoaded = function () {
return isPlayerReady;
};
/**
* Return the video sound level.
*
* @public
* @returns {Number} Between 0 and 100.
*/
self.getVolume = function () {
if (!player || !player.getVolume) {
return;
}
return player.getVolume() * 100;
};
/**
* Set video sound level.
*
* @public
* @param {Number} level Between 0 and 100.
*/
self.setVolume = function (level) {
if (!player || !player.setVolume) {
return;
}
player.setVolume(level/100);
self.volume = level;
};
/**
* Get list of available playback rates.
*
* @public
* @returns {Array} available playback rates
*/
self.getPlaybackRates = function () {
return [0.25, 0.5, 1, 1.25, 1.5, 2];
};
/**
* Get current playback rate.
*
* @public
* @returns {Number} such as 0.25, 0.5, 1, 1.25, 1.5 and 2
*/
self.getPlaybackRate = function () {
if (!player || !player.getPlaybackRate) {
return;
}
return player.getPlaybackRate();
};
/**
* Set current playback rate.
* Listen to event "playbackRateChange" to check if successful.
*
* @public
* @params {Number} suggested rate that may be rounded to supported values
*/
self.setPlaybackRate = function (newPlaybackRate) {
if (!player || !player.setPlaybackRate) {
return;
}
player.setPlaybackRate(newPlaybackRate);
};
/**
* Set current captions track.
*
* @param {H5P.Video.LabelValue} Captions track to show during playback
*/
self.setCaptionsTrack = function (track) {
if (!track) {
player.disableCaptions();
currentTrack = null;
}
else {
player.enableCaptions(track.value + '');
currentTrack = track;
}
};
/**
* Figure out which captions track is currently used.
*
* @return {H5P.Video.LabelValue} Captions track
*/
self.getCaptionsTrack = function () {
return currentTrack; // No function for getting active caption track?
};
// Respond to resize events by setting the player size.
self.on('resize', function () {
if (!$wrapper.is(':visible')) {
return;
}
if (!player) {
// Player isn't created yet. Try again.
create();
return;
}
$wrapper.removeAttr('font-size');
let width = $wrapper[0].clientWidth;
let height = options.fit ? $wrapper[0].clientHeight : (width * (9/16));
const $iframe = $placeholder.children('iframe');
if ($iframe.length) {
$iframe.attr('width', width);
$iframe.attr('height', height);
}
$wrapper.css({
'font-size': 0
});
});
let currentTrack;
}
/**
* Check to see if we can play any of the given sources.
*
* @public
* @static
* @param {Array} sources
* @returns {Boolean}
*/
Panopto.canPlay = function (sources) {
return getId(sources[0].path);
};
/**
* Find id of YouTube video from given URL.
*
* @private
* @param {String} url
* @returns {String} Panopto video identifier
*/
var getId = function (url) {
const matches = url.match(/^[^\/]+:\/\/([^\/]*panopto\.[^\/]+)\/Panopto\/.+\?id=(.+)$/);
if (matches && matches.length === 3) {
return [matches[1], matches[2]];
}
};
/**
* Load the IFrame Player API asynchronously.
*/
var loadAPI = function (loaded) {
if (window.onPanoptoEmbedApiReady !== undefined) {
// Someone else is loading, hook in
var original = window.onPanoptoEmbedApiReady;
window.onPanoptoEmbedApiReady = function (id) {
loaded(id);
original(id);
};
}
else {
// Load the API our self
var tag = document.createElement('script');
tag.src = 'https://developers.panopto.com/scripts/embedapi.min.js';
var firstScriptTag = document.getElementsByTagName('script')[0];
firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
window.onPanoptoEmbedApiReady = loaded;
}
};
/** @private */
var numInstances = 0;
return Panopto;
})(H5P.jQuery);
// Register video handler
H5P.videoHandlers = H5P.videoHandlers || [];
H5P.videoHandlers.push(H5P.VideoPanopto);
;
/** @namespace Echo */
H5P.VideoEchoVideo = (() => {
let numInstances = 0;
const CONTROLS_HEIGHT = 100;
/**
* EchoVideo video player for H5P.
*
* @class
* @param {Array} sources Video files to use
* @param {Object} options Settings for the player
* @param {Object} l10n Localization strings
*/
function EchoPlayer(sources, options, l10n) {
// State variables for the Player.
let player = undefined;
let buffered = 0;
let currentQuality;
let trackOptions = [];
let currentTime = 0;
let duration = 0;
let isMuted = false;
let loadingComplete = false;
let volume = 1;
let playbackRate = 1;
let qualities = [];
let loadingFailedTimeout;
let failedLoading = false;
let ratio = 9 / 16;
let currentState = H5P.Video.VIDEO_CUED;
// Echo360 server doesn't sync seek time with regular play time fast enough
let timelineUpdatesToSkip = 0;
let timeUpdateTimeout;
/*
* Echo360 player does send time updates ~ 0.25 seconds by default and
* ends playing the video without sending a final time update or an
* Video Ended event. We take care of determining reaching the video end
* ourselves.
*/
const echoMinUncertaintyCompensationS = 0.3;
const timelineUpdateDeltaSlackMS = 50;
let echoUncertaintyCompensationS = echoMinUncertaintyCompensationS;
let previousTickMS;
// Player specific immutable variables.
const LOADING_TIMEOUT_IN_SECONDS = 30;
const id = `h5p-echo-${++numInstances}`;
const instanceId = H5P.createUUID();
const wrapperElement = document.createElement('div');
const placeholderElement = document.createElement('div');
placeholderElement.classList.add('h5p-video-loading');
placeholderElement.setAttribute('style', 'height: 100%; min-height: 200px; display: block; z-index: 100; border: none;');
placeholderElement.setAttribute('aria-label', l10n.loading);
wrapperElement.setAttribute('id', id);
wrapperElement.append(placeholderElement);
/**
* Remove all elements from the placeholder dom element.
*
* @private
*/
const removeLoadingIndicator = () => {
placeholderElement.replaceChildren();
};
/**
* Generate an array of objects for use in a dropdown from the list of resolutions.
* @private
* @param {Array} qualityLevels - list of objects with supported qualities for the media
* @returns {Array} list of objects with label and name properties
*/
const mapQualityLevels = (qualityLevels) => {
const qualities = qualityLevels.map((quality) => {
return { label: quality.label.toLowerCase(), name: quality.value };
});
return qualities;
};
/**
* Register event listeners on the given Echo player.
*
* @private
* @param {HTMLElement} player
*/
const registerEchoPlayerEventListeneners = (player) => {
player.resolveLoading = null;
player.loadingPromise = new Promise((resolve) => {
player.resolveLoading = resolve;
});
player.onload = async () => {
clearTimeout(loadingFailedTimeout);
player.loadingPromise.then(async () => {
this.trigger('ready');
this.trigger('loaded');
this.loadingComplete = true;
this.trigger('resize');
if (trackOptions.length) {
this.trigger('captions', trackOptions);
}
const autoplayIsAllowed = !window.H5PEditor &&
await H5P.Video.isAutoplayAllowed();
if (options.autoplay && autoplayIsAllowed) {
this.play();
}
return true;
});
};
window.addEventListener('message', (event) => {
let message = '';
try {
message = JSON.parse(event.data);
}
catch (e) {
return;
}
if (
message.context !== 'Echo360' || message.instanceId !== instanceId
) {
return;
}
if (message.event === 'init') {
// Set ratio if width and height is received from Echo360
if (message.data.width && message.data.height) {
// If controls are displayed we have to add a magic height to make it visible :(
ratio = ((message.data.height + (options.controls ? CONTROLS_HEIGHT : 0)) / message.data.width);
}
duration = message.data.duration;
this.setCurrentTime(message.data.currentTime ?? 0);
textTracks = message.data.textTracks ?? [];
if (message.data.captions) {
trackOptions = textTracks.map((track) =>
new H5P.Video.LabelValue(track.label, track.value)
);
}
player.resolveLoading();
// Player sends `init` event after rebuffering, unfortunately.
if (!this.wasInitialized) {
qualities = mapQualityLevels(message.data.qualityOptions);
currentQuality = qualities[0].name;
this.trigger('qualityChange', currentQuality);
}
this.trigger('resize');
if (message.data.playing) {
changeState(H5P.Video.PLAYING);
}
this.wasInitialized = true;
}
else if (message.event === 'timeline') {
updateUncertaintyCompensation();
duration = message.data.duration ?? this.getDuration();
if (timelineUpdatesToSkip === 0) {
this.setCurrentTime(message.data.currentTime ?? 0);
}
else {
timelineUpdatesToSkip--;
}
/*
* Should work, but it was better if the player itself clearly sent
* the state (playing, paused, ended) instead of us having to infer.
*/
const compensatedTime = this.getCurrentTime() +
echoUncertaintyCompensationS * this.getPlaybackRate()
if (
currentState === H5P.Video.PLAYING &&
Math.ceil(compensatedTime) >= duration
) {
changeState(H5P.Video.ENDED);
if (options.loop) {
this.seek(0);
this.play();
}
return;
}
if (message.data.playing) {
timeUpdate(currentTime);
changeState(H5P.Video.PLAYING);
}
else if (currentState === H5P.Video.PLAYING) {
// Condition prevents video to be paused on startup
changeState(H5P.Video.PAUSED);
window.clearTimeout(timeUpdateTimeout);
}
}
});
};
/**
* Update the uncertainty compensation value.
* Computes the delta time between the last two timeline events sent by the
* Echo360 player and updates the compensation value.
*/
const updateUncertaintyCompensation = () => {
if (currentState === H5P.Video.PLAYING) {
const time = Date.now();
if (previousTickMS) {
echoUncertaintyCompensationS = Math.max(
echoMinUncertaintyCompensationS,
(time - previousTickMS + timelineUpdateDeltaSlackMS) /
1000
)
} else {
echoUncertaintyCompensationS = echoMinUncertaintyCompensationS;
}
previousTickMS = time;
}
else {
delete previousTickMS;
}
}
/**
* Change state of the player.
* @param {number} state State id (H5P.Video[statename]).
*/
const changeState = (state) => {
if (state !== currentState) {
currentState = state;
this.trigger('stateChange', state);
}
};
/**
* Determine if the element is visible by computing the styles.
*
* @private
* @param {HTMLElement} node - the element to check.
* @returns {Boolean} true if it is visible.
*/
const isNodeVisible = (node) => {
let style = window.getComputedStyle(node);
if (node.offsetWidth === 0) {
return false;
}
return ((style.display !== 'none') && (style.visibility !== 'hidden'));
};
const timeUpdate = (time) => {
window.clearTimeout(timeUpdateTimeout);
this.lastTimeUpdate = Date.now();
timeUpdateTimeout = window.setTimeout(() => {
if (currentState !== H5P.Video.PLAYING) {
return;
}
const delta = (Date.now() - this.lastTimeUpdate) * this.getPlaybackRate();
this.setCurrentTime(currentTime + delta / 1000);
timeUpdate(currentTime);
}, 40); // 25 fps
}
/**
* Create a new player by embedding an iframe.
*
* @private
* @returns {Promise}
*/
const createEchoPlayer = async () => {
if (!isNodeVisible(placeholderElement) || player !== undefined) {
return;
}
// Since the SDK is loaded asynchronously below, explicitly set player to
// null (unlike undefined) which indicates that creation has begun. This
// allows the guard statement above to be hit if this function is called
// more than once.
player = null;
let queryString = '?';
queryString += `instanceId=${instanceId}&`;
if (options.controls) {
queryString += 'controls=true&';
}
if (options.disableFullscreen) {
queryString += 'disableFullscreen=true&';
}
if (options.deactivateSound) {
queryString += 'deactivateSound=true&';
}
if (options.startAt) {
queryString += `startTimeMillis=${Math.round(options.startAt * 1000)}&`;
}
wrapperElement.innerHTML = `
`;
player = wrapperElement.firstChild;
// Create a new player
registerEchoPlayerEventListeneners(player);
loadingFailedTimeout = setTimeout(() => {
failedLoading = true;
removeLoadingIndicator();
wrapperElement.innerHTML = `
${l10n.unknownError}
`;
wrapperElement.style.cssText = 'width: null; height: null;';
this.trigger('resize');
this.trigger('error', l10n.unknownError);
}, LOADING_TIMEOUT_IN_SECONDS * 1000);
};
/**
* Appends the video player to the DOM.
*
* @public
* @param {jQuery} $container
*/
this.appendTo = ($container) => {
$container.addClass('h5p-echo').append(wrapperElement);
createEchoPlayer();
};
/**
* Determine if the video has loaded.
*
* @public
* @returns {Boolean}
*/
this.isLoaded = () => {
return loadingComplete;
};
/**
* Get list of available qualities.
*
* @public
* @returns {Array}
*/
this.getQualities = () => {
return qualities;
};
/**
* Get the current quality.
*
* @public
* @returns {String} Current quality identifier
*/
this.getQuality = () => {
return currentQuality;
};
/**
* Set the playback quality.
*
* @public
* @param {String} quality
*/
this.setQuality = async (quality) => {
this.post('quality', quality);
currentQuality = quality;
this.trigger('qualityChange', currentQuality);
};
/**
* Start the video.
*
* @public
*/
this.play = () => {
if (!player) {
this.on('ready', this.play);
return;
}
this.post('play', 0);
};
/**
* Pause the video.
*
* @public
*/
this.pause = () => {
// Compensate for Echo360's delayed time updates
timelineUpdatesToSkip = 1;
this.post('pause', 0);
};
/**
* Seek video to given time.
*
* @public
* @param {Number} time
*/
this.seek = (time) => {
this.post('seek', time);
this.setCurrentTime(time);
// Compensate for Echo360's delayed time updates
timelineUpdatesToSkip = 1;
};
/**
* Post a window message to the iframe.
*
* @public
* @param event
* @param data
*/
this.post = (event, data) => {
player?.contentWindow?.postMessage(
JSON.stringify({
event: event,
context: 'Echo360',
instanceId: instanceId,
data: data
}),
'*'
);
};
/**
* Return the current play position.
*
* @public
* @returns {Number} Seconds elapsed since beginning of video
*/
this.getCurrentTime = () => {
return currentTime;
};
/**
* Set current time.
* @param {number} timeS Time in seconds.
*/
this.setCurrentTime = (timeS) => {
currentTime = timeS;
}
/**
* Return the video duration.
*
* @public
* @returns {?Number} Video duration in seconds
*/
this.getDuration = () => {
if (duration > 0) {
return duration;
}
return null;
};
/**
* Get percentage of video that is buffered.
*
* @public
* @returns {Number} Between 0 and 100
*/
this.getBuffered = () => {
return buffered;
};
/**
* Mute the video.
*
* @public
*/
this.mute = () => {
this.post('mute', 0);
isMuted = true;
};
/**
* Unmute the video.
*
* @public
*/
this.unMute = () => {
this.post('unmute', 0);
isMuted = false;
};
/**
* Whether the video is muted.
*
* @public
* @returns {Boolean} True if the video is muted, false otherwise
*/
this.isMuted = () => {
return isMuted;
};
/**
* Get the video player's current sound volume.
*
* @public
* @returns {Number} Between 0 and 100.
*/
this.getVolume = () => {
return volume;
};
/**
* Set the video player's sound volume.
*
* @public
* @param {Number} level
*/
this.setVolume = (level) => {
this.post('volume', level);
volume = level;
};
/**
* Get list of available playback rates.
*
* @public
* @returns {Array} Available playback rates
*/
this.getPlaybackRates = () => {
return [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2];
};
/**
* Get the current playback rate.
*
* @public
* @returns {Number} e.g. 0.5, 1, 1.5 or 2
*/
this.getPlaybackRate = () => {
return playbackRate;
};
/**
* Set the current playback rate.
*
* @public
* @param {Number} rate Must be one of available rates from getPlaybackRates
*/
this.setPlaybackRate = async (rate) => {
const echoRate = parseFloat(rate);
this.post('playbackrate', echoRate);
playbackRate = rate;
this.trigger('playbackRateChange', rate);
};
/**
* Set current captions track.
*
* @public
* @param {H5P.Video.LabelValue} track Captions to display
*/
this.setCaptionsTrack = (track) => {
const echoCaption = trackOptions.find(
(trackItem) => track?.value === trackItem.value
);
trackOptions.forEach(trackItem => {
trackItem.mode = (trackItem === echoCaption) ? 'showing' : 'disabled';
});
this.post('captions', echoCaption ? echoCaption.value : 'off');
};
/**
* Get current captions track.
*
* @public
* @returns {H5P.Video.LabelValue|null} Current captions track.
*/
this.getCaptionsTrack = () => {
return trackOptions.find(
(trackItem) => trackItem.mode === 'showing'
) ?? null;
};
this.on('resize', () => {
if (failedLoading || !isNodeVisible(wrapperElement)) {
return;
}
if (player === undefined) {
// Player isn't created yet. Try again.
createEchoPlayer();
return;
}
// Use as much space as possible
wrapperElement.style.cssText = 'width: 100%; height: 100%;';
const width = wrapperElement.clientWidth;
const height = options.fit ? wrapperElement.clientHeight : (width * (ratio));
// Validate height before setting
if (height > 0) {
// Set size
wrapperElement.style.cssText = 'width: ' + width + 'px; height: ' + height + 'px;';
}
});
}
/**
* Find id of video from given URL.
*
* @private
* @param {String} url
* @returns {String} Echo video identifier
*/
const getId = (url) => {
const matches = url.match(/^[^/]+:\/\/(echo360[^/]+)\/media\/([^/]+)\/h5p.*$/i);
if (matches && matches.length === 3) {
return [matches[2], matches[2]];
}
};
/**
* Check to see if we can play any of the given sources.
*
* @public
* @static
* @param {Array} sources
* @returns {Boolean}
*/
EchoPlayer.canPlay = (sources) => {
return getId(sources[0].path);
};
return EchoPlayer;
})(H5P.jQuery);
// Register video handler
H5P.videoHandlers = H5P.videoHandlers || [];
H5P.videoHandlers.push(H5P.VideoEchoVideo);
;
/** @namespace H5P */
H5P.VideoHtml5 = (function ($) {
/**
* HTML5 video player for H5P.
*
* @class
* @param {Array} sources Video files to use
* @param {Object} options Settings for the player
* @param {Object} l10n Localization strings
*/
function Html5(sources, options, l10n) {
var self = this;
/**
* Small helper to ensure all video sources get the same cache buster.
*
* @private
* @param {Object} source
* @return {string}
*/
const getCrossOriginPath = function (source) {
let path = H5P.getPath(source.path, self.contentId);
if (video.crossOrigin !== null && H5P.addQueryParameter && H5PIntegration.crossoriginCacheBuster) {
path = H5P.addQueryParameter(path, H5PIntegration.crossoriginCacheBuster);
}
return path
};
/**
* Register track to video
*
* @param {Object} trackData Track object
* @param {string} trackData.kind Kind of track
* @param {Object} trackData.track Source path
* @param {string} [trackData.label] Label of track
* @param {string} [trackData.srcLang] Language code
*/
const addTrack = function (trackData) {
// Skip invalid tracks
if (!trackData.kind || !trackData.track.path) {
return;
}
var track = document.createElement('track');
track.kind = trackData.kind;
track.src = getCrossOriginPath(trackData.track); // Uses same crossOrigin as parent. You cannot mix.
if (trackData.label) {
track.label = trackData.label;
}
if (trackData.srcLang) {
track.srcLang = trackData.srcLang;
}
return track;
};
/**
* Small helper to set the inital video source.
* Useful if some of the loading happens asynchronously.
* NOTE: Setting the crossOrigin must happen before any of the
* sources(poster, tracks etc.) are loaded
*
* @private
*/
const setInitialSource = function () {
if (qualities[currentQuality] === undefined) {
return;
}
if (H5P.setSource !== undefined) {
H5P.setSource(video, qualities[currentQuality].source, self.contentId)
}
else {
// Backwards compatibility (H5P < v1.22)
const srcPath = H5P.getPath(qualities[currentQuality].source.path, self.contentId);
if (H5P.getCrossOrigin !== undefined) {
var crossOrigin = H5P.getCrossOrigin(srcPath);
video.setAttribute('crossorigin', crossOrigin !== null ? crossOrigin : 'anonymous');
}
video.src = srcPath;
}
// Add poster if provided
if (options.poster) {
video.poster = getCrossOriginPath(options.poster); // Uses same crossOrigin as parent. You cannot mix.
}
// Register tracks
options.tracks.forEach(function (track, i) {
var trackElement = addTrack(track);
if (i === 0) {
trackElement.default = true;
}
if (trackElement) {
video.appendChild(trackElement);
}
});
};
/**
* Displayed when the video is buffering
* @private
*/
var $throbber = $('
', {
'class': 'h5p-video-loading'
});
/**
* Used to display error messages
* @private
*/
var $error = $('
', {
'class': 'h5p-video-error'
});
/**
* Keep track of current state when changing quality.
* @private
*/
var stateBeforeChangingQuality;
var currentTimeBeforeChangingQuality;
/**
* Avoids firing the same event twice.
* @private
*/
var lastState;
/**
* Keeps track whether or not the video has been loaded.
* @private
*/
var isLoaded = false;
/**
*
* @private
*/
var playbackRate = 1;
var skipRateChange = false;
// Create player
var video = document.createElement('video');
// Sort sources into qualities
var qualities = getQualities(sources, video);
var currentQuality;
numQualities = 0;
for (let quality in qualities) {
numQualities++;
}
if (numQualities > 1 && H5P.VideoHtml5.getExternalQuality !== undefined) {
H5P.VideoHtml5.getExternalQuality(sources, function (chosenQuality) {
if (qualities[chosenQuality] !== undefined) {
currentQuality = chosenQuality;
}
setInitialSource();
});
}
else {
// Select quality and source
currentQuality = getPreferredQuality();
if (currentQuality === undefined || qualities[currentQuality] === undefined) {
// No preferred quality, pick the first.
for (currentQuality in qualities) {
if (qualities.hasOwnProperty(currentQuality)) {
break;
}
}
}
setInitialSource();
}
// Setting webkit-playsinline, which makes iOS 10 beeing able to play video
// inside browser.
video.setAttribute('webkit-playsinline', '');
video.setAttribute('playsinline', '');
video.setAttribute('preload', 'metadata');
// Remove buttons in Chrome's video player:
let controlsList = 'nodownload';
if (options.disableFullscreen) {
controlsList += ' nofullscreen';
}
if (options.disableRemotePlayback) {
controlsList += ' noremoteplayback';
}
video.setAttribute('controlsList', controlsList);
// Remove picture in picture as it interfers with other video players
video.disablePictureInPicture = true;
// Set options
video.disableRemotePlayback = (options.disableRemotePlayback ? true : false);
video.controls = (options.controls ? true : false);
// Hardcoded autoplay to false to avoid playing videos on init
video.autoplay = false;
video.loop = (options.loop ? true : false);
video.className = 'h5p-video';
video.style.display = 'block';
if (options.fit) {
// Style is used since attributes with relative sizes aren't supported by IE9.
video.style.width = '100%';
video.style.height = '100%';
}
/**
* Helps registering events.
*
* @private
* @param {String} native Event name
* @param {String} h5p Event name
* @param {String} [arg] Optional argument
*/
var mapEvent = function (native, h5p, arg) {
video.addEventListener(native, function () {
switch (h5p) {
case 'loaded':
isLoaded = true;
if (stateBeforeChangingQuality !== undefined) {
return; // Avoid loaded event when changing quality.
}
// Remove any errors
if ($error.is(':visible')) {
$error.remove();
}
if (OLD_ANDROID_FIX) {
var andLoaded = function () {
video.removeEventListener('durationchange', andLoaded, false);
// On Android seeking isn't ready until after play.
self.trigger(h5p);
};
video.addEventListener('durationchange', andLoaded, false);
return;
}
if (video.poster) {
$(video).one('play', function () {
self.seek(self.getCurrentTime() || options.startAt);
});
}
else {
self.seek(options.startAt);
}
break;
case 'error':
// Handle error and get message.
arg = error(arguments[0], arguments[1]);
break;
case 'playbackRateChange':
// Fix for keeping playback rate in IE11
if (skipRateChange) {
skipRateChange = false;
return; // Avoid firing event when changing back
}
if (H5P.Video.IE11_PLAYBACK_RATE_FIX && playbackRate != video.playbackRate) { // Intentional
// Prevent change in playback rate not triggered by the user
video.playbackRate = playbackRate;
skipRateChange = true;
return;
}
// End IE11 fix
arg = self.getPlaybackRate();
break;
}
self.trigger(h5p, arg);
}, false);
};
/**
* Handle errors from the video player.
*
* @private
* @param {Object} code Error
* @param {String} [message]
* @returns {String} Human readable error message.
*/
var error = function (code, message) {
if (code instanceof Event) {
// No error code
if (!code.target.error) {
return '';
}
switch (code.target.error.code) {
case MediaError.MEDIA_ERR_ABORTED:
message = l10n.aborted;
break;
case MediaError.MEDIA_ERR_NETWORK:
message = l10n.networkFailure;
break;
case MediaError.MEDIA_ERR_DECODE:
message = l10n.cannotDecode;
break;
case MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED:
message = l10n.formatNotSupported;
break;
case MediaError.MEDIA_ERR_ENCRYPTED:
message = l10n.mediaEncrypted;
break;
}
}
if (!message) {
message = l10n.unknownError;
}
// Hide throbber
$throbber.remove();
// Display error message to user
$error.text(message).insertAfter(video);
// Pass message to our error event
return message;
};
/**
* Appends the video player to the DOM.
*
* @public
* @param {jQuery} $container
*/
self.appendTo = function ($container) {
$container.append(video);
};
/**
* Get list of available qualities. Not available until after play.
*
* @public
* @returns {Array}
*/
self.getQualities = function () {
// Create reverse list
var options = [];
for (var q in qualities) {
if (qualities.hasOwnProperty(q)) {
options.splice(0, 0, {
name: q,
label: qualities[q].label
});
}
}
if (options.length < 2) {
// Do not return if only one quality.
return;
}
return options;
};
/**
* Get current playback quality. Not available until after play.
*
* @public
* @returns {String}
*/
self.getQuality = function () {
return currentQuality;
};
/**
* Set current playback quality. Not available until after play.
* Listen to event "qualityChange" to check if successful.
*
* @public
* @params {String} [quality]
*/
self.setQuality = function (quality) {
if (qualities[quality] === undefined || quality === currentQuality) {
return; // Invalid quality
}
// Keep track of last choice
setPreferredQuality(quality);
// Avoid multiple loaded events if changing quality multiple times.
if (!stateBeforeChangingQuality) {
// Keep track of last state
stateBeforeChangingQuality = lastState;
// Keep track of current time
currentTimeBeforeChangingQuality = video.currentTime;
// Seek and start video again after loading.
var loaded = function () {
video.removeEventListener('loadedmetadata', loaded, false);
if (OLD_ANDROID_FIX) {
var andLoaded = function () {
video.removeEventListener('durationchange', andLoaded, false);
// On Android seeking isn't ready until after play.
self.seek(currentTimeBeforeChangingQuality);
};
video.addEventListener('durationchange', andLoaded, false);
}
else {
// Seek to current time.
self.seek(currentTimeBeforeChangingQuality);
}
// Always play to get image.
video.play();
if (stateBeforeChangingQuality !== H5P.Video.PLAYING) {
// Do not resume playing
video.pause();
}
// Done changing quality
stateBeforeChangingQuality = undefined;
// Remove any errors
if ($error.is(':visible')) {
$error.remove();
}
};
video.addEventListener('loadedmetadata', loaded, false);
}
// Keep track of current quality
currentQuality = quality;
self.trigger('qualityChange', currentQuality);
// Display throbber
self.trigger('stateChange', H5P.Video.BUFFERING);
// Change source
video.src = getCrossOriginPath(qualities[quality].source); // (iPad does not support #t=).
// Note: Optional tracks use same crossOrigin as the original. You cannot mix.
// Remove poster so it will not show during quality change
video.removeAttribute('poster');
};
/**
* Starts the video.
*
* @public
* @return {Promise|undefined} May return a Promise that resolves when
* play has been processed.
*/
self.play = function () {
if ($error.is(':visible')) {
return;
}
if (!isLoaded) {
// Make sure video is loaded before playing
video.load();
video.addEventListener('loadeddata', function() {
video.play();
}, false);
}
else {
return video.play();
}
};
/**
* Pauses the video.
*
* @public
*/
self.pause = function () {
video.pause();
};
/**
* Seek video to given time.
*
* @public
* @param {Number} time
*/
self.seek = function (time) {
video.currentTime = time;
// Use canplaythrough for IOs devices
// Use loadedmetadata for all other devices.
const eventName = navigator.userAgent.match(/iPad|iPod|iPhone/i) ? "canplaythrough" : "loadedmetadata";
function seekTo() {
video.currentTime = time;
video.removeEventListener(eventName, seekTo);
};
if (video.readyState === 4) {
seekTo();
}
else {
video.addEventListener(eventName, seekTo);
}
};
/**
* Get elapsed time since video beginning.
*
* @public
* @returns {Number}
*/
self.getCurrentTime = function () {
return video.currentTime;
};
/**
* Get total video duration time.
*
* @public
* @returns {Number}
*/
self.getDuration = function () {
if (isNaN(video.duration)) {
return;
}
return video.duration;
};
/**
* Get percentage of video that is buffered.
*
* @public
* @returns {Number} Between 0 and 100
*/
self.getBuffered = function () {
// Find buffer currently playing from
var buffered = 0;
for (var i = 0; i < video.buffered.length; i++) {
var from = video.buffered.start(i);
var to = video.buffered.end(i);
if (video.currentTime > from && video.currentTime < to) {
buffered = to;
break;
}
}
// To percentage
return buffered ? (buffered / video.duration) * 100 : 0;
};
/**
* Turn off video sound.
*
* @public
*/
self.mute = function () {
video.muted = true;
};
/**
* Turn on video sound.
*
* @public
*/
self.unMute = function () {
video.muted = false;
};
/**
* Check if video sound is turned on or off.
*
* @public
* @returns {Boolean}
*/
self.isMuted = function () {
return video.muted;
};
/**
* Returns the video sound level.
*
* @public
* @returns {Number} Between 0 and 100.
*/
self.getVolume = function () {
return video.volume * 100;
};
/**
* Set video sound level.
*
* @public
* @param {Number} level Between 0 and 100.
*/
self.setVolume = function (level) {
video.volume = level / 100;
};
/**
* Get list of available playback rates.
*
* @public
* @returns {Array} available playback rates
*/
self.getPlaybackRates = function () {
/*
* not sure if there's a common rule about determining good speeds
* using Google's standard options via a constant for setting
*/
var playbackRates = PLAYBACK_RATES;
return playbackRates;
};
/**
* Get current playback rate.
*
* @public
* @returns {Number} such as 0.25, 0.5, 1, 1.25, 1.5 and 2
*/
self.getPlaybackRate = function () {
return video.playbackRate;
};
/**
* Set current playback rate.
* Listen to event "playbackRateChange" to check if successful.
*
* @public
* @params {Number} suggested rate that may be rounded to supported values
*/
self.setPlaybackRate = function (newPlaybackRate) {
playbackRate = newPlaybackRate;
video.playbackRate = newPlaybackRate;
};
/**
* Set current captions track.
*
* @param {H5P.Video.LabelValue} Captions track to show during playback
*/
self.setCaptionsTrack = function (track) {
for (var i = 0; i < video.textTracks.length; i++) {
video.textTracks[i].mode = (track && track.value === i ? 'showing' : 'disabled');
}
};
/**
* Figure out which captions track is currently used.
*
* @return {H5P.Video.LabelValue} Captions track
*/
self.getCaptionsTrack = function () {
for (var i = 0; i < video.textTracks.length; i++) {
if (video.textTracks[i].mode === 'showing') {
return new H5P.Video.LabelValue(video.textTracks[i].label, i);
}
}
return null;
};
// Register event listeners
mapEvent('ended', 'stateChange', H5P.Video.ENDED);
mapEvent('playing', 'stateChange', H5P.Video.PLAYING);
mapEvent('pause', 'stateChange', H5P.Video.PAUSED);
mapEvent('waiting', 'stateChange', H5P.Video.BUFFERING);
mapEvent('loadedmetadata', 'loaded');
mapEvent('canplay', 'canplay');
mapEvent('error', 'error');
mapEvent('ratechange', 'playbackRateChange');
if (!video.controls) {
// Disable context menu(right click) to prevent controls.
video.addEventListener('contextmenu', function (event) {
event.preventDefault();
}, false);
}
// Display throbber when buffering/loading video.
self.on('stateChange', function (event) {
var state = event.data;
lastState = state;
if (state === H5P.Video.BUFFERING) {
$throbber.insertAfter(video);
}
else {
$throbber.remove();
}
});
// Load captions after the video is loaded
self.on('loaded', function () {
nextTick(function () {
var textTracks = [];
for (var i = 0; i < video.textTracks.length; i++) {
textTracks.push(new H5P.Video.LabelValue(video.textTracks[i].label, i));
}
if (textTracks.length) {
self.trigger('captions', textTracks);
}
});
});
// Alternative to 'canplay' event
/*self.on('resize', function () {
if (video.offsetParent === null) {
return;
}
video.style.width = '100%';
video.style.height = '100%';
var width = video.clientWidth;
var height = options.fit ? video.clientHeight : (width * (video.videoHeight / video.videoWidth));
video.style.width = width + 'px';
video.style.height = height + 'px';
});*/
// Video controls are ready
nextTick(function () {
self.trigger('ready');
});
/**
* Check video is loaded and ready play.
*
* @public
* @param {Boolean}
*/
self.isLoaded = function () {
return isLoaded;
};
}
/**
* Check to see if we can play any of the given sources.
*
* @public
* @static
* @param {Array} sources
* @returns {Boolean}
*/
Html5.canPlay = function (sources) {
var video = document.createElement('video');
if (video.canPlayType === undefined) {
return false; // Not supported
}
// Cycle through sources
for (var i = 0; i < sources.length; i++) {
var type = getType(sources[i]);
if (type && video.canPlayType(type) !== '') {
// We should be able to play this
return true;
}
}
return false;
};
/**
* Find source type.
*
* @private
* @param {Object} source
* @returns {String}
*/
var getType = function (source) {
var type = source.mime;
if (!type) {
// Try to get type from URL
var matches = source.path.match(/\.(\w+)$/);
if (matches && matches[1]) {
type = 'video/' + matches[1];
}
}
if (type && source.codecs) {
// Add codecs
type += '; codecs="' + source.codecs + '"';
}
return type;
};
/**
* Sort sources into qualities.
*
* @private
* @static
* @param {Array} sources
* @param {Object} video
* @returns {Object} Quality mapping
*/
var getQualities = function (sources, video) {
var qualities = {};
var qualityIndex = 1;
var lastQuality;
// Cycle through sources
for (var i = 0; i < sources.length; i++) {
var source = sources[i];
// Find and update type.
var type = source.type = getType(source);
// Check if we support this type
var isPlayable = type && (type === 'video/unknown' || video.canPlayType(type) !== '');
if (!isPlayable) {
continue; // We cannot play this source
}
if (source.quality === undefined) {
/**
* No quality metadata. Create a quality tag to separate multiple sources of the same type,
* e.g. if two mp4 files with different quality has been uploaded
*/
if (lastQuality === undefined || qualities[lastQuality].source.type === type) {
// Create a new quality tag
source.quality = {
name: 'q' + qualityIndex,
label: (source.metadata && source.metadata.qualityName) ? source.metadata.qualityName : 'Quality ' + qualityIndex // TODO: l10n
};
qualityIndex++;
}
else {
/**
* Assumes quality already exists in a different format.
* Uses existing label for this quality.
*/
source.quality = qualities[lastQuality].source.quality;
}
}
// Log last quality
lastQuality = source.quality.name;
// Look to see if quality exists
var quality = qualities[lastQuality];
if (quality) {
// We have a source with this quality. Check if we have a better format.
if (source.mime.split('/')[1] === PREFERRED_FORMAT) {
quality.source = source;
}
}
else {
// Add new source with quality.
qualities[source.quality.name] = {
label: source.quality.label,
source: source
};
}
}
return qualities;
};
/**
* Set preferred video quality.
*
* @private
* @static
* @param {String} quality Index of preferred quality
*/
var setPreferredQuality = function (quality) {
try {
localStorage.setItem('h5pVideoQuality', quality);
}
catch (err) {
console.warn('Unable to set preferred video quality, localStorage is not available.');
}
};
/**
* Get preferred video quality.
*
* @private
* @static
* @returns {String} Index of preferred quality
*/
var getPreferredQuality = function () {
// First check localStorage
let quality;
try {
quality = localStorage.getItem('h5pVideoQuality');
}
catch (err) {
console.warn('Unable to retrieve preferred video quality from localStorage.');
}
if (!quality) {
try {
// The fallback to old cookie solution
var settings = document.cookie.split(';');
for (var i = 0; i < settings.length; i++) {
var setting = settings[i].split('=');
if (setting[0] === 'H5PVideoQuality') {
quality = setting[1];
break;
}
}
}
catch (err) {
console.warn('Unable to retrieve preferred video quality from cookie.');
}
}
return quality;
};
/**
* Helps schedule a task for the next tick.
* @param {function} task
*/
var nextTick = function (task) {
setTimeout(task, 0);
};
/** @constant {Boolean} */
var OLD_ANDROID_FIX = false;
/** @constant {Boolean} */
var PREFERRED_FORMAT = 'mp4';
/** @constant {Object} */
var PLAYBACK_RATES = [0.25, 0.5, 1, 1.25, 1.5, 2];
if (navigator.userAgent.indexOf('Android') !== -1) {
// We have Android, check version.
var version = navigator.userAgent.match(/AppleWebKit\/(\d+\.?\d*)/);
if (version && version[1] && Number(version[1]) <= 534.30) {
// Include fix for devices running the native Android browser.
// (We don't know when video was fixed, so the number is just the lastest
// native android browser we found.)
OLD_ANDROID_FIX = true;
}
}
else {
if (navigator.userAgent.indexOf('Chrome') !== -1) {
// If we're using chrome on a device that isn't Android, prefer the webm
// format. This is because Chrome has trouble with some mp4 codecs.
PREFERRED_FORMAT = 'webm';
}
}
return Html5;
})(H5P.jQuery);
// Register video handler
H5P.videoHandlers = H5P.videoHandlers || [];
H5P.videoHandlers.push(H5P.VideoHtml5);
;
/** @namespace H5P */
H5P.Video = (function ($, ContentCopyrights, MediaCopyright, handlers) {
/**
* The ultimate H5P video player!
*
* @class
* @param {Object} parameters Options for this library.
* @param {Object} parameters.visuals Visual options
* @param {Object} parameters.playback Playback options
* @param {Object} parameters.a11y Accessibility options
* @param {Boolean} [parameters.startAt] Start time of video
* @param {Number} id Content identifier
* @param {Object} [extras] Extra parameters.
*/
function Video(parameters, id, extras = {}) {
var self = this;
self.oldTime = extras.previousState?.time;
self.contentId = id;
self.WAS_RESET = false;
self.startAt = parameters.startAt || 0;
// Ref youtube.js - ipad & youtube - issue
self.pressToPlay = false;
// Reference to the handler
var handlerName = '';
// Initialize event inheritance
H5P.EventDispatcher.call(self);
// Default language localization
parameters = $.extend(true, parameters, {
l10n: {
name: 'Video',
loading: 'Video player loading...',
noPlayers: 'Found no video players that supports the given video format.',
noSources: 'Video source is missing.',
aborted: 'Media playback has been aborted.',
networkFailure: 'Network failure.',
cannotDecode: 'Unable to decode media.',
formatNotSupported: 'Video format not supported.',
mediaEncrypted: 'Media encrypted.',
unknownError: 'Unknown error.',
vimeoPasswordError: 'Password-protected Vimeo videos are not supported.',
vimeoPrivacyError: 'The Vimeo video cannot be used due to its privacy settings.',
vimeoLoadingError: 'The Vimeo video could not be loaded.',
invalidYtId: 'Invalid YouTube ID.',
unknownYtId: 'Unable to find video with the given YouTube ID.',
restrictedYt: 'The owner of this video does not allow it to be embedded.'
}
});
parameters.a11y = parameters.a11y || [];
parameters.playback = parameters.playback || {};
parameters.visuals = $.extend(
true, { disableFullscreen: false }, parameters.visuals
);
/** @private */
var sources = [];
if (parameters.sources) {
for (var i = 0; i < parameters.sources.length; i++) {
// Clone to avoid changing of parameters.
var source = $.extend(true, {}, parameters.sources[i]);
// Create working URL without html entities.
source.path = $cleaner.html(source.path).text();
sources.push(source);
}
}
/** @private */
var tracks = [];
parameters.a11y.forEach(function (track) {
// Clone to avoid changing of parameters.
var clone = $.extend(true, {}, track);
// Create working URL without html entities
if (clone.track && clone.track.path) {
clone.track.path = $cleaner.html(clone.track.path).text();
tracks.push(clone);
}
});
/**
* Handle autoplay. If autoplay is disabled, it will still autopause when
* video is not visible.
*
* @param {*} $container
*/
const handleAutoPlayPause = function ($container) {
// Keep the current state
let state;
self.on('stateChange', function(event) {
state = event.data;
});
// Keep record of autopauses.
// I.e: we don't wanna autoplay if the user has excplicitly paused.
self.autoPaused = !self.pressToPlay;
new IntersectionObserver(function (entries) {
const entry = entries[0];
// This video element became visible
if (entry.isIntersecting) {
// Autoplay if autoplay is enabled and it was not explicitly
// paused by a user
if (parameters.playback.autoplay && self.autoPaused) {
self.autoPaused = false;
self.play();
}
}
else if (state !== Video.PAUSED && state !== Video.ENDED) {
self.autoPaused = true;
self.pause();
}
}, {
root: null,
threshold: [0, 1] // Get events when it is shown and hidden
}).observe($container.get(0));
};
/**
* Attaches the video handler to the given container.
* Inserts text if no handler is found.
*
* @public
* @param {jQuery} $container
*/
self.attach = function ($container) {
$container.addClass('h5p-video').html('');
if (self.appendTo !== undefined) {
self.appendTo($container);
// Avoid autoplaying in authoring tool
if (window.H5PEditor === undefined) {
handleAutoPlayPause($container);
}
}
else if (sources.length) {
$container.text(parameters.l10n.noPlayers);
}
else {
$container.text(parameters.l10n.noSources);
}
};
/**
* Get name of the video handler
*
* @public
* @returns {string}
*/
self.getHandlerName = function() {
return handlerName;
};
/**
* @public
* Get current state for resume support.
*
* @returns {object} Current state.
*/
self.getCurrentState = function () {
if (self.getCurrentTime) {
return {
time: self.getCurrentTime() || self.oldTime,
};
}
};
/**
* The two functions below needs to be defined in this base class,
* since it is used in this class even if no handler was found.
*/
self.seek = () => {};
self.pause = () => {};
/**
* @public
* Reset current state (time).
*
*/
self.resetTask = function () {
delete self.oldTime;
self.resetPlayback(parameters.startAt || 0);
};
/**
* Default implementation of resetPlayback. May be overridden by sub classes.
*
* @param {*} startAt
*/
self.resetPlayback = startAt => {
self.seek(startAt);
self.pause();
self.WAS_RESET = true;
};
// Resize the video when we know its aspect ratio
self.on('loaded', function () {
self.trigger('resize');
// reset time if wasn't done immediately
if (self.WAS_RESET) {
self.seek(parameters.startAt || 0);
if (!parameters.playback.autoplay) {
self.pause();
}
self.WAS_RESET = false;
}
});
// Find player for video sources
if (sources.length) {
const options = {
controls: parameters.visuals.controls,
autoplay: parameters.playback.autoplay,
loop: parameters.playback.loop,
fit: parameters.visuals.fit,
poster: parameters.visuals.poster === undefined ? undefined : parameters.visuals.poster,
tracks: tracks,
disableRemotePlayback: parameters.visuals.disableRemotePlayback === true,
disableFullscreen: parameters.visuals.disableFullscreen === true,
deactivateSound: parameters.playback.deactivateSound,
}
if (!self.WAS_RESET) {
options.startAt = self.oldTime !== undefined ? self.oldTime : (parameters.startAt || 0);
}
var html5Handler;
for (var i = 0; i < handlers.length; i++) {
var handler = handlers[i];
if (handler.canPlay !== undefined && handler.canPlay(sources)) {
handler.call(self, sources, options, parameters.l10n);
handlerName = handler.name;
return;
}
if (handler === H5P.VideoHtml5) {
html5Handler = handler;
handlerName = handler.name;
}
}
// Fallback to trying HTML5 player
if (html5Handler) {
html5Handler.call(self, sources, options, parameters.l10n);
}
}
}
// Extends the event dispatcher
Video.prototype = Object.create(H5P.EventDispatcher.prototype);
Video.prototype.constructor = Video;
// Player states
/** @constant {Number} */
Video.ENDED = 0;
/** @constant {Number} */
Video.PLAYING = 1;
/** @constant {Number} */
Video.PAUSED = 2;
/** @constant {Number} */
Video.BUFFERING = 3;
/**
* When video is queued to start
* @constant {Number}
*/
Video.VIDEO_CUED = 5;
// Used to convert between html and text, since URLs have html entities.
var $cleaner = H5P.jQuery('
');
/**
* Help keep track of key value pairs used by the UI.
*
* @class
* @param {string} label
* @param {string} value
*/
Video.LabelValue = function (label, value) {
this.label = label;
this.value = value;
};
/**
* Determine whether video can be autoplayed.
* @returns {Promise
} Whether autoplay is allowed.
*/
Video.isAutoplayAllowed = async () => {
if (document.featurePolicy?.allowsFeature('autoplay')) {
return true; // Browser supports `featurePolicy` and can tell directly
}
const video = document.createElement('video');
/*
* Without a video source, the play Promise will be rejected with an error
* if it cannot be autoplayed, but not resolve at all if it can be
* autoplayed. Using a timeout to detect the latter case here.
*/
const timeoutMs = 50; // If play promise rejects, then within few ms
const timeoutPromise = new Promise((resolve) => {
window.setTimeout(() => {
resolve(true); // Timeout reached, autoplay is allowed
}, timeoutMs);
});
let result;
try {
result = (await Promise.race([video.play(), timeoutPromise])) ?? true;
} catch (error) {
result = false;
}
return result;
};
/** @constant {Boolean} */
Video.IE11_PLAYBACK_RATE_FIX = (navigator.userAgent.match(/Trident.*rv[ :]*11\./) ? true : false);
return Video;
})(H5P.jQuery, H5P.ContentCopyrights, H5P.MediaCopyright, H5P.videoHandlers || []);
;
(()=>{"use strict";const t=function(t){const e=t.length;return function o(){const n=Array.prototype.slice.call(arguments,0);return n.length>=e?t.apply(null,n):function(){const t=Array.prototype.slice.call(arguments,0);return o.apply(null,n.concat(t))}}},e=t((function(t,e){e.forEach(t)})),o=(t((function(t,e){return e.map(t)})),t((function(t,e){return e.filter(t)}))),n=(t((function(t,e){return e.some(t)})),t((function(t,e){return-1!=e.indexOf(t)}))),i=t((function(t,e){return o((e=>!n(e,t)),e)})),s=t(((t,e)=>e.getAttribute(t))),r=t(((t,e,o)=>o.setAttribute(t,e))),a=t(((t,e)=>e.removeAttribute(t))),c=t(((t,e)=>e.hasAttribute(t))),l=(t(((t,e,o)=>o.getAttribute(t)===e)),t(((t,e)=>{const o=s(t,e);r(t,("true"!==o).toString(),e)})),t(((t,e)=>t.appendChild(e))),t(((t,e)=>e.querySelector(t))),t(((t,e)=>{return o=e.querySelectorAll(t),Array.prototype.slice.call(o);var o})),t(((t,e)=>t.removeChild(e))),t(((t,e)=>e.classList.contains(t))),t(((t,e)=>e.classList.add(t)))),d=t(((t,e)=>e.classList.remove(t))),u=l("hidden"),h=d("hidden"),p=(t(((t,e)=>(t?h:u)(e))),t(((t,e,o)=>{o.classList[e?"add":"remove"](t)})),a("tabindex")),m=(e(p),r("tabindex","0")),v=r("tabindex","-1"),f=c("tabindex");class b{constructor(t){Object.assign(this,{listeners:{},on:function(t,e,o){const n={listener:e,scope:o};return this.listeners[t]=this.listeners[t]||[],this.listeners[t].push(n),this},fire:function(t,e){return(this.listeners[t]||[]).every((function(t){return!1!==t.listener.call(t.scope||this,e)}))},propagate:function(t,e){let o=this;t.forEach((t=>e.on(t,(e=>o.fire(t,e)))))}}),this.plugins=t||[],this.elements=[],this.negativeTabIndexAllowed=!1,this.on("nextElement",this.nextElement,this),this.on("previousElement",this.previousElement,this),this.on("firstElement",this.firstElement,this),this.on("lastElement",this.lastElement,this),this.initPlugins()}addElement(t){this.elements.push(t),this.firesEvent("addElement",t),1===this.elements.length&&this.setTabbable(t)}insertElementAt(t,e){this.elements.splice(e,0,t),this.firesEvent("addElement",t),1===this.elements.length&&this.setTabbable(t)}removeElement(t){this.elements=i([t],this.elements),f(t)&&(this.setUntabbable(t),this.elements[0]&&this.setTabbable(this.elements[0])),this.firesEvent("removeElement",t)}count(){return this.elements.length}firesEvent(t,e){const o=this.elements.indexOf(e);return this.fire(t,{element:e,index:o,elements:this.elements,oldElement:this.tabbableElement})}nextElement({index:t}){const e=t===this.elements.length-1,o=this.elements[e?0:t+1];this.setTabbable(o),o.focus()}firstElement(){const t=this.elements[0];this.setTabbable(t),t.focus()}lastElement(){const t=this.elements[this.elements.length-1];this.setTabbable(t),t.focus()}setTabbableByIndex(t){const e=this.elements[t];e&&this.setTabbable(e)}setTabbable(t){e(this.setUntabbable.bind(this),this.elements),m(t),this.tabbableElement=t}setUntabbable(t){t!==document.activeElement&&(this.negativeTabIndexAllowed?v(t):p(t))}previousElement({index:t}){const e=0===t,o=this.elements[e?this.elements.length-1:t-1];this.setTabbable(o),o.focus()}useNegativeTabIndex(){this.negativeTabIndexAllowed=!0,this.elements.forEach((t=>{t.hasAttribute("tabindex")||v(t)}))}initPlugins(){this.plugins.forEach((function(t){void 0!==t.init&&t.init(this)}),this)}}class g{constructor(){this.selectability=!0}init(t){this.boundHandleKeyDown=this.handleKeyDown.bind(this),this.controls=t,this.controls.on("addElement",this.listenForKeyDown,this),this.controls.on("removeElement",this.removeKeyDownListener,this)}listenForKeyDown({element:t}){t.addEventListener("keydown",this.boundHandleKeyDown)}removeKeyDownListener({element:t}){t.removeEventListener("keydown",this.boundHandleKeyDown)}handleKeyDown(t){switch(t.which){case 27:this.close(t.target),t.preventDefault(),t.stopPropagation();break;case 35:this.lastElement(t.target),t.preventDefault(),t.stopPropagation();break;case 36:this.firstElement(t.target),t.preventDefault(),t.stopPropagation();break;case 13:case 32:this.select(t.target),t.preventDefault(),t.stopPropagation();break;case 37:case 38:this.hasChromevoxModifiers(t)||(this.previousElement(t.target),t.preventDefault(),t.stopPropagation());break;case 39:case 40:this.hasChromevoxModifiers(t)||(this.nextElement(t.target),t.preventDefault(),t.stopPropagation())}}hasChromevoxModifiers(t){return t.shiftKey||t.ctrlKey}previousElement(t){!1!==this.controls.firesEvent("beforePreviousElement",t)&&(this.controls.firesEvent("previousElement",t),this.controls.firesEvent("afterPreviousElement",t))}nextElement(t){!1!==this.controls.firesEvent("beforeNextElement",t)&&(this.controls.firesEvent("nextElement",t),this.controls.firesEvent("afterNextElement",t))}select(t){this.selectability&&!1!==this.controls.firesEvent("before-select",t)&&(this.controls.firesEvent("select",t),this.controls.firesEvent("after-select",t))}firstElement(t){!1!==this.controls.firesEvent("beforeFirstElement",t)&&(this.controls.firesEvent("firstElement",t),this.controls.firesEvent("afterFirstElement",t))}lastElement(t){!1!==this.controls.firesEvent("beforeLastElement",t)&&(this.controls.firesEvent("lastElement",t),this.controls.firesEvent("afterLastElement",t))}disableSelectability(){this.selectability=!1}enableSelectability(){this.selectability=!0}close(t){!1!==this.controls.firesEvent("before-close",t)&&(this.controls.firesEvent("close",t),this.controls.firesEvent("after-close",t))}}var y=0,k=1,S=function(t,e,o,n,i,s){var r=this,a="interactive-video-".concat(s,"-menu-").concat(t),c=new b([new g]);c.on("close",(function(){return u()}));var l,d=[];H5P.EventDispatcher.call(r);var u=function(){r.control.setAttribute("aria-expanded","false"),r.control.focus(),r.popup.classList.remove("h5p-show"),r.trigger("close")},h=function(){"true"===r.control.getAttribute("aria-expanded")?u():(r.control.setAttribute("aria-expanded","true"),r.popup.classList.add("h5p-show"),r.trigger("open"),r.popup.querySelector('li[tabindex="0"]').focus())},p=function(t){var e=t.value===o.value,i=w(null,k,t.label,(function(){!function(t,e){o=e,function(t,e,o){for(var n=0;n').concat(i[t],"")),r.popup.setAttribute("role","dialog");var m=w("h5p-chooser-close-button",y,i.close,h,"div","button");r.popup.appendChild(m),r.control=w("h5p-control h5p-"+t,y,i[t],h,"div","button"),r.control.setAttribute("aria-haspopup","true"),r.overlayControl=w("h5p-minimal-button h5p-"+t,k,i[t],h,"div","menuitem"),r.overlayControl.tabIndex="-1",r.updateOptions(e)};(S.prototype=Object.create(H5P.EventDispatcher.prototype)).constructor=S;var $=function(t,e,o){var n=document.createElement(o||"div");return t&&(n.className=t),e&&(n.innerHTML=e),n},w=function(t,e,o,n,i,s){var r=$(t,e===k?o:"",i);return r.tabIndex=0,r.setAttribute("role",s),e===y&&(r.title=o),r.addEventListener("click",(function(t){n.call(r,t)}),!1),r.addEventListener("keydown",(function(t){32!==t.which&&13!==t.which||(t.preventDefault(),n.call(r,t))}),!1),r};const T=S,C={ENTER:13,SPACE:32,ARROW_LEFT:37,ARROW_UP:38,ARROW_RIGHT:39,ARROW_DOWN:40,A:65,B:66,C:67,D:68,E:69,F:70,G:71,H:72,I:73,J:74,K:75,L:76,M:77,N:78,O:79,P:80,Q:81,R:82,S:83,T:84,U:85,V:86,W:87,X:88,Y:89,Z:90},x=(t,e,o={ctrl:!1,shift:!1})=>-1!==e.indexOf(t.which)&&(!(o.ctrl&&!t.ctrlKey)&&!(o.shift&&!t.shiftKey)),P=t=>-1!==[C.ENTER,C.SPACE].indexOf(t.which),I=(t,e,o)=>{t.on("keydown",(t=>{for(let n=0;n0},A=function(t){return M(t)?B("".concat(t,"
")).text():void 0},H="timecode",D=function(t,e){return void 0!==t.goto&&t.goto.type===e},R=["H5P.Image","H5P.Nil","H5P.Table","H5P.Link","H5P.GoToQuestion","H5P.IVHotspot","H5P.Text"],V=["H5P.Text","H5P.Table"],O=function(t){return-1!==V.indexOf(t)};function q(t,e,o){var n,i,s,r,a=this;H5P.EventDispatcher.call(a);var c=t.action;o&&(c.userDatas={state:o});var l,d,u,h,p=c.library.split(" ")[0],m=e.l10n[function(t,e){return-1!==R.indexOf(t)||D(e,H)}(p,t)?"content":"interaction"],v="H5P.Nil"!==p&&"button"===t.displayType,f=[c.params.contentName,v?A(t.label):"","H5P.Link"===p&&void 0!==c.params.title?c.params.title:t.libraryTitle].filter(M)[0],b=!1,g=!1,y=!1,k=t.action.metadata;this.on("open-dialog",(function(){P()})),this.on("show-mask",(function(){L(this.getElement())}));var S=function(o){n=B("
",{tabindex:0,role:"button",class:"h5p-interaction "+l+(o?"":" h5p-hidden"),"aria-haspopup":"dialog","aria-expanded":"false","aria-label":f,css:{left:t.x+"%",top:t.y+"%",width:"",height:""},on:{click:function(){a.dialogDisabled||(P(),n.attr("aria-expanded","true"))},keydown:function(t){13!==t.which&&32!==t.which||a.dialogDisabled||(P(),n.attr("aria-expanded","true"),t.preventDefault())}}}),a.getRequiresCompletion()&&void 0===e.editor&&e.currentState!==H5P.InteractiveVideo.SEEKING&&P(!0),B("
",{class:"h5p-touch-area"}).appendTo(n),B("
",{class:"h5p-interaction-button"}).appendTo(n),e.editor&&n.hover((function(){n.is(".focused")||n.is(":focus")||e.dnb&&(!e.dnb||e.dnb.newElement)?(e.editor.hideInteractionTitle(),b=!1):(e.editor.showInteractionTitle(f,n),b=!0)}),(function(){e.editor.hideInteractionTitle(),b=!1})).focus((function(){e.editor.hideInteractionTitle(),b=!1})).click((function(){e.editor.hideInteractionTitle()}));var s=M(A(t.label));t.label&&s&&(i=$(t.label,"h5p-interaction").appendTo(n)),a.trigger("display",n),setTimeout((function(){n&&n.removeClass("h5p-hidden")}),0)},$=function(t){return B("
",{class:"h5p-interaction-label ".concat(arguments.length>1&&void 0!==arguments[1]?arguments[1]:""),html:''.concat(t,"
")})},w=function(){return(n=$(t.label,"h5p-interaction h5p-interaction-label-standalone")).css({left:"".concat(t.x,"%"),top:"".concat(t.y,"%"),width:"",height:"initial"}),a.trigger("display",n),setTimeout((function(){n&&n.removeClass("h5p-hidden")}),0),n},T=function(e){if("timecode"===t.goto.type)e.click((function(e){1===e.which&&V({data:t.goto.time})})).keypress((function(e){32===e.which&&V({data:t.goto.time})})).attr("href","#").attr("tabindex","0");else{var o=t.goto.url;e.keypress((function(t){32===t.which&&this.click()})).attr({href:("other"!==o.protocol?o.protocol:"")+o.url,target:"_blank"})}return e.addClass("goto-clickable")},C=function(t){var o=!e.hasUncompletedRequiredInteractions(t);d&&d.trigger("hide"),n&&a.trigger("hide",n),a.isButton()?o&&e.dnb.dialog.close():(e.isMobileView&&o&&e.dnb.dialog.close(),n&&n.detach()),a.trigger("remove",n),o&&W(n)},x=function(t){if("H5P.Questionnaire"===p){if(t.find(".h5p-interaction-continue-button").length)return;var o=function(){var t=document.createElement("button");return t.innerHTML=e.l10n.continueWithVideo,t.className="h5p-interaction-continue-button",t.addEventListener("click",(function(){C(),e.play()})),t}(),n=t.find(".h5p-questionnaire-success-center");n.length&&n.get(0).appendChild(o),d.on("noSuccessScreen",(function(){C(),e.play()}))}"H5P.FreeTextQuestion"===p&&d.on("continue",(function(){C(),e.play()}))},P=function(o){var i=e.$container.find(".h5p-dialog-wrapper"),s=i.find(".h5p-dialog-titlebar");"function"==typeof d.setActivityStarted&&"function"==typeof d.getScore&&d.setActivityStarted();var r=a.isGotoClickable(),l=i.find("[tabindex]"),u=B(r?"":"",{class:"h5p-dialog-interaction h5p-frame"});!r&&O(p)&&u.attr("tabindex","0"),void 0!==e.editor?u.attr("tabindex",-1):0===l.length&&u.attr("tabindex",0),a.getRequiresCompletion()&&i.keydown((function(t){!function(t,e){var o=function(t){return e.target===t.get(0)},n=9===e.which,i=t.first(),s=t.last();n&&e.shiftKey&&o(i)?(s.focus(),e.preventDefault()):n&&o(s)&&(i.focus(),e.preventDefault())}(i.find('[tabindex="0"], button, input').filter(":visible"),t)}));var h=r?T(u):u;if(d.attach(h),x(h),K(d)&&(a.score=d.getScore(),a.maxScore=d.getMaxScore()),a.hasFullScore()&&o){if(i.hasClass("h5p-hidden"))return}else a.getRequiresCompletion()&&!a.hasFullScore()&&(e.dnb.dialog.hideCloseButton(),e.dnb.dialog.disableOverlay=!0,i.click((function(){a.hasFullScore()||e.showWarningMask().find(".h5p-button-back").click((function(){return u.find("button").last().focus()}))})));e.dnb.dialog.open(u),e.disableTabIndexes(),e.dnb.dialog.addLibraryClass(p),e.dnb.dialog.toggleClass("goto-clickable-visualize",!(!r||!t.goto.visualize)),e.dnb.dialog.toggleClass("h5p-goto-timecode",D(t,H)),e.dnb.dialog.disableOverlay&&e.restorePosterTabIndexes();e.dnb.dialog.once("close",(function(){e.dnb.$dialogContainer.one("transitionend",(function(){u.is(".h5p-image")&&u.find("img").css({width:"",height:""})}));try{void 0!==d.pause&&(d.pause instanceof Function||"function"==typeof d.pause)&&d.pause()}catch(t){H5P.error(t)}n&&(e.dnb.calledFromResetTask||n.focus(),n.attr("aria-expanded","false")),d&&d.trigger("hide");var t=i.find(".h5p-dialog-titlebar").find(".h5p-sc-sound-control");t.length&&i.find(".h5p-sc-set-wrapper").append(t)}));if(e.dnb.dialog.on("close",(function t(){a.dialogWidth=e.dnb.dialog.getDialogWidth(),e.dnb.dialog.off("close",t)})),"H5P.Image"===p){var m=e.dnb.dialog.getMaxSize(n),v=u.find("img");c.params.file.width&&c.params.file.height?I(v,m,{width:c.params.file.width,height:c.params.file.height},!e.isMobileView):(v.on("load",(function(){v.is(":visible")&&I(v,m,{width:this.width,height:this.height},!e.isMobileView)})),e.dnb.dialog.position(n))}else if(!e.isMobileView){var f=null;"H5P.FreeTextQuestion"===p?f="big":"H5P.Text"!==p&&"H5P.Table"!==p&&(f="medium"),e.dnb.dialog.position(n,{width:a.dialogWidth/16},f)}if("H5P.Summary"===p){var b=0;H5P.on(d,"resize",(function(){var t=u.height();(b>t+10||b
o.height&&(i.width=i.width*o.height/i.height,i.height=o.height),i.width>o.width&&(i.height=i.height*o.width/i.width,i.width=o.width);var r=16/Number(t.css("fontSize").replace("px",""));t.css({width:i.width*r+"em",height:i.height*r+"em"}),s&&e.dnb.dialog.position(n,i)},V=function(o){a.isButton()&&e.dnb.dialog.close(),e.currentState!==H5P.Video.PAUSED&&e.currentState!==H5P.Video.ENDED||e.play();var n=o.data;n===t.duration.from&&(n+=.2),e.seek(n,{force:!0})},q=function(){var o=t.height||10,n=t.width||10,i=e.width/e.fontSize;return{height:o/(i/(e.$videoWrapper.width()/e.$videoWrapper.height()))*100+"%",width:n/i*100+"%"}},L=function(t){t.css("zIndex",52),e.showOverlayMask()},W=function(t){t&&t.css("zIndex",""),e.hideOverlayMask()},N=function(){var o=a.isGotoClickable(),i=q(),c=B.extend({},{backgroundColor:"rgb(255,255,255)",boxShadow:!0},t.visuals);if(n=B("
",{"aria-label":e.l10n.interaction,tabindex:"-1",class:"h5p-interaction h5p-poster "+l+(o&&t.goto.visualize?" goto-clickable-visualize":""),css:{left:t.x+"%",top:t.y+"%",width:i.width,height:i.height}}),"H5P.IVHotspot"!==p){n.css("background",c.backgroundColor);var u=c.backgroundColor.split(",");if(u[3])0===parseFloat(u[3].replace(")",""))&&n.addClass("h5p-transparent-interaction")}!1===c.boxShadow&&n.addClass("h5p-box-shadow-disabled"),"H5P.Link"===p&&(n.css("height","auto"),n.css("width","auto"),void 0===e.editor&&n.click((function(){return window.open(d.getUrl()),e.pause(),!1}))),r=B("",{class:"h5p-interaction-outer"}).appendTo(n),s=B(o?"
":"",{class:"h5p-interaction-inner h5p-frame"}).appendTo(r),!o&&O(p)&&s.attr("tabindex","0"),void 0!==e.editor&&d.disableAutoPlay&&d.disableAutoPlay();var h=o?T(s):s;d.attach(h),x(h),a.trigger("display",n),a.getRequiresCompletion()&&e.currentState!==H5P.InteractiveVideo.SEEKING&&void 0===e.editor&&!a.hasFullScore()&&(L(n),n.focus()),setTimeout((function(){H5P.trigger(d,"resize")}),0),"function"==typeof d.setActivityStarted&&"function"==typeof d.getScore&&d.setActivityStarted()},F=function(){var o,i,s=!0;if(t.adaptivity&&(i=a.hasFullScore(),s=!a.getRequiresCompletion()||i,i?o=t.adaptivity.correct:i||(o=t.adaptivity.wrong)),o&&void 0!==o.seekTo){e.pause(),!o.allowOptOut&&n&&(a.isButton()?(e.dnb.dialog.disableOverlay=!0,e.dnb.dialog.hideCloseButton()):L(n));var r=i?"correct":"wrong",c=o.seekLabel?o.seekLabel:e.l10n.defaultAdaptivitySeekLabel;d.hideButton("iv-continue").addButton("iv-adaptivity-"+r,c,(function(){C(o.seekTo),!i&&d.resetTask&&(d.resetTask(),d.hideButton("iv-adaptivity-"+r)),a.remove(),z(o.seekTo)})).showButton("iv-adaptivity-"+r,1).hideButton("iv-adaptivity-"+(i?"wrong":"correct"),1).hideButton("check-answer",1).hideButton("show-solution",1).hideButton("try-again",1),void 0!==d.disableInput&&(d.disableInput instanceof Function||"function"==typeof d.disableInput)&&d.disableInput(),setTimeout((function(){var t=o.message.replace("
","").replace("
","");d.updateFeedbackContent(t,!0),d.read(t)}),0)}else void 0!==d.hasButton&&(d.hasButton("iv-continue")||d.addButton("iv-continue",e.l10n.defaultAdaptivitySeekLabel,(function(){C(),z()})),d[s?"showButton":"hideButton"]("iv-continue"))},z=function(t){var o=U(),n=o.filter((function(t){return!t.isButton()}));if(n.length?o=n:o.length&&e.$container.find(".h5p-dialog-wrapper .h5p-dialog").show(),o.length){var i=o[0];return i.isButton()?i.trigger("open-dialog"):i.trigger("show-mask"),void e.pause()}e.currentState!==H5P.Video.ENDED&&(void 0!==t&&(e.pause(),e.seek(t,{force:!0})),e.play(),e.controls.$play.focus())},U=function(){return e.getVisibleInteractions().filter((function(t){return t!==a})).filter((function(t){return t.getRequiresCompletion()&&!t.hasFullScore()}))};a.isGotoClickable=function(){return-1!==["H5P.Text","H5P.Image"].indexOf(p)&&t.goto&&-1!==["timecode","url"].indexOf(t.goto.type)},a.getCurrentState=function(){if(d&&(d.getCurrentState instanceof Function||"function"==typeof d.getCurrentState))return d.getCurrentState()},a.getDuration=function(){return{from:t.duration.from,to:t.duration.to+1}},a.getRequiresCompletion=function(){return!!t.adaptivity&&!!t.adaptivity.requireCompletion},a.pause=function(){return t.pause},a.isButton=function(){return"button"===t.displayType||!(!e.isMobileView||"H5P.IVHotspot"===p)&&("H5P.Image"!==p||!1!==t.buttonOnMobile)},a.isStandaloneLabel=function(){return"H5P.Nil"===p},a.isMainSummary=function(){return!0===t.mainSummary},a.selectDot=function(){e.isSkippingProhibited(t.duration.from)?e.showPreventSkippingMessage({x:t.duration.from/e.video.getDuration()*e.controls.$slider.get(0).offsetWidth,y:-13},e.l10n.navForwardDisabled):(e.seekingTo=!0,Math.floor(10*e.video.getCurrentTime())!==Math.floor(10*t.duration.from)&&(e.currentState===H5P.Video.VIDEO_CUED?(e.play(),e.seek(t.duration.from)):e.currentState===H5P.Video.PLAYING?e.seek(t.duration.from):(e.play(),e.seek(t.duration.from),e.pause())))},a.addDot=function(){if("H5P.Nil"===p)return B("
",{class:o});var o="h5p-seekbar-interaction "+l,n=B("
",{role:"menuitem",class:o,"aria-label":"".concat(m,". ").concat(f),title:f,css:{left:t.duration.from*e.oneSecondInPercentage+"%"},on:{click:a.selectDot,keydown:function(t){if(13===t.which||32===t.which)return a.selectDot(),!1}}});return e.preventSkipping&&n.attr("aria-disabled","true").attr("tabindex","-1"),a.$menuitem=n,n},a.isVisible=function(){return y},a.visibleAt=function(e){return!(e
=t.duration.to+1)},a.toggle=function(o,i){return a.visibleAt(o)?n?void 0:(y=!0,a.isStandaloneLabel()?w():a.isButton()?S(i):N(),void 0===e.editor?h=e.dnb.add(n,void 0,{dnbElement:h,disableContextMenu:!0}):(a.fit&&(e.editor.fit(n,t),a.fit=!1),n.focus((function(){e.pause()}))),n):(y=!1,void(n&&(h&&(h.hideContextMenu(),h===e.dnb.focusedElement&&(h.blur(),delete e.dnb.focusedElement)),e.editor&&b&&(e.editor.hideInteractionTitle(),b=!1),a.remove())))},a.setTitle=function(t){n&&n.attr("aria-label",t),f=t},a.reCreateInteraction=function(){"H5P.IVHotspot"!==p&&n&&(d&&d.trigger("hide"),a.trigger("hide",n),n.detach(),a.isStandaloneLabel()?w():a.isButton()?S(!0):N())},a.resizeInteraction=function(){a.isStandaloneLabel()||H5P.trigger(d,"resize")},a.positionLabel=function(t){n&&a.isButton()&&i&&!a.isStandaloneLabel()&&(i.removeClass("h5p-left-label"),parseInt(n.css("left"))+i.position().left+i.outerWidth()>t&&i.addClass("h5p-left-label"))},a.setPosition=function(e,o){t.x=e,t.y=o,n.css({left:e+"%",top:o+"%"})},a.setSize=function(e,o){e&&(t.width=e),o&&(t.height=o),H5P.trigger(d,"resize")},a.remove=function(){n&&(a.trigger("domHidden",{$dom:n,key:"videoProgressedPast"},{bubbles:!0,external:!0}),d&&d.trigger("hide"),a.trigger("hide",n),n.detach(),n=void 0)},a.reCreate=function(){l=function(){var e=t.className;if(void 0===e){var o=c.library.split(" ")[0].toLowerCase().split(".");e=o[0]+"-"+o[1]+"-interaction"}return t.goto&&"timecode"===t.goto.type&&(e+=" h5p-goto-timecode"),e}(),a.isStandaloneLabel()||(c.params=c.params||{},d=H5P.newRunnable(c,e.contentId,void 0,void 0,{parent:e,editing:void 0!==e.editor}),void 0===a.maxScore&&d.getMaxScore&&(a.maxScore=d.getMaxScore()),c.userDatas&&K(d)&&(a.score=d.getScore()),e.isTask||void 0===e.options.assets.endscreens||(d.isTask||void 0===d.isTask&&void 0!==d.showSolutions)&&(e.isTask=!0),d.on&&(d.on("xAPI",(function(t){var o=t.getVerifiedStatementValue(["context","contextActivities","parent"])||[],n=t.getContentXAPIId(e),i="completed"===t.getVerb()||"answered"===t.getVerb(),s=o.some((function(t){return t.id===n}));d.getScore&&(a.score=d.getScore()),d.getMaxScore&&(a.maxScore=d.getMaxScore()),s&&i&&t.getMaxScore()&&(a.score=null==t.getScore()?0:t.getScore(),a.maxScore=a.maxScore?a.maxScore:t.getMaxScore(),F()),a.setLastXAPIVerb(t.getVerb()),a.trigger(t)})),d.on("question-finished",(function(){F()})),d.on("resize",(function(){delete a.dialogWidth,e&&e.dnb&&e.dnb.dialog.removeStaticWidth()})),"H5P.IVHotspot"===p&&d.on("goto",V),"H5P.GoToQuestion"===p&&d.on("chosen",V)))};var K=function(t){return"undefined"!==E(t)&&"function"==typeof t.getScore&&"function"==typeof t.getMaxScore};a.setDnbElement=function(t){return h!==t&&(h=t,!0)},a.hasFullScore=function(){return a.score>=a.maxScore},a.getLibraryName=function(){return p},a.getMetadata=function(){return k},a.getTitle=function(){return f},a.isAnswerable=function(){return-1===R.indexOf(a.getLibraryName())&&!a.isStandaloneLabel()},a.setProgress=function(t){this.progress=t},a.getProgress=function(){return this.progress},a.setLastXAPIVerb=function(t){u=t},a.getLastXAPIVerb=function(){return u},a.getClass=function(){return l},a.getCopyrights=function(){if(!a.isStandaloneLabel()){var o=H5P.newRunnable(c,e.contentId);if(void 0!==o){var n=new H5P.ContentCopyrights;return n.addContent(H5P.getCopyrights(o,t,e.contentId)),n.setLabel(f+" "+H5P.InteractiveVideo.humanizeTime(t.duration.from)+" - "+H5P.InteractiveVideo.humanizeTime(t.duration.to)),n}}},a.getXAPIData=function(){if(d&&(d.getXAPIData instanceof Function||"function"==typeof d.getXAPIData))return d.getXAPIData()},a.getSubcontentId=function(){return c.subContentId},a.getElement=function(){return n},a.focusOnFirstTabbableElement=function(){if(n){var t=B(n.get(0)).find("[tabindex]");t&&t.length?t.get(0).focus():a.focus()}},a.focus=function(){n&&n.focus()},a.getClipboardData=function(){return H5P.DragNBar.clipboardify(H5PEditor.InteractiveVideo.clipboardKey,t,"action")},a.repositionToWrapper=function(e){if(n&&"H5P.IVHotspot"!==p&&"H5P.FreeTextQuestion"!==p){if(g&&(n.css({top:t.y+"%",left:t.x+"%"}),n.css(a.isButton()?{height:"",width:""}:q()),g=!1),n.position().top+n.height()>e.height()){var o=(e.height()-n.height())/e.height()*100;if(o<0){o=0;var i=e.height()/parseFloat(n.css("font-size"));n.css("height",i+"em")}n.css("top",o+"%"),g=!0}if(n.position().left+n.width()>e.width()){var s=(e.width()-n.width())/e.width()*100;if(s<0){s=0;var r=e.width()/parseFloat(n.css("font-size"));n.css("width",r+"em")}n.css("left",s+"%"),g=!0}}},a.resetTask=function(){var t;void 0!==c.userDatas&&void 0!==c.userDatas.state&&delete c.userDatas.state,delete a.score,delete a.maxScore,delete a.progress,"function"==typeof(null===(t=a.getInstance())||void 0===t?void 0:t.resetTask)&&a.getInstance().resetTask(),a.reCreate()},a.getInstance=function(){return d},a.reCreate()}q.prototype=Object.create(H5P.EventDispatcher.prototype),q.prototype.constructor=q,q.PROGRESS_INTERACTED=0,q.PROGRESS_ANSWERED=1;const L=q;var W=function(){function t(t){this.l10n=t;var e=document.createElement("div");e.classList.add("h5p-iv-interactions-announcer"),e.setAttribute("aria-live","polite"),this.interactionsAnnouncer=e}var e=t.prototype;return e.getInteractionAnnouncer=function(){return this.interactionsAnnouncer},e.announceInteractions=function(t){t.length>0&&(this.interactionsAnnouncer.textContent="",this.interactionsAnnouncer.textContent="\n ".concat(this.getAnnouncementMessage(t.length),"\n ").concat(this.getTitleAnnouncement(t.length,t[0]),"\n ").concat(this.getPauseAnnouncement(t)))},e.getAnnouncementMessage=function(t){return 0===t?"":1===t?this.l10n.singleInteractionAnnouncement:this.l10n.multipleInteractionsAnnouncement},e.getTitleAnnouncement=function(t,e){return 1===t?e.getTitle():""},e.getPauseAnnouncement=function(t){return t.some((function(t){return t.pause()}))?". ".concat(this.l10n.videoPausedAnnouncement):""},t}(),N=H5P.jQuery,F=!!navigator.userAgent.match(/iPod|iPhone|iPad/g),z=function(){function t(t){var e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{content:"",maxWidth:"auto",style:"h5p-interactive-video-bubble",mode:"centered",focus:function(){}};this.$reference=t,this.maxWidth=e.maxWidth,this.style=e.style,this.mode=e.mode,this.focus=e.focus,this.$tail=N("
",{class:"".concat(this.style,"-tail")}),this.$innerTail=N("
",{class:"".concat(this.style,"-inner-tail")}),this.$content=N("
",{class:"".concat(this.style,"-text")}),"string"==typeof e.content?this.$content.html(e.content):this.$content.append(e.content),this.$innerBubble=N("
",{class:"".concat(this.style,"-inner")}).append(this.$content).prepend(this.$innerTail),this.$h5pContainer=this.$reference.closest(".h5p-interactive-video"),this.$bubble=N("
",{class:this.style,"aria-live":"polite"}).append([this.$tail,this.$innerBubble]).addClass("".concat(this.style,"-inactive")).appendTo(this.$h5pContainer),F&&H5P.$body.css("cursor","pointer"),"centered"===this.mode&&this.$bubble.css({width:"auto"===this.maxWidth?"auto":"".concat(this.maxWidth,"px")}),this.update()}var e=t.prototype;return e.update=function(){var t=this,e=this.getOffsetBetween(this.$h5pContainer,this.$reference),o="full"===this.mode?this.$bubble.outerWidth():Math.min(.9*e.outerWidth,"auto"===this.maxWidth?this.$bubble.outerWidth():this.maxWidth),n=this.getBubblePosition(o,e,this.mode);"centered"===this.mode&&this.$bubble.css({bottom:void 0===n.bottom?void 0:"".concat(n.bottom,"px"),left:"".concat(n.left,"px")}),setTimeout((function(){var e=t.getTailPosition(t.$reference),o={bottom:"".concat(e.bottom,"px"),left:"string"==typeof e.left?e.left:"".concat(e.left,"px")};t.$tail.css(o),t.$innerTail.css(o)}),75)},e.animate=function(){var t=this;this.$bubble.hasClass("".concat(this.style,"-inactive"))&&(this.$bubble.removeClass("".concat(this.style,"-inactive")).addClass("".concat(this.style,"-active")),setTimeout((function(){t.$bubble.removeClass("".concat(t.style,"-active")).addClass("".concat(t.style,"-inactive"))}),2e3))},e.setContent=function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:"";this.$content.html(t),this.update()},e.getContent=function(){return this.$content.get(0).outerHTML},e.isActive=function(){return this.$bubble.hasClass("".concat(this.style,"-active"))},e.toggle=function(t){var e=this,o=arguments.length>1&&void 0!==arguments[1]&&arguments[1];(t=void 0===t?!this.isActive():t)&&o?(setTimeout((function(){e.$bubble.removeClass("".concat(e.style,"-preparing")).addClass("".concat(e.style,"-active")),setTimeout((function(){return e.focus()}),400)}),100),this.$bubble.removeClass("".concat(this.style,"-inactive")).addClass("".concat(this.style,"-preparing"))):this.$bubble.toggleClass("".concat(this.style,"-inactive"),!t).toggleClass("".concat(this.style,"-active"),t),this.update()},e.getBubblePosition=function(t,e,o){var n=t/2;return{bottom:"full"===o?void 0:e.bottom+e.innerHeight+4,left:"full"===o?(e.outerWidth-t)/2:e.left-n+16}},e.getTailPosition=function(t){return{left:t.offset().left-this.$tail.parent().offset().left+8,top:-6,bottom:-6}},e.getOffsetBetween=function(t,e){var o=t[0].getBoundingClientRect(),n=e[0].getBoundingClientRect();return{top:n.top-o.top,right:o.right-n.right,bottom:o.bottom-n.bottom,left:n.left-o.left+parseInt(e.css("marginLeft")),innerWidth:n.width,innerHeight:n.height,outerWidth:o.width,outerHeight:o.height}},e.fullscreen=function(){var t=arguments.length>0&&void 0!==arguments[0]&&arguments[0],e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:void 0,o=arguments.length>2&&void 0!==arguments[2]?arguments[2]:void 0,n=this.isMobilePhone(),i={maxHeight:"",top:""};t&&!n&&void 0!==e&&void 0!==o&&(i.maxHeight="calc(".concat(o,"px - 1em - 9px)"),i.top="calc(((".concat(e-o,"px + 1em) / 2) - 9px)")),this.$bubble.toggleClass("mobile-fullscreen",n&&t),this.$bubble.css(i)},e.isMobilePhone=function(){return/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|Android|Silk|lge |maemo|midp|mmp|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows (ce|phone)|xda|xiino/i.test(navigator.userAgent)||/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw-(n|u)|c55\/|capi|ccwa|cdm-|cell|chtm|cldc|cmd-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc-s|devi|dica|dmob|do(c|p)o|ds(12|-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(-|_)|g1 u|g560|gene|gf-5|g-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd-(m|p|t)|hei-|hi(pt|ta)|hp( i|ip)|hs-c|ht(c(-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i-(20|go|ma)|i230|iac( |-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|-[a-w])|libw|lynx|m1-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|-([1-8]|c))|phil|pire|pl(ay|uc)|pn-2|po(ck|rt|se)|prox|psio|pt-g|qa-a|qc(07|12|21|32|60|-[2-7]|i-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h-|oo|p-)|sdk\/|se(c(-|0|1)|47|mc|nd|ri)|sgh-|shar|sie(-|m)|sk-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h-|v-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl-|tdg-|tel(i|m)|tim-|t-mo|to(pl|sh)|ts(70|m-|m3|m5)|tx-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas-|your|zeto|zte-/i.test(navigator.userAgent.substr(0,4))},t}();const U=z,K=(t,e,{ignoreKeyboard:o=!1,preventDefaultForKeys:n=!0}={})=>{const i=t=>{"click"!==t.type&&n&&t.preventDefault(),e(t)};t.click(i),!0!==o&&I(t,[{key:C.SPACE},{key:C.ENTER}],i)};function G(t,e){return G=Object.setPrototypeOf?Object.setPrototypeOf.bind():function(t,e){return t.__proto__=e,t},G(t,e)}var Q=H5P.jQuery,j="h5p-interactive-video-endscreen",Y="h5p-interactive-video-endscreen-submit-button-hidden",_=function(t){return null!=t};const X=function(t){function e(e){var o,n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};return(o=t.call(this)||this).parent=e,o.l10n=Q.extend({title:"@answered Questions answered",information:"You have answered @answered questions, click below to submit your answers.",informationOnSubmitButtonDisabled:"You have answered @answered questions.",informationNoAnswers:"You have not answered any questions.",informationMustHaveAnswer:"You have to answer at least one question before you can submit your answers.",submitButton:"Submit Answers",submitMessage:"Your answers have been submitted!",tableRowAnswered:"Answered questions",tableRowScore:"Score",answeredScore:"answered",tableRowSummaryWithScore:"You got @score out of @total points for the @question that appeared after @minutes minutes and @seconds seconds.",tableRowSummaryWithoutScore:"You have answered the @question that appeared after @minutes minutes and @seconds seconds."},n.l10n),o.isSubmitButtonEnabled=o.parent.isSubmitButtonEnabled,o.buildDOM(),o}var o,n;n=t,(o=e).prototype=Object.create(n.prototype),o.prototype.constructor=o,G(o,n);var i=e.prototype;return i.buildDOM=function(){var t=this;this.$endscreenIntroductionTitleText=Q("
",{class:"".concat(j,"-introduction-title-text"),id:"".concat(j,"-introduction-title-text")}),this.$closeButton=Q("",{role:"button",class:"".concat(j,"-close-button"),tabindex:"0","aria-label":this.parent.l10n.close}),K(this.$closeButton,(function(){return t.parent.toggleEndscreen(!1)}));var e=Q("
",{class:"".concat(j,"-introduction-title")}).append([this.$endscreenIntroductionTitleText,this.$closeButton]);this.$endscreenIntroductionText=Q("
",{class:"".concat(j,"-introduction-text"),id:"".concat(j,"-introduction-text")}),this.isSubmitButtonEnabled&&(this.$submitButtonContainer=Q("
",{class:"".concat(j,"-submit-button-container ").concat(Y)}),this.$submitButton=H5P.JoubelUI.createButton({class:"".concat(j,"-submit-button"),html:this.l10n.submitButton,appendTo:this.$submitButtonContainer,click:function(){return t.handleSubmit()}})),this.$endscreenOverviewTitle=Q("
",{class:"".concat(j,"-overview-title")}).append(Q("
",{class:"".concat(j,"-overview-title-answered-questions"),html:this.l10n.tableRowAnswered})).append(Q("
",{class:"".concat(j,"-overview-title-score"),html:this.l10n.tableRowScore})),this.$endscreenBottomTable=Q("
",{class:"".concat(j,"-overview-table")}),this.$endscreen=Q("
",{class:j,role:"dialog","aria-labelledby":"".concat(j,"-introduction-title-text"),"aria-describedby":"".concat(j,"-introduction-text")}).append(Q("
",{class:"".concat(j,"-introduction")}).append(Q("
",{class:"".concat(j,"-star-symbol")})).append(Q("
",{class:"".concat(j,"-introduction-container")}).append([e,this.$endscreenIntroductionText,this.$submitButtonContainer]))).append(Q("
",{class:"".concat(j,"-overview")}).append(this.$endscreenOverviewTitle).append(this.$endscreenBottomTable))},i.handleSubmit=function(){var t=this;this.$submitButtonContainer.hasClass(Y)||(this.parent.setUserSubmitted(!0),this.$submitButtonContainer.addClass(Y),this.$endscreenIntroductionText.html('
').concat(this.l10n.submitMessage,"
")),this.answered.forEach((function(e){if("completed"!==e.getLastXAPIVerb()&&"answered"!==e.getLastXAPIVerb()){var o=new H5P.XAPIEvent;o.data.statement=e.getXAPIData().statement,e.setLastXAPIVerb(o.getVerb()),t.trigger(o)}})),this.parent.triggerXAPIScored(this.parent.getUsersScore(),this.parent.getUsersMaxScore(),"completed"))},i.getDOM=function(){return this.$endscreen},i.buildTableRow=function(t,e,o,n){var i=this,s=_(o)&&_(n),r=s?this.l10n.tableRowSummaryWithScore:this.l10n.tableRowSummaryWithoutScore,a=this.parent.isSkippingProhibited(t)?" ".concat(j,"-no-link"):"",c=Q("
",{class:"".concat(j,"-overview-table-row").concat(a),role:"row",tabIndex:0,"aria-label":r.replace("@score",o).replace("@total",n).replace("@question",e).replace("@minutes",Math.floor(t/60)).replace("@seconds",t%60)});return K(c,(function(){return i.jump(t)})),Q("
",{class:"".concat(j,"-overview-table-row-time"),html:H5P.InteractiveVideo.humanizeTime(t),appendTo:c,"aria-hidden":!0}),Q("
",{class:"".concat(j,"-overview-table-row-title"),html:e,appendTo:c,"aria-hidden":!0}),Q("
",{class:"".concat(j,"-overview-table-row-score"),html:s?"".concat(o," / ").concat(n):this.l10n.answeredScore,appendTo:c,"aria-hidden":!0}),c},i.jump=function(t){this.parent.isSkippingProhibited(t)||(this.parent.seek(t),this.parent.toggleEndscreen(!1))},i.update=function(){var t,e=this,o=arguments.length>0&&void 0!==arguments[0]?arguments[0]:[];(this.answered=o.filter((function(t){return void 0!==t.getProgress()})).sort((function(t,e){return t.getDuration().from-e.getDuration().from})),this.isSubmitButtonEnabled)&&(null===(t=this.$submitButtonContainer)||void 0===t||t.addClass(Y));this.$endscreenBottomTable.empty(),this.answered.forEach((function(t){var o=t.getDuration().from,n=e.getDescription(t),i=t.getInstance(),s=i.getScore?i.getScore():void 0,r=i.getMaxScore?i.getMaxScore():void 0;e.$endscreenBottomTable.append(e.buildTableRow(o,n,s,r))}));var n=this.answered.length;this.$endscreenIntroductionTitleText.html(this.l10n.title.replace("@answered",n)),0===n?this.$endscreenIntroductionText.html('
').concat(this.l10n.informationNoAnswers,"
\n ").concat(this.isSubmitButtonEnabled?"
".concat(this.l10n.informationMustHaveAnswer,"
"):"")):this.$endscreenIntroductionText.html(this.isSubmitButtonEnabled?this.l10n.information.replace("@answered",n):this.l10n.informationOnSubmitButtonDisabled.replace("@answered",n)),this.isSubmitButtonEnabled&&n>0&&this.$submitButtonContainer.removeClass(Y)},i.getDescription=function(t){return"function"==typeof t.getInstance&&"function"==typeof t.getInstance().getTitle?t.getInstance().getTitle():t.getTitle()},i.focus=function(){!this.isSubmitButtonEnabled||this.$submitButtonContainer.hasClass(Y)?this.$closeButton.focus():this.$submitButton.focus()},e}(H5P.EventDispatcher);var J=H5P.jQuery;function Z(t,e,o){var n,i,s,r,a,c=this;H5P.EventDispatcher.call(c),c.contentId=e,c.contentData=o,c.params=t,c.instanceIndex=nt(),c.isSubmitButtonEnabled=void 0===o||void 0===o.isScoringEnabled||void 0===o.isReportingEnabled||o.isScoringEnabled||o.isReportingEnabled,c.bookmarksMenuId="interactive-video-"+this.contentId+"-bookmarks-chooser",c.endscreensMenuId="interactive-video-"+this.contentId+"-endscreens-chooser",c.qualityMenuId="interactive-video-"+this.contentId+"-quality-chooser",c.captionsMenuId="interactive-video-"+this.contentId+"-captions-chooser",c.playbackRateMenuId="interactive-video-"+this.contentId+"-playback-rate-chooser",c.popupMenuButtons=[],c.popupMenuChoosers=[],this.visibleInteractions=[],c.isMinimal=!1,c.options=J.extend({video:{textTracks:{videoTrack:[]}},assets:{}},t.interactiveVideo),c.options.video.startScreenOptions=c.options.video.startScreenOptions||{},c.qualities=void 0,c.options.video.startScreenOptions.title||(c.options.video.startScreenOptions.title="Interactive Video"),c.startScreenOptions=J.extend({hideStartTitle:!1,shortStartDescription:""},c.options.video.startScreenOptions),t.override&&(t.override.showSolutionButton||t.override.retryButton)&&(c.override={},t.override.showSolutionButton&&(c.override.enableSolutionsButton="on"===t.override.showSolutionButton),t.override.retryButton&&(c.override.enableRetry="on"===t.override.retryButton)),void 0!==t.override&&(c.showRewind10=void 0!==t.override.showRewind10&&t.override.showRewind10,c.showBookmarksmenuOnLoad=void 0!==t.override.showBookmarksmenuOnLoad&&t.override.showBookmarksmenuOnLoad,c.preventSkippingMode=t.override.preventSkippingMode||"none",c.deactivateSound=t.override.deactivateSound||!1),c.l10n=J.extend({interaction:"Interaction",play:"Play",pause:"Pause",mute:"Mute, currently unmuted",unmute:"Unmute, currently muted",quality:"Video quality",captions:"Captions",close:"Close",fullscreen:"Fullscreen",exitFullscreen:"Exit fullscreen",summary:"Open summary dialog",bookmarks:"Bookmarks",endscreen:"Submit Screen",endscreens:"Submit Screens",defaultAdaptivitySeekLabel:"Continue",continueWithVideo:"Continue with video",more:"More player options",playbackRate:"Playback rate",rewind10:"Rewind 10 seconds",navDisabled:"Navigation is disabled",navForwardDisabled:"Navigating forward is disabled",sndDisabled:"Sound is disabled",requiresCompletionWarning:"You need to answer all the questions correctly before continuing.",back:"Back",hours:"Hours",minutes:"Minutes",seconds:"Seconds",currentTime:"Current time:",totalTime:"Total time:",singleInteractionAnnouncement:"Interaction appeared:",multipleInteractionsAnnouncement:"Multiple interactions appeared:",videoPausedAnnouncement:"Video was paused",content:"Content",answered:"@answered answered!",videoProgressBar:"Video progress",howToCreateInteractions:"Play the video to start creating interactions"},t.l10n),c.l10n.play+=" (k)",c.l10n.pause+=" (k)",c.l10n.mute+=" (m)",c.l10n.unmute+=" (m)",o&&void 0!==o.previousState&&void 0!==o.previousState.progress&&void 0!==o.previousState.answers&&(c.previousState=o.previousState),c.menuitems=[],c.lastState=H5P.Video.ENDED,c.justVideo=!1;var l=navigator.userAgent.match(/(iPhone|iPod) OS (\d*)_/i);null!==l&&3===l.length&&(c.justVideo=l[2]<10),c.currentTime=Math.floor(void 0!==(null===(n=c.previousState)||void 0===n?void 0:n.progress)&&null!==(null===(i=c.previousState)||void 0===i?void 0:i.progress)?c.previousState.progress:(null===(s=t.override)||void 0===s?void 0:s.startVideoAt)||0),this.maxTimeReached=c.previousState&&c.previousState.maxTimeReached?c.previousState.maxTimeReached:0,c.interactionsProgress=[],c.previousState&&c.previousState.interactionsProgress&&(c.interactionsProgress=c.previousState.interactionsProgress),this.setUserSubmitted(null===(r=this.previousState)||void 0===r?void 0:r.submitted),a=t.override&&!!t.override.loop,c.$videoWrapper=J("
",{class:"h5p-video-wrapper"}),c.$controls=J("
",{role:"toolbar",class:"h5p-controls hidden"}),c.$read=J("
",{"aria-live":"polite",class:"hidden-but-read"}),this.fontSize=16,this.width=640;var d=!1,u=!0,h=!1;if(c.isTask=!1,c.interactions=[],c.options.assets.interactions)for(var p=0;p
0&&c.toggleEndscreen(!0),a){c.video.play();var r=t.override&&t.override.startVideoAt?t.override.startVideoAt:0;c.seek(r)}break;case H5P.Video.PLAYING:if(u){c.addQualityChooser(),c.addPlaybackRateChooser(),c.removeSplash(),c.startUpdatingBufferBar(),c.toggleBookmarksChooser(!1,{firstPlay:u}),c.toggleEndscreensChooser(!1,{firstPlay:u}),u=!1;var l=c.options.video.startScreenOptions.poster;l&&void 0!==l.path&&setTimeout((function(){c.trigger("resize")}),400)}c.currentState=H5P.Video.PLAYING,null===(o=c.controls)||void 0===o||o.$play.removeClass("h5p-pause").attr("aria-label",c.l10n.pause),null!==(n=c.controls)&&void 0!==n&&n.$play.is(":focus")&&(c.controls.$play.blur(),c.controls.$play.focus()),c.timeUpdate(c.video.getCurrentTime());break;case H5P.Video.PAUSED:c.currentState=H5P.Video.PAUSED,c.controls.$play.addClass("h5p-pause").attr("aria-label",c.l10n.play),c.focusInteraction?(c.focusInteraction.focusOnFirstTabbableElement(),delete c.focusInteraction):c.controls.$play.is(":focus")&&(c.controls.$play.blur(),c.controls.$play.focus()),c.timeUpdate(c.video.getCurrentTime());break;case H5P.Video.BUFFERING:c.currentState=H5P.Video.BUFFERING,c.removeSplash(),c.startUpdatingBufferBar()}})),c.video.on("qualityChange",(function(t){var e=t.data;if(c.controls&&c.controls.$qualityChooser){if("YouTube"===this.getHandlerName()){if(!c.qualities)return;var o=c.qualities.filter((function(e){return e.name===t.data}))[0];return void c.controls.$qualityChooser.find("li").attr("data-quality",t.data).html(o.label)}c.controls.$qualityChooser.find("li").attr("aria-checked","false").filter('[data-quality="'+e+'"]').attr("aria-checked","true")}})),c.video.on("playbackRateChange",(function(t){var e=t.data;c.controls&&c.controls.$playbackRateChooser&&c.controls.$playbackRateChooser.find("li").attr("aria-checked","false").filter('[playback-rate="'+e+'"]').attr("aria-checked","true")})),c.on("enterFullScreen",(function(){var t=this;c.hasFullScreen=!0,c.$container.parent(".h5p-content").css("height","100%"),c.controls.$fullscreen.addClass("h5p-exit").attr("aria-label",c.l10n.exitFullscreen),c.controls.$fullscreen.blur(),c.controls.$fullscreen.focus(),c.resizeInteractions(),setTimeout((function(){void 0!==t.bubbleEndscreen&&t.bubbleEndscreen.update()}),225)})),c.on("exitFullScreen",(function(){c.$container.hasClass("h5p-standalone")&&c.$container.hasClass("h5p-minimal")&&c.pause(),c.hasFullScreen=!1,c.$container.parent(".h5p-content").css("height",""),c.controls.$fullscreen.removeClass("h5p-exit").attr("aria-label",c.l10n.fullscreen),c.controls.$fullscreen.blur(),c.controls.$fullscreen.focus(),c.resizeInteractions(),c.dnb&&c.dnb.dialog&&!c.hasUncompletedRequiredInteractions()&&c.dnb.dialog.close()})),c.video.on("captions",(function(t){c.controls||(c.addControls(),c.trigger("resize")),c.setCaptionTracks(t.data)})),c.accessibility=new W(c.l10n))}},c.togglePlayPause=function(){var t=c.isDisabled(c.controls.$play);if(c.controls.$play.hasClass("h5p-pause")&&!t){var e=!screen||Math.min(screen.width,screen.height)<=c.width;H5P.fullscreenSupported&&!c.hasFullScreen&&e&&c.$container.hasClass("h5p-standalone")&&c.$container.hasClass("h5p-minimal")&&c.toggleFullScreen(),c.video.play(),c.toggleEndscreen(!1),c.closePopupMenus()}else c.video.pause();c.handleAnswered()},c.toggleMute=function(){var t=!(arguments.length>0&&void 0!==arguments[0])||arguments[0],e=c.controls.$volume;c.deactivateSound||(e.hasClass("h5p-muted")?(e.removeClass("h5p-muted").attr("aria-label",c.l10n.mute),c.video.unMute()):(e.addClass("h5p-muted").attr("aria-label",c.l10n.unmute),c.video.mute()),t&&(e.blur(),e.focus()))}}Z.prototype=Object.create(H5P.EventDispatcher.prototype),Z.prototype.constructor=Z,Z.prototype.setCaptionTracks=function(t){var e=this;if(t.unshift(new H5P.Video.LabelValue("Off","off")),e.captionsTrackSelector)e.captionsTrackSelector.updateOptions(t);else{var o=this.editor?void 0:e.options.video.textTracks.defaultTrackLabel,n=t.reduce((function(t,e){return void 0===t&&o&&e.label===o?e:t}),void 0)||e.video.getCaptionsTrack();n||(n=t[0]),e.captionsTrackSelector=new T("captions",t,n,"menuitemradio",e.l10n,e.contentId),e.controls.$captionsButton=J(e.captionsTrackSelector.control),e.popupMenuButtons.push(e.controls.$captionsButton),e.controls.$volume?J(e.captionsTrackSelector.control).insertAfter(e.controls.$volume):J(e.captionsTrackSelector.control).insertAfter(e.controls.$qualityButton),J(e.captionsTrackSelector.popup).css(e.controlsCss).insertAfter(J(e.captionsTrackSelector.control)),e.popupMenuChoosers.push(J(e.captionsTrackSelector.popup)),J(e.captionsTrackSelector.overlayControl).insertAfter(e.controls.$qualityButtonMinimal),e.controls.$overlayButtons=e.controls.$overlayButtons.add(e.captionsTrackSelector.overlayControl),e.captionsTrackSelector.on("select",(function(t){e.video.setCaptionsTrack("off"===t.data.value?null:t.data)})),e.captionsTrackSelector.on("close",(function(){e.controls.$overlayButtons.removeClass("h5p-hide"),"true"===e.controls.$more.attr("aria-expanded")&&e.controls.$more.click(),e.resumeVideo()})),e.captionsTrackSelector.on("open",(function(){e.controls.$overlayButtons.addClass("h5p-hide"),e.closePopupMenus(e.controls.$captionsButton)})),e.minimalMenuKeyboardControls.insertElementAt(e.captionsTrackSelector.overlayControl,2)}},Z.prototype.getCurrentState=function(){var t=this;if(t.video&&t.video.play){var e={submitted:this.userSubmitted,progress:t.currentTime,maxTimeReached:this.maxTimeReached||null,answers:[],interactionsProgress:t.interactions.slice().sort((function(t,e){return t.getDuration().from-e.getDuration().from})).map((function(t){return t.getProgress()}))};if(void 0!==t.interactions)for(var o=0;o ').appendTo(t))},Z.prototype.addSplash=function(){var t=this;void 0!==this.editor||this.video.pressToPlay||!this.video.play||this.$splash||(this.$splash=J('
'+this.options.video.startScreenOptions.title+'
").click((function(){t.video.play()})).appendTo(this.$overlay).find(".h5p-interaction-button").click((function(){return!1})).end(),J(".h5p-splash",this.$splash).keydown((function(e){P(e)&&(t.video.play(),e.preventDefault(),t.$controls.find(".h5p-play").focus())})),void 0!==this.startScreenOptions.shortStartDescription&&this.startScreenOptions.shortStartDescription.length||this.$splash.addClass("no-description"),this.startScreenOptions.hideStartTitle&&this.$splash.addClass("no-title"))},Z.prototype.getDuration=function(){return void 0===this.duration&&(this.duration=this.video.getDuration()),this.duration},Z.prototype.addControls=function(){var t=this;this.addSplash(),this.attachControls(this.$controls.removeClass("hidden"));var e=this.getDuration(),o=Z.humanizeTime(e),n=Z.formatTimeForA11y(e,this.l10n);if(this.controls.$totalTime.find(".human-time").html(o),this.controls.$totalTime.find(".hidden-but-read").html("".concat(this.l10n.totalTime," ").concat(n)),this.controls.$slider.slider("option","max",e),this.bookmarkMenuKeyboardControls=new b([new g]),this.bookmarkMenuKeyboardControls.on("close",(function(){return t.toggleBookmarksChooser(!1)})),this.endscreenMenuKeyboardControls=new b([new g]),this.endscreenMenuKeyboardControls.on("close",(function(){return t.toggleEndscreensChooser(!1)})),this.addSliderInteractions(),this.addBookmarks(),this.options.assets.endscreens&&this.options.assets.endscreens.length>0)for(var i=!1,s=this.getDuration(),r=this.options.assets.endscreens.length-1;r>=0;r--){this.options.assets.endscreens[r].time>s&&(i?(this.options.assets.endscreens.splice(r,1),this.trigger("endscreensChanged",{index:r,number:-1})):(this.options.assets.endscreens[r].time=s,this.options.assets.endscreens[r].label=Z.humanizeTime(s)+" "+this.l10n.endscreen,this.trigger("endscreensChanged",{index:r,number:1}),i=!0))}this.addEndscreenMarkers(),this.addBubbles(),this.trigger("controls")},Z.prototype.loaded=function(){var t=this.getDuration();if(this.oneSecondInPercentage=100/t,t=Math.floor(t),void 0!==this.editor){var e=et("interactions",this.editor.field.fields),o=et("duration",e.field.fields).fields;o[0].max=o[1].max=t,o[0].min=o[1].min=0;for(var n=et("adaptivity",e.field.fields).fields,i=0;i
0)for(i=this.options.assets.interactions.length-1;i>=0;i--)if(this.options.assets.interactions[i].duration.to>t){var s=this.options.assets.interactions[i].duration.to-this.options.assets.interactions[i].duration.from,r=t-s<=0?0:t-s;this.options.assets.interactions[i].duration.from=r,this.options.assets.interactions[i].duration.to=t}if(this.hasMainSummary()){var a=t-this.options.summary.displayAt;a<0&&(a=0),void 0===this.options.assets.interactions&&(this.options.assets.interactions=[]),this.options.assets.interactions.push({action:this.options.summary.task,x:80,y:80,duration:{from:a,to:t},displayType:"button",bigDialog:!0,className:"h5p-summary-interaction h5p-end-summary",label:""+this.l10n.summary+"
",mainSummary:!0}),this.initInteraction(this.options.assets.interactions.length-1)}this.currentState===Z.ATTACHED&&(this.video.pressToPlay||this.addControls(),this.trigger("resize")),this.currentState=Z.LOADED},Z.prototype.initInteraction=function(t){var e,o=this,n=o.options.assets.interactions[t];if(o.override){var i={};n.adaptivity&&n.adaptivity.requireCompletion&&(i.enableRetry=!0),H5P.jQuery.extend(n.action.params.behaviour,o.override,i)}void 0!==o.previousState&&void 0!==o.previousState.answers&&null!==o.previousState.answers[t]&&(e=o.previousState.answers[t]);var s=new L(n,o,e);return s.on("display",(function(t){var e=t.data;e.appendTo(o.$overlay),s.repositionToWrapper(o.$videoWrapper);var n=void 0!==o.video.pressToPlay;ot(n?100:null,(function(){(o.currentState===H5P.Video.PLAYING||o.currentState===H5P.Video.BUFFERING)&&s.pause()&&(o.focusInteraction||(o.focusInteraction=s),o.video.pause())})),o.seekingTo&&(o.seekingTo=void 0,e.focus()),setTimeout((function(){s.positionLabel(o.$videoWrapper.width())}),0),o.toggleEndscreen(!1)})),s.on("hide",(function(){o.handleAnswered()})),s.on("xAPI",(function(t){var e=t.getVerb();"interacted"===e&&this.setProgress(L.PROGRESS_INTERACTED),-1!==J.inArray(e,["completed","answered"])&&t.setVerb("answered"),void 0===t.data.statement.context.extensions&&(t.data.statement.context.extensions={}),t.data.statement.context.extensions["http://id.tincanapi.com/extension/ending-point"]="PT"+Math.floor(o.video.getCurrentTime())+"S"})),o.interactions.push(s),s},Z.prototype.handleAnswered=function(){var t=this;t.interactions.forEach((function(e){e.getProgress()===L.PROGRESS_INTERACTED&&(e.setProgress(L.PROGRESS_ANSWERED),e.$menuitem.addClass("h5p-interaction-answered"),t.hasStar&&(t.playStarAnimation(),t.playBubbleAnimation(t.l10n.answered.replace("@answered",""+t.getAnsweredTotal()+" ")),t.endscreen.update(t.interactions)))}))},Z.prototype.getAnsweredTotal=function(){return this.interactions.filter((function(t){return t.getProgress()===L.PROGRESS_ANSWERED})).length},Z.prototype.hasMainSummary=function(){var t=this.options.summary;return!(void 0===t||void 0===t.displayAt||void 0===t.task||void 0===t.task.params||void 0===t.task.params.summaries||!t.task.params.summaries.length||void 0===t.task.params.summaries[0].summary||!t.task.params.summaries[0].summary.length)},Z.prototype.addSliderInteractions=function(){var t=this,e=this;this.controls.$interactionsContainer.children().remove(),this.interactionKeyboardControls.elements=[],H5P.jQuery.extend([],this.interactions).sort((function(t,e){return t.getDuration().from-e.getDuration().from})).forEach((function(o){var n=o.addDot();e.menuitems.push(n),void 0===e.previousState&&e.interactionsProgress.push(void 0),e.interactionsProgress[e.menuitems.length-1]===L.PROGRESS_ANSWERED&&(o.setProgress(e.interactionsProgress[e.menuitems.length-1]),n.addClass("h5p-interaction-answered")),void 0!==n&&(n.appendTo(t.controls.$interactionsContainer),"both"!==e.preventSkippingMode&&t.interactionKeyboardControls.addElement(n.get(0)))})),e.interactionKeyboardControls.on("afterNextElement",(function(e){return t.handleInteractionTabIndex(e)})),e.interactionKeyboardControls.on("afterPreviousElement",(function(e){return t.handleInteractionTabIndex(e)}))},Z.prototype.handleInteractionTabIndex=function(t){t.element.removeAttribute("tabindex")},Z.prototype.closePopupMenus=function(t){this.popupMenuButtons.forEach((function(e){void 0!==e&&e!==t&&void 0===e.attr("aria-disabled")&&"true"===e.attr("aria-expanded")&&e.click()}))},Z.prototype.displayBookmarks=function(){return this.options.assets.bookmarks&&this.options.assets.bookmarks.length&&"both"!==this.preventSkippingMode},Z.prototype.addBookmarks=function(){if(this.bookmarksMap={},void 0!==this.options.assets.bookmarks&&"both"!==this.preventSkippingMode)for(var t=0;t1&&void 0!==arguments[1]?arguments[1]:{initialLoad:!1,keepStopped:!1,firstPlay:!1};if(null!==(e=this.controls)&&void 0!==e&&e.$bookmarksButton){t=void 0===t?!this.controls.$bookmarksChooser.hasClass("h5p-show"):t;var i=this.controls.$bookmarksChooser.hasClass("h5p-show");this.controls.$minimalOverlay.toggleClass("h5p-show",t),this.controls.$minimalOverlay.find(".h5p-minimal-button").toggleClass("h5p-hide",t),this.controls.$bookmarksButton.attr("aria-expanded",!!t&&"true"),this.controls.$more.attr("aria-expanded",t?"true":"false"),this.controls.$bookmarksChooser.css({maxHeight:t?this.controlsCss.maxHeight:"32px"}).toggleClass("h5p-show",t).toggleClass("h5p-transitioning",t||i)}t?(this.closePopupMenus(this.controls.$bookmarksButton),this.showBookmarksmenuOnLoad&&n.initialLoad||this.controls.$bookmarksChooser[0].addEventListener("transitionend",(function(){o.controls.$bookmarksChooser.find('[tabindex="0"]').first().focus()}),{once:!0}),this.editor&&(this.interruptVideo(),this.updateChooserTime(this.controls.$bookmarksChooser,".h5p-add-bookmark"))):n.firstPlay||(this.editor&&!n.keepStopped&&this.resumeVideo(),this.controls.$bookmarksChooser.hasClass("h5p-show")||this.controls.$bookmarksButton.focus())},Z.prototype.toggleEndscreensChooser=function(t){var e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{keepStopped:!1,firstPlay:!1};if(this.editor&&this.controls.$endscreensButton){t=void 0===t?!this.controls.$endscreensChooser.hasClass("h5p-show"):t;var o=this.controls.$endscreensChooser.hasClass("h5p-show");this.controls.$minimalOverlay.toggleClass("h5p-show",t),this.controls.$minimalOverlay.find(".h5p-minimal-button").toggleClass("h5p-hide",t),this.controls.$endscreensButton.attr("aria-expanded",t?"true":"false").toggleClass("h5p-star-active-editor",t),this.controls.$more.attr("aria-expanded",t?"true":"false");var n=-10+Math.min(0,this.$container.outerWidth()-this.controls.$endscreensChooser.parent().offset().left-this.controls.$endscreensChooser.outerWidth())+"px";this.controls.$endscreensChooser.css({maxHeight:t?this.controlsCss.maxHeight:"32px"}).css({left:n}).toggleClass("h5p-show",t).toggleClass("h5p-transitioning",t||o)}t?(this.closePopupMenus(this.controls.$endscreensButton),this.editor&&(this.interruptVideo(),this.updateChooserTime(this.controls.$endscreensChooser,".h5p-add-endscreen")),this.controls.$endscreensChooser.find('[tabindex="0"]').first().focus()):e.firstPlay||(this.editor&&!e.keepStopped&&this.resumeVideo(),this.controls.$endscreensChooser.hasClass("h5p-show")||this.controls.$endscreensButton.focus())},Z.prototype.updateChooserTime=function(t,e){var o=t.find(e);o.html(o.data("default").replace("@timecode",Z.humanizeTime(this.video.getCurrentTime())))},Z.prototype.interruptVideo=function(){this.currentState===H5P.Video.PLAYING&&(this.interruptedTemporarily=!0,this.video.pause())},Z.prototype.resumeVideo=function(t){if(!t){if(!this.interruptedTemporarily)return;if(this.popupMenuChoosers.some((function(t){return t.hasClass("h5p-show")})))return}this.interruptedTemporarily=!1,this.video.play()},Z.prototype.toggleEndscreen=function(t){var e,o;!this.editor&&this.hasStar&&t!==(null===(e=this.bubbleEndscreen)||void 0===e?void 0:e.isActive())&&null!==(o=this.controls)&&void 0!==o&&o.$endscreensButton&&((t=void 0===t?!this.bubbleEndscreen.isActive():t)?(this.disableTabIndexes(".h5p-interactive-video-endscreen"),this.stateBeforeEndscreen=this.currentState,this.video.pause(),this.endscreen.update(this.interactions)):(this.restoreTabIndexes(),this.controls.$endscreensButton.focus(),this.stateBeforeEndscreen===H5P.Video.PLAYING&&(this.video.play(),this.stateBeforeEndscreen=void 0)),this.controls.$endscreensButton.attr("aria-expanded",t),this.controls.$endscreensButton.toggleClass("h5p-star-active",t),this.bubbleEndscreen.toggle(t,!0))},Z.prototype.showPreventSkippingMessage=function(){var t,e,o,n=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},i=arguments.length>1?arguments[1]:void 0,s=this;if(n.x=null!==(t=n.x)&&void 0!==t?t:0,n.y="number"==typeof n.y?25-n.y:25,i=null!==(e=i)&&void 0!==e?e:s.l10n.navDisabled,!s.preventSkippingWarningTimeout){s.$preventSkippingMessage||(s.$preventSkippingMessage=J("",{class:"h5p-prevent-skipping-message",appendTo:s.controls.$bookmarksContainer}),s.$preventSkippingMessage=J("
",{class:"h5p-prevent-skipping-message",appendTo:s.controls.$endscreensContainer}),s.$preventSkippingMessageText=J("
",{class:"h5p-prevent-skipping-message-text",html:i,appendTo:s.$preventSkippingMessage}),s.$preventSkippingMessageTextA11y=J("
",{class:"hidden-but-read",html:i,appendTo:s.controls.$slider}));var r=s.$preventSkippingMessage.innerWidth()-s.$preventSkippingMessage.width();s.$preventSkippingMessage.css("max-width","".concat(s.$videoWrapper.width()-r,"px"));var a=parseFloat(null!==(o=s.$preventSkippingMessage.css("transform").split(",")[4])&&void 0!==o?o:0),c=s.$sliderContainer.get(0).offsetLeft+s.controls.$slider.get(0).offsetLeft+n.x+a+s.$preventSkippingMessage.outerWidth(),l=Math.max(0,c-s.$container.width());n.x-=l,s.$preventSkippingMessage.toggleClass("h5p-overflow",l>0),s.$preventSkippingMessage.css("left",n.x),s.$preventSkippingMessage.css("bottom",n.y),setTimeout((function(){s.$preventSkippingMessage.addClass("h5p-show").attr("aria-hidden","false")}),0),s.preventSkippingWarningTimeout=setTimeout((function(){s.$preventSkippingMessage.removeClass("h5p-show").attr("aria-hidden","true"),setTimeout((function(){s.preventSkippingWarningTimeout=void 0}),500)}),2e3)}},Z.prototype.onBookmarkSelect=function(t,e){var o=this;if(this.isSkippingProhibited(e.time))this.showPreventSkippingMessage({x:e.time/o.video.getDuration()*o.controls.$slider.get(0).offsetWidth+2,y:-23},o.l10n.navForwardDisabled);else{o.currentState!==H5P.Video.PLAYING&&(t.mouseover().mouseout(),setTimeout((function(){o.timeUpdate(o.video.getCurrentTime())}),0)),"true"===o.controls.$more.attr("aria-expanded")&&o.$container.hasClass("h5p-minimal")?o.controls.$more.click():o.toggleBookmarksChooser(!1),o.video.play(),o.seek(e.time);var n=Z.formatTimeForA11y(e.time,o.l10n);setTimeout((function(){return o.read("".concat(o.l10n.currentTime," ").concat(n))}),150)}},Z.prototype.onEndscreenSelect=function(t,e){var o=this;o.currentState!==H5P.Video.PLAYING&&(t.mouseover().mouseout(),setTimeout((function(){o.timeUpdate(o.video.getCurrentTime())}),0)),"true"===o.controls.$more.attr("aria-expanded")&&o.$container.hasClass("h5p-minimal")?o.controls.$more.click():o.toggleEndscreensChooser(!1),o.video.play(),o.seek(e.time);var n=Z.formatTimeForA11y(e.time,o.l10n);setTimeout((function(){return o.read("".concat(o.l10n.currentTime," ").concat(n))}),150)},Z.prototype.addBookmark=function(t,e){var o=this,n=o.options.assets.bookmarks[t];void 0===e&&(e=Math.floor(10*n.time)/10);var i=o.bookmarksMap[e]=J('
").appendTo(o.controls.$bookmarksContainer).data("id",t).hover((function(){void 0!==o.bookmarkTimeout&&clearTimeout(o.bookmarkTimeout),o.controls.$bookmarksContainer.children(".h5p-show").removeClass("h5p-show"),i.addClass("h5p-show")}),(function(){o.bookmarkTimeout=setTimeout((function(){i.removeClass("h5p-show")}),o.editor?1e3:2e3)}));i.find(".h5p-bookmark-label").css("maxWidth",parseInt(o.controls.$slider.parent().css("marginRight"))-35),void 0===o.controls.$bookmarksList&&(o.controls.$bookmarksList=J('
').insertAfter(o.controls.$bookmarksChooser.find("h2")));var s=J('
').concat(n.label," ")).click((function(){return o.onBookmarkSelect(i,n)})).keydown((function(t){P(t)&&o.onBookmarkSelect(i,n),t.stopPropagation()}));o.bookmarkMenuKeyboardControls.addElement(s.get(0));var r=o.controls.$bookmarksList.children(":eq("+t+")");return 0!==r.length?s.insertBefore(r):s.appendTo(o.controls.$bookmarksList),o.on("bookmarksChanged",(function(n){var r=n.data.index,a=n.data.number;r===t&&a<0?(s.remove(),delete o.bookmarksMap[e]):t>=r&&(t+=a,i.data("id",t))})),o.trigger("bookmarkAdded",{bookmark:i}),i},Z.prototype.addEndscreen=function(t,e){var o,n=this,i=n.options.assets.endscreens[t];if(void 0===e&&(e=Math.floor(10*i.time)/10),!this.editor)return n.getDuration()-e<1&&(e=n.getDuration()),void(o=n.endscreensMap[e]=!0);o=n.endscreensMap[e]=J('
").appendTo(n.controls.$endscreensContainer).data("id",t).hover((function(){void 0!==n.endscreenTimeout&&clearTimeout(n.endscreenTimeout),n.controls.$endscreensContainer.children(".h5p-show").removeClass("h5p-show"),o.addClass("h5p-show")}),(function(){n.endscreenTimeout=setTimeout((function(){o.removeClass("h5p-show")}),n.editor?1e3:2e3)})),n.editor&&(n.endscreenTimeout=setTimeout((function(){o.removeClass("h5p-show")}),1e3)),o.find(".h5p-endscreen-label").css("maxWidth",parseInt(n.controls.$slider.parent().css("marginRight"))-35),void 0===n.controls.$endscreensList&&(n.controls.$endscreensList=J('
').insertAfter(n.controls.$endscreensChooser.find("h2")));var s=J('
').concat(i.label," ")).click((function(){return n.onEndscreenSelect(o,i)})).keydown((function(t){P(t)&&n.onEndscreenSelect(o,i),t.stopPropagation()}));n.endscreenMenuKeyboardControls.addElement(s.get(0));var r=n.controls.$endscreensList.children(":eq("+t+")");return 0!==r.length?s.insertBefore(r):s.appendTo(n.controls.$endscreensList),n.on("endscreensChanged",(function(i){var r=i.data.index,a=i.data.number;r===t&&a<0?(s.remove(),delete n.endscreensMap[e]):t>=r&&(t+=a,o.data("id",t))})),n.trigger("endscreenAdded",{endscreen:o}),o},Z.prototype.attachControls=function(t){var e=this,o=J("
",{class:"h5p-controls-left",appendTo:t});e.$sliderContainer=J("
",{class:"h5p-control h5p-slider",appendTo:t}),e.hasStar&&(e.$star=J("
",{class:"h5p-control h5p-star",appendTo:t}),e.$starBar=J("
",{class:"h5p-control h5p-star h5p-star-bar",appendTo:e.$star}),J("
",{class:"h5p-control h5p-star h5p-star-background",appendTo:e.$star}),e.$starAnimation=J("
",{class:"h5p-control h5p-star h5p-star-animation h5p-star-animation-inactive",appendTo:e.$star}));var n=J("
",{class:"h5p-controls-right",appendTo:t});"both"===e.preventSkippingMode&&e.setDisabled(e.$sliderContainer),e.controls={},e.controls.$play=e.createButton("play","h5p-control h5p-pause",o,e.togglePlayPause),e.showRewind10&&(e.controls.$rewind10=e.createButton("rewind10","h5p-control",o,(function(){if(e.video.getCurrentTime()>0){var t=Math.max(e.video.getCurrentTime()-10,0);e.seek(t),e.currentState===H5P.Video.PAUSED&&e.timeUpdate(t),e.currentState===H5P.Video.ENDED&&e.video.play()}})));var i=function(t,o){return function(){var n=e.controls[t],i=e.controls[o],s="true"===n.attr("aria-disabled"),r="true"===n.attr("aria-expanded");s||(r?(n.attr("aria-expanded","false"),i.hasClass("h5p-show")||n.focus(),i.removeClass("h5p-show"),function(){var t=e.$container.hasClass("h5p-minimal")&&"true"===e.controls.$more.attr("aria-expanded");t&&e.controls.$more.click()}(),e.resumeVideo()):(n.attr("aria-expanded","true"),i.addClass("h5p-show"),i.find('[tabindex="0"]').focus(),e.closePopupMenus(n)))}},s=e.editor||e.displayBookmarks();if(s&&(e.controls.$bookmarksChooser=H5P.jQuery("
",{class:"h5p-chooser h5p-bookmarks",role:"dialog",html:'")}),e.popupMenuChoosers.push(e.controls.$bookmarksChooser),e.controls.$bookmarksChooser.append(J("
",{role:"button",class:"h5p-chooser-close-button",tabindex:"0","aria-label":e.l10n.close,click:function(){return e.toggleBookmarksChooser()},keydown:function(t){P(t)&&(e.toggleBookmarksChooser(),t.preventDefault())}})),e.showRewind10&&e.controls.$bookmarksChooser.addClass("h5p-rewind-displacement"),e.controls.$bookmarksButton=e.createButton("bookmarks","h5p-control",o,(function(){e.toggleBookmarksChooser()})),e.controls.$bookmarksButton.attr("aria-haspopup","true"),e.controls.$bookmarksButton.attr("aria-expanded","false"),e.controls.$bookmarksChooser.insertAfter(e.controls.$bookmarksButton),e.controls.$bookmarksChooser.bind("transitionend",(function(){e.controls.$bookmarksChooser.removeClass("h5p-transitioning")})),e.popupMenuButtons.push(e.controls.$bookmarksButton)),e.hasStar){var r=e.editor?"star h5p-star-foreground-editor":"star h5p-star-foreground",a=e.editor?function(){return e.toggleEndscreensChooser()}:function(){return e.toggleEndscreen()};e.controls.$endscreensButton=e.createButton(r,"h5p-control",e.$star,a),e.controls.$endscreensButton.attr("aria-label",e.l10n.summary).attr("aria-haspopup","dialog").attr("aria-expanded","false"),e.popupMenuButtons.push(e.controls.$endscreensButton)}e.editor&&(e.controls.$endscreensChooser=H5P.jQuery("
",{class:"h5p-chooser h5p-endscreens",role:"dialog",html:'")}),e.popupMenuChoosers.push(e.controls.$endscreensChooser),e.controls.$endscreensChooser.append(J("",{role:"button",class:"h5p-chooser-close-button",tabindex:"0","aria-label":e.l10n.close,click:function(){return e.toggleEndscreensChooser()},keydown:function(t){P(t)&&(e.toggleEndscreensChooser(),t.preventDefault())}})),e.hasStar&&(e.controls.$endscreensButton.attr("aria-haspopup","true").attr("aria-expanded","false"),e.controls.$endscreensChooser.insertAfter(e.controls.$endscreensButton).bind("transitionend",(function(){e.controls.$endscreensChooser.removeClass("h5p-transitioning")}))));var c='\n \n 0:00 \n ',l=J(''.concat(c,"
")).appendTo(o);e.controls.$currentTimeSimple=l.find(".human-time"),e.controls.$currentTimeA11ySimple=l.find(".hidden-but-read");var d=Z.formatTimeForA11y(0,e.l10n),u=J('\n '.concat(c,'\n / \n \n ').concat(e.l10n.totalTime," ").concat(d,' \n 0:00 \n \n
')).appendTo(n),h=u.find(".h5p-current");e.controls.$currentTime=h.find(".human-time"),e.controls.$currentTimeA11y=h.find(".hidden-but-read"),e.controls.$totalTime=u.find(".h5p-total"),e.updateCurrentTime(0);var p=function(){e.controls.$minimalOverlay.removeClass("h5p-show"),e.controls.$more.attr("aria-expanded","false"),e.controls.$more.focus(),setTimeout((function(){e.controls.$overlayButtons.removeClass("h5p-hide")}),150)};e.controls.$more=e.createButton("more","h5p-control",n,(function(){"true"===e.controls.$more.attr("aria-expanded")?p():(e.controls.$minimalOverlay.addClass("h5p-show"),e.controls.$more.attr("aria-expanded","true"),e.removeSplash(),setTimeout((function(){e.controls.$minimalOverlay.find('[tabindex="0"]').focus()}),150)),e.closePopupMenus()})),e.controls.$playbackRateChooser=H5P.jQuery("
",{class:"h5p-chooser h5p-playbackRate",role:"dialog",html:'")}),e.popupMenuChoosers.push(e.controls.$playbackRateChooser);var m=function(){e.isMinimal?e.controls.$more.click():e.controls.$playbackRateButton.click(),e.resumeVideo()};e.controls.$playbackRateChooser.append(J("",{role:"button",class:"h5p-chooser-close-button",tabindex:"0","aria-label":e.l10n.close,click:function(){return m()},keydown:function(t){P(t)&&(m(),t.preventDefault())}})),e.controls.$playbackRateButton=e.createButton("playbackRate","h5p-control",n,i("$playbackRateButton","$playbackRateChooser")),e.popupMenuButtons.push(e.controls.$playbackRateButton),e.setDisabled(e.controls.$playbackRateButton),e.controls.$playbackRateButton.attr("aria-haspopup","true"),e.controls.$playbackRateButton.attr("aria-expanded","false"),e.controls.$playbackRateChooser.insertAfter(e.controls.$playbackRateButton),-1===navigator.userAgent.indexOf("Android")&&-1===navigator.userAgent.indexOf("iPad")&&(e.controls.$volume=e.createButton("mute","h5p-control",n,e.toggleMute),e.deactivateSound&&(e.controls.$volume.addClass("h5p-muted").attr("aria-label",e.l10n.sndDisabled),e.setDisabled(e.controls.$volume))),e.deactivateSound&&e.video.mute(),e.video.isMuted()&&e.controls.$volume.addClass("h5p-muted").attr("aria-label",e.l10n.sndDisabled),e.controls.$qualityChooser=H5P.jQuery("
",{class:"h5p-chooser h5p-quality",role:"dialog",html:'")}),e.popupMenuChoosers.push(e.controls.$qualityChooser);var v=function(){e.isMinimal?e.controls.$more.click():e.controls.$qualityButton.click(),e.resumeVideo()};e.controls.$qualityChooser.append(J("",{role:"button",class:"h5p-chooser-close-button",tabindex:"0","aria-label":e.l10n.close,click:function(){return v()},keydown:function(t){P(t)&&(v(),t.preventDefault())}})),e.controls.$qualityButton=e.createButton("quality","h5p-control",n,i("$qualityButton","$qualityChooser")),e.popupMenuButtons.push(e.controls.$qualityButton),e.setDisabled(e.controls.$qualityButton),e.controls.$qualityButton.attr("aria-haspopup","true"),e.controls.$qualityButton.attr("aria-expanded","false"),e.controls.$qualityChooser.insertAfter(e.controls.$qualityButton),e.editor||!1===H5P.fullscreenSupported||(e.controls.$fullscreen=e.createButton("fullscreen","h5p-control",n,(function(){e.toggleFullScreen()}))),e.controls.$minimalOverlay=H5P.jQuery("
",{class:"h5p-minimal-overlay",appendTo:e.$container});var f=H5P.jQuery("
",{role:"menu",class:"h5p-minimal-wrap",appendTo:e.controls.$minimalOverlay});e.minimalMenuKeyboardControls=new b([new g]),e.minimalMenuKeyboardControls.on("close",(function(){return p()})),e.controls.$overlayButtons=H5P.jQuery([]),s&&(e.controls.$bookmarkButtonMinimal=e.createButton("bookmarks","h5p-minimal-button",f,(function(){e.controls.$overlayButtons.addClass("h5p-hide"),e.toggleBookmarksChooser(!0)}),!0),e.controls.$bookmarkButtonMinimal.attr("role","menuitem"),e.controls.$bookmarkButtonMinimal.attr("tabindex","-1"),e.controls.$overlayButtons=e.controls.$overlayButtons.add(e.controls.$bookmarkButtonMinimal),e.minimalMenuKeyboardControls.addElement(e.controls.$bookmarkButtonMinimal.get(0))),e.controls.$qualityButtonMinimal=e.createButton("quality","h5p-minimal-button",f,(function(){e.isDisabled(e.controls.$qualityButton)||(e.controls.$overlayButtons.addClass("h5p-hide"),e.controls.$qualityButton.click())}),!0),e.setDisabled(e.controls.$qualityButtonMinimal),e.controls.$qualityButtonMinimal.attr("role","menuitem"),e.controls.$overlayButtons=e.controls.$overlayButtons.add(e.controls.$qualityButtonMinimal),e.minimalMenuKeyboardControls.addElement(e.controls.$qualityButtonMinimal.get(0)),e.controls.$playbackRateButtonMinimal=e.createButton("playbackRate","h5p-minimal-button",f,(function(){e.isDisabled(e.controls.$playbackRateButton)||(e.controls.$overlayButtons.addClass("h5p-hide"),e.controls.$playbackRateButton.click())}),!0),e.controls.$playbackRateButtonMinimal.attr("role","menuitem"),e.setDisabled(e.controls.$playbackRateButtonMinimal),e.controls.$overlayButtons=e.controls.$overlayButtons.add(e.controls.$playbackRateButtonMinimal),e.minimalMenuKeyboardControls.addElement(e.controls.$playbackRateButtonMinimal.get(0)),e.addQualityChooser(),e.addPlaybackRateChooser(),e.interactionKeyboardControls=new b([new g]),e.controls.$interactionsContainer=J("
",{role:"menu",class:"h5p-interactions-container","aria-label":e.l10n.interaction}),e.controls.$bookmarksContainer=J("
",{class:"h5p-bookmarks-container",appendTo:e.$sliderContainer}),e.controls.$endscreensContainer=J("
",{class:"h5p-endscreens-container",appendTo:e.$sliderContainer}),e.hasPlayPromise=!1,e.hasQueuedPause=!1,e.delayed=!1,e.controls.$slider=J("
",{appendTo:e.$sliderContainer}).slider({value:0,step:.01,orientation:"horizontal",range:"min",max:0,create:function(t){var o=J(t.target).find(".ui-slider-handle");o.attr("role","slider").attr("aria-valuemin","0").attr("aria-valuemax",e.getDuration().toString()).attr("aria-valuetext",Z.formatTimeForA11y(0,e.l10n)).attr("aria-valuenow","0").attr("aria-label",e.l10n.videoProgressBar).attr("tabindex","0"),"both"===e.preventSkippingMode&&e.setDisabled(o).attr("aria-hidden","true")},start:function(){e.currentState!==Z.SEEKING&&(e.toggleEndscreen(!1),e.delayedState||(e.currentState===H5P.Video.ENDED?e.lastState=H5P.Video.PLAYING:e.currentState===H5P.Video.BUFFERING&&e.lastState||(e.lastState=e.currentState)),e.delayedState=!0,clearTimeout(e.delayTimeout),e.delayTimeout=setTimeout((function(){e.delayedState=!1}),200),e.hasPlayPromise?e.hasQueuedPause=!0:e.video.pause(),e.currentState=Z.SEEKING,e.removeSplash(),e.$overlay.addClass("h5p-visible"))},slide:function(t,o){var n=x(t,[C.ARROW_LEFT,C.ARROW_RIGHT]),i=o.value;if(e.isSkippingProhibited(i)&&(i=e.maxTimeReached),n){var s=x(t,[C.ARROW_RIGHT]),r=e.getDuration();i=s?Math.min(i+5,r):Math.max(i-5,0),e.isSkippingProhibited(i)&&(i=e.maxTimeReached),e.timeUpdate(i,!0)}return e.seek(i),e.updateInteractions(i),e.updateCurrentTime(i),!n},stop:function(t,o){e.currentState=e.lastState;var n=e.isSkippingProhibited(o.value)?e.maxTimeReached:o.value;e.seek(n),e.recreateCurrentInteractions();var i=e.lastState===H5P.Video.PLAYING||e.lastState===H5P.Video.VIDEO_CUED||e.hasQueuedPause;if(e.hasPlayPromise)e.hasQueuedPause=!1;else if(i){e.hasQueuedPause=!1;var s=e.video.play();e.hasQueuedPause=!1,s&&s.then?(e.hasPlayPromise=!0,s.then((function(){setTimeout((function(){(e.hasQueuedPause||e.hasActivePauseInteraction())&&e.video.pause(),e.hasPlayPromise=!1}),0)}))):e.hasActivePauseInteraction()?(e.video.play(),setTimeout((function(){e.video.pause()}),50)):e.timeUpdate(n)}else e.timeUpdate(n);e.isSkippingProhibited(o.value)&&(e.showPreventSkippingMessage({x:o.value/e.video.getDuration()*e.controls.$slider.get(0).offsetWidth},e.l10n.navForwardDisabled),e.setSliderPosition(n)),e.$overlay.removeClass("h5p-visible"),e.editor&&(e.updateChooserTime(e.controls.$bookmarksChooser,".h5p-add-bookmark"),e.updateChooserTime(e.controls.$endscreensChooser,".h5p-add-endscreen"))}}),e.controls.$interactionsContainer.appendTo(e.$sliderContainer),"both"===e.preventSkippingMode&&(e.controls.$slider.slider("disable"),e.controls.$slider.parent().click((function(t){var o=e.menuitems.map((function(t){return t.get(0)})).includes(t.target)?t.target.offsetLeft:t.offsetX;return e.showPreventSkippingMessage({x:o,y:-13}),!1}))),e.displayBookmarks()&&e.showBookmarksmenuOnLoad&&!1===e.video.pressToPlay&&e.toggleBookmarksChooser(!0,{initialLoad:!0}),e.controls.$buffered=J("
",{class:"h5p-buffered",prependTo:e.controls.$slider})},Z.prototype.playStarAnimation=function(){var t=this;this.$starAnimation.hasClass("h5p-star-animation-inactive")&&(this.$starAnimation.removeClass("h5p-star-animation-inactive").addClass("h5p-star-animation-active"),setTimeout((function(){t.$starAnimation.removeClass("h5p-star-animation-active").addClass("h5p-star-animation-inactive")}),1e3))},Z.prototype.playBubbleAnimation=function(t){this.bubbleScore.setContent(t),this.bubbleScore.animate()},Z.prototype.hasActivePauseInteraction=function(){var t=!1;return this.interactions.forEach((function(e){e.getElement()&&e.pause()&&(t=!0)})),t},Z.prototype.createButton=function(t,e,o,n,i){var s={role:"button",tabindex:0,class:(void 0===e?"":e+" ")+"h5p-"+t,on:{click:function(){n.call(this)},keydown:function(t){P(t)&&(n.call(this),t.preventDefault(),t.stopPropagation())}},appendTo:o};return s[i?"text":"aria-label"]=this.l10n[t],H5P.jQuery("
",s)},Z.prototype.addQualityChooser=function(){var t,e=this;if(e.qualityMenuKeyboardControls=new b([new g]),e.qualityMenuKeyboardControls.on("close",(function(){return e.controls.$qualityButton.click()})),this.video.getQualities&&(e.qualities=this.video.getQualities(),e.qualities&&void 0!==(null===(t=this.controls)||void 0===t?void 0:t.$qualityButton)&&e.isDisabled(e.controls.$qualityButton))){var o=this.video.getQuality(),n=e.qualities;"YouTube"===this.video.getHandlerName()&&(n=n.filter((function(t){return t.name===o})));for(var i="",s=0;s').concat(r.label,"")}var c=J('")).appendTo(this.controls.$qualityChooser);c.children().click((function(){var t=J(this).attr("data-quality");e.updateQuality(t)})).keydown((function(t){if(P(t)){var o=J(this).attr("data-quality");e.updateQuality(o)}t.stopPropagation()})),c.find("li").get().forEach((function(t){e.qualityMenuKeyboardControls.addElement(t);var o="true"===t.getAttribute("aria-checked");tt(t,o)})),e.removeDisabled(this.controls.$qualityButton.add(this.controls.$qualityButtonMinimal))}},Z.prototype.updateQuality=function(t){var e=this;e.video.setQuality(t),"true"===e.controls.$more.attr("aria-expanded")?e.controls.$more.click():(e.controls.$qualityButton.click(),e.controls.$qualityButton.focus())},Z.prototype.addPlaybackRateChooser=function(){var t,e=this,o=this;if(this.playbackRateMenuKeyboardControls=new b([new g]),this.playbackRateMenuKeyboardControls.on("close",(function(){return o.controls.$playbackRateButton.click()})),this.video.getPlaybackRates){var n=this.video.getPlaybackRates();if(!(n.length<2)&&n&&void 0!==(null===(t=this.controls)||void 0===t?void 0:t.$playbackRateButton)&&o.isDisabled(this.controls.$playbackRateButton)){for(var i=this.video.getPlaybackRate(),s="",r=0;r').concat(a,"")}var l=J('").appendTo(this.controls.$playbackRateChooser);l.children().click((function(){var t=J(this).attr("playback-rate");o.updatePlaybackRate(t)})).keydown((function(t){if(P(t)){var e=J(this).attr("playback-rate");o.updatePlaybackRate(e)}t.stopPropagation()})),l.find("li").get().forEach((function(t){e.playbackRateMenuKeyboardControls.addElement(t);var o="true"===t.getAttribute("aria-checked");tt(t,o)})),o.removeDisabled(this.controls.$playbackRateButton.add(this.controls.$playbackRateButtonMinimal))}}},Z.prototype.updatePlaybackRate=function(t){var e=this;e.video.setPlaybackRate(t),"true"===e.controls.$more.attr("aria-expanded")?e.controls.$more.click():e.controls.$playbackRateButton.click()},Z.prototype.startUpdatingBufferBar=function(){var t=this;if(!t.bufferLoop){!function e(){var o=t.video.getBuffered();o&&t.controls.$buffered&&(t.controls.$buffered.css("width",o+"%"),t.hasStar&&(o>99?t.$starBar.addClass("h5p-star-bar-buffered"):t.$starBar.removeClass("h5p-star-bar-buffered"))),t.bufferLoop=setTimeout(e,500)}()}},Z.prototype.resize=function(){if(this.$container){var t,e,o=this.$container.hasClass("h5p-fullscreen")||this.$container.hasClass("h5p-semi-fullscreen");this.$videoWrapper.css({marginTop:"",marginLeft:"",width:"",height:""}),this.video.trigger("resize");var n=this.justVideo?0:this.$controls.height(),i=this.$container.height();if(o){if((e=this.$videoWrapper.height())+n<=i)this.$videoWrapper.css("marginTop",(i-n-e)/2),t=this.$videoWrapper.width(),void 0!==this.bubbleEndscreen&&this.bubbleEndscreen.fullscreen(!0,i,e);else{var s=i-n;t=s*(this.$videoWrapper.width()/e),this.$videoWrapper.css({marginLeft:(this.$container.width()-t)/2,width:t,height:s}),void 0!==this.bubbleEndscreen&&this.bubbleEndscreen.fullscreen(!0)}this.video.trigger("resize")}else t=this.$container.width(),void 0!==this.bubbleEndscreen&&this.bubbleEndscreen.fullscreen();this.scaledFontSize=t>this.width?this.fontSize*(t/this.width):this.fontSize,this.$container.css("fontSize",this.scaledFontSize+"px"),this.editor||(t=0&&(this.maxTimeReached=Math.max(this.maxTimeReached,t),this.setSliderPosition(t)),o.updateInteractions(t),e||(this.timeUpdateTimeout=window.setTimeout((function(){(o.currentState===H5P.Video.PLAYING||o.currentState===H5P.Video.BUFFERING&&o.lastState===H5P.Video.PLAYING)&&o.timeUpdate(o.video.getCurrentTime())}),40))},Z.prototype.setSliderPosition=function(t){try{var e=this.controls.$slider.find(".ui-slider-handle"),o=Z.formatTimeForA11y(t,this.l10n);this.controls.$slider.slider("option","value",t),e.attr("aria-valuetext",o),e.attr("aria-valuenow",t.toString())}catch(t){return}},Z.prototype.updateInteractions=function(t){var e=this,o=Math.floor(10*t)/10;if(o!==e.lastTenth){void 0!==e.bookmarksMap&&void 0!==e.bookmarksMap[o]&&e.bookmarksMap[o].mouseover().mouseout();void 0!==e.endscreensMap&&void 0!==e.endscreensMap[o]&&e.currentState!==Z.SEEKING&&e.getAnsweredTotal()>0&&e.toggleEndscreen(!0)}e.lastTenth=o,e.toggleInteractions(t);var n=Math.floor(t);n!==e.lastSecond&&(e.currentState!==H5P.Video.PLAYING&&e.currentState!==H5P.Video.PAUSED||e.updateCurrentTime(n)),e.lastSecond=n},Z.prototype.updateCurrentTime=function(t){var e;if(null!==(e=this.controls)&&void 0!==e&&e.$currentTime){var o=this;t=Math.max(t,0);var n=Z.humanizeTime(t),i=Z.formatTimeForA11y(t,o.l10n);o.controls.$currentTime.html(n),o.controls.$currentTimeA11y.html("".concat(o.l10n.currentTime," ").concat(i)),o.controls.$currentTimeSimple.html(n),o.controls.$currentTimeA11ySimple.html("".concat(o.l10n.currentTime," ").concat(i))}},Z.prototype.getUsersScore=function(){for(var t=0,e=0;e0&&void 0!==arguments[0]?arguments[0]:".h5p-dialog-wrapper",e=this,o=e.$container.find(t);e.$tabbables=e.$container.find("a[href], area[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), iframe, object, embed, *[tabindex], *[contenteditable]").filter((function(){var t=J(this),e=J.contains(o.get(0),t.get(0));if(t.data("tabindex"))return!0;if(!e){var n=t.attr("tabindex");return t.data("tabindex",n),t.attr("tabindex","-1"),!0}return!1}))},Z.prototype.restoreTabIndexes=function(t){var e=this;e.$tabbables&&(e.$tabbables.each((function(){var e=J(this),o=e.data("tabindex");if(t&&!J.contains(t.get(0),e.get(0)))return!0;e.hasClass("ui-slider-handle")?(e.attr("tabindex",0),e.removeData("tabindex")):void 0!==o?(e.attr("tabindex",o),e.removeData("tabindex")):e.removeAttr("tabindex")})),t||(e.$tabbables=void 0))},Z.prototype.toggleFocusTrap=function(){var t=this,e=this.getVisibleInteractions().filter((function(t){return t.getRequiresCompletion()&&!t.hasFullScore()}));e.length>0?this.$container.off("focusin").on("focusin",(function(o){return t.trapFocusInInteractions(e,J(o.target))})):this.$container.off("focusin","**")},Z.prototype.trapFocusInInteractions=function(t,e){var o=t.some((function(t){var o=t.getElement();return rt(o,e)})),n=!!this.$mask&&rt(this.$mask,e);if(!o&&!n){var i=t[0].getElement();i&&i.focus()}},Z.prototype.hideOverlayMask=function(){var t=this;return t.restoreTabIndexes(),t.dnb.dialog.closeOverlay(),t.$videoWrapper.removeClass("h5p-disable-opt-out"),t.toggleFocusTrap(),t.$container.find(".h5p-dialog-wrapper")},Z.prototype.showWarningMask=function(){var t=this,e="interactive-video-".concat(t.contentId,"-").concat(t.instanceIndex,"-completion-warning-text");return t.$mask||(t.$mask=J('\n
\n
').concat(t.l10n.requiresCompletionWarning,'
\n
').concat(t.l10n.back," \n
\n
")).click((function(){t.$mask.hide()})).appendTo(t.$container)),t.$mask.show(),t.$mask.find(".h5p-button-back").focus(),t.$mask},Z.prototype.setDisabled=function(t){return t.attr("aria-disabled","true").attr("tabindex","-1")},Z.prototype.isDisabled=function(t){return"true"===t.attr("aria-disabled")},Z.prototype.removeDisabled=function(t){return t.removeAttr("aria-disabled").attr("tabindex","0")},Z.prototype.hasUncompletedRequiredInteractions=function(t){return(void 0!==t?this.getVisibleInteractionsAt(t):this.getVisibleInteractions()).some((function(t){return t.getRequiresCompletion()&&!t.hasFullScore()}))},Z.prototype.getVisibleInteractions=function(){return this.interactions.filter((function(t){return t.isVisible()}))},Z.prototype.getVisibleInteractionsAt=function(t){return this.interactions.filter((function(e){return e.visibleAt(t)}))},Z.prototype.setUserSubmitted=function(t){"boolean"==typeof t&&(this.userSubmitted=t)},Z.prototype.showSolutions=function(){},Z.prototype.getAnswerGiven=function(){return!!this.userSubmitted&&this.interactions.some((function(t){var e,o;return null===(e=(o=t.getInstance()).getAnswerGiven)||void 0===e?void 0:e.call(o)}))},Z.prototype.getTitle=function(){return H5P.createTitle(this.contentData&&this.contentData.metadata&&this.contentData.metadata.title?this.contentData.metadata.title:"Interactive Video")},Z.prototype.findNextInteractionToShow=function(t,e){for(var o,n=0;nt||i.from==t&&(void 0===e||n>e))&&(void 0===o||i.from0&&void 0!==arguments[0]?arguments[0]:0,e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};this.isSkippingProhibited(t)&&!e.force||(this.maxTimeReached=Math.max(this.maxTimeReached,t),this.nextInteractionToShow=this.nextInteractionToHide=void 0,this.video.seek(t))},Z.prototype.pause=function(){this.video&&this.video.pause&&this.video.pause()},Z.prototype.resetTask=function(){var t;this.interactionsProgress=[],this.dnb&&(this.dnb.calledFromResetTask=!0);for(var e=0;e0&&t.options.video.textTracks.videoTrack.forEach((function(t){t.track&&t.track.copyright&&e.addMedia(new H5P.MediaCopyright(t.track.copyright))})),e},Z.prototype.isSkippingProhibited=function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:0;return!this.editor&&("both"===this.preventSkippingMode||"none"!==this.preventSkippingMode&&this.maxTimeReached0?"".concat(o.hours," ").concat(e.hours,", "):"";return"".concat(n).concat(o.minutes," ").concat(e.minutes,", ").concat(o.seconds," ").concat(e.seconds)},Z.secondsToMinutesAndHours=function(t){var e=Math.floor(t/60);return{seconds:Math.floor(t%60),minutes:e%60,hours:Math.floor(e/60)}};var tt=function(t,e){e?t.setAttribute("tabindex","0"):t.removeAttribute("tabindex")},et=function(t,e){for(var o=0;o",{class:"h5p-video-info",text:t.l10n.howToCreateInteractions}).appendTo(t.$videoWrapper)),"VimeoPlayer"!==t.video.getHandlerName()||t.$videoPlaybutton||(t.$videoPlaybutton=J("",{class:"h5p-play-button"}).appendTo(t.$videoWrapper)),t.$videoInfo.show(),t.$videoPlaybutton&&t.$videoPlaybutton.show()):(t.$videoWrapper.removeClass("heart-beat info-text"),t.$videoInfo&&t.$videoInfo.hide(),t.$videoPlaybutton&&t.$videoPlaybutton.hide(),t.editor.startGuidedTour()))};var it=function(t){var e=t.getVerifiedStatementValue(["object","definition"]);H5P.jQuery.extend(e,st())},st=function(){var t={interactionType:"compound",type:"http://adlnet.gov/expapi/activities/cmi.interaction",description:{"en-US":""}};return t},rt=function(t,e){return void 0!==t&&void 0!==e&&(t.is(e)||J.contains(t.get(0),e.get(0)))},at=function(t){return t.map((function(t){return t.getXAPIData()})).filter((function(t){return!!t}))};const ct=Z;H5P=H5P||{},H5P.InteractiveVideo=ct})();;