JI-1059 Add offline request queue with retry dialog

Add logic for adding extra class and hiding buttons in conf dialog
Update font files with throbber
pull/61/head
Thomas Marstrander 2019-04-03 09:43:09 +02:00
parent ada2f4009d
commit 39d27ab9bb
13 changed files with 314 additions and 54 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
fonts/h5p-core-21.eot Normal file

Binary file not shown.

View File

@ -5,21 +5,21 @@
<json>
<![CDATA[
{
"fontFamily": "h5p-core-18",
"fontFamily": "h5p-core-21",
"description": "Font generated by IcoMoon.",
"majorVersion": 1,
"minorVersion": 1,
"version": "Version 1.1",
"fontId": "h5p-core-18",
"psName": "h5p-core-18",
"fontId": "h5p-core-21",
"psName": "h5p-core-21",
"subFamily": "Regular",
"fullName": "h5p-core-18"
"fullName": "h5p-core-21"
}
]]>
</json>
</metadata>
<defs>
<font id="h5p-core-18" horiz-adv-x="1024">
<font id="h5p-core-21" horiz-adv-x="1024">
<font-face units-per-em="1024" ascent="960" descent="-64" />
<missing-glyph horiz-adv-x="1024" />
<glyph unicode="&#x20;" horiz-adv-x="512" d="" />
@ -51,6 +51,6 @@
<glyph unicode="&#xe908;" glyph-name="info-important-description" data-tags="info-important-description" d="M512 718.701c-188.5 0-341.3-152.8-341.3-341.3s152.8-341.4 341.3-341.4 341.3 152.8 341.3 341.3-152.8 341.4-341.3 341.4v0zM512 64.601c-172.7 0-312.7 140-312.7 312.7s140 312.7 312.7 312.7c172.7 0 312.7-140 312.7-312.7-0.2-172.6-140.1-312.5-312.7-312.7v0zM512 626.901c-137.9 0-249.6-111.8-249.6-249.6s111.7-249.6 249.6-249.6 249.6 111.8 249.6 249.6-111.8 249.6-249.6 249.6v0z" />
<glyph unicode="&#xe909;" glyph-name="icon-info" data-tags="icon-info" d="M467.2 480.855h87.467c0.028 0 0.062 0 0.095 0 6.056 0 11.499 2.629 15.248 6.808 3.979 4.15 6.419 9.769 6.419 15.957 0 0.097-0.001 0.194-0.002 0.29v70.385c0.001 0.082 0.002 0.179 0.002 0.276 0 6.188-2.44 11.806-6.409 15.946-3.759 4.19-9.201 6.819-15.257 6.819-0.033 0-0.067 0-0.1 0h-87.462c-0.028 0-0.062 0-0.095 0-6.056 0-11.499-2.629-15.248-6.808-3.979-4.15-6.419-9.769-6.419-15.957 0-0.097 0.001-0.194 0.002-0.29v-69.959c-0.001-0.082-0.002-0.179-0.002-0.276 0-6.188 2.44-11.806 6.409-15.946 3.715-4.373 9.2-7.159 15.338-7.245zM597.333 177.922h-22.187v209.92c0.001 0.082 0.002 0.179 0.002 0.276 0 6.188-2.44 11.806-6.409 15.946-3.759 4.19-9.201 6.819-15.257 6.819-0.033 0-0.067 0-0.1 0h-130.128c-0.028 0-0.062 0-0.095 0-6.056 0-11.499-2.629-15.248-6.808-3.979-4.15-6.419-9.769-6.419-15.957 0-0.097 0.001-0.194 0.002-0.29v-46.492c-0.001-0.082-0.002-0.179-0.002-0.276 0-6.188 2.44-11.806 6.409-15.946 3.759-4.19 9.201-6.819 15.257-6.819 0.033 0 0.067 0 0.1 0h22.182v-139.947h-22.187c-0.028 0-0.062 0-0.095 0-6.056 0-11.499-2.629-15.248-6.808-3.979-4.15-6.419-9.769-6.419-15.957 0-0.097 0.001-0.194 0.002-0.29v-46.492c-0.001-0.082-0.002-0.179-0.002-0.276 0-6.188 2.44-11.806 6.409-15.946 3.759-4.19 9.201-6.819 15.257-6.819 0.033 0 0.067 0 0.1 0h174.075c0.028 0 0.062 0 0.095 0 6.056 0 11.499 2.629 15.248 6.808 3.979 4.15 6.419 9.769 6.419 15.957 0 0.097-0.001 0.194-0.002 0.29v46.065c0.043 0.527 0.067 1.141 0.067 1.761 0 5.302-1.791 10.185-4.8 14.079-3.742 4.424-9.36 7.247-15.636 7.247-0.489 0-0.975-0.017-1.456-0.051z" />
<glyph unicode="&#xe90a;" glyph-name="paste" data-tags="paste" d="M394.402 723.733h-75.333c-65.867 0-119.2-53.333-119.2-119.2v-288.533c0-56.4 37.6-100.4 87.867-116v69.067c-15.733 9.467-25.067 25.067-25.067 47.067v288.4c0 31.333 25.067 56.4 56.4 56.4h131.733c0 0 0 0 3.2 3.2v0c0 31.333-28.267 59.6-59.6 59.6zM704.802 613.866c0 0-28.267 0-40.8 0-12.533 34.533-43.867 59.6-84.667 59.6s-69.067-25.067-81.6-59.6c-12.533 0-40.8 0-40.8 0-65.867 0-119.2-53.333-119.2-119.2v-288.533c0-65.867 53.333-119.2 119.2-119.2h247.867c65.867 0 119.2 53.333 119.2 119.2v285.467c3.2 65.867-53.2 122.267-119.2 122.267zM582.535 626.533c22 0 40.8-18.8 40.8-40.8s-18.8-40.8-40.8-40.8c-22 0-40.8 18.8-40.8 40.8s15.733 40.8 40.8 40.8zM764.402 203.066c0-31.333-25.067-56.4-56.4-56.4h-250.933c-31.333 0-56.4 25.067-56.4 56.4v288.533c0 18.8 9.467 37.6 25.067 47.067v0c0-43.867 34.533-78.4 78.4-78.4h160c43.867 0 78.4 34.533 78.4 78.4v0c12.533-9.467 22-28.267 22-47.067v-288.533z" />
<glyph unicode="&#xe90b;" glyph-name="reuse" data-tags="reuse" d="M734.975 624.754c-54.605 61.619-134.123 100.573-222.977 100.573-164.936 0-298.661-133.724-298.661-298.661h74.667c0 123.721 100.272 223.993 223.993 223.993 68.214 0 128.747-30.96 169.766-79.119l-70.213-70.213h199.109v199.109l-75.689-75.689zM512 202.674c-68.214 0-128.747 30.96-169.766 79.119l70.213 70.213h-199.109v-199.109l75.689 75.689c54.605-61.619 134.123-100.573 222.977-100.573 164.936 0 298.661 133.724 298.661 298.661h-74.667c0-123.721-100.272-223.993-223.993-223.993z" />
<glyph unicode="&#xe90b;" glyph-name="reuse" data-tags="reuse" d="M734.974 646.084c-54.605 61.619-134.123 100.573-222.977 100.573-164.936 0-298.661-133.724-298.661-298.661h74.667c0 123.721 100.272 223.993 223.993 223.993 68.214 0 128.747-30.96 169.766-79.119l-70.213-70.213h199.109v199.109l-75.689-75.689zM511.999 224.004c-68.214 0-128.747 30.96-169.766 79.119l70.213 70.213h-199.109v-199.109l75.689 75.689c54.605-61.619 134.123-100.573 222.977-100.573 164.936 0 298.661 133.724 298.661 298.661h-74.667c0-123.721-100.272-223.993-223.993-223.993z" />
<glyph unicode="&#xe90c;" glyph-name="info-outlined" data-tags="info-outlined" d="M467.199 202.669h89.599v268.8h-89.599v-268.8zM512 874.668c-247.296 0-448.001-200.705-448.001-448.001s200.705-448.001 448.001-448.001 448.001 200.705 448.001 448.001-200.705 448.001-448.001 448.001zM512 68.269c-197.568 0-358.398 160.83-358.398 358.398s160.83 358.398 358.398 358.398 358.398-160.83 358.398-358.398-160.83-358.398-358.398-358.398zM467.199 561.067h89.599v89.599h-89.599v-89.599z" />
</font></defs></svg>

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 26 KiB

BIN
fonts/h5p-core-21.ttf Normal file

Binary file not shown.

BIN
fonts/h5p-core-21.woff Normal file

Binary file not shown.

View File

@ -3489,6 +3489,7 @@ class H5PCore {
'offlineDialogBody' => $this->h5pF->t('We were unable to send information about your completion of this task. Please check your internet connection.'),
'offlineDialogRetryMessage' => $this->h5pF->t('Retrying in :num....'),
'offlineDialogRetryButtonLabel' => $this->h5pF->t('Retry now'),
'offlineSuccessfulSubmit' => $this->h5pF->t('Successfully submitted results.'),
);
}
}

View File

@ -11,6 +11,9 @@ H5P.ConfirmationDialog = (function (EventDispatcher) {
* @param [options.dialogText] Dialog text
* @param [options.cancelText] Cancel dialog button text
* @param [options.confirmText] Confirm dialog button text
* @param [options.hideCancel] Hide cancel button
* @param [options.hideExit] Hide exit button
* @param [options.classes] Extra classes for popup
* @constructor
*/
function ConfirmationDialog(options) {
@ -73,6 +76,12 @@ H5P.ConfirmationDialog = (function (EventDispatcher) {
// Create outer popup
var popup = document.createElement('div');
popup.classList.add('h5p-confirmation-dialog-popup', 'hidden');
if (options.classes) {
options.classes.forEach(function (popupClass) {
popup.classList.add(popupClass);
});
}
popup.setAttribute('role', 'dialog');
popup.setAttribute('aria-labelledby', 'h5p-confirmation-dialog-dialog-text-' + uniqueId);
popupBackground.appendChild(popup);
@ -139,7 +148,14 @@ H5P.ConfirmationDialog = (function (EventDispatcher) {
flowTo(confirmButton, e);
}
});
buttons.appendChild(cancelButton);
if (!options.hideCancel) {
buttons.appendChild(cancelButton);
}
else {
// Center buttons
buttons.classList.add('center');
}
// Confirm handler
confirmButton.addEventListener('click', dialogConfirmed);
@ -148,7 +164,8 @@ H5P.ConfirmationDialog = (function (EventDispatcher) {
dialogConfirmed(e);
}
else if (e.which === 9 && !e.shiftKey) { // Tab
flowTo(cancelButton, e);
const nextButton = !options.hideCancel ? cancelButton : confirmButton;
flowTo(nextButton, e);
}
});
buttons.appendChild(confirmButton);
@ -160,7 +177,9 @@ H5P.ConfirmationDialog = (function (EventDispatcher) {
dialogCanceled(e);
}
});
popup.appendChild(exitButton);
if (!options.hideExit) {
popup.appendChild(exitButton);
}
// Wrapper element
var wrapperElement;
@ -346,6 +365,15 @@ H5P.ConfirmationDialog = (function (EventDispatcher) {
return this;
};
/**
* Retrieve element
*
* @return {HTMLElement}
*/
this.getElement = function () {
return popup;
};
/**
* Sets the minimum height of the view port
*

View File

@ -66,20 +66,7 @@ H5P.init = function (target) {
H5P.$body = H5P.jQuery(document.body);
}
H5P.offlineRequestQueue = new H5P.RequestQueue();
// We could handle previously failed requests here, instead we throw them away
// TODO: Add dialog
H5P.offlineRequestQueue.clear();
H5P.offlineRequestQueue.on('requestQueued', function () {
});
H5P.offlineRequestQueue.on('processingQueue', function () {
});
H5P.offlineRequestQueue.on('queueEmptied', function () {
});
H5P.offlineRequestQueue = new H5P.OfflineRequestQueue();
// Determine if we can use full screen
if (H5P.fullscreenSupported === undefined) {

View File

@ -7,14 +7,15 @@ H5P.RequestQueue = (function ($, EventDispatcher) {
/**
* A queue for requests, will be automatically processed when regaining connection
*
* @param {boolean} [options.showToast] Disable showing toast when losing or regaining connection
* @param {boolean} [options.showToast] Show toast when losing or regaining connection
* @constructor
*/
const RequestQueue = function (options) {
EventDispatcher.call(this);
this.processingQueue = false;
options = options || {};
this.showToast = options ? options.showToast : false;
this.showToast = options.showToast;
this.itemName = 'requestQueue';
// Initialize listener for when requests are added to queue
@ -23,7 +24,7 @@ H5P.RequestQueue = (function ($, EventDispatcher) {
};
/**
* Add request to queue
* Add request to queue. Only supports posts currently.
*
* @param {string} url
* @param {Object} data
@ -46,7 +47,10 @@ H5P.RequestQueue = (function ($, EventDispatcher) {
window.localStorage.setItem(this.itemName, JSON.stringify(storedStatements));
this.trigger('requestQueued', storedStatements);
this.trigger('requestQueued', {
storedStatements: storedStatements,
processingQueue: this.processingQueue,
});
return true;
};
@ -73,7 +77,7 @@ H5P.RequestQueue = (function ($, EventDispatcher) {
*
* @returns {boolean} True if the storage was successfully cleared
*/
RequestQueue.prototype.clear = function () {
RequestQueue.prototype.clearQueue = function () {
if (!window.localStorage) {
return false;
}
@ -98,20 +102,16 @@ H5P.RequestQueue = (function ($, EventDispatcher) {
return false;
}
// Application is offline, re-send when we detect a connection
if (!window.navigator.onLine) {
return false;
}
// We're online, attempt to send queued requests
// Attempt to send queued requests
const queue = this.getStoredRequests();
const queueLength = queue.length;
// Clear storage, failed requests will be re-added
this.clear();
this.clearQueue();
// No items left in queue
if (!queueLength) {
this.trigger('emptiedQueue', queue);
return true;
}
@ -169,28 +169,20 @@ H5P.RequestQueue = (function ($, EventDispatcher) {
// Finished processing this queue
this.processingQueue = false;
if (!window.navigator.onLine) {
return;
}
// Process next queue if items were added while processing current queue
// Run empty queue callback with next request queue
const requestQueue = this.getStoredRequests();
if (requestQueue.length) {
this.resumeQueue();
return;
}
// Run empty queue callback
this.trigger('queueEmptied');
this.trigger('queueEmptied', requestQueue);
};
/**
* Display toast message on the first content of current page
*
* @param {string} msg Message to display
* @param {boolean} [forceShow] Force override showing the toast
*/
RequestQueue.prototype.displayToastMessage = function (msg) {
if (!this.showToast) {
RequestQueue.prototype.displayToastMessage = function (msg, forceShow) {
if (!this.showToast && !forceShow) {
return;
}
H5P.attachToastTo(
@ -229,3 +221,192 @@ H5P.RequestQueue = (function ($, EventDispatcher) {
return RequestQueue;
})(H5P.jQuery, H5P.EventDispatcher);
/**
* Request queue for retrying failing requests, will automatically retry them when you come online
*
* @type {offlineRequestQueue}
*/
H5P.OfflineRequestQueue = (function (RequestQueue, Dialog) {
return function offlineRequestQueue() {
const requestQueue = new RequestQueue();
// We could handle requests from previous pages here, but instead we throw them away
requestQueue.clearQueue();
let startTime = null;
const retryIntervals = [10, 20, 40, 60, 120, 300, 600];
let intervalIndex = -1;
let currentInterval = null;
let isAttached = false;
let isShowing = false;
let isLoading = false;
const offlineDialog = new Dialog({
headerText: H5P.t('offlineDialogHeader'),
dialogText: H5P.t('offlineDialogBody'),
confirmText: H5P.t('offlineDialogRetryButtonLabel'),
hideCancel: true,
hideExit: true,
classes: ['offline'],
});
const dialog = offlineDialog.getElement();
// Add retry text to body
const countDownText = document.createElement('div');
countDownText.classList.add('count-down');
countDownText.innerHTML = H5P.t('offlineDialogRetryMessage')
.replace(':num', '<span class="count-down-num">0</span>');
dialog.querySelector('.h5p-confirmation-dialog-text').appendChild(countDownText);
const countDownNum = countDownText.querySelector('.count-down-num');
// Create throbber
const throbberWrapper = document.createElement('div');
throbberWrapper.classList.add('throbber-wrapper');
const throbber = document.createElement('div');
throbber.classList.add('sending-requests-throbber');
throbberWrapper.appendChild(throbber);
requestQueue.on('requestQueued', function (e) {
// Already processing queue, wait until queue has finished processing before showing dialog
if (e.data && e.data.processingQueue) {
return;
}
if (!isAttached) {
const rootContent = document.body.querySelector('.h5p-content');
if (!rootContent) {
return;
}
offlineDialog.appendTo(rootContent);
rootContent.appendChild(throbberWrapper);
isAttached = true;
}
startCountDown();
}.bind(this));
requestQueue.on('queueEmptied', function (e) {
if (e.data && e.data.length) {
// New requests were added while processing queue or requests failed again. Re-queue requests.
startCountDown(true);
return;
}
// Successfully emptied queue
clearInterval(currentInterval);
toggleThrobber(false);
intervalIndex = -1;
if (isShowing) {
offlineDialog.hide();
isShowing = false;
}
requestQueue.displayToastMessage(H5P.t('offlineSuccessfulSubmit'), true);
}.bind(this));
offlineDialog.on('confirmed', function () {
// Show dialog on next render in case it is being hidden by the 'confirm' button
isShowing = false;
setTimeout(function () {
retryRequests();
}, 100);
}.bind(this));
/**
* Toggle throbber visibility
*
* @param {boolean} [forceShow] Will force throbber visibility if set
*/
const toggleThrobber = function (forceShow) {
isLoading = !isLoading;
if (forceShow !== undefined) {
isLoading = forceShow;
}
if (isLoading && isShowing) {
offlineDialog.hide();
isShowing = false;
}
if (isLoading) {
throbberWrapper.classList.add('show');
}
else {
throbberWrapper.classList.remove('show');
}
};
/**
* Retries the failed requests
*/
const retryRequests = function () {
clearInterval(currentInterval);
toggleThrobber(true);
requestQueue.resumeQueue();
};
/**
* Increments retry interval
*/
const incrementRetryInterval = function () {
intervalIndex += 1;
if (intervalIndex >= retryIntervals.length) {
intervalIndex = retryIntervals.length - 1;
}
};
/**
* Starts counting down to retrying queued requests.
*
* @param forceDelayedShow
*/
const startCountDown = function (forceDelayedShow) {
toggleThrobber(false);
if (!isShowing) {
if (forceDelayedShow) {
// Must force delayed show since dialog may be hiding, and confirmation dialog does not
// support this.
setTimeout(function () {
offlineDialog.show();
}, 100);
}
else {
offlineDialog.show();
}
}
isShowing = true;
startTime = new Date().getTime();
incrementRetryInterval();
currentInterval = setInterval(updateCountDown, 100);
};
/**
* Updates the count down timer. Retries requests when time expires.
*/
const updateCountDown = function () {
const time = new Date().getTime();
const timeElapsed = Math.floor((time - startTime) / 1000);
const timeLeft = retryIntervals[intervalIndex] - timeElapsed;
countDownNum.textContent = timeLeft.toString();
// Retry interval reached, retry requests
if (timeLeft <= 0) {
retryRequests();
}
};
/**
* Add request to offline request queue. Only supports posts for now.
*
* @param {string} url The request url
* @param {Object} data The request data
*/
this.add = function (url, data) {
requestQueue.add(url, data);
};
};
})(H5P.RequestQueue, H5P.ConfirmationDialog);

View File

@ -116,3 +116,66 @@ button.h5p-confirmation-dialog-exit:hover {
margin-top: -6px;
display: inline-block;
}
.h5p-confirmation-dialog-popup.offline .h5p-confirmation-dialog-buttons {
float: none;
text-align: center;
}
.h5p-confirmation-dialog-popup.offline .count-down {
font-family: Arial;
margin-top: 0.15em;
color: #000;
}
.h5p-confirmation-dialog-popup.offline .h5p-confirmation-dialog-confirm-button:before {
content: "\e90b";
}
.throbber-wrapper {
display: none;
position: absolute;
height: 100%;
width: 100%;
top: 0;
left: 0;
z-index: 1;
background: rgba(44, 44, 44, 0.9);
}
.throbber-wrapper.show {
display: block;
}
.throbber-wrapper .throbber-container {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.throbber-wrapper .sending-requests-throbber{
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.throbber-wrapper .sending-requests-throbber:before {
display: block;
font-family: 'H5P';
content: "\e90b";
color: white;
font-size: 10em;
animation: request-throbber 1.5s infinite linear;
}
@keyframes request-throbber {
from {
transform: rotate(0);
}
to {
transform: rotate(359deg);
}
}

View File

@ -3,11 +3,11 @@
/* Custom H5P font to use for icons. */
@font-face {
font-family: 'h5p';
src: url('../fonts/h5p-core-20.eot?cb8kvi');
src: url('../fonts/h5p-core-20.eot?cb8kvi#iefix') format('embedded-opentype'),
url('../fonts/h5p-core-20.ttf?cb8kvi') format('truetype'),
url('../fonts/h5p-core-20.woff?cb8kvi') format('woff'),
url('../fonts/h5p-core-20.svg?cb8kvi#h5p') format('svg');
src: url('../fonts/h5p-core-21.eot?mz1lkp');
src: url('../fonts/h5p-core-21.eot?mz1lkp#iefix') format('embedded-opentype'),
url('../fonts/h5p-core-21.ttf?mz1lkp') format('truetype'),
url('../fonts/h5p-core-21.woff?mz1lkp') format('woff'),
url('../fonts/h5p-core-21.svg?mz1lkp#h5p') format('svg');
font-weight: normal;
font-style: normal;
}