", {
title: item.element.attr( "title" )
} );
if ( item.disabled ) {
this._addClass( li, null, "ui-state-disabled" );
}
this._setText( wrapper, item.label );
return li.append( wrapper ).appendTo( ul );
},
_setText: function( element, value ) {
if ( value ) {
element.text( value );
} else {
element.html( " " );
}
},
_move: function( direction, event ) {
var item, next,
filter = ".ui-menu-item";
if ( this.isOpen ) {
item = this.menuItems.eq( this.focusIndex ).parent( "li" );
} else {
item = this.menuItems.eq( this.element[ 0 ].selectedIndex ).parent( "li" );
filter += ":not(.ui-state-disabled)";
}
if ( direction === "first" || direction === "last" ) {
next = item[ direction === "first" ? "prevAll" : "nextAll" ]( filter ).eq( -1 );
} else {
next = item[ direction + "All" ]( filter ).eq( 0 );
}
if ( next.length ) {
this.menuInstance.focus( event, next );
}
},
_getSelectedItem: function() {
return this.menuItems.eq( this.element[ 0 ].selectedIndex ).parent( "li" );
},
_toggle: function( event ) {
this[ this.isOpen ? "close" : "open" ]( event );
},
_setSelection: function() {
var selection;
if ( !this.range ) {
return;
}
if ( window.getSelection ) {
selection = window.getSelection();
selection.removeAllRanges();
selection.addRange( this.range );
// Support: IE8
} else {
this.range.select();
}
// Support: IE
// Setting the text selection kills the button focus in IE, but
// restoring the focus doesn't kill the selection.
this.button.focus();
},
_documentClick: {
mousedown: function( event ) {
if ( !this.isOpen ) {
return;
}
if ( !$( event.target ).closest( ".ui-selectmenu-menu, #" +
$.escapeSelector( this.ids.button ) ).length ) {
this.close( event );
}
}
},
_buttonEvents: {
// Prevent text selection from being reset when interacting with the selectmenu (#10144)
mousedown: function() {
var selection;
if ( window.getSelection ) {
selection = window.getSelection();
if ( selection.rangeCount ) {
this.range = selection.getRangeAt( 0 );
}
// Support: IE8
} else {
this.range = document.selection.createRange();
}
},
click: function( event ) {
this._setSelection();
this._toggle( event );
},
keydown: function( event ) {
var preventDefault = true;
switch ( event.keyCode ) {
case $.ui.keyCode.TAB:
case $.ui.keyCode.ESCAPE:
this.close( event );
preventDefault = false;
break;
case $.ui.keyCode.ENTER:
if ( this.isOpen ) {
this._selectFocusedItem( event );
}
break;
case $.ui.keyCode.UP:
if ( event.altKey ) {
this._toggle( event );
} else {
this._move( "prev", event );
}
break;
case $.ui.keyCode.DOWN:
if ( event.altKey ) {
this._toggle( event );
} else {
this._move( "next", event );
}
break;
case $.ui.keyCode.SPACE:
if ( this.isOpen ) {
this._selectFocusedItem( event );
} else {
this._toggle( event );
}
break;
case $.ui.keyCode.LEFT:
this._move( "prev", event );
break;
case $.ui.keyCode.RIGHT:
this._move( "next", event );
break;
case $.ui.keyCode.HOME:
case $.ui.keyCode.PAGE_UP:
this._move( "first", event );
break;
case $.ui.keyCode.END:
case $.ui.keyCode.PAGE_DOWN:
this._move( "last", event );
break;
default:
this.menu.trigger( event );
preventDefault = false;
}
if ( preventDefault ) {
event.preventDefault();
}
}
},
_selectFocusedItem: function( event ) {
var item = this.menuItems.eq( this.focusIndex ).parent( "li" );
if ( !item.hasClass( "ui-state-disabled" ) ) {
this._select( item.data( "ui-selectmenu-item" ), event );
}
},
_select: function( item, event ) {
var oldIndex = this.element[ 0 ].selectedIndex;
// Change native select element
this.element[ 0 ].selectedIndex = item.index;
this.buttonItem.replaceWith( this.buttonItem = this._renderButtonItem( item ) );
this._setAria( item );
this._trigger( "select", event, { item: item } );
if ( item.index !== oldIndex ) {
this._trigger( "change", event, { item: item } );
}
this.close( event );
},
_setAria: function( item ) {
var id = this.menuItems.eq( item.index ).attr( "id" );
this.button.attr( {
"aria-labelledby": id,
"aria-activedescendant": id
} );
this.menu.attr( "aria-activedescendant", id );
},
_setOption: function( key, value ) {
if ( key === "icons" ) {
var icon = this.button.find( "span.ui-icon" );
this._removeClass( icon, null, this.options.icons.button )
._addClass( icon, null, value.button );
}
this._super( key, value );
if ( key === "appendTo" ) {
this.menuWrap.appendTo( this._appendTo() );
}
if ( key === "width" ) {
this._resizeButton();
}
},
_setOptionDisabled: function( value ) {
this._super( value );
this.menuInstance.option( "disabled", value );
this.button.attr( "aria-disabled", value );
this._toggleClass( this.button, null, "ui-state-disabled", value );
this.element.prop( "disabled", value );
if ( value ) {
this.button.attr( "tabindex", -1 );
this.close();
} else {
this.button.attr( "tabindex", 0 );
}
},
_appendTo: function() {
var element = this.options.appendTo;
if ( element ) {
element = element.jquery || element.nodeType ?
$( element ) :
this.document.find( element ).eq( 0 );
}
if ( !element || !element[ 0 ] ) {
element = this.element.closest( ".ui-front, dialog" );
}
if ( !element.length ) {
element = this.document[ 0 ].body;
}
return element;
},
_toggleAttr: function() {
this.button.attr( "aria-expanded", this.isOpen );
// We can't use two _toggleClass() calls here, because we need to make sure
// we always remove classes first and add them second, otherwise if both classes have the
// same theme class, it will be removed after we add it.
this._removeClass( this.button, "ui-selectmenu-button-" +
( this.isOpen ? "closed" : "open" ) )
._addClass( this.button, "ui-selectmenu-button-" +
( this.isOpen ? "open" : "closed" ) )
._toggleClass( this.menuWrap, "ui-selectmenu-open", null, this.isOpen );
this.menu.attr( "aria-hidden", !this.isOpen );
},
_resizeButton: function() {
var width = this.options.width;
// For `width: false`, just remove inline style and stop
if ( width === false ) {
this.button.css( "width", "" );
return;
}
// For `width: null`, match the width of the original element
if ( width === null ) {
width = this.element.show().outerWidth();
this.element.hide();
}
this.button.outerWidth( width );
},
_resizeMenu: function() {
this.menu.outerWidth( Math.max(
this.button.outerWidth(),
// Support: IE10
// IE10 wraps long text (possibly a rounding bug)
// so we add 1px to avoid the wrapping
this.menu.width( "" ).outerWidth() + 1
) );
},
_getCreateOptions: function() {
var options = this._super();
options.disabled = this.element.prop( "disabled" );
return options;
},
_parseOptions: function( options ) {
var that = this,
data = [];
options.each( function( index, item ) {
if ( item.hidden ) {
return;
}
data.push( that._parseOption( $( item ), index ) );
} );
this.items = data;
},
_parseOption: function( option, index ) {
var optgroup = option.parent( "optgroup" );
return {
element: option,
index: index,
value: option.val(),
label: option.text(),
optgroup: optgroup.attr( "label" ) || "",
disabled: optgroup.prop( "disabled" ) || option.prop( "disabled" )
};
},
_destroy: function() {
this._unbindFormResetHandler();
this.menuWrap.remove();
this.button.remove();
this.element.show();
this.element.removeUniqueId();
this.labels.attr( "for", this.ids.element );
}
} ] );
/*!
* jQuery UI Slider 1.13.0
* http://jqueryui.com
*
* Copyright jQuery Foundation and other contributors
* Released under the MIT license.
* http://jquery.org/license
*/
//>>label: Slider
//>>group: Widgets
//>>description: Displays a flexible slider with ranges and accessibility via keyboard.
//>>docs: http://api.jqueryui.com/slider/
//>>demos: http://jqueryui.com/slider/
//>>css.structure: ../../themes/base/core.css
//>>css.structure: ../../themes/base/slider.css
//>>css.theme: ../../themes/base/theme.css
var widgetsSlider = $.widget( "ui.slider", $.ui.mouse, {
version: "1.13.0",
widgetEventPrefix: "slide",
options: {
animate: false,
classes: {
"ui-slider": "ui-corner-all",
"ui-slider-handle": "ui-corner-all",
// Note: ui-widget-header isn't the most fittingly semantic framework class for this
// element, but worked best visually with a variety of themes
"ui-slider-range": "ui-corner-all ui-widget-header"
},
distance: 0,
max: 100,
min: 0,
orientation: "horizontal",
range: false,
step: 1,
value: 0,
values: null,
// Callbacks
change: null,
slide: null,
start: null,
stop: null
},
// Number of pages in a slider
// (how many times can you page up/down to go through the whole range)
numPages: 5,
_create: function() {
this._keySliding = false;
this._mouseSliding = false;
this._animateOff = true;
this._handleIndex = null;
this._detectOrientation();
this._mouseInit();
this._calculateNewMax();
this._addClass( "ui-slider ui-slider-" + this.orientation,
"ui-widget ui-widget-content" );
this._refresh();
this._animateOff = false;
},
_refresh: function() {
this._createRange();
this._createHandles();
this._setupEvents();
this._refreshValue();
},
_createHandles: function() {
var i, handleCount,
options = this.options,
existingHandles = this.element.find( ".ui-slider-handle" ),
handle = "
",
handles = [];
handleCount = ( options.values && options.values.length ) || 1;
if ( existingHandles.length > handleCount ) {
existingHandles.slice( handleCount ).remove();
existingHandles = existingHandles.slice( 0, handleCount );
}
for ( i = existingHandles.length; i < handleCount; i++ ) {
handles.push( handle );
}
this.handles = existingHandles.add( $( handles.join( "" ) ).appendTo( this.element ) );
this._addClass( this.handles, "ui-slider-handle", "ui-state-default" );
this.handle = this.handles.eq( 0 );
this.handles.each( function( i ) {
$( this )
.data( "ui-slider-handle-index", i )
.attr( "tabIndex", 0 );
} );
},
_createRange: function() {
var options = this.options;
if ( options.range ) {
if ( options.range === true ) {
if ( !options.values ) {
options.values = [ this._valueMin(), this._valueMin() ];
} else if ( options.values.length && options.values.length !== 2 ) {
options.values = [ options.values[ 0 ], options.values[ 0 ] ];
} else if ( Array.isArray( options.values ) ) {
options.values = options.values.slice( 0 );
}
}
if ( !this.range || !this.range.length ) {
this.range = $( "
" )
.appendTo( this.element );
this._addClass( this.range, "ui-slider-range" );
} else {
this._removeClass( this.range, "ui-slider-range-min ui-slider-range-max" );
// Handle range switching from true to min/max
this.range.css( {
"left": "",
"bottom": ""
} );
}
if ( options.range === "min" || options.range === "max" ) {
this._addClass( this.range, "ui-slider-range-" + options.range );
}
} else {
if ( this.range ) {
this.range.remove();
}
this.range = null;
}
},
_setupEvents: function() {
this._off( this.handles );
this._on( this.handles, this._handleEvents );
this._hoverable( this.handles );
this._focusable( this.handles );
},
_destroy: function() {
this.handles.remove();
if ( this.range ) {
this.range.remove();
}
this._mouseDestroy();
},
_mouseCapture: function( event ) {
var position, normValue, distance, closestHandle, index, allowed, offset, mouseOverHandle,
that = this,
o = this.options;
if ( o.disabled ) {
return false;
}
this.elementSize = {
width: this.element.outerWidth(),
height: this.element.outerHeight()
};
this.elementOffset = this.element.offset();
position = { x: event.pageX, y: event.pageY };
normValue = this._normValueFromMouse( position );
distance = this._valueMax() - this._valueMin() + 1;
this.handles.each( function( i ) {
var thisDistance = Math.abs( normValue - that.values( i ) );
if ( ( distance > thisDistance ) ||
( distance === thisDistance &&
( i === that._lastChangedValue || that.values( i ) === o.min ) ) ) {
distance = thisDistance;
closestHandle = $( this );
index = i;
}
} );
allowed = this._start( event, index );
if ( allowed === false ) {
return false;
}
this._mouseSliding = true;
this._handleIndex = index;
this._addClass( closestHandle, null, "ui-state-active" );
closestHandle.trigger( "focus" );
offset = closestHandle.offset();
mouseOverHandle = !$( event.target ).parents().addBack().is( ".ui-slider-handle" );
this._clickOffset = mouseOverHandle ? { left: 0, top: 0 } : {
left: event.pageX - offset.left - ( closestHandle.width() / 2 ),
top: event.pageY - offset.top -
( closestHandle.height() / 2 ) -
( parseInt( closestHandle.css( "borderTopWidth" ), 10 ) || 0 ) -
( parseInt( closestHandle.css( "borderBottomWidth" ), 10 ) || 0 ) +
( parseInt( closestHandle.css( "marginTop" ), 10 ) || 0 )
};
if ( !this.handles.hasClass( "ui-state-hover" ) ) {
this._slide( event, index, normValue );
}
this._animateOff = true;
return true;
},
_mouseStart: function() {
return true;
},
_mouseDrag: function( event ) {
var position = { x: event.pageX, y: event.pageY },
normValue = this._normValueFromMouse( position );
this._slide( event, this._handleIndex, normValue );
return false;
},
_mouseStop: function( event ) {
this._removeClass( this.handles, null, "ui-state-active" );
this._mouseSliding = false;
this._stop( event, this._handleIndex );
this._change( event, this._handleIndex );
this._handleIndex = null;
this._clickOffset = null;
this._animateOff = false;
return false;
},
_detectOrientation: function() {
this.orientation = ( this.options.orientation === "vertical" ) ? "vertical" : "horizontal";
},
_normValueFromMouse: function( position ) {
var pixelTotal,
pixelMouse,
percentMouse,
valueTotal,
valueMouse;
if ( this.orientation === "horizontal" ) {
pixelTotal = this.elementSize.width;
pixelMouse = position.x - this.elementOffset.left -
( this._clickOffset ? this._clickOffset.left : 0 );
} else {
pixelTotal = this.elementSize.height;
pixelMouse = position.y - this.elementOffset.top -
( this._clickOffset ? this._clickOffset.top : 0 );
}
percentMouse = ( pixelMouse / pixelTotal );
if ( percentMouse > 1 ) {
percentMouse = 1;
}
if ( percentMouse < 0 ) {
percentMouse = 0;
}
if ( this.orientation === "vertical" ) {
percentMouse = 1 - percentMouse;
}
valueTotal = this._valueMax() - this._valueMin();
valueMouse = this._valueMin() + percentMouse * valueTotal;
return this._trimAlignValue( valueMouse );
},
_uiHash: function( index, value, values ) {
var uiHash = {
handle: this.handles[ index ],
handleIndex: index,
value: value !== undefined ? value : this.value()
};
if ( this._hasMultipleValues() ) {
uiHash.value = value !== undefined ? value : this.values( index );
uiHash.values = values || this.values();
}
return uiHash;
},
_hasMultipleValues: function() {
return this.options.values && this.options.values.length;
},
_start: function( event, index ) {
return this._trigger( "start", event, this._uiHash( index ) );
},
_slide: function( event, index, newVal ) {
var allowed, otherVal,
currentValue = this.value(),
newValues = this.values();
if ( this._hasMultipleValues() ) {
otherVal = this.values( index ? 0 : 1 );
currentValue = this.values( index );
if ( this.options.values.length === 2 && this.options.range === true ) {
newVal = index === 0 ? Math.min( otherVal, newVal ) : Math.max( otherVal, newVal );
}
newValues[ index ] = newVal;
}
if ( newVal === currentValue ) {
return;
}
allowed = this._trigger( "slide", event, this._uiHash( index, newVal, newValues ) );
// A slide can be canceled by returning false from the slide callback
if ( allowed === false ) {
return;
}
if ( this._hasMultipleValues() ) {
this.values( index, newVal );
} else {
this.value( newVal );
}
},
_stop: function( event, index ) {
this._trigger( "stop", event, this._uiHash( index ) );
},
_change: function( event, index ) {
if ( !this._keySliding && !this._mouseSliding ) {
//store the last changed value index for reference when handles overlap
this._lastChangedValue = index;
this._trigger( "change", event, this._uiHash( index ) );
}
},
value: function( newValue ) {
if ( arguments.length ) {
this.options.value = this._trimAlignValue( newValue );
this._refreshValue();
this._change( null, 0 );
return;
}
return this._value();
},
values: function( index, newValue ) {
var vals,
newValues,
i;
if ( arguments.length > 1 ) {
this.options.values[ index ] = this._trimAlignValue( newValue );
this._refreshValue();
this._change( null, index );
return;
}
if ( arguments.length ) {
if ( Array.isArray( arguments[ 0 ] ) ) {
vals = this.options.values;
newValues = arguments[ 0 ];
for ( i = 0; i < vals.length; i += 1 ) {
vals[ i ] = this._trimAlignValue( newValues[ i ] );
this._change( null, i );
}
this._refreshValue();
} else {
if ( this._hasMultipleValues() ) {
return this._values( index );
} else {
return this.value();
}
}
} else {
return this._values();
}
},
_setOption: function( key, value ) {
var i,
valsLength = 0;
if ( key === "range" && this.options.range === true ) {
if ( value === "min" ) {
this.options.value = this._values( 0 );
this.options.values = null;
} else if ( value === "max" ) {
this.options.value = this._values( this.options.values.length - 1 );
this.options.values = null;
}
}
if ( Array.isArray( this.options.values ) ) {
valsLength = this.options.values.length;
}
this._super( key, value );
switch ( key ) {
case "orientation":
this._detectOrientation();
this._removeClass( "ui-slider-horizontal ui-slider-vertical" )
._addClass( "ui-slider-" + this.orientation );
this._refreshValue();
if ( this.options.range ) {
this._refreshRange( value );
}
// Reset positioning from previous orientation
this.handles.css( value === "horizontal" ? "bottom" : "left", "" );
break;
case "value":
this._animateOff = true;
this._refreshValue();
this._change( null, 0 );
this._animateOff = false;
break;
case "values":
this._animateOff = true;
this._refreshValue();
// Start from the last handle to prevent unreachable handles (#9046)
for ( i = valsLength - 1; i >= 0; i-- ) {
this._change( null, i );
}
this._animateOff = false;
break;
case "step":
case "min":
case "max":
this._animateOff = true;
this._calculateNewMax();
this._refreshValue();
this._animateOff = false;
break;
case "range":
this._animateOff = true;
this._refresh();
this._animateOff = false;
break;
}
},
_setOptionDisabled: function( value ) {
this._super( value );
this._toggleClass( null, "ui-state-disabled", !!value );
},
//internal value getter
// _value() returns value trimmed by min and max, aligned by step
_value: function() {
var val = this.options.value;
val = this._trimAlignValue( val );
return val;
},
//internal values getter
// _values() returns array of values trimmed by min and max, aligned by step
// _values( index ) returns single value trimmed by min and max, aligned by step
_values: function( index ) {
var val,
vals,
i;
if ( arguments.length ) {
val = this.options.values[ index ];
val = this._trimAlignValue( val );
return val;
} else if ( this._hasMultipleValues() ) {
// .slice() creates a copy of the array
// this copy gets trimmed by min and max and then returned
vals = this.options.values.slice();
for ( i = 0; i < vals.length; i += 1 ) {
vals[ i ] = this._trimAlignValue( vals[ i ] );
}
return vals;
} else {
return [];
}
},
// Returns the step-aligned value that val is closest to, between (inclusive) min and max
_trimAlignValue: function( val ) {
if ( val <= this._valueMin() ) {
return this._valueMin();
}
if ( val >= this._valueMax() ) {
return this._valueMax();
}
var step = ( this.options.step > 0 ) ? this.options.step : 1,
valModStep = ( val - this._valueMin() ) % step,
alignValue = val - valModStep;
if ( Math.abs( valModStep ) * 2 >= step ) {
alignValue += ( valModStep > 0 ) ? step : ( -step );
}
// Since JavaScript has problems with large floats, round
// the final value to 5 digits after the decimal point (see #4124)
return parseFloat( alignValue.toFixed( 5 ) );
},
_calculateNewMax: function() {
var max = this.options.max,
min = this._valueMin(),
step = this.options.step,
aboveMin = Math.round( ( max - min ) / step ) * step;
max = aboveMin + min;
if ( max > this.options.max ) {
//If max is not divisible by step, rounding off may increase its value
max -= step;
}
this.max = parseFloat( max.toFixed( this._precision() ) );
},
_precision: function() {
var precision = this._precisionOf( this.options.step );
if ( this.options.min !== null ) {
precision = Math.max( precision, this._precisionOf( this.options.min ) );
}
return precision;
},
_precisionOf: function( num ) {
var str = num.toString(),
decimal = str.indexOf( "." );
return decimal === -1 ? 0 : str.length - decimal - 1;
},
_valueMin: function() {
return this.options.min;
},
_valueMax: function() {
return this.max;
},
_refreshRange: function( orientation ) {
if ( orientation === "vertical" ) {
this.range.css( { "width": "", "left": "" } );
}
if ( orientation === "horizontal" ) {
this.range.css( { "height": "", "bottom": "" } );
}
},
_refreshValue: function() {
var lastValPercent, valPercent, value, valueMin, valueMax,
oRange = this.options.range,
o = this.options,
that = this,
animate = ( !this._animateOff ) ? o.animate : false,
_set = {};
if ( this._hasMultipleValues() ) {
this.handles.each( function( i ) {
valPercent = ( that.values( i ) - that._valueMin() ) / ( that._valueMax() -
that._valueMin() ) * 100;
_set[ that.orientation === "horizontal" ? "left" : "bottom" ] = valPercent + "%";
$( this ).stop( 1, 1 )[ animate ? "animate" : "css" ]( _set, o.animate );
if ( that.options.range === true ) {
if ( that.orientation === "horizontal" ) {
if ( i === 0 ) {
that.range.stop( 1, 1 )[ animate ? "animate" : "css" ]( {
left: valPercent + "%"
}, o.animate );
}
if ( i === 1 ) {
that.range[ animate ? "animate" : "css" ]( {
width: ( valPercent - lastValPercent ) + "%"
}, {
queue: false,
duration: o.animate
} );
}
} else {
if ( i === 0 ) {
that.range.stop( 1, 1 )[ animate ? "animate" : "css" ]( {
bottom: ( valPercent ) + "%"
}, o.animate );
}
if ( i === 1 ) {
that.range[ animate ? "animate" : "css" ]( {
height: ( valPercent - lastValPercent ) + "%"
}, {
queue: false,
duration: o.animate
} );
}
}
}
lastValPercent = valPercent;
} );
} else {
value = this.value();
valueMin = this._valueMin();
valueMax = this._valueMax();
valPercent = ( valueMax !== valueMin ) ?
( value - valueMin ) / ( valueMax - valueMin ) * 100 :
0;
_set[ this.orientation === "horizontal" ? "left" : "bottom" ] = valPercent + "%";
this.handle.stop( 1, 1 )[ animate ? "animate" : "css" ]( _set, o.animate );
if ( oRange === "min" && this.orientation === "horizontal" ) {
this.range.stop( 1, 1 )[ animate ? "animate" : "css" ]( {
width: valPercent + "%"
}, o.animate );
}
if ( oRange === "max" && this.orientation === "horizontal" ) {
this.range.stop( 1, 1 )[ animate ? "animate" : "css" ]( {
width: ( 100 - valPercent ) + "%"
}, o.animate );
}
if ( oRange === "min" && this.orientation === "vertical" ) {
this.range.stop( 1, 1 )[ animate ? "animate" : "css" ]( {
height: valPercent + "%"
}, o.animate );
}
if ( oRange === "max" && this.orientation === "vertical" ) {
this.range.stop( 1, 1 )[ animate ? "animate" : "css" ]( {
height: ( 100 - valPercent ) + "%"
}, o.animate );
}
}
},
_handleEvents: {
keydown: function( event ) {
var allowed, curVal, newVal, step,
index = $( event.target ).data( "ui-slider-handle-index" );
switch ( event.keyCode ) {
case $.ui.keyCode.HOME:
case $.ui.keyCode.END:
case $.ui.keyCode.PAGE_UP:
case $.ui.keyCode.PAGE_DOWN:
case $.ui.keyCode.UP:
case $.ui.keyCode.RIGHT:
case $.ui.keyCode.DOWN:
case $.ui.keyCode.LEFT:
event.preventDefault();
if ( !this._keySliding ) {
this._keySliding = true;
this._addClass( $( event.target ), null, "ui-state-active" );
allowed = this._start( event, index );
if ( allowed === false ) {
return;
}
}
break;
}
step = this.options.step;
if ( this._hasMultipleValues() ) {
curVal = newVal = this.values( index );
} else {
curVal = newVal = this.value();
}
switch ( event.keyCode ) {
case $.ui.keyCode.HOME:
newVal = this._valueMin();
break;
case $.ui.keyCode.END:
newVal = this._valueMax();
break;
case $.ui.keyCode.PAGE_UP:
newVal = this._trimAlignValue(
curVal + ( ( this._valueMax() - this._valueMin() ) / this.numPages )
);
break;
case $.ui.keyCode.PAGE_DOWN:
newVal = this._trimAlignValue(
curVal - ( ( this._valueMax() - this._valueMin() ) / this.numPages ) );
break;
case $.ui.keyCode.UP:
case $.ui.keyCode.RIGHT:
if ( curVal === this._valueMax() ) {
return;
}
newVal = this._trimAlignValue( curVal + step );
break;
case $.ui.keyCode.DOWN:
case $.ui.keyCode.LEFT:
if ( curVal === this._valueMin() ) {
return;
}
newVal = this._trimAlignValue( curVal - step );
break;
}
this._slide( event, index, newVal );
},
keyup: function( event ) {
var index = $( event.target ).data( "ui-slider-handle-index" );
if ( this._keySliding ) {
this._keySliding = false;
this._stop( event, index );
this._change( event, index );
this._removeClass( $( event.target ), null, "ui-state-active" );
}
}
}
} );
/*!
* jQuery UI Sortable 1.13.0
* http://jqueryui.com
*
* Copyright jQuery Foundation and other contributors
* Released under the MIT license.
* http://jquery.org/license
*/
//>>label: Sortable
//>>group: Interactions
//>>description: Enables items in a list to be sorted using the mouse.
//>>docs: http://api.jqueryui.com/sortable/
//>>demos: http://jqueryui.com/sortable/
//>>css.structure: ../../themes/base/sortable.css
var widgetsSortable = $.widget( "ui.sortable", $.ui.mouse, {
version: "1.13.0",
widgetEventPrefix: "sort",
ready: false,
options: {
appendTo: "parent",
axis: false,
connectWith: false,
containment: false,
cursor: "auto",
cursorAt: false,
dropOnEmpty: true,
forcePlaceholderSize: false,
forceHelperSize: false,
grid: false,
handle: false,
helper: "original",
items: "> *",
opacity: false,
placeholder: false,
revert: false,
scroll: true,
scrollSensitivity: 20,
scrollSpeed: 20,
scope: "default",
tolerance: "intersect",
zIndex: 1000,
// Callbacks
activate: null,
beforeStop: null,
change: null,
deactivate: null,
out: null,
over: null,
receive: null,
remove: null,
sort: null,
start: null,
stop: null,
update: null
},
_isOverAxis: function( x, reference, size ) {
return ( x >= reference ) && ( x < ( reference + size ) );
},
_isFloating: function( item ) {
return ( /left|right/ ).test( item.css( "float" ) ) ||
( /inline|table-cell/ ).test( item.css( "display" ) );
},
_create: function() {
this.containerCache = {};
this._addClass( "ui-sortable" );
//Get the items
this.refresh();
//Let's determine the parent's offset
this.offset = this.element.offset();
//Initialize mouse events for interaction
this._mouseInit();
this._setHandleClassName();
//We're ready to go
this.ready = true;
},
_setOption: function( key, value ) {
this._super( key, value );
if ( key === "handle" ) {
this._setHandleClassName();
}
},
_setHandleClassName: function() {
var that = this;
this._removeClass( this.element.find( ".ui-sortable-handle" ), "ui-sortable-handle" );
$.each( this.items, function() {
that._addClass(
this.instance.options.handle ?
this.item.find( this.instance.options.handle ) :
this.item,
"ui-sortable-handle"
);
} );
},
_destroy: function() {
this._mouseDestroy();
for ( var i = this.items.length - 1; i >= 0; i-- ) {
this.items[ i ].item.removeData( this.widgetName + "-item" );
}
return this;
},
_mouseCapture: function( event, overrideHandle ) {
var currentItem = null,
validHandle = false,
that = this;
if ( this.reverting ) {
return false;
}
if ( this.options.disabled || this.options.type === "static" ) {
return false;
}
//We have to refresh the items data once first
this._refreshItems( event );
//Find out if the clicked node (or one of its parents) is a actual item in this.items
$( event.target ).parents().each( function() {
if ( $.data( this, that.widgetName + "-item" ) === that ) {
currentItem = $( this );
return false;
}
} );
if ( $.data( event.target, that.widgetName + "-item" ) === that ) {
currentItem = $( event.target );
}
if ( !currentItem ) {
return false;
}
if ( this.options.handle && !overrideHandle ) {
$( this.options.handle, currentItem ).find( "*" ).addBack().each( function() {
if ( this === event.target ) {
validHandle = true;
}
} );
if ( !validHandle ) {
return false;
}
}
this.currentItem = currentItem;
this._removeCurrentsFromItems();
return true;
},
_mouseStart: function( event, overrideHandle, noActivation ) {
var i, body,
o = this.options;
this.currentContainer = this;
//We only need to call refreshPositions, because the refreshItems call has been moved to
// mouseCapture
this.refreshPositions();
//Prepare the dragged items parent
this.appendTo = $( o.appendTo !== "parent" ?
o.appendTo :
this.currentItem.parent() );
//Create and append the visible helper
this.helper = this._createHelper( event );
//Cache the helper size
this._cacheHelperProportions();
/*
* - Position generation -
* This block generates everything position related - it's the core of draggables.
*/
//Cache the margins of the original element
this._cacheMargins();
//The element's absolute position on the page minus margins
this.offset = this.currentItem.offset();
this.offset = {
top: this.offset.top - this.margins.top,
left: this.offset.left - this.margins.left
};
$.extend( this.offset, {
click: { //Where the click happened, relative to the element
left: event.pageX - this.offset.left,
top: event.pageY - this.offset.top
},
// This is a relative to absolute position minus the actual position calculation -
// only used for relative positioned helper
relative: this._getRelativeOffset()
} );
// After we get the helper offset, but before we get the parent offset we can
// change the helper's position to absolute
// TODO: Still need to figure out a way to make relative sorting possible
this.helper.css( "position", "absolute" );
this.cssPosition = this.helper.css( "position" );
//Adjust the mouse offset relative to the helper if "cursorAt" is supplied
if ( o.cursorAt ) {
this._adjustOffsetFromHelper( o.cursorAt );
}
//Cache the former DOM position
this.domPosition = {
prev: this.currentItem.prev()[ 0 ],
parent: this.currentItem.parent()[ 0 ]
};
// If the helper is not the original, hide the original so it's not playing any role during
// the drag, won't cause anything bad this way
if ( this.helper[ 0 ] !== this.currentItem[ 0 ] ) {
this.currentItem.hide();
}
//Create the placeholder
this._createPlaceholder();
//Get the next scrolling parent
this.scrollParent = this.placeholder.scrollParent();
$.extend( this.offset, {
parent: this._getParentOffset()
} );
//Set a containment if given in the options
if ( o.containment ) {
this._setContainment();
}
if ( o.cursor && o.cursor !== "auto" ) { // cursor option
body = this.document.find( "body" );
// Support: IE
this.storedCursor = body.css( "cursor" );
body.css( "cursor", o.cursor );
this.storedStylesheet =
$( "" ).appendTo( body );
}
// We need to make sure to grab the zIndex before setting the
// opacity, because setting the opacity to anything lower than 1
// causes the zIndex to change from "auto" to 0.
if ( o.zIndex ) { // zIndex option
if ( this.helper.css( "zIndex" ) ) {
this._storedZIndex = this.helper.css( "zIndex" );
}
this.helper.css( "zIndex", o.zIndex );
}
if ( o.opacity ) { // opacity option
if ( this.helper.css( "opacity" ) ) {
this._storedOpacity = this.helper.css( "opacity" );
}
this.helper.css( "opacity", o.opacity );
}
//Prepare scrolling
if ( this.scrollParent[ 0 ] !== this.document[ 0 ] &&
this.scrollParent[ 0 ].tagName !== "HTML" ) {
this.overflowOffset = this.scrollParent.offset();
}
//Call callbacks
this._trigger( "start", event, this._uiHash() );
//Recache the helper size
if ( !this._preserveHelperProportions ) {
this._cacheHelperProportions();
}
//Post "activate" events to possible containers
if ( !noActivation ) {
for ( i = this.containers.length - 1; i >= 0; i-- ) {
this.containers[ i ]._trigger( "activate", event, this._uiHash( this ) );
}
}
//Prepare possible droppables
if ( $.ui.ddmanager ) {
$.ui.ddmanager.current = this;
}
if ( $.ui.ddmanager && !o.dropBehaviour ) {
$.ui.ddmanager.prepareOffsets( this, event );
}
this.dragging = true;
this._addClass( this.helper, "ui-sortable-helper" );
//Move the helper, if needed
if ( !this.helper.parent().is( this.appendTo ) ) {
this.helper.detach().appendTo( this.appendTo );
//Update position
this.offset.parent = this._getParentOffset();
}
//Generate the original position
this.position = this.originalPosition = this._generatePosition( event );
this.originalPageX = event.pageX;
this.originalPageY = event.pageY;
this.lastPositionAbs = this.positionAbs = this._convertPositionTo( "absolute" );
this._mouseDrag( event );
return true;
},
_scroll: function( event ) {
var o = this.options,
scrolled = false;
if ( this.scrollParent[ 0 ] !== this.document[ 0 ] &&
this.scrollParent[ 0 ].tagName !== "HTML" ) {
if ( ( this.overflowOffset.top + this.scrollParent[ 0 ].offsetHeight ) -
event.pageY < o.scrollSensitivity ) {
this.scrollParent[ 0 ].scrollTop =
scrolled = this.scrollParent[ 0 ].scrollTop + o.scrollSpeed;
} else if ( event.pageY - this.overflowOffset.top < o.scrollSensitivity ) {
this.scrollParent[ 0 ].scrollTop =
scrolled = this.scrollParent[ 0 ].scrollTop - o.scrollSpeed;
}
if ( ( this.overflowOffset.left + this.scrollParent[ 0 ].offsetWidth ) -
event.pageX < o.scrollSensitivity ) {
this.scrollParent[ 0 ].scrollLeft = scrolled =
this.scrollParent[ 0 ].scrollLeft + o.scrollSpeed;
} else if ( event.pageX - this.overflowOffset.left < o.scrollSensitivity ) {
this.scrollParent[ 0 ].scrollLeft = scrolled =
this.scrollParent[ 0 ].scrollLeft - o.scrollSpeed;
}
} else {
if ( event.pageY - this.document.scrollTop() < o.scrollSensitivity ) {
scrolled = this.document.scrollTop( this.document.scrollTop() - o.scrollSpeed );
} else if ( this.window.height() - ( event.pageY - this.document.scrollTop() ) <
o.scrollSensitivity ) {
scrolled = this.document.scrollTop( this.document.scrollTop() + o.scrollSpeed );
}
if ( event.pageX - this.document.scrollLeft() < o.scrollSensitivity ) {
scrolled = this.document.scrollLeft(
this.document.scrollLeft() - o.scrollSpeed
);
} else if ( this.window.width() - ( event.pageX - this.document.scrollLeft() ) <
o.scrollSensitivity ) {
scrolled = this.document.scrollLeft(
this.document.scrollLeft() + o.scrollSpeed
);
}
}
return scrolled;
},
_mouseDrag: function( event ) {
var i, item, itemElement, intersection,
o = this.options;
//Compute the helpers position
this.position = this._generatePosition( event );
this.positionAbs = this._convertPositionTo( "absolute" );
//Set the helper position
if ( !this.options.axis || this.options.axis !== "y" ) {
this.helper[ 0 ].style.left = this.position.left + "px";
}
if ( !this.options.axis || this.options.axis !== "x" ) {
this.helper[ 0 ].style.top = this.position.top + "px";
}
//Post events to containers
this._contactContainers( event );
if ( this.innermostContainer !== null ) {
//Do scrolling
if ( o.scroll ) {
if ( this._scroll( event ) !== false ) {
//Update item positions used in position checks
this._refreshItemPositions( true );
if ( $.ui.ddmanager && !o.dropBehaviour ) {
$.ui.ddmanager.prepareOffsets( this, event );
}
}
}
this.dragDirection = {
vertical: this._getDragVerticalDirection(),
horizontal: this._getDragHorizontalDirection()
};
//Rearrange
for ( i = this.items.length - 1; i >= 0; i-- ) {
//Cache variables and intersection, continue if no intersection
item = this.items[ i ];
itemElement = item.item[ 0 ];
intersection = this._intersectsWithPointer( item );
if ( !intersection ) {
continue;
}
// Only put the placeholder inside the current Container, skip all
// items from other containers. This works because when moving
// an item from one container to another the
// currentContainer is switched before the placeholder is moved.
//
// Without this, moving items in "sub-sortables" can cause
// the placeholder to jitter between the outer and inner container.
if ( item.instance !== this.currentContainer ) {
continue;
}
// Cannot intersect with itself
// no useless actions that have been done before
// no action if the item moved is the parent of the item checked
if ( itemElement !== this.currentItem[ 0 ] &&
this.placeholder[ intersection === 1 ?
"next" : "prev" ]()[ 0 ] !== itemElement &&
!$.contains( this.placeholder[ 0 ], itemElement ) &&
( this.options.type === "semi-dynamic" ?
!$.contains( this.element[ 0 ], itemElement ) :
true
)
) {
this.direction = intersection === 1 ? "down" : "up";
if ( this.options.tolerance === "pointer" ||
this._intersectsWithSides( item ) ) {
this._rearrange( event, item );
} else {
break;
}
this._trigger( "change", event, this._uiHash() );
break;
}
}
}
//Interconnect with droppables
if ( $.ui.ddmanager ) {
$.ui.ddmanager.drag( this, event );
}
//Call callbacks
this._trigger( "sort", event, this._uiHash() );
this.lastPositionAbs = this.positionAbs;
return false;
},
_mouseStop: function( event, noPropagation ) {
if ( !event ) {
return;
}
//If we are using droppables, inform the manager about the drop
if ( $.ui.ddmanager && !this.options.dropBehaviour ) {
$.ui.ddmanager.drop( this, event );
}
if ( this.options.revert ) {
var that = this,
cur = this.placeholder.offset(),
axis = this.options.axis,
animation = {};
if ( !axis || axis === "x" ) {
animation.left = cur.left - this.offset.parent.left - this.margins.left +
( this.offsetParent[ 0 ] === this.document[ 0 ].body ?
0 :
this.offsetParent[ 0 ].scrollLeft
);
}
if ( !axis || axis === "y" ) {
animation.top = cur.top - this.offset.parent.top - this.margins.top +
( this.offsetParent[ 0 ] === this.document[ 0 ].body ?
0 :
this.offsetParent[ 0 ].scrollTop
);
}
this.reverting = true;
$( this.helper ).animate(
animation,
parseInt( this.options.revert, 10 ) || 500,
function() {
that._clear( event );
}
);
} else {
this._clear( event, noPropagation );
}
return false;
},
cancel: function() {
if ( this.dragging ) {
this._mouseUp( new $.Event( "mouseup", { target: null } ) );
if ( this.options.helper === "original" ) {
this.currentItem.css( this._storedCSS );
this._removeClass( this.currentItem, "ui-sortable-helper" );
} else {
this.currentItem.show();
}
//Post deactivating events to containers
for ( var i = this.containers.length - 1; i >= 0; i-- ) {
this.containers[ i ]._trigger( "deactivate", null, this._uiHash( this ) );
if ( this.containers[ i ].containerCache.over ) {
this.containers[ i ]._trigger( "out", null, this._uiHash( this ) );
this.containers[ i ].containerCache.over = 0;
}
}
}
if ( this.placeholder ) {
//$(this.placeholder[0]).remove(); would have been the jQuery way - unfortunately,
// it unbinds ALL events from the original node!
if ( this.placeholder[ 0 ].parentNode ) {
this.placeholder[ 0 ].parentNode.removeChild( this.placeholder[ 0 ] );
}
if ( this.options.helper !== "original" && this.helper &&
this.helper[ 0 ].parentNode ) {
this.helper.remove();
}
$.extend( this, {
helper: null,
dragging: false,
reverting: false,
_noFinalSort: null
} );
if ( this.domPosition.prev ) {
$( this.domPosition.prev ).after( this.currentItem );
} else {
$( this.domPosition.parent ).prepend( this.currentItem );
}
}
return this;
},
serialize: function( o ) {
var items = this._getItemsAsjQuery( o && o.connected ),
str = [];
o = o || {};
$( items ).each( function() {
var res = ( $( o.item || this ).attr( o.attribute || "id" ) || "" )
.match( o.expression || ( /(.+)[\-=_](.+)/ ) );
if ( res ) {
str.push(
( o.key || res[ 1 ] + "[]" ) +
"=" + ( o.key && o.expression ? res[ 1 ] : res[ 2 ] ) );
}
} );
if ( !str.length && o.key ) {
str.push( o.key + "=" );
}
return str.join( "&" );
},
toArray: function( o ) {
var items = this._getItemsAsjQuery( o && o.connected ),
ret = [];
o = o || {};
items.each( function() {
ret.push( $( o.item || this ).attr( o.attribute || "id" ) || "" );
} );
return ret;
},
/* Be careful with the following core functions */
_intersectsWith: function( item ) {
var x1 = this.positionAbs.left,
x2 = x1 + this.helperProportions.width,
y1 = this.positionAbs.top,
y2 = y1 + this.helperProportions.height,
l = item.left,
r = l + item.width,
t = item.top,
b = t + item.height,
dyClick = this.offset.click.top,
dxClick = this.offset.click.left,
isOverElementHeight = ( this.options.axis === "x" ) || ( ( y1 + dyClick ) > t &&
( y1 + dyClick ) < b ),
isOverElementWidth = ( this.options.axis === "y" ) || ( ( x1 + dxClick ) > l &&
( x1 + dxClick ) < r ),
isOverElement = isOverElementHeight && isOverElementWidth;
if ( this.options.tolerance === "pointer" ||
this.options.forcePointerForContainers ||
( this.options.tolerance !== "pointer" &&
this.helperProportions[ this.floating ? "width" : "height" ] >
item[ this.floating ? "width" : "height" ] )
) {
return isOverElement;
} else {
return ( l < x1 + ( this.helperProportions.width / 2 ) && // Right Half
x2 - ( this.helperProportions.width / 2 ) < r && // Left Half
t < y1 + ( this.helperProportions.height / 2 ) && // Bottom Half
y2 - ( this.helperProportions.height / 2 ) < b ); // Top Half
}
},
_intersectsWithPointer: function( item ) {
var verticalDirection, horizontalDirection,
isOverElementHeight = ( this.options.axis === "x" ) ||
this._isOverAxis(
this.positionAbs.top + this.offset.click.top, item.top, item.height ),
isOverElementWidth = ( this.options.axis === "y" ) ||
this._isOverAxis(
this.positionAbs.left + this.offset.click.left, item.left, item.width ),
isOverElement = isOverElementHeight && isOverElementWidth;
if ( !isOverElement ) {
return false;
}
verticalDirection = this.dragDirection.vertical;
horizontalDirection = this.dragDirection.horizontal;
return this.floating ?
( ( horizontalDirection === "right" || verticalDirection === "down" ) ? 2 : 1 ) :
( verticalDirection && ( verticalDirection === "down" ? 2 : 1 ) );
},
_intersectsWithSides: function( item ) {
var isOverBottomHalf = this._isOverAxis( this.positionAbs.top +
this.offset.click.top, item.top + ( item.height / 2 ), item.height ),
isOverRightHalf = this._isOverAxis( this.positionAbs.left +
this.offset.click.left, item.left + ( item.width / 2 ), item.width ),
verticalDirection = this.dragDirection.vertical,
horizontalDirection = this.dragDirection.horizontal;
if ( this.floating && horizontalDirection ) {
return ( ( horizontalDirection === "right" && isOverRightHalf ) ||
( horizontalDirection === "left" && !isOverRightHalf ) );
} else {
return verticalDirection && ( ( verticalDirection === "down" && isOverBottomHalf ) ||
( verticalDirection === "up" && !isOverBottomHalf ) );
}
},
_getDragVerticalDirection: function() {
var delta = this.positionAbs.top - this.lastPositionAbs.top;
return delta !== 0 && ( delta > 0 ? "down" : "up" );
},
_getDragHorizontalDirection: function() {
var delta = this.positionAbs.left - this.lastPositionAbs.left;
return delta !== 0 && ( delta > 0 ? "right" : "left" );
},
refresh: function( event ) {
this._refreshItems( event );
this._setHandleClassName();
this.refreshPositions();
return this;
},
_connectWith: function() {
var options = this.options;
return options.connectWith.constructor === String ?
[ options.connectWith ] :
options.connectWith;
},
_getItemsAsjQuery: function( connected ) {
var i, j, cur, inst,
items = [],
queries = [],
connectWith = this._connectWith();
if ( connectWith && connected ) {
for ( i = connectWith.length - 1; i >= 0; i-- ) {
cur = $( connectWith[ i ], this.document[ 0 ] );
for ( j = cur.length - 1; j >= 0; j-- ) {
inst = $.data( cur[ j ], this.widgetFullName );
if ( inst && inst !== this && !inst.options.disabled ) {
queries.push( [ typeof inst.options.items === "function" ?
inst.options.items.call( inst.element ) :
$( inst.options.items, inst.element )
.not( ".ui-sortable-helper" )
.not( ".ui-sortable-placeholder" ), inst ] );
}
}
}
}
queries.push( [ typeof this.options.items === "function" ?
this.options.items
.call( this.element, null, { options: this.options, item: this.currentItem } ) :
$( this.options.items, this.element )
.not( ".ui-sortable-helper" )
.not( ".ui-sortable-placeholder" ), this ] );
function addItems() {
items.push( this );
}
for ( i = queries.length - 1; i >= 0; i-- ) {
queries[ i ][ 0 ].each( addItems );
}
return $( items );
},
_removeCurrentsFromItems: function() {
var list = this.currentItem.find( ":data(" + this.widgetName + "-item)" );
this.items = $.grep( this.items, function( item ) {
for ( var j = 0; j < list.length; j++ ) {
if ( list[ j ] === item.item[ 0 ] ) {
return false;
}
}
return true;
} );
},
_refreshItems: function( event ) {
this.items = [];
this.containers = [ this ];
var i, j, cur, inst, targetData, _queries, item, queriesLength,
items = this.items,
queries = [ [ typeof this.options.items === "function" ?
this.options.items.call( this.element[ 0 ], event, { item: this.currentItem } ) :
$( this.options.items, this.element ), this ] ],
connectWith = this._connectWith();
//Shouldn't be run the first time through due to massive slow-down
if ( connectWith && this.ready ) {
for ( i = connectWith.length - 1; i >= 0; i-- ) {
cur = $( connectWith[ i ], this.document[ 0 ] );
for ( j = cur.length - 1; j >= 0; j-- ) {
inst = $.data( cur[ j ], this.widgetFullName );
if ( inst && inst !== this && !inst.options.disabled ) {
queries.push( [ typeof inst.options.items === "function" ?
inst.options.items
.call( inst.element[ 0 ], event, { item: this.currentItem } ) :
$( inst.options.items, inst.element ), inst ] );
this.containers.push( inst );
}
}
}
}
for ( i = queries.length - 1; i >= 0; i-- ) {
targetData = queries[ i ][ 1 ];
_queries = queries[ i ][ 0 ];
for ( j = 0, queriesLength = _queries.length; j < queriesLength; j++ ) {
item = $( _queries[ j ] );
// Data for target checking (mouse manager)
item.data( this.widgetName + "-item", targetData );
items.push( {
item: item,
instance: targetData,
width: 0, height: 0,
left: 0, top: 0
} );
}
}
},
_refreshItemPositions: function( fast ) {
var i, item, t, p;
for ( i = this.items.length - 1; i >= 0; i-- ) {
item = this.items[ i ];
//We ignore calculating positions of all connected containers when we're not over them
if ( this.currentContainer && item.instance !== this.currentContainer &&
item.item[ 0 ] !== this.currentItem[ 0 ] ) {
continue;
}
t = this.options.toleranceElement ?
$( this.options.toleranceElement, item.item ) :
item.item;
if ( !fast ) {
item.width = t.outerWidth();
item.height = t.outerHeight();
}
p = t.offset();
item.left = p.left;
item.top = p.top;
}
},
refreshPositions: function( fast ) {
// Determine whether items are being displayed horizontally
this.floating = this.items.length ?
this.options.axis === "x" || this._isFloating( this.items[ 0 ].item ) :
false;
if ( this.innermostContainer !== null ) {
this._refreshItemPositions( fast );
}
var i, p;
if ( this.options.custom && this.options.custom.refreshContainers ) {
this.options.custom.refreshContainers.call( this );
} else {
for ( i = this.containers.length - 1; i >= 0; i-- ) {
p = this.containers[ i ].element.offset();
this.containers[ i ].containerCache.left = p.left;
this.containers[ i ].containerCache.top = p.top;
this.containers[ i ].containerCache.width =
this.containers[ i ].element.outerWidth();
this.containers[ i ].containerCache.height =
this.containers[ i ].element.outerHeight();
}
}
return this;
},
_createPlaceholder: function( that ) {
that = that || this;
var className, nodeName,
o = that.options;
if ( !o.placeholder || o.placeholder.constructor === String ) {
className = o.placeholder;
nodeName = that.currentItem[ 0 ].nodeName.toLowerCase();
o.placeholder = {
element: function() {
var element = $( "<" + nodeName + ">", that.document[ 0 ] );
that._addClass( element, "ui-sortable-placeholder",
className || that.currentItem[ 0 ].className )
._removeClass( element, "ui-sortable-helper" );
if ( nodeName === "tbody" ) {
that._createTrPlaceholder(
that.currentItem.find( "tr" ).eq( 0 ),
$( "
", that.document[ 0 ] ).appendTo( element )
);
} else if ( nodeName === "tr" ) {
that._createTrPlaceholder( that.currentItem, element );
} else if ( nodeName === "img" ) {
element.attr( "src", that.currentItem.attr( "src" ) );
}
if ( !className ) {
element.css( "visibility", "hidden" );
}
return element;
},
update: function( container, p ) {
// 1. If a className is set as 'placeholder option, we don't force sizes -
// the class is responsible for that
// 2. The option 'forcePlaceholderSize can be enabled to force it even if a
// class name is specified
if ( className && !o.forcePlaceholderSize ) {
return;
}
// If the element doesn't have a actual height or width by itself (without
// styles coming from a stylesheet), it receives the inline height and width
// from the dragged item. Or, if it's a tbody or tr, it's going to have a height
// anyway since we're populating them with s above, but they're unlikely to
// be the correct height on their own if the row heights are dynamic, so we'll
// always assign the height of the dragged item given forcePlaceholderSize
// is true.
if ( !p.height() || ( o.forcePlaceholderSize &&
( nodeName === "tbody" || nodeName === "tr" ) ) ) {
p.height(
that.currentItem.innerHeight() -
parseInt( that.currentItem.css( "paddingTop" ) || 0, 10 ) -
parseInt( that.currentItem.css( "paddingBottom" ) || 0, 10 ) );
}
if ( !p.width() ) {
p.width(
that.currentItem.innerWidth() -
parseInt( that.currentItem.css( "paddingLeft" ) || 0, 10 ) -
parseInt( that.currentItem.css( "paddingRight" ) || 0, 10 ) );
}
}
};
}
//Create the placeholder
that.placeholder = $( o.placeholder.element.call( that.element, that.currentItem ) );
//Append it after the actual current item
that.currentItem.after( that.placeholder );
//Update the size of the placeholder (TODO: Logic to fuzzy, see line 316/317)
o.placeholder.update( that, that.placeholder );
},
_createTrPlaceholder: function( sourceTr, targetTr ) {
var that = this;
sourceTr.children().each( function() {
$( " ", that.document[ 0 ] )
.attr( "colspan", $( this ).attr( "colspan" ) || 1 )
.appendTo( targetTr );
} );
},
_contactContainers: function( event ) {
var i, j, dist, itemWithLeastDistance, posProperty, sizeProperty, cur, nearBottom,
floating, axis,
innermostContainer = null,
innermostIndex = null;
// Get innermost container that intersects with item
for ( i = this.containers.length - 1; i >= 0; i-- ) {
// Never consider a container that's located within the item itself
if ( $.contains( this.currentItem[ 0 ], this.containers[ i ].element[ 0 ] ) ) {
continue;
}
if ( this._intersectsWith( this.containers[ i ].containerCache ) ) {
// If we've already found a container and it's more "inner" than this, then continue
if ( innermostContainer &&
$.contains(
this.containers[ i ].element[ 0 ],
innermostContainer.element[ 0 ] ) ) {
continue;
}
innermostContainer = this.containers[ i ];
innermostIndex = i;
} else {
// container doesn't intersect. trigger "out" event if necessary
if ( this.containers[ i ].containerCache.over ) {
this.containers[ i ]._trigger( "out", event, this._uiHash( this ) );
this.containers[ i ].containerCache.over = 0;
}
}
}
this.innermostContainer = innermostContainer;
// If no intersecting containers found, return
if ( !innermostContainer ) {
return;
}
// Move the item into the container if it's not there already
if ( this.containers.length === 1 ) {
if ( !this.containers[ innermostIndex ].containerCache.over ) {
this.containers[ innermostIndex ]._trigger( "over", event, this._uiHash( this ) );
this.containers[ innermostIndex ].containerCache.over = 1;
}
} else {
// When entering a new container, we will find the item with the least distance and
// append our item near it
dist = 10000;
itemWithLeastDistance = null;
floating = innermostContainer.floating || this._isFloating( this.currentItem );
posProperty = floating ? "left" : "top";
sizeProperty = floating ? "width" : "height";
axis = floating ? "pageX" : "pageY";
for ( j = this.items.length - 1; j >= 0; j-- ) {
if ( !$.contains(
this.containers[ innermostIndex ].element[ 0 ], this.items[ j ].item[ 0 ] )
) {
continue;
}
if ( this.items[ j ].item[ 0 ] === this.currentItem[ 0 ] ) {
continue;
}
cur = this.items[ j ].item.offset()[ posProperty ];
nearBottom = false;
if ( event[ axis ] - cur > this.items[ j ][ sizeProperty ] / 2 ) {
nearBottom = true;
}
if ( Math.abs( event[ axis ] - cur ) < dist ) {
dist = Math.abs( event[ axis ] - cur );
itemWithLeastDistance = this.items[ j ];
this.direction = nearBottom ? "up" : "down";
}
}
//Check if dropOnEmpty is enabled
if ( !itemWithLeastDistance && !this.options.dropOnEmpty ) {
return;
}
if ( this.currentContainer === this.containers[ innermostIndex ] ) {
if ( !this.currentContainer.containerCache.over ) {
this.containers[ innermostIndex ]._trigger( "over", event, this._uiHash() );
this.currentContainer.containerCache.over = 1;
}
return;
}
if ( itemWithLeastDistance ) {
this._rearrange( event, itemWithLeastDistance, null, true );
} else {
this._rearrange( event, null, this.containers[ innermostIndex ].element, true );
}
this._trigger( "change", event, this._uiHash() );
this.containers[ innermostIndex ]._trigger( "change", event, this._uiHash( this ) );
this.currentContainer = this.containers[ innermostIndex ];
//Update the placeholder
this.options.placeholder.update( this.currentContainer, this.placeholder );
//Update scrollParent
this.scrollParent = this.placeholder.scrollParent();
//Update overflowOffset
if ( this.scrollParent[ 0 ] !== this.document[ 0 ] &&
this.scrollParent[ 0 ].tagName !== "HTML" ) {
this.overflowOffset = this.scrollParent.offset();
}
this.containers[ innermostIndex ]._trigger( "over", event, this._uiHash( this ) );
this.containers[ innermostIndex ].containerCache.over = 1;
}
},
_createHelper: function( event ) {
var o = this.options,
helper = typeof o.helper === "function" ?
$( o.helper.apply( this.element[ 0 ], [ event, this.currentItem ] ) ) :
( o.helper === "clone" ? this.currentItem.clone() : this.currentItem );
//Add the helper to the DOM if that didn't happen already
if ( !helper.parents( "body" ).length ) {
this.appendTo[ 0 ].appendChild( helper[ 0 ] );
}
if ( helper[ 0 ] === this.currentItem[ 0 ] ) {
this._storedCSS = {
width: this.currentItem[ 0 ].style.width,
height: this.currentItem[ 0 ].style.height,
position: this.currentItem.css( "position" ),
top: this.currentItem.css( "top" ),
left: this.currentItem.css( "left" )
};
}
if ( !helper[ 0 ].style.width || o.forceHelperSize ) {
helper.width( this.currentItem.width() );
}
if ( !helper[ 0 ].style.height || o.forceHelperSize ) {
helper.height( this.currentItem.height() );
}
return helper;
},
_adjustOffsetFromHelper: function( obj ) {
if ( typeof obj === "string" ) {
obj = obj.split( " " );
}
if ( Array.isArray( obj ) ) {
obj = { left: +obj[ 0 ], top: +obj[ 1 ] || 0 };
}
if ( "left" in obj ) {
this.offset.click.left = obj.left + this.margins.left;
}
if ( "right" in obj ) {
this.offset.click.left = this.helperProportions.width - obj.right + this.margins.left;
}
if ( "top" in obj ) {
this.offset.click.top = obj.top + this.margins.top;
}
if ( "bottom" in obj ) {
this.offset.click.top = this.helperProportions.height - obj.bottom + this.margins.top;
}
},
_getParentOffset: function() {
//Get the offsetParent and cache its position
this.offsetParent = this.helper.offsetParent();
var po = this.offsetParent.offset();
// This is a special case where we need to modify a offset calculated on start, since the
// following happened:
// 1. The position of the helper is absolute, so it's position is calculated based on the
// next positioned parent
// 2. The actual offset parent is a child of the scroll parent, and the scroll parent isn't
// the document, which means that the scroll is included in the initial calculation of the
// offset of the parent, and never recalculated upon drag
if ( this.cssPosition === "absolute" && this.scrollParent[ 0 ] !== this.document[ 0 ] &&
$.contains( this.scrollParent[ 0 ], this.offsetParent[ 0 ] ) ) {
po.left += this.scrollParent.scrollLeft();
po.top += this.scrollParent.scrollTop();
}
// This needs to be actually done for all browsers, since pageX/pageY includes this
// information with an ugly IE fix
if ( this.offsetParent[ 0 ] === this.document[ 0 ].body ||
( this.offsetParent[ 0 ].tagName &&
this.offsetParent[ 0 ].tagName.toLowerCase() === "html" && $.ui.ie ) ) {
po = { top: 0, left: 0 };
}
return {
top: po.top + ( parseInt( this.offsetParent.css( "borderTopWidth" ), 10 ) || 0 ),
left: po.left + ( parseInt( this.offsetParent.css( "borderLeftWidth" ), 10 ) || 0 )
};
},
_getRelativeOffset: function() {
if ( this.cssPosition === "relative" ) {
var p = this.currentItem.position();
return {
top: p.top - ( parseInt( this.helper.css( "top" ), 10 ) || 0 ) +
this.scrollParent.scrollTop(),
left: p.left - ( parseInt( this.helper.css( "left" ), 10 ) || 0 ) +
this.scrollParent.scrollLeft()
};
} else {
return { top: 0, left: 0 };
}
},
_cacheMargins: function() {
this.margins = {
left: ( parseInt( this.currentItem.css( "marginLeft" ), 10 ) || 0 ),
top: ( parseInt( this.currentItem.css( "marginTop" ), 10 ) || 0 )
};
},
_cacheHelperProportions: function() {
this.helperProportions = {
width: this.helper.outerWidth(),
height: this.helper.outerHeight()
};
},
_setContainment: function() {
var ce, co, over,
o = this.options;
if ( o.containment === "parent" ) {
o.containment = this.helper[ 0 ].parentNode;
}
if ( o.containment === "document" || o.containment === "window" ) {
this.containment = [
0 - this.offset.relative.left - this.offset.parent.left,
0 - this.offset.relative.top - this.offset.parent.top,
o.containment === "document" ?
this.document.width() :
this.window.width() - this.helperProportions.width - this.margins.left,
( o.containment === "document" ?
( this.document.height() || document.body.parentNode.scrollHeight ) :
this.window.height() || this.document[ 0 ].body.parentNode.scrollHeight
) - this.helperProportions.height - this.margins.top
];
}
if ( !( /^(document|window|parent)$/ ).test( o.containment ) ) {
ce = $( o.containment )[ 0 ];
co = $( o.containment ).offset();
over = ( $( ce ).css( "overflow" ) !== "hidden" );
this.containment = [
co.left + ( parseInt( $( ce ).css( "borderLeftWidth" ), 10 ) || 0 ) +
( parseInt( $( ce ).css( "paddingLeft" ), 10 ) || 0 ) - this.margins.left,
co.top + ( parseInt( $( ce ).css( "borderTopWidth" ), 10 ) || 0 ) +
( parseInt( $( ce ).css( "paddingTop" ), 10 ) || 0 ) - this.margins.top,
co.left + ( over ? Math.max( ce.scrollWidth, ce.offsetWidth ) : ce.offsetWidth ) -
( parseInt( $( ce ).css( "borderLeftWidth" ), 10 ) || 0 ) -
( parseInt( $( ce ).css( "paddingRight" ), 10 ) || 0 ) -
this.helperProportions.width - this.margins.left,
co.top + ( over ? Math.max( ce.scrollHeight, ce.offsetHeight ) : ce.offsetHeight ) -
( parseInt( $( ce ).css( "borderTopWidth" ), 10 ) || 0 ) -
( parseInt( $( ce ).css( "paddingBottom" ), 10 ) || 0 ) -
this.helperProportions.height - this.margins.top
];
}
},
_convertPositionTo: function( d, pos ) {
if ( !pos ) {
pos = this.position;
}
var mod = d === "absolute" ? 1 : -1,
scroll = this.cssPosition === "absolute" &&
!( this.scrollParent[ 0 ] !== this.document[ 0 ] &&
$.contains( this.scrollParent[ 0 ], this.offsetParent[ 0 ] ) ) ?
this.offsetParent :
this.scrollParent,
scrollIsRootNode = ( /(html|body)/i ).test( scroll[ 0 ].tagName );
return {
top: (
// The absolute mouse position
pos.top +
// Only for relative positioned nodes: Relative offset from element to offset parent
this.offset.relative.top * mod +
// The offsetParent's offset without borders (offset + border)
this.offset.parent.top * mod -
( ( this.cssPosition === "fixed" ?
-this.scrollParent.scrollTop() :
( scrollIsRootNode ? 0 : scroll.scrollTop() ) ) * mod )
),
left: (
// The absolute mouse position
pos.left +
// Only for relative positioned nodes: Relative offset from element to offset parent
this.offset.relative.left * mod +
// The offsetParent's offset without borders (offset + border)
this.offset.parent.left * mod -
( ( this.cssPosition === "fixed" ?
-this.scrollParent.scrollLeft() : scrollIsRootNode ? 0 :
scroll.scrollLeft() ) * mod )
)
};
},
_generatePosition: function( event ) {
var top, left,
o = this.options,
pageX = event.pageX,
pageY = event.pageY,
scroll = this.cssPosition === "absolute" &&
!( this.scrollParent[ 0 ] !== this.document[ 0 ] &&
$.contains( this.scrollParent[ 0 ], this.offsetParent[ 0 ] ) ) ?
this.offsetParent :
this.scrollParent,
scrollIsRootNode = ( /(html|body)/i ).test( scroll[ 0 ].tagName );
// This is another very weird special case that only happens for relative elements:
// 1. If the css position is relative
// 2. and the scroll parent is the document or similar to the offset parent
// we have to refresh the relative offset during the scroll so there are no jumps
if ( this.cssPosition === "relative" && !( this.scrollParent[ 0 ] !== this.document[ 0 ] &&
this.scrollParent[ 0 ] !== this.offsetParent[ 0 ] ) ) {
this.offset.relative = this._getRelativeOffset();
}
/*
* - Position constraining -
* Constrain the position to a mix of grid, containment.
*/
if ( this.originalPosition ) { //If we are not dragging yet, we won't check for options
if ( this.containment ) {
if ( event.pageX - this.offset.click.left < this.containment[ 0 ] ) {
pageX = this.containment[ 0 ] + this.offset.click.left;
}
if ( event.pageY - this.offset.click.top < this.containment[ 1 ] ) {
pageY = this.containment[ 1 ] + this.offset.click.top;
}
if ( event.pageX - this.offset.click.left > this.containment[ 2 ] ) {
pageX = this.containment[ 2 ] + this.offset.click.left;
}
if ( event.pageY - this.offset.click.top > this.containment[ 3 ] ) {
pageY = this.containment[ 3 ] + this.offset.click.top;
}
}
if ( o.grid ) {
top = this.originalPageY + Math.round( ( pageY - this.originalPageY ) /
o.grid[ 1 ] ) * o.grid[ 1 ];
pageY = this.containment ?
( ( top - this.offset.click.top >= this.containment[ 1 ] &&
top - this.offset.click.top <= this.containment[ 3 ] ) ?
top :
( ( top - this.offset.click.top >= this.containment[ 1 ] ) ?
top - o.grid[ 1 ] : top + o.grid[ 1 ] ) ) :
top;
left = this.originalPageX + Math.round( ( pageX - this.originalPageX ) /
o.grid[ 0 ] ) * o.grid[ 0 ];
pageX = this.containment ?
( ( left - this.offset.click.left >= this.containment[ 0 ] &&
left - this.offset.click.left <= this.containment[ 2 ] ) ?
left :
( ( left - this.offset.click.left >= this.containment[ 0 ] ) ?
left - o.grid[ 0 ] : left + o.grid[ 0 ] ) ) :
left;
}
}
return {
top: (
// The absolute mouse position
pageY -
// Click offset (relative to the element)
this.offset.click.top -
// Only for relative positioned nodes: Relative offset from element to offset parent
this.offset.relative.top -
// The offsetParent's offset without borders (offset + border)
this.offset.parent.top +
( ( this.cssPosition === "fixed" ?
-this.scrollParent.scrollTop() :
( scrollIsRootNode ? 0 : scroll.scrollTop() ) ) )
),
left: (
// The absolute mouse position
pageX -
// Click offset (relative to the element)
this.offset.click.left -
// Only for relative positioned nodes: Relative offset from element to offset parent
this.offset.relative.left -
// The offsetParent's offset without borders (offset + border)
this.offset.parent.left +
( ( this.cssPosition === "fixed" ?
-this.scrollParent.scrollLeft() :
scrollIsRootNode ? 0 : scroll.scrollLeft() ) )
)
};
},
_rearrange: function( event, i, a, hardRefresh ) {
if ( a ) {
a[ 0 ].appendChild( this.placeholder[ 0 ] );
} else {
i.item[ 0 ].parentNode.insertBefore( this.placeholder[ 0 ],
( this.direction === "down" ? i.item[ 0 ] : i.item[ 0 ].nextSibling ) );
}
//Various things done here to improve the performance:
// 1. we create a setTimeout, that calls refreshPositions
// 2. on the instance, we have a counter variable, that get's higher after every append
// 3. on the local scope, we copy the counter variable, and check in the timeout,
// if it's still the same
// 4. this lets only the last addition to the timeout stack through
this.counter = this.counter ? ++this.counter : 1;
var counter = this.counter;
this._delay( function() {
if ( counter === this.counter ) {
//Precompute after each DOM insertion, NOT on mousemove
this.refreshPositions( !hardRefresh );
}
} );
},
_clear: function( event, noPropagation ) {
this.reverting = false;
// We delay all events that have to be triggered to after the point where the placeholder
// has been removed and everything else normalized again
var i,
delayedTriggers = [];
// We first have to update the dom position of the actual currentItem
// Note: don't do it if the current item is already removed (by a user), or it gets
// reappended (see #4088)
if ( !this._noFinalSort && this.currentItem.parent().length ) {
this.placeholder.before( this.currentItem );
}
this._noFinalSort = null;
if ( this.helper[ 0 ] === this.currentItem[ 0 ] ) {
for ( i in this._storedCSS ) {
if ( this._storedCSS[ i ] === "auto" || this._storedCSS[ i ] === "static" ) {
this._storedCSS[ i ] = "";
}
}
this.currentItem.css( this._storedCSS );
this._removeClass( this.currentItem, "ui-sortable-helper" );
} else {
this.currentItem.show();
}
if ( this.fromOutside && !noPropagation ) {
delayedTriggers.push( function( event ) {
this._trigger( "receive", event, this._uiHash( this.fromOutside ) );
} );
}
if ( ( this.fromOutside ||
this.domPosition.prev !==
this.currentItem.prev().not( ".ui-sortable-helper" )[ 0 ] ||
this.domPosition.parent !== this.currentItem.parent()[ 0 ] ) && !noPropagation ) {
// Trigger update callback if the DOM position has changed
delayedTriggers.push( function( event ) {
this._trigger( "update", event, this._uiHash() );
} );
}
// Check if the items Container has Changed and trigger appropriate
// events.
if ( this !== this.currentContainer ) {
if ( !noPropagation ) {
delayedTriggers.push( function( event ) {
this._trigger( "remove", event, this._uiHash() );
} );
delayedTriggers.push( ( function( c ) {
return function( event ) {
c._trigger( "receive", event, this._uiHash( this ) );
};
} ).call( this, this.currentContainer ) );
delayedTriggers.push( ( function( c ) {
return function( event ) {
c._trigger( "update", event, this._uiHash( this ) );
};
} ).call( this, this.currentContainer ) );
}
}
//Post events to containers
function delayEvent( type, instance, container ) {
return function( event ) {
container._trigger( type, event, instance._uiHash( instance ) );
};
}
for ( i = this.containers.length - 1; i >= 0; i-- ) {
if ( !noPropagation ) {
delayedTriggers.push( delayEvent( "deactivate", this, this.containers[ i ] ) );
}
if ( this.containers[ i ].containerCache.over ) {
delayedTriggers.push( delayEvent( "out", this, this.containers[ i ] ) );
this.containers[ i ].containerCache.over = 0;
}
}
//Do what was originally in plugins
if ( this.storedCursor ) {
this.document.find( "body" ).css( "cursor", this.storedCursor );
this.storedStylesheet.remove();
}
if ( this._storedOpacity ) {
this.helper.css( "opacity", this._storedOpacity );
}
if ( this._storedZIndex ) {
this.helper.css( "zIndex", this._storedZIndex === "auto" ? "" : this._storedZIndex );
}
this.dragging = false;
if ( !noPropagation ) {
this._trigger( "beforeStop", event, this._uiHash() );
}
//$(this.placeholder[0]).remove(); would have been the jQuery way - unfortunately,
// it unbinds ALL events from the original node!
this.placeholder[ 0 ].parentNode.removeChild( this.placeholder[ 0 ] );
if ( !this.cancelHelperRemoval ) {
if ( this.helper[ 0 ] !== this.currentItem[ 0 ] ) {
this.helper.remove();
}
this.helper = null;
}
if ( !noPropagation ) {
for ( i = 0; i < delayedTriggers.length; i++ ) {
// Trigger all delayed events
delayedTriggers[ i ].call( this, event );
}
this._trigger( "stop", event, this._uiHash() );
}
this.fromOutside = false;
return !this.cancelHelperRemoval;
},
_trigger: function() {
if ( $.Widget.prototype._trigger.apply( this, arguments ) === false ) {
this.cancel();
}
},
_uiHash: function( _inst ) {
var inst = _inst || this;
return {
helper: inst.helper,
placeholder: inst.placeholder || $( [] ),
position: inst.position,
originalPosition: inst.originalPosition,
offset: inst.positionAbs,
item: inst.currentItem,
sender: _inst ? _inst.element : null
};
}
} );
/*!
* jQuery UI Spinner 1.13.0
* http://jqueryui.com
*
* Copyright jQuery Foundation and other contributors
* Released under the MIT license.
* http://jquery.org/license
*/
//>>label: Spinner
//>>group: Widgets
//>>description: Displays buttons to easily input numbers via the keyboard or mouse.
//>>docs: http://api.jqueryui.com/spinner/
//>>demos: http://jqueryui.com/spinner/
//>>css.structure: ../../themes/base/core.css
//>>css.structure: ../../themes/base/spinner.css
//>>css.theme: ../../themes/base/theme.css
function spinnerModifier( fn ) {
return function() {
var previous = this.element.val();
fn.apply( this, arguments );
this._refresh();
if ( previous !== this.element.val() ) {
this._trigger( "change" );
}
};
}
$.widget( "ui.spinner", {
version: "1.13.0",
defaultElement: " ",
widgetEventPrefix: "spin",
options: {
classes: {
"ui-spinner": "ui-corner-all",
"ui-spinner-down": "ui-corner-br",
"ui-spinner-up": "ui-corner-tr"
},
culture: null,
icons: {
down: "ui-icon-triangle-1-s",
up: "ui-icon-triangle-1-n"
},
incremental: true,
max: null,
min: null,
numberFormat: null,
page: 10,
step: 1,
change: null,
spin: null,
start: null,
stop: null
},
_create: function() {
// handle string values that need to be parsed
this._setOption( "max", this.options.max );
this._setOption( "min", this.options.min );
this._setOption( "step", this.options.step );
// Only format if there is a value, prevents the field from being marked
// as invalid in Firefox, see #9573.
if ( this.value() !== "" ) {
// Format the value, but don't constrain.
this._value( this.element.val(), true );
}
this._draw();
this._on( this._events );
this._refresh();
// Turning off autocomplete prevents the browser from remembering the
// value when navigating through history, so we re-enable autocomplete
// if the page is unloaded before the widget is destroyed. #7790
this._on( this.window, {
beforeunload: function() {
this.element.removeAttr( "autocomplete" );
}
} );
},
_getCreateOptions: function() {
var options = this._super();
var element = this.element;
$.each( [ "min", "max", "step" ], function( i, option ) {
var value = element.attr( option );
if ( value != null && value.length ) {
options[ option ] = value;
}
} );
return options;
},
_events: {
keydown: function( event ) {
if ( this._start( event ) && this._keydown( event ) ) {
event.preventDefault();
}
},
keyup: "_stop",
focus: function() {
this.previous = this.element.val();
},
blur: function( event ) {
if ( this.cancelBlur ) {
delete this.cancelBlur;
return;
}
this._stop();
this._refresh();
if ( this.previous !== this.element.val() ) {
this._trigger( "change", event );
}
},
mousewheel: function( event, delta ) {
var activeElement = $.ui.safeActiveElement( this.document[ 0 ] );
var isActive = this.element[ 0 ] === activeElement;
if ( !isActive || !delta ) {
return;
}
if ( !this.spinning && !this._start( event ) ) {
return false;
}
this._spin( ( delta > 0 ? 1 : -1 ) * this.options.step, event );
clearTimeout( this.mousewheelTimer );
this.mousewheelTimer = this._delay( function() {
if ( this.spinning ) {
this._stop( event );
}
}, 100 );
event.preventDefault();
},
"mousedown .ui-spinner-button": function( event ) {
var previous;
// We never want the buttons to have focus; whenever the user is
// interacting with the spinner, the focus should be on the input.
// If the input is focused then this.previous is properly set from
// when the input first received focus. If the input is not focused
// then we need to set this.previous based on the value before spinning.
previous = this.element[ 0 ] === $.ui.safeActiveElement( this.document[ 0 ] ) ?
this.previous : this.element.val();
function checkFocus() {
var isActive = this.element[ 0 ] === $.ui.safeActiveElement( this.document[ 0 ] );
if ( !isActive ) {
this.element.trigger( "focus" );
this.previous = previous;
// support: IE
// IE sets focus asynchronously, so we need to check if focus
// moved off of the input because the user clicked on the button.
this._delay( function() {
this.previous = previous;
} );
}
}
// Ensure focus is on (or stays on) the text field
event.preventDefault();
checkFocus.call( this );
// Support: IE
// IE doesn't prevent moving focus even with event.preventDefault()
// so we set a flag to know when we should ignore the blur event
// and check (again) if focus moved off of the input.
this.cancelBlur = true;
this._delay( function() {
delete this.cancelBlur;
checkFocus.call( this );
} );
if ( this._start( event ) === false ) {
return;
}
this._repeat( null, $( event.currentTarget )
.hasClass( "ui-spinner-up" ) ? 1 : -1, event );
},
"mouseup .ui-spinner-button": "_stop",
"mouseenter .ui-spinner-button": function( event ) {
// button will add ui-state-active if mouse was down while mouseleave and kept down
if ( !$( event.currentTarget ).hasClass( "ui-state-active" ) ) {
return;
}
if ( this._start( event ) === false ) {
return false;
}
this._repeat( null, $( event.currentTarget )
.hasClass( "ui-spinner-up" ) ? 1 : -1, event );
},
// TODO: do we really want to consider this a stop?
// shouldn't we just stop the repeater and wait until mouseup before
// we trigger the stop event?
"mouseleave .ui-spinner-button": "_stop"
},
// Support mobile enhanced option and make backcompat more sane
_enhance: function() {
this.uiSpinner = this.element
.attr( "autocomplete", "off" )
.wrap( "" )
.parent()
// Add buttons
.append(
" "
);
},
_draw: function() {
this._enhance();
this._addClass( this.uiSpinner, "ui-spinner", "ui-widget ui-widget-content" );
this._addClass( "ui-spinner-input" );
this.element.attr( "role", "spinbutton" );
// Button bindings
this.buttons = this.uiSpinner.children( "a" )
.attr( "tabIndex", -1 )
.attr( "aria-hidden", true )
.button( {
classes: {
"ui-button": ""
}
} );
// TODO: Right now button does not support classes this is already updated in button PR
this._removeClass( this.buttons, "ui-corner-all" );
this._addClass( this.buttons.first(), "ui-spinner-button ui-spinner-up" );
this._addClass( this.buttons.last(), "ui-spinner-button ui-spinner-down" );
this.buttons.first().button( {
"icon": this.options.icons.up,
"showLabel": false
} );
this.buttons.last().button( {
"icon": this.options.icons.down,
"showLabel": false
} );
// IE 6 doesn't understand height: 50% for the buttons
// unless the wrapper has an explicit height
if ( this.buttons.height() > Math.ceil( this.uiSpinner.height() * 0.5 ) &&
this.uiSpinner.height() > 0 ) {
this.uiSpinner.height( this.uiSpinner.height() );
}
},
_keydown: function( event ) {
var options = this.options,
keyCode = $.ui.keyCode;
switch ( event.keyCode ) {
case keyCode.UP:
this._repeat( null, 1, event );
return true;
case keyCode.DOWN:
this._repeat( null, -1, event );
return true;
case keyCode.PAGE_UP:
this._repeat( null, options.page, event );
return true;
case keyCode.PAGE_DOWN:
this._repeat( null, -options.page, event );
return true;
}
return false;
},
_start: function( event ) {
if ( !this.spinning && this._trigger( "start", event ) === false ) {
return false;
}
if ( !this.counter ) {
this.counter = 1;
}
this.spinning = true;
return true;
},
_repeat: function( i, steps, event ) {
i = i || 500;
clearTimeout( this.timer );
this.timer = this._delay( function() {
this._repeat( 40, steps, event );
}, i );
this._spin( steps * this.options.step, event );
},
_spin: function( step, event ) {
var value = this.value() || 0;
if ( !this.counter ) {
this.counter = 1;
}
value = this._adjustValue( value + step * this._increment( this.counter ) );
if ( !this.spinning || this._trigger( "spin", event, { value: value } ) !== false ) {
this._value( value );
this.counter++;
}
},
_increment: function( i ) {
var incremental = this.options.incremental;
if ( incremental ) {
return typeof incremental === "function" ?
incremental( i ) :
Math.floor( i * i * i / 50000 - i * i / 500 + 17 * i / 200 + 1 );
}
return 1;
},
_precision: function() {
var precision = this._precisionOf( this.options.step );
if ( this.options.min !== null ) {
precision = Math.max( precision, this._precisionOf( this.options.min ) );
}
return precision;
},
_precisionOf: function( num ) {
var str = num.toString(),
decimal = str.indexOf( "." );
return decimal === -1 ? 0 : str.length - decimal - 1;
},
_adjustValue: function( value ) {
var base, aboveMin,
options = this.options;
// Make sure we're at a valid step
// - find out where we are relative to the base (min or 0)
base = options.min !== null ? options.min : 0;
aboveMin = value - base;
// - round to the nearest step
aboveMin = Math.round( aboveMin / options.step ) * options.step;
// - rounding is based on 0, so adjust back to our base
value = base + aboveMin;
// Fix precision from bad JS floating point math
value = parseFloat( value.toFixed( this._precision() ) );
// Clamp the value
if ( options.max !== null && value > options.max ) {
return options.max;
}
if ( options.min !== null && value < options.min ) {
return options.min;
}
return value;
},
_stop: function( event ) {
if ( !this.spinning ) {
return;
}
clearTimeout( this.timer );
clearTimeout( this.mousewheelTimer );
this.counter = 0;
this.spinning = false;
this._trigger( "stop", event );
},
_setOption: function( key, value ) {
var prevValue, first, last;
if ( key === "culture" || key === "numberFormat" ) {
prevValue = this._parse( this.element.val() );
this.options[ key ] = value;
this.element.val( this._format( prevValue ) );
return;
}
if ( key === "max" || key === "min" || key === "step" ) {
if ( typeof value === "string" ) {
value = this._parse( value );
}
}
if ( key === "icons" ) {
first = this.buttons.first().find( ".ui-icon" );
this._removeClass( first, null, this.options.icons.up );
this._addClass( first, null, value.up );
last = this.buttons.last().find( ".ui-icon" );
this._removeClass( last, null, this.options.icons.down );
this._addClass( last, null, value.down );
}
this._super( key, value );
},
_setOptionDisabled: function( value ) {
this._super( value );
this._toggleClass( this.uiSpinner, null, "ui-state-disabled", !!value );
this.element.prop( "disabled", !!value );
this.buttons.button( value ? "disable" : "enable" );
},
_setOptions: spinnerModifier( function( options ) {
this._super( options );
} ),
_parse: function( val ) {
if ( typeof val === "string" && val !== "" ) {
val = window.Globalize && this.options.numberFormat ?
Globalize.parseFloat( val, 10, this.options.culture ) : +val;
}
return val === "" || isNaN( val ) ? null : val;
},
_format: function( value ) {
if ( value === "" ) {
return "";
}
return window.Globalize && this.options.numberFormat ?
Globalize.format( value, this.options.numberFormat, this.options.culture ) :
value;
},
_refresh: function() {
this.element.attr( {
"aria-valuemin": this.options.min,
"aria-valuemax": this.options.max,
// TODO: what should we do with values that can't be parsed?
"aria-valuenow": this._parse( this.element.val() )
} );
},
isValid: function() {
var value = this.value();
// Null is invalid
if ( value === null ) {
return false;
}
// If value gets adjusted, it's invalid
return value === this._adjustValue( value );
},
// Update the value without triggering change
_value: function( value, allowAny ) {
var parsed;
if ( value !== "" ) {
parsed = this._parse( value );
if ( parsed !== null ) {
if ( !allowAny ) {
parsed = this._adjustValue( parsed );
}
value = this._format( parsed );
}
}
this.element.val( value );
this._refresh();
},
_destroy: function() {
this.element
.prop( "disabled", false )
.removeAttr( "autocomplete role aria-valuemin aria-valuemax aria-valuenow" );
this.uiSpinner.replaceWith( this.element );
},
stepUp: spinnerModifier( function( steps ) {
this._stepUp( steps );
} ),
_stepUp: function( steps ) {
if ( this._start() ) {
this._spin( ( steps || 1 ) * this.options.step );
this._stop();
}
},
stepDown: spinnerModifier( function( steps ) {
this._stepDown( steps );
} ),
_stepDown: function( steps ) {
if ( this._start() ) {
this._spin( ( steps || 1 ) * -this.options.step );
this._stop();
}
},
pageUp: spinnerModifier( function( pages ) {
this._stepUp( ( pages || 1 ) * this.options.page );
} ),
pageDown: spinnerModifier( function( pages ) {
this._stepDown( ( pages || 1 ) * this.options.page );
} ),
value: function( newVal ) {
if ( !arguments.length ) {
return this._parse( this.element.val() );
}
spinnerModifier( this._value ).call( this, newVal );
},
widget: function() {
return this.uiSpinner;
}
} );
// DEPRECATED
// TODO: switch return back to widget declaration at top of file when this is removed
if ( $.uiBackCompat !== false ) {
// Backcompat for spinner html extension points
$.widget( "ui.spinner", $.ui.spinner, {
_enhance: function() {
this.uiSpinner = this.element
.attr( "autocomplete", "off" )
.wrap( this._uiSpinnerHtml() )
.parent()
// Add buttons
.append( this._buttonHtml() );
},
_uiSpinnerHtml: function() {
return "";
},
_buttonHtml: function() {
return " ";
}
} );
}
var widgetsSpinner = $.ui.spinner;
/*!
* jQuery UI Tabs 1.13.0
* http://jqueryui.com
*
* Copyright jQuery Foundation and other contributors
* Released under the MIT license.
* http://jquery.org/license
*/
//>>label: Tabs
//>>group: Widgets
//>>description: Transforms a set of container elements into a tab structure.
//>>docs: http://api.jqueryui.com/tabs/
//>>demos: http://jqueryui.com/tabs/
//>>css.structure: ../../themes/base/core.css
//>>css.structure: ../../themes/base/tabs.css
//>>css.theme: ../../themes/base/theme.css
$.widget( "ui.tabs", {
version: "1.13.0",
delay: 300,
options: {
active: null,
classes: {
"ui-tabs": "ui-corner-all",
"ui-tabs-nav": "ui-corner-all",
"ui-tabs-panel": "ui-corner-bottom",
"ui-tabs-tab": "ui-corner-top"
},
collapsible: false,
event: "click",
heightStyle: "content",
hide: null,
show: null,
// Callbacks
activate: null,
beforeActivate: null,
beforeLoad: null,
load: null
},
_isLocal: ( function() {
var rhash = /#.*$/;
return function( anchor ) {
var anchorUrl, locationUrl;
anchorUrl = anchor.href.replace( rhash, "" );
locationUrl = location.href.replace( rhash, "" );
// Decoding may throw an error if the URL isn't UTF-8 (#9518)
try {
anchorUrl = decodeURIComponent( anchorUrl );
} catch ( error ) {}
try {
locationUrl = decodeURIComponent( locationUrl );
} catch ( error ) {}
return anchor.hash.length > 1 && anchorUrl === locationUrl;
};
} )(),
_create: function() {
var that = this,
options = this.options;
this.running = false;
this._addClass( "ui-tabs", "ui-widget ui-widget-content" );
this._toggleClass( "ui-tabs-collapsible", null, options.collapsible );
this._processTabs();
options.active = this._initialActive();
// Take disabling tabs via class attribute from HTML
// into account and update option properly.
if ( Array.isArray( options.disabled ) ) {
options.disabled = $.uniqueSort( options.disabled.concat(
$.map( this.tabs.filter( ".ui-state-disabled" ), function( li ) {
return that.tabs.index( li );
} )
) ).sort();
}
// Check for length avoids error when initializing empty list
if ( this.options.active !== false && this.anchors.length ) {
this.active = this._findActive( options.active );
} else {
this.active = $();
}
this._refresh();
if ( this.active.length ) {
this.load( options.active );
}
},
_initialActive: function() {
var active = this.options.active,
collapsible = this.options.collapsible,
locationHash = location.hash.substring( 1 );
if ( active === null ) {
// check the fragment identifier in the URL
if ( locationHash ) {
this.tabs.each( function( i, tab ) {
if ( $( tab ).attr( "aria-controls" ) === locationHash ) {
active = i;
return false;
}
} );
}
// Check for a tab marked active via a class
if ( active === null ) {
active = this.tabs.index( this.tabs.filter( ".ui-tabs-active" ) );
}
// No active tab, set to false
if ( active === null || active === -1 ) {
active = this.tabs.length ? 0 : false;
}
}
// Handle numbers: negative, out of range
if ( active !== false ) {
active = this.tabs.index( this.tabs.eq( active ) );
if ( active === -1 ) {
active = collapsible ? false : 0;
}
}
// Don't allow collapsible: false and active: false
if ( !collapsible && active === false && this.anchors.length ) {
active = 0;
}
return active;
},
_getCreateEventData: function() {
return {
tab: this.active,
panel: !this.active.length ? $() : this._getPanelForTab( this.active )
};
},
_tabKeydown: function( event ) {
var focusedTab = $( $.ui.safeActiveElement( this.document[ 0 ] ) ).closest( "li" ),
selectedIndex = this.tabs.index( focusedTab ),
goingForward = true;
if ( this._handlePageNav( event ) ) {
return;
}
switch ( event.keyCode ) {
case $.ui.keyCode.RIGHT:
case $.ui.keyCode.DOWN:
selectedIndex++;
break;
case $.ui.keyCode.UP:
case $.ui.keyCode.LEFT:
goingForward = false;
selectedIndex--;
break;
case $.ui.keyCode.END:
selectedIndex = this.anchors.length - 1;
break;
case $.ui.keyCode.HOME:
selectedIndex = 0;
break;
case $.ui.keyCode.SPACE:
// Activate only, no collapsing
event.preventDefault();
clearTimeout( this.activating );
this._activate( selectedIndex );
return;
case $.ui.keyCode.ENTER:
// Toggle (cancel delayed activation, allow collapsing)
event.preventDefault();
clearTimeout( this.activating );
// Determine if we should collapse or activate
this._activate( selectedIndex === this.options.active ? false : selectedIndex );
return;
default:
return;
}
// Focus the appropriate tab, based on which key was pressed
event.preventDefault();
clearTimeout( this.activating );
selectedIndex = this._focusNextTab( selectedIndex, goingForward );
// Navigating with control/command key will prevent automatic activation
if ( !event.ctrlKey && !event.metaKey ) {
// Update aria-selected immediately so that AT think the tab is already selected.
// Otherwise AT may confuse the user by stating that they need to activate the tab,
// but the tab will already be activated by the time the announcement finishes.
focusedTab.attr( "aria-selected", "false" );
this.tabs.eq( selectedIndex ).attr( "aria-selected", "true" );
this.activating = this._delay( function() {
this.option( "active", selectedIndex );
}, this.delay );
}
},
_panelKeydown: function( event ) {
if ( this._handlePageNav( event ) ) {
return;
}
// Ctrl+up moves focus to the current tab
if ( event.ctrlKey && event.keyCode === $.ui.keyCode.UP ) {
event.preventDefault();
this.active.trigger( "focus" );
}
},
// Alt+page up/down moves focus to the previous/next tab (and activates)
_handlePageNav: function( event ) {
if ( event.altKey && event.keyCode === $.ui.keyCode.PAGE_UP ) {
this._activate( this._focusNextTab( this.options.active - 1, false ) );
return true;
}
if ( event.altKey && event.keyCode === $.ui.keyCode.PAGE_DOWN ) {
this._activate( this._focusNextTab( this.options.active + 1, true ) );
return true;
}
},
_findNextTab: function( index, goingForward ) {
var lastTabIndex = this.tabs.length - 1;
function constrain() {
if ( index > lastTabIndex ) {
index = 0;
}
if ( index < 0 ) {
index = lastTabIndex;
}
return index;
}
while ( $.inArray( constrain(), this.options.disabled ) !== -1 ) {
index = goingForward ? index + 1 : index - 1;
}
return index;
},
_focusNextTab: function( index, goingForward ) {
index = this._findNextTab( index, goingForward );
this.tabs.eq( index ).trigger( "focus" );
return index;
},
_setOption: function( key, value ) {
if ( key === "active" ) {
// _activate() will handle invalid values and update this.options
this._activate( value );
return;
}
this._super( key, value );
if ( key === "collapsible" ) {
this._toggleClass( "ui-tabs-collapsible", null, value );
// Setting collapsible: false while collapsed; open first panel
if ( !value && this.options.active === false ) {
this._activate( 0 );
}
}
if ( key === "event" ) {
this._setupEvents( value );
}
if ( key === "heightStyle" ) {
this._setupHeightStyle( value );
}
},
_sanitizeSelector: function( hash ) {
return hash ? hash.replace( /[!"$%&'()*+,.\/:;<=>?@\[\]\^`{|}~]/g, "\\$&" ) : "";
},
refresh: function() {
var options = this.options,
lis = this.tablist.children( ":has(a[href])" );
// Get disabled tabs from class attribute from HTML
// this will get converted to a boolean if needed in _refresh()
options.disabled = $.map( lis.filter( ".ui-state-disabled" ), function( tab ) {
return lis.index( tab );
} );
this._processTabs();
// Was collapsed or no tabs
if ( options.active === false || !this.anchors.length ) {
options.active = false;
this.active = $();
// was active, but active tab is gone
} else if ( this.active.length && !$.contains( this.tablist[ 0 ], this.active[ 0 ] ) ) {
// all remaining tabs are disabled
if ( this.tabs.length === options.disabled.length ) {
options.active = false;
this.active = $();
// activate previous tab
} else {
this._activate( this._findNextTab( Math.max( 0, options.active - 1 ), false ) );
}
// was active, active tab still exists
} else {
// make sure active index is correct
options.active = this.tabs.index( this.active );
}
this._refresh();
},
_refresh: function() {
this._setOptionDisabled( this.options.disabled );
this._setupEvents( this.options.event );
this._setupHeightStyle( this.options.heightStyle );
this.tabs.not( this.active ).attr( {
"aria-selected": "false",
"aria-expanded": "false",
tabIndex: -1
} );
this.panels.not( this._getPanelForTab( this.active ) )
.hide()
.attr( {
"aria-hidden": "true"
} );
// Make sure one tab is in the tab order
if ( !this.active.length ) {
this.tabs.eq( 0 ).attr( "tabIndex", 0 );
} else {
this.active
.attr( {
"aria-selected": "true",
"aria-expanded": "true",
tabIndex: 0
} );
this._addClass( this.active, "ui-tabs-active", "ui-state-active" );
this._getPanelForTab( this.active )
.show()
.attr( {
"aria-hidden": "false"
} );
}
},
_processTabs: function() {
var that = this,
prevTabs = this.tabs,
prevAnchors = this.anchors,
prevPanels = this.panels;
this.tablist = this._getList().attr( "role", "tablist" );
this._addClass( this.tablist, "ui-tabs-nav",
"ui-helper-reset ui-helper-clearfix ui-widget-header" );
// Prevent users from focusing disabled tabs via click
this.tablist
.on( "mousedown" + this.eventNamespace, "> li", function( event ) {
if ( $( this ).is( ".ui-state-disabled" ) ) {
event.preventDefault();
}
} )
// Support: IE <9
// Preventing the default action in mousedown doesn't prevent IE
// from focusing the element, so if the anchor gets focused, blur.
// We don't have to worry about focusing the previously focused
// element since clicking on a non-focusable element should focus
// the body anyway.
.on( "focus" + this.eventNamespace, ".ui-tabs-anchor", function() {
if ( $( this ).closest( "li" ).is( ".ui-state-disabled" ) ) {
this.blur();
}
} );
this.tabs = this.tablist.find( "> li:has(a[href])" )
.attr( {
role: "tab",
tabIndex: -1
} );
this._addClass( this.tabs, "ui-tabs-tab", "ui-state-default" );
this.anchors = this.tabs.map( function() {
return $( "a", this )[ 0 ];
} )
.attr( {
tabIndex: -1
} );
this._addClass( this.anchors, "ui-tabs-anchor" );
this.panels = $();
this.anchors.each( function( i, anchor ) {
var selector, panel, panelId,
anchorId = $( anchor ).uniqueId().attr( "id" ),
tab = $( anchor ).closest( "li" ),
originalAriaControls = tab.attr( "aria-controls" );
// Inline tab
if ( that._isLocal( anchor ) ) {
selector = anchor.hash;
panelId = selector.substring( 1 );
panel = that.element.find( that._sanitizeSelector( selector ) );
// remote tab
} else {
// If the tab doesn't already have aria-controls,
// generate an id by using a throw-away element
panelId = tab.attr( "aria-controls" ) || $( {} ).uniqueId()[ 0 ].id;
selector = "#" + panelId;
panel = that.element.find( selector );
if ( !panel.length ) {
panel = that._createPanel( panelId );
panel.insertAfter( that.panels[ i - 1 ] || that.tablist );
}
panel.attr( "aria-live", "polite" );
}
if ( panel.length ) {
that.panels = that.panels.add( panel );
}
if ( originalAriaControls ) {
tab.data( "ui-tabs-aria-controls", originalAriaControls );
}
tab.attr( {
"aria-controls": panelId,
"aria-labelledby": anchorId
} );
panel.attr( "aria-labelledby", anchorId );
} );
this.panels.attr( "role", "tabpanel" );
this._addClass( this.panels, "ui-tabs-panel", "ui-widget-content" );
// Avoid memory leaks (#10056)
if ( prevTabs ) {
this._off( prevTabs.not( this.tabs ) );
this._off( prevAnchors.not( this.anchors ) );
this._off( prevPanels.not( this.panels ) );
}
},
// Allow overriding how to find the list for rare usage scenarios (#7715)
_getList: function() {
return this.tablist || this.element.find( "ol, ul" ).eq( 0 );
},
_createPanel: function( id ) {
return $( "" )
.attr( "id", id )
.data( "ui-tabs-destroy", true );
},
_setOptionDisabled: function( disabled ) {
var currentItem, li, i;
if ( Array.isArray( disabled ) ) {
if ( !disabled.length ) {
disabled = false;
} else if ( disabled.length === this.anchors.length ) {
disabled = true;
}
}
// Disable tabs
for ( i = 0; ( li = this.tabs[ i ] ); i++ ) {
currentItem = $( li );
if ( disabled === true || $.inArray( i, disabled ) !== -1 ) {
currentItem.attr( "aria-disabled", "true" );
this._addClass( currentItem, null, "ui-state-disabled" );
} else {
currentItem.removeAttr( "aria-disabled" );
this._removeClass( currentItem, null, "ui-state-disabled" );
}
}
this.options.disabled = disabled;
this._toggleClass( this.widget(), this.widgetFullName + "-disabled", null,
disabled === true );
},
_setupEvents: function( event ) {
var events = {};
if ( event ) {
$.each( event.split( " " ), function( index, eventName ) {
events[ eventName ] = "_eventHandler";
} );
}
this._off( this.anchors.add( this.tabs ).add( this.panels ) );
// Always prevent the default action, even when disabled
this._on( true, this.anchors, {
click: function( event ) {
event.preventDefault();
}
} );
this._on( this.anchors, events );
this._on( this.tabs, { keydown: "_tabKeydown" } );
this._on( this.panels, { keydown: "_panelKeydown" } );
this._focusable( this.tabs );
this._hoverable( this.tabs );
},
_setupHeightStyle: function( heightStyle ) {
var maxHeight,
parent = this.element.parent();
if ( heightStyle === "fill" ) {
maxHeight = parent.height();
maxHeight -= this.element.outerHeight() - this.element.height();
this.element.siblings( ":visible" ).each( function() {
var elem = $( this ),
position = elem.css( "position" );
if ( position === "absolute" || position === "fixed" ) {
return;
}
maxHeight -= elem.outerHeight( true );
} );
this.element.children().not( this.panels ).each( function() {
maxHeight -= $( this ).outerHeight( true );
} );
this.panels.each( function() {
$( this ).height( Math.max( 0, maxHeight -
$( this ).innerHeight() + $( this ).height() ) );
} )
.css( "overflow", "auto" );
} else if ( heightStyle === "auto" ) {
maxHeight = 0;
this.panels.each( function() {
maxHeight = Math.max( maxHeight, $( this ).height( "" ).height() );
} ).height( maxHeight );
}
},
_eventHandler: function( event ) {
var options = this.options,
active = this.active,
anchor = $( event.currentTarget ),
tab = anchor.closest( "li" ),
clickedIsActive = tab[ 0 ] === active[ 0 ],
collapsing = clickedIsActive && options.collapsible,
toShow = collapsing ? $() : this._getPanelForTab( tab ),
toHide = !active.length ? $() : this._getPanelForTab( active ),
eventData = {
oldTab: active,
oldPanel: toHide,
newTab: collapsing ? $() : tab,
newPanel: toShow
};
event.preventDefault();
if ( tab.hasClass( "ui-state-disabled" ) ||
// tab is already loading
tab.hasClass( "ui-tabs-loading" ) ||
// can't switch durning an animation
this.running ||
// click on active header, but not collapsible
( clickedIsActive && !options.collapsible ) ||
// allow canceling activation
( this._trigger( "beforeActivate", event, eventData ) === false ) ) {
return;
}
options.active = collapsing ? false : this.tabs.index( tab );
this.active = clickedIsActive ? $() : tab;
if ( this.xhr ) {
this.xhr.abort();
}
if ( !toHide.length && !toShow.length ) {
$.error( "jQuery UI Tabs: Mismatching fragment identifier." );
}
if ( toShow.length ) {
this.load( this.tabs.index( tab ), event );
}
this._toggle( event, eventData );
},
// Handles show/hide for selecting tabs
_toggle: function( event, eventData ) {
var that = this,
toShow = eventData.newPanel,
toHide = eventData.oldPanel;
this.running = true;
function complete() {
that.running = false;
that._trigger( "activate", event, eventData );
}
function show() {
that._addClass( eventData.newTab.closest( "li" ), "ui-tabs-active", "ui-state-active" );
if ( toShow.length && that.options.show ) {
that._show( toShow, that.options.show, complete );
} else {
toShow.show();
complete();
}
}
// Start out by hiding, then showing, then completing
if ( toHide.length && this.options.hide ) {
this._hide( toHide, this.options.hide, function() {
that._removeClass( eventData.oldTab.closest( "li" ),
"ui-tabs-active", "ui-state-active" );
show();
} );
} else {
this._removeClass( eventData.oldTab.closest( "li" ),
"ui-tabs-active", "ui-state-active" );
toHide.hide();
show();
}
toHide.attr( "aria-hidden", "true" );
eventData.oldTab.attr( {
"aria-selected": "false",
"aria-expanded": "false"
} );
// If we're switching tabs, remove the old tab from the tab order.
// If we're opening from collapsed state, remove the previous tab from the tab order.
// If we're collapsing, then keep the collapsing tab in the tab order.
if ( toShow.length && toHide.length ) {
eventData.oldTab.attr( "tabIndex", -1 );
} else if ( toShow.length ) {
this.tabs.filter( function() {
return $( this ).attr( "tabIndex" ) === 0;
} )
.attr( "tabIndex", -1 );
}
toShow.attr( "aria-hidden", "false" );
eventData.newTab.attr( {
"aria-selected": "true",
"aria-expanded": "true",
tabIndex: 0
} );
},
_activate: function( index ) {
var anchor,
active = this._findActive( index );
// Trying to activate the already active panel
if ( active[ 0 ] === this.active[ 0 ] ) {
return;
}
// Trying to collapse, simulate a click on the current active header
if ( !active.length ) {
active = this.active;
}
anchor = active.find( ".ui-tabs-anchor" )[ 0 ];
this._eventHandler( {
target: anchor,
currentTarget: anchor,
preventDefault: $.noop
} );
},
_findActive: function( index ) {
return index === false ? $() : this.tabs.eq( index );
},
_getIndex: function( index ) {
// meta-function to give users option to provide a href string instead of a numerical index.
if ( typeof index === "string" ) {
index = this.anchors.index( this.anchors.filter( "[href$='" +
$.escapeSelector( index ) + "']" ) );
}
return index;
},
_destroy: function() {
if ( this.xhr ) {
this.xhr.abort();
}
this.tablist
.removeAttr( "role" )
.off( this.eventNamespace );
this.anchors
.removeAttr( "role tabIndex" )
.removeUniqueId();
this.tabs.add( this.panels ).each( function() {
if ( $.data( this, "ui-tabs-destroy" ) ) {
$( this ).remove();
} else {
$( this ).removeAttr( "role tabIndex " +
"aria-live aria-busy aria-selected aria-labelledby aria-hidden aria-expanded" );
}
} );
this.tabs.each( function() {
var li = $( this ),
prev = li.data( "ui-tabs-aria-controls" );
if ( prev ) {
li
.attr( "aria-controls", prev )
.removeData( "ui-tabs-aria-controls" );
} else {
li.removeAttr( "aria-controls" );
}
} );
this.panels.show();
if ( this.options.heightStyle !== "content" ) {
this.panels.css( "height", "" );
}
},
enable: function( index ) {
var disabled = this.options.disabled;
if ( disabled === false ) {
return;
}
if ( index === undefined ) {
disabled = false;
} else {
index = this._getIndex( index );
if ( Array.isArray( disabled ) ) {
disabled = $.map( disabled, function( num ) {
return num !== index ? num : null;
} );
} else {
disabled = $.map( this.tabs, function( li, num ) {
return num !== index ? num : null;
} );
}
}
this._setOptionDisabled( disabled );
},
disable: function( index ) {
var disabled = this.options.disabled;
if ( disabled === true ) {
return;
}
if ( index === undefined ) {
disabled = true;
} else {
index = this._getIndex( index );
if ( $.inArray( index, disabled ) !== -1 ) {
return;
}
if ( Array.isArray( disabled ) ) {
disabled = $.merge( [ index ], disabled ).sort();
} else {
disabled = [ index ];
}
}
this._setOptionDisabled( disabled );
},
load: function( index, event ) {
index = this._getIndex( index );
var that = this,
tab = this.tabs.eq( index ),
anchor = tab.find( ".ui-tabs-anchor" ),
panel = this._getPanelForTab( tab ),
eventData = {
tab: tab,
panel: panel
},
complete = function( jqXHR, status ) {
if ( status === "abort" ) {
that.panels.stop( false, true );
}
that._removeClass( tab, "ui-tabs-loading" );
panel.removeAttr( "aria-busy" );
if ( jqXHR === that.xhr ) {
delete that.xhr;
}
};
// Not remote
if ( this._isLocal( anchor[ 0 ] ) ) {
return;
}
this.xhr = $.ajax( this._ajaxSettings( anchor, event, eventData ) );
// Support: jQuery <1.8
// jQuery <1.8 returns false if the request is canceled in beforeSend,
// but as of 1.8, $.ajax() always returns a jqXHR object.
if ( this.xhr && this.xhr.statusText !== "canceled" ) {
this._addClass( tab, "ui-tabs-loading" );
panel.attr( "aria-busy", "true" );
this.xhr
.done( function( response, status, jqXHR ) {
// support: jQuery <1.8
// http://bugs.jquery.com/ticket/11778
setTimeout( function() {
panel.html( response );
that._trigger( "load", event, eventData );
complete( jqXHR, status );
}, 1 );
} )
.fail( function( jqXHR, status ) {
// support: jQuery <1.8
// http://bugs.jquery.com/ticket/11778
setTimeout( function() {
complete( jqXHR, status );
}, 1 );
} );
}
},
_ajaxSettings: function( anchor, event, eventData ) {
var that = this;
return {
// Support: IE <11 only
// Strip any hash that exists to prevent errors with the Ajax request
url: anchor.attr( "href" ).replace( /#.*$/, "" ),
beforeSend: function( jqXHR, settings ) {
return that._trigger( "beforeLoad", event,
$.extend( { jqXHR: jqXHR, ajaxSettings: settings }, eventData ) );
}
};
},
_getPanelForTab: function( tab ) {
var id = $( tab ).attr( "aria-controls" );
return this.element.find( this._sanitizeSelector( "#" + id ) );
}
} );
// DEPRECATED
// TODO: Switch return back to widget declaration at top of file when this is removed
if ( $.uiBackCompat !== false ) {
// Backcompat for ui-tab class (now ui-tabs-tab)
$.widget( "ui.tabs", $.ui.tabs, {
_processTabs: function() {
this._superApply( arguments );
this._addClass( this.tabs, "ui-tab" );
}
} );
}
var widgetsTabs = $.ui.tabs;
/*!
* jQuery UI Tooltip 1.13.0
* http://jqueryui.com
*
* Copyright jQuery Foundation and other contributors
* Released under the MIT license.
* http://jquery.org/license
*/
//>>label: Tooltip
//>>group: Widgets
//>>description: Shows additional information for any element on hover or focus.
//>>docs: http://api.jqueryui.com/tooltip/
//>>demos: http://jqueryui.com/tooltip/
//>>css.structure: ../../themes/base/core.css
//>>css.structure: ../../themes/base/tooltip.css
//>>css.theme: ../../themes/base/theme.css
$.widget( "ui.tooltip", {
version: "1.13.0",
options: {
classes: {
"ui-tooltip": "ui-corner-all ui-widget-shadow"
},
content: function() {
var title = $( this ).attr( "title" );
// Escape title, since we're going from an attribute to raw HTML
return $( "
" ).text( title ).html();
},
hide: true,
// Disabled elements have inconsistent behavior across browsers (#8661)
items: "[title]:not([disabled])",
position: {
my: "left top+15",
at: "left bottom",
collision: "flipfit flip"
},
show: true,
track: false,
// Callbacks
close: null,
open: null
},
_addDescribedBy: function( elem, id ) {
var describedby = ( elem.attr( "aria-describedby" ) || "" ).split( /\s+/ );
describedby.push( id );
elem
.data( "ui-tooltip-id", id )
.attr( "aria-describedby", String.prototype.trim.call( describedby.join( " " ) ) );
},
_removeDescribedBy: function( elem ) {
var id = elem.data( "ui-tooltip-id" ),
describedby = ( elem.attr( "aria-describedby" ) || "" ).split( /\s+/ ),
index = $.inArray( id, describedby );
if ( index !== -1 ) {
describedby.splice( index, 1 );
}
elem.removeData( "ui-tooltip-id" );
describedby = String.prototype.trim.call( describedby.join( " " ) );
if ( describedby ) {
elem.attr( "aria-describedby", describedby );
} else {
elem.removeAttr( "aria-describedby" );
}
},
_create: function() {
this._on( {
mouseover: "open",
focusin: "open"
} );
// IDs of generated tooltips, needed for destroy
this.tooltips = {};
// IDs of parent tooltips where we removed the title attribute
this.parents = {};
// Append the aria-live region so tooltips announce correctly
this.liveRegion = $( "" )
.attr( {
role: "log",
"aria-live": "assertive",
"aria-relevant": "additions"
} )
.appendTo( this.document[ 0 ].body );
this._addClass( this.liveRegion, null, "ui-helper-hidden-accessible" );
this.disabledTitles = $( [] );
},
_setOption: function( key, value ) {
var that = this;
this._super( key, value );
if ( key === "content" ) {
$.each( this.tooltips, function( id, tooltipData ) {
that._updateContent( tooltipData.element );
} );
}
},
_setOptionDisabled: function( value ) {
this[ value ? "_disable" : "_enable" ]();
},
_disable: function() {
var that = this;
// Close open tooltips
$.each( this.tooltips, function( id, tooltipData ) {
var event = $.Event( "blur" );
event.target = event.currentTarget = tooltipData.element[ 0 ];
that.close( event, true );
} );
// Remove title attributes to prevent native tooltips
this.disabledTitles = this.disabledTitles.add(
this.element.find( this.options.items ).addBack()
.filter( function() {
var element = $( this );
if ( element.is( "[title]" ) ) {
return element
.data( "ui-tooltip-title", element.attr( "title" ) )
.removeAttr( "title" );
}
} )
);
},
_enable: function() {
// restore title attributes
this.disabledTitles.each( function() {
var element = $( this );
if ( element.data( "ui-tooltip-title" ) ) {
element.attr( "title", element.data( "ui-tooltip-title" ) );
}
} );
this.disabledTitles = $( [] );
},
open: function( event ) {
var that = this,
target = $( event ? event.target : this.element )
// we need closest here due to mouseover bubbling,
// but always pointing at the same event target
.closest( this.options.items );
// No element to show a tooltip for or the tooltip is already open
if ( !target.length || target.data( "ui-tooltip-id" ) ) {
return;
}
if ( target.attr( "title" ) ) {
target.data( "ui-tooltip-title", target.attr( "title" ) );
}
target.data( "ui-tooltip-open", true );
// Kill parent tooltips, custom or native, for hover
if ( event && event.type === "mouseover" ) {
target.parents().each( function() {
var parent = $( this ),
blurEvent;
if ( parent.data( "ui-tooltip-open" ) ) {
blurEvent = $.Event( "blur" );
blurEvent.target = blurEvent.currentTarget = this;
that.close( blurEvent, true );
}
if ( parent.attr( "title" ) ) {
parent.uniqueId();
that.parents[ this.id ] = {
element: this,
title: parent.attr( "title" )
};
parent.attr( "title", "" );
}
} );
}
this._registerCloseHandlers( event, target );
this._updateContent( target, event );
},
_updateContent: function( target, event ) {
var content,
contentOption = this.options.content,
that = this,
eventType = event ? event.type : null;
if ( typeof contentOption === "string" || contentOption.nodeType ||
contentOption.jquery ) {
return this._open( event, target, contentOption );
}
content = contentOption.call( target[ 0 ], function( response ) {
// IE may instantly serve a cached response for ajax requests
// delay this call to _open so the other call to _open runs first
that._delay( function() {
// Ignore async response if tooltip was closed already
if ( !target.data( "ui-tooltip-open" ) ) {
return;
}
// JQuery creates a special event for focusin when it doesn't
// exist natively. To improve performance, the native event
// object is reused and the type is changed. Therefore, we can't
// rely on the type being correct after the event finished
// bubbling, so we set it back to the previous value. (#8740)
if ( event ) {
event.type = eventType;
}
this._open( event, target, response );
} );
} );
if ( content ) {
this._open( event, target, content );
}
},
_open: function( event, target, content ) {
var tooltipData, tooltip, delayedShow, a11yContent,
positionOption = $.extend( {}, this.options.position );
if ( !content ) {
return;
}
// Content can be updated multiple times. If the tooltip already
// exists, then just update the content and bail.
tooltipData = this._find( target );
if ( tooltipData ) {
tooltipData.tooltip.find( ".ui-tooltip-content" ).html( content );
return;
}
// If we have a title, clear it to prevent the native tooltip
// we have to check first to avoid defining a title if none exists
// (we don't want to cause an element to start matching [title])
//
// We use removeAttr only for key events, to allow IE to export the correct
// accessible attributes. For mouse events, set to empty string to avoid
// native tooltip showing up (happens only when removing inside mouseover).
if ( target.is( "[title]" ) ) {
if ( event && event.type === "mouseover" ) {
target.attr( "title", "" );
} else {
target.removeAttr( "title" );
}
}
tooltipData = this._tooltip( target );
tooltip = tooltipData.tooltip;
this._addDescribedBy( target, tooltip.attr( "id" ) );
tooltip.find( ".ui-tooltip-content" ).html( content );
// Support: Voiceover on OS X, JAWS on IE <= 9
// JAWS announces deletions even when aria-relevant="additions"
// Voiceover will sometimes re-read the entire log region's contents from the beginning
this.liveRegion.children().hide();
a11yContent = $( "
" ).html( tooltip.find( ".ui-tooltip-content" ).html() );
a11yContent.removeAttr( "name" ).find( "[name]" ).removeAttr( "name" );
a11yContent.removeAttr( "id" ).find( "[id]" ).removeAttr( "id" );
a11yContent.appendTo( this.liveRegion );
function position( event ) {
positionOption.of = event;
if ( tooltip.is( ":hidden" ) ) {
return;
}
tooltip.position( positionOption );
}
if ( this.options.track && event && /^mouse/.test( event.type ) ) {
this._on( this.document, {
mousemove: position
} );
// trigger once to override element-relative positioning
position( event );
} else {
tooltip.position( $.extend( {
of: target
}, this.options.position ) );
}
tooltip.hide();
this._show( tooltip, this.options.show );
// Handle tracking tooltips that are shown with a delay (#8644). As soon
// as the tooltip is visible, position the tooltip using the most recent
// event.
// Adds the check to add the timers only when both delay and track options are set (#14682)
if ( this.options.track && this.options.show && this.options.show.delay ) {
delayedShow = this.delayedShow = setInterval( function() {
if ( tooltip.is( ":visible" ) ) {
position( positionOption.of );
clearInterval( delayedShow );
}
}, 13 );
}
this._trigger( "open", event, { tooltip: tooltip } );
},
_registerCloseHandlers: function( event, target ) {
var events = {
keyup: function( event ) {
if ( event.keyCode === $.ui.keyCode.ESCAPE ) {
var fakeEvent = $.Event( event );
fakeEvent.currentTarget = target[ 0 ];
this.close( fakeEvent, true );
}
}
};
// Only bind remove handler for delegated targets. Non-delegated
// tooltips will handle this in destroy.
if ( target[ 0 ] !== this.element[ 0 ] ) {
events.remove = function() {
this._removeTooltip( this._find( target ).tooltip );
};
}
if ( !event || event.type === "mouseover" ) {
events.mouseleave = "close";
}
if ( !event || event.type === "focusin" ) {
events.focusout = "close";
}
this._on( true, target, events );
},
close: function( event ) {
var tooltip,
that = this,
target = $( event ? event.currentTarget : this.element ),
tooltipData = this._find( target );
// The tooltip may already be closed
if ( !tooltipData ) {
// We set ui-tooltip-open immediately upon open (in open()), but only set the
// additional data once there's actually content to show (in _open()). So even if the
// tooltip doesn't have full data, we always remove ui-tooltip-open in case we're in
// the period between open() and _open().
target.removeData( "ui-tooltip-open" );
return;
}
tooltip = tooltipData.tooltip;
// Disabling closes the tooltip, so we need to track when we're closing
// to avoid an infinite loop in case the tooltip becomes disabled on close
if ( tooltipData.closing ) {
return;
}
// Clear the interval for delayed tracking tooltips
clearInterval( this.delayedShow );
// Only set title if we had one before (see comment in _open())
// If the title attribute has changed since open(), don't restore
if ( target.data( "ui-tooltip-title" ) && !target.attr( "title" ) ) {
target.attr( "title", target.data( "ui-tooltip-title" ) );
}
this._removeDescribedBy( target );
tooltipData.hiding = true;
tooltip.stop( true );
this._hide( tooltip, this.options.hide, function() {
that._removeTooltip( $( this ) );
} );
target.removeData( "ui-tooltip-open" );
this._off( target, "mouseleave focusout keyup" );
// Remove 'remove' binding only on delegated targets
if ( target[ 0 ] !== this.element[ 0 ] ) {
this._off( target, "remove" );
}
this._off( this.document, "mousemove" );
if ( event && event.type === "mouseleave" ) {
$.each( this.parents, function( id, parent ) {
$( parent.element ).attr( "title", parent.title );
delete that.parents[ id ];
} );
}
tooltipData.closing = true;
this._trigger( "close", event, { tooltip: tooltip } );
if ( !tooltipData.hiding ) {
tooltipData.closing = false;
}
},
_tooltip: function( element ) {
var tooltip = $( "
" ).attr( "role", "tooltip" ),
content = $( "
" ).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;
;
!function(e){function t(i){if(n[i])return n[i].exports;var o=n[i]={i:i,l:!1,exports:{}};return e[i].call(o.exports,o,o.exports,t),o.l=!0,o.exports}var n={};t.m=e,t.c=n,t.d=function(e,n,i){t.o(e,n)||Object.defineProperty(e,n,{configurable:!1,enumerable:!0,get:i})},t.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(n,"a",n),n},t.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},t.p="",t(t.s=3)}([function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var i=t.curry=function(e){var t=e.length;return function n(){var i=Array.prototype.slice.call(arguments,0);return i.length>=t?e.apply(null,i):function(){var e=Array.prototype.slice.call(arguments,0);return n.apply(null,i.concat(e))}}},o=(t.compose=function(){for(var e=arguments.length,t=Array(e),n=0;n'+r.options.noDropzone+"
");var a=w(r.draggables,r.dropZones,r.$noDropZone[0]),s=function(e){for(var t=0;t
'+e.options.question.settings.questionTitle+""),e.setIntroduction(e.$introduction));var t="";if(void 0!==this.options.question.settings.background&&(t+="h5p-dragquestion-has-no-background"),"always"===e.options.behaviour.dropZoneHighlighting&&(t&&(t+=" "),t+="h5p-dq-highlight-dz-always"),e.setContent(e.createQuestionContent(),{class:t}),!1!==H5P.canHasFullScreen&&this.options.behaviour.enableFullScreen){var n=function(){H5P.isFullscreen?H5P.exitFullScreen(e.$container):H5P.fullScreen(e.$container.parent().parent(),e)},i=y("
",{class:"h5p-my-fullscreen-button-enter",title:this.options.localize.fullscreen,role:"button",tabindex:0,on:{click:n,keypress:function(e){13!==e.which&&32!==e.which||(n(),e.preventDefault())}},prependTo:this.$container.parent()});this.on("enterFullScreen",function(){i.attr("class","h5p-my-fullscreen-button-exit"),i.attr("title",this.options.localize.exitFullscreen)}),this.on("exitFullScreen",function(){i.attr("class","h5p-my-fullscreen-button-enter"),i.attr("title",this.options.localize.fullscreen)})}e.registerButtons(),setTimeout(function(){e.trigger("resize")},200)},o.prototype.getXAPIData=function(){var e=this.createXAPIEventTemplate("answered");return this.addQuestionToXAPI(e),this.addResponseToXAPI(e),{statement:e.data.statement}},o.prototype.addQuestionToXAPI=function(e){var t=e.getVerifiedStatementValue(["object","definition"]);y.extend(t,this.getXAPIDefinition())},o.prototype.getXAPIDefinition=function(){var e={};e.description={"en-US":y(""+this.options.question.settings.questionTitle+"
").text()},e.type="http://adlnet.gov/expapi/activities/cmi.interaction",e.interactionType="matching",e.source=[];for(var t=0;t"+i+" ").text()}})}}e.correctResponsesPattern=[""],e.target=[];var o=!0;for(t=0;t
"+this.options.question.task.dropZones[t].label+" ").text()}}),this.options.question.task.dropZones[t].correctElements)for(var r=0;r
'),void 0!==this.options.question.settings.background&&this.$container.css("backgroundImage",'url("'+H5P.getPath(this.options.question.settings.background.path,this.id)+'")');var t=this.options.question.task;for(e=0;e ').appendTo(this.$container).data("id",n)},o.prototype.resize=function(e){var t=this;if(void 0!==this.$container&&this.$container.is(":visible")){t.dropZones.forEach(function(e){e.updateBackgroundOpacity()});var n=e&&e.data&&e.data.decreaseSize;n||(this.$container.css("height","99999px"),t.$container.parents(".h5p-standalone.h5p-dragquestion").css("width",""));var i=this.options.question.settings.size,o=i.width/i.height,r=this.$container.parent(),a=r.width()-parseFloat(r.css("margin-left"))-parseFloat(r.css("margin-right")),s=t.$container.parents(".h5p-standalone.h5p-dragquestion.h5p-semi-fullscreen");if(s.length){s.css("width",""),n||(t.$container.css("width","10px"),s.css("width",""),setTimeout(function(){t.trigger("resize",{decreaseSize:!0})},200));var l=y(window.frameElement);if(l){a=l.parent().width(),s.css("width",a+"px")}}var u=a/o;a<=0&&(a=i.width,u=i.height),this.$container.css({width:a+"px",height:u+"px",fontSize:a/i.width*16+"px"})}},o.prototype.disableDraggables=function(){this.draggables.forEach(function(e){e.disable()})},o.prototype.enableDraggables=function(){this.draggables.forEach(function(e){e.enable()})},o.prototype.showAllSolutions=function(e){this.points=0,this.rawPoints=0,this.blankIsCorrect&&(this.points=1,this.rawPoints=1);var t;!e&&this.options.behaviour.showScorePoints&&!this.options.behaviour.singlePoint&&this.options.behaviour.applyPenalties&&(t=new H5P.Question.ScorePoints);for(var n=0;n',o="";n.showLabel&&(i=''+n.label+'
'+i,o=" h5p-has-label"),i=''+n.l10n.prefix.replace("{num}",n.id+1)+" "+i,n.$dropZone=s("
",{class:"h5p-dropzone"+o,tabindex:"-1",title:n.showLabel?s("
",{html:n.label}).text():null,role:"button","aria-disabled":!0,css:{left:n.x+"%",top:n.y+"%",width:n.width+"em",height:n.height+"em"},html:i}).appendTo(e).children(".h5p-inner").droppable({activeClass:"h5p-active",tolerance:"intersect",accept:function(e){var i=a.default.elementToDraggable(t,e);return!!i&&n.accepts(i.draggable,t)},drop:function(e,t){var i=s(this);a.default.setOpacity(i.removeClass("h5p-over"),"background",n.backgroundOpacity),t.draggable.data("addToZone",n.id),-1===n.getIndexOf(t.draggable)&&n.alignables.push(t.draggable),n.autoAlignable.enabled&&n.autoAlign()},over:function(){a.default.setOpacity(s(this).addClass("h5p-over"),"background",n.backgroundOpacity)},out:function(){a.default.setOpacity(s(this).removeClass("h5p-over"),"background",n.backgroundOpacity)}}).end().focus(function(){r instanceof H5P.jQuery&&r.attr("tabindex","0")}).blur(function(){r instanceof H5P.jQuery&&r.attr("tabindex","-1")});var r=H5P.JoubelUI.createTip(n.tip,{tipLabel:n.l10n.tipLabel,tabcontrol:!0});r instanceof H5P.jQuery&&s(" ",{class:"h5p-dq-tipwrap","aria-label":n.l10n.tipAvailable,append:r,appendTo:n.$dropZone}),t.forEach(function(e){var t=e.element.$;e.isInDropZone(n.id)&&-1===n.getIndexOf(t)&&n.alignables.push(t)}),n.autoAlignable.enabled&&n.autoAlign(),setTimeout(function(){n.updateBackgroundOpacity()},0)}},{key:"updateBackgroundOpacity",value:function(){a.default.setOpacity(this.$dropZone.children(".h5p-label"),"background",this.backgroundOpacity),a.default.setOpacity(this.$dropZone.children(".h5p-inner"),"background",this.backgroundOpacity)}},{key:"accepts",value:function(e,t){var n=this;if(!e.hasDropZone(n.id))return!1;if(n.single)for(var i=0;iu&&(u=a)},d=0;d=t.width)c();else{if(l.x=s.x,r.x=n.x+o.x,u&&(l.y-=u,r.y+=u/i.height*100,u=0),l.y<=0)return;c()}}},{key:"highlight",value:function(){this.$dropZone.attr("aria-disabled","false").children(".h5p-inner").addClass("h5p-active")}},{key:"dehighlight",value:function(){this.$dropZone.attr("aria-disabled","true").children(".h5p-inner").removeClass("h5p-active")}},{key:"reset",value:function(){this.alignables=[]}}]),e}();t.default=l},function(e,t,n){"use strict";function i(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function o(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function r(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(t,"__esModule",{value:!0});var a=function(){function e(e,t){for(var n=0;n ",{class:"h5p-draggable",tabindex:"-1",role:"button",css:{left:o.x+"%",top:o.y+"%",width:o.width+"em",height:o.height+"em"},appendTo:t,title:o.type.params.title}).on("click",function(){o.trigger("focus",this)}).on("touchstart",function(e){e.stopPropagation()}).draggable({revert:function(e){t.removeClass("h5p-dragging");var n=u(this);return n.data("uiDraggable").originalPosition={top:o.y+"%",left:o.x+"%"},o.updatePlacement(i),n[0].setAttribute("aria-grabbed","false"),o.trigger("dragend"),!e},start:function(){var e=u(this),n=o.mustCopyElement(i);n&&i.clone(),e.removeClass("h5p-wrong").detach().appendTo(t),t.addClass("h5p-dragging"),l.default.setElementOpacity(e,o.backgroundOpacity),this.setAttribute("aria-grabbed","true"),o.trigger("focus",this),o.trigger("dragstart",{element:this,effect:n?"copy":"move"})},stop:function(){var n=u(this);i.position=l.default.positionToPercentage(t,n),n.css(i.position);var r=n.data("addToZone");void 0!==r?(n.removeData("addToZone"),o.addToDropZone(e,i,r)):i.reset()}}).css("position",""),o.element=i,i.position&&(i.$.css(i.position),o.updatePlacement(i)),l.default.addHover(i.$,o.backgroundOpacity),H5P.newRunnable(o.type,n,i.$),u(''+o.l10n.prefix.replace("{num}",o.id+1)+" ").prependTo(i.$),u(' ').appendTo(i.$),setTimeout(function(){l.default.setElementOpacity(i.$,o.backgroundOpacity)},0),o.trigger("elementadd",i.$[0])}},{key:"setFeedback",value:function(e,t){this.elements.forEach(function(n){n.dropZone===t&&(void 0===n.$feedback&&(n.$feedback=u("",{class:"h5p-hidden-read",appendTo:n.$})),n.$feedback.html(e))})}},{key:"mustCopyElement",value:function(e){return this.multiple&&void 0===e.dropZone}},{key:"hasDropZone",value:function(e){for(var t=this,n=0;n'+this.l10n.suffix.replace("{num}",e.dropZone+1)+". ").appendTo(e.$)):(e.$.removeClass("h5p-dropped").removeClass("h5p-wrong").removeClass("h5p-correct").css({border:"",background:""}),l.default.setElementOpacity(e.$,this.backgroundOpacity))}},{key:"resetPosition",value:function(){var e=this;this.elements.forEach(function(t){if(t.$feedback&&(t.$feedback.remove(),delete t.$feedback),void 0!==t.dropZone){var n=t.$;n.animate({left:e.x+"%",top:e.y+"%"},function(){e.multiple&&(void 0!==n.dropZone&&e.trigger("leavingDropZone",n),n.remove(),e.elements.indexOf(t)>=0&&delete e.elements[e.elements.indexOf(t)],e.trigger("elementremove",n[0]))}),e.updatePlacement(t)}}),void 0!==e.element.dropZone&&(e.trigger("leavingDropZone",e.element),delete e.element.dropZone),e.updatePlacement(e.element)}},{key:"findElement",value:function(e){for(var t=this,n=0;n",{class:"h5p-hidden-read",html:this.l10n[t+"Answer"]+". "});n&&(i=i.add(n.getElement("correct"===t))),e.$suffix=e.$suffix.add(i),e.$.addClass("h5p-"+t).append(i),l.default.setElementOpacity(e.$,this.backgroundOpacity)}}]),t}(H5P.EventDispatcher);t.default=c}]);;
var H5P = H5P || {};
/**
* Constructor.
*
* @param {Object} params Options for this library.
* @param {Number} id Content identifier
* @returns {undefined}
*/
(function ($) {
H5P.Image = function (params, id, extras) {
H5P.EventDispatcher.call(this);
this.extras = extras;
if (params.file === undefined || !(params.file instanceof Object)) {
this.placeholder = true;
}
else {
this.source = H5P.getPath(params.file.path, id);
this.width = params.file.width;
this.height = params.file.height;
}
this.alt = (!params.decorative && params.alt !== undefined) ?
this.stripHTML(this.htmlDecode(params.alt)) :
'';
if (params.title !== undefined) {
this.title = this.stripHTML(this.htmlDecode(params.title));
}
};
H5P.Image.prototype = Object.create(H5P.EventDispatcher.prototype);
H5P.Image.prototype.constructor = H5P.Image;
/**
* Wipe out the content of the wrapper and put our HTML in it.
*
* @param {jQuery} $wrapper
* @returns {undefined}
*/
H5P.Image.prototype.attach = function ($wrapper) {
var self = this;
var source = this.source;
if (self.$img === undefined) {
if(self.placeholder) {
self.$img = $('', {
width: '100%',
height: '100%',
class: 'h5p-placeholder',
title: this.title === undefined ? '' : this.title,
on: {
load: function () {
self.trigger('loaded');
}
}
});
} else {
self.$img = $('
', {
width: '100%',
height: '100%',
src: source,
alt: this.alt,
title: this.title === undefined ? '' : this.title,
on: {
load: function () {
self.trigger('loaded');
}
}
});
}
}
$wrapper.addClass('h5p-image').html(self.$img);
};
/**
* Retrieve decoded HTML encoded string.
*
* @param {string} input HTML encoded string.
* @returns {string} Decoded string.
*/
H5P.Image.prototype.htmlDecode = function (input) {
const dparser = new DOMParser().parseFromString(input, 'text/html');
return dparser.documentElement.textContent;
};
/**
* Retrieve string without HTML tags.
*
* @param {string} input Input string.
* @returns {string} Output string.
*/
H5P.Image.prototype.stripHTML = function (html) {
const div = document.createElement('div');
div.innerHTML = html;
return div.textContent || div.innerText || '';
};
return H5P.Image;
}(H5P.jQuery));
;
H5P.TrueFalse = (function ($, Question) {
'use strict';
// Maximum score for True False
var MAX_SCORE = 1;
/**
* Enum containing the different states this content type can exist in
*
* @enum
*/
var State = Object.freeze({
ONGOING: 1,
FINISHED_WRONG: 2,
FINISHED_CORRECT: 3,
INTERNAL_SOLUTION: 4,
EXTERNAL_SOLUTION: 5
});
/**
* Button IDs
*/
var Button = Object.freeze({
CHECK: 'check-answer',
TRYAGAIN: 'try-again',
SHOW_SOLUTION: 'show-solution'
});
/**
* Initialize module.
*
* @class H5P.TrueFalse
* @extends H5P.Question
* @param {Object} options
* @param {number} id Content identification
* @param {Object} contentData Task specific content data
*/
function TrueFalse(options, id, contentData) {
var self = this;
// Inheritance
Question.call(self, 'true-false');
var params = $.extend(true, {
question: 'No question text provided',
correct: 'true',
l10n: {
trueText: 'True',
falseText: 'False',
score: 'You got @score of @total points',
checkAnswer: 'Check',
submitAnswer: 'Submit',
showSolutionButton: 'Show solution',
tryAgain: 'Retry',
wrongAnswerMessage: 'Wrong answer',
correctAnswerMessage: '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.',
},
behaviour: {
enableRetry: true,
enableSolutionsButton: true,
enableCheckButton: true,
confirmCheckDialog: false,
confirmRetryDialog: false,
autoCheck: false
}
}, options);
// Counter used to create unique id for this question
TrueFalse.counter = (TrueFalse.counter === undefined ? 0 : TrueFalse.counter + 1);
// A unique ID is needed for aria label
var domId = 'h5p-tfq' + H5P.TrueFalse.counter;
// saves the content id
this.contentId = id;
this.contentData = contentData;
// The radio group
var answerGroup = new H5P.TrueFalse.AnswerGroup(domId, params.correct, params.l10n);
if (contentData.previousState !== undefined && contentData.previousState.answer !== undefined) {
answerGroup.check(contentData.previousState.answer);
}
answerGroup.on('selected', function () {
self.triggerXAPI('interacted');
if (params.behaviour.autoCheck) {
checkAnswer();
triggerXAPIAnswered();
}
});
/**
* Create the answers
*
* @method createAnswers
* @private
* @return {H5P.jQuery}
*/
var createAnswers = function () {
return answerGroup.getDomElement();
};
/**
* Register buttons
*
* @method registerButtons
* @private
*/
var registerButtons = function () {
var $content = $('[data-content-id="' + self.contentId + '"].h5p-content');
var $containerParents = $content.parents('.h5p-container');
// select find container to attach dialogs to
var $container;
if($containerParents.length !== 0) {
// use parent highest up if any
$container = $containerParents.last();
}
else if($content.length !== 0){
$container = $content;
}
else {
$container = $(document.body);
}
// Show solution button
if (params.behaviour.enableSolutionsButton === true) {
self.addButton(Button.SHOW_SOLUTION, params.l10n.showSolutionButton, function () {
self.showSolutions(true);
}, false, {
'aria-label': params.l10n.a11yShowSolution,
});
}
// Check button
if (!params.behaviour.autoCheck && params.behaviour.enableCheckButton) {
self.addButton(Button.CHECK, params.l10n.checkAnswer, function () {
checkAnswer();
triggerXAPIAnswered();
}, true, {
'aria-label': params.l10n.a11yCheck
}, {
confirmationDialog: {
enable: params.behaviour.confirmCheckDialog,
l10n: params.confirmCheck,
instance: self,
$parentElement: $container
},
contentData: self.contentData,
textIfSubmitting: params.l10n.submitAnswer,
});
}
// Try again button
if (params.behaviour.enableRetry === true) {
self.addButton(Button.TRYAGAIN, params.l10n.tryAgain, function () {
self.resetTask();
}, true, {
'aria-label': params.l10n.a11yRetry,
}, {
confirmationDialog: {
enable: params.behaviour.confirmRetryDialog,
l10n: params.confirmRetry,
instance: self,
$parentElement: $container
}
});
}
toggleButtonState(State.ONGOING);
};
/**
* Creates and triggers the xAPI answered event
*
* @method triggerXAPIAnswered
* @private
* @fires xAPIEvent
*/
var triggerXAPIAnswered = function () {
var xAPIEvent = self.createXAPIEventTemplate('answered');
addQuestionToXAPI(xAPIEvent);
addResponseToXAPI(xAPIEvent);
self.trigger(xAPIEvent);
};
/**
* Add the question itself to the definition part of an xAPIEvent
*
* @method addQuestionToXAPI
* @param {XAPIEvent} xAPIEvent
* @private
*/
var addQuestionToXAPI = function(xAPIEvent) {
var definition = xAPIEvent.getVerifiedStatementValue(['object', 'definition']);
definition.description = {
// Remove tags, must wrap in div tag because jQuery 1.9 will crash if the string isn't wrapped in a tag.
'en-US': $('
' + params.question + '
').text()
};
definition.type = 'http://adlnet.gov/expapi/activities/cmi.interaction';
definition.interactionType = 'true-false';
definition.correctResponsesPattern = [getCorrectAnswer()];
};
/**
* Returns the correct answer
*
* @method getCorrectAnswer
* @private
* @return {String}
*/
var getCorrectAnswer = function () {
return (params.correct === 'true' ? 'true' : 'false');
};
/**
* Returns the wrong answer
*
* @method getWrongAnswer
* @private
* @return {String}
*/
var getWrongAnswer = function () {
return (params.correct === 'false' ? 'true' : 'false');
};
/**
* Add the response part to an xAPI event
*
* @method addResponseToXAPI
* @private
* @param {H5P.XAPIEvent} xAPIEvent
* The xAPI event we will add a response to
*/
var addResponseToXAPI = function(xAPIEvent) {
var isCorrect = answerGroup.isCorrect();
xAPIEvent.setScoredResult(isCorrect ? MAX_SCORE : 0, MAX_SCORE, self, true, isCorrect);
xAPIEvent.data.statement.result.response = (isCorrect ? getCorrectAnswer() : getWrongAnswer());
};
/**
* Toggles btton visibility dependent of current state
*
* @method toggleButtonVisibility
* @private
* @param {String} buttonId
* @param {Boolean} visible
*/
var toggleButtonVisibility = function (buttonId, visible) {
if (visible === true) {
self.showButton(buttonId);
}
else {
self.hideButton(buttonId);
}
};
/**
* Toggles buttons state
*
* @method toggleButtonState
* @private
* @param {String} state
*/
var toggleButtonState = function (state) {
toggleButtonVisibility(Button.SHOW_SOLUTION, state === State.FINISHED_WRONG);
toggleButtonVisibility(Button.CHECK, state === State.ONGOING);
toggleButtonVisibility(Button.TRYAGAIN, state === State.FINISHED_WRONG || state === State.INTERNAL_SOLUTION);
};
/**
* Check if answer is correct or wrong, and update visuals accordingly
*
* @method checkAnswer
* @private
*/
var checkAnswer = function () {
// Create feedback widget
var score = self.getScore();
var scoreText;
toggleButtonState(score === MAX_SCORE ? State.FINISHED_CORRECT : State.FINISHED_WRONG);
if (score === MAX_SCORE && params.behaviour.feedbackOnCorrect) {
scoreText = params.behaviour.feedbackOnCorrect;
}
else if (score === 0 && params.behaviour.feedbackOnWrong) {
scoreText = params.behaviour.feedbackOnWrong;
}
else {
scoreText = params.l10n.score;
}
// Replace relevant variables:
scoreText = scoreText.replace('@score', score).replace('@total', MAX_SCORE);
self.setFeedback(scoreText, score, MAX_SCORE, params.l10n.scoreBarLabel);
answerGroup.reveal();
};
/**
* Registers this question type's DOM elements before they are attached.
* Called from H5P.Question.
*
* @method registerDomElements
* @private
*/
self.registerDomElements = function () {
var self = this;
// Check for task media
var media = params.media;
if (media && media.type && media.type.library) {
media = media.type;
var type = media.library.split(' ')[0];
if (type === 'H5P.Image') {
if (media.params.file) {
// Register task image
self.setImage(media.params.file.path, {
disableImageZooming: params.media.disableImageZooming || false,
alt: media.params.alt
});
}
}
else if (type === 'H5P.Video') {
if (media.params.sources) {
// Register task video
self.setVideo(media);
}
}
}
// Add task question text
self.setIntroduction('
' + params.question + '
');
// Register task content area
self.$content = createAnswers();
self.setContent(self.$content);
// ... and buttons
registerButtons();
};
/**
* Implements resume (save content state)
*
* @method getCurrentState
* @public
* @returns {object} object containing answer
*/
self.getCurrentState = function () {
return {answer: answerGroup.getAnswer()};
};
/**
* Used for contracts.
* Checks if the parent program can proceed. Always true.
*
* @method getAnswerGiven
* @public
* @returns {Boolean} true
*/
self.getAnswerGiven = function () {
return answerGroup.hasAnswered();
};
/**
* Used for contracts.
* Checks the current score for this task.
*
* @method getScore
* @public
* @returns {Number} The current score.
*/
self.getScore = function () {
return answerGroup.isCorrect() ? MAX_SCORE : 0;
};
/**
* Used for contracts.
* Checks the maximum score for this task.
*
* @method getMaxScore
* @public
* @returns {Number} The maximum score.
*/
self.getMaxScore = function () {
return MAX_SCORE;
};
/**
* Get title of task
*
* @method getTitle
* @public
* @returns {string} title
*/
self.getTitle = function () {
return H5P.createTitle((self.contentData && self.contentData.metadata && self.contentData.metadata.title) ? self.contentData.metadata.title : 'True-False');
};
/**
* Used for contracts.
* Show the solution.
*
* @method showSolutions
* @public
*/
self.showSolutions = function (internal) {
checkAnswer();
answerGroup.showSolution();
toggleButtonState(internal ? State.INTERNAL_SOLUTION : State.EXTERNAL_SOLUTION);
};
/**
* Used for contracts.
* Resets the complete task back to its' initial state.
*
* @method resetTask
* @public
*/
self.resetTask = function () {
answerGroup.reset();
self.removeFeedback();
toggleButtonState(State.ONGOING);
};
/**
* Get xAPI data.
* Contract used by report rendering engine.
*
* @see contract at {@link https://h5p.org/documentation/developers/contracts#guides-header-6}
*/
self.getXAPIData = function(){
var xAPIEvent = this.createXAPIEventTemplate('answered');
this.addQuestionToXAPI(xAPIEvent);
this.addResponseToXAPI(xAPIEvent);
return {
statement: xAPIEvent.data.statement
};
};
/**
* Add the question itself to the definition part of an xAPIEvent
*/
self.addQuestionToXAPI = function(xAPIEvent) {
var definition = xAPIEvent.getVerifiedStatementValue(['object', 'definition']);
$.extend(definition, this.getxAPIDefinition());
};
/**
* Generate xAPI object definition used in xAPI statements.
* @return {Object}
*/
self.getxAPIDefinition = function () {
var definition = {};
definition.interactionType = 'true-false';
definition.type = 'http://adlnet.gov/expapi/activities/cmi.interaction';
definition.description = {
'en-US': $('
' + params.question + '
').text()
};
definition.correctResponsesPattern = [getCorrectAnswer()];
return definition;
};
/**
* Add the response part to an xAPI event
*
* @param {H5P.XAPIEvent} xAPIEvent
* The xAPI event we will add a response to
*/
self.addResponseToXAPI = function (xAPIEvent) {
var isCorrect = answerGroup.isCorrect();
var rawUserScore = isCorrect ? MAX_SCORE : 0;
var currentResponse = '';
xAPIEvent.setScoredResult(rawUserScore, MAX_SCORE, self, true, isCorrect);
if(self.getCurrentState().answer !== undefined) {
currentResponse += answerGroup.isCorrect() ? getCorrectAnswer() : getWrongAnswer();
}
xAPIEvent.data.statement.result.response = currentResponse;
};
}
// Inheritance
TrueFalse.prototype = Object.create(Question.prototype);
TrueFalse.prototype.constructor = TrueFalse;
return TrueFalse;
})(H5P.jQuery, H5P.Question);
;
H5P.TrueFalse.AnswerGroup = (function ($, EventDispatcher) {
'use strict';
/**
* Initialize module.
*
* @class H5P.TrueFalse.AnswerGroup
* @extends H5P.EventDispatcher
* @param {String} domId Id for label
* @param {String} correctOption Correct option ('true' or 'false')
* @param {Object} l10n Object containing all interface translations
*/
function AnswerGroup(domId, correctOption, l10n) {
var self = this;
EventDispatcher.call(self);
var $answers = $('
', {
'class': 'h5p-true-false-answers',
role: 'radiogroup',
'aria-labelledby': domId
});
var answer;
var trueAnswer = new H5P.TrueFalse.Answer(l10n.trueText, l10n.correctAnswerMessage, l10n.wrongAnswerMessage);
var falseAnswer = new H5P.TrueFalse.Answer(l10n.falseText, l10n.correctAnswerMessage, l10n.wrongAnswerMessage);
var correctAnswer = (correctOption === 'true' ? trueAnswer : falseAnswer);
var wrongAnswer = (correctOption === 'false' ? trueAnswer : falseAnswer);
// Handle checked
var handleChecked = function (newAnswer, other) {
return function () {
answer = newAnswer;
other.uncheck();
self.trigger('selected');
};
};
trueAnswer.on('checked', handleChecked(true, falseAnswer));
falseAnswer.on('checked', handleChecked(false, trueAnswer));
// Handle switches (using arrow keys)
var handleInvert = function (newAnswer, other) {
return function () {
answer = newAnswer;
other.check();
self.trigger('selected');
};
};
trueAnswer.on('invert', handleInvert(false, falseAnswer));
falseAnswer.on('invert', handleInvert(true, trueAnswer));
// Handle tabbing
var handleTabable = function(other, tabable) {
return function () {
// If one of them are checked, that one should get tabfocus
if (!tabable || !self.hasAnswered() || other.isChecked()) {
other.tabable(tabable);
}
};
};
// Need to remove tabIndex on the other alternative on focus
trueAnswer.on('focus', handleTabable(falseAnswer, false));
falseAnswer.on('focus', handleTabable(trueAnswer, false));
// Need to make both alternatives tabable on blur:
trueAnswer.on('blur', handleTabable(falseAnswer, true));
falseAnswer.on('blur', handleTabable(trueAnswer, true));
$answers.append(trueAnswer.getDomElement());
$answers.append(falseAnswer.getDomElement());
/**
* Get hold of the DOM element representing this thingy
* @method getDomElement
* @return {jQuery}
*/
self.getDomElement = function () {
return $answers;
};
/**
* Programatic check
* @method check
* @param {[type]} answer [description]
*/
self.check = function (answer) {
if (answer) {
trueAnswer.check();
}
else {
falseAnswer.check();
}
};
/**
* Return current answer
* @method getAnswer
* @return {Boolean} undefined if no answer if given
*/
self.getAnswer = function () {
return answer;
};
/**
* Check if user has answered question yet
* @method hasAnswered
* @return {Boolean}
*/
self.hasAnswered = function () {
return answer !== undefined;
};
/**
* Is answer correct?
* @method isCorrect
* @return {Boolean}
*/
self.isCorrect = function () {
return correctAnswer.isChecked();
};
/**
* Enable user input
*
* @method enable
*/
self.enable = function () {
trueAnswer.enable().tabable(true);
falseAnswer.enable();
};
/**
* Disable user input
*
* @method disable
*/
self.disable = function () {
trueAnswer.disable();
falseAnswer.disable();
};
/**
* Reveal correct/wrong answer
*
* @method reveal
*/
self.reveal = function () {
if (self.hasAnswered()) {
if (self.isCorrect()) {
correctAnswer.markCorrect();
}
else {
wrongAnswer.markWrong();
}
}
self.disable();
};
/**
* Reset task
* @method reset
*/
self.reset = function () {
trueAnswer.reset();
falseAnswer.reset();
self.enable();
answer = undefined;
};
/**
* Show the solution
* @method showSolution
* @return {[type]}
*/
self.showSolution = function () {
correctAnswer.markCorrect();
wrongAnswer.unmark();
};
}
// Inheritance
AnswerGroup.prototype = Object.create(EventDispatcher.prototype);
AnswerGroup.prototype.constructor = AnswerGroup;
return AnswerGroup;
})(H5P.jQuery, H5P.EventDispatcher);
;
H5P.TrueFalse.Answer = (function ($, EventDispatcher) {
'use strict';
var Keys = {
ENTER: 13,
SPACE: 32,
LEFT_ARROW: 37,
UP_ARROW: 38,
RIGHT_ARROW: 39,
DOWN_ARROW: 40
};
/**
* Initialize module.
*
* @class H5P.TrueFalse.Answer
* @extends H5P.EventDispatcher
* @param {String} text Label
* @param {String} correctMessage Message read by readspeaker when correct alternative is chosen
* @param {String} wrongMessage Message read by readspeaker when wrong alternative is chosen
*/
function Answer (text, correctMessage, wrongMessage) {
var self = this;
EventDispatcher.call(self);
var checked = false;
var enabled = true;
var $answer = $('
', {
'class': 'h5p-true-false-answer',
role: 'radio',
'aria-checked': false,
html: text + '
',
tabindex: 0, // Tabable by default
click: function (event) {
// Handle left mouse (or tap on touch devices)
if (event.which === 1) {
self.check();
}
},
keydown: function (event) {
if (!enabled) {
return;
}
if ([Keys.SPACE, Keys.ENTER].indexOf(event.keyCode) !== -1) {
self.check();
}
else if ([Keys.LEFT_ARROW, Keys.UP_ARROW, Keys.RIGHT_ARROW, Keys.DOWN_ARROW].indexOf(event.keyCode) !== -1) {
self.uncheck();
self.trigger('invert');
}
},
focus: function () {
self.trigger('focus');
},
blur: function () {
self.trigger('blur');
}
});
var $ariaLabel = $answer.find('.aria-label');
// A bug in Chrome 54 makes the :after icons (V and X) not beeing rendered.
// Doing this in a timeout solves this
// Might be removed when Chrome 56 is out
var chromeBugFixer = function (callback) {
setTimeout(function () {
callback();
}, 0);
};
/**
* Return the dom element representing the alternative
*
* @public
* @method getDomElement
* @return {H5P.jQuery}
*/
self.getDomElement = function () {
return $answer;
};
/**
* Unchecks the alternative
*
* @public
* @method uncheck
* @return {H5P.TrueFalse.Answer}
*/
self.uncheck = function () {
if (enabled) {
$answer.blur();
checked = false;
chromeBugFixer(function () {
$answer.attr('aria-checked', checked);
});
}
return self;
};
/**
* Set tabable or not
* @method tabable
* @param {Boolean} enabled
* @return {H5P.TrueFalse.Answer}
*/
self.tabable = function (enabled) {
$answer.attr('tabIndex', enabled ? 0 : null);
return self;
};
/**
* Checks the alternative
*
* @method check
* @return {H5P.TrueFalse.Answer}
*/
self.check = function () {
if (enabled) {
checked = true;
chromeBugFixer(function () {
$answer.attr('aria-checked', checked);
});
self.trigger('checked');
$answer.focus();
}
return self;
};
/**
* Is this alternative checked?
*
* @method isChecked
* @return {boolean}
*/
self.isChecked = function () {
return checked;
};
/**
* Enable alternative
*
* @method enable
* @return {H5P.TrueFalse.Answer}
*/
self.enable = function () {
$answer.attr({
'aria-disabled': '',
tabIndex: 0
});
enabled = true;
return self;
};
/**
* Disables alternative
*
* @method disable
* @return {H5P.TrueFalse.Answer}
*/
self.disable = function () {
$answer.attr({
'aria-disabled': true,
tabIndex: null
});
enabled = false;
return self;
};
/**
* Reset alternative
*
* @method reset
* @return {H5P.TrueFalse.Answer}
*/
self.reset = function () {
self.enable();
self.uncheck();
self.unmark();
$ariaLabel.html('');
return self;
};
/**
* Marks this alternative as the wrong one
*
* @method markWrong
* @return {H5P.TrueFalse.Answer}
*/
self.markWrong = function () {
chromeBugFixer(function () {
$answer.addClass('wrong');
});
$ariaLabel.html('.' + wrongMessage);
return self;
};
/**
* Marks this alternative as the wrong one
*
* @method markCorrect
* @return {H5P.TrueFalse.Answer}
*/
self.markCorrect = function () {
chromeBugFixer(function () {
$answer.addClass('correct');
});
$ariaLabel.html('.' + correctMessage);
return self;
};
self.unmark = function () {
chromeBugFixer(function () {
$answer.removeClass('wrong correct');
});
return self;
};
}
// Inheritance
Answer.prototype = Object.create(EventDispatcher.prototype);
Answer.prototype.constructor = Answer;
return Answer;
})(H5P.jQuery, H5P.EventDispatcher);
;
/**
* @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': replaceLineBreaks(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;
}
/**
* Replace line breaks
*
* @param {string} description
* @return {string}
*/
function replaceLineBreaks(description) {
var sanitized = $('
' + description + '
').text();
return sanitized.replace(/(\n)+/g, '
');
}
/**
* 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) {
var self = this;
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, {
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.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 () {
// 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)
};
};;
(function(){var rsplit=function(string,regex){var result=regex.exec(string),retArr=new Array(),first_idx,last_idx,first_bit;while(result!=null){first_idx=result.index;last_idx=regex.lastIndex;if((first_idx)!=0){first_bit=string.substring(0,first_idx);retArr.push(string.substring(0,first_idx));string=string.slice(first_idx)}retArr.push(result[0]);string=string.slice(result[0].length);result=regex.exec(string)}if(!string==""){retArr.push(string)}return retArr},chop=function(string){return string.substr(0,string.length-1)},extend=function(d,s){for(var n in s){if(s.hasOwnProperty(n)){d[n]=s[n]}}};EJS=function(options){options=typeof options=="string"?{view:options}:options;this.set_options(options);if(options.precompiled){this.template={};this.template.process=options.precompiled;EJS.update(this.name,this);return }if(options.element){if(typeof options.element=="string"){var name=options.element;options.element=document.getElementById(options.element);if(options.element==null){throw name+"does not exist!"}}if(options.element.value){this.text=options.element.value}else{this.text=options.element.innerHTML}this.name=options.element.id;this.type="["}else{if(options.url){options.url=EJS.endExt(options.url,this.extMatch);this.name=this.name?this.name:options.url;var url=options.url;var template=EJS.get(this.name,this.cache);if(template){return template}if(template==EJS.INVALID_PATH){return null}try{this.text=EJS.request(url+(this.cache?"":"?"+Math.random()))}catch(e){}if(this.text==null){throw ({type:"EJS",message:"There is no template at "+url})}}}var template=new EJS.Compiler(this.text,this.type);template.compile(options,this.name);EJS.update(this.name,this);this.template=template};EJS.prototype={render:function(object,extra_helpers){object=object||{};this._extra_helpers=extra_helpers;var v=new EJS.Helpers(object,extra_helpers||{});return this.template.process.call(object,object,v)},update:function(element,options){if(typeof element=="string"){element=document.getElementById(element)}if(options==null){_template=this;return function(object){EJS.prototype.update.call(_template,element,object)}}if(typeof options=="string"){params={};params.url=options;_template=this;params.onComplete=function(request){var object=eval(request.responseText);EJS.prototype.update.call(_template,element,object)};EJS.ajax_request(params)}else{element.innerHTML=this.render(options)}},out:function(){return this.template.out},set_options:function(options){this.type=options.type||EJS.type;this.cache=options.cache!=null?options.cache:EJS.cache;this.text=options.text||null;this.name=options.name||null;this.ext=options.ext||EJS.ext;this.extMatch=new RegExp(this.ext.replace(/\./,"."))}};EJS.endExt=function(path,match){if(!path){return null}match.lastIndex=0;return path+(match.test(path)?"":this.ext)};EJS.Scanner=function(source,left,right){extend(this,{left_delimiter:left+"%",right_delimiter:"%"+right,double_left:left+"%%",double_right:"%%"+right,left_equal:left+"%=",left_comment:left+"%#"});this.SplitRegexp=left=="["?/(\[%%)|(%%\])|(\[%=)|(\[%#)|(\[%)|(%\]\n)|(%\])|(\n)/:new RegExp("("+this.double_left+")|(%%"+this.double_right+")|("+this.left_equal+")|("+this.left_comment+")|("+this.left_delimiter+")|("+this.right_delimiter+"\n)|("+this.right_delimiter+")|(\n)");this.source=source;this.stag=null;this.lines=0};EJS.Scanner.to_text=function(input){if(input==null||input===undefined){return""}if(input instanceof Date){return input.toDateString()}if(input.toString){return input.toString()}return""};EJS.Scanner.prototype={scan:function(block){scanline=this.scanline;regex=this.SplitRegexp;if(!this.source==""){var source_split=rsplit(this.source,/\n/);for(var i=0;i
0){for(var i=0;i0){buff.push(put_cmd+'"'+clean(content)+'")')}content="";break;case scanner.double_left:content=content+scanner.left_delimiter;break;default:content=content+token;break}}else{switch(token){case scanner.right_delimiter:switch(scanner.stag){case scanner.left_delimiter:if(content[content.length-1]=="\n"){content=chop(content);buff.push(content);buff.cr()}else{buff.push(content)}break;case scanner.left_equal:buff.push(insert_cmd+"(EJS.Scanner.to_text("+content+")))");break}scanner.stag=null;content="";break;case scanner.double_right:content=content+scanner.right_delimiter;break;default:content=content+token;break}}});if(content.length>0){buff.push(put_cmd+'"'+clean(content)+'")')}buff.close();this.out=buff.script+";";var to_be_evaled="/*"+name+"*/this.process = function(_CONTEXT,_VIEW) { try { with(_VIEW) { with (_CONTEXT) {"+this.out+" return ___ViewO.join('');}}}catch(e){e.lineNumber=null;throw e;}};";try{eval(to_be_evaled)}catch(e){if(typeof JSLINT!="undefined"){JSLINT(this.out);for(var i=0;i ").replace(/''/g,"'")}return""}};EJS.newRequest=function(){var factories=[function(){return new ActiveXObject("Msxml2.XMLHTTP")},function(){return new XMLHttpRequest()},function(){return new ActiveXObject("Microsoft.XMLHTTP")}];for(var i=0;i ")};EJS.Helpers.prototype.start_tag_for=function(A,B){return this.tag(A,B)};EJS.Helpers.prototype.submit_tag=function(A,B){B=B||{};B.type=B.type||"submit";B.value=A||"Submit";return this.single_tag_for("input",B)};EJS.Helpers.prototype.tag=function(C,E,D){if(!D){var D=">"}var B=" ";for(var A in E){if(E[A]!=null){var F=E[A].toString()}else{var F=""}if(A=="Class"){A="class"}if(F.indexOf("'")!=-1){B+=A+'="'+F+'" '}else{B+=A+"='"+F+"' "}}return"<"+C+B+D};EJS.Helpers.prototype.tag_end=function(A){return""+A+">"};EJS.Helpers.prototype.text_area_tag=function(A,C,B){B=B||{};B.id=B.id||A;B.name=B.name||A;C=C||"";if(B.size){B.cols=B.size.split("x")[0];B.rows=B.size.split("x")[1];delete B.size}B.cols=B.cols||50;B.rows=B.rows||4;return this.start_tag_for("textarea",B)+C+this.tag_end("textarea")};EJS.Helpers.prototype.text_tag=EJS.Helpers.prototype.text_area_tag;EJS.Helpers.prototype.text_field_tag=function(A,C,B){return this.input_field_tag(A,C,"text",B)};EJS.Helpers.prototype.url_for=function(A){return'window.location="'+A+'";'};EJS.Helpers.prototype.img_tag=function(B,C,A){A=A||{};A.src=B;A.alt=C;return this.single_tag_for("img",A)};
EJS.Helpers.prototype.date_tag = function(name, value , html_options) {
if(! (value instanceof Date))
value = new Date()
var month_names = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
var years = [], months = [], days =[];
var year = value.getFullYear();
var month = value.getMonth();
var day = value.getDate();
for(var y = year - 15; y < year+15 ; y++)
{
years.push({value: y, text: y})
}
for(var m = 0; m < 12; m++)
{
months.push({value: (m), text: month_names[m]})
}
for(var d = 0; d < 31; d++)
{
days.push({value: (d+1), text: (d+1)})
}
var year_select = this.select_tag(name+'[year]', year, years, {id: name+'[year]'} )
var month_select = this.select_tag(name+'[month]', month, months, {id: name+'[month]'})
var day_select = this.select_tag(name+'[day]', day, days, {id: name+'[day]'})
return year_select+month_select+day_select;
}
EJS.Helpers.prototype.form_tag = function(action, html_options) {
html_options = html_options || {};
html_options.action = action
if(html_options.multipart == true) {
html_options.method = 'post';
html_options.enctype = 'multipart/form-data';
}
return this.start_tag_for('form', html_options)
}
EJS.Helpers.prototype.form_tag_end = function() { return this.tag_end('form'); }
EJS.Helpers.prototype.hidden_field_tag = function(name, value, html_options) {
return this.input_field_tag(name, value, 'hidden', html_options);
}
EJS.Helpers.prototype.input_field_tag = function(name, value , inputType, html_options) {
html_options = html_options || {};
html_options.id = html_options.id || name;
html_options.value = value || '';
html_options.type = inputType || 'text';
html_options.name = name;
return this.single_tag_for('input', html_options)
}
EJS.Helpers.prototype.is_current_page = function(url) {
return (window.location.href == url || window.location.pathname == url ? true : false);
}
EJS.Helpers.prototype.link_to = function(name, url, html_options) {
if(!name) var name = 'null';
if(!html_options) var html_options = {}
if(html_options.confirm){
html_options.onclick =
" var ret_confirm = confirm(\""+html_options.confirm+"\"); if(!ret_confirm){ return false;} "
html_options.confirm = null;
}
html_options.href=url
return this.start_tag_for('a', html_options)+name+ this.tag_end('a');
}
EJS.Helpers.prototype.submit_link_to = function(name, url, html_options){
if(!name) var name = 'null';
if(!html_options) var html_options = {}
html_options.onclick = html_options.onclick || '' ;
if(html_options.confirm){
html_options.onclick =
" var ret_confirm = confirm(\""+html_options.confirm+"\"); if(!ret_confirm){ return false;} "
html_options.confirm = null;
}
html_options.value = name;
html_options.type = 'submit'
html_options.onclick=html_options.onclick+
(url ? this.url_for(url) : '')+'return false;';
//html_options.href='#'+(options ? Routes.url_for(options) : '')
return this.start_tag_for('input', html_options)
}
EJS.Helpers.prototype.link_to_if = function(condition, name, url, html_options, post, block) {
return this.link_to_unless((condition == false), name, url, html_options, post, block);
}
EJS.Helpers.prototype.link_to_unless = function(condition, name, url, html_options, block) {
html_options = html_options || {};
if(condition) {
if(block && typeof block == 'function') {
return block(name, url, html_options, block);
} else {
return name;
}
} else
return this.link_to(name, url, html_options);
}
EJS.Helpers.prototype.link_to_unless_current = function(name, url, html_options, block) {
html_options = html_options || {};
return this.link_to_unless(this.is_current_page(url), name, url, html_options, block)
}
EJS.Helpers.prototype.password_field_tag = function(name, value, html_options) { return this.input_field_tag(name, value, 'password', html_options); }
EJS.Helpers.prototype.select_tag = function(name, value, choices, html_options) {
html_options = html_options || {};
html_options.id = html_options.id || name;
html_options.value = value;
html_options.name = name;
var txt = ''
txt += this.start_tag_for('select', html_options)
for(var i = 0; i < choices.length; i++)
{
var choice = choices[i];
var optionOptions = {value: choice.value}
if(choice.value == value)
optionOptions.selected ='selected'
txt += this.start_tag_for('option', optionOptions )+choice.text+this.tag_end('option')
}
txt += this.tag_end('select');
return txt;
}
EJS.Helpers.prototype.single_tag_for = function(tag, html_options) { return this.tag(tag, html_options, '/>');}
EJS.Helpers.prototype.start_tag_for = function(tag, html_options) { return this.tag(tag, html_options); }
EJS.Helpers.prototype.submit_tag = function(name, html_options) {
html_options = html_options || {};
//html_options.name = html_options.id || 'commit';
html_options.type = html_options.type || 'submit';
html_options.value = name || 'Submit';
return this.single_tag_for('input', html_options);
}
EJS.Helpers.prototype.tag = function(tag, html_options, end) {
if(!end) var end = '>'
var txt = ' '
for(var attr in html_options) {
if(html_options[attr] != null)
var value = html_options[attr].toString();
else
var value=''
if(attr == "Class") // special case because "class" is a reserved word in IE
attr = "class";
if( value.indexOf("'") != -1 )
txt += attr+'=\"'+value+'\" '
else
txt += attr+"='"+value+"' "
}
return '<'+tag+txt+end;
}
EJS.Helpers.prototype.tag_end = function(tag) { return ''+tag+'>'; }
EJS.Helpers.prototype.text_area_tag = function(name, value, html_options) {
html_options = html_options || {};
html_options.id = html_options.id || name;
html_options.name = html_options.name || name;
value = value || ''
if(html_options.size) {
html_options.cols = html_options.size.split('x')[0]
html_options.rows = html_options.size.split('x')[1];
delete html_options.size
}
html_options.cols = html_options.cols || 50;
html_options.rows = html_options.rows || 4;
return this.start_tag_for('textarea', html_options)+value+this.tag_end('textarea')
}
EJS.Helpers.prototype.text_tag = EJS.Helpers.prototype.text_area_tag
EJS.Helpers.prototype.text_field_tag = function(name, value, html_options) { return this.input_field_tag(name, value, 'text', html_options); }
EJS.Helpers.prototype.url_for = function(url) {
return 'window.location="'+url+'";'
}
EJS.Helpers.prototype.img_tag = function(image_location, alt, options){
options = options || {};
options.src = image_location
options.alt = alt
return this.single_tag_for('img', options)
}
;
/*global EJS*/
// Will render a Question with multiple choices for answers.
// Options format:
// {
// title: "Optional title for question box",
// question: "Question text",
// answers: [{text: "Answer text", correct: false}, ...],
// singleAnswer: true, // or false, will change rendered output slightly.
// singlePoint: true, // True if question give a single point score only
// // if all are correct, false to give 1 point per
// // correct answer. (Only for singleAnswer=false)
// randomAnswers: false // Whether to randomize the order of answers.
// }
//
// Events provided:
// - h5pQuestionAnswered: Triggered when a question has been answered.
var H5P = H5P || {};
/**
* @typedef {Object} Options
* Options for multiple choice
*
* @property {Object} behaviour
* @property {boolean} behaviour.confirmCheckDialog
* @property {boolean} behaviour.confirmRetryDialog
*
* @property {Object} UI
* @property {string} UI.tipsLabel
*
* @property {Object} [confirmRetry]
* @property {string} [confirmRetry.header]
* @property {string} [confirmRetry.body]
* @property {string} [confirmRetry.cancelLabel]
* @property {string} [confirmRetry.confirmLabel]
*
* @property {Object} [confirmCheck]
* @property {string} [confirmCheck.header]
* @property {string} [confirmCheck.body]
* @property {string} [confirmCheck.cancelLabel]
* @property {string} [confirmCheck.confirmLabel]
*/
/**
* Module for creating a multiple choice question
*
* @param {Options} options
* @param {number} contentId
* @param {Object} contentData
* @returns {H5P.MultiChoice}
* @constructor
*/
H5P.MultiChoice = function (options, contentId, contentData) {
if (!(this instanceof H5P.MultiChoice))
return new H5P.MultiChoice(options, contentId, contentData);
var self = this;
this.contentId = contentId;
this.contentData = contentData;
H5P.Question.call(self, 'multichoice');
var $ = H5P.jQuery;
// checkbox or radiobutton
var texttemplate =
'';
var defaults = {
image: null,
question: "No question text provided",
answers: [
{
tipsAndFeedback: {
tip: '',
chosenFeedback: '',
notChosenFeedback: ''
},
text: "Answer 1",
correct: true
}
],
overallFeedback: [],
weight: 1,
userAnswers: [],
UI: {
checkAnswerButton: 'Check',
submitAnswerButton: 'Submit',
showSolutionButton: 'Show solution',
tryAgainButton: 'Try again',
scoreBarLabel: 'You got :num out of :total points',
tipAvailable: "Tip available",
feedbackAvailable: "Feedback available",
readFeedback: 'Read feedback',
shouldCheck: "Should have been checked",
shouldNotCheck: "Should not have been checked",
noInput: 'Input is required before viewing the solution',
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.',
},
behaviour: {
enableRetry: true,
enableSolutionsButton: true,
enableCheckButton: true,
type: 'auto',
singlePoint: true,
randomAnswers: false,
showSolutionsRequiresInput: true,
autoCheck: false,
passPercentage: 100,
showScorePoints: true
}
};
var template = new EJS({text: texttemplate});
var params = $.extend(true, defaults, options);
// Keep track of number of correct choices
var numCorrect = 0;
// Loop through choices
for (var i = 0; i < params.answers.length; i++) {
var answer = params.answers[i];
// Make sure tips and feedback exists
answer.tipsAndFeedback = answer.tipsAndFeedback || {};
if (params.answers[i].correct) {
// Update number of correct choices
numCorrect++;
}
}
// Determine if no choices is the correct
var blankIsCorrect = (numCorrect === 0);
// Determine task type
if (params.behaviour.type === 'auto') {
// Use single choice if only one choice is correct
params.behaviour.singleAnswer = (numCorrect === 1);
}
else {
params.behaviour.singleAnswer = (params.behaviour.type === 'single');
}
var getCheckboxOrRadioIcon = function (radio, selected) {
var icon;
if (radio) {
icon = selected ? '' : '';
}
else {
icon = selected ? '' : '';
}
return icon;
};
// Initialize buttons and elements.
var $myDom;
var $feedbackDialog;
/**
* Remove all feedback dialogs
*/
var removeFeedbackDialog = function () {
// Remove the open feedback dialogs.
$myDom.unbind('click', removeFeedbackDialog);
$myDom.find('.h5p-feedback-button, .h5p-feedback-dialog').remove();
$myDom.find('.h5p-has-feedback').removeClass('h5p-has-feedback');
if ($feedbackDialog) {
$feedbackDialog.remove();
}
};
var score = 0;
var solutionsVisible = false;
/**
* Add feedback to element
* @param {jQuery} $element Element that feedback will be added to
* @param {string} feedback Feedback string
*/
var addFeedback = function ($element, feedback) {
$feedbackDialog = $('' +
'' +
'
' +
'
' + feedback + '
' +
'
' +
'
');
//make sure feedback is only added once
if (!$element.find($('.h5p-feedback-dialog')).length) {
$feedbackDialog.appendTo($element.addClass('h5p-has-feedback'));
}
};
/**
* Register the different parts of the task with the H5P.Question structure.
*/
self.registerDomElements = function () {
var media = params.media;
if (media && media.type && media.type.library) {
media = media.type;
var type = media.library.split(' ')[0];
if (type === 'H5P.Image') {
if (media.params.file) {
// Register task image
self.setImage(media.params.file.path, {
disableImageZooming: params.media.disableImageZooming || false,
alt: media.params.alt,
title: media.params.title
});
}
}
else if (type === 'H5P.Video') {
if (media.params.sources) {
// Register task video
self.setVideo(media);
}
}
}
// Determine if we're using checkboxes or radio buttons
for (var i = 0; i < params.answers.length; i++) {
params.answers[i].checkboxOrRadioIcon = getCheckboxOrRadioIcon(params.behaviour.singleAnswer, params.userAnswers.indexOf(i) > -1);
}
// Register Introduction
self.setIntroduction('' + params.question + '
');
// Register task content area
$myDom = $(template.render(params));
self.setContent($myDom, {
'class': params.behaviour.singleAnswer ? 'h5p-radio' : 'h5p-check'
});
// Create tips:
var $answers = $('.h5p-answer', $myDom).each(function (i) {
var tip = params.answers[i].tipsAndFeedback.tip;
if (tip === undefined) {
return; // No tip
}
tip = tip.trim();
var tipContent = tip
.replace(/ /g, '')
.replace(//g, '')
.replace(/<\/p>/g, '')
.trim();
if (!tipContent.length) {
return; // Empty tip
}
else {
$(this).addClass('h5p-has-tip');
}
// Add tip
var $wrap = $('
', {
'class': 'h5p-multichoice-tipwrap',
'aria-label': params.UI.tipAvailable + '.'
});
var $multichoiceTip = $('', {
'role': 'button',
'tabindex': 0,
'title': params.UI.tipsLabel,
'aria-label': params.UI.tipsLabel,
'aria-expanded': false,
'class': 'multichoice-tip',
appendTo: $wrap
});
var tipIconHtml = '
' +
' ' +
' ' +
' ' +
' ';
$multichoiceTip.append(tipIconHtml);
$multichoiceTip.click(function () {
var $tipContainer = $multichoiceTip.parents('.h5p-answer');
var openFeedback = !$tipContainer.children('.h5p-feedback-dialog').is($feedbackDialog);
removeFeedbackDialog();
// Do not open feedback if it was open
if (openFeedback) {
$multichoiceTip.attr('aria-expanded', true);
// Add tip dialog
addFeedback($tipContainer, tip);
$feedbackDialog.addClass('h5p-has-tip');
// Tip for readspeaker
self.read(tip);
}
else {
$multichoiceTip.attr('aria-expanded', false);
}
self.trigger('resize');
// Remove tip dialog on dom click
setTimeout(function () {
$myDom.click(removeFeedbackDialog);
}, 100);
// Do not propagate
return false;
}).keydown(function (e) {
if (e.which === 32) {
$(this).click();
return false;
}
});
$('.h5p-alternative-container', this).append($wrap);
});
// Set event listeners.
var toggleCheck = function ($ans) {
if ($ans.attr('aria-disabled') === 'true') {
return;
}
self.answered = true;
var num = parseInt($ans.data('id'));
if (params.behaviour.singleAnswer) {
// Store answer
params.userAnswers = [num];
// Calculate score
score = (params.answers[num].correct ? 1 : 0);
// De-select previous answer
$answers.not($ans).removeClass('h5p-selected').attr('tabindex', '-1').attr('aria-checked', 'false');
// Select new answer
$ans.addClass('h5p-selected').attr('tabindex', '0').attr('aria-checked', 'true');
}
else {
if ($ans.attr('aria-checked') === 'true') {
const pos = params.userAnswers.indexOf(num);
if (pos !== -1) {
params.userAnswers.splice(pos, 1);
}
// Do not allow un-checking when retry disabled and auto check
if (params.behaviour.autoCheck && !params.behaviour.enableRetry) {
return;
}
// Remove check
$ans.removeClass('h5p-selected').attr('aria-checked', 'false');
}
else {
params.userAnswers.push(num);
$ans.addClass('h5p-selected').attr('aria-checked', 'true');
}
// Calculate score
calcScore();
}
self.triggerXAPI('interacted');
hideSolution($ans);
if (params.userAnswers.length) {
self.showButton('check-answer');
self.hideButton('try-again');
self.hideButton('show-solution');
if (params.behaviour.autoCheck) {
if (params.behaviour.singleAnswer) {
// Only a single answer allowed
checkAnswer();
}
else {
// Show feedback for selected alternatives
self.showCheckSolution(true);
// Always finish task if it was completed successfully
if (score === self.getMaxScore()) {
checkAnswer();
}
}
}
}
};
$answers.click(function () {
toggleCheck($(this));
}).keydown(function (e) {
if (e.keyCode === 32) { // Space bar
// Select current item
toggleCheck($(this));
return false;
}
if (params.behaviour.singleAnswer) {
switch (e.keyCode) {
case 38: // Up
case 37: { // Left
// Try to select previous item
var $prev = $(this).prev();
if ($prev.length) {
toggleCheck($prev.focus());
}
return false;
}
case 40: // Down
case 39: { // Right
// Try to select next item
var $next = $(this).next();
if ($next.length) {
toggleCheck($next.focus());
}
return false;
}
}
}
});
if (params.behaviour.singleAnswer) {
// Special focus handler for radio buttons
$answers.focus(function () {
if ($(this).attr('aria-disabled') !== 'true') {
$answers.not(this).attr('tabindex', '-1');
}
}).blur(function () {
if (!$answers.filter('.h5p-selected').length) {
$answers.first().add($answers.last()).attr('tabindex', '0');
}
});
}
// Adds check and retry button
addButtons();
if (!params.behaviour.singleAnswer) {
calcScore();
}
else {
if (params.userAnswers.length && params.answers[params.userAnswers[0]].correct) {
score = 1;
}
else {
score = 0;
}
}
// Has answered through auto-check in a previous session
if (hasCheckedAnswer && params.behaviour.autoCheck) {
// Check answers if answer has been given or max score reached
if (params.behaviour.singleAnswer || score === self.getMaxScore()) {
checkAnswer();
}
else {
// Show feedback for checked checkboxes
self.showCheckSolution(true);
}
}
};
this.showAllSolutions = function () {
if (solutionsVisible) {
return;
}
solutionsVisible = true;
$myDom.find('.h5p-answer').each(function (i, e) {
var $e = $(e);
var a = params.answers[i];
if (a.correct) {
$e.addClass('h5p-should').append($('
', {
'class': 'h5p-solution-icon',
html: params.UI.shouldCheck + '.'
}));
}
else {
$e.addClass('h5p-should-not').append($('
', {
'class': 'h5p-solution-icon',
html: params.UI.shouldNotCheck + '.'
}));
}
}).find('.h5p-question-plus-one, .h5p-question-minus-one').remove();
// Make sure input is disabled in solution mode
disableInput();
// Move focus back to the first correct alternative so that the user becomes
// aware that the solution is being shown.
$myDom.find('.h5p-answer.h5p-should').first().focus();
//Hide buttons and retry depending on settings.
self.hideButton('check-answer');
self.hideButton('show-solution');
if (params.behaviour.enableRetry) {
self.showButton('try-again');
}
self.trigger('resize');
};
/**
* Used in contracts.
* Shows the solution for the task and hides all buttons.
*/
this.showSolutions = function () {
removeFeedbackDialog();
self.showCheckSolution();
self.showAllSolutions();
disableInput();
self.hideButton('try-again');
};
/**
* Hide solution for the given answer(s)
*
* @private
* @param {H5P.jQuery} $answer
*/
var hideSolution = function ($answer) {
$answer
.removeClass('h5p-correct')
.removeClass('h5p-wrong')
.removeClass('h5p-should')
.removeClass('h5p-should-not')
.removeClass('h5p-has-feedback')
.find('.h5p-question-plus-one, .h5p-question-minus-one, .h5p-answer-icon, .h5p-solution-icon, .h5p-feedback-dialog').remove();
};
/**
*
*/
this.hideSolutions = function () {
solutionsVisible = false;
hideSolution($('.h5p-answer', $myDom));
this.removeFeedback(); // Reset feedback
self.trigger('resize');
};
/**
* Resets the whole task.
* Used in contracts with integrated content.
* @private
*/
this.resetTask = function () {
self.answered = false;
self.hideSolutions();
params.userAnswers = [];
removeSelections();
self.showButton('check-answer');
self.hideButton('try-again');
self.hideButton('show-solution');
enableInput();
$myDom.find('.h5p-feedback-available').remove();
};
var calculateMaxScore = function () {
if (blankIsCorrect) {
return params.weight;
}
var maxScore = 0;
for (var i = 0; i < params.answers.length; i++) {
var choice = params.answers[i];
if (choice.correct) {
maxScore += (choice.weight !== undefined ? choice.weight : 1);
}
}
return maxScore;
};
this.getMaxScore = function () {
return (!params.behaviour.singleAnswer && !params.behaviour.singlePoint ? calculateMaxScore() : params.weight);
};
/**
* Check answer
*/
var checkAnswer = function () {
// Unbind removal of feedback dialogs on click
$myDom.unbind('click', removeFeedbackDialog);
// Remove all tip dialogs
removeFeedbackDialog();
if (params.behaviour.enableSolutionsButton) {
self.showButton('show-solution');
}
if (params.behaviour.enableRetry) {
self.showButton('try-again');
}
self.hideButton('check-answer');
self.showCheckSolution();
disableInput();
var xAPIEvent = self.createXAPIEventTemplate('answered');
addQuestionToXAPI(xAPIEvent);
addResponseToXAPI(xAPIEvent);
self.trigger(xAPIEvent);
};
/**
* Determine if any of the radios or checkboxes have been checked.
*
* @return {boolean}
*/
var isAnswerSelected = function () {
return !!$('.h5p-answer[aria-checked="true"]', $myDom).length;
};
/**
* Adds the ui buttons.
* @private
*/
var addButtons = function () {
var $content = $('[data-content-id="' + self.contentId + '"].h5p-content');
var $containerParents = $content.parents('.h5p-container');
// select find container to attach dialogs to
var $container;
if($containerParents.length !== 0) {
// use parent highest up if any
$container = $containerParents.last();
}
else if($content.length !== 0){
$container = $content;
}
else {
$container = $(document.body);
}
// Show solution button
self.addButton('show-solution', params.UI.showSolutionButton, function () {
if (params.behaviour.showSolutionsRequiresInput && !isAnswerSelected()) {
// Require answer before solution can be viewed
self.updateFeedbackContent(params.UI.noInput);
self.read(params.UI.noInput);
}
else {
calcScore();
self.showAllSolutions();
}
}, false, {
'aria-label': params.UI.a11yShowSolution,
});
// Check solution button
if (params.behaviour.enableCheckButton && (!params.behaviour.autoCheck || !params.behaviour.singleAnswer)) {
self.addButton('check-answer', params.UI.checkAnswerButton,
function () {
self.answered = true;
checkAnswer();
},
true,
{
'aria-label': params.UI.a11yCheck,
},
{
confirmationDialog: {
enable: params.behaviour.confirmCheckDialog,
l10n: params.confirmCheck,
instance: self,
$parentElement: $container
},
contentData: self.contentData,
textIfSubmitting: params.UI.submitAnswerButton,
}
);
}
// Try Again button
self.addButton('try-again', params.UI.tryAgainButton, function () {
self.resetTask();
if (params.behaviour.randomAnswers) {
// reshuffle answers
var oldIdMap = idMap;
idMap = getShuffleMap();
var answersDisplayed = $myDom.find('.h5p-answer');
// remember tips
var tip = [];
for (i = 0; i < answersDisplayed.length; i++) {
tip[i] = $(answersDisplayed[i]).find('.h5p-multichoice-tipwrap');
}
// Those two loops cannot be merged or you'll screw up your tips
for (i = 0; i < answersDisplayed.length; i++) {
// move tips and answers on display
$(answersDisplayed[i]).find('.h5p-alternative-inner').html(params.answers[i].text);
$(tip[i]).detach().appendTo($(answersDisplayed[idMap.indexOf(oldIdMap[i])]).find('.h5p-alternative-container'));
}
}
}, false, {
'aria-label': params.UI.a11yRetry,
}, {
confirmationDialog: {
enable: params.behaviour.confirmRetryDialog,
l10n: params.confirmRetry,
instance: self,
$parentElement: $container
}
});
};
/**
* @private
*/
var insertFeedback = function ($e, feedback) {
// Add visuals
addFeedback($e, feedback);
// Add button for readspeakers
var $wrap = $('
', {
'class': 'h5p-hidden-read h5p-feedback-available',
'aria-label': params.UI.feedbackAvailable + '.'
});
$('
', {
'role': 'button',
'tabindex': 0,
'aria-label': params.UI.readFeedback + '.',
appendTo: $wrap,
on: {
keydown: function (e) {
if (e.which === 32) { // Space
self.read(feedback);
return false;
}
}
}
});
$wrap.appendTo($e);
};
/**
* Determine which feedback text to display
*
* @param {number} score
* @param {number} max
* @return {string}
*/
var getFeedbackText = function (score, max) {
var ratio = (score / max);
var feedback = H5P.Question.determineOverallFeedback(params.overallFeedback, ratio);
return feedback.replace('@score', score).replace('@total', max);
};
/**
* Shows feedback on the selected fields.
* @public
* @param {boolean} [skipFeedback] Skip showing feedback if true
*/
this.showCheckSolution = function (skipFeedback) {
var scorePoints;
if (!(params.behaviour.singleAnswer || params.behaviour.singlePoint || !params.behaviour.showScorePoints)) {
scorePoints = new H5P.Question.ScorePoints();
}
$myDom.find('.h5p-answer').each(function (i, e) {
var $e = $(e);
var a = params.answers[i];
var chosen = ($e.attr('aria-checked') === 'true');
if (chosen) {
if (a.correct) {
// May already have been applied by instant feedback
if (!$e.hasClass('h5p-correct')) {
$e.addClass('h5p-correct').append($('
', {
'class': 'h5p-answer-icon',
html: params.UI.correctAnswer + '.'
}));
}
}
else {
if (!$e.hasClass('h5p-wrong')) {
$e.addClass('h5p-wrong').append($('
', {
'class': 'h5p-answer-icon',
html: params.UI.wrongAnswer + '.'
}));
}
}
if (scorePoints) {
var alternativeContainer = $e[0].querySelector('.h5p-alternative-container');
if (!params.behaviour.autoCheck || alternativeContainer.querySelector('.h5p-question-plus-one, .h5p-question-minus-one') === null) {
alternativeContainer.appendChild(scorePoints.getElement(a.correct));
}
}
}
if (!skipFeedback) {
if (chosen && a.tipsAndFeedback.chosenFeedback !== undefined && a.tipsAndFeedback.chosenFeedback !== '') {
insertFeedback($e, a.tipsAndFeedback.chosenFeedback);
}
else if (!chosen && a.tipsAndFeedback.notChosenFeedback !== undefined && a.tipsAndFeedback.notChosenFeedback !== '') {
insertFeedback($e, a.tipsAndFeedback.notChosenFeedback);
}
}
});
// Determine feedback
var max = self.getMaxScore();
// Disable task if maxscore is achieved
var fullScore = (score === max);
if (fullScore) {
self.hideButton('check-answer');
self.hideButton('try-again');
self.hideButton('show-solution');
}
// Show feedback
if (!skipFeedback) {
this.setFeedback(getFeedbackText(score, max), score, max, params.UI.scoreBarLabel);
}
self.trigger('resize');
};
/**
* Disables choosing new input.
*/
var disableInput = function () {
$('.h5p-answer', $myDom).attr({
'aria-disabled': 'true',
'tabindex': '-1'
});
};
/**
* Enables new input.
*/
var enableInput = function () {
$('.h5p-answer', $myDom).attr('aria-disabled', 'false');
};
var calcScore = function () {
score = 0;
for (const answer of params.userAnswers) {
const choice = params.answers[answer];
const weight = (choice.weight !== undefined ? choice.weight : 1);
if (choice.correct) {
score += weight;
}
else {
score -= weight;
}
}
if (score < 0) {
score = 0;
}
if (!params.userAnswers.length && blankIsCorrect) {
score = params.weight;
}
if (params.behaviour.singlePoint) {
score = (100 * score / calculateMaxScore()) >= params.behaviour.passPercentage ? params.weight : 0;
}
};
/**
* Removes selections from task.
*/
var removeSelections = function () {
var $answers = $('.h5p-answer', $myDom)
.removeClass('h5p-selected')
.attr('aria-checked', 'false');
if (!params.behaviour.singleAnswer) {
$answers.attr('tabindex', '0');
}
else {
$answers.first().attr('tabindex', '0');
}
// Set focus to first option
$answers.first().focus();
calcScore();
};
/**
* Get xAPI data.
* Contract used by report rendering engine.
*
* @see contract at {@link https://h5p.org/documentation/developers/contracts#guides-header-6}
*/
this.getXAPIData = function(){
var xAPIEvent = this.createXAPIEventTemplate('answered');
addQuestionToXAPI(xAPIEvent);
addResponseToXAPI(xAPIEvent);
return {
statement: xAPIEvent.data.statement
};
};
/**
* Add the question itself to the definition part of an xAPIEvent
*/
var addQuestionToXAPI = function (xAPIEvent) {
var definition = xAPIEvent.getVerifiedStatementValue(['object', 'definition']);
definition.description = {
// Remove tags, must wrap in div tag because jQuery 1.9 will crash if the string isn't wrapped in a tag.
'en-US': $('
' + params.question + '
').text()
};
definition.type = 'http://adlnet.gov/expapi/activities/cmi.interaction';
definition.interactionType = 'choice';
definition.correctResponsesPattern = [];
definition.choices = [];
for (var i = 0; i < params.answers.length; i++) {
definition.choices[i] = {
'id': params.answers[i].originalOrder + '',
'description': {
// Remove tags, must wrap in div tag because jQuery 1.9 will crash if the string isn't wrapped in a tag.
'en-US': $('
' + params.answers[i].text + '
').text()
}
};
if (params.answers[i].correct) {
if (!params.singleAnswer) {
if (definition.correctResponsesPattern.length) {
definition.correctResponsesPattern[0] += '[,]';
// This looks insane, but it's how you separate multiple answers
// that must all be chosen to achieve perfect score...
}
else {
definition.correctResponsesPattern.push('');
}
definition.correctResponsesPattern[0] += params.answers[i].originalOrder;
}
else {
definition.correctResponsesPattern.push('' + params.answers[i].originalOrder);
}
}
}
};
/**
* Add the response part to an xAPI event
*
* @param {H5P.XAPIEvent} xAPIEvent
* The xAPI event we will add a response to
*/
var addResponseToXAPI = function (xAPIEvent) {
var maxScore = self.getMaxScore();
var success = (100 * score / maxScore) >= params.behaviour.passPercentage;
xAPIEvent.setScoredResult(score, maxScore, self, true, success);
if (params.userAnswers === undefined) {
calcScore();
}
// Add the response
var response = '';
for (var i = 0; i < params.userAnswers.length; i++) {
if (response !== '') {
response += '[,]';
}
response += idMap === undefined ? params.userAnswers[i] : idMap[params.userAnswers[i]];
}
xAPIEvent.data.statement.result.response = response;
};
/**
* Create a map pointing from original answers to shuffled answers
*
* @return {number[]} map pointing from original answers to shuffled answers
*/
var getShuffleMap = function() {
params.answers = H5P.shuffleArray(params.answers);
// Create a map from the new id to the old one
var idMap = [];
for (i = 0; i < params.answers.length; i++) {
idMap[i] = params.answers[i].originalOrder;
}
return idMap;
};
// Initialization code
// Randomize order, if requested
var idMap;
// Store original order in answers
for (i = 0; i < params.answers.length; i++) {
params.answers[i].originalOrder = i;
}
if (params.behaviour.randomAnswers) {
idMap = getShuffleMap();
}
// Start with an empty set of user answers.
params.userAnswers = [];
// Restore previous state
if (contentData && contentData.previousState !== undefined) {
// Restore answers
if (contentData.previousState.answers) {
if (!idMap) {
params.userAnswers = contentData.previousState.answers;
}
else {
// The answers have been shuffled, and we must use the id mapping.
for (i = 0; i < contentData.previousState.answers.length; i++) {
for (var k = 0; k < idMap.length; k++) {
if (idMap[k] === contentData.previousState.answers[i]) {
params.userAnswers.push(k);
}
}
}
}
calcScore();
}
}
var hasCheckedAnswer = false;
// Loop through choices
for (var j = 0; j < params.answers.length; j++) {
var ans = params.answers[j];
if (!params.behaviour.singleAnswer) {
// Set role
ans.role = 'checkbox';
ans.tabindex = '0';
if (params.userAnswers.indexOf(j) !== -1) {
ans.checked = 'true';
hasCheckedAnswer = true;
}
}
else {
// Set role
ans.role = 'radio';
// Determine tabindex, checked and extra classes
if (params.userAnswers.length === 0) {
// No correct answers
if (i === 0 || i === params.answers.length) {
ans.tabindex = '0';
}
}
else if (params.userAnswers.indexOf(j) !== -1) {
// This is the correct choice
ans.tabindex = '0';
ans.checked = 'true';
hasCheckedAnswer = true;
}
}
// Set default
if (ans.tabindex === undefined) {
ans.tabindex = '-1';
}
if (ans.checked === undefined) {
ans.checked = 'false';
}
}
H5P.MultiChoice.counter = (H5P.MultiChoice.counter === undefined ? 0 : H5P.MultiChoice.counter + 1);
params.role = (params.behaviour.singleAnswer ? 'radiogroup' : 'group');
params.label = 'h5p-mcq' + H5P.MultiChoice.counter;
/**
* Pack the current state of the interactivity into a object that can be
* serialized.
*
* @public
*/
this.getCurrentState = function () {
var state = {};
if (!idMap) {
state.answers = params.userAnswers;
}
else {
// The answers have been shuffled and must be mapped back to their
// original ID.
state.answers = [];
for (var i = 0; i < params.userAnswers.length; i++) {
state.answers.push(idMap[params.userAnswers[i]]);
}
}
return state;
};
/**
* Check if user has given an answer.
*
* @param {boolean} [ignoreCheck] Ignore returning true from pressing "check-answer" button.
* @return {boolean} True if answer is given
*/
this.getAnswerGiven = function (ignoreCheck) {
var answered = ignoreCheck ? false : this.answered;
return answered || params.userAnswers.length > 0 || blankIsCorrect;
};
this.getScore = function () {
return score;
};
this.getTitle = function () {
return H5P.createTitle((this.contentData && this.contentData.metadata && this.contentData.metadata.title) ? this.contentData.metadata.title : 'Multiple Choice');
};
};
H5P.MultiChoice.prototype = Object.create(H5P.Question.prototype);
H5P.MultiChoice.prototype.constructor = H5P.MultiChoice;
;
var H5P = H5P || {};
/**
* H5P-Text Utilities
*
* Some functions that can be useful when dealing with texts in H5P.
*
* @param {H5P.jQuery} $
*/
H5P.TextUtilities = function () {
'use strict';
/**
* Create Text Utilities.
*
* Might be needed later.
*
* @constructor
*/
function TextUtilities () {
}
// Inheritance
TextUtilities.prototype = Object.create(H5P.EventDispatcher.prototype);
TextUtilities.prototype.constructor = TextUtilities;
/** @constant {object} */
TextUtilities.WORD_DELIMITER = /[\s.?!,\';\"]/g;
/**
* Check if a candidate string is considered isolated (in a larger string) by
* checking the symbol before and after the candidate.
*
* @param {string} candidate - String to be looked for.
* @param {string} text - (Larger) string that should contain candidate.
* @param {object} params - Parameters.
* @param {object} params.delimiter - Regular expression containing symbols used to isolate the candidate.
* @return {boolean} True if string is isolated.
*/
TextUtilities.isIsolated = function (candidate, text, params) {
// Sanitization
if (!candidate || !text) {
return;
}
var delimiter = (!!params && !!params.delimiter) ? params.delimiter : TextUtilities.WORD_DELIMITER;
var pos = (!!params && !!params.index && typeof params.index === 'number') ? params.index : text.indexOf(candidate);
if (pos < 0 || pos > text.length-1) {
return false;
}
var pred = (pos === 0 ? '' : text[pos - 1].replace(delimiter, ''));
var succ = (pos + candidate.length === text.length ? '' : text[pos + candidate.length].replace(delimiter, ''));
if (pred !== '' || succ !== '') {
return false;
}
return true;
};
/**
* Check whether two strings are considered to be similar.
* The similarity is temporarily computed by word length and number of number of operations
* required to change one word into the other (Damerau-Levenshtein). It's subject to
* change, cmp. https://github.com/otacke/udacity-machine-learning-engineer/blob/master/submissions/capstone_proposals/h5p_fuzzy_blanks.md
*
* @param {String} string1 - String #1.
* @param {String} string2 - String #2.
* @param {object} params - Parameters.
* @return {boolean} True, if strings are considered to be similar.
*/
TextUtilities.areSimilar = function (string1, string2) {
// Sanitization
if (!string1 || typeof string1 !== 'string') {
return;
}
if (!string2 || typeof string2 !== 'string') {
return;
}
// Just temporariliy this unflexible. Will be configurable via params.
var length = Math.min(string1.length, string2.length);
var levenshtein = H5P.TextUtilities.computeLevenshteinDistance(string1, string2, true);
if (levenshtein === 0) {
return true;
}
if ((length > 9) && (levenshtein <= 2)) {
return true;
}
if ((length > 3) && (levenshtein <= 1)) {
return true;
}
return false;
};
/**
* Compute the (Damerau-)Levenshtein distance for two strings.
*
* The (Damerau-)Levenshtein distance that is returned is equivalent to the
* number of operations that are necessary to transform one string into the
* other. Consequently, lower numbers indicate higher similarity between the
* two strings.
*
* While the Levenshtein distance counts deletions, insertions and mismatches,
* the Damerau-Levenshtein distance also counts swapping two characters as
* only one operation (instead of two mismatches), because this seems to
* happen quite often.
*
* See http://en.wikipedia.org/wiki/Damerau%E2%80%93Levenshtein_distance for details
*
* @public
* @param {string} str1 - String no. 1.
* @param {string} str2 - String no. 2.
* @param {boolean} [countSwapping=false] - If true, swapping chars will count as operation.
* @returns {number} Distance.
*/
TextUtilities.computeLevenshteinDistance = function(str1, str2, countSwapping) {
// sanity checks
if (typeof str1 !== 'string' || typeof str2 !== 'string') {
return undefined;
}
if (countSwapping && typeof countSwapping !== 'boolean') {
countSwapping = false;
}
// degenerate cases
if (str1 === str2) {
return 0;
}
if (str1.length === 0) {
return str2.length;
}
if (str2.length === 0) {
return str1.length;
}
// counter variables
var i, j;
// indicates characters that don't match
var cost;
// matrix for storing distances
var distance = [];
// initialization
for (i = 0; i <= str1.length; i++) {
distance[i] = [i];
}
for (j = 0; j <= str2.length; j++) {
distance[0][j] = j;
}
// computation
for (i = 1; i <= str1.length; i++) {
for (j = 1; j <= str2.length; j++) {
cost = (str1[i-1] === str2[j-1]) ? 0 : 1;
distance[i][j] = Math.min(
distance[i-1][j] + 1, // deletion
distance[i][j-1] + 1, // insertion
distance[i-1][j-1] + cost // mismatch
);
// in Damerau-Levenshtein distance, transpositions are operations
if (countSwapping) {
if (i > 1 && j > 1 && str1[i-1] === str2[j-2] && str1[i-2] === str2[j-1]) {
distance[i][j] = Math.min(distance[i][j], distance[i-2][j-2] + cost);
}
}
}
}
return distance[str1.length][str2.length];
};
/**
* Compute the Jaro(-Winkler) distance for two strings.
*
* The Jaro(-Winkler) distance will return a value between 0 and 1 indicating
* the similarity of two strings. The higher the value, the more similar the
* strings are.
*
* See https://en.wikipedia.org/wiki/Jaro%E2%80%93Winkler_distance for details
*
* It seems that a more generalized implementation of Winkler's modification
* can improve the results. This might be implemented later.
* http://disi.unitn.it/~p2p/RelatedWork/Matching/Hermans_bnaic-2012.pdf
*
* @public
* @param {string} str1 - String no. 1.
* @param {string} str2 - String no. 2.
* @param {boolean} [favorSameStart=false] - If true, strings with same start get higher distance value.
* @param {boolean} [longTolerance=false] - If true, Winkler's tolerance for long words will be used.
* @returns {number} Distance.
*/
TextUtilities.computeJaroDistance = function(str1, str2, favorSameStart, longTolerance) {
// sanity checks
if (typeof str1 !== 'string' || typeof str2 !== 'string') {
return undefined;
}
if (favorSameStart && typeof favorSameStart !== 'boolean') {
favorSameStart = false;
}
if (longTolerance && typeof longTolerance !== 'boolean') {
longTolerance = false;
}
// degenerate cases
if (str1.length === 0 || str2.length === 0) {
return 0;
}
if (str1 === str2) {
return 1;
}
// counter variables
var i, j, k;
// number of matches between both strings
var matches = 0;
// number of transpositions between both strings
var transpositions = 0;
// The Jaro-Winkler distance
var distance = 0;
// length of common prefix up to 4 chars
var l = 0;
// scaling factor, should not exceed 0.25 (Winkler default = 0.1)
var p = 0.1;
// will be used often
var str1Len = str1.length;
var str2Len = str2.length;
// determines the distance that still counts as a match
var matchWindow = Math.floor(Math.max(str1Len, str2Len) / 2)- 1;
// will store matches
var str1Flags = new Array(str1Len);
var str2Flags = new Array(str2Len);
// count matches
for (i = 0; i < str1Len; i++) {
var start = (i >= matchWindow) ? i - matchWindow : 0;
var end = (i + matchWindow <= (str2Len - 1)) ? (i + matchWindow) : (str2Len - 1);
for (j = start; j <= end; j++) {
if (str1Flags[i] !== true && str2Flags[j] !== true && str1[i] === str2[j]) {
str1Flags[i] = str2Flags[j] = true;
matches += 1;
break;
}
}
}
if (matches === 0) {
return 0;
}
// count transpositions
k = 0;
for (i = 0; i < str1Len; i++) {
if (!str1Flags[i]) {
continue;
}
while (!str2Flags[k]) {
k += 1;
}
if (str1[i] !== str2[k]) {
transpositions += 1;
}
k += 1;
}
transpositions = transpositions / 2;
// compute Jaro distance
distance = (matches/str1Len + matches/str2Len + (matches - transpositions) / matches) / 3;
// modification used by Winkler
if (favorSameStart) {
if (distance > 0.7 && str1Len > 3 && str2Len > 3) {
while (str1[l] === str2[l] && l < 4) {
l += 1;
}
distance = distance + l * p * (1 - distance);
// modification for long words
if (longTolerance) {
if (Math.max(str1Len, str2Len) > 4 && matches > l + 1 && 2 * matches >= Math.max(str1Len, str2Len) + l) {
distance += ((1.0 - distance) * ((matches - l - 1) / (str1Len + str2Len - 2 * l + 2)));
}
}
}
}
return distance;
};
/**
* Check whether a text contains a string, but fuzzy.
*
* This function is naive. It moves a window of needle's length (+2)
* over the haystack's text and each move compares for similarity using
* a given string metric. This will be slow for long texts!!!
*
* TODO: You might want to look into the bitap algorithm or experiment
* with regexps
*
* @param {String} needle - String to look for.
* @param {String} haystack - Text to look in.
*/
TextUtilities.fuzzyContains = function (needle, haystack) {
return this.fuzzyFind(needle, haystack).contains;
};
/**
* Find the first position of a fuzzy string within a text
* @param {String} needle - String to look for.
* @param {String} haystack - Text to look in.
*/
TextUtilities.fuzzyIndexOf = function (needle, haystack) {
return this.fuzzyFind(needle, haystack).indexOf;
};
/**
* Find the first fuzzy match of a string within a text
* @param {String} needle - String to look for.
* @param {String} haystack - Text to look in.
*/
TextUtilities.fuzzyMatch = function (needle, haystack) {
return this.fuzzyFind(needle, haystack).match;
};
/**
* Find a fuzzy string with in a text.
* TODO: This could be cleaned ...
* @param {String} needle - String to look for.
* @param {String} haystack - Text to look in.
* @param {object} params - Parameters.
*/
TextUtilities.fuzzyFind = function (needle, haystack, params) {
// Sanitization
if (!needle || typeof needle !== 'string') {
return false;
}
if (!haystack || typeof haystack !== 'string') {
return false;
}
if (params === undefined || params.windowSize === undefined || typeof params.windowSize !== 'number') {
params = {'windowSize': 3};
}
var match;
var found = haystack.split(' ').some(function(hay) {
match = hay;
return H5P.TextUtilities.areSimilar(needle, hay);
});
if (found) {
return {'contains' : found, 'match': match, 'index': haystack.indexOf(match)};
}
// This is not used for single words but for phrases
for (var i = 0; i < haystack.length - needle.length + 1; i++) {
var hay = [];
for (var j = 0; j < params.windowSize; j++) {
hay[j] = haystack.substr(i, needle.length + j);
}
// Checking isIsolated will e.g. prevent finding beginnings of words
for (var j = 0; j < hay.length; j++) {
if (TextUtilities.isIsolated(hay[j], haystack) && TextUtilities.areSimilar(hay[j], needle)) {
match = hay[j];
found = true;
break;
}
}
if (found) {
break;
}
}
if (!found) {
match = undefined;
}
return {'contains' : found, 'match': match, 'index': haystack.indexOf(match)};
};
return TextUtilities;
}();
;
/*global H5P*/
H5P.Blanks = (function ($, Question) {
/**
* @constant
* @default
*/
var STATE_ONGOING = 'ongoing';
var STATE_CHECKING = 'checking';
var STATE_SHOWING_SOLUTION = 'showing-solution';
var STATE_FINISHED = 'finished';
const XAPI_ALTERNATIVE_EXTENSION = 'https://h5p.org/x-api/alternatives';
const XAPI_CASE_SENSITIVITY = 'https://h5p.org/x-api/case-sensitivity';
const XAPI_REPORTING_VERSION_EXTENSION = 'https://h5p.org/x-api/h5p-reporting-version';
/**
* @typedef {Object} Params
* Parameters/configuration object for Blanks
*
* @property {Object} Params.behaviour
* @property {string} Params.behaviour.confirmRetryDialog
* @property {string} Params.behaviour.confirmCheckDialog
*
* @property {Object} Params.confirmRetry
* @property {string} Params.confirmRetry.header
* @property {string} Params.confirmRetry.body
* @property {string} Params.confirmRetry.cancelLabel
* @property {string} Params.confirmRetry.confirmLabel
*
* @property {Object} Params.confirmCheck
* @property {string} Params.confirmCheck.header
* @property {string} Params.confirmCheck.body
* @property {string} Params.confirmCheck.cancelLabel
* @property {string} Params.confirmCheck.confirmLabel
*/
/**
* Initialize module.
*
* @class H5P.Blanks
* @extends H5P.Question
* @param {Params} params
* @param {number} id Content identification
* @param {Object} contentData Task specific content data
*/
function Blanks(params, id, contentData) {
var self = this;
// Inheritance
Question.call(self, 'blanks');
// IDs
this.contentId = id;
this.contentData = contentData;
this.params = $.extend(true, {}, {
text: "Fill in",
questions: [
"
Oslo is the capital of *Norway*.
"
],
overallFeedback: [],
userAnswers: [], // TODO This isn't in semantics?
showSolutions: "Show solution",
tryAgain: "Try again",
checkAnswer: "Check",
changeAnswer: "Change answer",
notFilledOut: "Please fill in all blanks to view solution",
answerIsCorrect: "':ans' is correct",
answerIsWrong: "':ans' is wrong",
answeredCorrectly: "Answered correctly",
answeredIncorrectly: "Answered incorrectly",
solutionLabel: "Correct answer:",
inputLabel: "Blank input @num of @total",
inputHasTipLabel: "Tip available",
tipLabel: "Tip",
scoreBarLabel: 'You got :num out of :total points',
behaviour: {
enableRetry: true,
enableSolutionsButton: true,
enableCheckButton: true,
caseSensitive: true,
showSolutionsRequiresInput: true,
autoCheck: false,
separateLines: false
},
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.',
a11yHeader: 'Checking mode',
submitAnswer: 'Submit',
}, params);
// Delete empty questions
for (var i = this.params.questions.length - 1; i >= 0; i--) {
if (!this.params.questions[i]) {
this.params.questions.splice(i, 1);
}
}
// Previous state
this.contentData = contentData;
if (this.contentData !== undefined && this.contentData.previousState !== undefined) {
this.previousState = this.contentData.previousState;
}
// Clozes
this.clozes = [];
// Keep track tabbing forward or backwards
this.shiftPressed = false;
H5P.$body.keydown(function (event) {
if (event.keyCode === 16) {
self.shiftPressed = true;
}
}).keyup(function (event) {
if (event.keyCode === 16) {
self.shiftPressed = false;
}
});
}
// Inheritance
Blanks.prototype = Object.create(Question.prototype);
Blanks.prototype.constructor = Blanks;
/**
* Registers this question type's DOM elements before they are attached.
* Called from H5P.Question.
*/
Blanks.prototype.registerDomElements = function () {
var self = this;
// Check for task media
var media = self.params.media;
if (media && media.type && media.type.library) {
media = media.type;
var type = media.library.split(' ')[0];
if (type === 'H5P.Image') {
if (media.params.file) {
// Register task image
self.setImage(media.params.file.path, {
disableImageZooming: self.params.media.disableImageZooming || false,
alt: media.params.alt,
title: media.params.title
});
}
}
else if (type === 'H5P.Video') {
if (media.params.sources) {
// Register task video
self.setVideo(media);
}
}
}
// Using instructions as label for our text groups
const labelId = 'h5p-blanks-instructions-' + Blanks.idCounter;
// Register task introduction text
self.setIntroduction('
' + self.params.text + '
');
// Register task content area
self.setContent(self.createQuestions(labelId), {
'class': self.params.behaviour.separateLines ? 'h5p-separate-lines' : ''
});
// ... and buttons
self.registerButtons();
// Restore previous state
self.setH5PUserState();
};
/**
* Create all the buttons for the task
*/
Blanks.prototype.registerButtons = function () {
var self = this;
var $content = $('[data-content-id="' + self.contentId + '"].h5p-content');
var $containerParents = $content.parents('.h5p-container');
// select find container to attach dialogs to
var $container;
if ($containerParents.length !== 0) {
// use parent highest up if any
$container = $containerParents.last();
}
else if ($content.length !== 0) {
$container = $content;
}
else {
$container = $(document.body);
}
if (!self.params.behaviour.autoCheck && this.params.behaviour.enableCheckButton) {
// Check answer button
self.addButton('check-answer', self.params.checkAnswer, function () {
// Move focus to top of content
self.a11yHeader.innerHTML = self.params.a11yHeader;
self.a11yHeader.focus();
self.toggleButtonVisibility(STATE_CHECKING);
self.markResults();
self.showEvaluation();
self.triggerAnswered();
}, true, {
'aria-label': self.params.a11yCheck,
}, {
confirmationDialog: {
enable: self.params.behaviour.confirmCheckDialog,
l10n: self.params.confirmCheck,
instance: self,
$parentElement: $container
},
textIfSubmitting: self.params.submitAnswer,
contentData: self.contentData,
});
}
// Show solution button
self.addButton('show-solution', self.params.showSolutions, function () {
self.showCorrectAnswers(false);
}, self.params.behaviour.enableSolutionsButton, {
'aria-label': self.params.a11yShowSolution,
});
// Try again button
if (self.params.behaviour.enableRetry === true) {
self.addButton('try-again', self.params.tryAgain, function () {
self.a11yHeader.innerHTML = '';
self.resetTask();
self.$questions.filter(':first').find('input:first').focus();
}, true, {
'aria-label': self.params.a11yRetry,
}, {
confirmationDialog: {
enable: self.params.behaviour.confirmRetryDialog,
l10n: self.params.confirmRetry,
instance: self,
$parentElement: $container
}
});
}
self.toggleButtonVisibility(STATE_ONGOING);
};
/**
* Find blanks in a string and run a handler on those blanks
*
* @param {string} question
* Question text containing blanks enclosed in asterisks.
* @param {function} handler
* Replaces the blanks text with an input field.
* @returns {string}
* The question with blanks replaced by the given handler.
*/
Blanks.prototype.handleBlanks = function (question, handler) {
// Go through the text and run handler on all asterisk
var clozeEnd, clozeStart = question.indexOf('*');
var self = this;
while (clozeStart !== -1 && clozeEnd !== -1) {
clozeStart++;
clozeEnd = question.indexOf('*', clozeStart);
if (clozeEnd === -1) {
continue; // No end
}
var clozeContent = question.substring(clozeStart, clozeEnd);
var replacer = '';
if (clozeContent.length) {
replacer = handler(self.parseSolution(clozeContent));
clozeEnd++;
}
else {
clozeStart += 1;
}
question = question.slice(0, clozeStart - 1) + replacer + question.slice(clozeEnd);
clozeEnd -= clozeEnd - clozeStart - replacer.length;
// Find the next cloze
clozeStart = question.indexOf('*', clozeEnd);
}
return question;
};
/**
* Create questitons html for DOM
*/
Blanks.prototype.createQuestions = function (labelId) {
var self = this;
var html = '';
var clozeNumber = 0;
for (var i = 0; i < self.params.questions.length; i++) {
var question = self.params.questions[i];
// Go through the question text and replace all the asterisks with input fields
question = self.handleBlanks(question, function (solution) {
// Create new cloze
clozeNumber += 1;
var defaultUserAnswer = (self.params.userAnswers.length > self.clozes.length ? self.params.userAnswers[self.clozes.length] : null);
var cloze = new Blanks.Cloze(solution, self.params.behaviour, defaultUserAnswer, {
answeredCorrectly: self.params.answeredCorrectly,
answeredIncorrectly: self.params.answeredIncorrectly,
solutionLabel: self.params.solutionLabel,
inputLabel: self.params.inputLabel,
inputHasTipLabel: self.params.inputHasTipLabel,
tipLabel: self.params.tipLabel
});
self.clozes.push(cloze);
return cloze;
});
html += '
' + question + '
';
}
self.hasClozes = clozeNumber > 0;
this.$questions = $(html);
self.a11yHeader = document.createElement('div');
self.a11yHeader.classList.add('hidden-but-read');
self.a11yHeader.tabIndex = -1;
self.$questions[0].insertBefore(self.a11yHeader, this.$questions[0].childNodes[0] || null);
// Set input fields.
this.$questions.find('input').each(function (i) {
var afterCheck;
if (self.params.behaviour.autoCheck) {
afterCheck = function () {
var answer = $("
").text(this.getUserAnswer()).html();
self.read((this.correct() ? self.params.answerIsCorrect : self.params.answerIsWrong).replace(':ans', answer));
if (self.done || self.allBlanksFilledOut()) {
// All answers has been given. Show solutions button.
self.toggleButtonVisibility(STATE_CHECKING);
self.showEvaluation();
self.triggerAnswered();
self.done = true;
}
};
}
self.clozes[i].setInput($(this), afterCheck, function () {
self.toggleButtonVisibility(STATE_ONGOING);
if (!self.params.behaviour.autoCheck) {
self.hideEvaluation();
}
}, i, self.clozes.length);
}).keydown(function (event) {
var $this = $(this);
// Adjust width of text input field to match value
self.autoGrowTextField($this);
var $inputs, isLastInput;
var enterPressed = (event.keyCode === 13);
var tabPressedAutoCheck = (event.keyCode === 9 && self.params.behaviour.autoCheck);
if (enterPressed || tabPressedAutoCheck) {
// Figure out which inputs are left to answer
$inputs = self.$questions.find('.h5p-input-wrapper:not(.h5p-correct) .h5p-text-input');
// Figure out if this is the last input
isLastInput = $this.is($inputs[$inputs.length - 1]);
}
if ((tabPressedAutoCheck && isLastInput && !self.shiftPressed) ||
(enterPressed && isLastInput)) {
// Focus first button on next tick
setTimeout(function () {
self.focusButton();
}, 10);
}
if (enterPressed) {
if (isLastInput) {
// Check answers
$this.trigger('blur');
}
else {
// Find next input to focus
$inputs.eq($inputs.index($this) + 1).focus();
}
return false; // Prevent form submission on enter key
}
}).on('change', function () {
self.answered = true;
self.triggerXAPI('interacted');
});
self.on('resize', function () {
self.resetGrowTextField();
});
return this.$questions;
};
/**
*
*/
Blanks.prototype.autoGrowTextField = function ($input) {
// Do not set text field size when separate lines is enabled
if (this.params.behaviour.separateLines) {
return;
}
var self = this;
var fontSize = parseInt($input.css('font-size'), 10);
var minEm = 3;
var minPx = fontSize * minEm;
var rightPadEm = 3.25;
var rightPadPx = fontSize * rightPadEm;
var static_min_pad = 0.5 * fontSize;
setTimeout(function () {
var tmp = $('
', {
'text': $input.val()
});
tmp.css({
'position': 'absolute',
'white-space': 'nowrap',
'font-size': $input.css('font-size'),
'font-family': $input.css('font-family'),
'padding': $input.css('padding'),
'width': 'initial'
});
$input.parent().append(tmp);
var width = tmp.width();
var parentWidth = self.$questions.width();
tmp.remove();
if (width <= minPx) {
// Apply min width
$input.width(minPx + static_min_pad);
}
else if (width + rightPadPx >= parentWidth) {
// Apply max width of parent
$input.width(parentWidth - rightPadPx);
}
else {
// Apply width that wraps input
$input.width(width + static_min_pad);
}
}, 1);
};
/**
* Resize all text field growth to current size.
*/
Blanks.prototype.resetGrowTextField = function () {
var self = this;
this.$questions.find('input').each(function () {
self.autoGrowTextField($(this));
});
};
/**
* Toggle buttons dependent of state.
*
* Using CSS-rules to conditionally show/hide using the data-attribute [data-state]
*/
Blanks.prototype.toggleButtonVisibility = function (state) {
// The show solutions button is hidden if all answers are correct
var allCorrect = (this.getScore() === this.getMaxScore());
if (this.params.behaviour.autoCheck && allCorrect) {
// We are viewing the solutions
state = STATE_FINISHED;
}
if (this.params.behaviour.enableSolutionsButton) {
if (state === STATE_CHECKING && !allCorrect) {
this.showButton('show-solution');
}
else {
this.hideButton('show-solution');
}
}
if (this.params.behaviour.enableRetry) {
if ((state === STATE_CHECKING && !allCorrect) || state === STATE_SHOWING_SOLUTION) {
this.showButton('try-again');
}
else {
this.hideButton('try-again');
}
}
if (state === STATE_ONGOING) {
this.showButton('check-answer');
}
else {
this.hideButton('check-answer');
}
this.trigger('resize');
};
/**
* Check if solution is allowed. Warn user if not
*/
Blanks.prototype.allowSolution = function () {
if (this.params.behaviour.showSolutionsRequiresInput === true) {
if (!this.allBlanksFilledOut()) {
this.updateFeedbackContent(this.params.notFilledOut);
this.read(this.params.notFilledOut);
return false;
}
}
return true;
};
/**
* Check if all blanks are filled out
*
* @method allBlanksFilledOut
* @return {boolean} Returns true if all blanks are filled out.
*/
Blanks.prototype.allBlanksFilledOut = function () {
return !this.clozes.some(function (cloze) {
return !cloze.filledOut();
});
};
/**
* Mark which answers are correct and which are wrong and disable fields if retry is off.
*/
Blanks.prototype.markResults = function () {
var self = this;
for (var i = 0; i < self.clozes.length; i++) {
self.clozes[i].checkAnswer();
if (!self.params.behaviour.enableRetry) {
self.clozes[i].disableInput();
}
}
this.trigger('resize');
};
/**
* Removed marked results
*/
Blanks.prototype.removeMarkedResults = function () {
this.$questions.find('.h5p-input-wrapper').removeClass('h5p-correct h5p-wrong');
this.$questions.find('.h5p-input-wrapper > input').attr('disabled', false);
this.trigger('resize');
};
/**
* Displays the correct answers
* @param {boolean} [alwaysShowSolution]
* Will always show solution if true
*/
Blanks.prototype.showCorrectAnswers = function (alwaysShowSolution) {
if (!alwaysShowSolution && !this.allowSolution()) {
return;
}
this.toggleButtonVisibility(STATE_SHOWING_SOLUTION);
this.hideSolutions();
for (var i = 0; i < this.clozes.length; i++) {
this.clozes[i].showSolution();
}
this.trigger('resize');
};
/**
* Toggle input allowed for all input fields
*
* @method function
* @param {boolean} enabled True if fields should allow input, otherwise false
*/
Blanks.prototype.toggleAllInputs = function (enabled) {
for (var i = 0; i < this.clozes.length; i++) {
this.clozes[i].toggleInput(enabled);
}
};
/**
* Display the correct solution for the input boxes.
*
* This is invoked from CP and QS - be carefull!
*/
Blanks.prototype.showSolutions = function () {
this.params.behaviour.enableSolutionsButton = true;
this.toggleButtonVisibility(STATE_FINISHED);
this.markResults();
this.showEvaluation();
this.showCorrectAnswers(true);
this.toggleAllInputs(false);
//Hides all buttons in "show solution" mode.
this.hideButtons();
};
/**
* Resets the complete task.
* Used in contracts.
* @public
*/
Blanks.prototype.resetTask = function () {
this.answered = false;
this.hideEvaluation();
this.hideSolutions();
this.clearAnswers();
this.removeMarkedResults();
this.toggleButtonVisibility(STATE_ONGOING);
this.resetGrowTextField();
this.toggleAllInputs(true);
this.done = false;
};
/**
* Hides all buttons.
* @public
*/
Blanks.prototype.hideButtons = function () {
this.toggleButtonVisibility(STATE_FINISHED);
};
/**
* Trigger xAPI answered event
*/
Blanks.prototype.triggerAnswered = function () {
this.answered = true;
var xAPIEvent = this.createXAPIEventTemplate('answered');
this.addQuestionToXAPI(xAPIEvent);
this.addResponseToXAPI(xAPIEvent);
this.trigger(xAPIEvent);
};
/**
* Get xAPI data.
* Contract used by report rendering engine.
*
* @see contract at {@link https://h5p.org/documentation/developers/contracts#guides-header-6}
*/
Blanks.prototype.getXAPIData = function () {
var xAPIEvent = this.createXAPIEventTemplate('answered');
this.addQuestionToXAPI(xAPIEvent);
this.addResponseToXAPI(xAPIEvent);
return {
statement: xAPIEvent.data.statement
};
};
/**
* Generate xAPI object definition used in xAPI statements.
* @return {Object}
*/
Blanks.prototype.getxAPIDefinition = function () {
var definition = {};
definition.description = {
'en-US': this.params.text
};
definition.type = 'http://adlnet.gov/expapi/activities/cmi.interaction';
definition.interactionType = 'fill-in';
const clozeSolutions = [];
let crp = '';
// xAPI forces us to create solution patterns for all possible solution combinations
for (var i = 0; i < this.params.questions.length; i++) {
var question = this.handleBlanks(this.params.questions[i], function (solution) {
// Collect all solution combinations for the H5P Alternative extension
clozeSolutions.push(solution.solutions);
// Create a basic response pattern out of the first alternative for each blanks field
crp += (!crp ? '' : '[,]') + solution.solutions[0];
// We replace the solutions in the question with a "blank"
return '__________';
});
definition.description['en-US'] += question;
}
// Set the basic response pattern (not supporting multiple alternatives for blanks)
definition.correctResponsesPattern = [
'{case_matters=' + this.params.behaviour.caseSensitive + '}' + crp,
];
// Add the H5P Alternative extension which provides all the combinations of different answers
// Reporting software will need to support this extension for alternatives to work.
definition.extensions = definition.extensions || {};
definition.extensions[XAPI_CASE_SENSITIVITY] = this.params.behaviour.caseSensitive;
definition.extensions[XAPI_ALTERNATIVE_EXTENSION] = clozeSolutions;
return definition;
};
/**
* Add the question itselt to the definition part of an xAPIEvent
*/
Blanks.prototype.addQuestionToXAPI = function (xAPIEvent) {
var definition = xAPIEvent.getVerifiedStatementValue(['object', 'definition']);
$.extend(true, definition, this.getxAPIDefinition());
// Set reporting module version if alternative extension is used
if (this.hasAlternatives) {
const context = xAPIEvent.getVerifiedStatementValue(['context']);
context.extensions = context.extensions || {};
context.extensions[XAPI_REPORTING_VERSION_EXTENSION] = '1.1.0';
}
};
/**
* Parse the solution text (text between the asterisks)
*
* @param {string} solutionText
* @returns {object} with the following properties
* - tip: the tip text for this solution, undefined if no tip
* - solutions: array of solution words
*/
Blanks.prototype.parseSolution = function (solutionText) {
var tip, solution;
var tipStart = solutionText.indexOf(':');
if (tipStart !== -1) {
// Found tip, now extract
tip = solutionText.slice(tipStart + 1);
solution = solutionText.slice(0, tipStart);
}
else {
solution = solutionText;
}
// Split up alternatives
var solutions = solution.split('/');
this.hasAlternatives = this.hasAlternatives || solutions.length > 1;
// Trim solutions
for (var i = 0; i < solutions.length; i++) {
solutions[i] = H5P.trim(solutions[i]);
//decodes html entities
var elem = document.createElement('textarea');
elem.innerHTML = solutions[i];
solutions[i] = elem.value;
}
return {
tip: tip,
solutions: solutions
};
};
/**
* Add the response part to an xAPI event
*
* @param {H5P.XAPIEvent} xAPIEvent
* The xAPI event we will add a response to
*/
Blanks.prototype.addResponseToXAPI = function (xAPIEvent) {
xAPIEvent.setScoredResult(this.getScore(), this.getMaxScore(), this);
xAPIEvent.data.statement.result.response = this.getxAPIResponse();
};
/**
* Generate xAPI user response, used in xAPI statements.
* @return {string} User answers separated by the "[,]" pattern
*/
Blanks.prototype.getxAPIResponse = function () {
var usersAnswers = this.getCurrentState();
return usersAnswers.join('[,]');
};
/**
* Show evaluation widget, i.e: 'You got x of y blanks correct'
*/
Blanks.prototype.showEvaluation = function () {
var maxScore = this.getMaxScore();
var score = this.getScore();
var scoreText = H5P.Question.determineOverallFeedback(this.params.overallFeedback, score / maxScore).replace('@score', score).replace('@total', maxScore);
this.setFeedback(scoreText, score, maxScore, this.params.scoreBarLabel);
if (score === maxScore) {
this.toggleButtonVisibility(STATE_FINISHED);
}
};
/**
* Hide the evaluation widget
*/
Blanks.prototype.hideEvaluation = function () {
// Clear evaluation section.
this.removeFeedback();
};
/**
* Hide solutions. (/try again)
*/
Blanks.prototype.hideSolutions = function () {
// Clean solution from quiz
this.$questions.find('.h5p-correct-answer').remove();
};
/**
* Get maximum number of correct answers.
*
* @returns {Number} Max points
*/
Blanks.prototype.getMaxScore = function () {
var self = this;
return self.clozes.length;
};
/**
* Count the number of correct answers.
*
* @returns {Number} Points
*/
Blanks.prototype.getScore = function () {
var self = this;
var correct = 0;
for (var i = 0; i < self.clozes.length; i++) {
if (self.clozes[i].correct()) {
correct++;
}
self.params.userAnswers[i] = self.clozes[i].getUserAnswer();
}
return correct;
};
Blanks.prototype.getTitle = function () {
return H5P.createTitle((this.contentData.metadata && this.contentData.metadata.title) ? this.contentData.metadata.title : 'Fill In');
};
/**
* Clear the user's answers
*/
Blanks.prototype.clearAnswers = function () {
this.clozes.forEach(function (cloze) {
cloze.setUserInput('');
cloze.resetAriaLabel();
});
};
/**
* Checks if all has been answered.
*
* @returns {Boolean}
*/
Blanks.prototype.getAnswerGiven = function () {
return this.answered || !this.hasClozes;
};
/**
* Helps set focus the given input field.
* @param {jQuery} $input
*/
Blanks.setFocus = function ($input) {
setTimeout(function () {
$input.focus();
}, 1);
};
/**
* Returns an object containing content of each cloze
*
* @returns {object} object containing content for each cloze
*/
Blanks.prototype.getCurrentState = function () {
var clozesContent = [];
// Get user input for every cloze
this.clozes.forEach(function (cloze) {
clozesContent.push(cloze.getUserAnswer());
});
return clozesContent;
};
/**
* Sets answers to current user state
*/
Blanks.prototype.setH5PUserState = function () {
var self = this;
var isValidState = (this.previousState !== undefined &&
this.previousState.length &&
this.previousState.length === this.clozes.length);
// Check that stored user state is valid
if (!isValidState) {
return;
}
// Set input from user state
var hasAllClozesFilled = true;
this.previousState.forEach(function (clozeContent, ccIndex) {
// Register that an answer has been given
if (clozeContent.length) {
self.answered = true;
}
var cloze = self.clozes[ccIndex];
cloze.setUserInput(clozeContent);
// Handle instant feedback
if (self.params.behaviour.autoCheck) {
if (cloze.filledOut()) {
cloze.checkAnswer();
}
else {
hasAllClozesFilled = false;
}
}
});
if (self.params.behaviour.autoCheck && hasAllClozesFilled) {
self.showEvaluation();
self.toggleButtonVisibility(STATE_CHECKING);
}
};
/**
* Disables any active input. Useful for freezing the task and dis-allowing
* modification of wrong answers.
*/
Blanks.prototype.disableInput = function () {
this.$questions.find('input').attr('disabled', true);
};
Blanks.idCounter = 0;
return Blanks;
})(H5P.jQuery, H5P.Question);
/**
* Static utility method for parsing H5P.Blanks qestion into a format useful
* for creating reports.
*
* Example question: 'H5P content may be edited using a *browser/web-browser:something you use every day*.'
*
* Produces the following result:
* [
* {
* type: 'text',
* content: 'H5P content may be edited using a '
* },
* {
* type: 'answer',
* correct: ['browser', 'web-browser']
* },
* {
* type: 'text',
* content: '.'
* }
* ]
*
* @param {string} question
*/
H5P.Blanks.parseText = function (question) {
var blank = new H5P.Blanks({ question: question });
/**
* Parses a text into an array where words starting and ending
* with an asterisk are separated from other text.
* e.g ["this", "*is*", " an ", "*example*"]
*
* @param {string} text
*
* @return {string[]}
*/
function tokenizeQuestionText(text) {
return text.split(/(\*.*?\*)/).filter(function (str) {
return str.length > 0; }
);
}
function startsAndEndsWithAnAsterisk(str) {
return str.substr(0,1) === '*' && str.substr(-1) === '*';
}
function replaceHtmlTags(str, value) {
return str.replace(/<[^>]*>/g, value);
}
return tokenizeQuestionText(replaceHtmlTags(question, '')).map(function (part) {
return startsAndEndsWithAnAsterisk(part) ?
({
type: 'answer',
correct: blank.parseSolution(part.slice(1, -1)).solutions
}) :
({
type: 'text',
content: part
});
});
};
;
(function ($, Blanks) {
/**
* Simple private class for keeping track of clozes.
*
* @class H5P.Blanks.Cloze
* @param {string} answer
* @param {Object} behaviour Behavioral settings for the task from semantics
* @param {boolean} behaviour.acceptSpellingErrors - If true, answers will also count correct if they contain small spelling errors.
* @param {string} defaultUserAnswer
* @param {Object} l10n Localized texts
* @param {string} l10n.solutionLabel Assistive technology label for cloze solution
* @param {string} l10n.inputLabel Assistive technology label for cloze input
* @param {string} l10n.inputHasTipLabel Assistive technology label for input with tip
* @param {string} l10n.tipLabel Label for tip icon
*/
Blanks.Cloze = function (solution, behaviour, defaultUserAnswer, l10n) {
var self = this;
var $input, $wrapper;
var answers = solution.solutions;
var answer = answers.join('/');
var tip = solution.tip;
var checkedAnswer = null;
var inputLabel = l10n.inputLabel;
if (behaviour.caseSensitive !== true) {
// Convert possible solutions into lowercase
for (var i = 0; i < answers.length; i++) {
answers[i] = answers[i].toLowerCase();
}
}
/**
* Check if the answer is correct.
*
* @private
* @param {string} answered
*/
var correct = function (answered) {
if (behaviour.caseSensitive !== true) {
answered = answered.toLowerCase();
}
for (var i = 0; i < answers.length; i++) {
// Damerau-Levenshtein comparison
if (behaviour.acceptSpellingErrors === true) {
var levenshtein = H5P.TextUtilities.computeLevenshteinDistance(answered, answers[i], true);
/*
* The correctness is temporarily computed by word length and number of number of operations
* required to change one word into the other (Damerau-Levenshtein). It's subject to
* change, cmp. https://github.com/otacke/udacity-machine-learning-engineer/blob/master/submissions/capstone_proposals/h5p_fuzzy_blanks.md
*/
if ((answers[i].length > 9) && (levenshtein <= 2)) {
return true;
} else if ((answers[i].length > 3) && (levenshtein <= 1)) {
return true;
}
}
// regular comparison
if (answered === answers[i]) {
return true;
}
}
return false;
};
/**
* Check if filled out.
*
* @param {boolean}
*/
this.filledOut = function () {
var answered = this.getUserAnswer();
// Blank can be correct and is interpreted as filled out.
return (answered !== '' || correct(answered));
};
/**
* Check the cloze and mark it as wrong or correct.
*/
this.checkAnswer = function () {
checkedAnswer = this.getUserAnswer();
var isCorrect = correct(checkedAnswer);
if (isCorrect) {
$wrapper.addClass('h5p-correct');
$input.attr('disabled', true)
.attr('aria-label', inputLabel + '. ' + l10n.answeredCorrectly);
}
else {
$wrapper.addClass('h5p-wrong');
$input.attr('aria-label', inputLabel + '. ' + l10n.answeredIncorrectly);
}
};
/**
* Disables input.
* @method disableInput
*/
this.disableInput = function () {
this.toggleInput(false);
};
/**
* Enables input.
* @method enableInput
*/
this.enableInput = function () {
this.toggleInput(true);
};
/**
* Toggles input enable/disable
* @method toggleInput
* @param {boolean} enabled True if input should be enabled, otherwise false
*/
this.toggleInput = function (enabled) {
$input.attr('disabled', !enabled);
};
/**
* Show the correct solution.
*/
this.showSolution = function () {
if (correct(this.getUserAnswer())) {
return; // Only for the wrong ones
}
$('
', {
'aria-hidden': true,
'class': 'h5p-correct-answer',
text: answer,
insertAfter: $wrapper
});
$input.attr('disabled', true);
var ariaLabel = inputLabel + '. ' +
l10n.solutionLabel + ' ' + answer + '. ' +
l10n.answeredIncorrectly;
$input.attr('aria-label', ariaLabel);
};
/**
* @returns {boolean}
*/
this.correct = function () {
return correct(this.getUserAnswer());
};
/**
* Set input element.
*
* @param {H5P.jQuery} $element
* @param {function} afterCheck
* @param {function} afterFocus
* @param {number} clozeIndex Index of cloze
* @param {number} totalCloze Total amount of clozes in blanks
*/
this.setInput = function ($element, afterCheck, afterFocus, clozeIndex, totalCloze) {
$input = $element;
$wrapper = $element.parent();
inputLabel = inputLabel.replace('@num', (clozeIndex + 1))
.replace('@total', totalCloze);
// Add tip if tip is set
if(tip !== undefined && tip.trim().length > 0) {
$wrapper.addClass('has-tip')
.append(H5P.JoubelUI.createTip(tip, {
tipLabel: l10n.tipLabel
}));
inputLabel += '. ' + l10n.inputHasTipLabel;
}
$input.attr('aria-label', inputLabel);
if (afterCheck !== undefined) {
$input.blur(function () {
if (self.filledOut()) {
// Check answers
if (!behaviour.enableRetry) {
self.disableInput();
}
self.checkAnswer();
afterCheck.apply(self);
}
});
}
$input.keyup(function () {
if (checkedAnswer !== null && checkedAnswer !== self.getUserAnswer()) {
// The Answer has changed since last check
checkedAnswer = null;
$wrapper.removeClass('h5p-wrong');
$input.attr('aria-label', inputLabel);
if (afterFocus !== undefined) {
afterFocus();
}
}
});
};
/**
* @returns {string} Cloze html
*/
this.toString = function () {
var extra = defaultUserAnswer ? ' value="' + defaultUserAnswer + '"' : '';
var result = ' ';
self.length = result.length;
return result;
};
/**
* @returns {string} Trimmed answer
*/
this.getUserAnswer = function () {
return H5P.trim($input.val());
};
/**
* @param {string} text New input text
*/
this.setUserInput = function (text) {
$input.val(text);
};
/**
* Resets aria label of input field
*/
this.resetAriaLabel = function () {
$input.attr('aria-label', inputLabel);
};
};
})(H5P.jQuery, H5P.Blanks);
;
var H5P = H5P || {};
H5P.Essay = function ($, Question) {
'use strict';
// CSS Classes
var SOLUTION_CONTAINER = 'h5p-essay-solution-container';
var SOLUTION_TITLE = 'h5p-essay-solution-title';
var SOLUTION_INTRODUCTION = 'h5p-essay-solution-introduction';
var SOLUTION_SAMPLE = 'h5p-essay-solution-sample';
var SOLUTION_SAMPLE_TEXT = 'h5p-essay-solution-sample-text';
// The H5P feedback right now only expects true (green)/false (red) feedback, not neutral feedback
var FEEDBACK_EMPTY= '... ';
/**
* @constructor
* @param {Object} config - Config from semantics.json.
* @param {string} contentId - ContentId.
* @param {Object} [contentData] - contentData.
*/
function Essay(config, contentId, contentData) {
// Initialize
if (!config) {
return;
}
// Inheritance
Question.call(this, 'essay');
// Sanitize defaults
this.params = this.extend(
{
media: {},
taskDescription: '',
solution: {},
keywords: [],
overallFeedback: [],
behaviour: {
minimumLength: 0,
inputFieldSize: 10,
enableRetry: true,
ignoreScoring: false,
pointsHost: 1
},
checkAnswer: 'Check',
tryAgain: 'Retry',
showSolution: 'Show solution',
feedbackHeader: 'Feedback',
solutionTitle: 'Sample solution',
remainingChars: 'Remaining characters: @chars',
notEnoughChars: 'You must enter at least @chars characters!',
messageSave: 'saved',
ariaYourResult: 'You got @score out of @total points',
ariaNavigatedToSolution: 'Navigated to newly included sample solution after textarea.'
},
config);
this.contentId = contentId;
this.score = 0;
this.isAnswered = false;
this.internalShowSolutionsCall = false;
// Sanitize HTML encoding
this.params.placeholderText = this.htmlDecode(this.params.placeholderText || '');
// Get previous state from content data
if (typeof contentData !== 'undefined' && typeof contentData.previousState !== 'undefined') {
this.previousState = contentData.previousState;
}
/*
* this.params.behaviour.enableSolutionsButton and this.params.behaviour.enableRetry are used by
* contract at {@link https://h5p.org/documentation/developers/contracts#guides-header-8} and
* {@link https://h5p.org/documentation/developers/contracts#guides-header-9}
*/
this.params.behaviour.enableSolutionsButton = (typeof this.params.solution.sample !== 'undefined' && this.params.solution.sample !== '');
this.params.behaviour.enableRetry = this.params.behaviour.enableRetry || false;
// Determine the minimum number of characters that should be entered
this.params.behaviour.minimumLength = this.params.behaviour.minimumLength || 0;
if (this.params.behaviour.maximumLength !== undefined) {
this.params.behaviour.minimumLength = Math.min(this.params.behaviour.minimumLength, this.params.behaviour.maximumLength);
}
// map function
var toPoints = function (keyword) {
return (keyword.keyword && keyword.options && keyword.options.points || 0) * (keyword.options.occurrences || 1);
};
// reduce function
var sum = function (a, b) {
return a + b;
};
// scoreMax = Maximum number of points available by all keyword groups
var scoreMax = this.params.keywords
.map(toPoints)
.reduce(sum, 0);
// scoreMastering: score indicating mastery and maximum number on progress bar (can be < scoreMax)
this.scoreMastering = this.params.behaviour.percentageMastering === undefined ?
scoreMax :
this.params.behaviour.percentageMastering * scoreMax / 100;
// scorePassing: score to pass the task (<= scoreMastering)
this.scorePassing = Math.min(
this.getMaxScore(),
this.params.behaviour.percentagePassing * scoreMax / 100 || 0);
this.solution = this.buildSolution();
}
// Extends Question
Essay.prototype = Object.create(Question.prototype);
Essay.prototype.constructor = Essay;
/**
* Register the DOM elements with H5P.Question.
*/
Essay.prototype.registerDomElements = function () {
// Set optional media
var media = (this.params.media) ? this.params.media.type : undefined;
if (media && media.library) {
var type = media.library.split(' ')[0];
if (type === 'H5P.Image') {
if (media.params.file) {
this.setImage(media.params.file.path, {
disableImageZooming: this.params.media.disableImageZooming,
alt: media.params.alt,
title: media.params.title
});
}
}
else if (type === 'H5P.Video') {
if (media.params.sources) {
this.setVideo(media);
}
}
}
// Create InputField
this.inputField = new H5P.Essay.InputField({
'taskDescription': this.params.taskDescription,
'placeholderText': this.params.placeholderText,
'maximumLength': this.params.behaviour.maximumLength,
'remainingChars': this.params.remainingChars,
'inputFieldSize': this.params.behaviour.inputFieldSize
}, this.previousState);
// Register task introduction text
this.setIntroduction(this.inputField.getIntroduction());
// Register content
this.content = this.inputField.getContent();
this.setContent(this.content);
// Register Buttons
this.addButtons();
};
/**
* Add all the buttons that shall be passed to H5P.Question.
*/
Essay.prototype.addButtons = function () {
var that = this;
// Show solution button
that.addButton('show-solution', that.params.showSolution, function () {
// Not using a parameter for showSolutions to not mess with possibe future contract changes
that.internalShowSolutionsCall = true;
that.showSolutions();
that.internalShowSolutionsCall = false;
}, false, {}, {});
// Check answer button
that.addButton('check-answer', that.params.checkAnswer, function () {
// Show message if the minimum number of characters has not been met
if (that.inputField.getText().length < that.params.behaviour.minimumLength) {
var message = that.params.notEnoughChars.replace(/@chars/g, that.params.behaviour.minimumLength);
that.inputField.setMessageChars(message, true);
that.read(message);
return;
}
that.inputField.disable();
/*
* Only set true on "check". Result computation may take some time if
* there are many keywords due to the fuzzy match checking, so it's not
* a good idea to do this while typing.
*/
that.isAnswered = true;
that.handleEvaluation();
if (that.params.behaviour.enableSolutionsButton === true) {
that.showButton('show-solution');
}
that.hideButton('check-answer');
}, true, {}, {});
// Retry button
that.addButton('try-again', that.params.tryAgain, function () {
that.resetTask();
}, false, {}, {});
};
/**
* Get the user input from DOM.
* @param {string} [linebreakReplacement=' '] Replacement for line breaks.
* @return {string} Cleaned input.
*/
Essay.prototype.getInput = function (linebreakReplacement) {
linebreakReplacement = linebreakReplacement || ' ';
return this.inputField
.getText()
.replace(/(\r\n|\r|\n)/g, linebreakReplacement)
.replace(/\s\s/g, ' ');
};
/**
* Check if Essay has been submitted/minimum length met.
* @return {boolean} True, if answer was given.
* @see contract at {@link https://h5p.org/documentation/developers/contracts#guides-header-1}
*/
Essay.prototype.getAnswerGiven = function () {
return this.isAnswered;
};
/**
* Get latest score.
* @return {number} latest score.
* @see contract at {@link https://h5p.org/documentation/developers/contracts#guides-header-2}
*/
Essay.prototype.getScore = function () {
// Return value is rounded because reporting module for moodle's H5P plugin expects integers
return (this.params.behaviour.ignoreScoring) ? this.getMaxScore() : Math.round(this.score);
};
/**
* Get maximum possible score.
* @return {number} Score necessary for mastering.
* @see contract at {@link https://h5p.org/documentation/developers/contracts#guides-header-3}
*/
Essay.prototype.getMaxScore = function () {
// Return value is rounded because reporting module for moodle's H5P plugin expects integers
return (this.params.behaviour.ignoreScoring) ? this.params.behaviour.pointsHost || 0 : Math.round(this.scoreMastering);
};
/**
* Show solution.
* @see contract at {@link https://h5p.org/documentation/developers/contracts#guides-header-4}
*/
Essay.prototype.showSolutions = function () {
// We add the sample solution here to make cheating at least a little more difficult
if (this.solution.getElementsByClassName(SOLUTION_SAMPLE)[0].children.length === 0) {
var text = document.createElement('div');
text.classList.add(SOLUTION_SAMPLE_TEXT);
text.innerHTML = this.params.solution.sample;
this.solution.getElementsByClassName(SOLUTION_SAMPLE)[0].appendChild(text);
}
// Insert solution after explanations or content.
var predecessor = this.content.parentNode;
predecessor.parentNode.insertBefore(this.solution, predecessor.nextSibling);
// Useful for accessibility, but seems to jump to wrong position on some Safari versions
this.solutionAnnouncer.focus();
this.hideButton('show-solution');
// Handle calls from the outside
if (!this.internalShowSolutionsCall) {
this.hideButton('check-answer');
this.hideButton('try-again');
}
this.trigger('resize');
};
/**
* Reset task.
* @see contract at {@link https://h5p.org/documentation/developers/contracts#guides-header-5}
*/
Essay.prototype.resetTask = function () {
this.setExplanation();
this.removeFeedback();
this.hideSolution();
this.hideButton('show-solution');
this.hideButton('try-again');
this.showButton('check-answer');
this.inputField.enable();
this.inputField.focus();
this.isAnswered = false;
};
/**
* Get xAPI data.
* @return {Object} xAPI statement.
* @see contract at {@link https://h5p.org/documentation/developers/contracts#guides-header-6}
*/
Essay.prototype.getXAPIData = function () {
return {
statement: this.getXAPIAnswerEvent().data.statement
};
};
/**
* Determine whether the task has been passed by the user.
* @return {boolean} True if user passed or task is not scored.
*/
Essay.prototype.isPassed = function () {
return (this.params.behaviour.ignoreScoring || this.getScore() >= this.scorePassing);
};
/**
* Handle the evaluation.
*/
Essay.prototype.handleEvaluation = function () {
var results = this.computeResults();
// Build explanations
var explanations = this.buildExplanation(results);
// Show explanations
if (explanations.length > 0) {
this.setExplanation(explanations, this.params.feedbackHeader);
}
// Not all keyword groups might be necessary for mastering
this.score = Math.min(this.computeScore(results), this.getMaxScore());
var textScore = H5P.Question
.determineOverallFeedback(this.params.overallFeedback, this.getScore() / this.getMaxScore())
.replace('@score', this.getScore())
.replace('@total', this.getMaxScore());
if (!this.params.behaviour.ignoreScoring) {
var ariaMessage = (this.params.ariaYourResult)
.replace('@score', this.getScore())
.replace('@total', this.getMaxScore());
this.setFeedback(textScore, this.getScore(), this.getMaxScore(), ariaMessage);
}
// Show and hide buttons as necessary
this.handleButtons(this.getScore());
// Trigger xAPI statements as necessary
this.handleXAPI();
this.trigger('resize');
};
/**
* Build solution DOM object.
* @return {Object} DOM object.
*/
Essay.prototype.buildSolution = function () {
var solution = document.createElement('div');
solution.classList.add(SOLUTION_CONTAINER);
this.solutionAnnouncer = document.createElement('div');
this.solutionAnnouncer.setAttribute('tabindex', '0');
this.solutionAnnouncer.setAttribute('aria-label', this.params.ariaNavigatedToSolution);
this.solutionAnnouncer.addEventListener('focus', function (event) {
// Just temporary tabbable element. Will be announced by readspaker.
event.target.blur();
event.target.setAttribute('tabindex', '-1');
});
solution.appendChild(this.solutionAnnouncer);
var solutionTitle = document.createElement('div');
solutionTitle.classList.add(SOLUTION_TITLE);
solutionTitle.innerHTML = this.params.solutionTitle;
solution.appendChild(solutionTitle);
var solutionIntroduction = document.createElement('div');
solutionIntroduction.classList.add(SOLUTION_INTRODUCTION);
solutionIntroduction.innerHTML = this.params.solution.introduction;
solution.appendChild(solutionIntroduction);
var solutionSample = document.createElement('div');
solutionSample.classList.add(SOLUTION_SAMPLE);
solution.appendChild(solutionSample);
return solution;
};
/**
* Hide the solution.
*/
Essay.prototype.hideSolution = function () {
if (this.solution.parentNode !== null) {
this.solution.parentNode.removeChild(this.solution);
}
};
/**
* Compute results.
* @return {Object[]} Results: [[{"keyword": keyword, "match": match, "index": index}*]*].
*/
Essay.prototype.computeResults = function () {
var that = this;
var results = [];
// Should not happen, but just to be sure ...
this.params.keywords = this.params.keywords || [];
// Filter out keywords that have not been set.
this.params.keywords = this.params.keywords.filter(function (element) {
return typeof element.keyword !== 'undefined';
});
this.params.keywords.forEach(function (alternativeGroup) {
var resultsGroup = [];
var options = alternativeGroup.options;
var alternatives = [alternativeGroup.keyword || []]
.concat(alternativeGroup.alternatives || [])
.map(function (alternative) {
return that.htmlDecode(alternative);
});
// Not chained, because we still need the old value inside
alternatives = alternatives
// only "normal" alternatives
.filter(function (alternative) {
return (alternative[0] !== '/' || alternative[alternative.length - 1] !== '/');
})
// regular matches found in text for alternatives
.concat(that.getRegExpAlternatives(alternatives, that.getInput()))
// regular matches could match empty string
.filter(function (alternative) {
return alternative !== '';
});
// Detect all matches
alternatives.forEach(function (alternative) {
var inputTest = that.getInput();
// Check for case sensitivity
if (!options.caseSensitive || that.params.behaviour.overrideCaseSensitive === 'off') {
alternative = alternative.toLowerCase();
inputTest = inputTest.toLowerCase();
}
// Build array of matches for each type of match
var matchesExact = that.detectExactMatches(alternative, inputTest);
var matchesWildcard = alternative.indexOf('*') !== -1 ? that.detectWildcardMatches(alternative, inputTest) : [];
var matchesFuzzy = options.forgiveMistakes ? that.detectFuzzyMatches(alternative, inputTest) : [];
// Merge matches without duplicates
that.mergeMatches(matchesExact, matchesWildcard, matchesFuzzy).forEach(function (item) {
resultsGroup.push(item);
});
});
results.push(resultsGroup);
});
return results;
};
/**
* Compute the score for the results.
* @param {Object[]} results - Results from the task.
* @return {number} Score.
*/
Essay.prototype.computeScore = function (results) {
var score = 0;
this.params.keywords.forEach(function (keyword, i) {
score += Math.min(results[i].length, keyword.options.occurrences) * keyword.options.points;
});
return score;
};
/**
* Build the explanations for H5P.Question.setExplanation.
* @param {Object} results - Results from the task.
* @return {Object[]} Explanations for H5P.Question.
*/
Essay.prototype.buildExplanation = function (results) {
var explanations = [];
var word;
this.params.keywords.forEach(function (keyword, i) {
word = FEEDBACK_EMPTY;
// Keyword was not found and feedback is provided for this case
if (results[i].length === 0 && keyword.options.feedbackMissed) {
if (keyword.options.feedbackMissedWord === 'keyword') {
// Main keyword defined
word = keyword.keyword;
}
explanations.push({correct: word, text: keyword.options.feedbackMissed});
}
// Keyword found and feedback is provided for this case
if (results[i].length > 0 && keyword.options.feedbackIncluded) {
// Set word in front of feedback
switch (keyword.options.feedbackIncludedWord) {
case 'keyword':
// Main keyword defined
word = keyword.keyword;
break;
case 'alternative':
// Alternative that was found
word = results[i][0].keyword;
break;
case 'answer':
// Answer matching an alternative at the learner typed it
word = results[i][0].match;
break;
}
explanations.push({correct: word, text: keyword.options.feedbackIncluded});
}
});
if (explanations.length > 0) {
// Sort "included" before "not included", but keep order otherwise
explanations.sort(function (a, b) {
return a.correct === FEEDBACK_EMPTY && b.correct !== FEEDBACK_EMPTY;
});
}
return explanations;
};
/**
* Handle buttons' visibility.
* @param {number} score - Score the user received.
*/
Essay.prototype.handleButtons = function (score) {
if (this.params.solution.sample && !this.solution) {
this.showButton('show-solution');
}
// We need the retry button if the mastering score has not been reached or scoring is irrelevant
if (score < this.getMaxScore() || this.params.behaviour.ignoreScoring) {
if (this.params.behaviour.enableRetry) {
this.showButton('try-again');
}
}
else {
this.hideButton('try-again');
}
};
/**
* Handle xAPI event triggering
* @param {number} score - Score the user received.
*/
Essay.prototype.handleXAPI = function () {
this.trigger(this.getXAPIAnswerEvent());
// Additional xAPI verbs that might be useful for making analytics easier
if (!this.params.behaviour.ignoreScoring) {
if (this.getScore() < this.scorePassing) {
this.trigger(this.createEssayXAPIEvent('failed'));
}
else {
this.trigger(this.createEssayXAPIEvent('passed'));
}
if (this.getScore() >= this.getMaxScore()) {
this.trigger(this.createEssayXAPIEvent('mastered'));
}
}
};
/**
* Create an xAPI event for Essay.
* @param {string} verb - Short id of the verb we want to trigger.
* @return {H5P.XAPIEvent} Event template.
*/
Essay.prototype.createEssayXAPIEvent = function (verb) {
var xAPIEvent = this.createXAPIEventTemplate(verb);
this.extend(
xAPIEvent.getVerifiedStatementValue(['object', 'definition']),
this.getxAPIDefinition());
return xAPIEvent;
};
/**
* Get the xAPI definition for the xAPI object.
* return {Object} XAPI definition.
*/
Essay.prototype.getxAPIDefinition = function () {
var definition = {};
definition.name = {'en-US': 'Essay'};
// The H5P reporting module expects the "blanks" to be added to the description
definition.description = {'en-US': this.params.taskDescription + Essay.FILL_IN_PLACEHOLDER};
definition.type = 'http://id.tincanapi.com/activitytype/essay';
definition.interactionType = 'long-fill-in';
/*
* The official xAPI documentation discourages to use a correct response
* pattern it if the criteria for a question are complex and correct
* responses cannot be exhaustively listed. They can't.
*/
return definition;
};
/**
* Build xAPI answer event.
* @return {H5P.XAPIEvent} xAPI answer event.
*/
Essay.prototype.getXAPIAnswerEvent = function () {
var xAPIEvent = this.createEssayXAPIEvent('answered');
xAPIEvent.setScoredResult(this.getScore(), this.getMaxScore(), this, true, this.isPassed());
xAPIEvent.data.statement.result.response = this.inputField.getText();
return xAPIEvent;
};
/**
* Detect exact matches of needle in haystack.
* @param {string} needle - Word or phrase to find.
* @param {string} haystack - Text to find the word or phrase in.
* @return {Object[]} Results: [{'keyword': needle, 'match': needle, 'index': front + pos}*].
*/
Essay.prototype.detectExactMatches = function (needle, haystack) {
// Simply detect all exact matches and its positions in the haystack
var result = [];
var pos = -1;
var front = 0;
needle = needle.replace(/\*/, '');
while ((pos = haystack.indexOf(needle)) !== -1) {
if (H5P.TextUtilities.isIsolated(needle, haystack)) {
result.push({'keyword': needle, 'match': needle, 'index': front + pos});
}
front += pos + needle.length;
haystack = haystack.substr(pos + needle.length);
}
return result;
};
/**
* Detect wildcard matches of needle in haystack.
* @param {string} needle - Word or phrase to find.
* @param {string} haystack - Text to find the word or phrase in.
* @return {Object[]} Results: [{'keyword': needle, 'match': needle, 'index': front + pos}*].
*/
Essay.prototype.detectWildcardMatches = function (needle, haystack) {
if (needle.indexOf('*') === -1) {
return [];
}
// Clean needle from successive wildcards
needle = needle.replace(/[*]{2,}/g, '*');
// Clean needle from regular expression characters, * needed for wildcard
var regexpChars = ['\\', '.', '[', ']', '?', '+', '(', ')', '{', '}', '|', '!', '^', '-'];
regexpChars.forEach(function (char) {
needle = needle.split(char).join('\\' + char);
});
// We accept only characters for the wildcard
var regexp = new RegExp(needle.replace(/\*/g, Essay.CHARS_WILDCARD + '+'), 'g');
var result = [];
var match;
while ((match = regexp.exec(haystack)) !== null ) {
if (H5P.TextUtilities.isIsolated(match[0], haystack, {'index': match.index})) {
result.push({'keyword': needle, 'match': match[0], 'index': match.index});
}
}
return result;
};
/**
* Detect fuzzy matches of needle in haystack.
* @param {string} needle - Word or phrase to find.
* @param {string} haystack - Text to find the word or phrase in.
* @param {Object[]} Results.
*/
Essay.prototype.detectFuzzyMatches = function (needle, haystack) {
// Ideally, this should be the maximum number of allowed transformations for the Levenshtein disctance.
var windowSize = 2;
/*
* We cannot simple split words because we're also looking for phrases.
* If we were just looking for exact matches, we could use something smarter
* such as the KMP algorithm. Because we're dealing with fuzzy matches, using
* this intuitive exhaustive approach might be the best way to go.
*/
var results = [];
// Without looking at the surroundings we'd miss words that have additional or missing chars
for (var size = -windowSize; size <= windowSize; size++) {
for (var pos = 0; pos < haystack.length; pos++) {
var straw = haystack.substr(pos, needle.length + size);
if (H5P.TextUtilities.areSimilar(needle, straw) && H5P.TextUtilities.isIsolated(straw, haystack, {'index': pos})) {
// This will only add the match if it's not a duplicate that we found already in the proximity of pos
if (!this.contains(results, pos)) {
results.push({'keyword': needle, 'match': straw, 'index': pos});
}
}
}
}
return results;
};
/**
* Get all the matches found to a regular expression alternative.
* @param {string[]} alternatives - Alternatives.
* @param {string} inputTest - Original text by student.
* @return {string[]} Matches by regular expressions.
*/
Essay.prototype.getRegExpAlternatives = function (alternatives, inputTest) {
return alternatives
.filter(function (alternative) {
return (alternative[0] === '/' && alternative[alternative.length - 1] === '/');
})
.map(function (alternative) {
var regNeedle = new RegExp(alternative.slice(1, -1), 'g');
return inputTest.match(regNeedle);
})
.reduce(function (a, b) {
return a.concat(b);
}, [])
.filter(function (item) {
return item !== null;
});
};
/**
* Merge the matches.
* @param {...Object[]} matches - Detected matches.
* @return {Object[]} Merged matches.
*/
Essay.prototype.mergeMatches = function () {
// Sanitization
if (arguments.length === 0) {
return [];
}
if (arguments.length === 1) {
return arguments[0];
}
// Add all elements from args[1+] to args[0] if not already there close by.
var results = (arguments[0] || []).slice();
for (var i = 1; i < arguments.length; i++) {
var match2 = arguments[i] || [];
for (var j = 0; j < match2.length; j++) {
if (!this.contains(results, match2[j].index)) {
results.push(match2[j]);
}
}
}
return results.sort(function (a, b) {
return a.index > b.index;
});
};
/**
* Check if an array of detected results contains the same match in the word's proximity.
* Used to prevent double entries that can be caused by fuzzy matching.
* @param {Object} results - Preliminary results.
* @param {string} results.match - Match that was found before at a particular position.
* @param {number} results.index - Starting position of the match.
* @param {number} index - Index of solution to be checked for double entry.
*/
Essay.prototype.contains = function (results, index) {
return results.some(function (result) {
return Math.abs(result.index - index) <= result.match.length;
});
};
/**
* Extend an array just like JQuery's extend.
* @param {...Object} arguments - Objects to be merged.
* @return {Object} Merged objects.
*/
Essay.prototype.extend = function () {
for (var i = 1; i < arguments.length; i++) {
for (var key in arguments[i]) {
if (arguments[i].hasOwnProperty(key)) {
if (typeof arguments[0][key] === 'object' &&
typeof arguments[i][key] === 'object') {
this.extend(arguments[0][key], arguments[i][key]);
}
else {
arguments[0][key] = arguments[i][key];
}
}
}
}
return arguments[0];
};
/**
* Retrieve true string from HTML encoded string
* @param {string} input - Input string.
* @return {string} Output string.
*/
Essay.prototype.htmlDecode = function (input) {
var dparser = new DOMParser().parseFromString(input, 'text/html');
return dparser.documentElement.textContent;
};
/**
* Get current state for H5P.Question.
* @return {Object} Current state.
*/
Essay.prototype.getCurrentState = function () {
this.inputField.updateMessageSaved(this.params.messageSave);
// We could have just used a string, but you never know when you need to store more parameters
return {
'inputField': this.inputField.getText()
};
};
/** @constant {string}
* latin special chars: \u00C0-\u00D6\u00D8-\u00F6\u00F8-\u00FF
* greek chars: \u0370-\u03FF
* kyrillic chars: \u0400-\u04FF
* hiragana + katakana: \u3040-\u30FF
* common CJK characters: \u4E00-\u62FF\u6300-\u77FF\u7800-\u8CFF\u8D00-\u9FFF
* thai chars: \u0E00-\u0E7F
*/
Essay.CHARS_WILDCARD = '[A-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u00FF\u0370-\u03FF\u0400-\u04FF\u3040-\u309F\u3040-\u30FF\u4E00-\u62FF\u6300-\u77FF\u7800-\u8CFF\u8D00-\u9FFF\u0E00-\u0E7F]';
/** @constant {string}
* Required to be added to xAPI object description for H5P reporting
*/
Essay.FILL_IN_PLACEHOLDER = '__________';
return Essay;
}(H5P.jQuery, H5P.Question);
;
var H5P = H5P || {};
(function (Essay) {
'use strict';
// CSS Classes
var MAIN_CONTAINER = 'h5p-essay-input-field';
var INPUT_LABEL = 'h5p-essay-input-field-label';
var INPUT_FIELD = 'h5p-essay-input-field-textfield';
var WRAPPER_MESSAGE = 'h5p-essay-input-field-message-wrapper';
var CHAR_MESSAGE = 'h5p-essay-input-field-message-char';
var CHAR_MESSAGE_IMPORTANT = 'h5p-essay-input-field-message-char-important';
var SAVE_MESSAGE = 'h5p-essay-input-field-message-save';
var ANIMATION_MESSAGE = 'h5p-essay-input-field-message-save-animation';
var EMPTY_MESSAGE = ' ';
/**
* @constructor
* @param {Object} params - Parameters.
* @param {number} [params.inputFieldSize] - Number of rows for inputfield.
* @param {number} [params.maximumLength] - Maximum text length.
* @param {string} [params.placeholderText] - Placeholder text for input field.
* @param {string} [params.remainingChars] - Label for remaining chars information.
* @param {string} [params.taskDescription] - Task description (HTML).
* @param {Object} previousState - Content state of previous attempt.
*/
Essay.InputField = function (params, previousState) {
var that = this;
this.params = params;
this.previousState = previousState;
// Sanitization
this.params.taskDescription = this.params.taskDescription || '';
this.params.placeholderText = this.params.placeholderText || '';
// Task description
this.taskDescription = document.createElement('div');
this.taskDescription.classList.add(INPUT_LABEL);
this.taskDescription.innerHTML = this.params.taskDescription;
// InputField
this.inputField = document.createElement('textarea');
this.inputField.classList.add(INPUT_FIELD);
this.inputField.setAttribute('rows', this.params.inputFieldSize);
this.inputField.setAttribute('maxlength', this.params.maximumLength);
this.inputField.setAttribute('placeholder', this.params.placeholderText);
this.setText(previousState);
this.content = document.createElement('div');
this.content.appendChild(this.inputField);
// Container
this.container = document.createElement('div');
this.container.classList.add(MAIN_CONTAINER);
this.container.appendChild(this.taskDescription);
this.container.appendChild(this.content);
var statusWrapper = document.createElement('div');
statusWrapper.classList.add(WRAPPER_MESSAGE);
this.statusChars = document.createElement('div');
this.statusChars.classList.add(CHAR_MESSAGE);
statusWrapper.appendChild(this.statusChars);
['change', 'keyup', 'paste'].forEach(function (event) {
that.inputField.addEventListener(event, function () {
that.updateMessageSaved('');
that.updateMessageChars();
});
});
this.statusSaved = document.createElement('div');
this.statusSaved.classList.add(SAVE_MESSAGE);
statusWrapper.appendChild(this.statusSaved);
this.content.appendChild(statusWrapper);
this.updateMessageChars();
};
/**
* Get introduction for H5P.Question.
* @return {Object} DOM elements for introduction.
*/
Essay.InputField.prototype.getIntroduction = function () {
return this.taskDescription;
};
/**
* Get content for H5P.Question.
* @return {Object} DOM elements for content.
*/
Essay.InputField.prototype.getContent = function () {
return this.content;
};
/**
* Get current text in InputField.
* @return {string} Current text.
*/
Essay.InputField.prototype.getText = function () {
return this.inputField.value;
};
/**
* Disable the inputField.
*/
Essay.InputField.prototype.disable = function () {
this.inputField.disabled = true;
};
/**
* Enable the inputField.
*/
Essay.InputField.prototype.enable = function () {
this.inputField.disabled = false;
};
/**
* Enable the inputField.
*/
Essay.InputField.prototype.focus = function () {
this.inputField.focus();
};
/**
* Set the text for the InputField.
* @param {string|Object} previousState - Previous state that was saved.
*/
Essay.InputField.prototype.setText = function (previousState) {
if (typeof previousState === 'undefined') {
return;
}
if (typeof previousState === 'string') {
this.inputField.innerHTML = previousState;
}
if (typeof previousState === 'object' && !Array.isArray(previousState)) {
this.inputField.innerHTML = previousState.inputField || '';
}
};
/**
* Compute the remaining number of characters.
* @return {number} Number of characters left.
*/
Essay.InputField.prototype.computeRemainingChars = function () {
return this.params.maximumLength - this.inputField.value.length;
};
/**
* Update character message field.
*/
Essay.InputField.prototype.updateMessageChars = function () {
if (typeof this.params.maximumLength !== 'undefined') {
this.setMessageChars(this.params.remainingChars.replace(/@chars/g, this.computeRemainingChars()), false);
}
else {
// Use EMPTY_MESSAGE to keep height
this.setMessageChars(EMPTY_MESSAGE, false);
}
};
/**
* Update the indicator message for saved text.
* @param {string} saved - Message to indicate the text was saved.
*/
Essay.InputField.prototype.updateMessageSaved = function (saved) {
// Add/remove blending effect
if (typeof saved === 'undefined' || saved === '') {
this.statusSaved.classList.remove(ANIMATION_MESSAGE);
//this.statusSaved.removeAttribute('tabindex');
}
else {
this.statusSaved.classList.add(ANIMATION_MESSAGE);
//this.statusSaved.setAttribute('tabindex', 0);
}
this.statusSaved.innerHTML = saved;
};
/**
* Set the text for the character message.
* @param {string} message - Message text.
* @param {boolean} important - If true, message will added a particular CSS class.
*/
Essay.InputField.prototype.setMessageChars = function (message, important) {
if (typeof message !== 'string') {
return;
}
if (message === EMPTY_MESSAGE || important) {
/*
* Important messages should be read for a readspeaker by caller and need
* not be accessible when tabbing back again.
*/
this.statusChars.removeAttribute('tabindex');
}
else {
this.statusChars.setAttribute('tabindex', 0);
}
this.statusChars.innerHTML = message;
if (important) {
this.statusChars.classList.add(CHAR_MESSAGE_IMPORTANT);
}
else {
this.statusChars.classList.remove(CHAR_MESSAGE_IMPORTANT);
}
};
})(H5P.Essay);
;
/*
* flowplayer.js 3.2.12. The Flowplayer API
*
* Copyright 2009-2011 Flowplayer Oy
*
* This file is part of Flowplayer.
*
* Flowplayer is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Flowplayer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Flowplayer. If not, see .
*
* Date: ${date}
* Revision: ${revision}
*/
!function(){function h(p){console.log("$f.fireEvent",[].slice.call(p))}function l(r){if(!r||typeof r!="object"){return r}var p=new r.constructor();for(var q in r){if(r.hasOwnProperty(q)){p[q]=l(r[q])}}return p}function n(u,r){if(!u){return}var p,q=0,s=u.length;if(s===undefined){for(p in u){if(r.call(u[p],p,u[p])===false){break}}}else{for(var t=u[0];q1){var u=arguments[1],r=(arguments.length==3)?arguments[2]:{};if(typeof u=="string"){u={src:u}}u=j({bgcolor:"#000000",version:[10,1],expressInstall:"http://releases.flowplayer.org/swf/expressinstall.swf",cachebusting:false},u);if(typeof p=="string"){if(p.indexOf(".")!=-1){var t=[];n(o(p),function(){t.push(new b(this,l(u),l(r)))});return new d(t)}else{var s=c(p);return new b(s!==null?s:l(p),l(u),l(r))}}else{if(p){return new b(p,l(u),l(r))}}}return null};j(window.$f,{fireEvent:function(){var q=[].slice.call(arguments);var r=$f(q[0]);return r?r._fireEvent(q.slice(1)):null},addPlugin:function(p,q){b.prototype[p]=q;return $f},each:n,extend:j});if(typeof jQuery=="function"){jQuery.fn.flowplayer=function(r,q){if(!arguments.length||typeof arguments[0]=="number"){var p=[];this.each(function(){var s=$f(this);if(s){p.push(s)}});return arguments.length?p[arguments[0]]:new d(p)}return this.each(function(){$f(this,l(r),q?l(q):{})})}}}();!function(){var h=document.all,j="http://get.adobe.com/flashplayer",c=typeof jQuery=="function",e=/(\d+)[^\d]+(\d+)[^\d]*(\d*)/,b={width:"100%",height:"100%",id:"_"+(""+Math.random()).slice(9),allowfullscreen:true,allowscriptaccess:"always",quality:"high",version:[3,0],onFail:null,expressInstall:null,w3c:false,cachebusting:false};if(window.attachEvent){window.attachEvent("onbeforeunload",function(){__flash_unloadHandler=function(){};__flash_savedUnloadHandler=function(){}})}function i(m,l){if(l){for(var f in l){if(l.hasOwnProperty(f)){m[f]=l[f]}}}return m}function a(f,n){var m=[];for(var l in f){if(f.hasOwnProperty(l)){m[l]=n(f[l])}}return m}window.flashembed=function(f,m,l){if(typeof f=="string"){f=document.getElementById(f.replace("#",""))}if(!f){return}if(typeof m=="string"){m={src:m}}return new d(f,i(i({},b),m),l)};var g=i(window.flashembed,{conf:b,getVersion:function(){var m,f;try{f=navigator.plugins["Shockwave Flash"].description.slice(16)}catch(o){try{m=new ActiveXObject("ShockwaveFlash.ShockwaveFlash.7");f=m&&m.GetVariable("$version")}catch(n){try{m=new ActiveXObject("ShockwaveFlash.ShockwaveFlash.6");f=m&&m.GetVariable("$version")}catch(l){}}}f=e.exec(f);return f?[1*f[1],1*f[(f[1]*1>9?2:3)]*1]:[0,0]},asString:function(l){if(l===null||l===undefined){return null}var f=typeof l;if(f=="object"&&l.push){f="array"}switch(f){case"string":l=l.replace(new RegExp('(["\\\\])',"g"),"\\$1");l=l.replace(/^\s?(\d+\.?\d*)%/,"$1pct");return'"'+l+'"';case"array":return"["+a(l,function(o){return g.asString(o)}).join(",")+"]";case"function":return'"function()"';case"object":var m=[];for(var n in l){if(l.hasOwnProperty(n)){m.push('"'+n+'":'+g.asString(l[n]))}}return"{"+m.join(",")+"}"}return String(l).replace(/\s/g," ").replace(/\'/g,'"')},getHTML:function(o,l){o=i({},o);var n='";if(o.w3c||h){n+=' '}o.width=o.height=o.id=o.w3c=o.src=null;o.onFail=o.version=o.expressInstall=null;for(var m in o){if(o[m]){n+=' '}}var p="";if(l){for(var f in l){if(l[f]){var q=l[f];p+=f+"="+(/function|object/.test(typeof q)?g.asString(q):q)+"&"}}p=p.slice(0,-1);n+=' "}n+=" ";return n},isSupported:function(f){return k[0]>f[0]||k[0]==f[0]&&k[1]>=f[1]}});var k=g.getVersion();function d(f,n,m){if(g.isSupported(n.version)){f.innerHTML=g.getHTML(n,m)}else{if(n.expressInstall&&g.isSupported([6,65])){f.innerHTML=g.getHTML(i(n,{src:n.expressInstall}),{MMredirectURL:encodeURIComponent(location.href),MMplayerType:"PlugIn",MMdoctitle:document.title})}else{if(!f.innerHTML.replace(/\s/g,"")){f.innerHTML="Flash version "+n.version+" or greater is required "+(k[0]>0?"Your version is "+k:"You have no flash plugin installed")+" "+(f.tagName=="A"?" Click here to download latest version
":"Download latest version from here
");if(f.tagName=="A"||f.tagName=="DIV"){f.onclick=function(){location.href=j}}}if(n.onFail){var l=n.onFail.call(this);if(typeof l=="string"){f.innerHTML=l}}}}if(h){window[n.id]=document.getElementById(n.id)}i(this,{getRoot:function(){return f},getOptions:function(){return n},getConf:function(){return m},getApi:function(){return f.firstChild}})}if(c){jQuery.tools=jQuery.tools||{version:"3.2.12"};jQuery.tools.flashembed={conf:b};jQuery.fn.flashembed=function(l,f){return this.each(function(){$(this).data("flashembed",flashembed(this,l,f))})}}}();;
/** @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 $wrapper = $('
');
var $placeholder = $('
', {
id: id,
text: l10n.loading
}).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,
autoplay: options.autoplay ? 1 : 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: options.startAt,
playsinline: 1
},
events: {
onReady: function () {
self.trigger('ready');
self.trigger('loaded');
},
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) {
// 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);
}
}
});
};
/**
* 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
*/
self.pressToPlay = navigator.userAgent.match(/iPad/i) ? true : false;
/**
* 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 () {
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);
};
/**
* 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();
};
/**
* 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: '100%'
});
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;
var player;
var playbackRate = 1;
var id = 'h5p-panopto-' + numInstances;
numInstances++;
var $wrapper = $('
');
var $placeholder = $('
', {
id: id,
html: '' + l10n.loading + '
'
}).appendTo($wrapper);
/**
* 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: !!options.autoplay,
showbrand: false,
start: 0,
hideoverlay: !options.controls,
},
events: {
onIframeReady: function () {
$placeholder.children(0).text('');
player.loadVideo();
},
onReady: function () {
self.trigger('loaded');
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);
}
self.pause();
},
onStateChange: function (state) {
// TODO: Playback rate fix for IE11?
if (state > -1 && state < 4) {
self.trigger('stateChange', state);
}
},
onPlaybackRateChange: function () {
self.trigger('playbackRateChange', self.getPlaybackRate());
},
onError: function () {
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) {
return;
}
player.playVideo();
};
/**
* 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) {
return;
}
player.seekTo(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();
};
/**
* Check if video sound is turned on or off.
*
* @public
* @returns {Boolean}
*/
self.isMuted = function () {
if (!player || !player.isMuted) {
return;
}
return player.isMuted();
};
/**
* 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);
};
/**
* 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) {
console.log('Disable captions');
player.disableCaptions();
currentTrack = null;
}
else {
console.log('Set captions', track.value);
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;
}
// Use as much space as possible
$wrapper.css({
width: '100%',
height: '100%'
});
var width = $wrapper[0].clientWidth;
var height = options.fit ? $wrapper[0].clientHeight : (width * (9/16));
// Set size
$wrapper.css({
width: width + 'px',
height: height + 'px'
});
const $iframe = $placeholder.children('iframe');
if ($iframe.length) {
$iframe.attr('width', width);
$iframe.attr('height', height);
}
});
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 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);
video.autoplay = (options.autoplay ? true : 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 'stateChange':
if (lastState === arg) {
return; // Avoid firing event twice.
}
var validStartTime = options.startAt && options.startAt > 0;
if (arg === H5P.Video.PLAYING && validStartTime) {
video.currentTime = options.startAt;
delete options.startAt;
}
break;
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;
}
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();
}
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) {
if (lastState === undefined) {
// Make sure we always play before we seek to get an image.
// If not iOS devices will reset currentTime when pressing play.
video.play();
video.pause();
}
video.currentTime = time;
};
/**
* 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 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.VideoFlash = (function ($) {
/**
* Flash video player for H5P.
*
* @class
* @param {Array} sources Video files to use
* @param {Object} options Settings for the player
*/
function Flash(sources, options) {
var self = this;
// Player wrapper
var $wrapper = $('
', {
'class': 'h5p-video-flash',
css: {
width: '100%',
height: '100%'
}
});
/**
* 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;
// Sort sources into qualities
//var qualities = getQualities(sources);
var currentQuality;
// Create player options
var playerOptions = {
buffering: true,
clip: {
url: sources[0].path, // getPreferredQuality(),
autoPlay: options.autoplay,
autoBuffering: true,
scaling: 'fit',
onSeek: function () {
if (stateBeforeChangingQuality) {
// ????
}
},
onMetaData: function () {
setTimeout(function () {
if (stateBeforeChangingQuality !== undefined) {
fp.seek(currentTimeBeforeChangingQuality);
if (stateBeforeChangingQuality === H5P.Video.PLAYING) {
// Resume play
fp.play();
}
// Done changing quality
stateBeforeChangingQuality = undefined;
// Remove any errors
if ($error.is(':visible')) {
$error.remove();
}
}
else {
self.trigger('ready');
self.trigger('loaded');
}
}, 0); // Run on next tick
},
onBegin: function () {
self.trigger('stateChange', H5P.Video.PLAYING);
},
onResume: function () {
self.trigger('stateChange', H5P.Video.PLAYING);
},
onPause: function () {
self.trigger('stateChange', H5P.Video.PAUSED);
},
onFinish: function () {
self.trigger('stateChange', H5P.Video.ENDED);
},
onError: function (code, message) {
console.log('ERROR', code, message); // TODO
self.trigger('error', message);
}
},
plugins: {
controls: null
},
play: null, // Disable overlay controls
onPlaylistReplace: function () {
that.playlistReplaced();
}
};
if (options.controls) {
playerOptions.plugins.controls = {};
delete playerOptions.play;
}
var fp = flowplayer($wrapper[0], {
src: "http://releases.flowplayer.org/swf/flowplayer-3.2.16.swf",
wmode: "opaque"
}, playerOptions);
/**
* Appends the video player to the DOM.
*
* @public
* @param {jQuery} $container
*/
self.appendTo = function ($container) {
$wrapper.appendTo($container);
};
/**
* Get list of available qualities. Not available until after play.
*
* @public
* @returns {Array}
*/
self.getQualities = function () {
return;
};
/**
* 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;
}
// Keep track of current quality
currentQuality = quality;
self.trigger('qualityChange', currentQuality);
// Display throbber
self.trigger('stateChange', H5P.Video.BUFFERING);
// Change source
fp.setClip(qualities[quality].source.path);
fp.startBuffering();
};
/**
* Starts the video.
*
* @public
*/
self.play = function () {
if ($error.is(':visible')) {
return;
}
fp.play();
};
/**
* Pauses the video.
*
* @public
*/
self.pause = function () {
fp.pause();
};
/**
* Seek video to given time.
*
* @public
* @param {Number} time
*/
self.seek = function (time) {
fp.seek(time);
};
/**
* Get elapsed time since video beginning.
*
* @public
* @returns {Number}
*/
self.getCurrentTime = function () {
return fp.getTime();
};
/**
* Get total video duration time.
*
* @public
* @returns {Number}
*/
self.getDuration = function () {
return fp.getClip().metaData.duration;
};
/**
* Get percentage of video that is buffered.
*
* @public
* @returns {Number} Between 0 and 100
*/
self.getBuffered = function () {
return fp.getClip().buffer;
};
/**
* Turn off video sound.
*
* @public
*/
self.mute = function () {
fp.mute();
};
/**
* Turn on video sound.
*
* @public
*/
self.unMute = function () {
fp.unmute();
};
/**
* Check if video sound is turned on or off.
*
* @public
* @returns {Boolean}
*/
self.isMuted = function () {
return fp.muted;
};
/**
* Returns the video sound level.
*
* @public
* @returns {Number} Between 0 and 100.
*/
self.getVolume = function () {
return fp.volumeLevel * 100;
};
/**
* Set video sound level.
*
* @public
* @param {Number} volume Between 0 and 100.
*/
self.setVolume = function (level) {
fp.volume(level / 100);
};
// Handle resize events
self.on('resize', function () {
var $object = H5P.jQuery(fp.getParent()).children('object');
var clip = fp.getClip();
if (clip !== undefined) {
$object.css('height', $object.width() * (clip.metaData.height / clip.metaData.width));
}
});
}
/**
* Check to see if we can play any of the given sources.
*
* @public
* @static
* @param {Array} sources
* @returns {Boolean}
*/
Flash.canPlay = function (sources) {
// Cycle through sources
for (var i = 0; i < sources.length; i++) {
if (sources[i].mime === 'video/mp4' || /\.mp4$/.test(sources[i].mime)) {
return true; // We only play mp4
}
}
};
return Flash;
})(H5P.jQuery);
// Register video handler
H5P.videoHandlers = H5P.videoHandlers || [];
H5P.videoHandlers.push(H5P.VideoFlash);
;
/** @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
*/
function Video(parameters, id) {
var self = this;
self.contentId = id;
// 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.',
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, parameters.visuals, {
disableFullscreen: false
});
/** @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);
}
});
/**
* 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);
}
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;
};
// Resize the video when we know its aspect ratio
self.on('loaded', function () {
self.trigger('resize');
});
// 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,
startAt: parameters.startAt || 0,
tracks: tracks,
disableRemotePlayback: parameters.visuals.disableRemotePlayback === true,
disableFullscreen: parameters.visuals.disableFullscreen === true
}
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;
};
/** @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 || []);
;
H5P = H5P || {};
/**
* Will render a Question with multiple choices for answers.
*
* Events provided:
* - h5pQuestionSetFinished: Triggered when a question is finished. (User presses Finish-button)
*
* @param {Array} options
* @param {int} contentId
* @param {Object} contentData
* @returns {H5P.QuestionSet} Instance
*/
H5P.QuestionSet = function (options, contentId, contentData) {
if (!(this instanceof H5P.QuestionSet)) {
return new H5P.QuestionSet(options, contentId, contentData);
}
H5P.EventDispatcher.call(this);
var $ = H5P.jQuery;
var self = this;
this.contentId = contentId;
var defaults = {
initialQuestion: 0,
progressType: 'dots',
passPercentage: 50,
questions: [],
introPage: {
showIntroPage: false,
title: '',
introduction: '',
startButtonText: 'Start'
},
texts: {
prevButton: 'Previous question',
nextButton: 'Next question',
finishButton: 'Finish',
textualProgress: 'Question: @current of @total questions',
jumpToQuestion: 'Question %d of %total',
questionLabel: 'Question',
readSpeakerProgress: 'Question @current of @total',
unansweredText: 'Unanswered',
answeredText: 'Answered',
currentQuestionText: 'Current question'
},
endGame: {
showResultPage: true,
noResultMessage: 'Finished',
message: 'Your result:',
oldFeedback: {
successGreeting: '',
successComment: '',
failGreeting: '',
failComment: ''
},
overallFeedback: [],
finishButtonText: 'Finish',
solutionButtonText: 'Show solution',
retryButtonText: 'Retry',
showAnimations: false,
skipButtonText: 'Skip video',
showSolutionButton: true,
showRetryButton: true
},
override: {},
disableBackwardsNavigation: false
};
var params = $.extend(true, {}, defaults, options);
var texttemplate =
'<% if (introPage.showIntroPage) { %>' +
'' +
' <% if (introPage.title) { %>' +
'
<%= introPage.title %>
' +
' <% } %>' +
' <% if (introPage.introduction) { %>' +
'
<%= introPage.introduction %>
' +
' <% } %>' +
'
' +
'
' +
'<% } %>' +
'
' +
'' +
' <% for (var i=0; i
' +
'
' +
' <% } %>' +
' ' +
' ' +
' ';
var solutionButtonTemplate = params.endGame.showSolutionButton ?
'
<%= solutionButtonText %> ':
'';
const retryButtonTemplate = params.endGame.showRetryButton ?
'
<%= retryButtonText %> ':
'';
var resulttemplate =
'
' +
'
<%= message %>
' +
'
' +
' <% if (comment) { %>' +
' ' +
' <% } %>' +
' <% if (resulttext) { %>' +
'
<%= resulttext %>
' +
' <% } %>' +
'
' +
solutionButtonTemplate +
retryButtonTemplate +
'
' +
'
';
var template = new EJS({text: texttemplate});
var endTemplate = new EJS({text: resulttemplate});
var initialParams = $.extend(true, {}, defaults, options);
var poolOrder; // Order of questions in a pool
var currentQuestion = 0;
var questionInstances = [];
var questionOrder; //Stores order of questions to allow resuming of question set
var $myDom;
var scoreBar;
var up;
var renderSolutions = false;
var showingSolutions = false;
contentData = contentData || {};
// Bring question set up to date when resuming
if (contentData.previousState) {
if (contentData.previousState.progress) {
currentQuestion = contentData.previousState.progress;
}
questionOrder = contentData.previousState.order;
}
/**
* Randomizes questions in an array and updates an array containing their order
* @param {array} questions
* @return {Object.
} questionOrdering
*/
var randomizeQuestionOrdering = function (questions) {
// Save the original order of the questions in a multidimensional array [[question0,0],[question1,1]...
var questionOrdering = questions.map(function (questionInstance, index) {
return [questionInstance, index];
});
// Shuffle the multidimensional array
questionOrdering = H5P.shuffleArray(questionOrdering);
// Retrieve question objects from the first index
questions = [];
for (var i = 0; i < questionOrdering.length; i++) {
questions[i] = questionOrdering[i][0];
}
// Retrieve the new shuffled order from the second index
var newOrder = [];
for (var j = 0; j < questionOrdering.length; j++) {
// Use a previous order if it exists
if (contentData.previousState && contentData.previousState.questionOrder) {
newOrder[j] = questionOrder[questionOrdering[j][1]];
}
else {
newOrder[j] = questionOrdering[j][1];
}
}
// Return the questions in their new order *with* their new indexes
return {
questions: questions,
questionOrder: newOrder
};
};
// Create a pool (a subset) of questions if necessary
if (params.poolSize > 0) {
// If a previous pool exists, recreate it
if (contentData.previousState && contentData.previousState.poolOrder) {
poolOrder = contentData.previousState.poolOrder;
// Recreate the pool from the saved data
var pool = [];
for (var i = 0; i < poolOrder.length; i++) {
pool[i] = params.questions[poolOrder[i]];
}
// Replace original questions with just the ones in the pool
params.questions = pool;
}
else { // Otherwise create a new pool
// Randomize and get the results
var poolResult = randomizeQuestionOrdering(params.questions);
var poolQuestions = poolResult.questions;
poolOrder = poolResult.questionOrder;
// Discard extra questions
poolQuestions = poolQuestions.slice(0, params.poolSize);
poolOrder = poolOrder.slice(0, params.poolSize);
// Replace original questions with just the ones in the pool
params.questions = poolQuestions;
}
}
// Create the html template for the question container
var $template = $(template.render(params));
// Set overrides for questions
var override;
if (params.override.showSolutionButton || params.override.retryButton || params.override.checkButton === false) {
override = {};
if (params.override.showSolutionButton) {
// Force "Show solution" button to be on or off for all interactions
override.enableSolutionsButton =
(params.override.showSolutionButton === 'on' ? true : false);
}
if (params.override.retryButton) {
// Force "Retry" button to be on or off for all interactions
override.enableRetry =
(params.override.retryButton === 'on' ? true : false);
}
if (params.override.checkButton === false) {
// Force "Check" button to be on or off for all interactions
override.enableCheckButton = params.override.checkButton;
}
}
/**
* Generates question instances from H5P objects
*
* @param {object} questions H5P content types to be created as instances
* @return {array} Array of questions instances
*/
var createQuestionInstancesFromQuestions = function (questions) {
var result = [];
// Create question instances from questions
// Instantiate question instances
for (var i = 0; i < questions.length; i++) {
var question;
// If a previous order exists, use it
if (questionOrder !== undefined) {
question = questions[questionOrder[i]];
}
else {
// Use a generic order when initialzing for the first time
question = questions[i];
}
if (override) {
// Extend subcontent with the overrided settings.
$.extend(question.params.behaviour, override);
}
question.params = question.params || {};
var hasAnswers = contentData.previousState && contentData.previousState.answers;
var questionInstance = H5P.newRunnable(question, contentId, undefined, undefined,
{
previousState: hasAnswers ? contentData.previousState.answers[i] : undefined,
parent: self
});
questionInstance.on('resize', function () {
up = true;
self.trigger('resize');
});
result.push(questionInstance);
}
return result;
};
// Create question instances from questions given by params
questionInstances = createQuestionInstancesFromQuestions(params.questions);
// Randomize questions only on instantiation
if (params.randomQuestions && contentData.previousState === undefined) {
var result = randomizeQuestionOrdering(questionInstances);
questionInstances = result.questions;
questionOrder = result.questionOrder;
}
// Resize all interactions on resize
self.on('resize', function () {
if (up) {
// Prevent resizing the question again.
up = false;
return;
}
for (var i = 0; i < questionInstances.length; i++) {
questionInstances[i].trigger('resize');
}
});
// Update button state.
var _updateButtons = function () {
// Verify that current question is answered when backward nav is disabled
if (params.disableBackwardsNavigation) {
if (questionInstances[currentQuestion].getAnswerGiven() &&
questionInstances.length-1 !== currentQuestion) {
questionInstances[currentQuestion].showButton('next');
}
else {
questionInstances[currentQuestion].hideButton('next');
}
}
var answered = true;
for (var i = questionInstances.length - 1; i >= 0; i--) {
answered = answered && (questionInstances[i]).getAnswerGiven();
}
if (currentQuestion === (params.questions.length - 1) &&
questionInstances[currentQuestion]) {
if (answered) {
questionInstances[currentQuestion].showButton('finish');
}
else {
questionInstances[currentQuestion].hideButton('finish');
}
}
};
var _stopQuestion = function (questionNumber) {
if (questionInstances[questionNumber]) {
pauseMedia(questionInstances[questionNumber]);
}
};
var _showQuestion = function (questionNumber, preventAnnouncement) {
// Sanitize input.
if (questionNumber < 0) {
questionNumber = 0;
}
if (questionNumber >= params.questions.length) {
questionNumber = params.questions.length - 1;
}
currentQuestion = questionNumber;
handleAutoPlay(currentQuestion);
// Hide all questions
$('.question-container', $myDom).hide().eq(questionNumber).show();
if (questionInstances[questionNumber]) {
// Trigger resize on question in case the size of the QS has changed.
var instance = questionInstances[questionNumber];
instance.setActivityStarted();
if (instance.$ !== undefined) {
instance.trigger('resize');
}
}
// Update progress indicator
// Test if current has been answered.
if (params.progressType === 'textual') {
$('.progress-text', $myDom).text(params.texts.textualProgress.replace("@current", questionNumber+1).replace("@total", params.questions.length));
}
else {
// Set currentNess
var previousQuestion = $('.progress-dot.current', $myDom).parent().index();
if (previousQuestion >= 0) {
toggleCurrentDot(previousQuestion, false);
toggleAnsweredDot(previousQuestion, questionInstances[previousQuestion].getAnswerGiven());
}
toggleCurrentDot(questionNumber, true);
}
if (!preventAnnouncement) {
// Announce question number of total, must use timeout because of buttons logic
setTimeout(function () {
var humanizedProgress = params.texts.readSpeakerProgress
.replace('@current', (currentQuestion + 1).toString())
.replace('@total', questionInstances.length.toString());
$('.qs-progress-announcer', $myDom)
.html(humanizedProgress)
.show().focus();
if (instance && instance.readFeedback) {
instance.readFeedback();
}
}, 0);
}
// Remember where we are
_updateButtons();
self.trigger('resize');
return currentQuestion;
};
/**
* Handle autoplays, limit to one at a time
*
* @param {number} currentQuestionIndex
*/
var handleAutoPlay = function (currentQuestionIndex) {
for (var i = 0; i < questionInstances.length; i++) {
questionInstances[i].pause();
}
var currentQuestion = params.questions[currentQuestionIndex];
var hasAutoPlay = currentQuestion &&
currentQuestion.params.media &&
currentQuestion.params.media.params &&
currentQuestion.params.media.params.playback &&
currentQuestion.params.media.params.playback.autoplay;
if (hasAutoPlay && typeof questionInstances[currentQuestionIndex].play === 'function') {
questionInstances[currentQuestionIndex].play();
}
};
/**
* Show solutions for subcontent, and hide subcontent buttons.
* Used for contracts with integrated content.
* @public
*/
var showSolutions = function () {
showingSolutions = true;
for (var i = 0; i < questionInstances.length; i++) {
// Enable back and forth navigation in solution mode
toggleDotsNavigation(true);
if (i < questionInstances.length - 1) {
questionInstances[i].showButton('next');
}
if (i > 0) {
questionInstances[i].showButton('prev');
}
try {
// Do not read answers
questionInstances[i].toggleReadSpeaker(true);
questionInstances[i].showSolutions();
questionInstances[i].toggleReadSpeaker(false);
}
catch (error) {
H5P.error("subcontent does not contain a valid showSolutions function");
H5P.error(error);
}
}
};
/**
* Toggles whether dots are enabled for navigation
*/
var toggleDotsNavigation = function (enable) {
$('.progress-dot', $myDom).each(function () {
$(this).toggleClass('disabled', !enable);
$(this).attr('aria-disabled', enable ? 'false' : 'true');
// Remove tabindex
if (!enable) {
$(this).attr('tabindex', '-1');
}
});
};
/**
* Resets the task and every subcontent task.
* Used for contracts with integrated content.
* @public
*/
var resetTask = function () {
// Clear previous state to ensure questions are created cleanly
contentData.previousState = [];
showingSolutions = false;
for (var i = 0; i < questionInstances.length; i++) {
try {
questionInstances[i].resetTask();
// Hide back and forth navigation in normal mode
if (params.disableBackwardsNavigation) {
toggleDotsNavigation(false);
// Check if first question is answered by default
if (i === 0 && questionInstances[i].getAnswerGiven()) {
questionInstances[i].showButton('next');
}
else {
questionInstances[i].hideButton('next');
}
questionInstances[i].hideButton('prev');
}
}
catch (error) {
H5P.error("subcontent does not contain a valid resetTask function");
H5P.error(error);
}
}
// Hide finish button
questionInstances[questionInstances.length - 1].hideButton('finish');
// Mark all tasks as unanswered:
$('.progress-dot').each(function (idx) {
toggleAnsweredDot(idx, false);
});
//Force the last page to be reRendered
rendered = false;
if (params.poolSize > 0) {
// Make new pool from params.questions
// Randomize and get the results
var poolResult = randomizeQuestionOrdering(initialParams.questions);
var poolQuestions = poolResult.questions;
poolOrder = poolResult.questionOrder;
// Discard extra questions
poolQuestions = poolQuestions.slice(0, params.poolSize);
poolOrder = poolOrder.slice(0, params.poolSize);
// Replace original questions with just the ones in the pool
params.questions = poolQuestions;
// Recreate the question instances
questionInstances = createQuestionInstancesFromQuestions(params.questions);
// Update buttons
initializeQuestion();
}
else if (params.randomQuestions) {
randomizeQuestions();
}
};
var rendered = false;
this.reRender = function () {
rendered = false;
};
/**
* Randomizes question instances
*/
var randomizeQuestions = function () {
var result = randomizeQuestionOrdering(questionInstances);
questionInstances = result.questions;
questionOrder = result.questionOrder;
replaceQuestionsInDOM(questionInstances);
};
/**
* Empty the DOM of all questions, attach new questions and update buttons
*
* @param {type} questionInstances Array of questions to be attached to the DOM
*/
var replaceQuestionsInDOM = function (questionInstances) {
// Find all question containers and detach questions from them
$('.question-container', $myDom).each(function () {
$(this).children().detach();
});
// Reattach questions and their buttons in the new order
for (var i = 0; i < questionInstances.length; i++) {
var question = questionInstances[i];
// Make sure styles are not being added twice
$('.question-container:eq(' + i + ')', $myDom).attr('class', 'question-container');
question.attach($('.question-container:eq(' + i + ')', $myDom));
//Show buttons if necessary
if (questionInstances[questionInstances.length -1] === question &&
question.hasButton('finish')) {
question.showButton('finish');
}
if (questionInstances[questionInstances.length -1] !== question &&
question.hasButton('next')) {
question.showButton('next');
}
if (questionInstances[0] !== question &&
question.hasButton('prev') &&
!params.disableBackwardsNavigation) {
question.showButton('prev');
}
// Hide relevant buttons since the order has changed
if (questionInstances[0] === question) {
question.hideButton('prev');
}
if (questionInstances[questionInstances.length-1] === question) {
question.hideButton('next');
}
if (questionInstances[questionInstances.length-1] !== question) {
question.hideButton('finish');
}
}
};
var moveQuestion = function (direction) {
if (params.disableBackwardsNavigation && !questionInstances[currentQuestion].getAnswerGiven()) {
questionInstances[currentQuestion].hideButton('next');
questionInstances[currentQuestion].hideButton('finish');
return;
}
_stopQuestion(currentQuestion);
if (currentQuestion + direction >= questionInstances.length) {
_displayEndGame();
}
else {
// Allow movement if backward navigation enabled or answer given
_showQuestion(currentQuestion + direction);
}
};
/**
* Toggle answered state of dot at given index
* @param {number} dotIndex Index of dot
* @param {boolean} isAnswered True if is answered, False if not answered
*/
var toggleAnsweredDot = function (dotIndex, isAnswered) {
var $el = $('.progress-dot:eq(' + dotIndex +')', $myDom);
// Skip current button
if ($el.hasClass('current')) {
return;
}
// Ensure boolean
isAnswered = !!isAnswered;
var label = params.texts.jumpToQuestion
.replace('%d', (dotIndex + 1).toString())
.replace('%total', $('.progress-dot', $myDom).length) +
', ' +
(isAnswered ? params.texts.answeredText : params.texts.unansweredText);
$el.toggleClass('unanswered', !isAnswered)
.toggleClass('answered', isAnswered)
.attr('aria-label', label);
};
/**
* Toggle current state of dot at given index
* @param dotIndex
* @param isCurrent
*/
var toggleCurrentDot = function (dotIndex, isCurrent) {
var $el = $('.progress-dot:eq(' + dotIndex +')', $myDom);
var texts = params.texts;
var label = texts.jumpToQuestion
.replace('%d', (dotIndex + 1).toString())
.replace('%total', $('.progress-dot', $myDom).length);
if (!isCurrent) {
var isAnswered = $el.hasClass('answered');
label += ', ' + (isAnswered ? texts.answeredText : texts.unansweredText);
}
else {
label += ', ' + texts.currentQuestionText;
}
var disabledTabindex = params.disableBackwardsNavigation && !showingSolutions;
$el.toggleClass('current', isCurrent)
.attr('aria-label', label)
.attr('tabindex', isCurrent && !disabledTabindex ? 0 : -1);
};
var _displayEndGame = function () {
$('.progress-dot.current', $myDom).removeClass('current');
if (rendered) {
$myDom.children().hide().filter('.questionset-results').show();
self.trigger('resize');
return;
}
//Remove old score screen.
$myDom.children().hide().filter('.questionset-results').remove();
rendered = true;
// Get total score.
var finals = self.getScore();
var totals = self.getMaxScore();
var scoreString = H5P.Question.determineOverallFeedback(params.endGame.overallFeedback, finals / totals).replace('@score', finals).replace('@total', totals);
var success = ((100 * finals / totals) >= params.passPercentage);
/**
* Makes our buttons behave like other buttons.
*
* @private
* @param {string} classSelector
* @param {function} handler
*/
var hookUpButton = function (classSelector, handler) {
$(classSelector, $myDom).click(handler).keypress(function (e) {
if (e.which === 32) {
handler();
e.preventDefault();
}
});
};
var displayResults = function () {
self.triggerXAPICompleted(self.getScore(), self.getMaxScore(), success);
var eparams = {
message: params.endGame.showResultPage ? params.endGame.message : params.endGame.noResultMessage,
comment: params.endGame.showResultPage ? (success ? params.endGame.oldFeedback.successGreeting : params.endGame.oldFeedback.failGreeting) : undefined,
resulttext: params.endGame.showResultPage ? (success ? params.endGame.oldFeedback.successComment : params.endGame.oldFeedback.failComment) : undefined,
finishButtonText: params.endGame.finishButtonText,
solutionButtonText: params.endGame.solutionButtonText,
retryButtonText: params.endGame.retryButtonText
};
// Show result page.
$myDom.children().hide();
$myDom.append(endTemplate.render(eparams));
if (params.endGame.showResultPage) {
hookUpButton('.qs-solutionbutton', function () {
showSolutions();
$myDom.children().hide().filter('.questionset').show();
_showQuestion(params.initialQuestion);
});
hookUpButton('.qs-retrybutton', function () {
resetTask();
$myDom.children().hide();
var $intro = $('.intro-page', $myDom);
if ($intro.length) {
// Show intro
$('.intro-page', $myDom).show();
$('.qs-startbutton', $myDom).focus();
}
else {
// Show first question
$('.questionset', $myDom).show();
_showQuestion(params.initialQuestion);
}
});
if (scoreBar === undefined) {
scoreBar = H5P.JoubelUI.createScoreBar(totals);
}
scoreBar.appendTo($('.feedback-scorebar', $myDom));
$('.feedback-text', $myDom).html(scoreString);
// Announce that the question set is complete
setTimeout(function () {
$('.qs-progress-announcer', $myDom)
.html(eparams.message + '.' +
scoreString + '.' +
eparams.comment + '.' +
eparams.resulttext)
.show().focus();
scoreBar.setMaxScore(totals);
scoreBar.setScore(finals);
}, 0);
}
else {
// Remove buttons and feedback section
$('.qs-solutionbutton, .qs-retrybutton, .feedback-section', $myDom).remove();
}
self.trigger('resize');
};
if (params.endGame.showAnimations) {
var videoData = success ? params.endGame.successVideo : params.endGame.failVideo;
if (videoData) {
$myDom.children().hide();
var $videoContainer = $('
').appendTo($myDom);
var video = new H5P.Video({
sources: videoData,
fitToWrapper: true,
controls: false,
autoplay: false
}, contentId);
video.on('stateChange', function (event) {
if (event.data === H5P.Video.ENDED) {
displayResults();
$videoContainer.hide();
}
});
video.attach($videoContainer);
// Resize on video loaded
video.on('loaded', function () {
self.trigger('resize');
});
video.play();
if (params.endGame.skippable) {
$('' + params.endGame.skipButtonText + ' ').click(function () {
video.pause();
$videoContainer.hide();
displayResults();
}).appendTo($videoContainer);
}
return;
}
}
// Trigger finished event.
displayResults();
self.trigger('resize');
};
var registerImageLoadedListener = function (question) {
H5P.on(question, 'imageLoaded', function () {
self.trigger('resize');
});
};
/**
* Initialize a question and attach it to the DOM
*
*/
function initializeQuestion() {
// Attach questions
for (var i = 0; i < questionInstances.length; i++) {
var question = questionInstances[i];
// Make sure styles are not being added twice
$('.question-container:eq(' + i + ')', $myDom).attr('class', 'question-container');
question.attach($('.question-container:eq(' + i + ')', $myDom));
// Listen for image resize
registerImageLoadedListener(question);
// Add finish button
question.addButton('finish', params.texts.finishButton,
moveQuestion.bind(this, 1), false);
// Add next button
question.addButton('next', '', moveQuestion.bind(this, 1),
!params.disableBackwardsNavigation || !!question.getAnswerGiven(), {
href: '#', // Use href since this is a navigation button
'aria-label': params.texts.nextButton
});
// Add previous button
question.addButton('prev', '', moveQuestion.bind(this, -1),
!(questionInstances[0] === question || params.disableBackwardsNavigation), {
href: '#', // Use href since this is a navigation button
'aria-label': params.texts.prevButton
});
// Hide next button if it is the last question
if (questionInstances[questionInstances.length -1] === question) {
question.hideButton('next');
}
question.on('xAPI', function (event) {
var shortVerb = event.getVerb();
if (shortVerb === 'interacted' ||
shortVerb === 'answered' ||
shortVerb === 'attempted') {
toggleAnsweredDot(currentQuestion,
questionInstances[currentQuestion].getAnswerGiven());
_updateButtons();
}
if (shortVerb === 'completed') {
// An activity within this activity is not allowed to send completed events
event.setVerb('answered');
}
if (event.data.statement.context.extensions === undefined) {
event.data.statement.context.extensions = {};
}
event.data.statement.context.extensions['http://id.tincanapi.com/extension/ending-point'] = currentQuestion + 1;
});
// Mark question if answered
toggleAnsweredDot(i, question.getAnswerGiven());
}
}
this.attach = function (target) {
if (this.isRoot()) {
this.setActivityStarted();
}
if (typeof(target) === "string") {
$myDom = $('#' + target);
}
else {
$myDom = $(target);
}
// Render own DOM into target.
$myDom.children().remove();
$myDom.append($template);
if (params.backgroundImage !== undefined) {
$myDom.css({
overflow: 'hidden',
background: '#fff url("' + H5P.getPath(params.backgroundImage.path, contentId) + '") no-repeat 50% 50%',
backgroundSize: '100% auto'
});
}
if (params.introPage.backgroundImage !== undefined) {
var $intro = $myDom.find('.intro-page');
if ($intro.length) {
var bgImg = params.introPage.backgroundImage;
var bgImgRatio = (bgImg.height / bgImg.width);
$intro.css({
background: '#fff url("' + H5P.getPath(bgImg.path, contentId) + '") no-repeat 50% 50%',
backgroundSize: 'auto 100%',
minHeight: bgImgRatio * +window.getComputedStyle($intro[0]).width.replace('px','')
});
}
}
initializeQuestion();
// Allow other libraries to add transitions after the questions have been inited
$('.questionset', $myDom).addClass('started');
$('.qs-startbutton', $myDom)
.click(function () {
$(this).parents('.intro-page').hide();
$('.questionset', $myDom).show();
_showQuestion(params.initialQuestion);
event.preventDefault();
})
.keydown(function (event) {
switch (event.which) {
case 13: // Enter
case 32: // Space
$(this).parents('.intro-page').hide();
$('.questionset', $myDom).show();
_showQuestion(params.initialQuestion);
event.preventDefault();
}
});
/**
* Triggers changing the current question.
*
* @private
* @param {Object} [event]
*/
var handleProgressDotClick = function (event) {
// Disable dots when backward nav disabled
event.preventDefault();
if (params.disableBackwardsNavigation && !showingSolutions) {
return;
}
_stopQuestion(currentQuestion);
_showQuestion($(this).parent().index());
};
// Set event listeners.
$('.progress-dot', $myDom).click(handleProgressDotClick).keydown(function (event) {
var $this = $(this);
switch (event.which) {
case 13: // Enter
case 32: // Space
handleProgressDotClick.call(this, event);
break;
case 37: // Left Arrow
case 38: // Up Arrow
// Go to previous dot
var $prev = $this.parent().prev();
if ($prev.length) {
$prev.children('a').attr('tabindex', '0').focus();
$this.attr('tabindex', '-1');
}
break;
case 39: // Right Arrow
case 40: // Down Arrow
// Go to next dot
var $next = $this.parent().next();
if ($next.length) {
$next.children('a').attr('tabindex', '0').focus();
$this.attr('tabindex', '-1');
}
break;
}
});
// Hide all but current question
_showQuestion(currentQuestion, true);
if (renderSolutions) {
showSolutions();
}
// Update buttons in case they have changed (restored user state)
_updateButtons();
this.trigger('resize');
return this;
};
// Get current score for questionset.
this.getScore = function () {
var score = 0;
for (var i = questionInstances.length - 1; i >= 0; i--) {
score += questionInstances[i].getScore();
}
return score;
};
// Get total score possible for questionset.
this.getMaxScore = function () {
var score = 0;
for (var i = questionInstances.length - 1; i >= 0; i--) {
score += questionInstances[i].getMaxScore();
}
return score;
};
/**
* @deprecated since version 1.9.2
* @returns {number}
*/
this.totalScore = function () {
return this.getMaxScore();
};
/**
* Gather copyright information for the current content.
*
* @returns {H5P.ContentCopyrights}
*/
this.getCopyrights = function () {
var info = new H5P.ContentCopyrights();
// IntroPage Background
if (params.introPage !== undefined && params.introPage.backgroundImage !== undefined && params.introPage.backgroundImage.copyright !== undefined) {
var introBackground = new H5P.MediaCopyright(params.introPage.backgroundImage.copyright);
introBackground.setThumbnail(new H5P.Thumbnail(H5P.getPath(params.introPage.backgroundImage.path, contentId), params.introPage.backgroundImage.width, params.introPage.backgroundImage.height));
info.addMedia(introBackground);
}
// Background
if (params.backgroundImage !== undefined && params.backgroundImage.copyright !== undefined) {
var background = new H5P.MediaCopyright(params.backgroundImage.copyright);
background.setThumbnail(new H5P.Thumbnail(H5P.getPath(params.backgroundImage.path, contentId), params.backgroundImage.width, params.backgroundImage.height));
info.addMedia(background);
}
// Questions
var questionCopyrights;
for (var i = 0; i < questionInstances.length; i++) {
var instance = questionInstances[i];
var instanceParams = params.questions[i].params;
questionCopyrights = undefined;
if (instance.getCopyrights !== undefined) {
// Use the instance's own copyright generator
questionCopyrights = instance.getCopyrights();
}
if (questionCopyrights === undefined) {
// Create a generic flat copyright list
questionCopyrights = new H5P.ContentCopyrights();
H5P.findCopyrights(questionCopyrights, instanceParams.params, contentId,{
metadata: instanceParams.metadata,
machineName: instanceParams.library.split(' ')[0]
});
}
// Determine label
var label = (params.texts.questionLabel + ' ' + (i + 1));
if (instanceParams.params.contentName !== undefined) {
label += ': ' + instanceParams.params.contentName;
}
else if (instance.getTitle !== undefined) {
label += ': ' + instance.getTitle();
}
questionCopyrights.setLabel(label);
// Add info
info.addContent(questionCopyrights);
}
// Success video
var video;
if (params.endGame.successVideo !== undefined && params.endGame.successVideo.length > 0) {
video = params.endGame.successVideo[0];
if (video.copyright !== undefined) {
info.addMedia(new H5P.MediaCopyright(video.copyright));
}
}
// Fail video
if (params.endGame.failVideo !== undefined && params.endGame.failVideo.length > 0) {
video = params.endGame.failVideo[0];
if (video.copyright !== undefined) {
info.addMedia(new H5P.MediaCopyright(video.copyright));
}
}
return info;
};
this.getQuestions = function () {
return questionInstances;
};
this.showSolutions = function () {
renderSolutions = true;
};
/**
* Stop the given element's playback if any.
*
* @param {object} instance
*/
var pauseMedia = function (instance) {
try {
if (instance.pause !== undefined &&
(instance.pause instanceof Function ||
typeof instance.pause === 'function')) {
instance.pause();
}
}
catch (err) {
// Prevent crashing, log error.
H5P.error(err);
}
};
/**
* Returns the complete state of question set and sub-content
*
* @returns {Object} current state
*/
this.getCurrentState = function () {
return {
progress: showingSolutions ? questionInstances.length - 1 : currentQuestion,
answers: questionInstances.map(function (qi) {
return qi.getCurrentState();
}),
order: questionOrder,
poolOrder: poolOrder
};
};
/**
* Generate xAPI object definition used in xAPI statements.
* @return {Object}
*/
var getxAPIDefinition = function () {
var definition = {};
definition.interactionType = 'compound';
definition.type = 'http://adlnet.gov/expapi/activities/cmi.interaction';
definition.description = {
'en-US': ''
};
return definition;
};
/**
* Add the question itself to the definition part of an xAPIEvent
*/
var addQuestionToXAPI = function (xAPIEvent) {
var definition = xAPIEvent.getVerifiedStatementValue(['object', 'definition']);
$.extend(definition, getxAPIDefinition());
};
/**
* Get xAPI data from sub content types
*
* @param {Object} metaContentType
* @returns {array}
*/
var getXAPIDataFromChildren = function (metaContentType) {
return metaContentType.getQuestions().map(function (question) {
return question.getXAPIData();
});
};
/**
* Get xAPI data.
* Contract used by report rendering engine.
*
* @see contract at {@link https://h5p.org/documentation/developers/contracts#guides-header-6}
*/
this.getXAPIData = function () {
var xAPIEvent = this.createXAPIEventTemplate('answered');
addQuestionToXAPI(xAPIEvent);
xAPIEvent.setScoredResult(this.getScore(),
this.getMaxScore(),
this,
true,
this.getScore() === this.getMaxScore()
);
return {
statement: xAPIEvent.data.statement,
children: getXAPIDataFromChildren(this)
};
};
/**
* Get context data.
* Contract used for confusion report.
*/
this.getContext = function () {
// Get question index and add 1, count starts from 0
return {
type: 'question',
value: (currentQuestion + 1)
};
};
};
H5P.QuestionSet.prototype = Object.create(H5P.EventDispatcher.prototype);
H5P.QuestionSet.prototype.constructor = H5P.QuestionSet;
;