/** * response-monitor.js * https://github.com/witstep/response-monitor.js * Copyright (c) Jorge Paulo * Licensed under the MIT license */ (function (global, undefined) { 'use strict'; var factory = function (window) { if (typeof window.document !== 'object') { throw new Error('response-monitor.js requires a `window` with a `document` object'); } //the constructor var ResponseMonitor = function (trigger, options){ var self = this; this.trigger = trigger; this.downloadTimer = null; this._validateTrigger(); this._validateURL(); //no options at all, TRY to use spin.js as default spinner if(typeof options === 'undefined') options = this._configDefaultSpinner(options); //dummy function as default for all events var f = function(){}; //default options this.options = { timeout: 120, //seconds to wait for a response onRequest: f, //request to the server made onMonitor: f, //called once a second while checking for the cookie onTimeout: f, //the download didn't start within the time limit onResponse: f, //the wait is over, the response from the server is ready cookiePrefix: 'response-monitor' //should match the prefix on the server }; //use the # in the URI as default timeout if(typeof options.timeout === 'undefined' && typeof this.hash !== 'undefined') options.timeout = parseInt(this.hash.split('#')[1]); //merge user options with default options for (var attrname in options) this.options[attrname] = options[attrname]; //defined here, because IE<=9 doest support passing args in setInterval() this.monitor = function(){ var token = self._getCookie(); if(typeof token !== 'undefined'){ //using closure to support older versions of IE window.setTimeout(function(){self.options.onResponse(token);}, 0); self._terminate(); }else if (self.countdown < 0){ self._terminate(); window.setTimeout(self.options.onTimeout, 0); return; } self.options.onMonitor(self.countdown--); }; }; ResponseMonitor.prototype._configDefaultSpinner = function(options){ var self = this; try{ this.spinner = new Spinner(); }catch(e){ var SpinJS = require('spin'); this.spinner = new SpinJS(); } var onTerminate = function(status){ self.spinner.stop(); if(status===0) console.log('error'); }; return { onRequest: function(){ self.spinner.spin(); if(typeof self.trigger.nodeName !== 'undefined')//form or anchor self.trigger.appendChild(self.spinner.el); else//url document.body.appendChild(self.spinner.el); }, onResponse: onTerminate, onTimeout: function(){onTerminate(); console.log('timeout');}, }; }; ResponseMonitor.prototype.execute = function(event) { if(this.downloadTimer) return;//already running... if( typeof this.trigger.href !== 'undefined' ) this.trigger.href="javascript:void(0);";//prevent navigation away from the page during the request this.countdown=this.options.timeout; if( typeof event !== 'undefined' ) event.preventDefault(); //we are overriding form.submit and a.click /*the cookie name is not constant to allow simultaneous monitoring of multiple requests*/ var date = new Date(); var token = date.getTime(); this.cookie = { name: this.options.cookiePrefix+'_'+token, value: token }; this._createIframe(); //check whether the query string already has parameters or not var separator = this.url.indexOf('?') !== -1 ? "&" : "?"; //note that the complete cookie name is not passed to the server, only the prefix var url = this.url+separator+this.options.cookiePrefix+'='+token; if(typeof this.form !== 'undefined') this._submitForm(url); else this.iframe.src = url; this.options.onRequest(this.cookie.name); this.monitor();//check immediately this.downloadTimer = window.setInterval(this.monitor, 1000); }; ResponseMonitor.prototype._terminate = function () { //restore the link, removed to prevent navigation if( typeof this.trigger.href !== 'undefined' ) this.trigger.href = this.url + this.hash; //remove the hidden form field from forms with GET method if( typeof this.form !== 'undefined' ){ if(this.form.ACTION == 'GET') this.form.removeChild(this.hiddenInput); } window.clearInterval(this.downloadTimer); this.downloadTimer = null; this._expireCookie(this.cookie.name); this._removeIframe(); }; ResponseMonitor.prototype._getCookie = function() { var parts = document.cookie.split(this.cookie.name + "="); if (parts.length == 2) return parts.pop().split(";").shift(); }; ResponseMonitor.prototype._expireCookie = function(name) { document.cookie = encodeURIComponent(name) + "=deleted; expires=" + new Date(0).toUTCString(); }; ResponseMonitor.prototype._submitForm = function(url){ this.form.target=this.iframe.name; if(this.form.method.toUpperCase() == 'GET' && document.getElementById(this.options.cookiePrefix)===null ){//GET this.hiddenInput = document.createElement("input"); this.hiddenInput.type = "hidden"; this.hiddenInput.name = this.options.cookiePrefix; this.hiddenInput.value= this.cookie.value; this.form.appendChild(this.hiddenInput); this.form.submit(); }else{//POST this.form.action = url; this.form.submit(); } }; ResponseMonitor.prototype._createIframe = function (){ this.iframe = document.createElement('iframe'); this.iframe.id = this.options.cookiePrefix+'_iframe_'+this.cookie.value; this.iframe.name = this.iframe.id; this.iframe.style.display = 'none'; document.body.appendChild(this.iframe); }; ResponseMonitor.prototype._removeIframe = function(){ if(this.countdown < 0){//stop the request if it is a timeout try{ if (navigator.appName == 'Microsoft Internet Explorer') this.iframe.contentWindow.document.execCommand('Stop'); else// this.iframe.contentWindow.stop(); }catch(e){ //not possible to stop the request to different (sub-) domains } } document.body.removeChild(this.iframe); }; ResponseMonitor.prototype._invalidTriggerError = function(){ return new Error("Invalid request trigger: '"+this.trigger+"'\nValid request triggers are HTML anchors, forms and URLs as strings."); }; ResponseMonitor.prototype._validateTrigger = function(){ var self = this; if(typeof this.trigger === 'undefined' || this.trigger === null){ throw this._invalidTriggerError(); }else if(typeof this.trigger.href !=='undefined' && this.trigger.nodeName == 'A'){ this.trigger.onclick = function(event){self.execute(event);}; this.url = this.trigger.href; }else if(typeof this.trigger.action !=='undefined' && this.trigger.nodeName == 'FORM'){ this.trigger.onsubmit = function(event){self.execute(event);}; this.form = this.trigger; this.url = this.trigger.action; //HTML form }else if(typeof this.trigger.nodeName === 'undefined'){ this.url = this.trigger; //assuming URL string as last resort; }else{ throw this._invalidTriggerError(); } }; ResponseMonitor.prototype._validateURL = function(){ var parser = document.createElement('a'); parser.href = this.url; if( parser.hostname !== '' && parser.protocol.indexOf('http') !== -1 ) { this.url = parser.href.replace(parser.hash,'');//detach hash from url this.hash = parser.hash; return true;//looks like it can be resolved to a valid HTTP(S) url } return false; }; ResponseMonitor.register = function(trigger, options){ var elementArray; //check if it is a single element or a collection if (typeof trigger.nodeName !== 'undefined') elementArray = new Array(trigger); else elementArray = trigger; for(var i=0; i < elementArray.length; i++){ if(typeof elementArray[i].nodeName === 'undefined') throw new Error('Only HTML elements can be used as triggers'); //not garbage collected because it will be referenced by a.onclick or form.onsubmit new ResponseMonitor(elementArray[i], options); } }; ResponseMonitor.prototype.setTimeout = function(timeout){ this.options.timeout = timeout; }; ResponseMonitor.prototype.setCookiePrefix = function(cookiePrefix){ this.options.cookiePrefix = cookiePrefix; }; return ResponseMonitor; }; // AMD and CommonJS support pattern borrowed from https://github.com/ScottHamper/Cookies var responseMonitorExport = typeof global.document === 'object' ? factory(global) : factory; // AMD support if (typeof define === 'function' && define.amd) { define(function () { return responseMonitorExport; }); // CommonJS/Node.js support } else if (typeof exports === 'object') { // Support Node.js specific `module.exports` (which can be a function) if (typeof module === 'object' && typeof module.exports === 'object') { exports = module.exports = responseMonitorExport; } // But always support CommonJS module 1.1.1 spec (`exports` cannot be a function) exports.ResponseMonitor = responseMonitorExport; } else { global.ResponseMonitor = responseMonitorExport; } })(typeof window === 'undefined' ? this : window);