1/** 2 * Queue requests and handle them at your convenience 3 * 4 * @type {RequestQueue} 5 */ 6H5P.RequestQueue = (function ($, EventDispatcher) { 7 /** 8 * A queue for requests, will be automatically processed when regaining connection 9 * 10 * @param {boolean} [options.showToast] Show toast when losing or regaining connection 11 * @constructor 12 */ 13 const RequestQueue = function (options) { 14 EventDispatcher.call(this); 15 this.processingQueue = false; 16 options = options || {}; 17 18 this.showToast = options.showToast; 19 this.itemName = 'requestQueue'; 20 }; 21 22 /** 23 * Add request to queue. Only supports posts currently. 24 * 25 * @param {string} url 26 * @param {Object} data 27 * @returns {boolean} 28 */ 29 RequestQueue.prototype.add = function (url, data) { 30 if (!window.localStorage) { 31 return false; 32 } 33 34 let storedStatements = this.getStoredRequests(); 35 if (!storedStatements) { 36 storedStatements = []; 37 } 38 39 storedStatements.push({ 40 url: url, 41 data: data, 42 }); 43 44 window.localStorage.setItem(this.itemName, JSON.stringify(storedStatements)); 45 46 this.trigger('requestQueued', { 47 storedStatements: storedStatements, 48 processingQueue: this.processingQueue, 49 }); 50 return true; 51 }; 52 53 /** 54 * Get stored requests 55 * 56 * @returns {boolean|Array} Stored requests 57 */ 58 RequestQueue.prototype.getStoredRequests = function () { 59 if (!window.localStorage) { 60 return false; 61 } 62 63 const item = window.localStorage.getItem(this.itemName); 64 if (!item) { 65 return []; 66 } 67 68 return JSON.parse(item); 69 }; 70 71 /** 72 * Clear stored requests 73 * 74 * @returns {boolean} True if the storage was successfully cleared 75 */ 76 RequestQueue.prototype.clearQueue = function () { 77 if (!window.localStorage) { 78 return false; 79 } 80 81 window.localStorage.removeItem(this.itemName); 82 return true; 83 }; 84 85 /** 86 * Start processing of requests queue 87 * 88 * @return {boolean} Returns false if it was not possible to resume processing queue 89 */ 90 RequestQueue.prototype.resumeQueue = function () { 91 // Not supported 92 if (!H5PIntegration || !window.navigator || !window.localStorage) { 93 return false; 94 } 95 96 // Already processing 97 if (this.processingQueue) { 98 return false; 99 } 100 101 // Attempt to send queued requests 102 const queue = this.getStoredRequests(); 103 const queueLength = queue.length; 104 105 // Clear storage, failed requests will be re-added 106 this.clearQueue(); 107 108 // No items left in queue 109 if (!queueLength) { 110 this.trigger('emptiedQueue', queue); 111 return true; 112 } 113 114 // Make sure requests are not changed while they're being handled 115 this.processingQueue = true; 116 117 // Process queue in original order 118 this.processQueue(queue); 119 return true 120 }; 121 122 /** 123 * Process first item in the request queue 124 * 125 * @param {Array} queue Request queue 126 */ 127 RequestQueue.prototype.processQueue = function (queue) { 128 if (!queue.length) { 129 return; 130 } 131 132 this.trigger('processingQueue'); 133 134 // Make sure the requests are processed in a FIFO order 135 const request = queue.shift(); 136 137 const self = this; 138 $.post(request.url, request.data) 139 .fail(self.onQueuedRequestFail.bind(self, request)) 140 .always(self.onQueuedRequestProcessed.bind(self, queue)) 141 }; 142 143 /** 144 * Request fail handler 145 * 146 * @param {Object} request 147 */ 148 RequestQueue.prototype.onQueuedRequestFail = function (request) { 149 // Queue the failed request again if we're offline 150 if (!window.navigator.onLine) { 151 this.add(request.url, request.data); 152 } 153 }; 154 155 /** 156 * An item in the queue was processed 157 * 158 * @param {Array} queue Queue that was processed 159 */ 160 RequestQueue.prototype.onQueuedRequestProcessed = function (queue) { 161 if (queue.length) { 162 this.processQueue(queue); 163 return; 164 } 165 166 // Finished processing this queue 167 this.processingQueue = false; 168 169 // Run empty queue callback with next request queue 170 const requestQueue = this.getStoredRequests(); 171 this.trigger('queueEmptied', requestQueue); 172 }; 173 174 /** 175 * Display toast message on the first content of current page 176 * 177 * @param {string} msg Message to display 178 * @param {boolean} [forceShow] Force override showing the toast 179 * @param {Object} [configOverride] Override toast message config 180 */ 181 RequestQueue.prototype.displayToastMessage = function (msg, forceShow, configOverride) { 182 if (!this.showToast && !forceShow) { 183 return; 184 } 185 186 const config = H5P.jQuery.extend(true, {}, { 187 position: { 188 horizontal : 'centered', 189 vertical: 'centered', 190 noOverflowX: true, 191 } 192 }, configOverride); 193 194 H5P.attachToastTo(H5P.jQuery('.h5p-content:first')[0], msg, config); 195 }; 196 197 return RequestQueue; 198})(H5P.jQuery, H5P.EventDispatcher); 199 200/** 201 * Request queue for retrying failing requests, will automatically retry them when you come online 202 * 203 * @type {offlineRequestQueue} 204 */ 205H5P.OfflineRequestQueue = (function (RequestQueue, Dialog) { 206 207 /** 208 * Constructor 209 * 210 * @param {Object} [options] Options for offline request queue 211 * @param {Object} [options.instance] The H5P instance which UI components are placed within 212 */ 213 const offlineRequestQueue = function (options) { 214 const requestQueue = new RequestQueue(); 215 216 // We could handle requests from previous pages here, but instead we throw them away 217 requestQueue.clearQueue(); 218 219 let startTime = null; 220 const retryIntervals = [10, 20, 40, 60, 120, 300, 600]; 221 let intervalIndex = -1;
222 let currentInterval = null; 223 let isAttached = false; 224 let isShowing = false; 225 let isLoading = false; 226 const instance = options.instance; 227 228 const offlineDialog = new Dialog({ 229 headerText: H5P.t('offlineDialogHeader'), 230 dialogText: H5P.t('offlineDialogBody'), 231 confirmText: H5P.t('offlineDialogRetryButtonLabel'), 232 hideCancel: true, 233 hideExit: true, 234 classes: ['offline'], 235 instance: instance, 236 skipRestoreFocus: true, 237 }); 238 239 const dialog = offlineDialog.getElement(); 240 241 // Add retry text to body 242 const countDownText = document.createElement('div'); 243 countDownText.classList.add('count-down'); 244 countDownText.innerHTML = H5P.t('offlineDialogRetryMessage') 245 .replace(':num', '<span class="count-down-num">0</span>'); 246 247 dialog.querySelector('.h5p-confirmation-dialog-text').appendChild(countDownText); 248 const countDownNum = countDownText.querySelector('.count-down-num'); 249 250 // Create throbber 251 const throbberWrapper = document.createElement('div'); 252 throbberWrapper.classList.add('throbber-wrapper'); 253 const throbber = document.createElement('div'); 254 throbber.classList.add('sending-requests-throbber'); 255 throbberWrapper.appendChild(throbber); 256 257 requestQueue.on('requestQueued', function (e) { 258 // Already processing queue, wait until queue has finished processing before showing dialog 259 if (e.data && e.data.processingQueue) { 260 return; 261 } 262 263 if (!isAttached) { 264 const rootContent = document.body.querySelector('.h5p-content'); 265 if (!rootContent) { 266 return; 267 } 268 offlineDialog.appendTo(rootContent); 269 rootContent.appendChild(throbberWrapper); 270 isAttached = true; 271 } 272 273 startCountDown(); 274 }.bind(this)); 275 276 requestQueue.on('queueEmptied', function (e) { 277 if (e.data && e.data.length) { 278 // New requests were added while processing queue or requests failed again. Re-queue requests. 279 startCountDown(true); 280 return; 281 } 282 283 // Successfully emptied queue 284 clearInterval(currentInterval); 285 toggleThrobber(false); 286 intervalIndex = -1; 287 if (isShowing) { 288 offlineDialog.hide(); 289 isShowing = false; 290 } 291 292 requestQueue.displayToastMessage( 293 H5P.t('offlineSuccessfulSubmit'), 294 true, 295 { 296 position: { 297 vertical: 'top', 298 offsetVertical: '100', 299 } 300 } 301 ); 302 303 }.bind(this)); 304 305 offlineDialog.on('confirmed', function () { 306 // Show dialog on next render in case it is being hidden by the 'confirm' button 307 isShowing = false; 308 setTimeout(function () { 309 retryRequests(); 310 }, 100); 311 }.bind(this)); 312 313 // Initialize listener for when requests are added to queue 314 window.addEventListener('online', function () { 315 retryRequests(); 316 }.bind(this)); 317 318 // Listen for queued requests outside the iframe 319 window.addEventListener('message', function (event) { 320 const isValidQueueEvent = window.parent === event.source 321 && event.data.context === 'h5p' 322 && event.data.action === 'queueRequest'; 323 324 if (!isValidQueueEvent) { 325 return; 326 } 327 328 this.add(event.data.url, event.data.data); 329 }.bind(this)); 330 331 /** 332 * Toggle throbber visibility 333 * 334 * @param {boolean} [forceShow] Will force throbber visibility if set 335 */ 336 const toggleThrobber = function (forceShow) { 337 isLoading = !isLoading; 338 if (forceShow !== undefined) { 339 isLoading = forceShow; 340 } 341 342 if (isLoading && isShowing) { 343 offlineDialog.hide(); 344 isShowing = false; 345 } 346 347 if (isLoading) { 348 throbberWrapper.classList.add('show'); 349 } 350 else { 351 throbberWrapper.classList.remove('show'); 352 } 353 }; 354 /** 355 * Retries the failed requests 356 */ 357 const retryRequests = function () { 358 clearInterval(currentInterval); 359 toggleThrobber(true); 360 requestQueue.resumeQueue(); 361 }; 362 363 /** 364 * Increments retry interval 365 */ 366 const incrementRetryInterval = function () { 367 intervalIndex += 1; 368 if (intervalIndex >= retryIntervals.length) { 369 intervalIndex = retryIntervals.length - 1; 370 } 371 }; 372 373 /** 374 * Starts counting down to retrying queued requests. 375 * 376 * @param forceDelayedShow 377 */ 378 const startCountDown = function (forceDelayedShow) { 379 // Already showing, wait for retry 380 if (isShowing) { 381 return; 382 } 383 384 toggleThrobber(false); 385 if (!isShowing) { 386 if (forceDelayedShow) { 387 // Must force delayed show since dialog may be hiding, and confirmation dialog does not 388 // support this. 389 setTimeout(function () { 390 offlineDialog.show(0); 391 }, 100); 392 } 393 else { 394 offlineDialog.show(0); 395 } 396 } 397 isShowing = true; 398 startTime = new Date().getTime(); 399 incrementRetryInterval(); 400 clearInterval(currentInterval); 401 currentInterval = setInterval(updateCountDown, 100); 402 }; 403 404 /** 405 * Updates the count down timer. Retries requests when time expires. 406 */ 407 const updateCountDown = function () { 408 const time = new Date().getTime(); 409 const timeElapsed = Math.floor((time - startTime) / 1000); 410 const timeLeft = retryIntervals[intervalIndex] - timeElapsed; 411 countDownNum.textContent = timeLeft.toString(); 412 413 // Retry interval reached, retry requests 414 if (timeLeft <= 0) { 415 retryRequests(); 416 } 417 }; 418 419 /** 420 * Add request to offline request queue. Only supports posts for now. 421 * 422 * @param {string} url The request url 423 * @param {Object} data The request data 424 */ 425 this.add = function (url, data) { 426 // Only queue request if it failed because we are offline 427 if (window.navigator.onLine) { 428 return false;
429 } 430 431 requestQueue.add(url, data); 432 }; 433 }; 434 435 return offlineRequestQueue; 436})(H5P.RequestQueue, H5P.ConfirmationDialog);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.