", {
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;
;
(()=>{"use strict";const e=function(e){const t=e.length;return function n(){const o=Array.prototype.slice.call(arguments,0);return o.length>=t?e.apply(null,o):function(){const e=Array.prototype.slice.call(arguments,0);return n.apply(null,o.concat(e))}}},t=(...e)=>e.reduce(((e,t)=>(...n)=>e(t(...n)))),n=e((function(e,t){t.forEach(e)})),o=(e((function(e,t){return t.map(e)})),e((function(e,t){return t.filter(e)}))),i=e((function(e,t){return t.some(e)})),r=e((function(e,t){return-1!=t.indexOf(e)})),s=e((function(e,t){return o((t=>!r(t,e)),t)})),a=e(((e,t)=>t.getAttribute(e))),l=e(((e,t,n)=>n.setAttribute(e,t))),c=e(((e,t)=>t.removeAttribute(e))),d=e(((e,t)=>t.hasAttribute(e))),h=e(((e,t,n)=>n.getAttribute(e)===t)),p=(e(((e,t)=>{const n=a(e,t);l(e,("true"!==n).toString(),t)})),e(((e,t)=>e.appendChild(t))),e(((e,t)=>t.querySelector(e))),e(((e,t)=>{return n=t.querySelectorAll(e),Array.prototype.slice.call(n);var n})),e(((e,t)=>e.removeChild(t))),e(((e,t)=>t.classList.contains(e))),e(((e,t)=>t.classList.add(e)))),u=e(((e,t)=>t.classList.remove(e))),g=p("hidden"),f=u("hidden"),b=(e(((e,t)=>(e?f:g)(t))),e(((e,t,n)=>{n.classList[t?"add":"remove"](e)})),c("tabindex")),v=(n(b),l("tabindex","0")),m=l("tabindex","-1"),y=d("tabindex");class w{constructor(e){Object.assign(this,{listeners:{},on:function(e,t,n){const o={listener:t,scope:n};return this.listeners[e]=this.listeners[e]||[],this.listeners[e].push(o),this},fire:function(e,t){return(this.listeners[e]||[]).every((function(e){return!1!==e.listener.call(e.scope||this,t)}))},propagate:function(e,t){let n=this;e.forEach((e=>t.on(e,(t=>n.fire(e,t)))))}}),this.plugins=e||[],this.elements=[],this.negativeTabIndexAllowed=!1,this.on("nextElement",this.nextElement,this),this.on("previousElement",this.previousElement,this),this.on("firstElement",this.firstElement,this),this.on("lastElement",this.lastElement,this),this.initPlugins()}addElement(e){this.elements.push(e),this.firesEvent("addElement",e),1===this.elements.length&&this.setTabbable(e)}insertElementAt(e,t){this.elements.splice(t,0,e),this.firesEvent("addElement",e),1===this.elements.length&&this.setTabbable(e)}removeElement(e){this.elements=s([e],this.elements),y(e)&&(this.setUntabbable(e),this.elements[0]&&this.setTabbable(this.elements[0])),this.firesEvent("removeElement",e)}count(){return this.elements.length}firesEvent(e,t){const n=this.elements.indexOf(t);return this.fire(e,{element:t,index:n,elements:this.elements,oldElement:this.tabbableElement})}nextElement({index:e}){const t=e===this.elements.length-1,n=this.elements[t?0:e+1];this.setTabbable(n),n.focus()}firstElement(){const e=this.elements[0];this.setTabbable(e),e.focus()}lastElement(){const e=this.elements[this.elements.length-1];this.setTabbable(e),e.focus()}setTabbableByIndex(e){const t=this.elements[e];t&&this.setTabbable(t)}setTabbable(e){n(this.setUntabbable.bind(this),this.elements),v(e),this.tabbableElement=e}setUntabbable(e){e!==document.activeElement&&(this.negativeTabIndexAllowed?m(e):b(e))}previousElement({index:e}){const t=0===e,n=this.elements[t?this.elements.length-1:e-1];this.setTabbable(n),n.focus()}useNegativeTabIndex(){this.negativeTabIndexAllowed=!0,this.elements.forEach((e=>{e.hasAttribute("tabindex")||m(e)}))}initPlugins(){this.plugins.forEach((function(e){void 0!==e.init&&e.init(this)}),this)}}const E="aria-grabbed",k=l(E),x=h(E,"true"),Z=o(d(E)),P=t(n(l(E,"false")),Z),$=t(i(x),Z);class O{init(e){this.controls=e,this.controls.on("select",this.select,this)}addElement(e){k("false",e),this.controls.addElement(e)}setAllGrabbedToFalse(){P(this.controls.elements)}hasAnyGrabbed(){return $(this.controls.elements)}select({element:e}){const t=x(e);this.setAllGrabbedToFalse(),t||k("true",e)}}const T="aria-dropeffect",A=l(T,"none"),D=l(T,"move"),S=o(d(T)),C=t(n(D),S),I=t(n(A),S);class B{init(e){this.controls=e}setAllToMove(){C(this.controls.elements)}setAllToNone(){I(this.controls.elements)}}B.DropEffect={COPY:"copy",MOVE:"move",EXECUTE:"execute",POPUP:"popup",NONE:"none"};class z{constructor(){this.selectability=!0}init(e){this.boundHandleKeyDown=this.handleKeyDown.bind(this),this.controls=e,this.controls.on("addElement",this.listenForKeyDown,this),this.controls.on("removeElement",this.removeKeyDownListener,this)}listenForKeyDown({element:e}){e.addEventListener("keydown",this.boundHandleKeyDown)}removeKeyDownListener({element:e}){e.removeEventListener("keydown",this.boundHandleKeyDown)}handleKeyDown(e){switch(e.which){case 27:this.close(e.target),e.preventDefault(),e.stopPropagation();break;case 35:this.lastElement(e.target),e.preventDefault(),e.stopPropagation();break;case 36:this.firstElement(e.target),e.preventDefault(),e.stopPropagation();break;case 13:case 32:this.select(e.target),e.preventDefault(),e.stopPropagation();break;case 37:case 38:this.hasChromevoxModifiers(e)||(this.previousElement(e.target),e.preventDefault(),e.stopPropagation());break;case 39:case 40:this.hasChromevoxModifiers(e)||(this.nextElement(e.target),e.preventDefault(),e.stopPropagation())}}hasChromevoxModifiers(e){return e.shiftKey||e.ctrlKey}previousElement(e){!1!==this.controls.firesEvent("beforePreviousElement",e)&&(this.controls.firesEvent("previousElement",e),this.controls.firesEvent("afterPreviousElement",e))}nextElement(e){!1!==this.controls.firesEvent("beforeNextElement",e)&&(this.controls.firesEvent("nextElement",e),this.controls.firesEvent("afterNextElement",e))}select(e){this.selectability&&!1!==this.controls.firesEvent("before-select",e)&&(this.controls.firesEvent("select",e),this.controls.firesEvent("after-select",e))}firstElement(e){!1!==this.controls.firesEvent("beforeFirstElement",e)&&(this.controls.firesEvent("firstElement",e),this.controls.firesEvent("afterFirstElement",e))}lastElement(e){!1!==this.controls.firesEvent("beforeLastElement",e)&&(this.controls.firesEvent("lastElement",e),this.controls.firesEvent("afterLastElement",e))}disableSelectability(){this.selectability=!1}enableSelectability(){this.selectability=!0}close(e){!1!==this.controls.firesEvent("before-close",e)&&(this.controls.firesEvent("close",e),this.controls.firesEvent("after-close",e))}}class q{constructor(){this.selectability=!0,this.handleClickBound=this.handleClick.bind(this),this.handleDragBound=this.handleDrag.bind(this)}init(e){this.controls=e,this.controls.on("addElement",this.listenForKeyDown,this),this.controls.on("removeElement",this.unlistenForKeyDown,this)}listenForKeyDown({element:e}){e.addEventListener("click",this.handleClickBound),e.addEventListener("drag",this.handleClickBound)}unlistenForKeyDown({element:e}){e.removeEventListener("click",this.handleClickBound),e.removeEventListener("drag",this.handleDragBound)}handleClick(e){this.controls.firesEvent("select",e.currentTarget)}handleDrag(e){this.controls.firesEvent("drag",e.currentTarget)}disableSelectability(){this.selectability=!1}enableSelectability(){this.selectability=!0}}function H(e,t){for(var n=0;n
',i="";n.showLabel&&(o='
'+n.label+'
'+o,i=" h5p-has-label"),o='
'+n.l10n.prefix.replace("{num}",n.id+1)+" "+o,n.$dropZone=L("
",{class:"h5p-dropzone"+i,tabindex:"-1",role:"button","aria-disabled":!0,css:{left:n.x+"%",top:n.y+"%",width:n.width+"em",height:n.height+"em"},html:o}).appendTo(e).children(".h5p-inner").droppable({activeClass:"h5p-active",tolerance:"intersect",accept:function(e){var o=F.elementToDraggable(t,e);return!!o&&n.accepts(o.draggable,t)},drop:function(e,t){var o=L(this);F.setOpacity(o.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(){F.setOpacity(L(this).addClass("h5p-over"),"background",n.backgroundOpacity)},out:function(){F.setOpacity(L(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&&L("
",{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(){F.setOpacity(this.$dropZone.children(".h5p-label"),"background",this.backgroundOpacity),F.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 o=0;o
d&&(d=r)},p=0;p=t.width)h();else{if(c.x=l.x,s.x=n.x+i,d&&(c.y-=d,s.y+=d/o.height*100,d=0),c.y<=0)return;h()}}},{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=[]}}])&&R(t.prototype,n),Object.defineProperty(t,"prototype",{writable:!1}),e}();function Q(e){return Q="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},Q(e)}function K(e,t){for(var n=0;n ",{class:"h5p-draggable",tabindex:"-1",role:"button",css:{left:i.x+"%",top:i.y+"%",width:i.width+"em",height:i.height+"em"},appendTo:t,title:i.type.params.title}).on("click",(function(){i.trigger("focus",this)})).on("touchmove",G).on("touchstart",G).on("touchend",G).draggable({revert:function(e){t.removeClass("h5p-dragging");var n=_(this);return n.data("uiDraggable").originalPosition={top:i.y+"%",left:i.x+"%"},i.updatePlacement(o),n[0].setAttribute("aria-grabbed","false"),i.trigger("dragend"),!e},start:function(){var e=_(this),n=i.mustCopyElement(o);n&&o.clone(),e.removeClass("h5p-wrong").detach().appendTo(t),t.addClass("h5p-dragging"),F.setElementOpacity(e,i.backgroundOpacity),this.setAttribute("aria-grabbed","true"),i.trigger("focus",this),i.trigger("dragstart",{element:this,effect:n?"copy":"move"})},stop:function(){var n=_(this);o.position=F.positionToPercentage(t,n),n.css(o.position);var r=n.data("addToZone");void 0!==r?(n.removeData("addToZone"),i.addToDropZone(e,o,r)):o.reset()}}).css("position",""),i.element=o,o.position&&(o.$.css(o.position),i.updatePlacement(o)),F.addHover(o.$,i.backgroundOpacity),H5P.newRunnable(i.type,n,o.$),_(''+i.l10n.prefix.replace("{num}",i.draggableNum)+" ").prependTo(o.$),_(' ').appendTo(o.$),setTimeout((function(){F.setElementOpacity(o.$,i.backgroundOpacity)}),0),i.trigger("elementadd",o.$[0])}},{key:"setFeedback",value:function(e,t){this.elements.forEach((function(n){n.dropZone===t&&(void 0===n.$feedback&&(n.$feedback=_("",{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=0;t'+this.l10n.suffix.replace("{num}",t)+" ").appendTo(e.$)}else e.$.removeClass("h5p-dropped").removeClass("h5p-wrong").removeClass("h5p-correct").css({border:"",background:""}),F.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)}})),e.element&&(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&&(o=o.add(n.getElement("correct"===t))),e.$suffix=e.$suffix.add(o),e.$.addClass("h5p-"+t).append(o),F.setElementOpacity(e.$,this.backgroundOpacity)}}])&&K(t.prototype,n),Object.defineProperty(t,"prototype",{writable:!1}),s}(),V=H5P.jQuery,Y=0;function J(e,t,n){var o,i,r=this;Y++,this.id=this.contentId=t,this.contentData=n,H5P.Question.call(r,"dragquestion"),this.options=V.extend(!0,{},{scoreShow:"Check",tryAgain:"Retry",grabbablePrefix:"Grabbable {num} of {total}.",grabbableSuffix:"Placed in dropzone {num}.",dropzonePrefix:"Dropzone {num} of {total}.",noDropzone:"No dropzone",tipLabel:"Show tip.",tipAvailable:"Tip available",correctAnswer:"Correct answer",wrongAnswer:"Wrong answer",feedbackHeader:"Feedback",scoreBarLabel:"You got :num out of :total points",scoreExplanationButtonLabel:"Show score explanation",question:{settings:{questionTitle:this.contentData&&this.contentData.metadata&&this.contentData.metadata.title?this.contentData.metadata.title:"Drag and drop",size:{width:620,height:310}},task:{elements:[],dropZones:[]}},overallFeedback:[],behaviour:{enableRetry:!0,enableCheckButton:!0,preventResize:!1,singlePoint:!1,applyPenalties:!0,enableScoreExplanation:!0,dropZoneHighlighting:"dragging",autoAlignSpacing:2,showScorePoints:!0,showTitle:!1},a11yCheck:"Check the answers. The responses will be marked as correct, incorrect, or unanswered.",a11yRetry:"Retry the task. Reset all responses and start the task over again.",submit:"Submit"},e),this.options.behaviour.singlePoint&&(this.options.behaviour.enableScoreExplanation=!1),this.draggables=[],this.dropZones=[],this.answered=n&&void 0!==n.previousState&&void 0!==n.previousState.answers&&n.previousState.answers.length,this.blankIsCorrect=!0,this.backgroundOpacity=void 0===this.options.behaviour.backgroundOpacity||""===this.options.behaviour.backgroundOpacity.trim()?void 0:this.options.behaviour.backgroundOpacity,r.$noDropZone=V(''+r.options.noDropzone+"
");var s=ee(r.draggables,r.dropZones,r.$noDropZone[0]),a=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)},o=V("
",{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(){o.attr("class","h5p-my-fullscreen-button-exit"),o.attr("title",this.options.localize.exitFullscreen)})),this.on("exitFullScreen",(function(){o.attr("class","h5p-my-fullscreen-button-enter"),o.attr("title",this.options.localize.fullscreen)}))}e.registerButtons(),setTimeout((function(){e.trigger("resize")}),200)},J.prototype.getXAPIData=function(){var e=this.createXAPIEventTemplate("answered");return this.addQuestionToXAPI(e),this.addResponseToXAPI(e),{statement:e.data.statement}},J.prototype.addQuestionToXAPI=function(e){var t=e.getVerifiedStatementValue(["object","definition"]);V.extend(t,this.getXAPIDefinition())},J.prototype.getXAPIDefinition=function(){var e={};e.description={"en-US":V(""+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"+o+" ").text()}})}}e.correctResponsesPattern=[""],e.target=[];var i=!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)},J.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||t.$container.parents(".h5p-standalone.h5p-dragquestion").css("width","");var o=this.options.question.settings.size,i=o.width/o.height,r=this.$container.parent(),s=r.width()-parseFloat(r.css("margin-left"))-parseFloat(r.css("margin-right")),a=t.$container.parents(".h5p-standalone.h5p-dragquestion.h5p-semi-fullscreen");if(a.length){a.css("width",""),n||(t.$container.css("width","10px"),a.css("width",""),setTimeout((function(){t.trigger("resize",{decreaseSize:!0})}),200));var l=V(window.frameElement);l&&(s=l.parent().width(),a.css("width",s+"px"))}var c=s/i;s<=0&&(s=o.width,c=o.height),this.$container.css({width:s+"px",height:c+"px",fontSize:s/o.width*16+"px"})}},J.prototype.disableDraggables=function(){this.draggables.forEach((function(e){e.disable()}))},J.prototype.enableDraggables=function(){this.draggables.forEach((function(e){e.enable()}))},J.prototype.showAllSolutions=function(e){var t;this.points=0,this.rawPoints=0,this.blankIsCorrect&&(this.points=1,this.rawPoints=1),!e&&this.options.behaviour.showScorePoints&&!this.options.behaviour.singlePoint&&this.options.behaviour.applyPenalties&&(t=new H5P.Question.ScorePoints);for(var n=0;n 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;
}
});
// Using instructions as label for our text groups
this.labelId = 'h5p-blanks-instructions-' + Blanks.idCounter + '-' + H5P.createUUID();
this.content = self.createQuestions();
// 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,
expandImage: media.params.expandImage,
minimizeImage: media.params.minimizeImage
});
}
}
else if (type === 'H5P.Video') {
if (media.params.sources) {
// Register task video
self.setVideo(media);
}
}
else if (type === 'H5P.Audio') {
if (media.params.files) {
// Register task audio
self.setAudio(media);
}
}
}
// Register task introduction text
self.setIntroduction('' + self.params.text + '
');
// Register task content area
self.setContent(self.content, {
'class': self.params.behaviour.separateLines ? 'h5p-separate-lines' : ''
});
// ... and buttons
self.registerButtons();
// Restore previous state
self.setH5PUserState();
}
// Inheritance
Blanks.prototype = Object.create(Question.prototype);
Blanks.prototype.constructor = Blanks;
/**
* 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 () {
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) {
/**
* Observe resizing of input field, so that we can resize
* the H5P to fit all content when the input field grows in size
*/
let resizeTimer;
new ResizeObserver(function () {
// To avoid triggering resize too often, we wait a second after the last
// resize event has been received
clearTimeout(resizeTimer);
resizeTimer = setTimeout(function () {
self.trigger('resize');
}, 1000);
}).observe(this);
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 = {};
// The below replaceAll makes sure we don't get any unwanted XAPI_PLACEHOLDERs in the description
definition.description = {
'en-US': this.params.text.replaceAll(/_{10,}/gi, '_________')
};
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++) {
// The below replaceAll makes sure we don't get any unwanted XAPI_PLACEHOLDERs in the questions
let question = this.params.questions[i].replaceAll(/_{10,}/gi, '_________');
question = this.handleBlanks(question, 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, H5P.trim(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 === H5P.trim(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: H5P.trim(answer.replace(/\s*\/\s*/g, '/')),
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 () {
const trimmedAnswer = H5P.trim($input.val().replace(/\ /g, ' '));
// Set trimmed answer
$input.val(trimmedAnswer);
if (behaviour.formulaEditor) {
// If fomula editor is enabled set trimmed text
$input.parent().find('.wiris-h5p-input').html(trimmedAnswer);
}
return trimmedAnswer;
};
/**
* @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);
;
/** @namespace H5P */
H5P.VideoVimeo = (function ($) {
let numInstances = 0;
/**
* Vimeo video player for H5P.
*
* @class
* @param {Array} sources Video files to use
* @param {Object} options Settings for the player
* @param {Object} l10n Localization strings
*/
function VimeoPlayer(sources, options, l10n) {
const self = this;
let player;
// Since all the methods of the Vimeo Player SDK are promise-based, we keep
// track of all relevant state variables so that we can implement the
// H5P.Video API where all methods return synchronously.
let buffered = 0;
let currentQuality;
let currentTextTrack;
let currentTime = 0;
let duration = 0;
let isMuted = 0;
let volume = 0;
let playbackRate = 1;
let qualities = [];
let loadingFailedTimeout;
let failedLoading = false;
let ratio = 9/16;
let isLoaded = false;
const LOADING_TIMEOUT_IN_SECONDS = 8;
const id = `h5p-vimeo-${++numInstances}`;
const $wrapper = $('
');
const $placeholder = $('
', {
id: id,
html: `
`
}).appendTo($wrapper);
/**
* Create a new player with the Vimeo Player SDK.
*
* @private
*/
const createVimeoPlayer = async () => {
if (!$placeholder.is(':visible') || player !== undefined) {
return;
}
// Since the SDK is loaded asynchronously below, explicitly set player to
// null (unlike undefined) which indicates that creation has begun. This
// allows the guard statement above to be hit if this function is called
// more than once.
player = null;
const Vimeo = await loadVimeoPlayerSDK();
const MIN_WIDTH = 200;
const width = Math.max($wrapper.width(), MIN_WIDTH);
const canHasControls = options.controls || self.pressToPlay;
const embedOptions = {
url: sources[0].path,
controls: canHasControls,
responsive: true,
dnt: true,
// Hardcoded autoplay to false to avoid playing videos on init
autoplay: false,
loop: options.loop ? true : false,
playsinline: true,
quality: 'auto',
width: width,
muted: false,
keyboard: canHasControls,
};
// Create a new player
player = new Vimeo.Player(id, embedOptions);
registerVimeoPlayerEventListeneners(player);
// Failsafe timeout to handle failed loading of videos.
// This seems to happen for private videos even though the SDK docs
// suggests to catch PrivacyError when attempting play()
loadingFailedTimeout = setTimeout(() => {
failedLoading = true;
removeLoadingIndicator();
$wrapper.html(`${l10n.vimeoLoadingError}
`);
$wrapper.css({
width: null,
height: null
});
self.trigger('resize');
self.trigger('error', l10n.vimeoLoadingError);
}, LOADING_TIMEOUT_IN_SECONDS * 1000);
}
const removeLoadingIndicator = () => {
$placeholder.find('div.h5p-video-loading').remove();
};
/**
* Register event listeners on the given Vimeo player.
*
* @private
* @param {Vimeo.Player} player
*/
const registerVimeoPlayerEventListeneners = (player) => {
let isFirstPlay, tracks;
player.on('loaded', async () => {
isFirstPlay = true;
isLoaded = true;
clearTimeout(loadingFailedTimeout);
const videoDetails = await getVimeoVideoMetadata(player);
tracks = videoDetails.tracks.options;
currentTextTrack = tracks.current;
duration = videoDetails.duration;
qualities = videoDetails.qualities;
currentQuality = 'auto';
try {
ratio = videoDetails.dimensions.height / videoDetails.dimensions.width;
}
catch (e) { /* Intentionally ignore this, and fallback on the default ratio */ }
removeLoadingIndicator();
if (options.startAt) {
// Vimeo.Player doesn't have an option for setting start time upon
// instantiation, so we instead perform an initial seek here.
currentTime = await self.seek(options.startAt);
}
self.trigger('ready');
self.trigger('loaded');
self.trigger('qualityChange', currentQuality);
self.trigger('resize');
});
player.on('play', () => {
if (isFirstPlay) {
isFirstPlay = false;
if (tracks.length) {
self.trigger('captions', tracks);
}
}
});
// Handle playback state changes.
player.on('playing', () => self.trigger('stateChange', H5P.Video.PLAYING));
player.on('pause', () => self.trigger('stateChange', H5P.Video.PAUSED));
player.on('ended', () => self.trigger('stateChange', H5P.Video.ENDED));
// Track the percentage of video that has finished loading (buffered).
player.on('progress', (data) => {
buffered = data.percent * 100;
});
// Track the current time. The update frequency may be browser-dependent,
// according to the official docs:
// https://developer.vimeo.com/player/sdk/reference#timeupdate
player.on('timeupdate', (time) => {
currentTime = time.seconds;
});
};
/**
* Get metadata about the video loaded in the given Vimeo player.
*
* Example resolved value:
*
* ```
* {
* "duration": 39,
* "qualities": [
* {
* "name": "auto",
* "label": "Auto"
* },
* {
* "name": "1080p",
* "label": "1080p"
* },
* {
* "name": "720p",
* "label": "720p"
* }
* ],
* "dimensions": {
* "width": 1920,
* "height": 1080
* },
* "tracks": {
* "current": {
* "label": "English",
* "value": "en"
* },
* "options": [
* {
* "label": "English",
* "value": "en"
* },
* {
* "label": "Norsk bokmål",
* "value": "nb"
* }
* ]
* }
* }
* ```
*
* @private
* @param {Vimeo.Player} player
* @returns {Promise}
*/
const getVimeoVideoMetadata = (player) => {
// Create an object for easy lookup of relevant metadata
const massageVideoMetadata = (data) => {
const duration = data[0];
const qualities = data[1].map(q => ({
name: q.id,
label: q.label
}));
const tracks = data[2].reduce((tracks, current) => {
const h5pVideoTrack = new H5P.Video.LabelValue(current.label, current.language);
tracks.options.push(h5pVideoTrack);
if (current.mode === 'showing') {
tracks.current = h5pVideoTrack;
}
return tracks;
}, { current: undefined, options: [] });
const dimensions = { width: data[3], height: data[4] };
return {
duration,
qualities,
tracks,
dimensions
};
};
return Promise.all([
player.getDuration(),
player.getQualities(),
player.getTextTracks(),
player.getVideoWidth(),
player.getVideoHeight(),
]).then(data => massageVideoMetadata(data));
}
try {
if (document.featurePolicy.allowsFeature('autoplay') === false) {
self.pressToPlay = true;
}
}
catch (err) {}
/**
* Appends the video player to the DOM.
*
* @public
* @param {jQuery} $container
*/
self.appendTo = ($container) => {
$container.addClass('h5p-vimeo').append($wrapper);
createVimeoPlayer();
};
/**
* Get list of available qualities.
*
* @public
* @returns {Array}
*/
self.getQualities = () => {
return qualities;
};
/**
* Get the current quality.
*
* @returns {String} Current quality identifier
*/
self.getQuality = () => {
return currentQuality;
};
/**
* Set the playback quality.
*
* @public
* @param {String} quality
*/
self.setQuality = async (quality) => {
currentQuality = await player.setQuality(quality);
self.trigger('qualityChange', currentQuality);
};
/**
* Start the video.
*
* @public
*/
self.play = async () => {
if (!player) {
self.on('ready', self.play);
return;
}
try {
await player.play();
}
catch (error) {
switch (error.name) {
case 'PasswordError': // The video is password-protected
self.trigger('error', l10n.vimeoPasswordError);
break;
case 'PrivacyError': // The video is private
self.trigger('error', l10n.vimeoPrivacyError);
break;
default:
self.trigger('error', l10n.unknownError);
break;
}
}
};
/**
* Pause the video.
*
* @public
*/
self.pause = () => {
if (player) {
player.pause();
}
};
/**
* Seek video to given time.
*
* @public
* @param {Number} time
*/
self.seek = async (time) => {
if (!player) {
return;
}
currentTime = time;
await player.setCurrentTime(time);
};
/**
* @public
* @returns {Number} Seconds elapsed since beginning of video
*/
self.getCurrentTime = () => {
return currentTime;
};
/**
* @public
* @returns {Number} Video duration in seconds
*/
self.getDuration = () => {
return duration;
};
/**
* Get percentage of video that is buffered.
*
* @public
* @returns {Number} Between 0 and 100
*/
self.getBuffered = () => {
return buffered;
};
/**
* Mute the video.
*
* @public
*/
self.mute = async () => {
isMuted = await player.setMuted(true);
};
/**
* Unmute the video.
*
* @public
*/
self.unMute = async () => {
isMuted = await player.setMuted(false);
};
/**
* Whether the video is muted.
*
* @public
* @returns {Boolean} True if the video is muted, false otherwise
*/
self.isMuted = () => {
return isMuted;
};
/**
* Whether the video is loaded.
*
* @public
* @returns {Boolean} True if the video is muted, false otherwise
*/
self.isLoaded = () => {
return isLoaded;
};
/**
* Get the video player's current sound volume.
*
* @public
* @returns {Number} Between 0 and 100.
*/
self.getVolume = () => {
return volume;
};
/**
* Set the video player's sound volume.
*
* @public
* @param {Number} level
*/
self.setVolume = async (level) => {
volume = await player.setVolume(level);
};
/**
* Get list of available playback rates.
*
* @public
* @returns {Array} Available playback rates
*/
self.getPlaybackRates = () => {
return [0.5, 1, 1.5, 2];
};
/**
* Get the current playback rate.
*
* @public
* @returns {Number} e.g. 0.5, 1, 1.5 or 2
*/
self.getPlaybackRate = () => {
return playbackRate;
};
/**
* Set the current playback rate.
*
* @public
* @param {Number} rate Must be one of available rates from getPlaybackRates
*/
self.setPlaybackRate = async (rate) => {
playbackRate = await player.setPlaybackRate(rate);
self.trigger('playbackRateChange', rate);
};
/**
* Set current captions track.
*
* @public
* @param {H5P.Video.LabelValue} track Captions to display
*/
self.setCaptionsTrack = (track) => {
if (!track) {
return player.disableTextTrack().then(() => {
currentTextTrack = null;
});
}
player.enableTextTrack(track.value).then(() => {
currentTextTrack = track;
});
};
/**
* Get current captions track.
*
* @public
* @returns {H5P.Video.LabelValue}
*/
self.getCaptionsTrack = () => {
return currentTextTrack;
};
self.on('resize', () => {
if (failedLoading || !$wrapper.is(':visible')) {
return;
}
if (player === undefined) {
// Player isn't created yet. Try again.
createVimeoPlayer();
return;
}
// Use as much space as possible
$wrapper.css({
width: '100%',
height: 'auto'
});
const width = $wrapper[0].clientWidth;
const height = options.fit ? $wrapper[0].clientHeight : (width * (ratio));
// Validate height before setting
if (height > 0) {
// Set size
$wrapper.css({
width: width + 'px',
height: height + 'px'
});
}
});
}
/**
* Check to see if we can play any of the given sources.
*
* @public
* @static
* @param {Array} sources
* @returns {Boolean}
*/
VimeoPlayer.canPlay = (sources) => {
return getId(sources[0].path);
};
/**
* Find id of Vimeo video from given URL.
*
* @private
* @param {String} url
* @returns {String} Vimeo video ID
*/
const getId = (url) => {
// https://stackoverflow.com/a/11660798
const matches = url.match(/^.*(vimeo\.com\/)((channels\/[A-z]+\/)|(groups\/[A-z]+\/videos\/))?([0-9]+)/);
if (matches && matches[5]) {
return matches[5];
}
};
/**
* Load the Vimeo Player SDK asynchronously.
*
* @private
* @returns {Promise} Vimeo Player SDK object
*/
const loadVimeoPlayerSDK = async () => {
if (window.Vimeo) {
return await Promise.resolve(window.Vimeo);
}
return await new Promise((resolve, reject) => {
const tag = document.createElement('script');
tag.src = 'https://player.vimeo.com/api/player.js';
tag.onload = () => resolve(window.Vimeo);
tag.onerror = reject;
document.querySelector('script').before(tag);
});
};
return VimeoPlayer;
})(H5P.jQuery);
// Register video handler
H5P.videoHandlers = H5P.videoHandlers || [];
H5P.videoHandlers.push(H5P.VideoVimeo);
;
/** @namespace H5P */
H5P.VideoYouTube = (function ($) {
/**
* YouTube video player for H5P.
*
* @class
* @param {Array} sources Video files to use
* @param {Object} options Settings for the player
* @param {Object} l10n Localization strings
*/
function YouTube(sources, options, l10n) {
var self = this;
var player;
var playbackRate = 1;
var id = 'h5p-youtube-' + numInstances;
numInstances++;
var ratio = 9/16;
var $wrapper = $('
');
var $placeholder = $('
', {
text: l10n.loading,
html: ``
}).appendTo($wrapper);
// Optional placeholder
// var $placeholder = $('VIDEO ').appendTo($wrapper);
/**
* Use the YouTube API to create a new player
*
* @private
*/
var create = function () {
if (!$placeholder.is(':visible') || player !== undefined) {
return;
}
if (window.YT === undefined) {
// Load API first
loadAPI(create);
return;
}
if (YT.Player === undefined) {
return;
}
var width = $wrapper.width();
if (width < 200) {
width = 200;
}
var loadCaptionsModule = true;
var videoId = getId(sources[0].path);
player = new YT.Player(id, {
width: width,
height: width * (9/16),
videoId: videoId,
playerVars: {
origin: ORIGIN,
// Hardcoded autoplay to false to avoid playing videos on init
autoplay: 0,
controls: options.controls ? 1 : 0,
disablekb: options.controls ? 0 : 1,
fs: 0,
loop: options.loop ? 1 : 0,
playlist: options.loop ? videoId : undefined,
rel: 0,
showinfo: 0,
iv_load_policy: 3,
wmode: "opaque",
start: Math.floor(options.startAt),
playsinline: 1
},
events: {
onReady: function () {
self.trigger('ready');
self.trigger('loaded');
if (!options.autoplay) {
self.toPause = true;
}
if (options.deactivateSound) {
self.mute();
}
},
onApiChange: function () {
if (loadCaptionsModule) {
loadCaptionsModule = false;
// Always load captions
player.loadModule('captions');
}
var trackList;
try {
// Grab tracklist from player
trackList = player.getOption('captions', 'tracklist');
}
catch (err) {}
if (trackList && trackList.length) {
// Format track list into valid track options
var trackOptions = [];
for (var i = 0; i < trackList.length; i++) {
trackOptions.push(new H5P.Video.LabelValue(trackList[i].displayName, trackList[i].languageCode));
}
// Captions are ready for loading
self.trigger('captions', trackOptions);
}
},
onStateChange: function (state) {
if (state.data > -1 && state.data < 4) {
if (self.toPause) {
// if video buffering, was likely paused already - skip
if (state.data === H5P.Video.BUFFERING) {
delete self.toPause;
}
else {
self.pause();
}
}
// Fix for keeping playback rate in IE11
if (H5P.Video.IE11_PLAYBACK_RATE_FIX && state.data === H5P.Video.PLAYING && playbackRate !== 1) {
// YT doesn't know that IE11 changed the rate so it must be reset before it's set to the correct value
player.setPlaybackRate(1);
player.setPlaybackRate(playbackRate);
}
// End IE11 fix
self.trigger('stateChange', state.data);
}
},
onPlaybackQualityChange: function (quality) {
self.trigger('qualityChange', quality.data);
},
onPlaybackRateChange: function (playbackRate) {
self.trigger('playbackRateChange', playbackRate.data);
},
onError: function (error) {
var message;
switch (error.data) {
case 2:
message = l10n.invalidYtId;
break;
case 100:
message = l10n.unknownYtId;
break;
case 101:
case 150:
message = l10n.restrictedYt;
break;
default:
message = l10n.unknownError + ' ' + error.data;
break;
}
self.trigger('error', message);
}
}
});
player.g.style = "position:absolute;top:0;left:0;width:100%;height:100%;";
};
/**
* Indicates if the video must be clicked for it to start playing.
* For instance YouTube videos on iPad must be pressed to start playing.
*
* @public
*/
if (navigator.userAgent.match(/iPad/i)) {
self.pressToPlay = true;
}
else {
try {
if (document.featurePolicy.allowsFeature('autoplay') === false) {
self.pressToPlay = true;
}
}
catch (err) {}
}
/**
* Appends the video player to the DOM.
*
* @public
* @param {jQuery} $container
*/
self.appendTo = function ($container) {
$container.addClass('h5p-youtube').append($wrapper);
create();
};
/**
* Get list of available qualities. Not available until after play.
*
* @public
* @returns {Array}
*/
self.getQualities = function () {
if (!player || !player.getAvailableQualityLevels) {
return;
}
var qualities = player.getAvailableQualityLevels();
if (!qualities.length) {
return; // No qualities
}
// Add labels
for (var i = 0; i < qualities.length; i++) {
var quality = qualities[i];
var label = (LABELS[quality] !== undefined ? LABELS[quality] : 'Unknown'); // TODO: l10n
qualities[i] = {
name: quality,
label: LABELS[quality]
};
}
return qualities;
};
/**
* Get current playback quality. Not available until after play.
*
* @public
* @returns {String}
*/
self.getQuality = function () {
if (!player || !player.getPlaybackQuality) {
return;
}
var quality = player.getPlaybackQuality();
return quality === 'unknown' ? undefined : quality;
};
/**
* Set current playback quality. Not available until after play.
* Listen to event "qualityChange" to check if successful.
*
* @public
* @params {String} [quality]
*/
self.setQuality = function (quality) {
if (!player || !player.setPlaybackQuality) {
return;
}
player.setPlaybackQuality(quality);
};
/**
* Start the video.
*
* @public
*/
self.play = function () {
if (!player || !player.playVideo) {
self.on('ready', self.play);
return;
}
player.playVideo();
};
/**
* Pause the video.
*
* @public
*/
self.pause = function () {
delete self.toPause;
self.off('ready', self.play);
if (!player || !player.pauseVideo) {
return;
}
player.pauseVideo();
};
/**
* Seek video to given time.
*
* @public
* @param {Number} time
*/
self.seek = function (time) {
if (!player || !player.seekTo) {
return;
}
player.seekTo(time, true);
};
/**
* Recreate player with initial time
*
* @public
* @param {Number} time
*/
self.resetPlayback = function (time) {
options.startAt = time;
if (player) {
if (player.getPlayerState() === H5P.Video.PLAYING) {
player.pauseVideo();
self.trigger('stateChange', H5P.Video.PAUSED);
}
player.destroy();
player = undefined;
}
create();
}
/**
* Get elapsed time since video beginning.
*
* @public
* @returns {Number}
*/
self.getCurrentTime = function () {
if (!player || !player.getCurrentTime) {
return;
}
return player.getCurrentTime();
};
/**
* Get total video duration time.
*
* @public
* @returns {Number}
*/
self.getDuration = function () {
if (!player || !player.getDuration) {
return;
}
return player.getDuration();
};
/**
* Get percentage of video that is buffered.
*
* @public
* @returns {Number} Between 0 and 100
*/
self.getBuffered = function () {
if (!player || !player.getVideoLoadedFraction) {
return;
}
return player.getVideoLoadedFraction() * 100;
};
/**
* Turn off video sound.
*
* @public
*/
self.mute = function () {
if (!player || !player.mute) {
return;
}
player.mute();
};
/**
* Turn on video sound.
*
* @public
*/
self.unMute = function () {
if (!player || !player.unMute) {
return;
}
player.unMute();
};
/**
* Check if video sound is turned on or off.
*
* @public
* @returns {Boolean}
*/
self.isMuted = function () {
if (!player || !player.isMuted) {
return;
}
return player.isMuted();
};
/**
* Check if video is loaded and ready to play.
*
* @public
* @returns {Boolean}
*/
self.isLoaded = function () {
if (!player || !player.getPlayerState) {
return;
}
return player.getPlayerState() === 5;
};
/**
* Return the video sound level.
*
* @public
* @returns {Number} Between 0 and 100.
*/
self.getVolume = function () {
if (!player || !player.getVolume) {
return;
}
return player.getVolume();
};
/**
* Set video sound level.
*
* @public
* @param {Number} level Between 0 and 100.
*/
self.setVolume = function (level) {
if (!player || !player.setVolume) {
return;
}
player.setVolume(level);
};
/**
* Get list of available playback rates.
*
* @public
* @returns {Array} available playback rates
*/
self.getPlaybackRates = function () {
if (!player || !player.getAvailablePlaybackRates) {
return;
}
var playbackRates = player.getAvailablePlaybackRates();
if (!playbackRates.length) {
return; // No rates, but the array should contain at least 1
}
return playbackRates;
};
/**
* Get current playback rate.
*
* @public
* @returns {Number} such as 0.25, 0.5, 1, 1.25, 1.5 and 2
*/
self.getPlaybackRate = function () {
if (!player || !player.getPlaybackRate) {
return;
}
return player.getPlaybackRate();
};
/**
* Set current playback rate.
* Listen to event "playbackRateChange" to check if successful.
*
* @public
* @params {Number} suggested rate that may be rounded to supported values
*/
self.setPlaybackRate = function (newPlaybackRate) {
if (!player || !player.setPlaybackRate) {
return;
}
playbackRate = Number(newPlaybackRate);
player.setPlaybackRate(playbackRate);
};
/**
* Set current captions track.
*
* @param {H5P.Video.LabelValue} Captions track to show during playback
*/
self.setCaptionsTrack = function (track) {
player.setOption('captions', 'track', track ? {languageCode: track.value} : {});
};
/**
* Figure out which captions track is currently used.
*
* @return {H5P.Video.LabelValue} Captions track
*/
self.getCaptionsTrack = function () {
var track = player.getOption('captions', 'track');
return (track.languageCode ? new H5P.Video.LabelValue(track.displayName, track.languageCode) : null);
};
// Respond to resize events by setting the YT player size.
self.on('resize', function () {
if (!$wrapper.is(':visible')) {
return;
}
if (!player) {
// Player isn't created yet. Try again.
create();
return;
}
// Use as much space as possible
$wrapper.css({
width: '100%',
height: 'auto'
});
var width = $wrapper[0].clientWidth;
var height = options.fit ? $wrapper[0].clientHeight : (width * (9/16));
// Validate height before setting
if (height > 0) {
// Set size
$wrapper.css({
width: width + 'px',
height: height + 'px'
});
player.setSize(width, height);
}
});
}
/**
* Check to see if we can play any of the given sources.
*
* @public
* @static
* @param {Array} sources
* @returns {Boolean}
*/
YouTube.canPlay = function (sources) {
return getId(sources[0].path);
};
/**
* Find id of YouTube video from given URL.
*
* @private
* @param {String} url
* @returns {String} YouTube video identifier
*/
var getId = function (url) {
// Has some false positives, but should cover all regular URLs that people can find
var matches = url.match(/(?:(?:youtube.com\/(?:attribution_link\?(?:\S+))?(?:v\/|embed\/|watch\/|(?:user\/(?:\S+)\/)?watch(?:\S+)v\=))|(?:youtu.be\/|y2u.be\/))([A-Za-z0-9_-]{11})/i);
if (matches && matches[1]) {
return matches[1];
}
};
/**
* Load the IFrame Player API asynchronously.
*/
var loadAPI = function (loaded) {
if (window.onYouTubeIframeAPIReady !== undefined) {
// Someone else is loading, hook in
var original = window.onYouTubeIframeAPIReady;
window.onYouTubeIframeAPIReady = function (id) {
loaded(id);
original(id);
};
}
else {
// Load the API our self
var tag = document.createElement('script');
tag.src = "https://www.youtube.com/iframe_api";
var firstScriptTag = document.getElementsByTagName('script')[0];
firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
window.onYouTubeIframeAPIReady = loaded;
}
};
/** @constant {Object} */
var LABELS = {
highres: '2160p', // Old API support
hd2160: '2160p', // (New API)
hd1440: '1440p',
hd1080: '1080p',
hd720: '720p',
large: '480p',
medium: '360p',
small: '240p',
tiny: '144p',
auto: 'Auto'
};
/** @private */
var numInstances = 0;
// Extract the current origin (used for security)
var ORIGIN = window.location.href.match(/http[s]?:\/\/[^\/]+/);
ORIGIN = !ORIGIN || ORIGIN[0] === undefined ? undefined : ORIGIN[0];
// ORIGIN = undefined is needed to support fetching file from device local storage
return YouTube;
})(H5P.jQuery);
// Register video handler
H5P.videoHandlers = H5P.videoHandlers || [];
H5P.videoHandlers.push(H5P.VideoYouTube);
;
/** @namespace H5P */
H5P.VideoPanopto = (function ($) {
/**
* Panopto video player for H5P.
*
* @class
* @param {Array} sources Video files to use
* @param {Object} options Settings for the player
* @param {Object} l10n Localization strings
*/
function Panopto(sources, options, l10n) {
var self = this;
self.volume = 100;
self.toSeek = undefined;
var player;
var playbackRate = 1;
let canHasAutoplay;
var id = 'h5p-panopto-' + numInstances;
numInstances++;
let isLoaded = false;
let isPlayerReady = false;
var $wrapper = $('
');
var $placeholder = $('
', {
id: id,
html: '' + l10n.loading + '
'
}).appendTo($wrapper);
// Determine autoplay/play.
try {
if (document.featurePolicy.allowsFeature('autoplay') !== false) {
canHasAutoplay = true;
}
}
catch (err) {}
/**
* Use the Panopto API to create a new player
*
* @private
*/
var create = function () {
if (!$placeholder.is(':visible') || player !== undefined) {
return;
}
if (window.EmbedApi === undefined) {
// Load API first
loadAPI(create);
return;
}
var width = $wrapper.width();
if (width < 200) {
width = 200;
}
const videoId = getId(sources[0].path);
player = new EmbedApi(id, {
width: width,
height: width * (9/16),
serverName: videoId[0],
sessionId: videoId[1],
videoParams: { // Optional
interactivity: 'none',
showtitle: false,
autohide: true,
offerviewer: false,
autoplay: false,
showbrand: false,
start: 0,
hideoverlay: !options.controls,
},
events: {
onIframeReady: function () {
isPlayerReady = true;
$placeholder.children(0).text('');
if (options.autoplay && canHasAutoplay) {
player.loadVideo();
isLoaded = true;
}
self.trigger('containerLoaded');
self.trigger('resize'); // Avoid black iframe if loading is slow
},
onReady: function () {
self.videoLoaded = true;
self.trigger('loaded');
if (typeof self.oldTime === 'number') {
self.seek(self.oldTime);
}
else if (typeof self.startAt === 'number' && self.startAt > 0) {
self.seek(self.startAt);
}
if (player.hasCaptions()) {
const captions = [];
const captionTracks = player.getCaptionTracks();
for (trackIndex in captionTracks) {
captions.push(new H5P.Video.LabelValue(captionTracks[trackIndex], trackIndex));
}
// Select active track
currentTrack = player.getSelectedCaptionTrack();
currentTrack = captions[currentTrack] ? captions[currentTrack] : null;
self.trigger('captions', captions);
}
},
onStateChange: function (state) {
if ([H5P.Video.PLAYING, H5P.Video.PAUSED].includes(state) && typeof self.seekToTime === 'number') {
player.seekTo(self.seekToTime);
delete self.seekToTime;
}
// since panopto has different load sequence in IV, need additional condition here
if (self.WAS_RESET) {
self.WAS_RESET = false;
}
// TODO: Playback rate fix for IE11?
if (state > -1 && state < 4) {
self.trigger('stateChange', state);
}
},
onPlaybackRateChange: function () {
self.trigger('playbackRateChange', self.getPlaybackRate());
},
onError: function (error) {
if (error === ApiError.PlayWithSoundNotAllowed) {
// pause and allow user to handle playing
self.pause();
self.unMute(); // because player is automuted on this error
}
else {
self.trigger('error', l10n.unknownError);
}
},
onLoginShown: function () {
$placeholder.children().first().remove(); // Remove loading message
self.trigger('loaded'); // Resize parent
}
}
});
};
/**
* Indicates if the video must be clicked for it to start playing.
* This is always true for Panopto since all videos auto play.
*
* @public
*/
self.pressToPlay = true;
/**
* Appends the video player to the DOM.
*
* @public
* @param {jQuery} $container
*/
self.appendTo = function ($container) {
$container.addClass('h5p-panopto').append($wrapper);
create();
};
/**
* Get list of available qualities. Not available until after play.
*
* @public
* @returns {Array}
*/
self.getQualities = function () {
// Not available for Panopto
};
/**
* Get current playback quality. Not available until after play.
*
* @public
* @returns {String}
*/
self.getQuality = function () {
// Not available for Panopto
};
/**
* Set current playback quality. Not available until after play.
* Listen to event "qualityChange" to check if successful.
*
* @public
* @params {String} [quality]
*/
self.setQuality = function (quality) {
// Not available for Panopto
};
/**
* Start the video.
*
* @public
*/
self.play = function () {
if (!player || !player.playVideo || !isPlayerReady) {
return;
}
if (isLoaded || self.videoLoaded) {
player.playVideo();
}
else {
player.loadVideo(); // Loads and starts playing
isLoaded = true;
}
};
/**
* Pause the video.
*
* @public
*/
self.pause = function () {
if (!player || !player.pauseVideo) {
return;
}
try {
player.pauseVideo();
}
catch (err) {
// Swallow Panopto throwing an error. This has been seen in the authoring
// tool if Panopto has been used inside Iv inside CP
}
};
/**
* Seek video to given time.
*
* @public
* @param {Number} time
*/
self.seek = function (time) {
if (!player || !player.seekTo || !self.videoLoaded) {
return;
}
if (!player.isReady) {
self.seekToTime = time;
return;
}
player.seekTo(time);
if (self.WAS_RESET) {
// need to check just to be sure, since state === 1 is unusable
delete self.seekToTime;
self.WAS_RESET = false;
}
};
/**
* Recreate player with initial time
*
* @public
* @param {Number} time
*/
self.resetPlayback = function (time) {
if (player && player.isReady && self.videoLoaded) {
self.seek(time);
self.pause();
}
else {
self.seekToTime = time;
}
}
/**
* Get elapsed time since video beginning.
*
* @public
* @returns {Number}
*/
self.getCurrentTime = function () {
if (!player || !player.getCurrentTime) {
return;
}
return player.getCurrentTime();
};
/**
* Get total video duration time.
*
* @public
* @returns {Number}
*/
self.getDuration = function () {
if (!player || !player.getDuration) {
return;
}
return player.getDuration();
};
/**
* Get percentage of video that is buffered.
*
* @public
* @returns {Number} Between 0 and 100
*/
self.getBuffered = function () {
// Not available for Panopto
};
/**
* Turn off video sound.
*
* @public
*/
self.mute = function () {
if (!player || !player.muteVideo) {
return;
}
player.muteVideo();
};
/**
* Turn on video sound.
*
* @public
*/
self.unMute = function () {
if (!player || !player.unmuteVideo) {
return;
}
player.unmuteVideo();
// The volume is set to 0 when the browser prevents autoplay,
// causing there to be no sound despite unmuting
self.setVolume(self.volume);
};
/**
* Check if video sound is turned on or off.
*
* @public
* @returns {Boolean}
*/
self.isMuted = function () {
if (!player || !player.isMuted) {
return;
}
return player.isMuted();
};
/**
* Check video is loaded and ready to play
*
* @public
* @returns {Boolean}
*/
self.isLoaded = function () {
return isPlayerReady;
};
/**
* Return the video sound level.
*
* @public
* @returns {Number} Between 0 and 100.
*/
self.getVolume = function () {
if (!player || !player.getVolume) {
return;
}
return player.getVolume() * 100;
};
/**
* Set video sound level.
*
* @public
* @param {Number} level Between 0 and 100.
*/
self.setVolume = function (level) {
if (!player || !player.setVolume) {
return;
}
player.setVolume(level/100);
self.volume = level;
};
/**
* Get list of available playback rates.
*
* @public
* @returns {Array} available playback rates
*/
self.getPlaybackRates = function () {
return [0.25, 0.5, 1, 1.25, 1.5, 2];
};
/**
* Get current playback rate.
*
* @public
* @returns {Number} such as 0.25, 0.5, 1, 1.25, 1.5 and 2
*/
self.getPlaybackRate = function () {
if (!player || !player.getPlaybackRate) {
return;
}
return player.getPlaybackRate();
};
/**
* Set current playback rate.
* Listen to event "playbackRateChange" to check if successful.
*
* @public
* @params {Number} suggested rate that may be rounded to supported values
*/
self.setPlaybackRate = function (newPlaybackRate) {
if (!player || !player.setPlaybackRate) {
return;
}
player.setPlaybackRate(newPlaybackRate);
};
/**
* Set current captions track.
*
* @param {H5P.Video.LabelValue} Captions track to show during playback
*/
self.setCaptionsTrack = function (track) {
if (!track) {
player.disableCaptions();
currentTrack = null;
}
else {
player.enableCaptions(track.value + '');
currentTrack = track;
}
};
/**
* Figure out which captions track is currently used.
*
* @return {H5P.Video.LabelValue} Captions track
*/
self.getCaptionsTrack = function () {
return currentTrack; // No function for getting active caption track?
};
// Respond to resize events by setting the player size.
self.on('resize', function () {
if (!$wrapper.is(':visible')) {
return;
}
if (!player) {
// Player isn't created yet. Try again.
create();
return;
}
// 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 Echo */
H5P.VideoEchoVideo = (() => {
let numInstances = 0;
const CONTROLS_HEIGHT = 100;
/**
* EchoVideo video player for H5P.
*
* @class
* @param {Array} sources Video files to use
* @param {Object} options Settings for the player
* @param {Object} l10n Localization strings
*/
function EchoPlayer(sources, options, l10n) {
// State variables for the Player.
let player = undefined;
let buffered = 0;
let currentQuality;
let trackOptions = [];
let currentTime = 0;
let duration = 0;
let isMuted = false;
let loadingComplete = false;
let volume = 1;
let playbackRate = 1;
let qualities = [];
let loadingFailedTimeout;
let failedLoading = false;
let ratio = 9 / 16;
let currentState = H5P.Video.VIDEO_CUED;
// Echo360 server doesn't sync seek time with regular play time fast enough
let timelineUpdatesToSkip = 0;
let timeUpdateTimeout;
/*
* Echo360 player does send time updates ~ 0.25 seconds by default and
* ends playing the video without sending a final time update or an
* Video Ended event. We take care of determining reaching the video end
* ourselves.
*/
const echoMinUncertaintyCompensationS = 0.3;
const timelineUpdateDeltaSlackMS = 50;
let echoUncertaintyCompensationS = echoMinUncertaintyCompensationS;
let previousTickMS;
// Player specific immutable variables.
const LOADING_TIMEOUT_IN_SECONDS = 30;
const id = `h5p-echo-${++numInstances}`;
const instanceId = H5P.createUUID();
const wrapperElement = document.createElement('div');
const placeholderElement = document.createElement('div');
placeholderElement.classList.add('h5p-video-loading');
placeholderElement.setAttribute('style', 'height: 100%; min-height: 200px; display: block; z-index: 100; border: none;');
placeholderElement.setAttribute('aria-label', l10n.loading);
wrapperElement.setAttribute('id', id);
wrapperElement.append(placeholderElement);
/**
* Remove all elements from the placeholder dom element.
*
* @private
*/
const removeLoadingIndicator = () => {
placeholderElement.replaceChildren();
};
/**
* Generate an array of objects for use in a dropdown from the list of resolutions.
* @private
* @param {Array} qualityLevels - list of objects with supported qualities for the media
* @returns {Array} list of objects with label and name properties
*/
const mapQualityLevels = (qualityLevels) => {
const qualities = qualityLevels.map((quality) => {
return { label: quality.label.toLowerCase(), name: quality.value };
});
return qualities;
};
/**
* Register event listeners on the given Echo player.
*
* @private
* @param {HTMLElement} player
*/
const registerEchoPlayerEventListeneners = (player) => {
player.resolveLoading = null;
player.loadingPromise = new Promise((resolve) => {
player.resolveLoading = resolve;
});
player.onload = async () => {
clearTimeout(loadingFailedTimeout);
player.loadingPromise.then(async () => {
this.trigger('ready');
this.trigger('loaded');
this.loadingComplete = true;
this.trigger('resize');
if (trackOptions.length) {
this.trigger('captions', trackOptions);
}
const autoplayIsAllowed = !window.H5PEditor &&
await H5P.Video.isAutoplayAllowed();
if (options.autoplay && autoplayIsAllowed) {
this.play();
}
return true;
});
};
window.addEventListener('message', (event) => {
let message = '';
try {
message = JSON.parse(event.data);
}
catch (e) {
return;
}
if (
message.context !== 'Echo360' || message.instanceId !== instanceId
) {
return;
}
if (message.event === 'init') {
// Set ratio if width and height is received from Echo360
if (message.data.width && message.data.height) {
// If controls are displayed we have to add a magic height to make it visible :(
ratio = ((message.data.height + (options.controls ? CONTROLS_HEIGHT : 0)) / message.data.width);
}
duration = message.data.duration;
this.setCurrentTime(message.data.currentTime ?? 0);
textTracks = message.data.textTracks ?? [];
if (message.data.captions) {
trackOptions = textTracks.map((track) =>
new H5P.Video.LabelValue(track.label, track.value)
);
}
player.resolveLoading();
// Player sends `init` event after rebuffering, unfortunately.
if (!this.wasInitialized) {
qualities = mapQualityLevels(message.data.qualityOptions);
currentQuality = qualities[0].name;
this.trigger('qualityChange', currentQuality);
}
this.trigger('resize');
if (message.data.playing) {
changeState(H5P.Video.PLAYING);
}
this.wasInitialized = true;
}
else if (message.event === 'timeline') {
updateUncertaintyCompensation();
duration = message.data.duration ?? this.getDuration();
if (timelineUpdatesToSkip === 0) {
this.setCurrentTime(message.data.currentTime ?? 0);
}
else {
timelineUpdatesToSkip--;
}
/*
* Should work, but it was better if the player itself clearly sent
* the state (playing, paused, ended) instead of us having to infer.
*/
const compensatedTime = this.getCurrentTime() +
echoUncertaintyCompensationS * this.getPlaybackRate()
if (
currentState === H5P.Video.PLAYING &&
Math.ceil(compensatedTime) >= duration
) {
changeState(H5P.Video.ENDED);
if (options.loop) {
this.seek(0);
this.play();
}
return;
}
if (message.data.playing) {
timeUpdate(currentTime);
changeState(H5P.Video.PLAYING);
}
else if (currentState === H5P.Video.PLAYING) {
// Condition prevents video to be paused on startup
changeState(H5P.Video.PAUSED);
window.clearTimeout(timeUpdateTimeout);
}
}
});
};
/**
* Update the uncertainty compensation value.
* Computes the delta time between the last two timeline events sent by the
* Echo360 player and updates the compensation value.
*/
const updateUncertaintyCompensation = () => {
if (currentState === H5P.Video.PLAYING) {
const time = Date.now();
if (previousTickMS) {
echoUncertaintyCompensationS = Math.max(
echoMinUncertaintyCompensationS,
(time - previousTickMS + timelineUpdateDeltaSlackMS) /
1000
)
} else {
echoUncertaintyCompensationS = echoMinUncertaintyCompensationS;
}
previousTickMS = time;
}
else {
delete previousTickMS;
}
}
/**
* Change state of the player.
* @param {number} state State id (H5P.Video[statename]).
*/
const changeState = (state) => {
if (state !== currentState) {
currentState = state;
this.trigger('stateChange', state);
}
};
/**
* Determine if the element is visible by computing the styles.
*
* @private
* @param {HTMLElement} node - the element to check.
* @returns {Boolean} true if it is visible.
*/
const isNodeVisible = (node) => {
let style = window.getComputedStyle(node);
if (node.offsetWidth === 0) {
return false;
}
return ((style.display !== 'none') && (style.visibility !== 'hidden'));
};
const timeUpdate = (time) => {
window.clearTimeout(timeUpdateTimeout);
this.lastTimeUpdate = Date.now();
timeUpdateTimeout = window.setTimeout(() => {
if (currentState !== H5P.Video.PLAYING) {
return;
}
const delta = (Date.now() - this.lastTimeUpdate) * this.getPlaybackRate();
this.setCurrentTime(currentTime + delta / 1000);
timeUpdate(currentTime);
}, 40); // 25 fps
}
/**
* Create a new player by embedding an iframe.
*
* @private
* @returns {Promise}
*/
const createEchoPlayer = async () => {
if (!isNodeVisible(placeholderElement) || player !== undefined) {
return;
}
// Since the SDK is loaded asynchronously below, explicitly set player to
// null (unlike undefined) which indicates that creation has begun. This
// allows the guard statement above to be hit if this function is called
// more than once.
player = null;
let queryString = '?';
queryString += `instanceId=${instanceId}&`;
if (options.controls) {
queryString += 'controls=true&';
}
if (options.disableFullscreen) {
queryString += 'disableFullscreen=true&';
}
if (options.deactivateSound) {
queryString += 'deactivateSound=true&';
}
if (options.startAt) {
queryString += `startTimeMillis=${Math.round(options.startAt * 1000)}&`;
}
wrapperElement.innerHTML = ``;
player = wrapperElement.firstChild;
// Create a new player
registerEchoPlayerEventListeneners(player);
loadingFailedTimeout = setTimeout(() => {
failedLoading = true;
removeLoadingIndicator();
wrapperElement.innerHTML = `${l10n.unknownError}
`;
wrapperElement.style.cssText = 'width: null; height: null;';
this.trigger('resize');
this.trigger('error', l10n.unknownError);
}, LOADING_TIMEOUT_IN_SECONDS * 1000);
};
/**
* Appends the video player to the DOM.
*
* @public
* @param {jQuery} $container
*/
this.appendTo = ($container) => {
$container.addClass('h5p-echo').append(wrapperElement);
createEchoPlayer();
};
/**
* Determine if the video has loaded.
*
* @public
* @returns {Boolean}
*/
this.isLoaded = () => {
return loadingComplete;
};
/**
* Get list of available qualities.
*
* @public
* @returns {Array}
*/
this.getQualities = () => {
return qualities;
};
/**
* Get the current quality.
*
* @public
* @returns {String} Current quality identifier
*/
this.getQuality = () => {
return currentQuality;
};
/**
* Set the playback quality.
*
* @public
* @param {String} quality
*/
this.setQuality = async (quality) => {
this.post('quality', quality);
currentQuality = quality;
this.trigger('qualityChange', currentQuality);
};
/**
* Start the video.
*
* @public
*/
this.play = () => {
if (!player) {
this.on('ready', this.play);
return;
}
this.post('play', 0);
};
/**
* Pause the video.
*
* @public
*/
this.pause = () => {
// Compensate for Echo360's delayed time updates
timelineUpdatesToSkip = 1;
this.post('pause', 0);
};
/**
* Seek video to given time.
*
* @public
* @param {Number} time
*/
this.seek = (time) => {
this.post('seek', time);
this.setCurrentTime(time);
// Compensate for Echo360's delayed time updates
timelineUpdatesToSkip = 1;
};
/**
* Post a window message to the iframe.
*
* @public
* @param event
* @param data
*/
this.post = (event, data) => {
player?.contentWindow?.postMessage(
JSON.stringify({
event: event,
context: 'Echo360',
instanceId: instanceId,
data: data
}),
'*'
);
};
/**
* Return the current play position.
*
* @public
* @returns {Number} Seconds elapsed since beginning of video
*/
this.getCurrentTime = () => {
return currentTime;
};
/**
* Set current time.
* @param {number} timeS Time in seconds.
*/
this.setCurrentTime = (timeS) => {
currentTime = timeS;
}
/**
* Return the video duration.
*
* @public
* @returns {?Number} Video duration in seconds
*/
this.getDuration = () => {
if (duration > 0) {
return duration;
}
return null;
};
/**
* Get percentage of video that is buffered.
*
* @public
* @returns {Number} Between 0 and 100
*/
this.getBuffered = () => {
return buffered;
};
/**
* Mute the video.
*
* @public
*/
this.mute = () => {
this.post('mute', 0);
isMuted = true;
};
/**
* Unmute the video.
*
* @public
*/
this.unMute = () => {
this.post('unmute', 0);
isMuted = false;
};
/**
* Whether the video is muted.
*
* @public
* @returns {Boolean} True if the video is muted, false otherwise
*/
this.isMuted = () => {
return isMuted;
};
/**
* Get the video player's current sound volume.
*
* @public
* @returns {Number} Between 0 and 100.
*/
this.getVolume = () => {
return volume;
};
/**
* Set the video player's sound volume.
*
* @public
* @param {Number} level
*/
this.setVolume = (level) => {
this.post('volume', level);
volume = level;
};
/**
* Get list of available playback rates.
*
* @public
* @returns {Array} Available playback rates
*/
this.getPlaybackRates = () => {
return [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2];
};
/**
* Get the current playback rate.
*
* @public
* @returns {Number} e.g. 0.5, 1, 1.5 or 2
*/
this.getPlaybackRate = () => {
return playbackRate;
};
/**
* Set the current playback rate.
*
* @public
* @param {Number} rate Must be one of available rates from getPlaybackRates
*/
this.setPlaybackRate = async (rate) => {
const echoRate = parseFloat(rate);
this.post('playbackrate', echoRate);
playbackRate = rate;
this.trigger('playbackRateChange', rate);
};
/**
* Set current captions track.
*
* @public
* @param {H5P.Video.LabelValue} track Captions to display
*/
this.setCaptionsTrack = (track) => {
const echoCaption = trackOptions.find(
(trackItem) => track?.value === trackItem.value
);
trackOptions.forEach(trackItem => {
trackItem.mode = (trackItem === echoCaption) ? 'showing' : 'disabled';
});
this.post('captions', echoCaption ? echoCaption.value : 'off');
};
/**
* Get current captions track.
*
* @public
* @returns {H5P.Video.LabelValue|null} Current captions track.
*/
this.getCaptionsTrack = () => {
return trackOptions.find(
(trackItem) => trackItem.mode === 'showing'
) ?? null;
};
this.on('resize', () => {
if (failedLoading || !isNodeVisible(wrapperElement)) {
return;
}
if (player === undefined) {
// Player isn't created yet. Try again.
createEchoPlayer();
return;
}
// Use as much space as possible
wrapperElement.style.cssText = 'width: 100%; height: 100%;';
const width = wrapperElement.clientWidth;
const height = options.fit ? wrapperElement.clientHeight : (width * (ratio));
// Validate height before setting
if (height > 0) {
// Set size
wrapperElement.style.cssText = 'width: ' + width + 'px; height: ' + height + 'px;';
}
});
}
/**
* Find id of video from given URL.
*
* @private
* @param {String} url
* @returns {String} Echo video identifier
*/
const getId = (url) => {
const matches = url.match(/^[^/]+:\/\/(echo360[^/]+)\/media\/([^/]+)\/h5p.*$/i);
if (matches && matches.length === 3) {
return [matches[2], matches[2]];
}
};
/**
* Check to see if we can play any of the given sources.
*
* @public
* @static
* @param {Array} sources
* @returns {Boolean}
*/
EchoPlayer.canPlay = (sources) => {
return getId(sources[0].path);
};
return EchoPlayer;
})(H5P.jQuery);
// Register video handler
H5P.videoHandlers = H5P.videoHandlers || [];
H5P.videoHandlers.push(H5P.VideoEchoVideo);
;
/** @namespace H5P */
H5P.VideoHtml5 = (function ($) {
/**
* HTML5 video player for H5P.
*
* @class
* @param {Array} sources Video files to use
* @param {Object} options Settings for the player
* @param {Object} l10n Localization strings
*/
function Html5(sources, options, l10n) {
var self = this;
/**
* Small helper to ensure all video sources get the same cache buster.
*
* @private
* @param {Object} source
* @return {string}
*/
const getCrossOriginPath = function (source) {
let path = H5P.getPath(source.path, self.contentId);
if (video.crossOrigin !== null && H5P.addQueryParameter && H5PIntegration.crossoriginCacheBuster) {
path = H5P.addQueryParameter(path, H5PIntegration.crossoriginCacheBuster);
}
return path
};
/**
* Register track to video
*
* @param {Object} trackData Track object
* @param {string} trackData.kind Kind of track
* @param {Object} trackData.track Source path
* @param {string} [trackData.label] Label of track
* @param {string} [trackData.srcLang] Language code
*/
const addTrack = function (trackData) {
// Skip invalid tracks
if (!trackData.kind || !trackData.track.path) {
return;
}
var track = document.createElement('track');
track.kind = trackData.kind;
track.src = getCrossOriginPath(trackData.track); // Uses same crossOrigin as parent. You cannot mix.
if (trackData.label) {
track.label = trackData.label;
}
if (trackData.srcLang) {
track.srcLang = trackData.srcLang;
}
return track;
};
/**
* Small helper to set the inital video source.
* Useful if some of the loading happens asynchronously.
* NOTE: Setting the crossOrigin must happen before any of the
* sources(poster, tracks etc.) are loaded
*
* @private
*/
const setInitialSource = function () {
if (qualities[currentQuality] === undefined) {
return;
}
if (H5P.setSource !== undefined) {
H5P.setSource(video, qualities[currentQuality].source, self.contentId)
}
else {
// Backwards compatibility (H5P < v1.22)
const srcPath = H5P.getPath(qualities[currentQuality].source.path, self.contentId);
if (H5P.getCrossOrigin !== undefined) {
var crossOrigin = H5P.getCrossOrigin(srcPath);
video.setAttribute('crossorigin', crossOrigin !== null ? crossOrigin : 'anonymous');
}
video.src = srcPath;
}
// Add poster if provided
if (options.poster) {
video.poster = getCrossOriginPath(options.poster); // Uses same crossOrigin as parent. You cannot mix.
}
// Register tracks
options.tracks.forEach(function (track, i) {
var trackElement = addTrack(track);
if (i === 0) {
trackElement.default = true;
}
if (trackElement) {
video.appendChild(trackElement);
}
});
};
/**
* Displayed when the video is buffering
* @private
*/
var $throbber = $('
', {
'class': 'h5p-video-loading'
});
/**
* Used to display error messages
* @private
*/
var $error = $('
', {
'class': 'h5p-video-error'
});
/**
* Keep track of current state when changing quality.
* @private
*/
var stateBeforeChangingQuality;
var currentTimeBeforeChangingQuality;
/**
* Avoids firing the same event twice.
* @private
*/
var lastState;
/**
* Keeps track whether or not the video has been loaded.
* @private
*/
var isLoaded = false;
/**
*
* @private
*/
var playbackRate = 1;
var skipRateChange = false;
// Create player
var video = document.createElement('video');
// Sort sources into qualities
var qualities = getQualities(sources, video);
var currentQuality;
numQualities = 0;
for (let quality in qualities) {
numQualities++;
}
if (numQualities > 1 && H5P.VideoHtml5.getExternalQuality !== undefined) {
H5P.VideoHtml5.getExternalQuality(sources, function (chosenQuality) {
if (qualities[chosenQuality] !== undefined) {
currentQuality = chosenQuality;
}
setInitialSource();
});
}
else {
// Select quality and source
currentQuality = getPreferredQuality();
if (currentQuality === undefined || qualities[currentQuality] === undefined) {
// No preferred quality, pick the first.
for (currentQuality in qualities) {
if (qualities.hasOwnProperty(currentQuality)) {
break;
}
}
}
setInitialSource();
}
// Setting webkit-playsinline, which makes iOS 10 beeing able to play video
// inside browser.
video.setAttribute('webkit-playsinline', '');
video.setAttribute('playsinline', '');
video.setAttribute('preload', 'metadata');
// Remove buttons in Chrome's video player:
let controlsList = 'nodownload';
if (options.disableFullscreen) {
controlsList += ' nofullscreen';
}
if (options.disableRemotePlayback) {
controlsList += ' noremoteplayback';
}
video.setAttribute('controlsList', controlsList);
// Remove picture in picture as it interfers with other video players
video.disablePictureInPicture = true;
// Set options
video.disableRemotePlayback = (options.disableRemotePlayback ? true : false);
video.controls = (options.controls ? true : false);
// Hardcoded autoplay to false to avoid playing videos on init
video.autoplay = false;
video.loop = (options.loop ? true : false);
video.className = 'h5p-video';
video.style.display = 'block';
if (options.fit) {
// Style is used since attributes with relative sizes aren't supported by IE9.
video.style.width = '100%';
video.style.height = '100%';
}
/**
* Helps registering events.
*
* @private
* @param {String} native Event name
* @param {String} h5p Event name
* @param {String} [arg] Optional argument
*/
var mapEvent = function (native, h5p, arg) {
video.addEventListener(native, function () {
switch (h5p) {
case 'loaded':
isLoaded = true;
if (stateBeforeChangingQuality !== undefined) {
return; // Avoid loaded event when changing quality.
}
// Remove any errors
if ($error.is(':visible')) {
$error.remove();
}
if (OLD_ANDROID_FIX) {
var andLoaded = function () {
video.removeEventListener('durationchange', andLoaded, false);
// On Android seeking isn't ready until after play.
self.trigger(h5p);
};
video.addEventListener('durationchange', andLoaded, false);
return;
}
if (video.poster) {
$(video).one('play', function () {
self.seek(self.getCurrentTime() || options.startAt);
});
}
else {
self.seek(options.startAt);
}
break;
case 'error':
// Handle error and get message.
arg = error(arguments[0], arguments[1]);
break;
case 'playbackRateChange':
// Fix for keeping playback rate in IE11
if (skipRateChange) {
skipRateChange = false;
return; // Avoid firing event when changing back
}
if (H5P.Video.IE11_PLAYBACK_RATE_FIX && playbackRate != video.playbackRate) { // Intentional
// Prevent change in playback rate not triggered by the user
video.playbackRate = playbackRate;
skipRateChange = true;
return;
}
// End IE11 fix
arg = self.getPlaybackRate();
break;
}
self.trigger(h5p, arg);
}, false);
};
/**
* Handle errors from the video player.
*
* @private
* @param {Object} code Error
* @param {String} [message]
* @returns {String} Human readable error message.
*/
var error = function (code, message) {
if (code instanceof Event) {
// No error code
if (!code.target.error) {
return '';
}
switch (code.target.error.code) {
case MediaError.MEDIA_ERR_ABORTED:
message = l10n.aborted;
break;
case MediaError.MEDIA_ERR_NETWORK:
message = l10n.networkFailure;
break;
case MediaError.MEDIA_ERR_DECODE:
message = l10n.cannotDecode;
break;
case MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED:
message = l10n.formatNotSupported;
break;
case MediaError.MEDIA_ERR_ENCRYPTED:
message = l10n.mediaEncrypted;
break;
}
}
if (!message) {
message = l10n.unknownError;
}
// Hide throbber
$throbber.remove();
// Display error message to user
$error.text(message).insertAfter(video);
// Pass message to our error event
return message;
};
/**
* Appends the video player to the DOM.
*
* @public
* @param {jQuery} $container
*/
self.appendTo = function ($container) {
$container.append(video);
};
/**
* Get list of available qualities. Not available until after play.
*
* @public
* @returns {Array}
*/
self.getQualities = function () {
// Create reverse list
var options = [];
for (var q in qualities) {
if (qualities.hasOwnProperty(q)) {
options.splice(0, 0, {
name: q,
label: qualities[q].label
});
}
}
if (options.length < 2) {
// Do not return if only one quality.
return;
}
return options;
};
/**
* Get current playback quality. Not available until after play.
*
* @public
* @returns {String}
*/
self.getQuality = function () {
return currentQuality;
};
/**
* Set current playback quality. Not available until after play.
* Listen to event "qualityChange" to check if successful.
*
* @public
* @params {String} [quality]
*/
self.setQuality = function (quality) {
if (qualities[quality] === undefined || quality === currentQuality) {
return; // Invalid quality
}
// Keep track of last choice
setPreferredQuality(quality);
// Avoid multiple loaded events if changing quality multiple times.
if (!stateBeforeChangingQuality) {
// Keep track of last state
stateBeforeChangingQuality = lastState;
// Keep track of current time
currentTimeBeforeChangingQuality = video.currentTime;
// Seek and start video again after loading.
var loaded = function () {
video.removeEventListener('loadedmetadata', loaded, false);
if (OLD_ANDROID_FIX) {
var andLoaded = function () {
video.removeEventListener('durationchange', andLoaded, false);
// On Android seeking isn't ready until after play.
self.seek(currentTimeBeforeChangingQuality);
};
video.addEventListener('durationchange', andLoaded, false);
}
else {
// Seek to current time.
self.seek(currentTimeBeforeChangingQuality);
}
// Always play to get image.
video.play();
if (stateBeforeChangingQuality !== H5P.Video.PLAYING) {
// Do not resume playing
video.pause();
}
// Done changing quality
stateBeforeChangingQuality = undefined;
// Remove any errors
if ($error.is(':visible')) {
$error.remove();
}
};
video.addEventListener('loadedmetadata', loaded, false);
}
// Keep track of current quality
currentQuality = quality;
self.trigger('qualityChange', currentQuality);
// Display throbber
self.trigger('stateChange', H5P.Video.BUFFERING);
// Change source
video.src = getCrossOriginPath(qualities[quality].source); // (iPad does not support #t=).
// Note: Optional tracks use same crossOrigin as the original. You cannot mix.
// Remove poster so it will not show during quality change
video.removeAttribute('poster');
};
/**
* Starts the video.
*
* @public
* @return {Promise|undefined} May return a Promise that resolves when
* play has been processed.
*/
self.play = function () {
if ($error.is(':visible')) {
return;
}
if (!isLoaded) {
// Make sure video is loaded before playing
video.load();
video.addEventListener('loadeddata', function() {
video.play();
}, false);
}
else {
return video.play();
}
};
/**
* Pauses the video.
*
* @public
*/
self.pause = function () {
video.pause();
};
/**
* Seek video to given time.
*
* @public
* @param {Number} time
*/
self.seek = function (time) {
video.currentTime = time;
// Use canplaythrough for IOs devices
// Use loadedmetadata for all other devices.
const eventName = navigator.userAgent.match(/iPad|iPod|iPhone/i) ? "canplaythrough" : "loadedmetadata";
function seekTo() {
video.currentTime = time;
video.removeEventListener(eventName, seekTo);
};
if (video.readyState === 4) {
seekTo();
}
else {
video.addEventListener(eventName, seekTo);
}
};
/**
* Get elapsed time since video beginning.
*
* @public
* @returns {Number}
*/
self.getCurrentTime = function () {
return video.currentTime;
};
/**
* Get total video duration time.
*
* @public
* @returns {Number}
*/
self.getDuration = function () {
if (isNaN(video.duration)) {
return;
}
return video.duration;
};
/**
* Get percentage of video that is buffered.
*
* @public
* @returns {Number} Between 0 and 100
*/
self.getBuffered = function () {
// Find buffer currently playing from
var buffered = 0;
for (var i = 0; i < video.buffered.length; i++) {
var from = video.buffered.start(i);
var to = video.buffered.end(i);
if (video.currentTime > from && video.currentTime < to) {
buffered = to;
break;
}
}
// To percentage
return buffered ? (buffered / video.duration) * 100 : 0;
};
/**
* Turn off video sound.
*
* @public
*/
self.mute = function () {
video.muted = true;
};
/**
* Turn on video sound.
*
* @public
*/
self.unMute = function () {
video.muted = false;
};
/**
* Check if video sound is turned on or off.
*
* @public
* @returns {Boolean}
*/
self.isMuted = function () {
return video.muted;
};
/**
* Returns the video sound level.
*
* @public
* @returns {Number} Between 0 and 100.
*/
self.getVolume = function () {
return video.volume * 100;
};
/**
* Set video sound level.
*
* @public
* @param {Number} level Between 0 and 100.
*/
self.setVolume = function (level) {
video.volume = level / 100;
};
/**
* Get list of available playback rates.
*
* @public
* @returns {Array} available playback rates
*/
self.getPlaybackRates = function () {
/*
* not sure if there's a common rule about determining good speeds
* using Google's standard options via a constant for setting
*/
var playbackRates = PLAYBACK_RATES;
return playbackRates;
};
/**
* Get current playback rate.
*
* @public
* @returns {Number} such as 0.25, 0.5, 1, 1.25, 1.5 and 2
*/
self.getPlaybackRate = function () {
return video.playbackRate;
};
/**
* Set current playback rate.
* Listen to event "playbackRateChange" to check if successful.
*
* @public
* @params {Number} suggested rate that may be rounded to supported values
*/
self.setPlaybackRate = function (newPlaybackRate) {
playbackRate = newPlaybackRate;
video.playbackRate = newPlaybackRate;
};
/**
* Set current captions track.
*
* @param {H5P.Video.LabelValue} Captions track to show during playback
*/
self.setCaptionsTrack = function (track) {
for (var i = 0; i < video.textTracks.length; i++) {
video.textTracks[i].mode = (track && track.value === i ? 'showing' : 'disabled');
}
};
/**
* Figure out which captions track is currently used.
*
* @return {H5P.Video.LabelValue} Captions track
*/
self.getCaptionsTrack = function () {
for (var i = 0; i < video.textTracks.length; i++) {
if (video.textTracks[i].mode === 'showing') {
return new H5P.Video.LabelValue(video.textTracks[i].label, i);
}
}
return null;
};
// Register event listeners
mapEvent('ended', 'stateChange', H5P.Video.ENDED);
mapEvent('playing', 'stateChange', H5P.Video.PLAYING);
mapEvent('pause', 'stateChange', H5P.Video.PAUSED);
mapEvent('waiting', 'stateChange', H5P.Video.BUFFERING);
mapEvent('loadedmetadata', 'loaded');
mapEvent('canplay', 'canplay');
mapEvent('error', 'error');
mapEvent('ratechange', 'playbackRateChange');
if (!video.controls) {
// Disable context menu(right click) to prevent controls.
video.addEventListener('contextmenu', function (event) {
event.preventDefault();
}, false);
}
// Display throbber when buffering/loading video.
self.on('stateChange', function (event) {
var state = event.data;
lastState = state;
if (state === H5P.Video.BUFFERING) {
$throbber.insertAfter(video);
}
else {
$throbber.remove();
}
});
// Load captions after the video is loaded
self.on('loaded', function () {
nextTick(function () {
var textTracks = [];
for (var i = 0; i < video.textTracks.length; i++) {
textTracks.push(new H5P.Video.LabelValue(video.textTracks[i].label, i));
}
if (textTracks.length) {
self.trigger('captions', textTracks);
}
});
});
// Alternative to 'canplay' event
/*self.on('resize', function () {
if (video.offsetParent === null) {
return;
}
video.style.width = '100%';
video.style.height = '100%';
var width = video.clientWidth;
var height = options.fit ? video.clientHeight : (width * (video.videoHeight / video.videoWidth));
video.style.width = width + 'px';
video.style.height = height + 'px';
});*/
// Video controls are ready
nextTick(function () {
self.trigger('ready');
});
/**
* Check video is loaded and ready play.
*
* @public
* @param {Boolean}
*/
self.isLoaded = function () {
return isLoaded;
};
}
/**
* Check to see if we can play any of the given sources.
*
* @public
* @static
* @param {Array} sources
* @returns {Boolean}
*/
Html5.canPlay = function (sources) {
var video = document.createElement('video');
if (video.canPlayType === undefined) {
return false; // Not supported
}
// Cycle through sources
for (var i = 0; i < sources.length; i++) {
var type = getType(sources[i]);
if (type && video.canPlayType(type) !== '') {
// We should be able to play this
return true;
}
}
return false;
};
/**
* Find source type.
*
* @private
* @param {Object} source
* @returns {String}
*/
var getType = function (source) {
var type = source.mime;
if (!type) {
// Try to get type from URL
var matches = source.path.match(/\.(\w+)$/);
if (matches && matches[1]) {
type = 'video/' + matches[1];
}
}
if (type && source.codecs) {
// Add codecs
type += '; codecs="' + source.codecs + '"';
}
return type;
};
/**
* Sort sources into qualities.
*
* @private
* @static
* @param {Array} sources
* @param {Object} video
* @returns {Object} Quality mapping
*/
var getQualities = function (sources, video) {
var qualities = {};
var qualityIndex = 1;
var lastQuality;
// Cycle through sources
for (var i = 0; i < sources.length; i++) {
var source = sources[i];
// Find and update type.
var type = source.type = getType(source);
// Check if we support this type
var isPlayable = type && (type === 'video/unknown' || video.canPlayType(type) !== '');
if (!isPlayable) {
continue; // We cannot play this source
}
if (source.quality === undefined) {
/**
* No quality metadata. Create a quality tag to separate multiple sources of the same type,
* e.g. if two mp4 files with different quality has been uploaded
*/
if (lastQuality === undefined || qualities[lastQuality].source.type === type) {
// Create a new quality tag
source.quality = {
name: 'q' + qualityIndex,
label: (source.metadata && source.metadata.qualityName) ? source.metadata.qualityName : 'Quality ' + qualityIndex // TODO: l10n
};
qualityIndex++;
}
else {
/**
* Assumes quality already exists in a different format.
* Uses existing label for this quality.
*/
source.quality = qualities[lastQuality].source.quality;
}
}
// Log last quality
lastQuality = source.quality.name;
// Look to see if quality exists
var quality = qualities[lastQuality];
if (quality) {
// We have a source with this quality. Check if we have a better format.
if (source.mime.split('/')[1] === PREFERRED_FORMAT) {
quality.source = source;
}
}
else {
// Add new source with quality.
qualities[source.quality.name] = {
label: source.quality.label,
source: source
};
}
}
return qualities;
};
/**
* Set preferred video quality.
*
* @private
* @static
* @param {String} quality Index of preferred quality
*/
var setPreferredQuality = function (quality) {
try {
localStorage.setItem('h5pVideoQuality', quality);
}
catch (err) {
console.warn('Unable to set preferred video quality, localStorage is not available.');
}
};
/**
* Get preferred video quality.
*
* @private
* @static
* @returns {String} Index of preferred quality
*/
var getPreferredQuality = function () {
// First check localStorage
let quality;
try {
quality = localStorage.getItem('h5pVideoQuality');
}
catch (err) {
console.warn('Unable to retrieve preferred video quality from localStorage.');
}
if (!quality) {
try {
// The fallback to old cookie solution
var settings = document.cookie.split(';');
for (var i = 0; i < settings.length; i++) {
var setting = settings[i].split('=');
if (setting[0] === 'H5PVideoQuality') {
quality = setting[1];
break;
}
}
}
catch (err) {
console.warn('Unable to retrieve preferred video quality from cookie.');
}
}
return quality;
};
/**
* Helps schedule a task for the next tick.
* @param {function} task
*/
var nextTick = function (task) {
setTimeout(task, 0);
};
/** @constant {Boolean} */
var OLD_ANDROID_FIX = false;
/** @constant {Boolean} */
var PREFERRED_FORMAT = 'mp4';
/** @constant {Object} */
var PLAYBACK_RATES = [0.25, 0.5, 1, 1.25, 1.5, 2];
if (navigator.userAgent.indexOf('Android') !== -1) {
// We have Android, check version.
var version = navigator.userAgent.match(/AppleWebKit\/(\d+\.?\d*)/);
if (version && version[1] && Number(version[1]) <= 534.30) {
// Include fix for devices running the native Android browser.
// (We don't know when video was fixed, so the number is just the lastest
// native android browser we found.)
OLD_ANDROID_FIX = true;
}
}
else {
if (navigator.userAgent.indexOf('Chrome') !== -1) {
// If we're using chrome on a device that isn't Android, prefer the webm
// format. This is because Chrome has trouble with some mp4 codecs.
PREFERRED_FORMAT = 'webm';
}
}
return Html5;
})(H5P.jQuery);
// Register video handler
H5P.videoHandlers = H5P.videoHandlers || [];
H5P.videoHandlers.push(H5P.VideoHtml5);
;
/** @namespace H5P */
H5P.Video = (function ($, ContentCopyrights, MediaCopyright, handlers) {
/**
* The ultimate H5P video player!
*
* @class
* @param {Object} parameters Options for this library.
* @param {Object} parameters.visuals Visual options
* @param {Object} parameters.playback Playback options
* @param {Object} parameters.a11y Accessibility options
* @param {Boolean} [parameters.startAt] Start time of video
* @param {Number} id Content identifier
* @param {Object} [extras] Extra parameters.
*/
function Video(parameters, id, extras = {}) {
var self = this;
self.oldTime = extras.previousState?.time;
self.contentId = id;
self.WAS_RESET = false;
self.startAt = parameters.startAt || 0;
// Ref youtube.js - ipad & youtube - issue
self.pressToPlay = false;
// Reference to the handler
var handlerName = '';
// Initialize event inheritance
H5P.EventDispatcher.call(self);
// Default language localization
parameters = $.extend(true, parameters, {
l10n: {
name: 'Video',
loading: 'Video player loading...',
noPlayers: 'Found no video players that supports the given video format.',
noSources: 'Video source is missing.',
aborted: 'Media playback has been aborted.',
networkFailure: 'Network failure.',
cannotDecode: 'Unable to decode media.',
formatNotSupported: 'Video format not supported.',
mediaEncrypted: 'Media encrypted.',
unknownError: 'Unknown error.',
vimeoPasswordError: 'Password-protected Vimeo videos are not supported.',
vimeoPrivacyError: 'The Vimeo video cannot be used due to its privacy settings.',
vimeoLoadingError: 'The Vimeo video could not be loaded.',
invalidYtId: 'Invalid YouTube ID.',
unknownYtId: 'Unable to find video with the given YouTube ID.',
restrictedYt: 'The owner of this video does not allow it to be embedded.'
}
});
parameters.a11y = parameters.a11y || [];
parameters.playback = parameters.playback || {};
parameters.visuals = $.extend(true, 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);
}
});
/**
* Handle autoplay. If autoplay is disabled, it will still autopause when
* video is not visible.
*
* @param {*} $container
*/
const handleAutoPlayPause = function ($container) {
// Keep the current state
let state;
self.on('stateChange', function(event) {
state = event.data;
});
// Keep record of autopauses.
// I.e: we don't wanna autoplay if the user has excplicitly paused.
self.autoPaused = !self.pressToPlay;
new IntersectionObserver(function (entries) {
const entry = entries[0];
// This video element became visible
if (entry.isIntersecting) {
// Autoplay if autoplay is enabled and it was not explicitly
// paused by a user
if (parameters.playback.autoplay && self.autoPaused) {
self.autoPaused = false;
self.play();
}
}
else if (state !== Video.PAUSED && state !== Video.ENDED) {
self.autoPaused = true;
self.pause();
}
}, {
root: null,
threshold: [0, 1] // Get events when it is shown and hidden
}).observe($container.get(0));
};
/**
* Attaches the video handler to the given container.
* Inserts text if no handler is found.
*
* @public
* @param {jQuery} $container
*/
self.attach = function ($container) {
$container.addClass('h5p-video').html('');
if (self.appendTo !== undefined) {
self.appendTo($container);
// Avoid autoplaying in authoring tool
if (window.H5PEditor === undefined) {
handleAutoPlayPause($container);
}
}
else if (sources.length) {
$container.text(parameters.l10n.noPlayers);
}
else {
$container.text(parameters.l10n.noSources);
}
};
/**
* Get name of the video handler
*
* @public
* @returns {string}
*/
self.getHandlerName = function() {
return handlerName;
};
/**
* @public
* Get current state for resume support.
*
* @returns {object} Current state.
*/
self.getCurrentState = function () {
if (self.getCurrentTime) {
return {
time: self.getCurrentTime() || self.oldTime,
};
}
};
/**
* The two functions below needs to be defined in this base class,
* since it is used in this class even if no handler was found.
*/
self.seek = () => {};
self.pause = () => {};
/**
* @public
* Reset current state (time).
*
*/
self.resetTask = function () {
delete self.oldTime;
self.resetPlayback(parameters.startAt || 0);
};
/**
* Default implementation of resetPlayback. May be overridden by sub classes.
*
* @param {*} startAt
*/
self.resetPlayback = startAt => {
self.seek(startAt);
self.pause();
self.WAS_RESET = true;
};
// Resize the video when we know its aspect ratio
self.on('loaded', function () {
self.trigger('resize');
// reset time if wasn't done immediately
if (self.WAS_RESET) {
self.seek(parameters.startAt || 0);
if (!parameters.playback.autoplay) {
self.pause();
}
self.WAS_RESET = false;
}
});
// Find player for video sources
if (sources.length) {
const options = {
controls: parameters.visuals.controls,
autoplay: parameters.playback.autoplay,
loop: parameters.playback.loop,
fit: parameters.visuals.fit,
poster: parameters.visuals.poster === undefined ? undefined : parameters.visuals.poster,
tracks: tracks,
disableRemotePlayback: parameters.visuals.disableRemotePlayback === true,
disableFullscreen: parameters.visuals.disableFullscreen === true,
deactivateSound: parameters.playback.deactivateSound,
}
if (!self.WAS_RESET) {
options.startAt = self.oldTime !== undefined ? self.oldTime : (parameters.startAt || 0);
}
var html5Handler;
for (var i = 0; i < handlers.length; i++) {
var handler = handlers[i];
if (handler.canPlay !== undefined && handler.canPlay(sources)) {
handler.call(self, sources, options, parameters.l10n);
handlerName = handler.name;
return;
}
if (handler === H5P.VideoHtml5) {
html5Handler = handler;
handlerName = handler.name;
}
}
// Fallback to trying HTML5 player
if (html5Handler) {
html5Handler.call(self, sources, options, parameters.l10n);
}
}
}
// Extends the event dispatcher
Video.prototype = Object.create(H5P.EventDispatcher.prototype);
Video.prototype.constructor = Video;
// Player states
/** @constant {Number} */
Video.ENDED = 0;
/** @constant {Number} */
Video.PLAYING = 1;
/** @constant {Number} */
Video.PAUSED = 2;
/** @constant {Number} */
Video.BUFFERING = 3;
/**
* When video is queued to start
* @constant {Number}
*/
Video.VIDEO_CUED = 5;
// Used to convert between html and text, since URLs have html entities.
var $cleaner = H5P.jQuery('
');
/**
* Help keep track of key value pairs used by the UI.
*
* @class
* @param {string} label
* @param {string} value
*/
Video.LabelValue = function (label, value) {
this.label = label;
this.value = value;
};
/**
* Determine whether video can be autoplayed.
* @returns {Promise} Whether autoplay is allowed.
*/
Video.isAutoplayAllowed = async () => {
if (document.featurePolicy?.allowsFeature('autoplay')) {
return true; // Browser supports `featurePolicy` and can tell directly
}
const video = document.createElement('video');
/*
* Without a video source, the play Promise will be rejected with an error
* if it cannot be autoplayed, but not resolve at all if it can be
* autoplayed. Using a timeout to detect the latter case here.
*/
const timeoutMs = 50; // If play promise rejects, then within few ms
const timeoutPromise = new Promise((resolve) => {
window.setTimeout(() => {
resolve(true); // Timeout reached, autoplay is allowed
}, timeoutMs);
});
let result;
try {
result = (await Promise.race([video.play(), timeoutPromise])) ?? true;
} catch (error) {
result = false;
}
return result;
};
/** @constant {Boolean} */
Video.IE11_PLAYBACK_RATE_FIX = (navigator.userAgent.match(/Trident.*rv[ :]*11\./) ? true : false);
return Video;
})(H5P.jQuery, H5P.ContentCopyrights, H5P.MediaCopyright, H5P.videoHandlers || []);
;
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',
submitButton: 'Submit',
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',
navigationLabel: 'Questions'
},
endGame: {
showResultPage: true,
noResultMessage: 'Finished',
message: 'Your result:',
scoreBarLabel: 'You got @finals out of @totals points',
oldFeedback: {
successGreeting: '',
successComment: '',
failGreeting: '',
failComment: ''
},
overallFeedback: [],
finishButtonText: 'Finish',
submitButtonText: 'Submit',
solutionButtonText: 'Show solution',
retryButtonText: 'Retry',
showAnimations: false,
skipButtonText: 'Skip video',
showSolutionButton: true,
showRetryButton: true
},
override: {},
disableBackwardsNavigation: false
};
this.isSubmitting = contentData
&& (contentData.isScoringEnabled || contentData.isReportingEnabled);
var params = $.extend(true, {}, defaults, options);
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 || {};
// Need to check with isEmpty, as {} == true
this.hasPrevState = !H5P.isEmpty(contentData.previousState);
// Bring question set up to date when resuming
if (self.hasPrevState) {
if (contentData.previousState.progress !== undefined) {
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) {
// Use a previous order if it exists
if (self.hasPrevState && contentData.previousState.order) {
questions = questions.slice(0, questionOrder.length);
return {
questions: questions,
questionOrder: questionOrder,
}
}
// 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++) {
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 (self.hasPrevState && 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;
}
}
// 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 = self.hasPrevState && 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);
params.noOfQuestionAnswered = 0;
if (self.hasPrevState) {
// get numbers of questions answered by user
if (contentData.previousState.answers) {
for (var i = 0; i < questionInstances.length; i++) {
let answered = questionInstances[i].getAnswerGiven();
if (answered){
params.noOfQuestionAnswered++;
}
}
}
}
// Create html for intro page layout
self.$introPage = '';
if (params.introPage.showIntroPage && params.noOfQuestionAnswered === 0) {
self.$introPage = $('', {
class: 'intro-page'
});
if (params.introPage.title) {
$('
', {
class: 'title',
html: '
' +
params.introPage.title +
' ',
appendTo: self.$introPage
});
}
if (params.introPage.introduction) {
$('
', {
class: 'introduction',
html: params.introPage.introduction,
appendTo: self.$introPage
});
}
self.$introButtonsContainer = $('
', {
class: 'buttons',
appendTo: self.$introPage
});
$('
', {
class: 'qs-startbutton h5p-joubelui-button h5p-button',
html: params.introPage.startButtonText,
appendTo: self.$introButtonsContainer
});
}
// Create html for progress announcer
self.$progressAnnouncer = $('', {
class: 'qs-progress-announcer',
'aria-live': 'polite',
});
// Create html for questionset
self.$questionsContainer = $('
', {
class: 'questionset ' +
((params.introPage.showIntroPage && params.noOfQuestionAnswered === 0) ? 'hidden' : ''),
});
for (let i=0; i
', {
class: 'question-container',
appendTo: self.$questionsContainer
});
}
self.$footer = $('', {
class: 'qs-footer',
appendTo: self.$questionsContainer
});
self.$progressBar = $('
', {
class: 'qs-progress',
role: 'navigation',
'aria-label': params.texts.navigationLabel,
appendTo: self.$footer
});
if (params.progressType == "dots") {
self.$dotsContainer = $('
', {
class: 'dots-container',
appendTo: self.$progressBar
});
for (let i=0; i', {
class: 'progress-item',
html: ' ',
appendTo: self.$dotsContainer
})
}
}
else if (params.progressType == "textual") {
$('', {
class: 'progress-text',
appendTo: self.$progressBar
})
}
// Randomize questions only on instantiation
if (params.randomQuestions && !self.hasPrevState) {
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 _showQuestion = function (questionNumber, preventAnnouncement) {
// Sanitize input.
if (questionNumber < 0) {
questionNumber = 0;
}
if (questionNumber >= params.questions.length) {
questionNumber = params.questions.length - 1;
}
currentQuestion = questionNumber;
// 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());
self.$progressAnnouncer.html(humanizedProgress);
if (instance && instance.readFeedback) {
instance.readFeedback();
}
}, 0);
}
// Remember where we are
_updateButtons();
self.trigger('resize');
return currentQuestion;
};
/**
* 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
* @param {boolean} moveFocus True to move the focus to first option
* This prevents loss of focus if reset from within content
*/
this.resetTask = function (moveFocus = false) {
// Clear previous state to ensure questions are created cleanly
contentData.previousState = {};
self.hasPrevState = false;
questionOrder = undefined;
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();
}
// Reset currentQuestion
currentQuestion = 0;
$myDom.children().hide();
var $intro = $('.intro-page', $myDom);
if ($intro.length) {
// Show intro
$('.intro-page', $myDom).show();
if (moveFocus) {
$('.qs-startbutton', $myDom).focus();
}
}
else {
// Show first question
$('.questionset', $myDom).show();
_showQuestion(params.initialQuestion);
if (moveFocus) {
// Focus first tabbable element
$myDom[0].querySelectorAll(
'audio, button, input, select, textarea, video, [contenteditable], [href], [tabindex="0"]'
)[0].focus();
}
}
};
var rendered = false;
this.reRender = function () {
rendered = false;
};
/**
* Randomizes question instances
*/
var randomizeQuestions = function () {
// Recreate questioninstances in original order
questionInstances = createQuestionInstancesFromQuestions(params.questions);
// Scramble them
var result = randomizeQuestionOrdering(questionInstances);
questionInstances = result.questions;
questionOrder = result.questionOrder;
// Update buttons
initializeQuestion();
};
var moveQuestion = function (direction) {
if (params.disableBackwardsNavigation && !questionInstances[currentQuestion].getAnswerGiven()) {
questionInstances[currentQuestion].hideButton('next');
questionInstances[currentQuestion].hideButton('finish');
return;
}
if (currentQuestion + direction >= questionInstances.length) {
_displayEndGame();
}
else {
// Allow movement if backward navigation enabled or answer given
_showQuestion(currentQuestion + direction);
}
// Trigger xAPI
self.triggerXAPIProgressed();
};
/**
* 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: (self.isSubmitting) ? params.endGame.submitButtonText : params.endGame.finishButtonText,
solutionButtonText: params.endGame.solutionButtonText,
retryButtonText: params.endGame.retryButtonText
};
// Create html for end screen
self.$resultPage = $('', {
'class': 'questionset-results'
});
$('
', {
class: 'greeting',
html: eparams.message,
appendTo: self.$resultPage
});
$('
', {
class: 'feedback-section',
html: '
' +
'
',
appendTo: self.$resultPage
});
if (params.comment) {
$('
', {
'class': 'result-header',
html: eparams.comment,
appendTo: self.$resultPage
});
}
if (params.resulttext) {
$('
', {
class: 'result-text',
html: eparams.resulttext,
appendTo: self.$resultPage
});
}
self.$buttonsContainer = $('
', {
class: 'buttons',
appendTo: self.$resultPage
});
if (params.endGame.showSolutionButton) {
$('
', {
class: 'h5p-joubelui-button h5p-button qs-solutionbutton',
type: 'button',
html: eparams.solutionButtonText,
appendTo: self.$buttonsContainer
});
}
if (params.endGame.showRetryButton) {
$('', {
class: 'h5p-joubelui-button h5p-button qs-retrybutton',
type: 'button',
html: eparams.retryButtonText,
appendTo: self.$buttonsContainer
})
}
// Show result page.
$myDom.children().hide();
$myDom.append(self.$resultPage);
if (params.endGame.showResultPage) {
hookUpButton('.qs-solutionbutton', function () {
showSolutions();
$myDom.children().hide().filter('.questionset').show();
_showQuestion(params.initialQuestion);
});
hookUpButton('.qs-retrybutton', function () {
self.resetTask(true);
});
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 () {
self.$progressAnnouncer
.html(eparams.message +
scoreString + '.' +
(params.endGame.scoreBarLabel).replace('@finals', finals).replace('@totals', totals) + '.' +
eparams.comment + '.' +
eparams.resulttext);
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
const finishButtonText = (self.isSubmitting) ? params.texts.submitButton : params.texts.finishButton
question.addButton('finish', finishButtonText,
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 buttonq
'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(self.$introPage, self.$questionsContainer);
$myDom.parent().append(self.$progressAnnouncer);
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;
}
_showQuestion($(this).parent().index());
// Trigger xAPI
self.triggerXAPIProgressed();
};
// 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;
};
/**
* Returns the complete state of question set and sub-content
*
* @returns {Object} current state
*/
this.getCurrentState = function () {
const progress = showingSolutions ? questionInstances.length - 1 : currentQuestion;
const answers = questionInstances.map(function (qi) {
return qi.getCurrentState();
});
// If the user has moved past the first question, if the content has been resumed,
// or if at least one of the answers to the questions are considered not empty.
if (progress || self.hasPrevState || answers.some(answer => !H5P.isEmpty(answer))) {
return {
progress: progress,
answers: answers,
order: questionOrder,
poolOrder: poolOrder,
};
}
return {};
};
/**
* Trigger the xAPI progressed event
*/
this.triggerXAPIProgressed = function () {
const progressedEvent = this.createXAPIEventTemplate('progressed');
if (progressedEvent.data.statement.context.extensions === undefined) {
progressedEvent.data.statement.context.extensions = {};
}
progressedEvent.data.statement.context.extensions['http://id.tincanapi.com/extension/ending-point'] = currentQuestion + 1;
this.trigger(progressedEvent);
}
/**
* 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
let contextObject = {
type: 'question',
value: (currentQuestion + 1)
};
// Send actual index of the question if questions are randomized
if (params.randomQuestions) {
contextObject.actual = questionOrder[currentQuestion] + 1;
}
return contextObject;
};
};
H5P.QuestionSet.prototype = Object.create(H5P.EventDispatcher.prototype);
H5P.QuestionSet.prototype.constructor = H5P.QuestionSet;
;