upgrade
This commit is contained in:
@@ -0,0 +1,356 @@
|
||||
var H5P = H5P || {};
|
||||
|
||||
/**
|
||||
* Class responsible for creating speech bubbles
|
||||
*/
|
||||
H5P.JoubelSpeechBubble = (function ($) {
|
||||
|
||||
var $currentSpeechBubble;
|
||||
var $currentContainer;
|
||||
var $tail;
|
||||
var $innerTail;
|
||||
var removeSpeechBubbleTimeout;
|
||||
var currentMaxWidth;
|
||||
|
||||
var DEFAULT_MAX_WIDTH = 400;
|
||||
|
||||
var iDevice = navigator.userAgent.match(/iPod|iPhone|iPad/g) ? true : false;
|
||||
|
||||
/**
|
||||
* Creates a new speech bubble
|
||||
*
|
||||
* @param {H5P.jQuery} $container The speaking object
|
||||
* @param {string} text The text to display
|
||||
* @param {number} maxWidth The maximum width of the bubble
|
||||
* @return {H5P.JoubelSpeechBubble}
|
||||
*/
|
||||
function JoubelSpeechBubble($container, text, maxWidth) {
|
||||
maxWidth = maxWidth || DEFAULT_MAX_WIDTH;
|
||||
currentMaxWidth = maxWidth;
|
||||
$currentContainer = $container;
|
||||
|
||||
this.isCurrent = function ($tip) {
|
||||
return $tip.is($currentContainer);
|
||||
};
|
||||
|
||||
this.remove = function () {
|
||||
remove();
|
||||
};
|
||||
|
||||
var fadeOutSpeechBubble = function ($speechBubble) {
|
||||
if (!$speechBubble) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Stop removing bubble
|
||||
clearTimeout(removeSpeechBubbleTimeout);
|
||||
|
||||
$speechBubble.removeClass('show');
|
||||
setTimeout(function () {
|
||||
if ($speechBubble) {
|
||||
$speechBubble.remove();
|
||||
$speechBubble = undefined;
|
||||
}
|
||||
}, 500);
|
||||
};
|
||||
|
||||
if ($currentSpeechBubble !== undefined) {
|
||||
remove();
|
||||
}
|
||||
|
||||
var $h5pContainer = getH5PContainer($container);
|
||||
|
||||
// Make sure we fade out old speech bubble
|
||||
fadeOutSpeechBubble($currentSpeechBubble);
|
||||
|
||||
// Create bubble
|
||||
$tail = $('<div class="joubel-speech-bubble-tail"></div>');
|
||||
$innerTail = $('<div class="joubel-speech-bubble-inner-tail"></div>');
|
||||
var $innerBubble = $(
|
||||
'<div class="joubel-speech-bubble-inner">' +
|
||||
'<div class="joubel-speech-bubble-text">' + text + '</div>' +
|
||||
'</div>'
|
||||
).prepend($innerTail);
|
||||
|
||||
$currentSpeechBubble = $(
|
||||
'<div class="joubel-speech-bubble" aria-live="assertive">'
|
||||
).append([$tail, $innerBubble])
|
||||
.appendTo($h5pContainer);
|
||||
|
||||
// Show speech bubble with transition
|
||||
setTimeout(function () {
|
||||
$currentSpeechBubble.addClass('show');
|
||||
}, 0);
|
||||
|
||||
position($currentSpeechBubble, $currentContainer, maxWidth, $tail, $innerTail);
|
||||
|
||||
// Handle click to close
|
||||
H5P.$body.on('mousedown.speechBubble', handleOutsideClick);
|
||||
|
||||
// Handle window resizing
|
||||
H5P.$window.on('resize', '', handleResize);
|
||||
|
||||
// Handle clicks when inside IV which blocks bubbling.
|
||||
$container.parents('.h5p-dialog')
|
||||
.on('mousedown.speechBubble', handleOutsideClick);
|
||||
|
||||
if (iDevice) {
|
||||
H5P.$body.css('cursor', 'pointer');
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
// Remove speechbubble if it belongs to a dom element that is about to be hidden
|
||||
H5P.externalDispatcher.on('domHidden', function (event) {
|
||||
if ($currentSpeechBubble !== undefined && event.data.$dom.find($currentContainer).length !== 0) {
|
||||
remove();
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Returns the closest h5p container for the given DOM element.
|
||||
*
|
||||
* @param {object} $container jquery element
|
||||
* @return {object} the h5p container (jquery element)
|
||||
*/
|
||||
function getH5PContainer($container) {
|
||||
var $h5pContainer = $container.closest('.h5p-frame');
|
||||
|
||||
// Check closest h5p frame first, then check for container in case there is no frame.
|
||||
if (!$h5pContainer.length) {
|
||||
$h5pContainer = $container.closest('.h5p-container');
|
||||
}
|
||||
|
||||
return $h5pContainer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Event handler that is called when the window is resized.
|
||||
*/
|
||||
function handleResize() {
|
||||
position($currentSpeechBubble, $currentContainer, currentMaxWidth, $tail, $innerTail);
|
||||
}
|
||||
|
||||
/**
|
||||
* Repositions the speech bubble according to the position of the container.
|
||||
*
|
||||
* @param {object} $currentSpeechbubble the speech bubble that should be positioned
|
||||
* @param {object} $container the container to which the speech bubble should point
|
||||
* @param {number} maxWidth the maximum width of the speech bubble
|
||||
* @param {object} $tail the tail (the triangle that points to the referenced container)
|
||||
* @param {object} $innerTail the inner tail (the triangle that points to the referenced container)
|
||||
*/
|
||||
function position($currentSpeechBubble, $container, maxWidth, $tail, $innerTail) {
|
||||
var $h5pContainer = getH5PContainer($container);
|
||||
|
||||
// Calculate offset between the button and the h5p frame
|
||||
var offset = getOffsetBetween($h5pContainer, $container);
|
||||
|
||||
var direction = (offset.bottom > offset.top ? 'bottom' : 'top');
|
||||
var tipWidth = offset.outerWidth * 0.9; // Var needs to be renamed to make sense
|
||||
var bubbleWidth = tipWidth > maxWidth ? maxWidth : tipWidth;
|
||||
|
||||
var bubblePosition = getBubblePosition(bubbleWidth, offset);
|
||||
var tailPosition = getTailPosition(bubbleWidth, bubblePosition, offset, $container.width());
|
||||
// Need to set font-size, since element is appended to body.
|
||||
// Using same font-size as parent. In that way it will grow accordingly
|
||||
// when resizing
|
||||
var fontSize = 16;//parseFloat($parent.css('font-size'));
|
||||
|
||||
// Set width and position of speech bubble
|
||||
$currentSpeechBubble.css(bubbleCSS(
|
||||
direction,
|
||||
bubbleWidth,
|
||||
bubblePosition,
|
||||
fontSize
|
||||
));
|
||||
|
||||
var preparedTailCSS = tailCSS(direction, tailPosition);
|
||||
$tail.css(preparedTailCSS);
|
||||
$innerTail.css(preparedTailCSS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Static function for removing the speechbubble
|
||||
*/
|
||||
var remove = function () {
|
||||
H5P.$body.off('mousedown.speechBubble');
|
||||
H5P.$window.off('resize', '', handleResize);
|
||||
$currentContainer.parents('.h5p-dialog').off('mousedown.speechBubble');
|
||||
if (iDevice) {
|
||||
H5P.$body.css('cursor', '');
|
||||
}
|
||||
if ($currentSpeechBubble !== undefined) {
|
||||
// Apply transition, then remove speech bubble
|
||||
$currentSpeechBubble.removeClass('show');
|
||||
|
||||
// Make sure we remove any old timeout before reassignment
|
||||
clearTimeout(removeSpeechBubbleTimeout);
|
||||
removeSpeechBubbleTimeout = setTimeout(function () {
|
||||
$currentSpeechBubble.remove();
|
||||
$currentSpeechBubble = undefined;
|
||||
}, 500);
|
||||
}
|
||||
// Don't return false here. If the user e.g. clicks a button when the bubble is visible,
|
||||
// we want the bubble to disapear AND the button to receive the event
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove the speech bubble and container reference
|
||||
*/
|
||||
function handleOutsideClick(event) {
|
||||
if (event.target === $currentContainer[0]) {
|
||||
return; // Button clicks are not outside clicks
|
||||
}
|
||||
|
||||
remove();
|
||||
// There is no current container when a container isn't clicked
|
||||
$currentContainer = undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate position for speech bubble
|
||||
*
|
||||
* @param {number} bubbleWidth The width of the speech bubble
|
||||
* @param {object} offset
|
||||
* @return {object} Return position for the speech bubble
|
||||
*/
|
||||
function getBubblePosition(bubbleWidth, offset) {
|
||||
var bubblePosition = {};
|
||||
|
||||
var tailOffset = 9;
|
||||
var widthOffset = bubbleWidth / 2;
|
||||
|
||||
// Calculate top position
|
||||
bubblePosition.top = offset.top + offset.innerHeight;
|
||||
|
||||
// Calculate bottom position
|
||||
bubblePosition.bottom = offset.bottom + offset.innerHeight + tailOffset;
|
||||
|
||||
// Calculate left position
|
||||
if (offset.left < widthOffset) {
|
||||
bubblePosition.left = 3;
|
||||
}
|
||||
else if ((offset.left + widthOffset) > offset.outerWidth) {
|
||||
bubblePosition.left = offset.outerWidth - bubbleWidth - 3;
|
||||
}
|
||||
else {
|
||||
bubblePosition.left = offset.left - widthOffset + (offset.innerWidth / 2);
|
||||
}
|
||||
|
||||
return bubblePosition;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate position for speech bubble tail
|
||||
*
|
||||
* @param {number} bubbleWidth The width of the speech bubble
|
||||
* @param {object} bubblePosition Speech bubble position
|
||||
* @param {object} offset
|
||||
* @param {number} iconWidth The width of the tip icon
|
||||
* @return {object} Return position for the tail
|
||||
*/
|
||||
function getTailPosition(bubbleWidth, bubblePosition, offset, iconWidth) {
|
||||
var tailPosition = {};
|
||||
// Magic numbers. Tuned by hand so that the tail fits visually within
|
||||
// the bounds of the speech bubble.
|
||||
var leftBoundary = 9;
|
||||
var rightBoundary = bubbleWidth - 20;
|
||||
|
||||
tailPosition.left = offset.left - bubblePosition.left + (iconWidth / 2) - 6;
|
||||
if (tailPosition.left < leftBoundary) {
|
||||
tailPosition.left = leftBoundary;
|
||||
}
|
||||
if (tailPosition.left > rightBoundary) {
|
||||
tailPosition.left = rightBoundary;
|
||||
}
|
||||
|
||||
tailPosition.top = -6;
|
||||
tailPosition.bottom = -6;
|
||||
|
||||
return tailPosition;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return bubble CSS for the desired growth direction
|
||||
*
|
||||
* @param {string} direction The direction the speech bubble will grow
|
||||
* @param {number} width The width of the speech bubble
|
||||
* @param {object} position Speech bubble position
|
||||
* @param {number} fontSize The size of the bubbles font
|
||||
* @return {object} Return CSS
|
||||
*/
|
||||
function bubbleCSS(direction, width, position, fontSize) {
|
||||
if (direction === 'top') {
|
||||
return {
|
||||
width: width + 'px',
|
||||
bottom: position.bottom + 'px',
|
||||
left: position.left + 'px',
|
||||
fontSize: fontSize + 'px',
|
||||
top: ''
|
||||
};
|
||||
}
|
||||
else {
|
||||
return {
|
||||
width: width + 'px',
|
||||
top: position.top + 'px',
|
||||
left: position.left + 'px',
|
||||
fontSize: fontSize + 'px',
|
||||
bottom: ''
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return tail CSS for the desired growth direction
|
||||
*
|
||||
* @param {string} direction The direction the speech bubble will grow
|
||||
* @param {object} position Tail position
|
||||
* @return {object} Return CSS
|
||||
*/
|
||||
function tailCSS(direction, position) {
|
||||
if (direction === 'top') {
|
||||
return {
|
||||
bottom: position.bottom + 'px',
|
||||
left: position.left + 'px',
|
||||
top: ''
|
||||
};
|
||||
}
|
||||
else {
|
||||
return {
|
||||
top: position.top + 'px',
|
||||
left: position.left + 'px',
|
||||
bottom: ''
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the offset between an element inside a container and the
|
||||
* container. Only works if all the edges of the inner element are inside the
|
||||
* outer element.
|
||||
* Width/height of the elements is included as a convenience.
|
||||
*
|
||||
* @param {H5P.jQuery} $outer
|
||||
* @param {H5P.jQuery} $inner
|
||||
* @return {object} Position offset
|
||||
*/
|
||||
function getOffsetBetween($outer, $inner) {
|
||||
var outer = $outer[0].getBoundingClientRect();
|
||||
var inner = $inner[0].getBoundingClientRect();
|
||||
|
||||
return {
|
||||
top: inner.top - outer.top,
|
||||
right: outer.right - inner.right,
|
||||
bottom: outer.bottom - inner.bottom,
|
||||
left: inner.left - outer.left,
|
||||
innerWidth: inner.width,
|
||||
innerHeight: inner.height,
|
||||
outerWidth: outer.width,
|
||||
outerHeight: outer.height
|
||||
};
|
||||
}
|
||||
|
||||
return JoubelSpeechBubble;
|
||||
})(H5P.jQuery);
|
||||
Reference in New Issue
Block a user