/* License: GNU Lesser General Public License (http://www.gnu.org/licenses/lgpl.html) Copyright (C) 2005 tagnetic.org. Please do not remove this copyright/license comment. */ //************************************************************************* // Start Dsr useDsr //************************************************************************* /** * The Dsr class allows you to fetch data from a server using a SCRIPT tag. * Notes: * - Only HTTP GET requests to the server are possible. * - Be aware of the limitations on the size of a GET request. It seems like 1KB is * a safe bet. * - The data from the server must be returned as script. HTML/XML should not be the result. * - There is no inherent, cross-browser way to know when the data has been loaded. More * information below. * * How to know when the data has been loaded * ========================================= * 1) Have the server call a javascript method in your page to tell it when it is loaded. * This call should be made at the end of the response from the server. This is the * preferred solution, if you own the data source and the UI framework. * 2) Use a timer to periodically check for a data member that is part of the response. * The Dsr class below allows you to specify a test to use as part of a timer * test. This solution can be used when you don't own the data source. * * Credits: * ======== * The main mechanism of attaching the script to the DOM was inspired by: * http://cssing.blogspot.com/2004/08/including-script-files-from-script.html */ Dsr = { //******************************************************************* /** * Adds a script tag to the page, but waits for the included script to * call back a specified handler (window.onscriptload, defined in this file) * to indicate the script has finished loading. * This method also supports segmenting long URLs into a multipart script request. * * Multipart script requests mean that Dsr will segment the * server requests into multiple requests if the URL is bigger than about * 1 KB of data. Multipart requests require the server to call window.onscriptload with a status of * 100 in order for this library to work. * * This method will take care of determining if an URL will be segmented and add the appropriate URL * parameters for you. However, you will need to know about the parameters if you are a server * developer that is receiving such requests. * * Please see the Dynamic Server Request API at: * * http://tagneto.org/how/reference/js/DynamicScriptRequest.html * * NOTE: Are there charset issues between origin page and server data feed? * NOTE: This method does account for fragment identifiers (#identifier on an URL). * Don't use it if you have fragment identifiers. If there is a great enough * need for fragment identifier support, then it might be added later. * * FUNCTION PARAMETERS: * * @param {String} url: The URL for the script source. * * @param {Object} listener: Optional, but recommended. * An object that contains function callbacks for the different events: * onLoad(), onError(status, statusText, response) and onTimeout(). A timeout in number of * seconds can be specified too. Example listener object: * * var listener = { * onLoad: function() { alert('loaded'); }, * onError: function(status, statusText, response) { alert('status: ' + status + ', error: ' + response); }, * onTimeout: function() { alert('timeout'); }, * timeout: 30 * }; * * @param {String} apiId: Optional. * The server URL's API ID, if it defines one. This only needs to be set if the server does * use one of the following for the onscriptload event.id: * - the _dsrid parameter that Dsr.send adds to the URL * - The URL that is the first parameter of this function * - The URL that is actually used in the SCRIPT SRC attribute * or if the API for the data service specifically mentions an API ID. * * @param {String} constantParams: Optional. * URL parameters that should always be sent up. Particularly useful if * the URL is split across multiple SCRIPT tags. This allows you to maintain * your own state/context for the request, if you don't want to rely on the basic * multipart URL API parameters. Note that the server can override these values. * Format of the data should be the normal URL-encoded form: * * name=urlEncodedValue&name=urlEncodedValue */ send: function(url, listener, apiId, constantParams) { //Make sure we have a good listener if (!listener || !listener.onLoad) { var message = 'Dsr.send requires an onLoad method on the listener'; if (listener && listener.onError) listener.onError(message); else throw message; return; } //Set up the state for this request. var state = Dsr._createState(listener, apiId); state.constantParams = (constantParams == null ? '' : constantParams); //Start the timer, if the caller wants to have a timeout. if (listener.onTimeout) state.intervalId = setInterval("Dsr._checkLoad('" + state.id + "')", 200); //Start constructing URL parameters that will be added. state.idParam = '_dsrid=' + state.id; //Get total length URL, if we were to do it as one URL. //8 is for some padding, extra & separators. var urlLength = url.length + state.constantParams.length + state.idParam.length + 8; if (urlLength > Dsr._kMaxUrlLength) { //Break off the domain/path of the URL. var end = url.indexOf('?'); if (end == -1) { //Error. The URL domain and path are too long. We can't //segment that, so return an error. Dsr._finish(state, 'onError', {status: 500, statusText: 'url.tooBig'}); return; } else { state.href = url.substring(0, end); state.query = url.substring(end + 1, url.length); //Strip off the ? with the + 1. } //Start the multiple requests. Dsr._multiAttach(state, 1); } else { //Send one URL. Does the URL already have query parameters? var paramPrefix = (url.indexOf('?') == -1 ? '?' : '&'); var finalUrl = url + paramPrefix + state.idParam; if (state.constantParams) finalUrl += '&' + state.constantParams; //Save the original URL for later, in case that is the id the server //uses for the event ID. state.url = url; Dsr._attach(state.id, finalUrl); } }, //******************************************************************* /** * Adds a script src to a page, and watches for a string of javascript * that when evaled to something that is not undefined indicates that the * script has finished loading. * * @param {String} url: The URL for the script source. * * @param {String} checkString: Optional, but recommended. * String of javascript that if evals to to true means the script * src has loaded. Required if listener is specified. * * Don't specify this parameter if you are relying on server callbacks * to indicate the script has finished loading (and you don't want to use * send(), which requires certain URL API). * * @param {Object} listener: Optional, but recommended. * An object that contains function callbacks for the different events: * onLoad(), onError(error) and onTimeout(). A timeout in number of * seconds can be specified too. Example listener object: * * var listener = { * onLoad: function() { alert('loaded'); }, * onError: function(status, statusText, response) { alert('status: ' + status + ', error: ' + response); }, * onTimeout: function() { alert('timeout'); }, * timeout: 30 * }; */ sendAndPoll: function(url, checkString, listener) { var state = Dsr._createState(listener); state.check = checkString; //Allow for not setting a checkString. Assuming the server will know what to call //when the script finishes loading. if (checkString) state.intervalId = setInterval("Dsr._checkLoad('" + state.id + "')", 200); Dsr._attach(state.id, url); }, //******************************************************************* /** * Clears any script tags from the DOM that may have been added by Dsr. * Be careful though, by removing them from the script, you may invalidate some * script objects that were defined by the js file that was pulled in as the * src of the script tag. Test carefully if you decide to call this method. * * In MSIE 6, if you removed the script element while part of the script is still executing, * the browser will crash. */ clear: function() { var scripts = document.getElementsByTagName('script'); for (var i = 0; scripts && i < scripts.length; i++) { var scriptTag = scripts[i]; if (scriptTag.getAttribute('class') == 'Dsr') { var parent = scriptTag.parentNode; parent.removeChild(scriptTag); i--; //Set the index back one since we removed an item. } } }, //******************************************************************* //Private properties/methods. _kMaxUrlLength: 1000, //Used to calculate if script request should be multipart. //******************************************************************* _createState: function(listener, apiId) { var id = apiId ? apiId : 'id' + this._counter++; Dsr._state[id] = { id: id, listener: listener, startTime: (new Date()).getTime() }; return Dsr._state[id]; }, //******************************************************************* _attach: function(id, url) { //Attach the script to the DOM. var element = document.createElement('script'); element.type = 'text/javascript'; element.src = url; element.id = id; element.setAttribute('class', 'Dsr'); document.getElementsByTagName('head')[0].appendChild(element); }, //******************************************************************* _multiAttach: function(state, part) { //Check to make sure we still have a query to send up. This is mostly //a protection from a goof on the server side when it sends a part OK //response instead of a final response. if (state.query == null) { Dsr._finish(state, 'onError', {status: 500, statusText: 'query.null'}); return; } if (!state.constantParams) state.constantParams = ''; //How much of the query can we take? //Add a padding constant to account for _part and a couple extra amperstands. //Also add space for id since we'll need it now. var queryMax = Dsr._kMaxUrlLength - state.idParam.length - state.constantParams.length - state.href.length - 16; //Figure out if this is the last part. var isDone = state.query.length < queryMax; //Break up the query string if necessary. var currentQuery; if (isDone) { currentQuery = state.query; state.query = null; } else { //Find the & or = nearest the max url length. var ampEnd = state.query.lastIndexOf('&', queryMax - 1); var eqEnd = state.query.lastIndexOf('=', queryMax - 1); //See if & is closer, or if = is right at the edge, //which means we should put it on the next URL. if (ampEnd > eqEnd || eqEnd == queryMax - 1) { //& is nearer the end. So just chop off from there. currentQuery = state.query.substring(0, ampEnd); state.query = state.query.substring(ampEnd + 1, state.query.length) //strip off amperstand with the + 1. } else { //= is nearer the end. Take the max amount possible. currentQuery = state.query.substring(0, queryMax); //Find the last query name in the currentQuery so we can prepend it to //ampEnd could be -1 (not there), so account for that. var queryName = currentQuery.substring((ampEnd == -1 ? 0 : ampEnd + 1), eqEnd); state.query = queryName + '=' + state.query.substring(queryMax, state.query.length); } } //Now send a part of the script var url = state.href + '?' + currentQuery + '&' + state.idParam; if (state.constantParams) url += '&' + state.constantParams; if (!isDone) url += '&_part=' + part; Dsr._attach(state.id + '_' + part, url); }, //******************************************************************* _checkLoad: function(id) { var state = Dsr._state[id]; var listener = state.listener; try { if (state.check && eval("typeof(" + state.check + ") != 'undefined'")) this._finish(state, 'onLoad'); else if (listener && listener.onTimeout) { if (state.startTime + (listener.timeout * 1000) < (new Date()).getTime()) this._finish(state, 'onTimeout'); } } catch(e) { this._finish(state, 'onError', {status: 500, response: e}); } }, //******************************************************************* _finish: function(state, callback, event) { clearInterval(state.intervalId); //Only attempt to dispatch result if the listener is defined. //Ignore 'onPartOk' because that is an internal callback for Dsr. if (callback != 'onPartOk' && (!state.listener || !state.listener[callback])) { if (callback == 'onError') throw event; } else { switch (callback) { case 'onLoad': var response = event ? event.response : null; state.listener[callback](response); break; case 'onPartOk': var part = parseInt(event.response.part, 10) + 1; //Update the constant params, if any. if (event.response.constantParams) state.constantParams = event.response.constantParams; Dsr._multiAttach(state, part); break; case 'onError': state.listener[callback](event.status, event.statusText, event.response); break; default: state.listener[callback](event); } } }, //******************************************************************* _counter: 1 }; //Private variable used by Dsr to hold state. Dsr._state = new Object(); //****************************************************************** //Define callback handler. window.onscriptload = function(event) { var state = null; //Find the matching state object for event ID. if (Dsr._state[event.id]) state = Dsr._state[event.id]; else { //The ID did not match directly to an entry in the state list. //Try searching the state objects for a matching original URL. var tempState; for (var param in Dsr._state) { tempState = Dsr._state[param]; if (tempState.url && tempState.url == event.id) { state = tempState; break; } } //If no matching original URL is found, then use the URL that was actually used //in the SCRIPT SRC attribute. if (state == null) { var scripts = document.getElementsByTagName('script'); for (var i = 0; scripts && i < scripts.length; i++) { var scriptTag = scripts[i]; if (scriptTag.getAttribute('class') == 'Dsr' && scriptTag.src == event.id) { state = Dsr._state[scriptTag.id]; break; } } } //If state is still null, then throw an error. if (state == null) throw 'No matching state for onscriptload event.id: ' + event.id; } var callbackName = 'onError'; switch (event.status) { case 100: //A part of a multipart request. callbackName = 'onPartOk'; break; case 200: //Successful reponse. callbackName = 'onLoad'; break; } Dsr._finish(state, callbackName, event); }; //************************************************************************* //End Dsr //*************************************************************************