", {
title: item.element.attr( "title" )
} );
if ( item.disabled ) {
this._addClass( li, null, "ui-state-disabled" );
}
this._setText( wrapper, item.label );
return li.append( wrapper ).appendTo( ul );
},
_setText: function( element, value ) {
if ( value ) {
element.text( value );
} else {
element.html( " " );
}
},
_move: function( direction, event ) {
var item, next,
filter = ".ui-menu-item";
if ( this.isOpen ) {
item = this.menuItems.eq( this.focusIndex ).parent( "li" );
} else {
item = this.menuItems.eq( this.element[ 0 ].selectedIndex ).parent( "li" );
filter += ":not(.ui-state-disabled)";
}
if ( direction === "first" || direction === "last" ) {
next = item[ direction === "first" ? "prevAll" : "nextAll" ]( filter ).eq( -1 );
} else {
next = item[ direction + "All" ]( filter ).eq( 0 );
}
if ( next.length ) {
this.menuInstance.focus( event, next );
}
},
_getSelectedItem: function() {
return this.menuItems.eq( this.element[ 0 ].selectedIndex ).parent( "li" );
},
_toggle: function( event ) {
this[ this.isOpen ? "close" : "open" ]( event );
},
_setSelection: function() {
var selection;
if ( !this.range ) {
return;
}
if ( window.getSelection ) {
selection = window.getSelection();
selection.removeAllRanges();
selection.addRange( this.range );
// Support: IE8
} else {
this.range.select();
}
// Support: IE
// Setting the text selection kills the button focus in IE, but
// restoring the focus doesn't kill the selection.
this.button.focus();
},
_documentClick: {
mousedown: function( event ) {
if ( !this.isOpen ) {
return;
}
if ( !$( event.target ).closest( ".ui-selectmenu-menu, #" +
$.escapeSelector( this.ids.button ) ).length ) {
this.close( event );
}
}
},
_buttonEvents: {
// Prevent text selection from being reset when interacting with the selectmenu (#10144)
mousedown: function() {
var selection;
if ( window.getSelection ) {
selection = window.getSelection();
if ( selection.rangeCount ) {
this.range = selection.getRangeAt( 0 );
}
// Support: IE8
} else {
this.range = document.selection.createRange();
}
},
click: function( event ) {
this._setSelection();
this._toggle( event );
},
keydown: function( event ) {
var preventDefault = true;
switch ( event.keyCode ) {
case $.ui.keyCode.TAB:
case $.ui.keyCode.ESCAPE:
this.close( event );
preventDefault = false;
break;
case $.ui.keyCode.ENTER:
if ( this.isOpen ) {
this._selectFocusedItem( event );
}
break;
case $.ui.keyCode.UP:
if ( event.altKey ) {
this._toggle( event );
} else {
this._move( "prev", event );
}
break;
case $.ui.keyCode.DOWN:
if ( event.altKey ) {
this._toggle( event );
} else {
this._move( "next", event );
}
break;
case $.ui.keyCode.SPACE:
if ( this.isOpen ) {
this._selectFocusedItem( event );
} else {
this._toggle( event );
}
break;
case $.ui.keyCode.LEFT:
this._move( "prev", event );
break;
case $.ui.keyCode.RIGHT:
this._move( "next", event );
break;
case $.ui.keyCode.HOME:
case $.ui.keyCode.PAGE_UP:
this._move( "first", event );
break;
case $.ui.keyCode.END:
case $.ui.keyCode.PAGE_DOWN:
this._move( "last", event );
break;
default:
this.menu.trigger( event );
preventDefault = false;
}
if ( preventDefault ) {
event.preventDefault();
}
}
},
_selectFocusedItem: function( event ) {
var item = this.menuItems.eq( this.focusIndex ).parent( "li" );
if ( !item.hasClass( "ui-state-disabled" ) ) {
this._select( item.data( "ui-selectmenu-item" ), event );
}
},
_select: function( item, event ) {
var oldIndex = this.element[ 0 ].selectedIndex;
// Change native select element
this.element[ 0 ].selectedIndex = item.index;
this.buttonItem.replaceWith( this.buttonItem = this._renderButtonItem( item ) );
this._setAria( item );
this._trigger( "select", event, { item: item } );
if ( item.index !== oldIndex ) {
this._trigger( "change", event, { item: item } );
}
this.close( event );
},
_setAria: function( item ) {
var id = this.menuItems.eq( item.index ).attr( "id" );
this.button.attr( {
"aria-labelledby": id,
"aria-activedescendant": id
} );
this.menu.attr( "aria-activedescendant", id );
},
_setOption: function( key, value ) {
if ( key === "icons" ) {
var icon = this.button.find( "span.ui-icon" );
this._removeClass( icon, null, this.options.icons.button )
._addClass( icon, null, value.button );
}
this._super( key, value );
if ( key === "appendTo" ) {
this.menuWrap.appendTo( this._appendTo() );
}
if ( key === "width" ) {
this._resizeButton();
}
},
_setOptionDisabled: function( value ) {
this._super( value );
this.menuInstance.option( "disabled", value );
this.button.attr( "aria-disabled", value );
this._toggleClass( this.button, null, "ui-state-disabled", value );
this.element.prop( "disabled", value );
if ( value ) {
this.button.attr( "tabindex", -1 );
this.close();
} else {
this.button.attr( "tabindex", 0 );
}
},
_appendTo: function() {
var element = this.options.appendTo;
if ( element ) {
element = element.jquery || element.nodeType ?
$( element ) :
this.document.find( element ).eq( 0 );
}
if ( !element || !element[ 0 ] ) {
element = this.element.closest( ".ui-front, dialog" );
}
if ( !element.length ) {
element = this.document[ 0 ].body;
}
return element;
},
_toggleAttr: function() {
this.button.attr( "aria-expanded", this.isOpen );
// We can't use two _toggleClass() calls here, because we need to make sure
// we always remove classes first and add them second, otherwise if both classes have the
// same theme class, it will be removed after we add it.
this._removeClass( this.button, "ui-selectmenu-button-" +
( this.isOpen ? "closed" : "open" ) )
._addClass( this.button, "ui-selectmenu-button-" +
( this.isOpen ? "open" : "closed" ) )
._toggleClass( this.menuWrap, "ui-selectmenu-open", null, this.isOpen );
this.menu.attr( "aria-hidden", !this.isOpen );
},
_resizeButton: function() {
var width = this.options.width;
// For `width: false`, just remove inline style and stop
if ( width === false ) {
this.button.css( "width", "" );
return;
}
// For `width: null`, match the width of the original element
if ( width === null ) {
width = this.element.show().outerWidth();
this.element.hide();
}
this.button.outerWidth( width );
},
_resizeMenu: function() {
this.menu.outerWidth( Math.max(
this.button.outerWidth(),
// Support: IE10
// IE10 wraps long text (possibly a rounding bug)
// so we add 1px to avoid the wrapping
this.menu.width( "" ).outerWidth() + 1
) );
},
_getCreateOptions: function() {
var options = this._super();
options.disabled = this.element.prop( "disabled" );
return options;
},
_parseOptions: function( options ) {
var that = this,
data = [];
options.each( function( index, item ) {
if ( item.hidden ) {
return;
}
data.push( that._parseOption( $( item ), index ) );
} );
this.items = data;
},
_parseOption: function( option, index ) {
var optgroup = option.parent( "optgroup" );
return {
element: option,
index: index,
value: option.val(),
label: option.text(),
optgroup: optgroup.attr( "label" ) || "",
disabled: optgroup.prop( "disabled" ) || option.prop( "disabled" )
};
},
_destroy: function() {
this._unbindFormResetHandler();
this.menuWrap.remove();
this.button.remove();
this.element.show();
this.element.removeUniqueId();
this.labels.attr( "for", this.ids.element );
}
} ] );
/*!
* jQuery UI Slider 1.13.0
* http://jqueryui.com
*
* Copyright jQuery Foundation and other contributors
* Released under the MIT license.
* http://jquery.org/license
*/
//>>label: Slider
//>>group: Widgets
//>>description: Displays a flexible slider with ranges and accessibility via keyboard.
//>>docs: http://api.jqueryui.com/slider/
//>>demos: http://jqueryui.com/slider/
//>>css.structure: ../../themes/base/core.css
//>>css.structure: ../../themes/base/slider.css
//>>css.theme: ../../themes/base/theme.css
var widgetsSlider = $.widget( "ui.slider", $.ui.mouse, {
version: "1.13.0",
widgetEventPrefix: "slide",
options: {
animate: false,
classes: {
"ui-slider": "ui-corner-all",
"ui-slider-handle": "ui-corner-all",
// Note: ui-widget-header isn't the most fittingly semantic framework class for this
// element, but worked best visually with a variety of themes
"ui-slider-range": "ui-corner-all ui-widget-header"
},
distance: 0,
max: 100,
min: 0,
orientation: "horizontal",
range: false,
step: 1,
value: 0,
values: null,
// Callbacks
change: null,
slide: null,
start: null,
stop: null
},
// Number of pages in a slider
// (how many times can you page up/down to go through the whole range)
numPages: 5,
_create: function() {
this._keySliding = false;
this._mouseSliding = false;
this._animateOff = true;
this._handleIndex = null;
this._detectOrientation();
this._mouseInit();
this._calculateNewMax();
this._addClass( "ui-slider ui-slider-" + this.orientation,
"ui-widget ui-widget-content" );
this._refresh();
this._animateOff = false;
},
_refresh: function() {
this._createRange();
this._createHandles();
this._setupEvents();
this._refreshValue();
},
_createHandles: function() {
var i, handleCount,
options = this.options,
existingHandles = this.element.find( ".ui-slider-handle" ),
handle = "
",
handles = [];
handleCount = ( options.values && options.values.length ) || 1;
if ( existingHandles.length > handleCount ) {
existingHandles.slice( handleCount ).remove();
existingHandles = existingHandles.slice( 0, handleCount );
}
for ( i = existingHandles.length; i < handleCount; i++ ) {
handles.push( handle );
}
this.handles = existingHandles.add( $( handles.join( "" ) ).appendTo( this.element ) );
this._addClass( this.handles, "ui-slider-handle", "ui-state-default" );
this.handle = this.handles.eq( 0 );
this.handles.each( function( i ) {
$( this )
.data( "ui-slider-handle-index", i )
.attr( "tabIndex", 0 );
} );
},
_createRange: function() {
var options = this.options;
if ( options.range ) {
if ( options.range === true ) {
if ( !options.values ) {
options.values = [ this._valueMin(), this._valueMin() ];
} else if ( options.values.length && options.values.length !== 2 ) {
options.values = [ options.values[ 0 ], options.values[ 0 ] ];
} else if ( Array.isArray( options.values ) ) {
options.values = options.values.slice( 0 );
}
}
if ( !this.range || !this.range.length ) {
this.range = $( "
" )
.appendTo( this.element );
this._addClass( this.range, "ui-slider-range" );
} else {
this._removeClass( this.range, "ui-slider-range-min ui-slider-range-max" );
// Handle range switching from true to min/max
this.range.css( {
"left": "",
"bottom": ""
} );
}
if ( options.range === "min" || options.range === "max" ) {
this._addClass( this.range, "ui-slider-range-" + options.range );
}
} else {
if ( this.range ) {
this.range.remove();
}
this.range = null;
}
},
_setupEvents: function() {
this._off( this.handles );
this._on( this.handles, this._handleEvents );
this._hoverable( this.handles );
this._focusable( this.handles );
},
_destroy: function() {
this.handles.remove();
if ( this.range ) {
this.range.remove();
}
this._mouseDestroy();
},
_mouseCapture: function( event ) {
var position, normValue, distance, closestHandle, index, allowed, offset, mouseOverHandle,
that = this,
o = this.options;
if ( o.disabled ) {
return false;
}
this.elementSize = {
width: this.element.outerWidth(),
height: this.element.outerHeight()
};
this.elementOffset = this.element.offset();
position = { x: event.pageX, y: event.pageY };
normValue = this._normValueFromMouse( position );
distance = this._valueMax() - this._valueMin() + 1;
this.handles.each( function( i ) {
var thisDistance = Math.abs( normValue - that.values( i ) );
if ( ( distance > thisDistance ) ||
( distance === thisDistance &&
( i === that._lastChangedValue || that.values( i ) === o.min ) ) ) {
distance = thisDistance;
closestHandle = $( this );
index = i;
}
} );
allowed = this._start( event, index );
if ( allowed === false ) {
return false;
}
this._mouseSliding = true;
this._handleIndex = index;
this._addClass( closestHandle, null, "ui-state-active" );
closestHandle.trigger( "focus" );
offset = closestHandle.offset();
mouseOverHandle = !$( event.target ).parents().addBack().is( ".ui-slider-handle" );
this._clickOffset = mouseOverHandle ? { left: 0, top: 0 } : {
left: event.pageX - offset.left - ( closestHandle.width() / 2 ),
top: event.pageY - offset.top -
( closestHandle.height() / 2 ) -
( parseInt( closestHandle.css( "borderTopWidth" ), 10 ) || 0 ) -
( parseInt( closestHandle.css( "borderBottomWidth" ), 10 ) || 0 ) +
( parseInt( closestHandle.css( "marginTop" ), 10 ) || 0 )
};
if ( !this.handles.hasClass( "ui-state-hover" ) ) {
this._slide( event, index, normValue );
}
this._animateOff = true;
return true;
},
_mouseStart: function() {
return true;
},
_mouseDrag: function( event ) {
var position = { x: event.pageX, y: event.pageY },
normValue = this._normValueFromMouse( position );
this._slide( event, this._handleIndex, normValue );
return false;
},
_mouseStop: function( event ) {
this._removeClass( this.handles, null, "ui-state-active" );
this._mouseSliding = false;
this._stop( event, this._handleIndex );
this._change( event, this._handleIndex );
this._handleIndex = null;
this._clickOffset = null;
this._animateOff = false;
return false;
},
_detectOrientation: function() {
this.orientation = ( this.options.orientation === "vertical" ) ? "vertical" : "horizontal";
},
_normValueFromMouse: function( position ) {
var pixelTotal,
pixelMouse,
percentMouse,
valueTotal,
valueMouse;
if ( this.orientation === "horizontal" ) {
pixelTotal = this.elementSize.width;
pixelMouse = position.x - this.elementOffset.left -
( this._clickOffset ? this._clickOffset.left : 0 );
} else {
pixelTotal = this.elementSize.height;
pixelMouse = position.y - this.elementOffset.top -
( this._clickOffset ? this._clickOffset.top : 0 );
}
percentMouse = ( pixelMouse / pixelTotal );
if ( percentMouse > 1 ) {
percentMouse = 1;
}
if ( percentMouse < 0 ) {
percentMouse = 0;
}
if ( this.orientation === "vertical" ) {
percentMouse = 1 - percentMouse;
}
valueTotal = this._valueMax() - this._valueMin();
valueMouse = this._valueMin() + percentMouse * valueTotal;
return this._trimAlignValue( valueMouse );
},
_uiHash: function( index, value, values ) {
var uiHash = {
handle: this.handles[ index ],
handleIndex: index,
value: value !== undefined ? value : this.value()
};
if ( this._hasMultipleValues() ) {
uiHash.value = value !== undefined ? value : this.values( index );
uiHash.values = values || this.values();
}
return uiHash;
},
_hasMultipleValues: function() {
return this.options.values && this.options.values.length;
},
_start: function( event, index ) {
return this._trigger( "start", event, this._uiHash( index ) );
},
_slide: function( event, index, newVal ) {
var allowed, otherVal,
currentValue = this.value(),
newValues = this.values();
if ( this._hasMultipleValues() ) {
otherVal = this.values( index ? 0 : 1 );
currentValue = this.values( index );
if ( this.options.values.length === 2 && this.options.range === true ) {
newVal = index === 0 ? Math.min( otherVal, newVal ) : Math.max( otherVal, newVal );
}
newValues[ index ] = newVal;
}
if ( newVal === currentValue ) {
return;
}
allowed = this._trigger( "slide", event, this._uiHash( index, newVal, newValues ) );
// A slide can be canceled by returning false from the slide callback
if ( allowed === false ) {
return;
}
if ( this._hasMultipleValues() ) {
this.values( index, newVal );
} else {
this.value( newVal );
}
},
_stop: function( event, index ) {
this._trigger( "stop", event, this._uiHash( index ) );
},
_change: function( event, index ) {
if ( !this._keySliding && !this._mouseSliding ) {
//store the last changed value index for reference when handles overlap
this._lastChangedValue = index;
this._trigger( "change", event, this._uiHash( index ) );
}
},
value: function( newValue ) {
if ( arguments.length ) {
this.options.value = this._trimAlignValue( newValue );
this._refreshValue();
this._change( null, 0 );
return;
}
return this._value();
},
values: function( index, newValue ) {
var vals,
newValues,
i;
if ( arguments.length > 1 ) {
this.options.values[ index ] = this._trimAlignValue( newValue );
this._refreshValue();
this._change( null, index );
return;
}
if ( arguments.length ) {
if ( Array.isArray( arguments[ 0 ] ) ) {
vals = this.options.values;
newValues = arguments[ 0 ];
for ( i = 0; i < vals.length; i += 1 ) {
vals[ i ] = this._trimAlignValue( newValues[ i ] );
this._change( null, i );
}
this._refreshValue();
} else {
if ( this._hasMultipleValues() ) {
return this._values( index );
} else {
return this.value();
}
}
} else {
return this._values();
}
},
_setOption: function( key, value ) {
var i,
valsLength = 0;
if ( key === "range" && this.options.range === true ) {
if ( value === "min" ) {
this.options.value = this._values( 0 );
this.options.values = null;
} else if ( value === "max" ) {
this.options.value = this._values( this.options.values.length - 1 );
this.options.values = null;
}
}
if ( Array.isArray( this.options.values ) ) {
valsLength = this.options.values.length;
}
this._super( key, value );
switch ( key ) {
case "orientation":
this._detectOrientation();
this._removeClass( "ui-slider-horizontal ui-slider-vertical" )
._addClass( "ui-slider-" + this.orientation );
this._refreshValue();
if ( this.options.range ) {
this._refreshRange( value );
}
// Reset positioning from previous orientation
this.handles.css( value === "horizontal" ? "bottom" : "left", "" );
break;
case "value":
this._animateOff = true;
this._refreshValue();
this._change( null, 0 );
this._animateOff = false;
break;
case "values":
this._animateOff = true;
this._refreshValue();
// Start from the last handle to prevent unreachable handles (#9046)
for ( i = valsLength - 1; i >= 0; i-- ) {
this._change( null, i );
}
this._animateOff = false;
break;
case "step":
case "min":
case "max":
this._animateOff = true;
this._calculateNewMax();
this._refreshValue();
this._animateOff = false;
break;
case "range":
this._animateOff = true;
this._refresh();
this._animateOff = false;
break;
}
},
_setOptionDisabled: function( value ) {
this._super( value );
this._toggleClass( null, "ui-state-disabled", !!value );
},
//internal value getter
// _value() returns value trimmed by min and max, aligned by step
_value: function() {
var val = this.options.value;
val = this._trimAlignValue( val );
return val;
},
//internal values getter
// _values() returns array of values trimmed by min and max, aligned by step
// _values( index ) returns single value trimmed by min and max, aligned by step
_values: function( index ) {
var val,
vals,
i;
if ( arguments.length ) {
val = this.options.values[ index ];
val = this._trimAlignValue( val );
return val;
} else if ( this._hasMultipleValues() ) {
// .slice() creates a copy of the array
// this copy gets trimmed by min and max and then returned
vals = this.options.values.slice();
for ( i = 0; i < vals.length; i += 1 ) {
vals[ i ] = this._trimAlignValue( vals[ i ] );
}
return vals;
} else {
return [];
}
},
// Returns the step-aligned value that val is closest to, between (inclusive) min and max
_trimAlignValue: function( val ) {
if ( val <= this._valueMin() ) {
return this._valueMin();
}
if ( val >= this._valueMax() ) {
return this._valueMax();
}
var step = ( this.options.step > 0 ) ? this.options.step : 1,
valModStep = ( val - this._valueMin() ) % step,
alignValue = val - valModStep;
if ( Math.abs( valModStep ) * 2 >= step ) {
alignValue += ( valModStep > 0 ) ? step : ( -step );
}
// Since JavaScript has problems with large floats, round
// the final value to 5 digits after the decimal point (see #4124)
return parseFloat( alignValue.toFixed( 5 ) );
},
_calculateNewMax: function() {
var max = this.options.max,
min = this._valueMin(),
step = this.options.step,
aboveMin = Math.round( ( max - min ) / step ) * step;
max = aboveMin + min;
if ( max > this.options.max ) {
//If max is not divisible by step, rounding off may increase its value
max -= step;
}
this.max = parseFloat( max.toFixed( this._precision() ) );
},
_precision: function() {
var precision = this._precisionOf( this.options.step );
if ( this.options.min !== null ) {
precision = Math.max( precision, this._precisionOf( this.options.min ) );
}
return precision;
},
_precisionOf: function( num ) {
var str = num.toString(),
decimal = str.indexOf( "." );
return decimal === -1 ? 0 : str.length - decimal - 1;
},
_valueMin: function() {
return this.options.min;
},
_valueMax: function() {
return this.max;
},
_refreshRange: function( orientation ) {
if ( orientation === "vertical" ) {
this.range.css( { "width": "", "left": "" } );
}
if ( orientation === "horizontal" ) {
this.range.css( { "height": "", "bottom": "" } );
}
},
_refreshValue: function() {
var lastValPercent, valPercent, value, valueMin, valueMax,
oRange = this.options.range,
o = this.options,
that = this,
animate = ( !this._animateOff ) ? o.animate : false,
_set = {};
if ( this._hasMultipleValues() ) {
this.handles.each( function( i ) {
valPercent = ( that.values( i ) - that._valueMin() ) / ( that._valueMax() -
that._valueMin() ) * 100;
_set[ that.orientation === "horizontal" ? "left" : "bottom" ] = valPercent + "%";
$( this ).stop( 1, 1 )[ animate ? "animate" : "css" ]( _set, o.animate );
if ( that.options.range === true ) {
if ( that.orientation === "horizontal" ) {
if ( i === 0 ) {
that.range.stop( 1, 1 )[ animate ? "animate" : "css" ]( {
left: valPercent + "%"
}, o.animate );
}
if ( i === 1 ) {
that.range[ animate ? "animate" : "css" ]( {
width: ( valPercent - lastValPercent ) + "%"
}, {
queue: false,
duration: o.animate
} );
}
} else {
if ( i === 0 ) {
that.range.stop( 1, 1 )[ animate ? "animate" : "css" ]( {
bottom: ( valPercent ) + "%"
}, o.animate );
}
if ( i === 1 ) {
that.range[ animate ? "animate" : "css" ]( {
height: ( valPercent - lastValPercent ) + "%"
}, {
queue: false,
duration: o.animate
} );
}
}
}
lastValPercent = valPercent;
} );
} else {
value = this.value();
valueMin = this._valueMin();
valueMax = this._valueMax();
valPercent = ( valueMax !== valueMin ) ?
( value - valueMin ) / ( valueMax - valueMin ) * 100 :
0;
_set[ this.orientation === "horizontal" ? "left" : "bottom" ] = valPercent + "%";
this.handle.stop( 1, 1 )[ animate ? "animate" : "css" ]( _set, o.animate );
if ( oRange === "min" && this.orientation === "horizontal" ) {
this.range.stop( 1, 1 )[ animate ? "animate" : "css" ]( {
width: valPercent + "%"
}, o.animate );
}
if ( oRange === "max" && this.orientation === "horizontal" ) {
this.range.stop( 1, 1 )[ animate ? "animate" : "css" ]( {
width: ( 100 - valPercent ) + "%"
}, o.animate );
}
if ( oRange === "min" && this.orientation === "vertical" ) {
this.range.stop( 1, 1 )[ animate ? "animate" : "css" ]( {
height: valPercent + "%"
}, o.animate );
}
if ( oRange === "max" && this.orientation === "vertical" ) {
this.range.stop( 1, 1 )[ animate ? "animate" : "css" ]( {
height: ( 100 - valPercent ) + "%"
}, o.animate );
}
}
},
_handleEvents: {
keydown: function( event ) {
var allowed, curVal, newVal, step,
index = $( event.target ).data( "ui-slider-handle-index" );
switch ( event.keyCode ) {
case $.ui.keyCode.HOME:
case $.ui.keyCode.END:
case $.ui.keyCode.PAGE_UP:
case $.ui.keyCode.PAGE_DOWN:
case $.ui.keyCode.UP:
case $.ui.keyCode.RIGHT:
case $.ui.keyCode.DOWN:
case $.ui.keyCode.LEFT:
event.preventDefault();
if ( !this._keySliding ) {
this._keySliding = true;
this._addClass( $( event.target ), null, "ui-state-active" );
allowed = this._start( event, index );
if ( allowed === false ) {
return;
}
}
break;
}
step = this.options.step;
if ( this._hasMultipleValues() ) {
curVal = newVal = this.values( index );
} else {
curVal = newVal = this.value();
}
switch ( event.keyCode ) {
case $.ui.keyCode.HOME:
newVal = this._valueMin();
break;
case $.ui.keyCode.END:
newVal = this._valueMax();
break;
case $.ui.keyCode.PAGE_UP:
newVal = this._trimAlignValue(
curVal + ( ( this._valueMax() - this._valueMin() ) / this.numPages )
);
break;
case $.ui.keyCode.PAGE_DOWN:
newVal = this._trimAlignValue(
curVal - ( ( this._valueMax() - this._valueMin() ) / this.numPages ) );
break;
case $.ui.keyCode.UP:
case $.ui.keyCode.RIGHT:
if ( curVal === this._valueMax() ) {
return;
}
newVal = this._trimAlignValue( curVal + step );
break;
case $.ui.keyCode.DOWN:
case $.ui.keyCode.LEFT:
if ( curVal === this._valueMin() ) {
return;
}
newVal = this._trimAlignValue( curVal - step );
break;
}
this._slide( event, index, newVal );
},
keyup: function( event ) {
var index = $( event.target ).data( "ui-slider-handle-index" );
if ( this._keySliding ) {
this._keySliding = false;
this._stop( event, index );
this._change( event, index );
this._removeClass( $( event.target ), null, "ui-state-active" );
}
}
}
} );
/*!
* jQuery UI Sortable 1.13.0
* http://jqueryui.com
*
* Copyright jQuery Foundation and other contributors
* Released under the MIT license.
* http://jquery.org/license
*/
//>>label: Sortable
//>>group: Interactions
//>>description: Enables items in a list to be sorted using the mouse.
//>>docs: http://api.jqueryui.com/sortable/
//>>demos: http://jqueryui.com/sortable/
//>>css.structure: ../../themes/base/sortable.css
var widgetsSortable = $.widget( "ui.sortable", $.ui.mouse, {
version: "1.13.0",
widgetEventPrefix: "sort",
ready: false,
options: {
appendTo: "parent",
axis: false,
connectWith: false,
containment: false,
cursor: "auto",
cursorAt: false,
dropOnEmpty: true,
forcePlaceholderSize: false,
forceHelperSize: false,
grid: false,
handle: false,
helper: "original",
items: "> *",
opacity: false,
placeholder: false,
revert: false,
scroll: true,
scrollSensitivity: 20,
scrollSpeed: 20,
scope: "default",
tolerance: "intersect",
zIndex: 1000,
// Callbacks
activate: null,
beforeStop: null,
change: null,
deactivate: null,
out: null,
over: null,
receive: null,
remove: null,
sort: null,
start: null,
stop: null,
update: null
},
_isOverAxis: function( x, reference, size ) {
return ( x >= reference ) && ( x < ( reference + size ) );
},
_isFloating: function( item ) {
return ( /left|right/ ).test( item.css( "float" ) ) ||
( /inline|table-cell/ ).test( item.css( "display" ) );
},
_create: function() {
this.containerCache = {};
this._addClass( "ui-sortable" );
//Get the items
this.refresh();
//Let's determine the parent's offset
this.offset = this.element.offset();
//Initialize mouse events for interaction
this._mouseInit();
this._setHandleClassName();
//We're ready to go
this.ready = true;
},
_setOption: function( key, value ) {
this._super( key, value );
if ( key === "handle" ) {
this._setHandleClassName();
}
},
_setHandleClassName: function() {
var that = this;
this._removeClass( this.element.find( ".ui-sortable-handle" ), "ui-sortable-handle" );
$.each( this.items, function() {
that._addClass(
this.instance.options.handle ?
this.item.find( this.instance.options.handle ) :
this.item,
"ui-sortable-handle"
);
} );
},
_destroy: function() {
this._mouseDestroy();
for ( var i = this.items.length - 1; i >= 0; i-- ) {
this.items[ i ].item.removeData( this.widgetName + "-item" );
}
return this;
},
_mouseCapture: function( event, overrideHandle ) {
var currentItem = null,
validHandle = false,
that = this;
if ( this.reverting ) {
return false;
}
if ( this.options.disabled || this.options.type === "static" ) {
return false;
}
//We have to refresh the items data once first
this._refreshItems( event );
//Find out if the clicked node (or one of its parents) is a actual item in this.items
$( event.target ).parents().each( function() {
if ( $.data( this, that.widgetName + "-item" ) === that ) {
currentItem = $( this );
return false;
}
} );
if ( $.data( event.target, that.widgetName + "-item" ) === that ) {
currentItem = $( event.target );
}
if ( !currentItem ) {
return false;
}
if ( this.options.handle && !overrideHandle ) {
$( this.options.handle, currentItem ).find( "*" ).addBack().each( function() {
if ( this === event.target ) {
validHandle = true;
}
} );
if ( !validHandle ) {
return false;
}
}
this.currentItem = currentItem;
this._removeCurrentsFromItems();
return true;
},
_mouseStart: function( event, overrideHandle, noActivation ) {
var i, body,
o = this.options;
this.currentContainer = this;
//We only need to call refreshPositions, because the refreshItems call has been moved to
// mouseCapture
this.refreshPositions();
//Prepare the dragged items parent
this.appendTo = $( o.appendTo !== "parent" ?
o.appendTo :
this.currentItem.parent() );
//Create and append the visible helper
this.helper = this._createHelper( event );
//Cache the helper size
this._cacheHelperProportions();
/*
* - Position generation -
* This block generates everything position related - it's the core of draggables.
*/
//Cache the margins of the original element
this._cacheMargins();
//The element's absolute position on the page minus margins
this.offset = this.currentItem.offset();
this.offset = {
top: this.offset.top - this.margins.top,
left: this.offset.left - this.margins.left
};
$.extend( this.offset, {
click: { //Where the click happened, relative to the element
left: event.pageX - this.offset.left,
top: event.pageY - this.offset.top
},
// This is a relative to absolute position minus the actual position calculation -
// only used for relative positioned helper
relative: this._getRelativeOffset()
} );
// After we get the helper offset, but before we get the parent offset we can
// change the helper's position to absolute
// TODO: Still need to figure out a way to make relative sorting possible
this.helper.css( "position", "absolute" );
this.cssPosition = this.helper.css( "position" );
//Adjust the mouse offset relative to the helper if "cursorAt" is supplied
if ( o.cursorAt ) {
this._adjustOffsetFromHelper( o.cursorAt );
}
//Cache the former DOM position
this.domPosition = {
prev: this.currentItem.prev()[ 0 ],
parent: this.currentItem.parent()[ 0 ]
};
// If the helper is not the original, hide the original so it's not playing any role during
// the drag, won't cause anything bad this way
if ( this.helper[ 0 ] !== this.currentItem[ 0 ] ) {
this.currentItem.hide();
}
//Create the placeholder
this._createPlaceholder();
//Get the next scrolling parent
this.scrollParent = this.placeholder.scrollParent();
$.extend( this.offset, {
parent: this._getParentOffset()
} );
//Set a containment if given in the options
if ( o.containment ) {
this._setContainment();
}
if ( o.cursor && o.cursor !== "auto" ) { // cursor option
body = this.document.find( "body" );
// Support: IE
this.storedCursor = body.css( "cursor" );
body.css( "cursor", o.cursor );
this.storedStylesheet =
$( "" ).appendTo( body );
}
// We need to make sure to grab the zIndex before setting the
// opacity, because setting the opacity to anything lower than 1
// causes the zIndex to change from "auto" to 0.
if ( o.zIndex ) { // zIndex option
if ( this.helper.css( "zIndex" ) ) {
this._storedZIndex = this.helper.css( "zIndex" );
}
this.helper.css( "zIndex", o.zIndex );
}
if ( o.opacity ) { // opacity option
if ( this.helper.css( "opacity" ) ) {
this._storedOpacity = this.helper.css( "opacity" );
}
this.helper.css( "opacity", o.opacity );
}
//Prepare scrolling
if ( this.scrollParent[ 0 ] !== this.document[ 0 ] &&
this.scrollParent[ 0 ].tagName !== "HTML" ) {
this.overflowOffset = this.scrollParent.offset();
}
//Call callbacks
this._trigger( "start", event, this._uiHash() );
//Recache the helper size
if ( !this._preserveHelperProportions ) {
this._cacheHelperProportions();
}
//Post "activate" events to possible containers
if ( !noActivation ) {
for ( i = this.containers.length - 1; i >= 0; i-- ) {
this.containers[ i ]._trigger( "activate", event, this._uiHash( this ) );
}
}
//Prepare possible droppables
if ( $.ui.ddmanager ) {
$.ui.ddmanager.current = this;
}
if ( $.ui.ddmanager && !o.dropBehaviour ) {
$.ui.ddmanager.prepareOffsets( this, event );
}
this.dragging = true;
this._addClass( this.helper, "ui-sortable-helper" );
//Move the helper, if needed
if ( !this.helper.parent().is( this.appendTo ) ) {
this.helper.detach().appendTo( this.appendTo );
//Update position
this.offset.parent = this._getParentOffset();
}
//Generate the original position
this.position = this.originalPosition = this._generatePosition( event );
this.originalPageX = event.pageX;
this.originalPageY = event.pageY;
this.lastPositionAbs = this.positionAbs = this._convertPositionTo( "absolute" );
this._mouseDrag( event );
return true;
},
_scroll: function( event ) {
var o = this.options,
scrolled = false;
if ( this.scrollParent[ 0 ] !== this.document[ 0 ] &&
this.scrollParent[ 0 ].tagName !== "HTML" ) {
if ( ( this.overflowOffset.top + this.scrollParent[ 0 ].offsetHeight ) -
event.pageY < o.scrollSensitivity ) {
this.scrollParent[ 0 ].scrollTop =
scrolled = this.scrollParent[ 0 ].scrollTop + o.scrollSpeed;
} else if ( event.pageY - this.overflowOffset.top < o.scrollSensitivity ) {
this.scrollParent[ 0 ].scrollTop =
scrolled = this.scrollParent[ 0 ].scrollTop - o.scrollSpeed;
}
if ( ( this.overflowOffset.left + this.scrollParent[ 0 ].offsetWidth ) -
event.pageX < o.scrollSensitivity ) {
this.scrollParent[ 0 ].scrollLeft = scrolled =
this.scrollParent[ 0 ].scrollLeft + o.scrollSpeed;
} else if ( event.pageX - this.overflowOffset.left < o.scrollSensitivity ) {
this.scrollParent[ 0 ].scrollLeft = scrolled =
this.scrollParent[ 0 ].scrollLeft - o.scrollSpeed;
}
} else {
if ( event.pageY - this.document.scrollTop() < o.scrollSensitivity ) {
scrolled = this.document.scrollTop( this.document.scrollTop() - o.scrollSpeed );
} else if ( this.window.height() - ( event.pageY - this.document.scrollTop() ) <
o.scrollSensitivity ) {
scrolled = this.document.scrollTop( this.document.scrollTop() + o.scrollSpeed );
}
if ( event.pageX - this.document.scrollLeft() < o.scrollSensitivity ) {
scrolled = this.document.scrollLeft(
this.document.scrollLeft() - o.scrollSpeed
);
} else if ( this.window.width() - ( event.pageX - this.document.scrollLeft() ) <
o.scrollSensitivity ) {
scrolled = this.document.scrollLeft(
this.document.scrollLeft() + o.scrollSpeed
);
}
}
return scrolled;
},
_mouseDrag: function( event ) {
var i, item, itemElement, intersection,
o = this.options;
//Compute the helpers position
this.position = this._generatePosition( event );
this.positionAbs = this._convertPositionTo( "absolute" );
//Set the helper position
if ( !this.options.axis || this.options.axis !== "y" ) {
this.helper[ 0 ].style.left = this.position.left + "px";
}
if ( !this.options.axis || this.options.axis !== "x" ) {
this.helper[ 0 ].style.top = this.position.top + "px";
}
//Post events to containers
this._contactContainers( event );
if ( this.innermostContainer !== null ) {
//Do scrolling
if ( o.scroll ) {
if ( this._scroll( event ) !== false ) {
//Update item positions used in position checks
this._refreshItemPositions( true );
if ( $.ui.ddmanager && !o.dropBehaviour ) {
$.ui.ddmanager.prepareOffsets( this, event );
}
}
}
this.dragDirection = {
vertical: this._getDragVerticalDirection(),
horizontal: this._getDragHorizontalDirection()
};
//Rearrange
for ( i = this.items.length - 1; i >= 0; i-- ) {
//Cache variables and intersection, continue if no intersection
item = this.items[ i ];
itemElement = item.item[ 0 ];
intersection = this._intersectsWithPointer( item );
if ( !intersection ) {
continue;
}
// Only put the placeholder inside the current Container, skip all
// items from other containers. This works because when moving
// an item from one container to another the
// currentContainer is switched before the placeholder is moved.
//
// Without this, moving items in "sub-sortables" can cause
// the placeholder to jitter between the outer and inner container.
if ( item.instance !== this.currentContainer ) {
continue;
}
// Cannot intersect with itself
// no useless actions that have been done before
// no action if the item moved is the parent of the item checked
if ( itemElement !== this.currentItem[ 0 ] &&
this.placeholder[ intersection === 1 ?
"next" : "prev" ]()[ 0 ] !== itemElement &&
!$.contains( this.placeholder[ 0 ], itemElement ) &&
( this.options.type === "semi-dynamic" ?
!$.contains( this.element[ 0 ], itemElement ) :
true
)
) {
this.direction = intersection === 1 ? "down" : "up";
if ( this.options.tolerance === "pointer" ||
this._intersectsWithSides( item ) ) {
this._rearrange( event, item );
} else {
break;
}
this._trigger( "change", event, this._uiHash() );
break;
}
}
}
//Interconnect with droppables
if ( $.ui.ddmanager ) {
$.ui.ddmanager.drag( this, event );
}
//Call callbacks
this._trigger( "sort", event, this._uiHash() );
this.lastPositionAbs = this.positionAbs;
return false;
},
_mouseStop: function( event, noPropagation ) {
if ( !event ) {
return;
}
//If we are using droppables, inform the manager about the drop
if ( $.ui.ddmanager && !this.options.dropBehaviour ) {
$.ui.ddmanager.drop( this, event );
}
if ( this.options.revert ) {
var that = this,
cur = this.placeholder.offset(),
axis = this.options.axis,
animation = {};
if ( !axis || axis === "x" ) {
animation.left = cur.left - this.offset.parent.left - this.margins.left +
( this.offsetParent[ 0 ] === this.document[ 0 ].body ?
0 :
this.offsetParent[ 0 ].scrollLeft
);
}
if ( !axis || axis === "y" ) {
animation.top = cur.top - this.offset.parent.top - this.margins.top +
( this.offsetParent[ 0 ] === this.document[ 0 ].body ?
0 :
this.offsetParent[ 0 ].scrollTop
);
}
this.reverting = true;
$( this.helper ).animate(
animation,
parseInt( this.options.revert, 10 ) || 500,
function() {
that._clear( event );
}
);
} else {
this._clear( event, noPropagation );
}
return false;
},
cancel: function() {
if ( this.dragging ) {
this._mouseUp( new $.Event( "mouseup", { target: null } ) );
if ( this.options.helper === "original" ) {
this.currentItem.css( this._storedCSS );
this._removeClass( this.currentItem, "ui-sortable-helper" );
} else {
this.currentItem.show();
}
//Post deactivating events to containers
for ( var i = this.containers.length - 1; i >= 0; i-- ) {
this.containers[ i ]._trigger( "deactivate", null, this._uiHash( this ) );
if ( this.containers[ i ].containerCache.over ) {
this.containers[ i ]._trigger( "out", null, this._uiHash( this ) );
this.containers[ i ].containerCache.over = 0;
}
}
}
if ( this.placeholder ) {
//$(this.placeholder[0]).remove(); would have been the jQuery way - unfortunately,
// it unbinds ALL events from the original node!
if ( this.placeholder[ 0 ].parentNode ) {
this.placeholder[ 0 ].parentNode.removeChild( this.placeholder[ 0 ] );
}
if ( this.options.helper !== "original" && this.helper &&
this.helper[ 0 ].parentNode ) {
this.helper.remove();
}
$.extend( this, {
helper: null,
dragging: false,
reverting: false,
_noFinalSort: null
} );
if ( this.domPosition.prev ) {
$( this.domPosition.prev ).after( this.currentItem );
} else {
$( this.domPosition.parent ).prepend( this.currentItem );
}
}
return this;
},
serialize: function( o ) {
var items = this._getItemsAsjQuery( o && o.connected ),
str = [];
o = o || {};
$( items ).each( function() {
var res = ( $( o.item || this ).attr( o.attribute || "id" ) || "" )
.match( o.expression || ( /(.+)[\-=_](.+)/ ) );
if ( res ) {
str.push(
( o.key || res[ 1 ] + "[]" ) +
"=" + ( o.key && o.expression ? res[ 1 ] : res[ 2 ] ) );
}
} );
if ( !str.length && o.key ) {
str.push( o.key + "=" );
}
return str.join( "&" );
},
toArray: function( o ) {
var items = this._getItemsAsjQuery( o && o.connected ),
ret = [];
o = o || {};
items.each( function() {
ret.push( $( o.item || this ).attr( o.attribute || "id" ) || "" );
} );
return ret;
},
/* Be careful with the following core functions */
_intersectsWith: function( item ) {
var x1 = this.positionAbs.left,
x2 = x1 + this.helperProportions.width,
y1 = this.positionAbs.top,
y2 = y1 + this.helperProportions.height,
l = item.left,
r = l + item.width,
t = item.top,
b = t + item.height,
dyClick = this.offset.click.top,
dxClick = this.offset.click.left,
isOverElementHeight = ( this.options.axis === "x" ) || ( ( y1 + dyClick ) > t &&
( y1 + dyClick ) < b ),
isOverElementWidth = ( this.options.axis === "y" ) || ( ( x1 + dxClick ) > l &&
( x1 + dxClick ) < r ),
isOverElement = isOverElementHeight && isOverElementWidth;
if ( this.options.tolerance === "pointer" ||
this.options.forcePointerForContainers ||
( this.options.tolerance !== "pointer" &&
this.helperProportions[ this.floating ? "width" : "height" ] >
item[ this.floating ? "width" : "height" ] )
) {
return isOverElement;
} else {
return ( l < x1 + ( this.helperProportions.width / 2 ) && // Right Half
x2 - ( this.helperProportions.width / 2 ) < r && // Left Half
t < y1 + ( this.helperProportions.height / 2 ) && // Bottom Half
y2 - ( this.helperProportions.height / 2 ) < b ); // Top Half
}
},
_intersectsWithPointer: function( item ) {
var verticalDirection, horizontalDirection,
isOverElementHeight = ( this.options.axis === "x" ) ||
this._isOverAxis(
this.positionAbs.top + this.offset.click.top, item.top, item.height ),
isOverElementWidth = ( this.options.axis === "y" ) ||
this._isOverAxis(
this.positionAbs.left + this.offset.click.left, item.left, item.width ),
isOverElement = isOverElementHeight && isOverElementWidth;
if ( !isOverElement ) {
return false;
}
verticalDirection = this.dragDirection.vertical;
horizontalDirection = this.dragDirection.horizontal;
return this.floating ?
( ( horizontalDirection === "right" || verticalDirection === "down" ) ? 2 : 1 ) :
( verticalDirection && ( verticalDirection === "down" ? 2 : 1 ) );
},
_intersectsWithSides: function( item ) {
var isOverBottomHalf = this._isOverAxis( this.positionAbs.top +
this.offset.click.top, item.top + ( item.height / 2 ), item.height ),
isOverRightHalf = this._isOverAxis( this.positionAbs.left +
this.offset.click.left, item.left + ( item.width / 2 ), item.width ),
verticalDirection = this.dragDirection.vertical,
horizontalDirection = this.dragDirection.horizontal;
if ( this.floating && horizontalDirection ) {
return ( ( horizontalDirection === "right" && isOverRightHalf ) ||
( horizontalDirection === "left" && !isOverRightHalf ) );
} else {
return verticalDirection && ( ( verticalDirection === "down" && isOverBottomHalf ) ||
( verticalDirection === "up" && !isOverBottomHalf ) );
}
},
_getDragVerticalDirection: function() {
var delta = this.positionAbs.top - this.lastPositionAbs.top;
return delta !== 0 && ( delta > 0 ? "down" : "up" );
},
_getDragHorizontalDirection: function() {
var delta = this.positionAbs.left - this.lastPositionAbs.left;
return delta !== 0 && ( delta > 0 ? "right" : "left" );
},
refresh: function( event ) {
this._refreshItems( event );
this._setHandleClassName();
this.refreshPositions();
return this;
},
_connectWith: function() {
var options = this.options;
return options.connectWith.constructor === String ?
[ options.connectWith ] :
options.connectWith;
},
_getItemsAsjQuery: function( connected ) {
var i, j, cur, inst,
items = [],
queries = [],
connectWith = this._connectWith();
if ( connectWith && connected ) {
for ( i = connectWith.length - 1; i >= 0; i-- ) {
cur = $( connectWith[ i ], this.document[ 0 ] );
for ( j = cur.length - 1; j >= 0; j-- ) {
inst = $.data( cur[ j ], this.widgetFullName );
if ( inst && inst !== this && !inst.options.disabled ) {
queries.push( [ typeof inst.options.items === "function" ?
inst.options.items.call( inst.element ) :
$( inst.options.items, inst.element )
.not( ".ui-sortable-helper" )
.not( ".ui-sortable-placeholder" ), inst ] );
}
}
}
}
queries.push( [ typeof this.options.items === "function" ?
this.options.items
.call( this.element, null, { options: this.options, item: this.currentItem } ) :
$( this.options.items, this.element )
.not( ".ui-sortable-helper" )
.not( ".ui-sortable-placeholder" ), this ] );
function addItems() {
items.push( this );
}
for ( i = queries.length - 1; i >= 0; i-- ) {
queries[ i ][ 0 ].each( addItems );
}
return $( items );
},
_removeCurrentsFromItems: function() {
var list = this.currentItem.find( ":data(" + this.widgetName + "-item)" );
this.items = $.grep( this.items, function( item ) {
for ( var j = 0; j < list.length; j++ ) {
if ( list[ j ] === item.item[ 0 ] ) {
return false;
}
}
return true;
} );
},
_refreshItems: function( event ) {
this.items = [];
this.containers = [ this ];
var i, j, cur, inst, targetData, _queries, item, queriesLength,
items = this.items,
queries = [ [ typeof this.options.items === "function" ?
this.options.items.call( this.element[ 0 ], event, { item: this.currentItem } ) :
$( this.options.items, this.element ), this ] ],
connectWith = this._connectWith();
//Shouldn't be run the first time through due to massive slow-down
if ( connectWith && this.ready ) {
for ( i = connectWith.length - 1; i >= 0; i-- ) {
cur = $( connectWith[ i ], this.document[ 0 ] );
for ( j = cur.length - 1; j >= 0; j-- ) {
inst = $.data( cur[ j ], this.widgetFullName );
if ( inst && inst !== this && !inst.options.disabled ) {
queries.push( [ typeof inst.options.items === "function" ?
inst.options.items
.call( inst.element[ 0 ], event, { item: this.currentItem } ) :
$( inst.options.items, inst.element ), inst ] );
this.containers.push( inst );
}
}
}
}
for ( i = queries.length - 1; i >= 0; i-- ) {
targetData = queries[ i ][ 1 ];
_queries = queries[ i ][ 0 ];
for ( j = 0, queriesLength = _queries.length; j < queriesLength; j++ ) {
item = $( _queries[ j ] );
// Data for target checking (mouse manager)
item.data( this.widgetName + "-item", targetData );
items.push( {
item: item,
instance: targetData,
width: 0, height: 0,
left: 0, top: 0
} );
}
}
},
_refreshItemPositions: function( fast ) {
var i, item, t, p;
for ( i = this.items.length - 1; i >= 0; i-- ) {
item = this.items[ i ];
//We ignore calculating positions of all connected containers when we're not over them
if ( this.currentContainer && item.instance !== this.currentContainer &&
item.item[ 0 ] !== this.currentItem[ 0 ] ) {
continue;
}
t = this.options.toleranceElement ?
$( this.options.toleranceElement, item.item ) :
item.item;
if ( !fast ) {
item.width = t.outerWidth();
item.height = t.outerHeight();
}
p = t.offset();
item.left = p.left;
item.top = p.top;
}
},
refreshPositions: function( fast ) {
// Determine whether items are being displayed horizontally
this.floating = this.items.length ?
this.options.axis === "x" || this._isFloating( this.items[ 0 ].item ) :
false;
if ( this.innermostContainer !== null ) {
this._refreshItemPositions( fast );
}
var i, p;
if ( this.options.custom && this.options.custom.refreshContainers ) {
this.options.custom.refreshContainers.call( this );
} else {
for ( i = this.containers.length - 1; i >= 0; i-- ) {
p = this.containers[ i ].element.offset();
this.containers[ i ].containerCache.left = p.left;
this.containers[ i ].containerCache.top = p.top;
this.containers[ i ].containerCache.width =
this.containers[ i ].element.outerWidth();
this.containers[ i ].containerCache.height =
this.containers[ i ].element.outerHeight();
}
}
return this;
},
_createPlaceholder: function( that ) {
that = that || this;
var className, nodeName,
o = that.options;
if ( !o.placeholder || o.placeholder.constructor === String ) {
className = o.placeholder;
nodeName = that.currentItem[ 0 ].nodeName.toLowerCase();
o.placeholder = {
element: function() {
var element = $( "<" + nodeName + ">", that.document[ 0 ] );
that._addClass( element, "ui-sortable-placeholder",
className || that.currentItem[ 0 ].className )
._removeClass( element, "ui-sortable-helper" );
if ( nodeName === "tbody" ) {
that._createTrPlaceholder(
that.currentItem.find( "tr" ).eq( 0 ),
$( "
", that.document[ 0 ] ).appendTo( element )
);
} else if ( nodeName === "tr" ) {
that._createTrPlaceholder( that.currentItem, element );
} else if ( nodeName === "img" ) {
element.attr( "src", that.currentItem.attr( "src" ) );
}
if ( !className ) {
element.css( "visibility", "hidden" );
}
return element;
},
update: function( container, p ) {
// 1. If a className is set as 'placeholder option, we don't force sizes -
// the class is responsible for that
// 2. The option 'forcePlaceholderSize can be enabled to force it even if a
// class name is specified
if ( className && !o.forcePlaceholderSize ) {
return;
}
// If the element doesn't have a actual height or width by itself (without
// styles coming from a stylesheet), it receives the inline height and width
// from the dragged item. Or, if it's a tbody or tr, it's going to have a height
// anyway since we're populating them with s above, but they're unlikely to
// be the correct height on their own if the row heights are dynamic, so we'll
// always assign the height of the dragged item given forcePlaceholderSize
// is true.
if ( !p.height() || ( o.forcePlaceholderSize &&
( nodeName === "tbody" || nodeName === "tr" ) ) ) {
p.height(
that.currentItem.innerHeight() -
parseInt( that.currentItem.css( "paddingTop" ) || 0, 10 ) -
parseInt( that.currentItem.css( "paddingBottom" ) || 0, 10 ) );
}
if ( !p.width() ) {
p.width(
that.currentItem.innerWidth() -
parseInt( that.currentItem.css( "paddingLeft" ) || 0, 10 ) -
parseInt( that.currentItem.css( "paddingRight" ) || 0, 10 ) );
}
}
};
}
//Create the placeholder
that.placeholder = $( o.placeholder.element.call( that.element, that.currentItem ) );
//Append it after the actual current item
that.currentItem.after( that.placeholder );
//Update the size of the placeholder (TODO: Logic to fuzzy, see line 316/317)
o.placeholder.update( that, that.placeholder );
},
_createTrPlaceholder: function( sourceTr, targetTr ) {
var that = this;
sourceTr.children().each( function() {
$( " ", that.document[ 0 ] )
.attr( "colspan", $( this ).attr( "colspan" ) || 1 )
.appendTo( targetTr );
} );
},
_contactContainers: function( event ) {
var i, j, dist, itemWithLeastDistance, posProperty, sizeProperty, cur, nearBottom,
floating, axis,
innermostContainer = null,
innermostIndex = null;
// Get innermost container that intersects with item
for ( i = this.containers.length - 1; i >= 0; i-- ) {
// Never consider a container that's located within the item itself
if ( $.contains( this.currentItem[ 0 ], this.containers[ i ].element[ 0 ] ) ) {
continue;
}
if ( this._intersectsWith( this.containers[ i ].containerCache ) ) {
// If we've already found a container and it's more "inner" than this, then continue
if ( innermostContainer &&
$.contains(
this.containers[ i ].element[ 0 ],
innermostContainer.element[ 0 ] ) ) {
continue;
}
innermostContainer = this.containers[ i ];
innermostIndex = i;
} else {
// container doesn't intersect. trigger "out" event if necessary
if ( this.containers[ i ].containerCache.over ) {
this.containers[ i ]._trigger( "out", event, this._uiHash( this ) );
this.containers[ i ].containerCache.over = 0;
}
}
}
this.innermostContainer = innermostContainer;
// If no intersecting containers found, return
if ( !innermostContainer ) {
return;
}
// Move the item into the container if it's not there already
if ( this.containers.length === 1 ) {
if ( !this.containers[ innermostIndex ].containerCache.over ) {
this.containers[ innermostIndex ]._trigger( "over", event, this._uiHash( this ) );
this.containers[ innermostIndex ].containerCache.over = 1;
}
} else {
// When entering a new container, we will find the item with the least distance and
// append our item near it
dist = 10000;
itemWithLeastDistance = null;
floating = innermostContainer.floating || this._isFloating( this.currentItem );
posProperty = floating ? "left" : "top";
sizeProperty = floating ? "width" : "height";
axis = floating ? "pageX" : "pageY";
for ( j = this.items.length - 1; j >= 0; j-- ) {
if ( !$.contains(
this.containers[ innermostIndex ].element[ 0 ], this.items[ j ].item[ 0 ] )
) {
continue;
}
if ( this.items[ j ].item[ 0 ] === this.currentItem[ 0 ] ) {
continue;
}
cur = this.items[ j ].item.offset()[ posProperty ];
nearBottom = false;
if ( event[ axis ] - cur > this.items[ j ][ sizeProperty ] / 2 ) {
nearBottom = true;
}
if ( Math.abs( event[ axis ] - cur ) < dist ) {
dist = Math.abs( event[ axis ] - cur );
itemWithLeastDistance = this.items[ j ];
this.direction = nearBottom ? "up" : "down";
}
}
//Check if dropOnEmpty is enabled
if ( !itemWithLeastDistance && !this.options.dropOnEmpty ) {
return;
}
if ( this.currentContainer === this.containers[ innermostIndex ] ) {
if ( !this.currentContainer.containerCache.over ) {
this.containers[ innermostIndex ]._trigger( "over", event, this._uiHash() );
this.currentContainer.containerCache.over = 1;
}
return;
}
if ( itemWithLeastDistance ) {
this._rearrange( event, itemWithLeastDistance, null, true );
} else {
this._rearrange( event, null, this.containers[ innermostIndex ].element, true );
}
this._trigger( "change", event, this._uiHash() );
this.containers[ innermostIndex ]._trigger( "change", event, this._uiHash( this ) );
this.currentContainer = this.containers[ innermostIndex ];
//Update the placeholder
this.options.placeholder.update( this.currentContainer, this.placeholder );
//Update scrollParent
this.scrollParent = this.placeholder.scrollParent();
//Update overflowOffset
if ( this.scrollParent[ 0 ] !== this.document[ 0 ] &&
this.scrollParent[ 0 ].tagName !== "HTML" ) {
this.overflowOffset = this.scrollParent.offset();
}
this.containers[ innermostIndex ]._trigger( "over", event, this._uiHash( this ) );
this.containers[ innermostIndex ].containerCache.over = 1;
}
},
_createHelper: function( event ) {
var o = this.options,
helper = typeof o.helper === "function" ?
$( o.helper.apply( this.element[ 0 ], [ event, this.currentItem ] ) ) :
( o.helper === "clone" ? this.currentItem.clone() : this.currentItem );
//Add the helper to the DOM if that didn't happen already
if ( !helper.parents( "body" ).length ) {
this.appendTo[ 0 ].appendChild( helper[ 0 ] );
}
if ( helper[ 0 ] === this.currentItem[ 0 ] ) {
this._storedCSS = {
width: this.currentItem[ 0 ].style.width,
height: this.currentItem[ 0 ].style.height,
position: this.currentItem.css( "position" ),
top: this.currentItem.css( "top" ),
left: this.currentItem.css( "left" )
};
}
if ( !helper[ 0 ].style.width || o.forceHelperSize ) {
helper.width( this.currentItem.width() );
}
if ( !helper[ 0 ].style.height || o.forceHelperSize ) {
helper.height( this.currentItem.height() );
}
return helper;
},
_adjustOffsetFromHelper: function( obj ) {
if ( typeof obj === "string" ) {
obj = obj.split( " " );
}
if ( Array.isArray( obj ) ) {
obj = { left: +obj[ 0 ], top: +obj[ 1 ] || 0 };
}
if ( "left" in obj ) {
this.offset.click.left = obj.left + this.margins.left;
}
if ( "right" in obj ) {
this.offset.click.left = this.helperProportions.width - obj.right + this.margins.left;
}
if ( "top" in obj ) {
this.offset.click.top = obj.top + this.margins.top;
}
if ( "bottom" in obj ) {
this.offset.click.top = this.helperProportions.height - obj.bottom + this.margins.top;
}
},
_getParentOffset: function() {
//Get the offsetParent and cache its position
this.offsetParent = this.helper.offsetParent();
var po = this.offsetParent.offset();
// This is a special case where we need to modify a offset calculated on start, since the
// following happened:
// 1. The position of the helper is absolute, so it's position is calculated based on the
// next positioned parent
// 2. The actual offset parent is a child of the scroll parent, and the scroll parent isn't
// the document, which means that the scroll is included in the initial calculation of the
// offset of the parent, and never recalculated upon drag
if ( this.cssPosition === "absolute" && this.scrollParent[ 0 ] !== this.document[ 0 ] &&
$.contains( this.scrollParent[ 0 ], this.offsetParent[ 0 ] ) ) {
po.left += this.scrollParent.scrollLeft();
po.top += this.scrollParent.scrollTop();
}
// This needs to be actually done for all browsers, since pageX/pageY includes this
// information with an ugly IE fix
if ( this.offsetParent[ 0 ] === this.document[ 0 ].body ||
( this.offsetParent[ 0 ].tagName &&
this.offsetParent[ 0 ].tagName.toLowerCase() === "html" && $.ui.ie ) ) {
po = { top: 0, left: 0 };
}
return {
top: po.top + ( parseInt( this.offsetParent.css( "borderTopWidth" ), 10 ) || 0 ),
left: po.left + ( parseInt( this.offsetParent.css( "borderLeftWidth" ), 10 ) || 0 )
};
},
_getRelativeOffset: function() {
if ( this.cssPosition === "relative" ) {
var p = this.currentItem.position();
return {
top: p.top - ( parseInt( this.helper.css( "top" ), 10 ) || 0 ) +
this.scrollParent.scrollTop(),
left: p.left - ( parseInt( this.helper.css( "left" ), 10 ) || 0 ) +
this.scrollParent.scrollLeft()
};
} else {
return { top: 0, left: 0 };
}
},
_cacheMargins: function() {
this.margins = {
left: ( parseInt( this.currentItem.css( "marginLeft" ), 10 ) || 0 ),
top: ( parseInt( this.currentItem.css( "marginTop" ), 10 ) || 0 )
};
},
_cacheHelperProportions: function() {
this.helperProportions = {
width: this.helper.outerWidth(),
height: this.helper.outerHeight()
};
},
_setContainment: function() {
var ce, co, over,
o = this.options;
if ( o.containment === "parent" ) {
o.containment = this.helper[ 0 ].parentNode;
}
if ( o.containment === "document" || o.containment === "window" ) {
this.containment = [
0 - this.offset.relative.left - this.offset.parent.left,
0 - this.offset.relative.top - this.offset.parent.top,
o.containment === "document" ?
this.document.width() :
this.window.width() - this.helperProportions.width - this.margins.left,
( o.containment === "document" ?
( this.document.height() || document.body.parentNode.scrollHeight ) :
this.window.height() || this.document[ 0 ].body.parentNode.scrollHeight
) - this.helperProportions.height - this.margins.top
];
}
if ( !( /^(document|window|parent)$/ ).test( o.containment ) ) {
ce = $( o.containment )[ 0 ];
co = $( o.containment ).offset();
over = ( $( ce ).css( "overflow" ) !== "hidden" );
this.containment = [
co.left + ( parseInt( $( ce ).css( "borderLeftWidth" ), 10 ) || 0 ) +
( parseInt( $( ce ).css( "paddingLeft" ), 10 ) || 0 ) - this.margins.left,
co.top + ( parseInt( $( ce ).css( "borderTopWidth" ), 10 ) || 0 ) +
( parseInt( $( ce ).css( "paddingTop" ), 10 ) || 0 ) - this.margins.top,
co.left + ( over ? Math.max( ce.scrollWidth, ce.offsetWidth ) : ce.offsetWidth ) -
( parseInt( $( ce ).css( "borderLeftWidth" ), 10 ) || 0 ) -
( parseInt( $( ce ).css( "paddingRight" ), 10 ) || 0 ) -
this.helperProportions.width - this.margins.left,
co.top + ( over ? Math.max( ce.scrollHeight, ce.offsetHeight ) : ce.offsetHeight ) -
( parseInt( $( ce ).css( "borderTopWidth" ), 10 ) || 0 ) -
( parseInt( $( ce ).css( "paddingBottom" ), 10 ) || 0 ) -
this.helperProportions.height - this.margins.top
];
}
},
_convertPositionTo: function( d, pos ) {
if ( !pos ) {
pos = this.position;
}
var mod = d === "absolute" ? 1 : -1,
scroll = this.cssPosition === "absolute" &&
!( this.scrollParent[ 0 ] !== this.document[ 0 ] &&
$.contains( this.scrollParent[ 0 ], this.offsetParent[ 0 ] ) ) ?
this.offsetParent :
this.scrollParent,
scrollIsRootNode = ( /(html|body)/i ).test( scroll[ 0 ].tagName );
return {
top: (
// The absolute mouse position
pos.top +
// Only for relative positioned nodes: Relative offset from element to offset parent
this.offset.relative.top * mod +
// The offsetParent's offset without borders (offset + border)
this.offset.parent.top * mod -
( ( this.cssPosition === "fixed" ?
-this.scrollParent.scrollTop() :
( scrollIsRootNode ? 0 : scroll.scrollTop() ) ) * mod )
),
left: (
// The absolute mouse position
pos.left +
// Only for relative positioned nodes: Relative offset from element to offset parent
this.offset.relative.left * mod +
// The offsetParent's offset without borders (offset + border)
this.offset.parent.left * mod -
( ( this.cssPosition === "fixed" ?
-this.scrollParent.scrollLeft() : scrollIsRootNode ? 0 :
scroll.scrollLeft() ) * mod )
)
};
},
_generatePosition: function( event ) {
var top, left,
o = this.options,
pageX = event.pageX,
pageY = event.pageY,
scroll = this.cssPosition === "absolute" &&
!( this.scrollParent[ 0 ] !== this.document[ 0 ] &&
$.contains( this.scrollParent[ 0 ], this.offsetParent[ 0 ] ) ) ?
this.offsetParent :
this.scrollParent,
scrollIsRootNode = ( /(html|body)/i ).test( scroll[ 0 ].tagName );
// This is another very weird special case that only happens for relative elements:
// 1. If the css position is relative
// 2. and the scroll parent is the document or similar to the offset parent
// we have to refresh the relative offset during the scroll so there are no jumps
if ( this.cssPosition === "relative" && !( this.scrollParent[ 0 ] !== this.document[ 0 ] &&
this.scrollParent[ 0 ] !== this.offsetParent[ 0 ] ) ) {
this.offset.relative = this._getRelativeOffset();
}
/*
* - Position constraining -
* Constrain the position to a mix of grid, containment.
*/
if ( this.originalPosition ) { //If we are not dragging yet, we won't check for options
if ( this.containment ) {
if ( event.pageX - this.offset.click.left < this.containment[ 0 ] ) {
pageX = this.containment[ 0 ] + this.offset.click.left;
}
if ( event.pageY - this.offset.click.top < this.containment[ 1 ] ) {
pageY = this.containment[ 1 ] + this.offset.click.top;
}
if ( event.pageX - this.offset.click.left > this.containment[ 2 ] ) {
pageX = this.containment[ 2 ] + this.offset.click.left;
}
if ( event.pageY - this.offset.click.top > this.containment[ 3 ] ) {
pageY = this.containment[ 3 ] + this.offset.click.top;
}
}
if ( o.grid ) {
top = this.originalPageY + Math.round( ( pageY - this.originalPageY ) /
o.grid[ 1 ] ) * o.grid[ 1 ];
pageY = this.containment ?
( ( top - this.offset.click.top >= this.containment[ 1 ] &&
top - this.offset.click.top <= this.containment[ 3 ] ) ?
top :
( ( top - this.offset.click.top >= this.containment[ 1 ] ) ?
top - o.grid[ 1 ] : top + o.grid[ 1 ] ) ) :
top;
left = this.originalPageX + Math.round( ( pageX - this.originalPageX ) /
o.grid[ 0 ] ) * o.grid[ 0 ];
pageX = this.containment ?
( ( left - this.offset.click.left >= this.containment[ 0 ] &&
left - this.offset.click.left <= this.containment[ 2 ] ) ?
left :
( ( left - this.offset.click.left >= this.containment[ 0 ] ) ?
left - o.grid[ 0 ] : left + o.grid[ 0 ] ) ) :
left;
}
}
return {
top: (
// The absolute mouse position
pageY -
// Click offset (relative to the element)
this.offset.click.top -
// Only for relative positioned nodes: Relative offset from element to offset parent
this.offset.relative.top -
// The offsetParent's offset without borders (offset + border)
this.offset.parent.top +
( ( this.cssPosition === "fixed" ?
-this.scrollParent.scrollTop() :
( scrollIsRootNode ? 0 : scroll.scrollTop() ) ) )
),
left: (
// The absolute mouse position
pageX -
// Click offset (relative to the element)
this.offset.click.left -
// Only for relative positioned nodes: Relative offset from element to offset parent
this.offset.relative.left -
// The offsetParent's offset without borders (offset + border)
this.offset.parent.left +
( ( this.cssPosition === "fixed" ?
-this.scrollParent.scrollLeft() :
scrollIsRootNode ? 0 : scroll.scrollLeft() ) )
)
};
},
_rearrange: function( event, i, a, hardRefresh ) {
if ( a ) {
a[ 0 ].appendChild( this.placeholder[ 0 ] );
} else {
i.item[ 0 ].parentNode.insertBefore( this.placeholder[ 0 ],
( this.direction === "down" ? i.item[ 0 ] : i.item[ 0 ].nextSibling ) );
}
//Various things done here to improve the performance:
// 1. we create a setTimeout, that calls refreshPositions
// 2. on the instance, we have a counter variable, that get's higher after every append
// 3. on the local scope, we copy the counter variable, and check in the timeout,
// if it's still the same
// 4. this lets only the last addition to the timeout stack through
this.counter = this.counter ? ++this.counter : 1;
var counter = this.counter;
this._delay( function() {
if ( counter === this.counter ) {
//Precompute after each DOM insertion, NOT on mousemove
this.refreshPositions( !hardRefresh );
}
} );
},
_clear: function( event, noPropagation ) {
this.reverting = false;
// We delay all events that have to be triggered to after the point where the placeholder
// has been removed and everything else normalized again
var i,
delayedTriggers = [];
// We first have to update the dom position of the actual currentItem
// Note: don't do it if the current item is already removed (by a user), or it gets
// reappended (see #4088)
if ( !this._noFinalSort && this.currentItem.parent().length ) {
this.placeholder.before( this.currentItem );
}
this._noFinalSort = null;
if ( this.helper[ 0 ] === this.currentItem[ 0 ] ) {
for ( i in this._storedCSS ) {
if ( this._storedCSS[ i ] === "auto" || this._storedCSS[ i ] === "static" ) {
this._storedCSS[ i ] = "";
}
}
this.currentItem.css( this._storedCSS );
this._removeClass( this.currentItem, "ui-sortable-helper" );
} else {
this.currentItem.show();
}
if ( this.fromOutside && !noPropagation ) {
delayedTriggers.push( function( event ) {
this._trigger( "receive", event, this._uiHash( this.fromOutside ) );
} );
}
if ( ( this.fromOutside ||
this.domPosition.prev !==
this.currentItem.prev().not( ".ui-sortable-helper" )[ 0 ] ||
this.domPosition.parent !== this.currentItem.parent()[ 0 ] ) && !noPropagation ) {
// Trigger update callback if the DOM position has changed
delayedTriggers.push( function( event ) {
this._trigger( "update", event, this._uiHash() );
} );
}
// Check if the items Container has Changed and trigger appropriate
// events.
if ( this !== this.currentContainer ) {
if ( !noPropagation ) {
delayedTriggers.push( function( event ) {
this._trigger( "remove", event, this._uiHash() );
} );
delayedTriggers.push( ( function( c ) {
return function( event ) {
c._trigger( "receive", event, this._uiHash( this ) );
};
} ).call( this, this.currentContainer ) );
delayedTriggers.push( ( function( c ) {
return function( event ) {
c._trigger( "update", event, this._uiHash( this ) );
};
} ).call( this, this.currentContainer ) );
}
}
//Post events to containers
function delayEvent( type, instance, container ) {
return function( event ) {
container._trigger( type, event, instance._uiHash( instance ) );
};
}
for ( i = this.containers.length - 1; i >= 0; i-- ) {
if ( !noPropagation ) {
delayedTriggers.push( delayEvent( "deactivate", this, this.containers[ i ] ) );
}
if ( this.containers[ i ].containerCache.over ) {
delayedTriggers.push( delayEvent( "out", this, this.containers[ i ] ) );
this.containers[ i ].containerCache.over = 0;
}
}
//Do what was originally in plugins
if ( this.storedCursor ) {
this.document.find( "body" ).css( "cursor", this.storedCursor );
this.storedStylesheet.remove();
}
if ( this._storedOpacity ) {
this.helper.css( "opacity", this._storedOpacity );
}
if ( this._storedZIndex ) {
this.helper.css( "zIndex", this._storedZIndex === "auto" ? "" : this._storedZIndex );
}
this.dragging = false;
if ( !noPropagation ) {
this._trigger( "beforeStop", event, this._uiHash() );
}
//$(this.placeholder[0]).remove(); would have been the jQuery way - unfortunately,
// it unbinds ALL events from the original node!
this.placeholder[ 0 ].parentNode.removeChild( this.placeholder[ 0 ] );
if ( !this.cancelHelperRemoval ) {
if ( this.helper[ 0 ] !== this.currentItem[ 0 ] ) {
this.helper.remove();
}
this.helper = null;
}
if ( !noPropagation ) {
for ( i = 0; i < delayedTriggers.length; i++ ) {
// Trigger all delayed events
delayedTriggers[ i ].call( this, event );
}
this._trigger( "stop", event, this._uiHash() );
}
this.fromOutside = false;
return !this.cancelHelperRemoval;
},
_trigger: function() {
if ( $.Widget.prototype._trigger.apply( this, arguments ) === false ) {
this.cancel();
}
},
_uiHash: function( _inst ) {
var inst = _inst || this;
return {
helper: inst.helper,
placeholder: inst.placeholder || $( [] ),
position: inst.position,
originalPosition: inst.originalPosition,
offset: inst.positionAbs,
item: inst.currentItem,
sender: _inst ? _inst.element : null
};
}
} );
/*!
* jQuery UI Spinner 1.13.0
* http://jqueryui.com
*
* Copyright jQuery Foundation and other contributors
* Released under the MIT license.
* http://jquery.org/license
*/
//>>label: Spinner
//>>group: Widgets
//>>description: Displays buttons to easily input numbers via the keyboard or mouse.
//>>docs: http://api.jqueryui.com/spinner/
//>>demos: http://jqueryui.com/spinner/
//>>css.structure: ../../themes/base/core.css
//>>css.structure: ../../themes/base/spinner.css
//>>css.theme: ../../themes/base/theme.css
function spinnerModifier( fn ) {
return function() {
var previous = this.element.val();
fn.apply( this, arguments );
this._refresh();
if ( previous !== this.element.val() ) {
this._trigger( "change" );
}
};
}
$.widget( "ui.spinner", {
version: "1.13.0",
defaultElement: " ",
widgetEventPrefix: "spin",
options: {
classes: {
"ui-spinner": "ui-corner-all",
"ui-spinner-down": "ui-corner-br",
"ui-spinner-up": "ui-corner-tr"
},
culture: null,
icons: {
down: "ui-icon-triangle-1-s",
up: "ui-icon-triangle-1-n"
},
incremental: true,
max: null,
min: null,
numberFormat: null,
page: 10,
step: 1,
change: null,
spin: null,
start: null,
stop: null
},
_create: function() {
// handle string values that need to be parsed
this._setOption( "max", this.options.max );
this._setOption( "min", this.options.min );
this._setOption( "step", this.options.step );
// Only format if there is a value, prevents the field from being marked
// as invalid in Firefox, see #9573.
if ( this.value() !== "" ) {
// Format the value, but don't constrain.
this._value( this.element.val(), true );
}
this._draw();
this._on( this._events );
this._refresh();
// Turning off autocomplete prevents the browser from remembering the
// value when navigating through history, so we re-enable autocomplete
// if the page is unloaded before the widget is destroyed. #7790
this._on( this.window, {
beforeunload: function() {
this.element.removeAttr( "autocomplete" );
}
} );
},
_getCreateOptions: function() {
var options = this._super();
var element = this.element;
$.each( [ "min", "max", "step" ], function( i, option ) {
var value = element.attr( option );
if ( value != null && value.length ) {
options[ option ] = value;
}
} );
return options;
},
_events: {
keydown: function( event ) {
if ( this._start( event ) && this._keydown( event ) ) {
event.preventDefault();
}
},
keyup: "_stop",
focus: function() {
this.previous = this.element.val();
},
blur: function( event ) {
if ( this.cancelBlur ) {
delete this.cancelBlur;
return;
}
this._stop();
this._refresh();
if ( this.previous !== this.element.val() ) {
this._trigger( "change", event );
}
},
mousewheel: function( event, delta ) {
var activeElement = $.ui.safeActiveElement( this.document[ 0 ] );
var isActive = this.element[ 0 ] === activeElement;
if ( !isActive || !delta ) {
return;
}
if ( !this.spinning && !this._start( event ) ) {
return false;
}
this._spin( ( delta > 0 ? 1 : -1 ) * this.options.step, event );
clearTimeout( this.mousewheelTimer );
this.mousewheelTimer = this._delay( function() {
if ( this.spinning ) {
this._stop( event );
}
}, 100 );
event.preventDefault();
},
"mousedown .ui-spinner-button": function( event ) {
var previous;
// We never want the buttons to have focus; whenever the user is
// interacting with the spinner, the focus should be on the input.
// If the input is focused then this.previous is properly set from
// when the input first received focus. If the input is not focused
// then we need to set this.previous based on the value before spinning.
previous = this.element[ 0 ] === $.ui.safeActiveElement( this.document[ 0 ] ) ?
this.previous : this.element.val();
function checkFocus() {
var isActive = this.element[ 0 ] === $.ui.safeActiveElement( this.document[ 0 ] );
if ( !isActive ) {
this.element.trigger( "focus" );
this.previous = previous;
// support: IE
// IE sets focus asynchronously, so we need to check if focus
// moved off of the input because the user clicked on the button.
this._delay( function() {
this.previous = previous;
} );
}
}
// Ensure focus is on (or stays on) the text field
event.preventDefault();
checkFocus.call( this );
// Support: IE
// IE doesn't prevent moving focus even with event.preventDefault()
// so we set a flag to know when we should ignore the blur event
// and check (again) if focus moved off of the input.
this.cancelBlur = true;
this._delay( function() {
delete this.cancelBlur;
checkFocus.call( this );
} );
if ( this._start( event ) === false ) {
return;
}
this._repeat( null, $( event.currentTarget )
.hasClass( "ui-spinner-up" ) ? 1 : -1, event );
},
"mouseup .ui-spinner-button": "_stop",
"mouseenter .ui-spinner-button": function( event ) {
// button will add ui-state-active if mouse was down while mouseleave and kept down
if ( !$( event.currentTarget ).hasClass( "ui-state-active" ) ) {
return;
}
if ( this._start( event ) === false ) {
return false;
}
this._repeat( null, $( event.currentTarget )
.hasClass( "ui-spinner-up" ) ? 1 : -1, event );
},
// TODO: do we really want to consider this a stop?
// shouldn't we just stop the repeater and wait until mouseup before
// we trigger the stop event?
"mouseleave .ui-spinner-button": "_stop"
},
// Support mobile enhanced option and make backcompat more sane
_enhance: function() {
this.uiSpinner = this.element
.attr( "autocomplete", "off" )
.wrap( "" )
.parent()
// Add buttons
.append(
" "
);
},
_draw: function() {
this._enhance();
this._addClass( this.uiSpinner, "ui-spinner", "ui-widget ui-widget-content" );
this._addClass( "ui-spinner-input" );
this.element.attr( "role", "spinbutton" );
// Button bindings
this.buttons = this.uiSpinner.children( "a" )
.attr( "tabIndex", -1 )
.attr( "aria-hidden", true )
.button( {
classes: {
"ui-button": ""
}
} );
// TODO: Right now button does not support classes this is already updated in button PR
this._removeClass( this.buttons, "ui-corner-all" );
this._addClass( this.buttons.first(), "ui-spinner-button ui-spinner-up" );
this._addClass( this.buttons.last(), "ui-spinner-button ui-spinner-down" );
this.buttons.first().button( {
"icon": this.options.icons.up,
"showLabel": false
} );
this.buttons.last().button( {
"icon": this.options.icons.down,
"showLabel": false
} );
// IE 6 doesn't understand height: 50% for the buttons
// unless the wrapper has an explicit height
if ( this.buttons.height() > Math.ceil( this.uiSpinner.height() * 0.5 ) &&
this.uiSpinner.height() > 0 ) {
this.uiSpinner.height( this.uiSpinner.height() );
}
},
_keydown: function( event ) {
var options = this.options,
keyCode = $.ui.keyCode;
switch ( event.keyCode ) {
case keyCode.UP:
this._repeat( null, 1, event );
return true;
case keyCode.DOWN:
this._repeat( null, -1, event );
return true;
case keyCode.PAGE_UP:
this._repeat( null, options.page, event );
return true;
case keyCode.PAGE_DOWN:
this._repeat( null, -options.page, event );
return true;
}
return false;
},
_start: function( event ) {
if ( !this.spinning && this._trigger( "start", event ) === false ) {
return false;
}
if ( !this.counter ) {
this.counter = 1;
}
this.spinning = true;
return true;
},
_repeat: function( i, steps, event ) {
i = i || 500;
clearTimeout( this.timer );
this.timer = this._delay( function() {
this._repeat( 40, steps, event );
}, i );
this._spin( steps * this.options.step, event );
},
_spin: function( step, event ) {
var value = this.value() || 0;
if ( !this.counter ) {
this.counter = 1;
}
value = this._adjustValue( value + step * this._increment( this.counter ) );
if ( !this.spinning || this._trigger( "spin", event, { value: value } ) !== false ) {
this._value( value );
this.counter++;
}
},
_increment: function( i ) {
var incremental = this.options.incremental;
if ( incremental ) {
return typeof incremental === "function" ?
incremental( i ) :
Math.floor( i * i * i / 50000 - i * i / 500 + 17 * i / 200 + 1 );
}
return 1;
},
_precision: function() {
var precision = this._precisionOf( this.options.step );
if ( this.options.min !== null ) {
precision = Math.max( precision, this._precisionOf( this.options.min ) );
}
return precision;
},
_precisionOf: function( num ) {
var str = num.toString(),
decimal = str.indexOf( "." );
return decimal === -1 ? 0 : str.length - decimal - 1;
},
_adjustValue: function( value ) {
var base, aboveMin,
options = this.options;
// Make sure we're at a valid step
// - find out where we are relative to the base (min or 0)
base = options.min !== null ? options.min : 0;
aboveMin = value - base;
// - round to the nearest step
aboveMin = Math.round( aboveMin / options.step ) * options.step;
// - rounding is based on 0, so adjust back to our base
value = base + aboveMin;
// Fix precision from bad JS floating point math
value = parseFloat( value.toFixed( this._precision() ) );
// Clamp the value
if ( options.max !== null && value > options.max ) {
return options.max;
}
if ( options.min !== null && value < options.min ) {
return options.min;
}
return value;
},
_stop: function( event ) {
if ( !this.spinning ) {
return;
}
clearTimeout( this.timer );
clearTimeout( this.mousewheelTimer );
this.counter = 0;
this.spinning = false;
this._trigger( "stop", event );
},
_setOption: function( key, value ) {
var prevValue, first, last;
if ( key === "culture" || key === "numberFormat" ) {
prevValue = this._parse( this.element.val() );
this.options[ key ] = value;
this.element.val( this._format( prevValue ) );
return;
}
if ( key === "max" || key === "min" || key === "step" ) {
if ( typeof value === "string" ) {
value = this._parse( value );
}
}
if ( key === "icons" ) {
first = this.buttons.first().find( ".ui-icon" );
this._removeClass( first, null, this.options.icons.up );
this._addClass( first, null, value.up );
last = this.buttons.last().find( ".ui-icon" );
this._removeClass( last, null, this.options.icons.down );
this._addClass( last, null, value.down );
}
this._super( key, value );
},
_setOptionDisabled: function( value ) {
this._super( value );
this._toggleClass( this.uiSpinner, null, "ui-state-disabled", !!value );
this.element.prop( "disabled", !!value );
this.buttons.button( value ? "disable" : "enable" );
},
_setOptions: spinnerModifier( function( options ) {
this._super( options );
} ),
_parse: function( val ) {
if ( typeof val === "string" && val !== "" ) {
val = window.Globalize && this.options.numberFormat ?
Globalize.parseFloat( val, 10, this.options.culture ) : +val;
}
return val === "" || isNaN( val ) ? null : val;
},
_format: function( value ) {
if ( value === "" ) {
return "";
}
return window.Globalize && this.options.numberFormat ?
Globalize.format( value, this.options.numberFormat, this.options.culture ) :
value;
},
_refresh: function() {
this.element.attr( {
"aria-valuemin": this.options.min,
"aria-valuemax": this.options.max,
// TODO: what should we do with values that can't be parsed?
"aria-valuenow": this._parse( this.element.val() )
} );
},
isValid: function() {
var value = this.value();
// Null is invalid
if ( value === null ) {
return false;
}
// If value gets adjusted, it's invalid
return value === this._adjustValue( value );
},
// Update the value without triggering change
_value: function( value, allowAny ) {
var parsed;
if ( value !== "" ) {
parsed = this._parse( value );
if ( parsed !== null ) {
if ( !allowAny ) {
parsed = this._adjustValue( parsed );
}
value = this._format( parsed );
}
}
this.element.val( value );
this._refresh();
},
_destroy: function() {
this.element
.prop( "disabled", false )
.removeAttr( "autocomplete role aria-valuemin aria-valuemax aria-valuenow" );
this.uiSpinner.replaceWith( this.element );
},
stepUp: spinnerModifier( function( steps ) {
this._stepUp( steps );
} ),
_stepUp: function( steps ) {
if ( this._start() ) {
this._spin( ( steps || 1 ) * this.options.step );
this._stop();
}
},
stepDown: spinnerModifier( function( steps ) {
this._stepDown( steps );
} ),
_stepDown: function( steps ) {
if ( this._start() ) {
this._spin( ( steps || 1 ) * -this.options.step );
this._stop();
}
},
pageUp: spinnerModifier( function( pages ) {
this._stepUp( ( pages || 1 ) * this.options.page );
} ),
pageDown: spinnerModifier( function( pages ) {
this._stepDown( ( pages || 1 ) * this.options.page );
} ),
value: function( newVal ) {
if ( !arguments.length ) {
return this._parse( this.element.val() );
}
spinnerModifier( this._value ).call( this, newVal );
},
widget: function() {
return this.uiSpinner;
}
} );
// DEPRECATED
// TODO: switch return back to widget declaration at top of file when this is removed
if ( $.uiBackCompat !== false ) {
// Backcompat for spinner html extension points
$.widget( "ui.spinner", $.ui.spinner, {
_enhance: function() {
this.uiSpinner = this.element
.attr( "autocomplete", "off" )
.wrap( this._uiSpinnerHtml() )
.parent()
// Add buttons
.append( this._buttonHtml() );
},
_uiSpinnerHtml: function() {
return "";
},
_buttonHtml: function() {
return " ";
}
} );
}
var widgetsSpinner = $.ui.spinner;
/*!
* jQuery UI Tabs 1.13.0
* http://jqueryui.com
*
* Copyright jQuery Foundation and other contributors
* Released under the MIT license.
* http://jquery.org/license
*/
//>>label: Tabs
//>>group: Widgets
//>>description: Transforms a set of container elements into a tab structure.
//>>docs: http://api.jqueryui.com/tabs/
//>>demos: http://jqueryui.com/tabs/
//>>css.structure: ../../themes/base/core.css
//>>css.structure: ../../themes/base/tabs.css
//>>css.theme: ../../themes/base/theme.css
$.widget( "ui.tabs", {
version: "1.13.0",
delay: 300,
options: {
active: null,
classes: {
"ui-tabs": "ui-corner-all",
"ui-tabs-nav": "ui-corner-all",
"ui-tabs-panel": "ui-corner-bottom",
"ui-tabs-tab": "ui-corner-top"
},
collapsible: false,
event: "click",
heightStyle: "content",
hide: null,
show: null,
// Callbacks
activate: null,
beforeActivate: null,
beforeLoad: null,
load: null
},
_isLocal: ( function() {
var rhash = /#.*$/;
return function( anchor ) {
var anchorUrl, locationUrl;
anchorUrl = anchor.href.replace( rhash, "" );
locationUrl = location.href.replace( rhash, "" );
// Decoding may throw an error if the URL isn't UTF-8 (#9518)
try {
anchorUrl = decodeURIComponent( anchorUrl );
} catch ( error ) {}
try {
locationUrl = decodeURIComponent( locationUrl );
} catch ( error ) {}
return anchor.hash.length > 1 && anchorUrl === locationUrl;
};
} )(),
_create: function() {
var that = this,
options = this.options;
this.running = false;
this._addClass( "ui-tabs", "ui-widget ui-widget-content" );
this._toggleClass( "ui-tabs-collapsible", null, options.collapsible );
this._processTabs();
options.active = this._initialActive();
// Take disabling tabs via class attribute from HTML
// into account and update option properly.
if ( Array.isArray( options.disabled ) ) {
options.disabled = $.uniqueSort( options.disabled.concat(
$.map( this.tabs.filter( ".ui-state-disabled" ), function( li ) {
return that.tabs.index( li );
} )
) ).sort();
}
// Check for length avoids error when initializing empty list
if ( this.options.active !== false && this.anchors.length ) {
this.active = this._findActive( options.active );
} else {
this.active = $();
}
this._refresh();
if ( this.active.length ) {
this.load( options.active );
}
},
_initialActive: function() {
var active = this.options.active,
collapsible = this.options.collapsible,
locationHash = location.hash.substring( 1 );
if ( active === null ) {
// check the fragment identifier in the URL
if ( locationHash ) {
this.tabs.each( function( i, tab ) {
if ( $( tab ).attr( "aria-controls" ) === locationHash ) {
active = i;
return false;
}
} );
}
// Check for a tab marked active via a class
if ( active === null ) {
active = this.tabs.index( this.tabs.filter( ".ui-tabs-active" ) );
}
// No active tab, set to false
if ( active === null || active === -1 ) {
active = this.tabs.length ? 0 : false;
}
}
// Handle numbers: negative, out of range
if ( active !== false ) {
active = this.tabs.index( this.tabs.eq( active ) );
if ( active === -1 ) {
active = collapsible ? false : 0;
}
}
// Don't allow collapsible: false and active: false
if ( !collapsible && active === false && this.anchors.length ) {
active = 0;
}
return active;
},
_getCreateEventData: function() {
return {
tab: this.active,
panel: !this.active.length ? $() : this._getPanelForTab( this.active )
};
},
_tabKeydown: function( event ) {
var focusedTab = $( $.ui.safeActiveElement( this.document[ 0 ] ) ).closest( "li" ),
selectedIndex = this.tabs.index( focusedTab ),
goingForward = true;
if ( this._handlePageNav( event ) ) {
return;
}
switch ( event.keyCode ) {
case $.ui.keyCode.RIGHT:
case $.ui.keyCode.DOWN:
selectedIndex++;
break;
case $.ui.keyCode.UP:
case $.ui.keyCode.LEFT:
goingForward = false;
selectedIndex--;
break;
case $.ui.keyCode.END:
selectedIndex = this.anchors.length - 1;
break;
case $.ui.keyCode.HOME:
selectedIndex = 0;
break;
case $.ui.keyCode.SPACE:
// Activate only, no collapsing
event.preventDefault();
clearTimeout( this.activating );
this._activate( selectedIndex );
return;
case $.ui.keyCode.ENTER:
// Toggle (cancel delayed activation, allow collapsing)
event.preventDefault();
clearTimeout( this.activating );
// Determine if we should collapse or activate
this._activate( selectedIndex === this.options.active ? false : selectedIndex );
return;
default:
return;
}
// Focus the appropriate tab, based on which key was pressed
event.preventDefault();
clearTimeout( this.activating );
selectedIndex = this._focusNextTab( selectedIndex, goingForward );
// Navigating with control/command key will prevent automatic activation
if ( !event.ctrlKey && !event.metaKey ) {
// Update aria-selected immediately so that AT think the tab is already selected.
// Otherwise AT may confuse the user by stating that they need to activate the tab,
// but the tab will already be activated by the time the announcement finishes.
focusedTab.attr( "aria-selected", "false" );
this.tabs.eq( selectedIndex ).attr( "aria-selected", "true" );
this.activating = this._delay( function() {
this.option( "active", selectedIndex );
}, this.delay );
}
},
_panelKeydown: function( event ) {
if ( this._handlePageNav( event ) ) {
return;
}
// Ctrl+up moves focus to the current tab
if ( event.ctrlKey && event.keyCode === $.ui.keyCode.UP ) {
event.preventDefault();
this.active.trigger( "focus" );
}
},
// Alt+page up/down moves focus to the previous/next tab (and activates)
_handlePageNav: function( event ) {
if ( event.altKey && event.keyCode === $.ui.keyCode.PAGE_UP ) {
this._activate( this._focusNextTab( this.options.active - 1, false ) );
return true;
}
if ( event.altKey && event.keyCode === $.ui.keyCode.PAGE_DOWN ) {
this._activate( this._focusNextTab( this.options.active + 1, true ) );
return true;
}
},
_findNextTab: function( index, goingForward ) {
var lastTabIndex = this.tabs.length - 1;
function constrain() {
if ( index > lastTabIndex ) {
index = 0;
}
if ( index < 0 ) {
index = lastTabIndex;
}
return index;
}
while ( $.inArray( constrain(), this.options.disabled ) !== -1 ) {
index = goingForward ? index + 1 : index - 1;
}
return index;
},
_focusNextTab: function( index, goingForward ) {
index = this._findNextTab( index, goingForward );
this.tabs.eq( index ).trigger( "focus" );
return index;
},
_setOption: function( key, value ) {
if ( key === "active" ) {
// _activate() will handle invalid values and update this.options
this._activate( value );
return;
}
this._super( key, value );
if ( key === "collapsible" ) {
this._toggleClass( "ui-tabs-collapsible", null, value );
// Setting collapsible: false while collapsed; open first panel
if ( !value && this.options.active === false ) {
this._activate( 0 );
}
}
if ( key === "event" ) {
this._setupEvents( value );
}
if ( key === "heightStyle" ) {
this._setupHeightStyle( value );
}
},
_sanitizeSelector: function( hash ) {
return hash ? hash.replace( /[!"$%&'()*+,.\/:;<=>?@\[\]\^`{|}~]/g, "\\$&" ) : "";
},
refresh: function() {
var options = this.options,
lis = this.tablist.children( ":has(a[href])" );
// Get disabled tabs from class attribute from HTML
// this will get converted to a boolean if needed in _refresh()
options.disabled = $.map( lis.filter( ".ui-state-disabled" ), function( tab ) {
return lis.index( tab );
} );
this._processTabs();
// Was collapsed or no tabs
if ( options.active === false || !this.anchors.length ) {
options.active = false;
this.active = $();
// was active, but active tab is gone
} else if ( this.active.length && !$.contains( this.tablist[ 0 ], this.active[ 0 ] ) ) {
// all remaining tabs are disabled
if ( this.tabs.length === options.disabled.length ) {
options.active = false;
this.active = $();
// activate previous tab
} else {
this._activate( this._findNextTab( Math.max( 0, options.active - 1 ), false ) );
}
// was active, active tab still exists
} else {
// make sure active index is correct
options.active = this.tabs.index( this.active );
}
this._refresh();
},
_refresh: function() {
this._setOptionDisabled( this.options.disabled );
this._setupEvents( this.options.event );
this._setupHeightStyle( this.options.heightStyle );
this.tabs.not( this.active ).attr( {
"aria-selected": "false",
"aria-expanded": "false",
tabIndex: -1
} );
this.panels.not( this._getPanelForTab( this.active ) )
.hide()
.attr( {
"aria-hidden": "true"
} );
// Make sure one tab is in the tab order
if ( !this.active.length ) {
this.tabs.eq( 0 ).attr( "tabIndex", 0 );
} else {
this.active
.attr( {
"aria-selected": "true",
"aria-expanded": "true",
tabIndex: 0
} );
this._addClass( this.active, "ui-tabs-active", "ui-state-active" );
this._getPanelForTab( this.active )
.show()
.attr( {
"aria-hidden": "false"
} );
}
},
_processTabs: function() {
var that = this,
prevTabs = this.tabs,
prevAnchors = this.anchors,
prevPanels = this.panels;
this.tablist = this._getList().attr( "role", "tablist" );
this._addClass( this.tablist, "ui-tabs-nav",
"ui-helper-reset ui-helper-clearfix ui-widget-header" );
// Prevent users from focusing disabled tabs via click
this.tablist
.on( "mousedown" + this.eventNamespace, "> li", function( event ) {
if ( $( this ).is( ".ui-state-disabled" ) ) {
event.preventDefault();
}
} )
// Support: IE <9
// Preventing the default action in mousedown doesn't prevent IE
// from focusing the element, so if the anchor gets focused, blur.
// We don't have to worry about focusing the previously focused
// element since clicking on a non-focusable element should focus
// the body anyway.
.on( "focus" + this.eventNamespace, ".ui-tabs-anchor", function() {
if ( $( this ).closest( "li" ).is( ".ui-state-disabled" ) ) {
this.blur();
}
} );
this.tabs = this.tablist.find( "> li:has(a[href])" )
.attr( {
role: "tab",
tabIndex: -1
} );
this._addClass( this.tabs, "ui-tabs-tab", "ui-state-default" );
this.anchors = this.tabs.map( function() {
return $( "a", this )[ 0 ];
} )
.attr( {
tabIndex: -1
} );
this._addClass( this.anchors, "ui-tabs-anchor" );
this.panels = $();
this.anchors.each( function( i, anchor ) {
var selector, panel, panelId,
anchorId = $( anchor ).uniqueId().attr( "id" ),
tab = $( anchor ).closest( "li" ),
originalAriaControls = tab.attr( "aria-controls" );
// Inline tab
if ( that._isLocal( anchor ) ) {
selector = anchor.hash;
panelId = selector.substring( 1 );
panel = that.element.find( that._sanitizeSelector( selector ) );
// remote tab
} else {
// If the tab doesn't already have aria-controls,
// generate an id by using a throw-away element
panelId = tab.attr( "aria-controls" ) || $( {} ).uniqueId()[ 0 ].id;
selector = "#" + panelId;
panel = that.element.find( selector );
if ( !panel.length ) {
panel = that._createPanel( panelId );
panel.insertAfter( that.panels[ i - 1 ] || that.tablist );
}
panel.attr( "aria-live", "polite" );
}
if ( panel.length ) {
that.panels = that.panels.add( panel );
}
if ( originalAriaControls ) {
tab.data( "ui-tabs-aria-controls", originalAriaControls );
}
tab.attr( {
"aria-controls": panelId,
"aria-labelledby": anchorId
} );
panel.attr( "aria-labelledby", anchorId );
} );
this.panels.attr( "role", "tabpanel" );
this._addClass( this.panels, "ui-tabs-panel", "ui-widget-content" );
// Avoid memory leaks (#10056)
if ( prevTabs ) {
this._off( prevTabs.not( this.tabs ) );
this._off( prevAnchors.not( this.anchors ) );
this._off( prevPanels.not( this.panels ) );
}
},
// Allow overriding how to find the list for rare usage scenarios (#7715)
_getList: function() {
return this.tablist || this.element.find( "ol, ul" ).eq( 0 );
},
_createPanel: function( id ) {
return $( "" )
.attr( "id", id )
.data( "ui-tabs-destroy", true );
},
_setOptionDisabled: function( disabled ) {
var currentItem, li, i;
if ( Array.isArray( disabled ) ) {
if ( !disabled.length ) {
disabled = false;
} else if ( disabled.length === this.anchors.length ) {
disabled = true;
}
}
// Disable tabs
for ( i = 0; ( li = this.tabs[ i ] ); i++ ) {
currentItem = $( li );
if ( disabled === true || $.inArray( i, disabled ) !== -1 ) {
currentItem.attr( "aria-disabled", "true" );
this._addClass( currentItem, null, "ui-state-disabled" );
} else {
currentItem.removeAttr( "aria-disabled" );
this._removeClass( currentItem, null, "ui-state-disabled" );
}
}
this.options.disabled = disabled;
this._toggleClass( this.widget(), this.widgetFullName + "-disabled", null,
disabled === true );
},
_setupEvents: function( event ) {
var events = {};
if ( event ) {
$.each( event.split( " " ), function( index, eventName ) {
events[ eventName ] = "_eventHandler";
} );
}
this._off( this.anchors.add( this.tabs ).add( this.panels ) );
// Always prevent the default action, even when disabled
this._on( true, this.anchors, {
click: function( event ) {
event.preventDefault();
}
} );
this._on( this.anchors, events );
this._on( this.tabs, { keydown: "_tabKeydown" } );
this._on( this.panels, { keydown: "_panelKeydown" } );
this._focusable( this.tabs );
this._hoverable( this.tabs );
},
_setupHeightStyle: function( heightStyle ) {
var maxHeight,
parent = this.element.parent();
if ( heightStyle === "fill" ) {
maxHeight = parent.height();
maxHeight -= this.element.outerHeight() - this.element.height();
this.element.siblings( ":visible" ).each( function() {
var elem = $( this ),
position = elem.css( "position" );
if ( position === "absolute" || position === "fixed" ) {
return;
}
maxHeight -= elem.outerHeight( true );
} );
this.element.children().not( this.panels ).each( function() {
maxHeight -= $( this ).outerHeight( true );
} );
this.panels.each( function() {
$( this ).height( Math.max( 0, maxHeight -
$( this ).innerHeight() + $( this ).height() ) );
} )
.css( "overflow", "auto" );
} else if ( heightStyle === "auto" ) {
maxHeight = 0;
this.panels.each( function() {
maxHeight = Math.max( maxHeight, $( this ).height( "" ).height() );
} ).height( maxHeight );
}
},
_eventHandler: function( event ) {
var options = this.options,
active = this.active,
anchor = $( event.currentTarget ),
tab = anchor.closest( "li" ),
clickedIsActive = tab[ 0 ] === active[ 0 ],
collapsing = clickedIsActive && options.collapsible,
toShow = collapsing ? $() : this._getPanelForTab( tab ),
toHide = !active.length ? $() : this._getPanelForTab( active ),
eventData = {
oldTab: active,
oldPanel: toHide,
newTab: collapsing ? $() : tab,
newPanel: toShow
};
event.preventDefault();
if ( tab.hasClass( "ui-state-disabled" ) ||
// tab is already loading
tab.hasClass( "ui-tabs-loading" ) ||
// can't switch durning an animation
this.running ||
// click on active header, but not collapsible
( clickedIsActive && !options.collapsible ) ||
// allow canceling activation
( this._trigger( "beforeActivate", event, eventData ) === false ) ) {
return;
}
options.active = collapsing ? false : this.tabs.index( tab );
this.active = clickedIsActive ? $() : tab;
if ( this.xhr ) {
this.xhr.abort();
}
if ( !toHide.length && !toShow.length ) {
$.error( "jQuery UI Tabs: Mismatching fragment identifier." );
}
if ( toShow.length ) {
this.load( this.tabs.index( tab ), event );
}
this._toggle( event, eventData );
},
// Handles show/hide for selecting tabs
_toggle: function( event, eventData ) {
var that = this,
toShow = eventData.newPanel,
toHide = eventData.oldPanel;
this.running = true;
function complete() {
that.running = false;
that._trigger( "activate", event, eventData );
}
function show() {
that._addClass( eventData.newTab.closest( "li" ), "ui-tabs-active", "ui-state-active" );
if ( toShow.length && that.options.show ) {
that._show( toShow, that.options.show, complete );
} else {
toShow.show();
complete();
}
}
// Start out by hiding, then showing, then completing
if ( toHide.length && this.options.hide ) {
this._hide( toHide, this.options.hide, function() {
that._removeClass( eventData.oldTab.closest( "li" ),
"ui-tabs-active", "ui-state-active" );
show();
} );
} else {
this._removeClass( eventData.oldTab.closest( "li" ),
"ui-tabs-active", "ui-state-active" );
toHide.hide();
show();
}
toHide.attr( "aria-hidden", "true" );
eventData.oldTab.attr( {
"aria-selected": "false",
"aria-expanded": "false"
} );
// If we're switching tabs, remove the old tab from the tab order.
// If we're opening from collapsed state, remove the previous tab from the tab order.
// If we're collapsing, then keep the collapsing tab in the tab order.
if ( toShow.length && toHide.length ) {
eventData.oldTab.attr( "tabIndex", -1 );
} else if ( toShow.length ) {
this.tabs.filter( function() {
return $( this ).attr( "tabIndex" ) === 0;
} )
.attr( "tabIndex", -1 );
}
toShow.attr( "aria-hidden", "false" );
eventData.newTab.attr( {
"aria-selected": "true",
"aria-expanded": "true",
tabIndex: 0
} );
},
_activate: function( index ) {
var anchor,
active = this._findActive( index );
// Trying to activate the already active panel
if ( active[ 0 ] === this.active[ 0 ] ) {
return;
}
// Trying to collapse, simulate a click on the current active header
if ( !active.length ) {
active = this.active;
}
anchor = active.find( ".ui-tabs-anchor" )[ 0 ];
this._eventHandler( {
target: anchor,
currentTarget: anchor,
preventDefault: $.noop
} );
},
_findActive: function( index ) {
return index === false ? $() : this.tabs.eq( index );
},
_getIndex: function( index ) {
// meta-function to give users option to provide a href string instead of a numerical index.
if ( typeof index === "string" ) {
index = this.anchors.index( this.anchors.filter( "[href$='" +
$.escapeSelector( index ) + "']" ) );
}
return index;
},
_destroy: function() {
if ( this.xhr ) {
this.xhr.abort();
}
this.tablist
.removeAttr( "role" )
.off( this.eventNamespace );
this.anchors
.removeAttr( "role tabIndex" )
.removeUniqueId();
this.tabs.add( this.panels ).each( function() {
if ( $.data( this, "ui-tabs-destroy" ) ) {
$( this ).remove();
} else {
$( this ).removeAttr( "role tabIndex " +
"aria-live aria-busy aria-selected aria-labelledby aria-hidden aria-expanded" );
}
} );
this.tabs.each( function() {
var li = $( this ),
prev = li.data( "ui-tabs-aria-controls" );
if ( prev ) {
li
.attr( "aria-controls", prev )
.removeData( "ui-tabs-aria-controls" );
} else {
li.removeAttr( "aria-controls" );
}
} );
this.panels.show();
if ( this.options.heightStyle !== "content" ) {
this.panels.css( "height", "" );
}
},
enable: function( index ) {
var disabled = this.options.disabled;
if ( disabled === false ) {
return;
}
if ( index === undefined ) {
disabled = false;
} else {
index = this._getIndex( index );
if ( Array.isArray( disabled ) ) {
disabled = $.map( disabled, function( num ) {
return num !== index ? num : null;
} );
} else {
disabled = $.map( this.tabs, function( li, num ) {
return num !== index ? num : null;
} );
}
}
this._setOptionDisabled( disabled );
},
disable: function( index ) {
var disabled = this.options.disabled;
if ( disabled === true ) {
return;
}
if ( index === undefined ) {
disabled = true;
} else {
index = this._getIndex( index );
if ( $.inArray( index, disabled ) !== -1 ) {
return;
}
if ( Array.isArray( disabled ) ) {
disabled = $.merge( [ index ], disabled ).sort();
} else {
disabled = [ index ];
}
}
this._setOptionDisabled( disabled );
},
load: function( index, event ) {
index = this._getIndex( index );
var that = this,
tab = this.tabs.eq( index ),
anchor = tab.find( ".ui-tabs-anchor" ),
panel = this._getPanelForTab( tab ),
eventData = {
tab: tab,
panel: panel
},
complete = function( jqXHR, status ) {
if ( status === "abort" ) {
that.panels.stop( false, true );
}
that._removeClass( tab, "ui-tabs-loading" );
panel.removeAttr( "aria-busy" );
if ( jqXHR === that.xhr ) {
delete that.xhr;
}
};
// Not remote
if ( this._isLocal( anchor[ 0 ] ) ) {
return;
}
this.xhr = $.ajax( this._ajaxSettings( anchor, event, eventData ) );
// Support: jQuery <1.8
// jQuery <1.8 returns false if the request is canceled in beforeSend,
// but as of 1.8, $.ajax() always returns a jqXHR object.
if ( this.xhr && this.xhr.statusText !== "canceled" ) {
this._addClass( tab, "ui-tabs-loading" );
panel.attr( "aria-busy", "true" );
this.xhr
.done( function( response, status, jqXHR ) {
// support: jQuery <1.8
// http://bugs.jquery.com/ticket/11778
setTimeout( function() {
panel.html( response );
that._trigger( "load", event, eventData );
complete( jqXHR, status );
}, 1 );
} )
.fail( function( jqXHR, status ) {
// support: jQuery <1.8
// http://bugs.jquery.com/ticket/11778
setTimeout( function() {
complete( jqXHR, status );
}, 1 );
} );
}
},
_ajaxSettings: function( anchor, event, eventData ) {
var that = this;
return {
// Support: IE <11 only
// Strip any hash that exists to prevent errors with the Ajax request
url: anchor.attr( "href" ).replace( /#.*$/, "" ),
beforeSend: function( jqXHR, settings ) {
return that._trigger( "beforeLoad", event,
$.extend( { jqXHR: jqXHR, ajaxSettings: settings }, eventData ) );
}
};
},
_getPanelForTab: function( tab ) {
var id = $( tab ).attr( "aria-controls" );
return this.element.find( this._sanitizeSelector( "#" + id ) );
}
} );
// DEPRECATED
// TODO: Switch return back to widget declaration at top of file when this is removed
if ( $.uiBackCompat !== false ) {
// Backcompat for ui-tab class (now ui-tabs-tab)
$.widget( "ui.tabs", $.ui.tabs, {
_processTabs: function() {
this._superApply( arguments );
this._addClass( this.tabs, "ui-tab" );
}
} );
}
var widgetsTabs = $.ui.tabs;
/*!
* jQuery UI Tooltip 1.13.0
* http://jqueryui.com
*
* Copyright jQuery Foundation and other contributors
* Released under the MIT license.
* http://jquery.org/license
*/
//>>label: Tooltip
//>>group: Widgets
//>>description: Shows additional information for any element on hover or focus.
//>>docs: http://api.jqueryui.com/tooltip/
//>>demos: http://jqueryui.com/tooltip/
//>>css.structure: ../../themes/base/core.css
//>>css.structure: ../../themes/base/tooltip.css
//>>css.theme: ../../themes/base/theme.css
$.widget( "ui.tooltip", {
version: "1.13.0",
options: {
classes: {
"ui-tooltip": "ui-corner-all ui-widget-shadow"
},
content: function() {
var title = $( this ).attr( "title" );
// Escape title, since we're going from an attribute to raw HTML
return $( "
" ).text( title ).html();
},
hide: true,
// Disabled elements have inconsistent behavior across browsers (#8661)
items: "[title]:not([disabled])",
position: {
my: "left top+15",
at: "left bottom",
collision: "flipfit flip"
},
show: true,
track: false,
// Callbacks
close: null,
open: null
},
_addDescribedBy: function( elem, id ) {
var describedby = ( elem.attr( "aria-describedby" ) || "" ).split( /\s+/ );
describedby.push( id );
elem
.data( "ui-tooltip-id", id )
.attr( "aria-describedby", String.prototype.trim.call( describedby.join( " " ) ) );
},
_removeDescribedBy: function( elem ) {
var id = elem.data( "ui-tooltip-id" ),
describedby = ( elem.attr( "aria-describedby" ) || "" ).split( /\s+/ ),
index = $.inArray( id, describedby );
if ( index !== -1 ) {
describedby.splice( index, 1 );
}
elem.removeData( "ui-tooltip-id" );
describedby = String.prototype.trim.call( describedby.join( " " ) );
if ( describedby ) {
elem.attr( "aria-describedby", describedby );
} else {
elem.removeAttr( "aria-describedby" );
}
},
_create: function() {
this._on( {
mouseover: "open",
focusin: "open"
} );
// IDs of generated tooltips, needed for destroy
this.tooltips = {};
// IDs of parent tooltips where we removed the title attribute
this.parents = {};
// Append the aria-live region so tooltips announce correctly
this.liveRegion = $( "" )
.attr( {
role: "log",
"aria-live": "assertive",
"aria-relevant": "additions"
} )
.appendTo( this.document[ 0 ].body );
this._addClass( this.liveRegion, null, "ui-helper-hidden-accessible" );
this.disabledTitles = $( [] );
},
_setOption: function( key, value ) {
var that = this;
this._super( key, value );
if ( key === "content" ) {
$.each( this.tooltips, function( id, tooltipData ) {
that._updateContent( tooltipData.element );
} );
}
},
_setOptionDisabled: function( value ) {
this[ value ? "_disable" : "_enable" ]();
},
_disable: function() {
var that = this;
// Close open tooltips
$.each( this.tooltips, function( id, tooltipData ) {
var event = $.Event( "blur" );
event.target = event.currentTarget = tooltipData.element[ 0 ];
that.close( event, true );
} );
// Remove title attributes to prevent native tooltips
this.disabledTitles = this.disabledTitles.add(
this.element.find( this.options.items ).addBack()
.filter( function() {
var element = $( this );
if ( element.is( "[title]" ) ) {
return element
.data( "ui-tooltip-title", element.attr( "title" ) )
.removeAttr( "title" );
}
} )
);
},
_enable: function() {
// restore title attributes
this.disabledTitles.each( function() {
var element = $( this );
if ( element.data( "ui-tooltip-title" ) ) {
element.attr( "title", element.data( "ui-tooltip-title" ) );
}
} );
this.disabledTitles = $( [] );
},
open: function( event ) {
var that = this,
target = $( event ? event.target : this.element )
// we need closest here due to mouseover bubbling,
// but always pointing at the same event target
.closest( this.options.items );
// No element to show a tooltip for or the tooltip is already open
if ( !target.length || target.data( "ui-tooltip-id" ) ) {
return;
}
if ( target.attr( "title" ) ) {
target.data( "ui-tooltip-title", target.attr( "title" ) );
}
target.data( "ui-tooltip-open", true );
// Kill parent tooltips, custom or native, for hover
if ( event && event.type === "mouseover" ) {
target.parents().each( function() {
var parent = $( this ),
blurEvent;
if ( parent.data( "ui-tooltip-open" ) ) {
blurEvent = $.Event( "blur" );
blurEvent.target = blurEvent.currentTarget = this;
that.close( blurEvent, true );
}
if ( parent.attr( "title" ) ) {
parent.uniqueId();
that.parents[ this.id ] = {
element: this,
title: parent.attr( "title" )
};
parent.attr( "title", "" );
}
} );
}
this._registerCloseHandlers( event, target );
this._updateContent( target, event );
},
_updateContent: function( target, event ) {
var content,
contentOption = this.options.content,
that = this,
eventType = event ? event.type : null;
if ( typeof contentOption === "string" || contentOption.nodeType ||
contentOption.jquery ) {
return this._open( event, target, contentOption );
}
content = contentOption.call( target[ 0 ], function( response ) {
// IE may instantly serve a cached response for ajax requests
// delay this call to _open so the other call to _open runs first
that._delay( function() {
// Ignore async response if tooltip was closed already
if ( !target.data( "ui-tooltip-open" ) ) {
return;
}
// JQuery creates a special event for focusin when it doesn't
// exist natively. To improve performance, the native event
// object is reused and the type is changed. Therefore, we can't
// rely on the type being correct after the event finished
// bubbling, so we set it back to the previous value. (#8740)
if ( event ) {
event.type = eventType;
}
this._open( event, target, response );
} );
} );
if ( content ) {
this._open( event, target, content );
}
},
_open: function( event, target, content ) {
var tooltipData, tooltip, delayedShow, a11yContent,
positionOption = $.extend( {}, this.options.position );
if ( !content ) {
return;
}
// Content can be updated multiple times. If the tooltip already
// exists, then just update the content and bail.
tooltipData = this._find( target );
if ( tooltipData ) {
tooltipData.tooltip.find( ".ui-tooltip-content" ).html( content );
return;
}
// If we have a title, clear it to prevent the native tooltip
// we have to check first to avoid defining a title if none exists
// (we don't want to cause an element to start matching [title])
//
// We use removeAttr only for key events, to allow IE to export the correct
// accessible attributes. For mouse events, set to empty string to avoid
// native tooltip showing up (happens only when removing inside mouseover).
if ( target.is( "[title]" ) ) {
if ( event && event.type === "mouseover" ) {
target.attr( "title", "" );
} else {
target.removeAttr( "title" );
}
}
tooltipData = this._tooltip( target );
tooltip = tooltipData.tooltip;
this._addDescribedBy( target, tooltip.attr( "id" ) );
tooltip.find( ".ui-tooltip-content" ).html( content );
// Support: Voiceover on OS X, JAWS on IE <= 9
// JAWS announces deletions even when aria-relevant="additions"
// Voiceover will sometimes re-read the entire log region's contents from the beginning
this.liveRegion.children().hide();
a11yContent = $( "
" ).html( tooltip.find( ".ui-tooltip-content" ).html() );
a11yContent.removeAttr( "name" ).find( "[name]" ).removeAttr( "name" );
a11yContent.removeAttr( "id" ).find( "[id]" ).removeAttr( "id" );
a11yContent.appendTo( this.liveRegion );
function position( event ) {
positionOption.of = event;
if ( tooltip.is( ":hidden" ) ) {
return;
}
tooltip.position( positionOption );
}
if ( this.options.track && event && /^mouse/.test( event.type ) ) {
this._on( this.document, {
mousemove: position
} );
// trigger once to override element-relative positioning
position( event );
} else {
tooltip.position( $.extend( {
of: target
}, this.options.position ) );
}
tooltip.hide();
this._show( tooltip, this.options.show );
// Handle tracking tooltips that are shown with a delay (#8644). As soon
// as the tooltip is visible, position the tooltip using the most recent
// event.
// Adds the check to add the timers only when both delay and track options are set (#14682)
if ( this.options.track && this.options.show && this.options.show.delay ) {
delayedShow = this.delayedShow = setInterval( function() {
if ( tooltip.is( ":visible" ) ) {
position( positionOption.of );
clearInterval( delayedShow );
}
}, 13 );
}
this._trigger( "open", event, { tooltip: tooltip } );
},
_registerCloseHandlers: function( event, target ) {
var events = {
keyup: function( event ) {
if ( event.keyCode === $.ui.keyCode.ESCAPE ) {
var fakeEvent = $.Event( event );
fakeEvent.currentTarget = target[ 0 ];
this.close( fakeEvent, true );
}
}
};
// Only bind remove handler for delegated targets. Non-delegated
// tooltips will handle this in destroy.
if ( target[ 0 ] !== this.element[ 0 ] ) {
events.remove = function() {
this._removeTooltip( this._find( target ).tooltip );
};
}
if ( !event || event.type === "mouseover" ) {
events.mouseleave = "close";
}
if ( !event || event.type === "focusin" ) {
events.focusout = "close";
}
this._on( true, target, events );
},
close: function( event ) {
var tooltip,
that = this,
target = $( event ? event.currentTarget : this.element ),
tooltipData = this._find( target );
// The tooltip may already be closed
if ( !tooltipData ) {
// We set ui-tooltip-open immediately upon open (in open()), but only set the
// additional data once there's actually content to show (in _open()). So even if the
// tooltip doesn't have full data, we always remove ui-tooltip-open in case we're in
// the period between open() and _open().
target.removeData( "ui-tooltip-open" );
return;
}
tooltip = tooltipData.tooltip;
// Disabling closes the tooltip, so we need to track when we're closing
// to avoid an infinite loop in case the tooltip becomes disabled on close
if ( tooltipData.closing ) {
return;
}
// Clear the interval for delayed tracking tooltips
clearInterval( this.delayedShow );
// Only set title if we had one before (see comment in _open())
// If the title attribute has changed since open(), don't restore
if ( target.data( "ui-tooltip-title" ) && !target.attr( "title" ) ) {
target.attr( "title", target.data( "ui-tooltip-title" ) );
}
this._removeDescribedBy( target );
tooltipData.hiding = true;
tooltip.stop( true );
this._hide( tooltip, this.options.hide, function() {
that._removeTooltip( $( this ) );
} );
target.removeData( "ui-tooltip-open" );
this._off( target, "mouseleave focusout keyup" );
// Remove 'remove' binding only on delegated targets
if ( target[ 0 ] !== this.element[ 0 ] ) {
this._off( target, "remove" );
}
this._off( this.document, "mousemove" );
if ( event && event.type === "mouseleave" ) {
$.each( this.parents, function( id, parent ) {
$( parent.element ).attr( "title", parent.title );
delete that.parents[ id ];
} );
}
tooltipData.closing = true;
this._trigger( "close", event, { tooltip: tooltip } );
if ( !tooltipData.hiding ) {
tooltipData.closing = false;
}
},
_tooltip: function( element ) {
var tooltip = $( "
" ).attr( "role", "tooltip" ),
content = $( "
" ).appendTo( tooltip ),
id = tooltip.uniqueId().attr( "id" );
this._addClass( content, "ui-tooltip-content" );
this._addClass( tooltip, "ui-tooltip", "ui-widget ui-widget-content" );
tooltip.appendTo( this._appendTo( element ) );
return this.tooltips[ id ] = {
element: element,
tooltip: tooltip
};
},
_find: function( target ) {
var id = target.data( "ui-tooltip-id" );
return id ? this.tooltips[ id ] : null;
},
_removeTooltip: function( tooltip ) {
// Clear the interval for delayed tracking tooltips
clearInterval( this.delayedShow );
tooltip.remove();
delete this.tooltips[ tooltip.attr( "id" ) ];
},
_appendTo: function( target ) {
var element = target.closest( ".ui-front, dialog" );
if ( !element.length ) {
element = this.document[ 0 ].body;
}
return element;
},
_destroy: function() {
var that = this;
// Close open tooltips
$.each( this.tooltips, function( id, tooltipData ) {
// Delegate to close method to handle common cleanup
var event = $.Event( "blur" ),
element = tooltipData.element;
event.target = event.currentTarget = element[ 0 ];
that.close( event, true );
// Remove immediately; destroying an open tooltip doesn't use the
// hide animation
$( "#" + id ).remove();
// Restore the title
if ( element.data( "ui-tooltip-title" ) ) {
// If the title attribute has changed since open(), don't restore
if ( !element.attr( "title" ) ) {
element.attr( "title", element.data( "ui-tooltip-title" ) );
}
element.removeData( "ui-tooltip-title" );
}
} );
this.liveRegion.remove();
}
});
// DEPRECATED
// TODO: Switch return back to widget declaration at top of file when this is removed
if ( $.uiBackCompat !== false ) {
// Backcompat for tooltipClass option
$.widget( "ui.tooltip", $.ui.tooltip, {
options: {
tooltipClass: null
},
_tooltip: function() {
var tooltipData = this._superApply( arguments );
if ( this.options.tooltipClass ) {
tooltipData.tooltip.addClass( this.options.tooltipClass );
}
return tooltipData;
}
} );
}
var widgetsTooltip = $.ui.tooltip;
});
/*!
* jQuery UI Touch Punch 0.2.2
*
* Copyright 2011, Dave Furfero
* Dual licensed under the MIT or GPL Version 2 licenses.
*
* Depends:
* jquery.ui.widget.js
* jquery.ui.mouse.js
*/
(function ($) {
// Detect touch support
$.support.touch = ('ontouchend' in document || navigator.maxTouchPoints > 0);
// Ignore browsers without touch support
if (!$.support.touch) {
return;
}
var mouseProto = $.ui.mouse.prototype,
_mouseInit = mouseProto._mouseInit,
touchHandled;
/**
* Simulate a mouse event based on a corresponding touch event
* @param {Object} event A touch event
* @param {String} simulatedType The corresponding mouse event
*/
function simulateMouseEvent (event, simulatedType) {
// Ignore multi-touch events
if (event.originalEvent.touches.length > 1) {
return;
}
event.preventDefault();
var touch = event.originalEvent.changedTouches[0],
simulatedEvent = document.createEvent('MouseEvents');
// Initialize the simulated mouse event using the touch event's coordinates
simulatedEvent.initMouseEvent(
simulatedType, // type
true, // bubbles
true, // cancelable
window, // view
1, // detail
touch.screenX, // screenX
touch.screenY, // screenY
touch.clientX, // clientX
touch.clientY, // clientY
false, // ctrlKey
false, // altKey
false, // shiftKey
false, // metaKey
0, // button
null // relatedTarget
);
// Dispatch the simulated event to the target element
event.target.dispatchEvent(simulatedEvent);
}
/**
* Handle the jQuery UI widget's touchstart events
* @param {Object} event The widget element's touchstart event
*/
mouseProto._touchStart = function (event) {
var self = this;
// Ignore the event if another widget is already being handled
if (touchHandled || !self._mouseCapture(event.originalEvent.changedTouches[0])) {
return;
}
// Set the flag to prevent other widgets from inheriting the touch event
touchHandled = true;
// Track movement to determine if interaction was a click
self._touchMoved = false;
// Simulate the mouseover event
simulateMouseEvent(event, 'mouseover');
// Simulate the mousemove event
simulateMouseEvent(event, 'mousemove');
// Simulate the mousedown event
simulateMouseEvent(event, 'mousedown');
};
/**
* Handle the jQuery UI widget's touchmove events
* @param {Object} event The document's touchmove event
*/
mouseProto._touchMove = function (event) {
// Ignore event if not handled
if (!touchHandled) {
return;
}
// Interaction was not a click
this._touchMoved = true;
// Simulate the mousemove event
simulateMouseEvent(event, 'mousemove');
};
/**
* Handle the jQuery UI widget's touchend events
* @param {Object} event The document's touchend event
*/
mouseProto._touchEnd = function (event) {
// Ignore event if not handled
if (!touchHandled) {
return;
}
// Simulate the mouseup event
simulateMouseEvent(event, 'mouseup');
// Simulate the mouseout event
simulateMouseEvent(event, 'mouseout');
// If the touch interaction did not move, it should trigger a click
if (!this._touchMoved) {
// Simulate the click event
simulateMouseEvent(event, 'click');
}
// Unset the flag to allow other widgets to inherit the touch event
touchHandled = false;
};
/**
* A duck punch of the $.ui.mouse _mouseInit method to support touch events.
* This method extends the widget with bound touch event handlers that
* translate touch events to mouse events and pass them to the widget's
* original mouse event handling methods.
*/
mouseProto._mouseInit = function () {
var self = this;
// Delegate the touch handlers to the widget's element
self.element
.bind('touchstart', $.proxy(self, '_touchStart'))
.bind('touchmove', $.proxy(self, '_touchMove'))
.bind('touchend', $.proxy(self, '_touchEnd'));
// Call the original $.ui.mouse init method
_mouseInit.call(self);
};
})(jQuery);
(function (jQuery) {
// This is a hack to make ckeditor work inside modal dialogs. Since ckeditor dialogs are placed on body and not in the ui.dialog's DOM. See http://bugs.jqueryui.com/ticket/9087
jQuery.widget("ui.dialog", jQuery.ui.dialog, {
_allowInteraction: function (event) {
return true;
}
});
jQuery.ui.dialog.prototype._focusTabbable = function () {};
})(jQuery);
jQuery = oldJQuery;
;
!function(e){function t(r){if(n[r])return n[r].exports;var o=n[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,t),o.l=!0,o.exports}var n={};t.m=e,t.c=n,t.i=function(e){return e},t.d=function(e,n,r){t.o(e,n)||Object.defineProperty(e,n,{configurable:!1,enumerable:!0,get:r})},t.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(n,"a",n),n},t.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},t.p="",t(t.s=11)}([function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var r=t.curry=function(e){var t=e.length;return function n(){var r=Array.prototype.slice.call(arguments,0);return r.length>=t?e.apply(null,r):function(){var e=Array.prototype.slice.call(arguments,0);return n.apply(null,r.concat(e))}}},o=(t.compose=function(){for(var e=arguments.length,t=Array(e),n=0;n
=t?e.apply(null,r):function(){var e=Array.prototype.slice.call(arguments,0);return n.apply(null,r.concat(e))}}},o=function(e,t){return t.substr(0,1)===e},a=function(e,t){return t.substr(-1)===e},i=r(function(e,t){return o(e,t)&&(t=t.slice(1)),a(e,t)&&(t=t.slice(0,-1)),t}),s=function(e){for(var t=e.length;t>0;){var n=Math.floor(Math.random()*t);t--;var r=e[t];e[t]=e[n],e[n]=r}return e},l=function(e){var t=document.createElement("span");return t.innerHTML=e,t};t.default={curry:r,cleanCharacter:i,startsWith:o,endsWith:a,shuffle:s,createElementWithTextPart:l}},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var o=n(14),a=n(15),i=r(a),s=n(2),l=r(s),c=n(12),u=r(c),d=n(13),h=r(d),p=n(7),g=r(p),b=n(5),f=r(b),m=n(6),v=r(m),y=n(9),D=r(y),E=n(10),w=r(E);H5P.DragText=function(e,t,n){function r(n,r,o){var a=this;this.$=e(this),this.contentId=r,this.contentData=o,t.call(this,"drag-text"),this.params=e.extend(!0,{taskDescription:"Set in adjectives in the following sentence",textField:"This is a *nice*, *flexible* content type, which allows you to highlight all the *wonderful* words in this *exciting* sentence.\nThis is another line of *fantastic* text.",overallFeedback:[],checkAnswer:"Check",tryAgain:"Retry",behaviour:{enableRetry:!0,enableSolutionsButton:!0,enableCheckButton:!0,instantFeedback:!1},showSolution:"Show solution",dropZoneIndex:"Drop Zone @index.",empty:"Empty.",contains:"Drop Zone @index contains draggable @draggable.",ariaDraggableIndex:"@index of @count.",tipLabel:"Show tip",correctText:"Correct!",incorrectText:"Incorrect!",resetDropTitle:"Reset drop",resetDropDescription:"Are you sure you want to reset this drop zone?",grabbed:"Draggable is grabbed.",cancelledDragging:"Cancelled dragging.",correctAnswer:"Correct answer:",scoreBarLabel:"You got :num out of :total points",a11yCheck:"Check the answers. The responses will be marked as correct, incorrect, or unanswered.",a11yShowSolution:"Show the solution. The task will be marked with its correct solution.",a11yRetry:"Retry the task. Reset all responses and start the task over again."},n),this.contentData=o,void 0!==this.contentData&&void 0!==this.contentData.previousState&&void 0!==this.contentData.previousState.length&&(this.previousState=this.contentData.previousState),this.answered=!1,this.instantFeedbackEvaluationFilled=!1,this.textFieldHtml=this.params.textField.replace(/(\r\n|\n|\r)/gm," "),this.introductionId="h5p-drag-text-"+r+"-introduction",this.selectedElement=void 0,this.ariaDragControls=new f.default,this.ariaDropControls=new v.default,this.dragControls=new g.default([new D.default,new w.default,this.ariaDragControls]),this.dragControls.useNegativeTabIndex(),this.dropControls=new g.default([new D.default,new w.default,this.ariaDropControls]),this.dropControls.useNegativeTabIndex(),this.dragControls.on("before-select",function(e){return!a.isElementDisabled(e.element)}),this.dragControls.on("select",this.keyboardDraggableSelected,this),this.dropControls.on("select",this.keyboardDroppableSelected,this),this.on("start",this.addAllDroppablesToControls,this),this.on("revert",this.removeControlsFromEmptyDropZones,this),this.on("stop",function(e){e.data.target||a.removeControlsFromDropZonesIfAllEmpty()},this),this.on("drop",this.removeControlsFromEmptyDropZones,this),this.on("start",function(e){var t=e.data.element,n=a.getDraggableByElement(t);a.toggleDropEffect(),t.setAttribute("aria-grabbed","true"),a.setDraggableAriaLabel(n)}),this.on("stop",function(e){var t=e.data.element,n=a.getDraggableByElement(t);a.toggleDropEffect(),t.setAttribute("aria-grabbed","false"),a.setDraggableAriaLabel(n)}),this.on("drop",this.ariaDropControls.setAllToNone,this.ariaDropControls),this.on("drop",function(e){this.dragControls.removeElement(e.data.element)},this),this.on("revert",function(e){this.dragControls.insertElementAt(e.data.element,0)},this),this.on("drop",this.updateDroppableElement,this),this.on("revert",this.updateDroppableElement,this),this.initDragText(),this.stopWatch=new i.default,this.stopWatch.start(),this.on("resize",this.resize,this),this.on("revert",this.toggleDraggablesContainer,this),this.on("drop",this.toggleDraggablesContainer,this),this.on("stop",function(e){e.data.target||a.read(a.params.cancelledDragging)}),this.params.behaviour.instantFeedback&&this.on("revert",function(){return a.instantFeedbackEvaluation()})}return r.prototype=Object.create(t.prototype),r.prototype.constructor=r,r.prototype.updateDroppableElement=function(e){var t=e.data.target,n=e.data.element,r=this.getDroppableByElement(t);t&&this.setDroppableLabel(t,n.textContent,r.getIndex())},r.prototype.removeControlsFromDropZonesIfAllEmpty=function(){this.anyDropZoneHasDraggable()||this.removeAllDroppablesFromControls()},r.prototype.removeControlsFromEmptyDropZones=function(){var e=this;this.droppables.filter(function(e){return!e.hasDraggable()}).map(function(e){return e.getElement()}).forEach(function(t){e.dropControls.removeElement(t)})},r.prototype.addAllDroppablesToControls=function(){var e=this;this.dropControls.count()>0&&this.removeAllDroppablesFromControls(),this.droppables.map(function(e){return e.getElement()}).forEach(function(t){return e.dropControls.addElement(t)})},r.prototype.removeAllDroppablesFromControls=function(){var e=this;this.droppables.map(function(e){return e.getElement()}).forEach(function(t){return e.dropControls.removeElement(t)})},r.prototype.anyDropZoneHasDraggable=function(){return this.droppables.some(function(e){return e.hasDraggable()})},r.prototype.setDroppableLabel=function(e,t,n){var r=this.params.dropZoneIndex.replace("@index",n.toString()),o=e.classList.contains("h5p-drag-correct-feedback"),a=e.classList.contains("h5p-drag-wrong-feedback"),i=o||a,s=e.childNodes.length>0;if(e)if(i){var l=this.getDroppableByElement(e),c="";c=o?l.correctFeedback?l.correctFeedback:this.params.correctText:l.incorrectFeedback?l.incorrectFeedback:this.params.incorrectText,e.setAttribute("aria-label",r+" "+this.params.contains.replace("@index",n.toString()).replace("@draggable",t)+" "+c+".")}else s?e.setAttribute("aria-label",r+" "+this.params.contains.replace("@index",n.toString()).replace("@draggable",t)):e.setAttribute("aria-label",r+" "+this.params.empty.replace("@index",n.toString()))},r.prototype.registerDomElements=function(){this.$introduction=e(''+this.params.taskDescription+"
"),this.setIntroduction(this.$introduction),this.$introduction.parent().attr("tabindex","-1"),this.setContent(this.$inner),this.addButtons()},r.prototype.initDragText=function(){return this.$inner=e("
",{"aria-describedby":this.introductionId,class:"h5p-drag-inner"}),this.addTaskTo(this.$inner),this.setH5PUserState(),this.$inner},r.prototype.resize=function(){this.changeLayoutToFitWidth()},r.prototype.changeLayoutToFitWidth=function(){var e=this;e.addDropzoneWidth(),e.$inner.width()/parseFloat(e.$inner.css("font-size"),10)>43&&e.widestDraggable<=e.$inner.width()/3?(e.$draggables.addClass("h5p-drag-wide-screen"),e.$wordContainer.detach().appendTo(e.$taskContainer),e.draggables.forEach(function(e){e.getDraggableElement().addClass("h5p-drag-draggable-wide-screen")}),e.$wordContainer.css({"margin-right":e.$draggables.width()})):(e.$wordContainer.css({"margin-right":0}),e.$draggables.removeClass("h5p-drag-wide-screen"),e.$draggables.detach().appendTo(e.$taskContainer),e.draggables.forEach(function(e){e.getDraggableElement().removeClass("h5p-drag-draggable-wide-screen")}))},r.prototype.addButtons=function(){var e=this;e.params.behaviour.enableCheckButton&&e.addButton("check-answer",e.params.checkAnswer,function(){e.answered=!0,e.removeAllElementsFromDragControl(),e.showEvaluation()?(e.hideButton("show-solution"),e.hideButton("try-again"),e.hideButton("check-answer")):(e.params.behaviour.enableRetry&&e.showButton("try-again"),e.params.behaviour.enableSolutionsButton&&e.showButton("show-solution"),e.hideButton("check-answer"),e.disableDraggables()),e.$introduction.parent().focus()},!e.params.behaviour.instantFeedback,{"aria-label":e.params.a11yCheck}),e.addButton("show-solution",e.params.showSolution,function(){e.droppables.forEach(function(e){e.showSolution()}),e.draggables.forEach(function(t){return e.setDraggableAriaLabel(t)}),e.disableDraggables(),e.removeAllDroppablesFromControls(),e.hideButton("show-solution")},e.initShowShowSolutionButton||!1,{"aria-label":e.params.a11yShowSolution}),e.addButton("try-again",e.params.tryAgain,function(){e.answered&&e.resetDraggables(),e.answered=!1,e.hideEvaluation(),e.hideExplanation(),e.hideButton("try-again"),e.hideButton("show-solution"),e.params.behaviour.instantFeedback?e.enableAllDropzonesAndDraggables():(e.showButton("check-answer"),e.enableDraggables()),e.hideAllSolutions(),e.stopWatch.reset(),e.read(e.params.taskDescription)},e.initShowTryAgainButton||!1,{"aria-label":e.params.a11yRetry})},r.prototype.removeAllElementsFromDragControl=function(){var e=this;this.dragControls.elements.forEach(function(t){return e.dragControls.removeElement(t)})},r.prototype.keyboardDraggableSelected=function(e){var t=this.selectedElement,n=void 0!==this.selectedElement,r=this.selectedElement===e.element;n&&(this.selectedElement=void 0,this.trigger("stop",{element:t})),n&&r||this.isElementDisabled(e.element)||(this.selectedElement=e.element,this.trigger("start",{element:e.element}),this.focusOnFirstEmptyDropZone())},r.prototype.focusOnFirstEmptyDropZone=function(){var e=this.droppables.filter(function(e){return!e.hasDraggable()})[0],t=e.getElement();this.dropControls.setTabbable(t),t.focus()},r.prototype.isElementDisabled=function(e){return"true"===e.getAttribute("aria-disabled")},r.prototype.keyboardDroppableSelected=function(e){var t=this,n=e.element,r=t.getDroppableByElement(n),o=t.getDraggableByElement(this.selectedElement),a=this.params.behaviour.instantFeedback&&r&&r.isCorrect(),i=!this.params.behaviour.instantFeedback&&r.hasFeedback();if(o&&r&&!a){var s=t.selectedElement;t.drop(o,r),t.selectedElement=void 0,this.trigger("stop",{element:s,target:r.getElement()})}else if(r&&r.hasDraggable()&&!i&&!a){var l=n.querySelector("[aria-grabbed]");this.createConfirmResetDialog(function(){t.revert(t.getDraggableByElement(l))}).show()}},r.prototype.toggleDraggablesContainer=function(){var e=0===this.$draggables.children().length;this.$draggables.toggleClass("hide",e)},r.prototype.createConfirmResetDialog=function(e,t){var r=this,o=new n({headerText:r.params.resetDropTitle,dialogText:r.params.resetDropDescription});return o.appendTo(document.body),o.on("confirmed",e,t||this),o},r.prototype.showDropzoneFeedback=function(){var e=this;this.droppables.forEach(function(t){t.addFeedback();var n=t.containedDraggable;t&&n&&(e.setDroppableLabel(t.getElement(),n.getElement().textContent,t.getIndex()),e.setDraggableAriaLabel(n))})},r.prototype.showExplanation=function(){var e=this,t=[];this.droppables.forEach(function(e){var n=e.containedDraggable;e&&n&&(e.isCorrect()&&e.correctFeedback&&t.push({correct:n.text,text:e.correctFeedback}),!e.isCorrect()&&e.incorrectFeedback&&t.push({correct:e.text,wrong:n.text,text:e.incorrectFeedback}))}),0!==t.length&&this.setExplanation(t,e.params.feedbackHeader)},r.prototype.showEvaluation=function(e){this.hideEvaluation(),this.showDropzoneFeedback(),this.showExplanation();var t=this.calculateScore(),n=this.droppables.length;if(!e){var r=this.createXAPIEventTemplate("answered");this.addQuestionToXAPI(r),this.addResponseToXAPI(r),this.trigger(r)}var o=H5P.Question.determineOverallFeedback(this.params.overallFeedback,t/n).replace(/@score/g,t.toString()).replace(/@total/g,n.toString());return t===n&&(this.hideButton("check-answer"),this.hideButton("show-solution"),this.hideButton("try-again"),this.disableDraggables()),this.trigger("resize"),this.setFeedback(o,t,n,this.params.scoreBarLabel),t===n},r.prototype.calculateScore=function(){return this.droppables.reduce(function(e,t){return e+(t.isCorrect()?1:0)},0)},r.prototype.hideEvaluation=function(){this.removeFeedback(),this.trigger("resize")},r.prototype.hideExplanation=function(){this.setExplanation(),this.trigger("resize")},r.prototype.hideAllSolutions=function(){this.droppables.forEach(function(e){e.hideSolution()}),this.trigger("resize")},r.prototype.addTaskTo=function(t){var n=this;n.widest=0,n.widestDraggable=0,n.droppables=[],n.draggables=[],n.$taskContainer=e("
",{class:"h5p-drag-task"}),n.$draggables=e("
",{class:"h5p-drag-draggables-container"}),n.$wordContainer=e("
",{class:"h5p-drag-droppable-words"}),(0,o.parseText)(n.textFieldHtml).forEach(function(e){if(n.isAnswerPart(e)){var t=(0,o.lex)(e),r=n.createDraggable(t.text),a=n.createDroppable(t.text,t.tip,t.correctFeedback,t.incorrectFeedback);n.params.behaviour.instantFeedback&&r.getDraggableElement().on("dragstop",function(){a.addFeedback(),n.instantFeedbackEvaluation()})}else{var i=l.default.createElementWithTextPart(e);n.$wordContainer.append(i)}}),n.shuffleAndAddDraggables(n.$draggables),n.$draggables.appendTo(n.$taskContainer),n.$wordContainer.appendTo(n.$taskContainer),n.$taskContainer.appendTo(t),n.addDropzoneWidth()},r.prototype.isAnswerPart=function(e){return l.default.startsWith("*",e)&&l.default.endsWith("*",e)},r.prototype.addDropzoneWidth=function(){var e=this,t=0,n=0,r=parseInt(this.$inner.css("font-size"),10),o=3*r,a=r;this.draggables.forEach(function(e){var r=e.getDraggableElement(),o=r.clone().css({position:"absolute","white-space":"nowrap",width:"auto",padding:0,margin:0}).html(e.getAnswerText()).appendTo(r.parent()),i=o.outerWidth();n=i>n?i:n,o.text().length>=20&&(o.html(e.getShortFormat()),i=o.width()),i+a>t&&(t=i+a),o.remove()}),t",{html:""+t+" ",role:"button","aria-grabbed":"false",tabindex:"-1"}).draggable({revert:function(e){return e||n.revert(o),!1},drag:n.propagateDragEvent("drag",n),start:n.propagateDragEvent("start",n),stop:function(e){n.trigger("stop",{element:o.getElement(),target:e.target})},containment:n.$taskContainer}).append(e("",{class:"h5p-hidden-read"})),o=new u.default(t,r,n.draggables.length);return o.on("addedToZone",function(){n.triggerXAPI("interacted")}),n.draggables.push(o),o},r.prototype.createDroppable=function(t,n,r,o){var a=this,i=this.draggables.length,s=e("
",{class:"h5p-drag-dropzone-container"}),l=e("
",{"aria-dropeffect":"none","aria-label":this.params.dropZoneIndex.replace("@index",i.toString())+" "+this.params.empty.replace("@index",i.toString()),tabindex:"-1"}).appendTo(s).droppable({tolerance:"pointer",drop:function(e,t){var n=a.getDraggableByElement(t.draggable[0]),r=a.getDroppableByElement(e.target);n&&r&&a.drop(n,r)}}),c=new h.default(t,n,r,o,l,s,i,a.params);return c.appendDroppableTo(a.$wordContainer),a.droppables.push(c),c},r.prototype.propagateDragEvent=l.default.curry(function(e,t,n){t.trigger(e,{element:n.target})}),r.prototype.revert=function(e){var t=e.removeFromZone(),n=t?t.getElement():void 0;e.revertDraggableTo(this.$draggables),this.setDraggableAriaLabel(e),this.trigger("revert",{element:e.getElement(),target:n}),this.trigger("resize")},r.prototype.drop=function(e,t){var n=this;n.answered=!0,e.removeFromZone();var r=t.appendInsideDroppableTo(this.$draggables);r&&n.trigger("revert",{element:r.getElement(),target:t.getElement()}),t.setDraggable(e),e.appendDraggableTo(t.getDropzone()),n.params.behaviour.instantFeedback&&(t.addFeedback(),n.instantFeedbackEvaluation(),n.params.behaviour.enableRetry&&!t.isCorrect()||t.disableDropzoneAndContainedDraggable()),this.trigger("drop",{element:e.getElement(),target:t.getElement()}),this.trigger("resize"),t.getElement().focus()},r.prototype.shuffleAndAddDraggables=function(e){var t=this;return l.default.shuffle(this.draggables).map(function(e,t){return e.setIndex(t)}).map(function(n){return t.addDraggableToContainer(e,n)}).map(function(e){return t.setDraggableAriaLabel(e)}).map(function(e){return t.addDraggableToControls(t.dragControls,e)})},r.prototype.setDraggableAriaLabel=function(e){return e.updateAriaLabel(this.params.ariaDraggableIndex.replace("@index",(e.getIndex()+1).toString()).replace("@count",this.draggables.length.toString())),e},r.prototype.isGrabbed=function(e){return"true"===e.getAttribute("aria-grabbed")},r.prototype.addDraggableToContainer=function(e,t){return t.appendDraggableTo(e),t},r.prototype.addDraggableToControls=function(e,t){return e.addElement(t.getElement()),t},r.prototype.instantFeedbackEvaluation=function(){var e=this;e.isAllAnswersFilled()?(e.params.behaviour.enableSolutionsButton&&e.showButton("show-solution"),e.params.behaviour.enableRetry&&e.showButton("try-again"),e.showEvaluation(!e.instantFeedbackEvaluationFilled),e.instantFeedbackEvaluationFilled=!0):(e.instantFeedbackEvaluationFilled=!1,e.hideButton("try-again"),e.hideButton("show-solution"),e.hideEvaluation())},r.prototype.isAllAnswersFilled=function(){return this.draggables.every(function(e){return e.isInsideDropZone()})},r.prototype.enableAllDropzonesAndDraggables=function(){this.enableDraggables(),this.droppables.forEach(function(e){e.enableDropzone()})},r.prototype.disableDraggables=function(){this.draggables.forEach(function(e){e.disableDraggable()})},r.prototype.enableDraggables=function(){this.draggables.forEach(function(e){e.enableDraggable()})},r.prototype.getAnswerGiven=function(){return this.answered},r.prototype.getScore=function(){return this.calculateScore()},r.prototype.getMaxScore=function(){return this.droppables.length},r.prototype.getTitle=function(){return H5P.createTitle(this.contentData&&this.contentData.metadata&&this.contentData.metadata.title?this.contentData.metadata.title:"Drag the Words")},r.prototype.toggleDropEffect=function(){var e=void 0!==this.selectedElement;this.ariaDropControls[e?"setAllToMove":"setAllToNone"]()},r.prototype.getDraggableByElement=function(e){return this.draggables.filter(function(t){return t.$draggable.get(0)===e},this)[0]},r.prototype.getDroppableByElement=function(e){return this.droppables.filter(function(t){return t.$dropzone.get(0)===e},this)[0]},r.prototype.showSolutions=function(){this.showEvaluation(!0),this.droppables.forEach(function(e){e.addFeedback(),e.showSolution()}),this.removeAllDroppablesFromControls(),this.disableDraggables(),this.hideButton("try-again"),this.hideButton("show-solution"),this.hideButton("check-answer"),this.trigger("resize")},r.prototype.resetTask=function(){var e=this;e.answered=!1,e.instantFeedbackEvaluationFilled=!1,e.resetDraggables(),e.hideEvaluation(),e.hideExplanation(),e.enableAllDropzonesAndDraggables(),e.hideButton("try-again"),e.hideButton("show-solution"),e.params.behaviour.instantFeedback||e.showButton("check-answer"),e.hideAllSolutions(),this.trigger("resize")},r.prototype.resetDraggables=function(){l.default.shuffle(this.draggables).forEach(this.revert,this)},r.prototype.getCurrentState=function(){var e=this;if(void 0!==this.draggables)return this.draggables.filter(function(e){return null!==e.getInsideDropzone()}).map(function(t){return{draggable:t.getInitialIndex(),droppable:e.droppables.indexOf(t.getInsideDropzone())}})},r.prototype.setH5PUserState=function(){var e=this,t=this;void 0!==this.previousState&&(this.previousState.forEach(function(n){if(!t.isValidIndex(n.draggable)||!t.isValidIndex(n.droppable))throw new Error("Stored user state is invalid");var r=e.getDraggableByInitialIndex(n.draggable),o=t.droppables[n.droppable];t.drop(r,o),t.params.behaviour.instantFeedback&&(null!==o&&o.addFeedback(),o.isCorrect()&&o.disableDropzoneAndContainedDraggable())}),t.params.behaviour.instantFeedback&&t.isAllAnswersFilled()&&!t.showEvaluation()&&(t.params.behaviour.enableSolutionsButton&&(t.initShowShowSolutionButton=!0),t.params.behaviour.enableRetry&&(t.initShowTryAgainButton=!0)))},r.prototype.isValidIndex=function(e){return!isNaN(e)&&e=0},r.prototype.getDraggableByInitialIndex=function(e){return this.draggables.filter(function(t){return t.hasInitialIndex(e)})[0]},r.prototype.getXAPIData=function(){var e=this.createXAPIEventTemplate("answered");return this.addQuestionToXAPI(e),this.addResponseToXAPI(e),{statement:e.data.statement}},r.prototype.addQuestionToXAPI=function(t){var n=t.getVerifiedStatementValue(["object","definition"]);e.extend(n,this.getxAPIDefinition())},r.prototype.getxAPIDefinition=function(){var e={};e.interactionType="fill-in",e.type="http://adlnet.gov/expapi/activities/cmi.interaction";var t=this.textFieldHtml,n=this.params.taskDescription+" ";return e.description={"en-US":n+this.replaceSolutionsWithBlanks(t)},e.correctResponsesPattern=[this.getSolutionsFromQuestion(t)],e},r.prototype.addResponseToXAPI=function(e){var t,n=this,r=n.getScore(),o=n.droppables.length;e.setScoredResult(r,o,n);var a={min:0,raw:r,max:o,scaled:Math.round(r/o*1e4)/1e4};n.stopWatch&&(t="PT"+n.stopWatch.stop()+"S"),e.data.statement.result={response:n.getXAPIResponse(),score:a,duration:t,completion:!0}},r.prototype.getXAPIResponse=function(){return this.droppables.map(function(e){return e.hasDraggable()?e.containedDraggable.text:""}).join("[,]")},r.prototype.replaceSolutionsWithBlanks=function(e){var t=this;return(0,o.parseText)(e).map(function(e){return t.isAnswerPart(e)?"__________":e}).join("")},r.prototype.getSolutionsFromQuestion=function(e){return(0,o.parseText)(e).filter(this.isAnswerPart).map(function(e){return(0,o.lex)(e)}).map(function(e){return e.text}).join("[,]")},r}(H5P.jQuery,H5P.Question,H5P.ConfirmationDialog),t.default=H5P.DragText},function(e,t){},function(e,t,n){"use strict";function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}Object.defineProperty(t,"__esModule",{value:!0});var o=function(){function e(e,t){for(var n=0;n20&&(o.shortFormat=o.shortFormat.slice(0,17)+"...")}return t.prototype=Object.create(H5P.EventDispatcher.prototype),t.prototype.constructor=t,t.prototype.getIndex=function(){return this.index},t.prototype.setIndex=function(e){return this.index=e,this},t.prototype.getInitialIndex=function(){return this.initialIndex},t.prototype.hasInitialIndex=function(e){return this.initialIndex===e},t.prototype.appendDraggableTo=function(e){this.$draggable.detach().css({left:0,top:0}).appendTo(e)},t.prototype.revertDraggableTo=function(e){var t=this.$draggable.offset().left-e.offset().left,n=this.$draggable.offset().top-e.offset().top;this.$draggable.detach().prependTo(e).css({left:t,top:n}).animate({left:0,top:0})},t.prototype.toggleDroppedFeedback=function(e){e?this.$draggable.addClass("h5p-drag-dropped"):this.$draggable.removeClass("h5p-drag-dropped")},t.prototype.disableDraggable=function(){this.$draggable.draggable({disabled:!0})},t.prototype.enableDraggable=function(){this.$draggable.draggable({disabled:!1})},t.prototype.getDraggableElement=function(){return this.$draggable},t.prototype.updateAriaLabel=function(e){this.$ariaLabel.html(e)},t.prototype.getElement=function(){return this.$draggable.get(0)},t.prototype.removeFromZone=function(){var e=this.insideDropzone;return null!==this.insideDropzone&&(this.insideDropzone.removeFeedback(),this.insideDropzone.removeDraggable()),this.toggleDroppedFeedback(!1),this.removeShortFormat(),this.insideDropzone=null,e},t.prototype.addToZone=function(e){null!==this.insideDropzone&&this.insideDropzone.removeDraggable(),this.toggleDroppedFeedback(!0),this.insideDropzone=e,this.setShortFormat(),this.trigger("addedToZone")},t.prototype.getAnswerText=function(){return this.text},t.prototype.setShortFormat=function(){this.$draggable.html(this.shortFormat)},t.prototype.getShortFormat=function(){return this.shortFormat},t.prototype.removeShortFormat=function(){this.$draggable.html(this.text)},t.prototype.getInsideDropzone=function(){return this.insideDropzone},t.prototype.isInsideDropZone=function(){return!!this.insideDropzone},t}(H5P.jQuery),t.default=H5P.TextDraggable},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),H5P.TextDroppable=function(e){function t(t,r,o,a,i,s,l,c){var u=this;u.text=t,u.tip=r,u.correctFeedback=o,u.incorrectFeedback=a,u.index=l,u.params=c,u.containedDraggable=null,u.$dropzone=e(i),u.$dropzoneContainer=e(s),u.tip&&(u.$tip=H5P.JoubelUI.createTip(u.tip,{tipLabel:u.params.tipLabel,tabcontrol:!0}),u.$dropzoneContainer.append(u.$tip),u.$dropzone.focus(function(){return u.$tip.attr("tabindex","0")}),u.$dropzone.blur(function(){return u.removeTipTabIndexIfNoFocus()}),u.$tip.blur(function(){return u.removeTipTabIndexIfNoFocus()})),u.$incorrectText=e("
",{html:u.params.incorrectText+" "+u.params.correctAnswer,class:"correct-answer"}),u.$correctText=e("
",{html:u.params.correctText,class:"correct-answer"}),u.$showSolution=e("
",{class:n}).appendTo(u.$dropzoneContainer).hide()}var n="h5p-drag-show-solution-container",r="h5p-drag-correct-feedback",o="h5p-drag-wrong-feedback";return t.prototype.removeTipTabIndexIfNoFocus=function(){var e=this;setTimeout(function(){e.$dropzone.is(":focus")||e.$tip.is(":focus")||e.$tip.attr("tabindex","-1")},0)},t.prototype.showSolution=function(){var e=null!==this.containedDraggable&&this.containedDraggable.getAnswerText()===this.text;e||this.$showSolution.html(this.text),this.$showSolution.prepend(e?this.$correctText:this.$incorrectText),this.$showSolution.toggleClass("incorrect",!e),this.$showSolution.show()},t.prototype.hideSolution=function(){this.$showSolution.html(""),this.$showSolution.hide()},t.prototype.getElement=function(){return this.$dropzone.get(0)},t.prototype.appendDroppableTo=function(e){this.$dropzoneContainer.appendTo(e)},t.prototype.appendInsideDroppableTo=function(e){if(null!==this.containedDraggable)return this.containedDraggable.revertDraggableTo(e),this.containedDraggable},t.prototype.setDraggable=function(e){var t=this;t.containedDraggable!==e&&(null!==t.containedDraggable&&t.containedDraggable.removeFromZone(),t.containedDraggable=e,e.addToZone(t))},t.prototype.hasDraggable=function(){return!!this.containedDraggable},t.prototype.removeDraggable=function(){null!==this.containedDraggable&&(this.containedDraggable=null)},t.prototype.isCorrect=function(){return null!==this.containedDraggable&&this.containedDraggable.getAnswerText()===this.text},t.prototype.addFeedback=function(){this.isCorrect()?(this.$dropzone.removeClass(o).addClass(r),this.containedDraggable.getDraggableElement().removeClass("h5p-drag-draggable-wrong").addClass("h5p-drag-draggable-correct")):null===this.containedDraggable?this.$dropzone.removeClass(o).removeClass(r):(this.$dropzone.removeClass(r).addClass(o),null!==this.containedDraggable&&this.containedDraggable.getDraggableElement().addClass("h5p-drag-draggable-wrong").removeClass("h5p-drag-draggable-correct"))},t.prototype.removeFeedback=function(){this.$dropzone.removeClass(o).removeClass(r),null!==this.containedDraggable&&this.containedDraggable.getDraggableElement().removeClass("h5p-drag-draggable-wrong").removeClass("h5p-drag-draggable-correct")},t.prototype.hasFeedback=function(){return this.$dropzone.hasClass(o)||this.$dropzone.hasClass(r)},t.prototype.setShortFormat=function(){null!==this.containedDraggable&&this.containedDraggable.setShortFormat()},t.prototype.disableDropzoneAndContainedDraggable=function(){null!==this.containedDraggable&&this.containedDraggable.disableDraggable(),this.$dropzone.droppable({disabled:!0})},t.prototype.enableDropzone=function(){this.$dropzone.droppable({disabled:!1})},t.prototype.removeShortFormat=function(){null!==this.containedDraggable&&this.containedDraggable.removeShortFormat()},t.prototype.getDropzone=function(){return this.$dropzone},t.prototype.getIndex=function(){return this.index},t}(H5P.jQuery),t.default=H5P.TextDroppable},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.lex=t.parseText=void 0;var r=n(2),o=function(e){return e&&e.__esModule?e:{default:e}}(r),a=function(e){return e.split(/(\*.*?\*)/).filter(function(e){return e.length>0})},i=function(e){var t=e.match(/(:([^\\*]+))/g),n=e.match(/(\\\+([^\\*:]+))/g),r=e.match(/(\\\-([^\\*:]+))/g),a=o.default.cleanCharacter("*",e);return t&&(a=a.replace(t,""),t=t[0].replace(":",""),t=t.replace(/\s+$/,"")),n&&(a=a.replace(n,""),n=n[0].replace("\\+",""),n=n.replace(/\s+$/,"")),r&&(a=a.replace(r,""),r=r[0].replace("\\-",""),r=r.replace(/\s+$/,"")),a=a.replace(/\s+$/,""),{tip:t,correctFeedback:n,incorrectFeedback:r,text:a}};t.parseText=a,t.lex=i},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var r=r||{};r.DragText=r.DragText||{},r.DragText.StopWatch=function(){function e(){this.duration=0}return e.prototype.start=function(){return this.startTime=Date.now(),this},e.prototype.stop=function(){return this.duration=this.duration+Date.now()-this.startTime,this.passedTime()},e.prototype.reset=function(){this.duration=0,this.startTime=Date.now()},e.prototype.passedTime=function(){return Math.round(this.duration/10)/100},e}(),t.default=r.DragText.StopWatch}]);;
/*
* flowplayer.js 3.2.12. The Flowplayer API
*
* Copyright 2009-2011 Flowplayer Oy
*
* This file is part of Flowplayer.
*
* Flowplayer is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Flowplayer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Flowplayer. If not, see .
*
* Date: ${date}
* Revision: ${revision}
*/
!function(){function h(p){console.log("$f.fireEvent",[].slice.call(p))}function l(r){if(!r||typeof r!="object"){return r}var p=new r.constructor();for(var q in r){if(r.hasOwnProperty(q)){p[q]=l(r[q])}}return p}function n(u,r){if(!u){return}var p,q=0,s=u.length;if(s===undefined){for(p in u){if(r.call(u[p],p,u[p])===false){break}}}else{for(var t=u[0];q1){var u=arguments[1],r=(arguments.length==3)?arguments[2]:{};if(typeof u=="string"){u={src:u}}u=j({bgcolor:"#000000",version:[10,1],expressInstall:"http://releases.flowplayer.org/swf/expressinstall.swf",cachebusting:false},u);if(typeof p=="string"){if(p.indexOf(".")!=-1){var t=[];n(o(p),function(){t.push(new b(this,l(u),l(r)))});return new d(t)}else{var s=c(p);return new b(s!==null?s:l(p),l(u),l(r))}}else{if(p){return new b(p,l(u),l(r))}}}return null};j(window.$f,{fireEvent:function(){var q=[].slice.call(arguments);var r=$f(q[0]);return r?r._fireEvent(q.slice(1)):null},addPlugin:function(p,q){b.prototype[p]=q;return $f},each:n,extend:j});if(typeof jQuery=="function"){jQuery.fn.flowplayer=function(r,q){if(!arguments.length||typeof arguments[0]=="number"){var p=[];this.each(function(){var s=$f(this);if(s){p.push(s)}});return arguments.length?p[arguments[0]]:new d(p)}return this.each(function(){$f(this,l(r),q?l(q):{})})}}}();!function(){var h=document.all,j="http://get.adobe.com/flashplayer",c=typeof jQuery=="function",e=/(\d+)[^\d]+(\d+)[^\d]*(\d*)/,b={width:"100%",height:"100%",id:"_"+(""+Math.random()).slice(9),allowfullscreen:true,allowscriptaccess:"always",quality:"high",version:[3,0],onFail:null,expressInstall:null,w3c:false,cachebusting:false};if(window.attachEvent){window.attachEvent("onbeforeunload",function(){__flash_unloadHandler=function(){};__flash_savedUnloadHandler=function(){}})}function i(m,l){if(l){for(var f in l){if(l.hasOwnProperty(f)){m[f]=l[f]}}}return m}function a(f,n){var m=[];for(var l in f){if(f.hasOwnProperty(l)){m[l]=n(f[l])}}return m}window.flashembed=function(f,m,l){if(typeof f=="string"){f=document.getElementById(f.replace("#",""))}if(!f){return}if(typeof m=="string"){m={src:m}}return new d(f,i(i({},b),m),l)};var g=i(window.flashembed,{conf:b,getVersion:function(){var m,f;try{f=navigator.plugins["Shockwave Flash"].description.slice(16)}catch(o){try{m=new ActiveXObject("ShockwaveFlash.ShockwaveFlash.7");f=m&&m.GetVariable("$version")}catch(n){try{m=new ActiveXObject("ShockwaveFlash.ShockwaveFlash.6");f=m&&m.GetVariable("$version")}catch(l){}}}f=e.exec(f);return f?[1*f[1],1*f[(f[1]*1>9?2:3)]*1]:[0,0]},asString:function(l){if(l===null||l===undefined){return null}var f=typeof l;if(f=="object"&&l.push){f="array"}switch(f){case"string":l=l.replace(new RegExp('(["\\\\])',"g"),"\\$1");l=l.replace(/^\s?(\d+\.?\d*)%/,"$1pct");return'"'+l+'"';case"array":return"["+a(l,function(o){return g.asString(o)}).join(",")+"]";case"function":return'"function()"';case"object":var m=[];for(var n in l){if(l.hasOwnProperty(n)){m.push('"'+n+'":'+g.asString(l[n]))}}return"{"+m.join(",")+"}"}return String(l).replace(/\s/g," ").replace(/\'/g,'"')},getHTML:function(o,l){o=i({},o);var n='";if(o.w3c||h){n+=' '}o.width=o.height=o.id=o.w3c=o.src=null;o.onFail=o.version=o.expressInstall=null;for(var m in o){if(o[m]){n+=' '}}var p="";if(l){for(var f in l){if(l[f]){var q=l[f];p+=f+"="+(/function|object/.test(typeof q)?g.asString(q):q)+"&"}}p=p.slice(0,-1);n+=' "}n+=" ";return n},isSupported:function(f){return k[0]>f[0]||k[0]==f[0]&&k[1]>=f[1]}});var k=g.getVersion();function d(f,n,m){if(g.isSupported(n.version)){f.innerHTML=g.getHTML(n,m)}else{if(n.expressInstall&&g.isSupported([6,65])){f.innerHTML=g.getHTML(i(n,{src:n.expressInstall}),{MMredirectURL:encodeURIComponent(location.href),MMplayerType:"PlugIn",MMdoctitle:document.title})}else{if(!f.innerHTML.replace(/\s/g,"")){f.innerHTML="Flash version "+n.version+" or greater is required "+(k[0]>0?"Your version is "+k:"You have no flash plugin installed")+" "+(f.tagName=="A"?" Click here to download latest version
":"Download latest version from here
");if(f.tagName=="A"||f.tagName=="DIV"){f.onclick=function(){location.href=j}}}if(n.onFail){var l=n.onFail.call(this);if(typeof l=="string"){f.innerHTML=l}}}}if(h){window[n.id]=document.getElementById(n.id)}i(this,{getRoot:function(){return f},getOptions:function(){return n},getConf:function(){return m},getApi:function(){return f.firstChild}})}if(c){jQuery.tools=jQuery.tools||{version:"3.2.12"};jQuery.tools.flashembed={conf:b};jQuery.fn.flashembed=function(l,f){return this.each(function(){$(this).data("flashembed",flashembed(this,l,f))})}}}();;
/** @namespace H5P */
H5P.VideoYouTube = (function ($) {
/**
* YouTube video player for H5P.
*
* @class
* @param {Array} sources Video files to use
* @param {Object} options Settings for the player
* @param {Object} l10n Localization strings
*/
function YouTube(sources, options, l10n) {
var self = this;
var player;
var playbackRate = 1;
var id = 'h5p-youtube-' + numInstances;
numInstances++;
var $wrapper = $('
');
var $placeholder = $('
', {
id: id,
text: l10n.loading
}).appendTo($wrapper);
// Optional placeholder
// var $placeholder = $('VIDEO ').appendTo($wrapper);
/**
* Use the YouTube API to create a new player
*
* @private
*/
var create = function () {
if (!$placeholder.is(':visible') || player !== undefined) {
return;
}
if (window.YT === undefined) {
// Load API first
loadAPI(create);
return;
}
if (YT.Player === undefined) {
return;
}
var width = $wrapper.width();
if (width < 200) {
width = 200;
}
var loadCaptionsModule = true;
var videoId = getId(sources[0].path);
player = new YT.Player(id, {
width: width,
height: width * (9/16),
videoId: videoId,
playerVars: {
origin: ORIGIN,
autoplay: options.autoplay ? 1 : 0,
controls: options.controls ? 1 : 0,
disablekb: options.controls ? 0 : 1,
fs: 0,
loop: options.loop ? 1 : 0,
playlist: options.loop ? videoId : undefined,
rel: 0,
showinfo: 0,
iv_load_policy: 3,
wmode: "opaque",
start: options.startAt,
playsinline: 1
},
events: {
onReady: function () {
self.trigger('ready');
self.trigger('loaded');
},
onApiChange: function () {
if (loadCaptionsModule) {
loadCaptionsModule = false;
// Always load captions
player.loadModule('captions');
}
var trackList;
try {
// Grab tracklist from player
trackList = player.getOption('captions', 'tracklist');
}
catch (err) {}
if (trackList && trackList.length) {
// Format track list into valid track options
var trackOptions = [];
for (var i = 0; i < trackList.length; i++) {
trackOptions.push(new H5P.Video.LabelValue(trackList[i].displayName, trackList[i].languageCode));
}
// Captions are ready for loading
self.trigger('captions', trackOptions);
}
},
onStateChange: function (state) {
if (state.data > -1 && state.data < 4) {
// Fix for keeping playback rate in IE11
if (H5P.Video.IE11_PLAYBACK_RATE_FIX && state.data === H5P.Video.PLAYING && playbackRate !== 1) {
// YT doesn't know that IE11 changed the rate so it must be reset before it's set to the correct value
player.setPlaybackRate(1);
player.setPlaybackRate(playbackRate);
}
// End IE11 fix
self.trigger('stateChange', state.data);
}
},
onPlaybackQualityChange: function (quality) {
self.trigger('qualityChange', quality.data);
},
onPlaybackRateChange: function (playbackRate) {
self.trigger('playbackRateChange', playbackRate.data);
},
onError: function (error) {
var message;
switch (error.data) {
case 2:
message = l10n.invalidYtId;
break;
case 100:
message = l10n.unknownYtId;
break;
case 101:
case 150:
message = l10n.restrictedYt;
break;
default:
message = l10n.unknownError + ' ' + error.data;
break;
}
self.trigger('error', message);
}
}
});
};
/**
* Indicates if the video must be clicked for it to start playing.
* For instance YouTube videos on iPad must be pressed to start playing.
*
* @public
*/
self.pressToPlay = navigator.userAgent.match(/iPad/i) ? true : false;
/**
* Appends the video player to the DOM.
*
* @public
* @param {jQuery} $container
*/
self.appendTo = function ($container) {
$container.addClass('h5p-youtube').append($wrapper);
create();
};
/**
* Get list of available qualities. Not available until after play.
*
* @public
* @returns {Array}
*/
self.getQualities = function () {
if (!player || !player.getAvailableQualityLevels) {
return;
}
var qualities = player.getAvailableQualityLevels();
if (!qualities.length) {
return; // No qualities
}
// Add labels
for (var i = 0; i < qualities.length; i++) {
var quality = qualities[i];
var label = (LABELS[quality] !== undefined ? LABELS[quality] : 'Unknown'); // TODO: l10n
qualities[i] = {
name: quality,
label: LABELS[quality]
};
}
return qualities;
};
/**
* Get current playback quality. Not available until after play.
*
* @public
* @returns {String}
*/
self.getQuality = function () {
if (!player || !player.getPlaybackQuality) {
return;
}
var quality = player.getPlaybackQuality();
return quality === 'unknown' ? undefined : quality;
};
/**
* Set current playback quality. Not available until after play.
* Listen to event "qualityChange" to check if successful.
*
* @public
* @params {String} [quality]
*/
self.setQuality = function (quality) {
if (!player || !player.setPlaybackQuality) {
return;
}
player.setPlaybackQuality(quality);
};
/**
* Start the video.
*
* @public
*/
self.play = function () {
if (!player || !player.playVideo) {
self.on('ready', self.play);
return;
}
player.playVideo();
};
/**
* Pause the video.
*
* @public
*/
self.pause = function () {
self.off('ready', self.play);
if (!player || !player.pauseVideo) {
return;
}
player.pauseVideo();
};
/**
* Seek video to given time.
*
* @public
* @param {Number} time
*/
self.seek = function (time) {
if (!player || !player.seekTo) {
return;
}
player.seekTo(time, true);
};
/**
* Get elapsed time since video beginning.
*
* @public
* @returns {Number}
*/
self.getCurrentTime = function () {
if (!player || !player.getCurrentTime) {
return;
}
return player.getCurrentTime();
};
/**
* Get total video duration time.
*
* @public
* @returns {Number}
*/
self.getDuration = function () {
if (!player || !player.getDuration) {
return;
}
return player.getDuration();
};
/**
* Get percentage of video that is buffered.
*
* @public
* @returns {Number} Between 0 and 100
*/
self.getBuffered = function () {
if (!player || !player.getVideoLoadedFraction) {
return;
}
return player.getVideoLoadedFraction() * 100;
};
/**
* Turn off video sound.
*
* @public
*/
self.mute = function () {
if (!player || !player.mute) {
return;
}
player.mute();
};
/**
* Turn on video sound.
*
* @public
*/
self.unMute = function () {
if (!player || !player.unMute) {
return;
}
player.unMute();
};
/**
* Check if video sound is turned on or off.
*
* @public
* @returns {Boolean}
*/
self.isMuted = function () {
if (!player || !player.isMuted) {
return;
}
return player.isMuted();
};
/**
* Return the video sound level.
*
* @public
* @returns {Number} Between 0 and 100.
*/
self.getVolume = function () {
if (!player || !player.getVolume) {
return;
}
return player.getVolume();
};
/**
* Set video sound level.
*
* @public
* @param {Number} level Between 0 and 100.
*/
self.setVolume = function (level) {
if (!player || !player.setVolume) {
return;
}
player.setVolume(level);
};
/**
* Get list of available playback rates.
*
* @public
* @returns {Array} available playback rates
*/
self.getPlaybackRates = function () {
if (!player || !player.getAvailablePlaybackRates) {
return;
}
var playbackRates = player.getAvailablePlaybackRates();
if (!playbackRates.length) {
return; // No rates, but the array should contain at least 1
}
return playbackRates;
};
/**
* Get current playback rate.
*
* @public
* @returns {Number} such as 0.25, 0.5, 1, 1.25, 1.5 and 2
*/
self.getPlaybackRate = function () {
if (!player || !player.getPlaybackRate) {
return;
}
return player.getPlaybackRate();
};
/**
* Set current playback rate.
* Listen to event "playbackRateChange" to check if successful.
*
* @public
* @params {Number} suggested rate that may be rounded to supported values
*/
self.setPlaybackRate = function (newPlaybackRate) {
if (!player || !player.setPlaybackRate) {
return;
}
playbackRate = Number(newPlaybackRate);
player.setPlaybackRate(playbackRate);
};
/**
* Set current captions track.
*
* @param {H5P.Video.LabelValue} Captions track to show during playback
*/
self.setCaptionsTrack = function (track) {
player.setOption('captions', 'track', track ? {languageCode: track.value} : {});
};
/**
* Figure out which captions track is currently used.
*
* @return {H5P.Video.LabelValue} Captions track
*/
self.getCaptionsTrack = function () {
var track = player.getOption('captions', 'track');
return (track.languageCode ? new H5P.Video.LabelValue(track.displayName, track.languageCode) : null);
};
// Respond to resize events by setting the YT player size.
self.on('resize', function () {
if (!$wrapper.is(':visible')) {
return;
}
if (!player) {
// Player isn't created yet. Try again.
create();
return;
}
// Use as much space as possible
$wrapper.css({
width: '100%',
height: '100%'
});
var width = $wrapper[0].clientWidth;
var height = options.fit ? $wrapper[0].clientHeight : (width * (9/16));
// Validate height before setting
if (height > 0) {
// Set size
$wrapper.css({
width: width + 'px',
height: height + 'px'
});
player.setSize(width, height);
}
});
}
/**
* Check to see if we can play any of the given sources.
*
* @public
* @static
* @param {Array} sources
* @returns {Boolean}
*/
YouTube.canPlay = function (sources) {
return getId(sources[0].path);
};
/**
* Find id of YouTube video from given URL.
*
* @private
* @param {String} url
* @returns {String} YouTube video identifier
*/
var getId = function (url) {
// Has some false positives, but should cover all regular URLs that people can find
var matches = url.match(/(?:(?:youtube.com\/(?:attribution_link\?(?:\S+))?(?:v\/|embed\/|watch\/|(?:user\/(?:\S+)\/)?watch(?:\S+)v\=))|(?:youtu.be\/|y2u.be\/))([A-Za-z0-9_-]{11})/i);
if (matches && matches[1]) {
return matches[1];
}
};
/**
* Load the IFrame Player API asynchronously.
*/
var loadAPI = function (loaded) {
if (window.onYouTubeIframeAPIReady !== undefined) {
// Someone else is loading, hook in
var original = window.onYouTubeIframeAPIReady;
window.onYouTubeIframeAPIReady = function (id) {
loaded(id);
original(id);
};
}
else {
// Load the API our self
var tag = document.createElement('script');
tag.src = "https://www.youtube.com/iframe_api";
var firstScriptTag = document.getElementsByTagName('script')[0];
firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
window.onYouTubeIframeAPIReady = loaded;
}
};
/** @constant {Object} */
var LABELS = {
highres: '2160p', // Old API support
hd2160: '2160p', // (New API)
hd1440: '1440p',
hd1080: '1080p',
hd720: '720p',
large: '480p',
medium: '360p',
small: '240p',
tiny: '144p',
auto: 'Auto'
};
/** @private */
var numInstances = 0;
// Extract the current origin (used for security)
var ORIGIN = window.location.href.match(/http[s]?:\/\/[^\/]+/);
ORIGIN = !ORIGIN || ORIGIN[0] === undefined ? undefined : ORIGIN[0];
// ORIGIN = undefined is needed to support fetching file from device local storage
return YouTube;
})(H5P.jQuery);
// Register video handler
H5P.videoHandlers = H5P.videoHandlers || [];
H5P.videoHandlers.push(H5P.VideoYouTube);
;
/** @namespace H5P */
H5P.VideoPanopto = (function ($) {
/**
* Panopto video player for H5P.
*
* @class
* @param {Array} sources Video files to use
* @param {Object} options Settings for the player
* @param {Object} l10n Localization strings
*/
function Panopto(sources, options, l10n) {
var self = this;
var player;
var playbackRate = 1;
var id = 'h5p-panopto-' + numInstances;
numInstances++;
var $wrapper = $('
');
var $placeholder = $('
', {
id: id,
html: '' + l10n.loading + '
'
}).appendTo($wrapper);
/**
* Use the Panopto API to create a new player
*
* @private
*/
var create = function () {
if (!$placeholder.is(':visible') || player !== undefined) {
return;
}
if (window.EmbedApi === undefined) {
// Load API first
loadAPI(create);
return;
}
var width = $wrapper.width();
if (width < 200) {
width = 200;
}
const videoId = getId(sources[0].path);
player = new EmbedApi(id, {
width: width,
height: width * (9/16),
serverName: videoId[0],
sessionId: videoId[1],
videoParams: { // Optional
interactivity: 'none',
showtitle: false,
autohide: true,
offerviewer: false,
autoplay: !!options.autoplay,
showbrand: false,
start: 0,
hideoverlay: !options.controls,
},
events: {
onIframeReady: function () {
$placeholder.children(0).text('');
player.loadVideo();
},
onReady: function () {
self.trigger('loaded');
if (player.hasCaptions()) {
const captions = [];
const captionTracks = player.getCaptionTracks();
for (trackIndex in captionTracks) {
captions.push(new H5P.Video.LabelValue(captionTracks[trackIndex], trackIndex));
}
// Select active track
currentTrack = player.getSelectedCaptionTrack();
currentTrack = captions[currentTrack] ? captions[currentTrack] : null;
self.trigger('captions', captions);
}
self.pause();
},
onStateChange: function (state) {
// TODO: Playback rate fix for IE11?
if (state > -1 && state < 4) {
self.trigger('stateChange', state);
}
},
onPlaybackRateChange: function () {
self.trigger('playbackRateChange', self.getPlaybackRate());
},
onError: function () {
self.trigger('error', l10n.unknownError);
},
onLoginShown: function () {
$placeholder.children().first().remove(); // Remove loading message
self.trigger('loaded'); // Resize parent
}
}
});
};
/**
* Indicates if the video must be clicked for it to start playing.
* This is always true for Panopto since all videos auto play.
*
* @public
*/
self.pressToPlay = true;
/**
* Appends the video player to the DOM.
*
* @public
* @param {jQuery} $container
*/
self.appendTo = function ($container) {
$container.addClass('h5p-panopto').append($wrapper);
create();
};
/**
* Get list of available qualities. Not available until after play.
*
* @public
* @returns {Array}
*/
self.getQualities = function () {
// Not available for Panopto
};
/**
* Get current playback quality. Not available until after play.
*
* @public
* @returns {String}
*/
self.getQuality = function () {
// Not available for Panopto
};
/**
* Set current playback quality. Not available until after play.
* Listen to event "qualityChange" to check if successful.
*
* @public
* @params {String} [quality]
*/
self.setQuality = function (quality) {
// Not available for Panopto
};
/**
* Start the video.
*
* @public
*/
self.play = function () {
if (!player || !player.playVideo) {
return;
}
player.playVideo();
};
/**
* Pause the video.
*
* @public
*/
self.pause = function () {
if (!player || !player.pauseVideo) {
return;
}
player.pauseVideo();
};
/**
* Seek video to given time.
*
* @public
* @param {Number} time
*/
self.seek = function (time) {
if (!player || !player.seekTo) {
return;
}
player.seekTo(time);
};
/**
* Get elapsed time since video beginning.
*
* @public
* @returns {Number}
*/
self.getCurrentTime = function () {
if (!player || !player.getCurrentTime) {
return;
}
return player.getCurrentTime();
};
/**
* Get total video duration time.
*
* @public
* @returns {Number}
*/
self.getDuration = function () {
if (!player || !player.getDuration) {
return;
}
return player.getDuration();
};
/**
* Get percentage of video that is buffered.
*
* @public
* @returns {Number} Between 0 and 100
*/
self.getBuffered = function () {
// Not available for Panopto
};
/**
* Turn off video sound.
*
* @public
*/
self.mute = function () {
if (!player || !player.muteVideo) {
return;
}
player.muteVideo();
};
/**
* Turn on video sound.
*
* @public
*/
self.unMute = function () {
if (!player || !player.unmuteVideo) {
return;
}
player.unmuteVideo();
};
/**
* Check if video sound is turned on or off.
*
* @public
* @returns {Boolean}
*/
self.isMuted = function () {
if (!player || !player.isMuted) {
return;
}
return player.isMuted();
};
/**
* Return the video sound level.
*
* @public
* @returns {Number} Between 0 and 100.
*/
self.getVolume = function () {
if (!player || !player.getVolume) {
return;
}
return player.getVolume() * 100;
};
/**
* Set video sound level.
*
* @public
* @param {Number} level Between 0 and 100.
*/
self.setVolume = function (level) {
if (!player || !player.setVolume) {
return;
}
player.setVolume(level/100);
};
/**
* Get list of available playback rates.
*
* @public
* @returns {Array} available playback rates
*/
self.getPlaybackRates = function () {
return [0.25, 0.5, 1, 1.25, 1.5, 2];
};
/**
* Get current playback rate.
*
* @public
* @returns {Number} such as 0.25, 0.5, 1, 1.25, 1.5 and 2
*/
self.getPlaybackRate = function () {
if (!player || !player.getPlaybackRate) {
return;
}
return player.getPlaybackRate();
};
/**
* Set current playback rate.
* Listen to event "playbackRateChange" to check if successful.
*
* @public
* @params {Number} suggested rate that may be rounded to supported values
*/
self.setPlaybackRate = function (newPlaybackRate) {
if (!player || !player.setPlaybackRate) {
return;
}
player.setPlaybackRate(newPlaybackRate);
};
/**
* Set current captions track.
*
* @param {H5P.Video.LabelValue} Captions track to show during playback
*/
self.setCaptionsTrack = function (track) {
if (!track) {
console.log('Disable captions');
player.disableCaptions();
currentTrack = null;
}
else {
console.log('Set captions', track.value);
player.enableCaptions(track.value + '');
currentTrack = track;
}
};
/**
* Figure out which captions track is currently used.
*
* @return {H5P.Video.LabelValue} Captions track
*/
self.getCaptionsTrack = function () {
return currentTrack; // No function for getting active caption track?
};
// Respond to resize events by setting the player size.
self.on('resize', function () {
if (!$wrapper.is(':visible')) {
return;
}
if (!player) {
// Player isn't created yet. Try again.
create();
return;
}
// Use as much space as possible
$wrapper.css({
width: '100%',
height: '100%'
});
var width = $wrapper[0].clientWidth;
var height = options.fit ? $wrapper[0].clientHeight : (width * (9/16));
// Set size
$wrapper.css({
width: width + 'px',
height: height + 'px'
});
const $iframe = $placeholder.children('iframe');
if ($iframe.length) {
$iframe.attr('width', width);
$iframe.attr('height', height);
}
});
let currentTrack;
}
/**
* Check to see if we can play any of the given sources.
*
* @public
* @static
* @param {Array} sources
* @returns {Boolean}
*/
Panopto.canPlay = function (sources) {
return getId(sources[0].path);
};
/**
* Find id of YouTube video from given URL.
*
* @private
* @param {String} url
* @returns {String} Panopto video identifier
*/
var getId = function (url) {
const matches = url.match(/^[^\/]+:\/\/([^\/]*panopto\.[^\/]+)\/Panopto\/.+\?id=(.+)$/);
if (matches && matches.length === 3) {
return [matches[1], matches[2]];
}
};
/**
* Load the IFrame Player API asynchronously.
*/
var loadAPI = function (loaded) {
if (window.onPanoptoEmbedApiReady !== undefined) {
// Someone else is loading, hook in
var original = window.onPanoptoEmbedApiReady;
window.onPanoptoEmbedApiReady = function (id) {
loaded(id);
original(id);
};
}
else {
// Load the API our self
var tag = document.createElement('script');
tag.src = 'https://developers.panopto.com/scripts/embedapi.min.js';
var firstScriptTag = document.getElementsByTagName('script')[0];
firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
window.onPanoptoEmbedApiReady = loaded;
}
};
/** @private */
var numInstances = 0;
return Panopto;
})(H5P.jQuery);
// Register video handler
H5P.videoHandlers = H5P.videoHandlers || [];
H5P.videoHandlers.push(H5P.VideoPanopto);
;
/** @namespace H5P */
H5P.VideoHtml5 = (function ($) {
/**
* HTML5 video player for H5P.
*
* @class
* @param {Array} sources Video files to use
* @param {Object} options Settings for the player
* @param {Object} l10n Localization strings
*/
function Html5(sources, options, l10n) {
var self = this;
/**
* Small helper to ensure all video sources get the same cache buster.
*
* @private
* @param {Object} source
* @return {string}
*/
const getCrossOriginPath = function (source) {
let path = H5P.getPath(source.path, self.contentId);
if (video.crossOrigin !== null && H5P.addQueryParameter && H5PIntegration.crossoriginCacheBuster) {
path = H5P.addQueryParameter(path, H5PIntegration.crossoriginCacheBuster);
}
return path
};
/**
* Register track to video
*
* @param {Object} trackData Track object
* @param {string} trackData.kind Kind of track
* @param {Object} trackData.track Source path
* @param {string} [trackData.label] Label of track
* @param {string} [trackData.srcLang] Language code
*/
const addTrack = function (trackData) {
// Skip invalid tracks
if (!trackData.kind || !trackData.track.path) {
return;
}
var track = document.createElement('track');
track.kind = trackData.kind;
track.src = getCrossOriginPath(trackData.track); // Uses same crossOrigin as parent. You cannot mix.
if (trackData.label) {
track.label = trackData.label;
}
if (trackData.srcLang) {
track.srcLang = trackData.srcLang;
}
return track;
};
/**
* Small helper to set the inital video source.
* Useful if some of the loading happens asynchronously.
* NOTE: Setting the crossOrigin must happen before any of the
* sources(poster, tracks etc.) are loaded
*
* @private
*/
const setInitialSource = function () {
if (qualities[currentQuality] === undefined) {
return;
}
if (H5P.setSource !== undefined) {
H5P.setSource(video, qualities[currentQuality].source, self.contentId)
}
else {
// Backwards compatibility (H5P < v1.22)
const srcPath = H5P.getPath(qualities[currentQuality].source.path, self.contentId);
if (H5P.getCrossOrigin !== undefined) {
var crossOrigin = H5P.getCrossOrigin(srcPath);
video.setAttribute('crossorigin', crossOrigin !== null ? crossOrigin : 'anonymous');
}
video.src = srcPath;
}
// Add poster if provided
if (options.poster) {
video.poster = getCrossOriginPath(options.poster); // Uses same crossOrigin as parent. You cannot mix.
}
// Register tracks
options.tracks.forEach(function (track, i) {
var trackElement = addTrack(track);
if (i === 0) {
trackElement.default = true;
}
if (trackElement) {
video.appendChild(trackElement);
}
});
};
/**
* Displayed when the video is buffering
* @private
*/
var $throbber = $('
', {
'class': 'h5p-video-loading'
});
/**
* Used to display error messages
* @private
*/
var $error = $('
', {
'class': 'h5p-video-error'
});
/**
* Keep track of current state when changing quality.
* @private
*/
var stateBeforeChangingQuality;
var currentTimeBeforeChangingQuality;
/**
* Avoids firing the same event twice.
* @private
*/
var lastState;
/**
* Keeps track whether or not the video has been loaded.
* @private
*/
var isLoaded = false;
/**
*
* @private
*/
var playbackRate = 1;
var skipRateChange = false;
// Create player
var video = document.createElement('video');
// Sort sources into qualities
var qualities = getQualities(sources, video);
var currentQuality;
numQualities = 0;
for (let quality in qualities) {
numQualities++;
}
if (numQualities > 1 && H5P.VideoHtml5.getExternalQuality !== undefined) {
H5P.VideoHtml5.getExternalQuality(sources, function (chosenQuality) {
if (qualities[chosenQuality] !== undefined) {
currentQuality = chosenQuality;
}
setInitialSource();
});
}
else {
// Select quality and source
currentQuality = getPreferredQuality();
if (currentQuality === undefined || qualities[currentQuality] === undefined) {
// No preferred quality, pick the first.
for (currentQuality in qualities) {
if (qualities.hasOwnProperty(currentQuality)) {
break;
}
}
}
setInitialSource();
}
// Setting webkit-playsinline, which makes iOS 10 beeing able to play video
// inside browser.
video.setAttribute('webkit-playsinline', '');
video.setAttribute('playsinline', '');
video.setAttribute('preload', 'metadata');
// Remove buttons in Chrome's video player:
let controlsList = 'nodownload';
if (options.disableFullscreen) {
controlsList += ' nofullscreen';
}
if (options.disableRemotePlayback) {
controlsList += ' noremoteplayback';
}
video.setAttribute('controlsList', controlsList);
// Remove picture in picture as it interfers with other video players
video.disablePictureInPicture = true;
// Set options
video.disableRemotePlayback = (options.disableRemotePlayback ? true : false);
video.controls = (options.controls ? true : false);
video.autoplay = (options.autoplay ? true : false);
video.loop = (options.loop ? true : false);
video.className = 'h5p-video';
video.style.display = 'block';
if (options.fit) {
// Style is used since attributes with relative sizes aren't supported by IE9.
video.style.width = '100%';
video.style.height = '100%';
}
/**
* Helps registering events.
*
* @private
* @param {String} native Event name
* @param {String} h5p Event name
* @param {String} [arg] Optional argument
*/
var mapEvent = function (native, h5p, arg) {
video.addEventListener(native, function () {
switch (h5p) {
case 'stateChange':
if (lastState === arg) {
return; // Avoid firing event twice.
}
var validStartTime = options.startAt && options.startAt > 0;
if (arg === H5P.Video.PLAYING && validStartTime) {
video.currentTime = options.startAt;
delete options.startAt;
}
break;
case 'loaded':
isLoaded = true;
if (stateBeforeChangingQuality !== undefined) {
return; // Avoid loaded event when changing quality.
}
// Remove any errors
if ($error.is(':visible')) {
$error.remove();
}
if (OLD_ANDROID_FIX) {
var andLoaded = function () {
video.removeEventListener('durationchange', andLoaded, false);
// On Android seeking isn't ready until after play.
self.trigger(h5p);
};
video.addEventListener('durationchange', andLoaded, false);
return;
}
break;
case 'error':
// Handle error and get message.
arg = error(arguments[0], arguments[1]);
break;
case 'playbackRateChange':
// Fix for keeping playback rate in IE11
if (skipRateChange) {
skipRateChange = false;
return; // Avoid firing event when changing back
}
if (H5P.Video.IE11_PLAYBACK_RATE_FIX && playbackRate != video.playbackRate) { // Intentional
// Prevent change in playback rate not triggered by the user
video.playbackRate = playbackRate;
skipRateChange = true;
return;
}
// End IE11 fix
arg = self.getPlaybackRate();
break;
}
self.trigger(h5p, arg);
}, false);
};
/**
* Handle errors from the video player.
*
* @private
* @param {Object} code Error
* @param {String} [message]
* @returns {String} Human readable error message.
*/
var error = function (code, message) {
if (code instanceof Event) {
// No error code
if (!code.target.error) {
return '';
}
switch (code.target.error.code) {
case MediaError.MEDIA_ERR_ABORTED:
message = l10n.aborted;
break;
case MediaError.MEDIA_ERR_NETWORK:
message = l10n.networkFailure;
break;
case MediaError.MEDIA_ERR_DECODE:
message = l10n.cannotDecode;
break;
case MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED:
message = l10n.formatNotSupported;
break;
case MediaError.MEDIA_ERR_ENCRYPTED:
message = l10n.mediaEncrypted;
break;
}
}
if (!message) {
message = l10n.unknownError;
}
// Hide throbber
$throbber.remove();
// Display error message to user
$error.text(message).insertAfter(video);
// Pass message to our error event
return message;
};
/**
* Appends the video player to the DOM.
*
* @public
* @param {jQuery} $container
*/
self.appendTo = function ($container) {
$container.append(video);
};
/**
* Get list of available qualities. Not available until after play.
*
* @public
* @returns {Array}
*/
self.getQualities = function () {
// Create reverse list
var options = [];
for (var q in qualities) {
if (qualities.hasOwnProperty(q)) {
options.splice(0, 0, {
name: q,
label: qualities[q].label
});
}
}
if (options.length < 2) {
// Do not return if only one quality.
return;
}
return options;
};
/**
* Get current playback quality. Not available until after play.
*
* @public
* @returns {String}
*/
self.getQuality = function () {
return currentQuality;
};
/**
* Set current playback quality. Not available until after play.
* Listen to event "qualityChange" to check if successful.
*
* @public
* @params {String} [quality]
*/
self.setQuality = function (quality) {
if (qualities[quality] === undefined || quality === currentQuality) {
return; // Invalid quality
}
// Keep track of last choice
setPreferredQuality(quality);
// Avoid multiple loaded events if changing quality multiple times.
if (!stateBeforeChangingQuality) {
// Keep track of last state
stateBeforeChangingQuality = lastState;
// Keep track of current time
currentTimeBeforeChangingQuality = video.currentTime;
// Seek and start video again after loading.
var loaded = function () {
video.removeEventListener('loadedmetadata', loaded, false);
if (OLD_ANDROID_FIX) {
var andLoaded = function () {
video.removeEventListener('durationchange', andLoaded, false);
// On Android seeking isn't ready until after play.
self.seek(currentTimeBeforeChangingQuality);
};
video.addEventListener('durationchange', andLoaded, false);
}
else {
// Seek to current time.
self.seek(currentTimeBeforeChangingQuality);
}
// Always play to get image.
video.play();
if (stateBeforeChangingQuality !== H5P.Video.PLAYING) {
// Do not resume playing
video.pause();
}
// Done changing quality
stateBeforeChangingQuality = undefined;
// Remove any errors
if ($error.is(':visible')) {
$error.remove();
}
};
video.addEventListener('loadedmetadata', loaded, false);
}
// Keep track of current quality
currentQuality = quality;
self.trigger('qualityChange', currentQuality);
// Display throbber
self.trigger('stateChange', H5P.Video.BUFFERING);
// Change source
video.src = getCrossOriginPath(qualities[quality].source); // (iPad does not support #t=).
// Note: Optional tracks use same crossOrigin as the original. You cannot mix.
// Remove poster so it will not show during quality change
video.removeAttribute('poster');
};
/**
* Starts the video.
*
* @public
* @return {Promise|undefined} May return a Promise that resolves when
* play has been processed.
*/
self.play = function () {
if ($error.is(':visible')) {
return;
}
if (!isLoaded) {
// Make sure video is loaded before playing
video.load();
}
return video.play();
};
/**
* Pauses the video.
*
* @public
*/
self.pause = function () {
video.pause();
};
/**
* Seek video to given time.
*
* @public
* @param {Number} time
*/
self.seek = function (time) {
if (lastState === undefined) {
// Make sure we always play before we seek to get an image.
// If not iOS devices will reset currentTime when pressing play.
video.play();
video.pause();
}
video.currentTime = time;
};
/**
* Get elapsed time since video beginning.
*
* @public
* @returns {Number}
*/
self.getCurrentTime = function () {
return video.currentTime;
};
/**
* Get total video duration time.
*
* @public
* @returns {Number}
*/
self.getDuration = function () {
if (isNaN(video.duration)) {
return;
}
return video.duration;
};
/**
* Get percentage of video that is buffered.
*
* @public
* @returns {Number} Between 0 and 100
*/
self.getBuffered = function () {
// Find buffer currently playing from
var buffered = 0;
for (var i = 0; i < video.buffered.length; i++) {
var from = video.buffered.start(i);
var to = video.buffered.end(i);
if (video.currentTime > from && video.currentTime < to) {
buffered = to;
break;
}
}
// To percentage
return buffered ? (buffered / video.duration) * 100 : 0;
};
/**
* Turn off video sound.
*
* @public
*/
self.mute = function () {
video.muted = true;
};
/**
* Turn on video sound.
*
* @public
*/
self.unMute = function () {
video.muted = false;
};
/**
* Check if video sound is turned on or off.
*
* @public
* @returns {Boolean}
*/
self.isMuted = function () {
return video.muted;
};
/**
* Returns the video sound level.
*
* @public
* @returns {Number} Between 0 and 100.
*/
self.getVolume = function () {
return video.volume * 100;
};
/**
* Set video sound level.
*
* @public
* @param {Number} level Between 0 and 100.
*/
self.setVolume = function (level) {
video.volume = level / 100;
};
/**
* Get list of available playback rates.
*
* @public
* @returns {Array} available playback rates
*/
self.getPlaybackRates = function () {
/*
* not sure if there's a common rule about determining good speeds
* using Google's standard options via a constant for setting
*/
var playbackRates = PLAYBACK_RATES;
return playbackRates;
};
/**
* Get current playback rate.
*
* @public
* @returns {Number} such as 0.25, 0.5, 1, 1.25, 1.5 and 2
*/
self.getPlaybackRate = function () {
return video.playbackRate;
};
/**
* Set current playback rate.
* Listen to event "playbackRateChange" to check if successful.
*
* @public
* @params {Number} suggested rate that may be rounded to supported values
*/
self.setPlaybackRate = function (newPlaybackRate) {
playbackRate = newPlaybackRate;
video.playbackRate = newPlaybackRate;
};
/**
* Set current captions track.
*
* @param {H5P.Video.LabelValue} Captions track to show during playback
*/
self.setCaptionsTrack = function (track) {
for (var i = 0; i < video.textTracks.length; i++) {
video.textTracks[i].mode = (track && track.value === i ? 'showing' : 'disabled');
}
};
/**
* Figure out which captions track is currently used.
*
* @return {H5P.Video.LabelValue} Captions track
*/
self.getCaptionsTrack = function () {
for (var i = 0; i < video.textTracks.length; i++) {
if (video.textTracks[i].mode === 'showing') {
return new H5P.Video.LabelValue(video.textTracks[i].label, i);
}
}
return null;
};
// Register event listeners
mapEvent('ended', 'stateChange', H5P.Video.ENDED);
mapEvent('playing', 'stateChange', H5P.Video.PLAYING);
mapEvent('pause', 'stateChange', H5P.Video.PAUSED);
mapEvent('waiting', 'stateChange', H5P.Video.BUFFERING);
mapEvent('loadedmetadata', 'loaded');
mapEvent('canplay', 'canplay');
mapEvent('error', 'error');
mapEvent('ratechange', 'playbackRateChange');
if (!video.controls) {
// Disable context menu(right click) to prevent controls.
video.addEventListener('contextmenu', function (event) {
event.preventDefault();
}, false);
}
// Display throbber when buffering/loading video.
self.on('stateChange', function (event) {
var state = event.data;
lastState = state;
if (state === H5P.Video.BUFFERING) {
$throbber.insertAfter(video);
}
else {
$throbber.remove();
}
});
// Load captions after the video is loaded
self.on('loaded', function () {
nextTick(function () {
var textTracks = [];
for (var i = 0; i < video.textTracks.length; i++) {
textTracks.push(new H5P.Video.LabelValue(video.textTracks[i].label, i));
}
if (textTracks.length) {
self.trigger('captions', textTracks);
}
});
});
// Alternative to 'canplay' event
/*self.on('resize', function () {
if (video.offsetParent === null) {
return;
}
video.style.width = '100%';
video.style.height = '100%';
var width = video.clientWidth;
var height = options.fit ? video.clientHeight : (width * (video.videoHeight / video.videoWidth));
video.style.width = width + 'px';
video.style.height = height + 'px';
});*/
// Video controls are ready
nextTick(function () {
self.trigger('ready');
});
}
/**
* Check to see if we can play any of the given sources.
*
* @public
* @static
* @param {Array} sources
* @returns {Boolean}
*/
Html5.canPlay = function (sources) {
var video = document.createElement('video');
if (video.canPlayType === undefined) {
return false; // Not supported
}
// Cycle through sources
for (var i = 0; i < sources.length; i++) {
var type = getType(sources[i]);
if (type && video.canPlayType(type) !== '') {
// We should be able to play this
return true;
}
}
return false;
};
/**
* Find source type.
*
* @private
* @param {Object} source
* @returns {String}
*/
var getType = function (source) {
var type = source.mime;
if (!type) {
// Try to get type from URL
var matches = source.path.match(/\.(\w+)$/);
if (matches && matches[1]) {
type = 'video/' + matches[1];
}
}
if (type && source.codecs) {
// Add codecs
type += '; codecs="' + source.codecs + '"';
}
return type;
};
/**
* Sort sources into qualities.
*
* @private
* @static
* @param {Array} sources
* @param {Object} video
* @returns {Object} Quality mapping
*/
var getQualities = function (sources, video) {
var qualities = {};
var qualityIndex = 1;
var lastQuality;
// Cycle through sources
for (var i = 0; i < sources.length; i++) {
var source = sources[i];
// Find and update type.
var type = source.type = getType(source);
// Check if we support this type
var isPlayable = type && (type === 'video/unknown' || video.canPlayType(type) !== '');
if (!isPlayable) {
continue; // We cannot play this source
}
if (source.quality === undefined) {
/**
* No quality metadata. Create a quality tag to separate multiple sources of the same type,
* e.g. if two mp4 files with different quality has been uploaded
*/
if (lastQuality === undefined || qualities[lastQuality].source.type === type) {
// Create a new quality tag
source.quality = {
name: 'q' + qualityIndex,
label: (source.metadata && source.metadata.qualityName) ? source.metadata.qualityName : 'Quality ' + qualityIndex // TODO: l10n
};
qualityIndex++;
}
else {
/**
* Assumes quality already exists in a different format.
* Uses existing label for this quality.
*/
source.quality = qualities[lastQuality].source.quality;
}
}
// Log last quality
lastQuality = source.quality.name;
// Look to see if quality exists
var quality = qualities[lastQuality];
if (quality) {
// We have a source with this quality. Check if we have a better format.
if (source.mime.split('/')[1] === PREFERRED_FORMAT) {
quality.source = source;
}
}
else {
// Add new source with quality.
qualities[source.quality.name] = {
label: source.quality.label,
source: source
};
}
}
return qualities;
};
/**
* Set preferred video quality.
*
* @private
* @static
* @param {String} quality Index of preferred quality
*/
var setPreferredQuality = function (quality) {
try {
localStorage.setItem('h5pVideoQuality', quality);
}
catch (err) {
console.warn('Unable to set preferred video quality, localStorage is not available.');
}
};
/**
* Get preferred video quality.
*
* @private
* @static
* @returns {String} Index of preferred quality
*/
var getPreferredQuality = function () {
// First check localStorage
let quality;
try {
quality = localStorage.getItem('h5pVideoQuality');
}
catch (err) {
console.warn('Unable to retrieve preferred video quality from localStorage.');
}
if (!quality) {
try {
// The fallback to old cookie solution
var settings = document.cookie.split(';');
for (var i = 0; i < settings.length; i++) {
var setting = settings[i].split('=');
if (setting[0] === 'H5PVideoQuality') {
quality = setting[1];
break;
}
}
}
catch (err) {
console.warn('Unable to retrieve preferred video quality from cookie.');
}
}
return quality;
};
/**
* Helps schedule a task for the next tick.
* @param {function} task
*/
var nextTick = function (task) {
setTimeout(task, 0);
};
/** @constant {Boolean} */
var OLD_ANDROID_FIX = false;
/** @constant {Boolean} */
var PREFERRED_FORMAT = 'mp4';
/** @constant {Object} */
var PLAYBACK_RATES = [0.25, 0.5, 1, 1.25, 1.5, 2];
if (navigator.userAgent.indexOf('Android') !== -1) {
// We have Android, check version.
var version = navigator.userAgent.match(/AppleWebKit\/(\d+\.?\d*)/);
if (version && version[1] && Number(version[1]) <= 534.30) {
// Include fix for devices running the native Android browser.
// (We don't know when video was fixed, so the number is just the lastest
// native android browser we found.)
OLD_ANDROID_FIX = true;
}
}
else {
if (navigator.userAgent.indexOf('Chrome') !== -1) {
// If we're using chrome on a device that isn't Android, prefer the webm
// format. This is because Chrome has trouble with some mp4 codecs.
PREFERRED_FORMAT = 'webm';
}
}
return Html5;
})(H5P.jQuery);
// Register video handler
H5P.videoHandlers = H5P.videoHandlers || [];
H5P.videoHandlers.push(H5P.VideoHtml5);
;
/** @namespace H5P */
H5P.VideoFlash = (function ($) {
/**
* Flash video player for H5P.
*
* @class
* @param {Array} sources Video files to use
* @param {Object} options Settings for the player
*/
function Flash(sources, options) {
var self = this;
// Player wrapper
var $wrapper = $('
', {
'class': 'h5p-video-flash',
css: {
width: '100%',
height: '100%'
}
});
/**
* Used to display error messages
* @private
*/
var $error = $('
', {
'class': 'h5p-video-error'
});
/**
* Keep track of current state when changing quality.
* @private
*/
var stateBeforeChangingQuality;
var currentTimeBeforeChangingQuality;
// Sort sources into qualities
//var qualities = getQualities(sources);
var currentQuality;
// Create player options
var playerOptions = {
buffering: true,
clip: {
url: sources[0].path, // getPreferredQuality(),
autoPlay: options.autoplay,
autoBuffering: true,
scaling: 'fit',
onSeek: function () {
if (stateBeforeChangingQuality) {
// ????
}
},
onMetaData: function () {
setTimeout(function () {
if (stateBeforeChangingQuality !== undefined) {
fp.seek(currentTimeBeforeChangingQuality);
if (stateBeforeChangingQuality === H5P.Video.PLAYING) {
// Resume play
fp.play();
}
// Done changing quality
stateBeforeChangingQuality = undefined;
// Remove any errors
if ($error.is(':visible')) {
$error.remove();
}
}
else {
self.trigger('ready');
self.trigger('loaded');
}
}, 0); // Run on next tick
},
onBegin: function () {
self.trigger('stateChange', H5P.Video.PLAYING);
},
onResume: function () {
self.trigger('stateChange', H5P.Video.PLAYING);
},
onPause: function () {
self.trigger('stateChange', H5P.Video.PAUSED);
},
onFinish: function () {
self.trigger('stateChange', H5P.Video.ENDED);
},
onError: function (code, message) {
console.log('ERROR', code, message); // TODO
self.trigger('error', message);
}
},
plugins: {
controls: null
},
play: null, // Disable overlay controls
onPlaylistReplace: function () {
that.playlistReplaced();
}
};
if (options.controls) {
playerOptions.plugins.controls = {};
delete playerOptions.play;
}
var fp = flowplayer($wrapper[0], {
src: "http://releases.flowplayer.org/swf/flowplayer-3.2.16.swf",
wmode: "opaque"
}, playerOptions);
/**
* Appends the video player to the DOM.
*
* @public
* @param {jQuery} $container
*/
self.appendTo = function ($container) {
$wrapper.appendTo($container);
};
/**
* Get list of available qualities. Not available until after play.
*
* @public
* @returns {Array}
*/
self.getQualities = function () {
return;
};
/**
* Get current playback quality. Not available until after play.
*
* @public
* @returns {String}
*/
self.getQuality = function () {
return currentQuality;
};
/**
* Set current playback quality. Not available until after play.
* Listen to event "qualityChange" to check if successful.
*
* @public
* @params {String} [quality]
*/
self.setQuality = function (quality) {
if (qualities[quality] === undefined || quality === currentQuality) {
return; // Invalid quality
}
// Keep track of last choice
setPreferredQuality(quality);
// Avoid multiple loaded events if changing quality multiple times.
if (!stateBeforeChangingQuality) {
// Keep track of last state
stateBeforeChangingQuality = lastState;
// Keep track of current time
currentTimeBeforeChangingQuality = video.currentTime;
}
// Keep track of current quality
currentQuality = quality;
self.trigger('qualityChange', currentQuality);
// Display throbber
self.trigger('stateChange', H5P.Video.BUFFERING);
// Change source
fp.setClip(qualities[quality].source.path);
fp.startBuffering();
};
/**
* Starts the video.
*
* @public
*/
self.play = function () {
if ($error.is(':visible')) {
return;
}
fp.play();
};
/**
* Pauses the video.
*
* @public
*/
self.pause = function () {
fp.pause();
};
/**
* Seek video to given time.
*
* @public
* @param {Number} time
*/
self.seek = function (time) {
fp.seek(time);
};
/**
* Get elapsed time since video beginning.
*
* @public
* @returns {Number}
*/
self.getCurrentTime = function () {
return fp.getTime();
};
/**
* Get total video duration time.
*
* @public
* @returns {Number}
*/
self.getDuration = function () {
return fp.getClip().metaData.duration;
};
/**
* Get percentage of video that is buffered.
*
* @public
* @returns {Number} Between 0 and 100
*/
self.getBuffered = function () {
return fp.getClip().buffer;
};
/**
* Turn off video sound.
*
* @public
*/
self.mute = function () {
fp.mute();
};
/**
* Turn on video sound.
*
* @public
*/
self.unMute = function () {
fp.unmute();
};
/**
* Check if video sound is turned on or off.
*
* @public
* @returns {Boolean}
*/
self.isMuted = function () {
return fp.muted;
};
/**
* Returns the video sound level.
*
* @public
* @returns {Number} Between 0 and 100.
*/
self.getVolume = function () {
return fp.volumeLevel * 100;
};
/**
* Set video sound level.
*
* @public
* @param {Number} volume Between 0 and 100.
*/
self.setVolume = function (level) {
fp.volume(level / 100);
};
// Handle resize events
self.on('resize', function () {
var $object = H5P.jQuery(fp.getParent()).children('object');
var clip = fp.getClip();
if (clip !== undefined) {
$object.css('height', $object.width() * (clip.metaData.height / clip.metaData.width));
}
});
}
/**
* Check to see if we can play any of the given sources.
*
* @public
* @static
* @param {Array} sources
* @returns {Boolean}
*/
Flash.canPlay = function (sources) {
// Cycle through sources
for (var i = 0; i < sources.length; i++) {
if (sources[i].mime === 'video/mp4' || /\.mp4$/.test(sources[i].mime)) {
return true; // We only play mp4
}
}
};
return Flash;
})(H5P.jQuery);
// Register video handler
H5P.videoHandlers = H5P.videoHandlers || [];
H5P.videoHandlers.push(H5P.VideoFlash);
;
/** @namespace H5P */
H5P.Video = (function ($, ContentCopyrights, MediaCopyright, handlers) {
/**
* The ultimate H5P video player!
*
* @class
* @param {Object} parameters Options for this library.
* @param {Object} parameters.visuals Visual options
* @param {Object} parameters.playback Playback options
* @param {Object} parameters.a11y Accessibility options
* @param {Boolean} [parameters.startAt] Start time of video
* @param {Number} id Content identifier
*/
function Video(parameters, id) {
var self = this;
self.contentId = id;
// Ref youtube.js - ipad & youtube - issue
self.pressToPlay = false;
// Reference to the handler
var handlerName = '';
// Initialize event inheritance
H5P.EventDispatcher.call(self);
// Default language localization
parameters = $.extend(true, parameters, {
l10n: {
name: 'Video',
loading: 'Video player loading...',
noPlayers: 'Found no video players that supports the given video format.',
noSources: 'Video source is missing.',
aborted: 'Media playback has been aborted.',
networkFailure: 'Network failure.',
cannotDecode: 'Unable to decode media.',
formatNotSupported: 'Video format not supported.',
mediaEncrypted: 'Media encrypted.',
unknownError: 'Unknown error.',
invalidYtId: 'Invalid YouTube ID.',
unknownYtId: 'Unable to find video with the given YouTube ID.',
restrictedYt: 'The owner of this video does not allow it to be embedded.'
}
});
parameters.a11y = parameters.a11y || [];
parameters.playback = parameters.playback || {};
parameters.visuals = $.extend(true, parameters.visuals, {
disableFullscreen: false
});
/** @private */
var sources = [];
if (parameters.sources) {
for (var i = 0; i < parameters.sources.length; i++) {
// Clone to avoid changing of parameters.
var source = $.extend(true, {}, parameters.sources[i]);
// Create working URL without html entities.
source.path = $cleaner.html(source.path).text();
sources.push(source);
}
}
/** @private */
var tracks = [];
parameters.a11y.forEach(function (track) {
// Clone to avoid changing of parameters.
var clone = $.extend(true, {}, track);
// Create working URL without html entities
if (clone.track && clone.track.path) {
clone.track.path = $cleaner.html(clone.track.path).text();
tracks.push(clone);
}
});
/**
* Attaches the video handler to the given container.
* Inserts text if no handler is found.
*
* @public
* @param {jQuery} $container
*/
self.attach = function ($container) {
$container.addClass('h5p-video').html('');
if (self.appendTo !== undefined) {
self.appendTo($container);
}
else {
if (sources.length) {
$container.text(parameters.l10n.noPlayers);
}
else {
$container.text(parameters.l10n.noSources);
}
}
};
/**
* Get name of the video handler
*
* @public
* @returns {string}
*/
self.getHandlerName = function() {
return handlerName;
};
// Resize the video when we know its aspect ratio
self.on('loaded', function () {
self.trigger('resize');
});
// Find player for video sources
if (sources.length) {
const options = {
controls: parameters.visuals.controls,
autoplay: parameters.playback.autoplay,
loop: parameters.playback.loop,
fit: parameters.visuals.fit,
poster: parameters.visuals.poster === undefined ? undefined : parameters.visuals.poster,
startAt: parameters.startAt || 0,
tracks: tracks,
disableRemotePlayback: parameters.visuals.disableRemotePlayback === true,
disableFullscreen: parameters.visuals.disableFullscreen === true
}
var html5Handler;
for (var i = 0; i < handlers.length; i++) {
var handler = handlers[i];
if (handler.canPlay !== undefined && handler.canPlay(sources)) {
handler.call(self, sources, options, parameters.l10n);
handlerName = handler.name;
return;
}
if (handler === H5P.VideoHtml5) {
html5Handler = handler;
handlerName = handler.name;
}
}
// Fallback to trying HTML5 player
if (html5Handler) {
html5Handler.call(self, sources, options, parameters.l10n);
}
}
}
// Extends the event dispatcher
Video.prototype = Object.create(H5P.EventDispatcher.prototype);
Video.prototype.constructor = Video;
// Player states
/** @constant {Number} */
Video.ENDED = 0;
/** @constant {Number} */
Video.PLAYING = 1;
/** @constant {Number} */
Video.PAUSED = 2;
/** @constant {Number} */
Video.BUFFERING = 3;
/**
* When video is queued to start
* @constant {Number}
*/
Video.VIDEO_CUED = 5;
// Used to convert between html and text, since URLs have html entities.
var $cleaner = H5P.jQuery('
');
/**
* Help keep track of key value pairs used by the UI.
*
* @class
* @param {string} label
* @param {string} value
*/
Video.LabelValue = function (label, value) {
this.label = label;
this.value = value;
};
/** @constant {Boolean} */
Video.IE11_PLAYBACK_RATE_FIX = (navigator.userAgent.match(/Trident.*rv[ :]*11\./) ? true : false);
return Video;
})(H5P.jQuery, H5P.ContentCopyrights, H5P.MediaCopyright, H5P.videoHandlers || []);
;
H5P = H5P || {};
/**
* Will render a Question with multiple choices for answers.
*
* Events provided:
* - h5pQuestionSetFinished: Triggered when a question is finished. (User presses Finish-button)
*
* @param {Array} options
* @param {int} contentId
* @param {Object} contentData
* @returns {H5P.QuestionSet} Instance
*/
H5P.QuestionSet = function (options, contentId, contentData) {
if (!(this instanceof H5P.QuestionSet)) {
return new H5P.QuestionSet(options, contentId, contentData);
}
H5P.EventDispatcher.call(this);
var $ = H5P.jQuery;
var self = this;
this.contentId = contentId;
var defaults = {
initialQuestion: 0,
progressType: 'dots',
passPercentage: 50,
questions: [],
introPage: {
showIntroPage: false,
title: '',
introduction: '',
startButtonText: 'Start'
},
texts: {
prevButton: 'Previous question',
nextButton: 'Next question',
finishButton: 'Finish',
textualProgress: 'Question: @current of @total questions',
jumpToQuestion: 'Question %d of %total',
questionLabel: 'Question',
readSpeakerProgress: 'Question @current of @total',
unansweredText: 'Unanswered',
answeredText: 'Answered',
currentQuestionText: 'Current question'
},
endGame: {
showResultPage: true,
noResultMessage: 'Finished',
message: 'Your result:',
oldFeedback: {
successGreeting: '',
successComment: '',
failGreeting: '',
failComment: ''
},
overallFeedback: [],
finishButtonText: 'Finish',
solutionButtonText: 'Show solution',
retryButtonText: 'Retry',
showAnimations: false,
skipButtonText: 'Skip video',
showSolutionButton: true,
showRetryButton: true
},
override: {},
disableBackwardsNavigation: false
};
var params = $.extend(true, {}, defaults, options);
var texttemplate =
'<% if (introPage.showIntroPage) { %>' +
'' +
' <% if (introPage.title) { %>' +
'
<%= introPage.title %>
' +
' <% } %>' +
' <% if (introPage.introduction) { %>' +
'
<%= introPage.introduction %>
' +
' <% } %>' +
'
' +
'
' +
'<% } %>' +
'
' +
'' +
' <% for (var i=0; i
' +
'
' +
' <% } %>' +
' ' +
' ' +
' ';
var solutionButtonTemplate = params.endGame.showSolutionButton ?
'
<%= solutionButtonText %> ':
'';
const retryButtonTemplate = params.endGame.showRetryButton ?
'
<%= retryButtonText %> ':
'';
var resulttemplate =
'
' +
'
<%= message %>
' +
'
' +
' <% if (comment) { %>' +
' ' +
' <% } %>' +
' <% if (resulttext) { %>' +
'
<%= resulttext %>
' +
' <% } %>' +
'
' +
solutionButtonTemplate +
retryButtonTemplate +
'
' +
'
';
var template = new EJS({text: texttemplate});
var endTemplate = new EJS({text: resulttemplate});
var initialParams = $.extend(true, {}, defaults, options);
var poolOrder; // Order of questions in a pool
var currentQuestion = 0;
var questionInstances = [];
var questionOrder; //Stores order of questions to allow resuming of question set
var $myDom;
var scoreBar;
var up;
var renderSolutions = false;
var showingSolutions = false;
contentData = contentData || {};
// Bring question set up to date when resuming
if (contentData.previousState) {
if (contentData.previousState.progress) {
currentQuestion = contentData.previousState.progress;
}
questionOrder = contentData.previousState.order;
}
/**
* Randomizes questions in an array and updates an array containing their order
* @param {array} questions
* @return {Object.
} questionOrdering
*/
var randomizeQuestionOrdering = function (questions) {
// Save the original order of the questions in a multidimensional array [[question0,0],[question1,1]...
var questionOrdering = questions.map(function (questionInstance, index) {
return [questionInstance, index];
});
// Shuffle the multidimensional array
questionOrdering = H5P.shuffleArray(questionOrdering);
// Retrieve question objects from the first index
questions = [];
for (var i = 0; i < questionOrdering.length; i++) {
questions[i] = questionOrdering[i][0];
}
// Retrieve the new shuffled order from the second index
var newOrder = [];
for (var j = 0; j < questionOrdering.length; j++) {
// Use a previous order if it exists
if (contentData.previousState && contentData.previousState.questionOrder) {
newOrder[j] = questionOrder[questionOrdering[j][1]];
}
else {
newOrder[j] = questionOrdering[j][1];
}
}
// Return the questions in their new order *with* their new indexes
return {
questions: questions,
questionOrder: newOrder
};
};
// Create a pool (a subset) of questions if necessary
if (params.poolSize > 0) {
// If a previous pool exists, recreate it
if (contentData.previousState && contentData.previousState.poolOrder) {
poolOrder = contentData.previousState.poolOrder;
// Recreate the pool from the saved data
var pool = [];
for (var i = 0; i < poolOrder.length; i++) {
pool[i] = params.questions[poolOrder[i]];
}
// Replace original questions with just the ones in the pool
params.questions = pool;
}
else { // Otherwise create a new pool
// Randomize and get the results
var poolResult = randomizeQuestionOrdering(params.questions);
var poolQuestions = poolResult.questions;
poolOrder = poolResult.questionOrder;
// Discard extra questions
poolQuestions = poolQuestions.slice(0, params.poolSize);
poolOrder = poolOrder.slice(0, params.poolSize);
// Replace original questions with just the ones in the pool
params.questions = poolQuestions;
}
}
// Create the html template for the question container
var $template = $(template.render(params));
// Set overrides for questions
var override;
if (params.override.showSolutionButton || params.override.retryButton || params.override.checkButton === false) {
override = {};
if (params.override.showSolutionButton) {
// Force "Show solution" button to be on or off for all interactions
override.enableSolutionsButton =
(params.override.showSolutionButton === 'on' ? true : false);
}
if (params.override.retryButton) {
// Force "Retry" button to be on or off for all interactions
override.enableRetry =
(params.override.retryButton === 'on' ? true : false);
}
if (params.override.checkButton === false) {
// Force "Check" button to be on or off for all interactions
override.enableCheckButton = params.override.checkButton;
}
}
/**
* Generates question instances from H5P objects
*
* @param {object} questions H5P content types to be created as instances
* @return {array} Array of questions instances
*/
var createQuestionInstancesFromQuestions = function (questions) {
var result = [];
// Create question instances from questions
// Instantiate question instances
for (var i = 0; i < questions.length; i++) {
var question;
// If a previous order exists, use it
if (questionOrder !== undefined) {
question = questions[questionOrder[i]];
}
else {
// Use a generic order when initialzing for the first time
question = questions[i];
}
if (override) {
// Extend subcontent with the overrided settings.
$.extend(question.params.behaviour, override);
}
question.params = question.params || {};
var hasAnswers = contentData.previousState && contentData.previousState.answers;
var questionInstance = H5P.newRunnable(question, contentId, undefined, undefined,
{
previousState: hasAnswers ? contentData.previousState.answers[i] : undefined,
parent: self
});
questionInstance.on('resize', function () {
up = true;
self.trigger('resize');
});
result.push(questionInstance);
}
return result;
};
// Create question instances from questions given by params
questionInstances = createQuestionInstancesFromQuestions(params.questions);
// Randomize questions only on instantiation
if (params.randomQuestions && contentData.previousState === undefined) {
var result = randomizeQuestionOrdering(questionInstances);
questionInstances = result.questions;
questionOrder = result.questionOrder;
}
// Resize all interactions on resize
self.on('resize', function () {
if (up) {
// Prevent resizing the question again.
up = false;
return;
}
for (var i = 0; i < questionInstances.length; i++) {
questionInstances[i].trigger('resize');
}
});
// Update button state.
var _updateButtons = function () {
// Verify that current question is answered when backward nav is disabled
if (params.disableBackwardsNavigation) {
if (questionInstances[currentQuestion].getAnswerGiven() &&
questionInstances.length-1 !== currentQuestion) {
questionInstances[currentQuestion].showButton('next');
}
else {
questionInstances[currentQuestion].hideButton('next');
}
}
var answered = true;
for (var i = questionInstances.length - 1; i >= 0; i--) {
answered = answered && (questionInstances[i]).getAnswerGiven();
}
if (currentQuestion === (params.questions.length - 1) &&
questionInstances[currentQuestion]) {
if (answered) {
questionInstances[currentQuestion].showButton('finish');
}
else {
questionInstances[currentQuestion].hideButton('finish');
}
}
};
var _stopQuestion = function (questionNumber) {
if (questionInstances[questionNumber]) {
pauseMedia(questionInstances[questionNumber]);
}
};
var _showQuestion = function (questionNumber, preventAnnouncement) {
// Sanitize input.
if (questionNumber < 0) {
questionNumber = 0;
}
if (questionNumber >= params.questions.length) {
questionNumber = params.questions.length - 1;
}
currentQuestion = questionNumber;
handleAutoPlay(currentQuestion);
// Hide all questions
$('.question-container', $myDom).hide().eq(questionNumber).show();
if (questionInstances[questionNumber]) {
// Trigger resize on question in case the size of the QS has changed.
var instance = questionInstances[questionNumber];
instance.setActivityStarted();
if (instance.$ !== undefined) {
instance.trigger('resize');
}
}
// Update progress indicator
// Test if current has been answered.
if (params.progressType === 'textual') {
$('.progress-text', $myDom).text(params.texts.textualProgress.replace("@current", questionNumber+1).replace("@total", params.questions.length));
}
else {
// Set currentNess
var previousQuestion = $('.progress-dot.current', $myDom).parent().index();
if (previousQuestion >= 0) {
toggleCurrentDot(previousQuestion, false);
toggleAnsweredDot(previousQuestion, questionInstances[previousQuestion].getAnswerGiven());
}
toggleCurrentDot(questionNumber, true);
}
if (!preventAnnouncement) {
// Announce question number of total, must use timeout because of buttons logic
setTimeout(function () {
var humanizedProgress = params.texts.readSpeakerProgress
.replace('@current', (currentQuestion + 1).toString())
.replace('@total', questionInstances.length.toString());
$('.qs-progress-announcer', $myDom)
.html(humanizedProgress)
.show().focus();
if (instance && instance.readFeedback) {
instance.readFeedback();
}
}, 0);
}
// Remember where we are
_updateButtons();
self.trigger('resize');
return currentQuestion;
};
/**
* Handle autoplays, limit to one at a time
*
* @param {number} currentQuestionIndex
*/
var handleAutoPlay = function (currentQuestionIndex) {
for (var i = 0; i < questionInstances.length; i++) {
questionInstances[i].pause();
}
var currentQuestion = params.questions[currentQuestionIndex];
var hasAutoPlay = currentQuestion &&
currentQuestion.params.media &&
currentQuestion.params.media.params &&
currentQuestion.params.media.params.playback &&
currentQuestion.params.media.params.playback.autoplay;
if (hasAutoPlay && typeof questionInstances[currentQuestionIndex].play === 'function') {
questionInstances[currentQuestionIndex].play();
}
};
/**
* Show solutions for subcontent, and hide subcontent buttons.
* Used for contracts with integrated content.
* @public
*/
var showSolutions = function () {
showingSolutions = true;
for (var i = 0; i < questionInstances.length; i++) {
// Enable back and forth navigation in solution mode
toggleDotsNavigation(true);
if (i < questionInstances.length - 1) {
questionInstances[i].showButton('next');
}
if (i > 0) {
questionInstances[i].showButton('prev');
}
try {
// Do not read answers
questionInstances[i].toggleReadSpeaker(true);
questionInstances[i].showSolutions();
questionInstances[i].toggleReadSpeaker(false);
}
catch (error) {
H5P.error("subcontent does not contain a valid showSolutions function");
H5P.error(error);
}
}
};
/**
* Toggles whether dots are enabled for navigation
*/
var toggleDotsNavigation = function (enable) {
$('.progress-dot', $myDom).each(function () {
$(this).toggleClass('disabled', !enable);
$(this).attr('aria-disabled', enable ? 'false' : 'true');
// Remove tabindex
if (!enable) {
$(this).attr('tabindex', '-1');
}
});
};
/**
* Resets the task and every subcontent task.
* Used for contracts with integrated content.
* @public
*/
var resetTask = function () {
// Clear previous state to ensure questions are created cleanly
contentData.previousState = [];
showingSolutions = false;
for (var i = 0; i < questionInstances.length; i++) {
try {
questionInstances[i].resetTask();
// Hide back and forth navigation in normal mode
if (params.disableBackwardsNavigation) {
toggleDotsNavigation(false);
// Check if first question is answered by default
if (i === 0 && questionInstances[i].getAnswerGiven()) {
questionInstances[i].showButton('next');
}
else {
questionInstances[i].hideButton('next');
}
questionInstances[i].hideButton('prev');
}
}
catch (error) {
H5P.error("subcontent does not contain a valid resetTask function");
H5P.error(error);
}
}
// Hide finish button
questionInstances[questionInstances.length - 1].hideButton('finish');
// Mark all tasks as unanswered:
$('.progress-dot').each(function (idx) {
toggleAnsweredDot(idx, false);
});
//Force the last page to be reRendered
rendered = false;
if (params.poolSize > 0) {
// Make new pool from params.questions
// Randomize and get the results
var poolResult = randomizeQuestionOrdering(initialParams.questions);
var poolQuestions = poolResult.questions;
poolOrder = poolResult.questionOrder;
// Discard extra questions
poolQuestions = poolQuestions.slice(0, params.poolSize);
poolOrder = poolOrder.slice(0, params.poolSize);
// Replace original questions with just the ones in the pool
params.questions = poolQuestions;
// Recreate the question instances
questionInstances = createQuestionInstancesFromQuestions(params.questions);
// Update buttons
initializeQuestion();
}
else if (params.randomQuestions) {
randomizeQuestions();
}
};
var rendered = false;
this.reRender = function () {
rendered = false;
};
/**
* Randomizes question instances
*/
var randomizeQuestions = function () {
var result = randomizeQuestionOrdering(questionInstances);
questionInstances = result.questions;
questionOrder = result.questionOrder;
replaceQuestionsInDOM(questionInstances);
};
/**
* Empty the DOM of all questions, attach new questions and update buttons
*
* @param {type} questionInstances Array of questions to be attached to the DOM
*/
var replaceQuestionsInDOM = function (questionInstances) {
// Find all question containers and detach questions from them
$('.question-container', $myDom).each(function () {
$(this).children().detach();
});
// Reattach questions and their buttons in the new order
for (var i = 0; i < questionInstances.length; i++) {
var question = questionInstances[i];
// Make sure styles are not being added twice
$('.question-container:eq(' + i + ')', $myDom).attr('class', 'question-container');
question.attach($('.question-container:eq(' + i + ')', $myDom));
//Show buttons if necessary
if (questionInstances[questionInstances.length -1] === question &&
question.hasButton('finish')) {
question.showButton('finish');
}
if (questionInstances[questionInstances.length -1] !== question &&
question.hasButton('next')) {
question.showButton('next');
}
if (questionInstances[0] !== question &&
question.hasButton('prev') &&
!params.disableBackwardsNavigation) {
question.showButton('prev');
}
// Hide relevant buttons since the order has changed
if (questionInstances[0] === question) {
question.hideButton('prev');
}
if (questionInstances[questionInstances.length-1] === question) {
question.hideButton('next');
}
if (questionInstances[questionInstances.length-1] !== question) {
question.hideButton('finish');
}
}
};
var moveQuestion = function (direction) {
if (params.disableBackwardsNavigation && !questionInstances[currentQuestion].getAnswerGiven()) {
questionInstances[currentQuestion].hideButton('next');
questionInstances[currentQuestion].hideButton('finish');
return;
}
_stopQuestion(currentQuestion);
if (currentQuestion + direction >= questionInstances.length) {
_displayEndGame();
}
else {
// Allow movement if backward navigation enabled or answer given
_showQuestion(currentQuestion + direction);
}
};
/**
* Toggle answered state of dot at given index
* @param {number} dotIndex Index of dot
* @param {boolean} isAnswered True if is answered, False if not answered
*/
var toggleAnsweredDot = function (dotIndex, isAnswered) {
var $el = $('.progress-dot:eq(' + dotIndex +')', $myDom);
// Skip current button
if ($el.hasClass('current')) {
return;
}
// Ensure boolean
isAnswered = !!isAnswered;
var label = params.texts.jumpToQuestion
.replace('%d', (dotIndex + 1).toString())
.replace('%total', $('.progress-dot', $myDom).length) +
', ' +
(isAnswered ? params.texts.answeredText : params.texts.unansweredText);
$el.toggleClass('unanswered', !isAnswered)
.toggleClass('answered', isAnswered)
.attr('aria-label', label);
};
/**
* Toggle current state of dot at given index
* @param dotIndex
* @param isCurrent
*/
var toggleCurrentDot = function (dotIndex, isCurrent) {
var $el = $('.progress-dot:eq(' + dotIndex +')', $myDom);
var texts = params.texts;
var label = texts.jumpToQuestion
.replace('%d', (dotIndex + 1).toString())
.replace('%total', $('.progress-dot', $myDom).length);
if (!isCurrent) {
var isAnswered = $el.hasClass('answered');
label += ', ' + (isAnswered ? texts.answeredText : texts.unansweredText);
}
else {
label += ', ' + texts.currentQuestionText;
}
var disabledTabindex = params.disableBackwardsNavigation && !showingSolutions;
$el.toggleClass('current', isCurrent)
.attr('aria-label', label)
.attr('tabindex', isCurrent && !disabledTabindex ? 0 : -1);
};
var _displayEndGame = function () {
$('.progress-dot.current', $myDom).removeClass('current');
if (rendered) {
$myDom.children().hide().filter('.questionset-results').show();
self.trigger('resize');
return;
}
//Remove old score screen.
$myDom.children().hide().filter('.questionset-results').remove();
rendered = true;
// Get total score.
var finals = self.getScore();
var totals = self.getMaxScore();
var scoreString = H5P.Question.determineOverallFeedback(params.endGame.overallFeedback, finals / totals).replace('@score', finals).replace('@total', totals);
var success = ((100 * finals / totals) >= params.passPercentage);
/**
* Makes our buttons behave like other buttons.
*
* @private
* @param {string} classSelector
* @param {function} handler
*/
var hookUpButton = function (classSelector, handler) {
$(classSelector, $myDom).click(handler).keypress(function (e) {
if (e.which === 32) {
handler();
e.preventDefault();
}
});
};
var displayResults = function () {
self.triggerXAPICompleted(self.getScore(), self.getMaxScore(), success);
var eparams = {
message: params.endGame.showResultPage ? params.endGame.message : params.endGame.noResultMessage,
comment: params.endGame.showResultPage ? (success ? params.endGame.oldFeedback.successGreeting : params.endGame.oldFeedback.failGreeting) : undefined,
resulttext: params.endGame.showResultPage ? (success ? params.endGame.oldFeedback.successComment : params.endGame.oldFeedback.failComment) : undefined,
finishButtonText: params.endGame.finishButtonText,
solutionButtonText: params.endGame.solutionButtonText,
retryButtonText: params.endGame.retryButtonText
};
// Show result page.
$myDom.children().hide();
$myDom.append(endTemplate.render(eparams));
if (params.endGame.showResultPage) {
hookUpButton('.qs-solutionbutton', function () {
showSolutions();
$myDom.children().hide().filter('.questionset').show();
_showQuestion(params.initialQuestion);
});
hookUpButton('.qs-retrybutton', function () {
resetTask();
$myDom.children().hide();
var $intro = $('.intro-page', $myDom);
if ($intro.length) {
// Show intro
$('.intro-page', $myDom).show();
$('.qs-startbutton', $myDom).focus();
}
else {
// Show first question
$('.questionset', $myDom).show();
_showQuestion(params.initialQuestion);
}
});
if (scoreBar === undefined) {
scoreBar = H5P.JoubelUI.createScoreBar(totals);
}
scoreBar.appendTo($('.feedback-scorebar', $myDom));
$('.feedback-text', $myDom).html(scoreString);
// Announce that the question set is complete
setTimeout(function () {
$('.qs-progress-announcer', $myDom)
.html(eparams.message + '.' +
scoreString + '.' +
eparams.comment + '.' +
eparams.resulttext)
.show().focus();
scoreBar.setMaxScore(totals);
scoreBar.setScore(finals);
}, 0);
}
else {
// Remove buttons and feedback section
$('.qs-solutionbutton, .qs-retrybutton, .feedback-section', $myDom).remove();
}
self.trigger('resize');
};
if (params.endGame.showAnimations) {
var videoData = success ? params.endGame.successVideo : params.endGame.failVideo;
if (videoData) {
$myDom.children().hide();
var $videoContainer = $('
').appendTo($myDom);
var video = new H5P.Video({
sources: videoData,
fitToWrapper: true,
controls: false,
autoplay: false
}, contentId);
video.on('stateChange', function (event) {
if (event.data === H5P.Video.ENDED) {
displayResults();
$videoContainer.hide();
}
});
video.attach($videoContainer);
// Resize on video loaded
video.on('loaded', function () {
self.trigger('resize');
});
video.play();
if (params.endGame.skippable) {
$('' + params.endGame.skipButtonText + ' ').click(function () {
video.pause();
$videoContainer.hide();
displayResults();
}).appendTo($videoContainer);
}
return;
}
}
// Trigger finished event.
displayResults();
self.trigger('resize');
};
var registerImageLoadedListener = function (question) {
H5P.on(question, 'imageLoaded', function () {
self.trigger('resize');
});
};
/**
* Initialize a question and attach it to the DOM
*
*/
function initializeQuestion() {
// Attach questions
for (var i = 0; i < questionInstances.length; i++) {
var question = questionInstances[i];
// Make sure styles are not being added twice
$('.question-container:eq(' + i + ')', $myDom).attr('class', 'question-container');
question.attach($('.question-container:eq(' + i + ')', $myDom));
// Listen for image resize
registerImageLoadedListener(question);
// Add finish button
question.addButton('finish', params.texts.finishButton,
moveQuestion.bind(this, 1), false);
// Add next button
question.addButton('next', '', moveQuestion.bind(this, 1),
!params.disableBackwardsNavigation || !!question.getAnswerGiven(), {
href: '#', // Use href since this is a navigation button
'aria-label': params.texts.nextButton
});
// Add previous button
question.addButton('prev', '', moveQuestion.bind(this, -1),
!(questionInstances[0] === question || params.disableBackwardsNavigation), {
href: '#', // Use href since this is a navigation button
'aria-label': params.texts.prevButton
});
// Hide next button if it is the last question
if (questionInstances[questionInstances.length -1] === question) {
question.hideButton('next');
}
question.on('xAPI', function (event) {
var shortVerb = event.getVerb();
if (shortVerb === 'interacted' ||
shortVerb === 'answered' ||
shortVerb === 'attempted') {
toggleAnsweredDot(currentQuestion,
questionInstances[currentQuestion].getAnswerGiven());
_updateButtons();
}
if (shortVerb === 'completed') {
// An activity within this activity is not allowed to send completed events
event.setVerb('answered');
}
if (event.data.statement.context.extensions === undefined) {
event.data.statement.context.extensions = {};
}
event.data.statement.context.extensions['http://id.tincanapi.com/extension/ending-point'] = currentQuestion + 1;
});
// Mark question if answered
toggleAnsweredDot(i, question.getAnswerGiven());
}
}
this.attach = function (target) {
if (this.isRoot()) {
this.setActivityStarted();
}
if (typeof(target) === "string") {
$myDom = $('#' + target);
}
else {
$myDom = $(target);
}
// Render own DOM into target.
$myDom.children().remove();
$myDom.append($template);
if (params.backgroundImage !== undefined) {
$myDom.css({
overflow: 'hidden',
background: '#fff url("' + H5P.getPath(params.backgroundImage.path, contentId) + '") no-repeat 50% 50%',
backgroundSize: '100% auto'
});
}
if (params.introPage.backgroundImage !== undefined) {
var $intro = $myDom.find('.intro-page');
if ($intro.length) {
var bgImg = params.introPage.backgroundImage;
var bgImgRatio = (bgImg.height / bgImg.width);
$intro.css({
background: '#fff url("' + H5P.getPath(bgImg.path, contentId) + '") no-repeat 50% 50%',
backgroundSize: 'auto 100%',
minHeight: bgImgRatio * +window.getComputedStyle($intro[0]).width.replace('px','')
});
}
}
initializeQuestion();
// Allow other libraries to add transitions after the questions have been inited
$('.questionset', $myDom).addClass('started');
$('.qs-startbutton', $myDom)
.click(function () {
$(this).parents('.intro-page').hide();
$('.questionset', $myDom).show();
_showQuestion(params.initialQuestion);
event.preventDefault();
})
.keydown(function (event) {
switch (event.which) {
case 13: // Enter
case 32: // Space
$(this).parents('.intro-page').hide();
$('.questionset', $myDom).show();
_showQuestion(params.initialQuestion);
event.preventDefault();
}
});
/**
* Triggers changing the current question.
*
* @private
* @param {Object} [event]
*/
var handleProgressDotClick = function (event) {
// Disable dots when backward nav disabled
event.preventDefault();
if (params.disableBackwardsNavigation && !showingSolutions) {
return;
}
_stopQuestion(currentQuestion);
_showQuestion($(this).parent().index());
};
// Set event listeners.
$('.progress-dot', $myDom).click(handleProgressDotClick).keydown(function (event) {
var $this = $(this);
switch (event.which) {
case 13: // Enter
case 32: // Space
handleProgressDotClick.call(this, event);
break;
case 37: // Left Arrow
case 38: // Up Arrow
// Go to previous dot
var $prev = $this.parent().prev();
if ($prev.length) {
$prev.children('a').attr('tabindex', '0').focus();
$this.attr('tabindex', '-1');
}
break;
case 39: // Right Arrow
case 40: // Down Arrow
// Go to next dot
var $next = $this.parent().next();
if ($next.length) {
$next.children('a').attr('tabindex', '0').focus();
$this.attr('tabindex', '-1');
}
break;
}
});
// Hide all but current question
_showQuestion(currentQuestion, true);
if (renderSolutions) {
showSolutions();
}
// Update buttons in case they have changed (restored user state)
_updateButtons();
this.trigger('resize');
return this;
};
// Get current score for questionset.
this.getScore = function () {
var score = 0;
for (var i = questionInstances.length - 1; i >= 0; i--) {
score += questionInstances[i].getScore();
}
return score;
};
// Get total score possible for questionset.
this.getMaxScore = function () {
var score = 0;
for (var i = questionInstances.length - 1; i >= 0; i--) {
score += questionInstances[i].getMaxScore();
}
return score;
};
/**
* @deprecated since version 1.9.2
* @returns {number}
*/
this.totalScore = function () {
return this.getMaxScore();
};
/**
* Gather copyright information for the current content.
*
* @returns {H5P.ContentCopyrights}
*/
this.getCopyrights = function () {
var info = new H5P.ContentCopyrights();
// IntroPage Background
if (params.introPage !== undefined && params.introPage.backgroundImage !== undefined && params.introPage.backgroundImage.copyright !== undefined) {
var introBackground = new H5P.MediaCopyright(params.introPage.backgroundImage.copyright);
introBackground.setThumbnail(new H5P.Thumbnail(H5P.getPath(params.introPage.backgroundImage.path, contentId), params.introPage.backgroundImage.width, params.introPage.backgroundImage.height));
info.addMedia(introBackground);
}
// Background
if (params.backgroundImage !== undefined && params.backgroundImage.copyright !== undefined) {
var background = new H5P.MediaCopyright(params.backgroundImage.copyright);
background.setThumbnail(new H5P.Thumbnail(H5P.getPath(params.backgroundImage.path, contentId), params.backgroundImage.width, params.backgroundImage.height));
info.addMedia(background);
}
// Questions
var questionCopyrights;
for (var i = 0; i < questionInstances.length; i++) {
var instance = questionInstances[i];
var instanceParams = params.questions[i].params;
questionCopyrights = undefined;
if (instance.getCopyrights !== undefined) {
// Use the instance's own copyright generator
questionCopyrights = instance.getCopyrights();
}
if (questionCopyrights === undefined) {
// Create a generic flat copyright list
questionCopyrights = new H5P.ContentCopyrights();
H5P.findCopyrights(questionCopyrights, instanceParams.params, contentId,{
metadata: instanceParams.metadata,
machineName: instanceParams.library.split(' ')[0]
});
}
// Determine label
var label = (params.texts.questionLabel + ' ' + (i + 1));
if (instanceParams.params.contentName !== undefined) {
label += ': ' + instanceParams.params.contentName;
}
else if (instance.getTitle !== undefined) {
label += ': ' + instance.getTitle();
}
questionCopyrights.setLabel(label);
// Add info
info.addContent(questionCopyrights);
}
// Success video
var video;
if (params.endGame.successVideo !== undefined && params.endGame.successVideo.length > 0) {
video = params.endGame.successVideo[0];
if (video.copyright !== undefined) {
info.addMedia(new H5P.MediaCopyright(video.copyright));
}
}
// Fail video
if (params.endGame.failVideo !== undefined && params.endGame.failVideo.length > 0) {
video = params.endGame.failVideo[0];
if (video.copyright !== undefined) {
info.addMedia(new H5P.MediaCopyright(video.copyright));
}
}
return info;
};
this.getQuestions = function () {
return questionInstances;
};
this.showSolutions = function () {
renderSolutions = true;
};
/**
* Stop the given element's playback if any.
*
* @param {object} instance
*/
var pauseMedia = function (instance) {
try {
if (instance.pause !== undefined &&
(instance.pause instanceof Function ||
typeof instance.pause === 'function')) {
instance.pause();
}
}
catch (err) {
// Prevent crashing, log error.
H5P.error(err);
}
};
/**
* Returns the complete state of question set and sub-content
*
* @returns {Object} current state
*/
this.getCurrentState = function () {
return {
progress: showingSolutions ? questionInstances.length - 1 : currentQuestion,
answers: questionInstances.map(function (qi) {
return qi.getCurrentState();
}),
order: questionOrder,
poolOrder: poolOrder
};
};
/**
* Generate xAPI object definition used in xAPI statements.
* @return {Object}
*/
var getxAPIDefinition = function () {
var definition = {};
definition.interactionType = 'compound';
definition.type = 'http://adlnet.gov/expapi/activities/cmi.interaction';
definition.description = {
'en-US': ''
};
return definition;
};
/**
* Add the question itself to the definition part of an xAPIEvent
*/
var addQuestionToXAPI = function (xAPIEvent) {
var definition = xAPIEvent.getVerifiedStatementValue(['object', 'definition']);
$.extend(definition, getxAPIDefinition());
};
/**
* Get xAPI data from sub content types
*
* @param {Object} metaContentType
* @returns {array}
*/
var getXAPIDataFromChildren = function (metaContentType) {
return metaContentType.getQuestions().map(function (question) {
return question.getXAPIData();
});
};
/**
* Get xAPI data.
* Contract used by report rendering engine.
*
* @see contract at {@link https://h5p.org/documentation/developers/contracts#guides-header-6}
*/
this.getXAPIData = function () {
var xAPIEvent = this.createXAPIEventTemplate('answered');
addQuestionToXAPI(xAPIEvent);
xAPIEvent.setScoredResult(this.getScore(),
this.getMaxScore(),
this,
true,
this.getScore() === this.getMaxScore()
);
return {
statement: xAPIEvent.data.statement,
children: getXAPIDataFromChildren(this)
};
};
};
H5P.QuestionSet.prototype = Object.create(H5P.EventDispatcher.prototype);
H5P.QuestionSet.prototype.constructor = H5P.QuestionSet;
;