| /* |
| Copyright (c) 2004-2006, The Dojo Foundation |
| All Rights Reserved. |
| |
| Licensed under the Academic Free License version 2.1 or above OR the |
| modified BSD license. For more information on Dojo licensing, see: |
| |
| http://dojotoolkit.org/community/licensing.shtml |
| */ |
| |
| dojo.provide("dojo.io.ScriptSrcIO"); |
| dojo.require("dojo.io.BrowserIO"); |
| dojo.require("dojo.undo.browser"); |
| |
| //FIXME: should constantParams be JS object? |
| //FIXME: check dojo.io calls. Can we move the BrowserIO defined calls somewhere |
| // else so that we don't depend on BrowserIO at all? The dependent calls |
| // have to do with dealing with forms and making query params from JS object. |
| /** |
| * See test_ScriptSrcIO.html for usage information. |
| * Notes: |
| * - The watchInFlight timer is set to 100 ms instead of 10ms (which is what BrowserIO.js uses). |
| */ |
| dojo.io.ScriptSrcTransport = new function(){ |
| this.preventCache = false; // if this is true, we'll always force GET requests to not cache |
| this.maxUrlLength = 1000; //Used to calculate if script request should be multipart. |
| this.inFlightTimer = null; |
| |
| this.DsrStatusCodes = { |
| Continue: 100, |
| Ok: 200, |
| Error: 500 |
| }; |
| |
| this.startWatchingInFlight = function(){ |
| //summary: Internal method to start the process of watching for in-flight requests. |
| if(!this.inFlightTimer){ |
| this.inFlightTimer = setInterval("dojo.io.ScriptSrcTransport.watchInFlight();", 100); |
| } |
| } |
| |
| this.watchInFlight = function(){ |
| //summary: Internal method to watch for in-flight requests. |
| var totalCount = 0; |
| var doneCount = 0; |
| for(var param in this._state){ |
| totalCount++; |
| var currentState = this._state[param]; |
| if(currentState.isDone){ |
| doneCount++; |
| delete this._state[param]; |
| }else if(!currentState.isFinishing){ |
| var listener = currentState.kwArgs; |
| try{ |
| if(currentState.checkString && eval("typeof(" + currentState.checkString + ") != 'undefined'")){ |
| currentState.isFinishing = true; |
| this._finish(currentState, "load"); |
| doneCount++; |
| delete this._state[param]; |
| }else if(listener.timeoutSeconds && listener.timeout){ |
| if(currentState.startTime + (listener.timeoutSeconds * 1000) < (new Date()).getTime()){ |
| currentState.isFinishing = true; |
| this._finish(currentState, "timeout"); |
| doneCount++; |
| delete this._state[param]; |
| } |
| }else if(!listener.timeoutSeconds){ |
| //Increment the done count if no timeout is specified, so |
| //that we turn off the timer if all that is left in the state |
| //list are things we can't clean up because they fail without |
| //getting a callback. |
| doneCount++; |
| } |
| }catch(e){ |
| currentState.isFinishing = true; |
| this._finish(currentState, "error", {status: this.DsrStatusCodes.Error, response: e}); |
| } |
| } |
| } |
| |
| if(doneCount >= totalCount){ |
| clearInterval(this.inFlightTimer); |
| this.inFlightTimer = null; |
| } |
| } |
| |
| this.canHandle = function(/*dojo.io.Request*/kwArgs){ |
| //summary: Tells dojo.io.bind() if this is a good transport to |
| //use for the particular type of request. This type of transport can only |
| //handle responses that are JavaScript or JSON that is passed to a JavaScript |
| //callback. It can only do asynchronous binds, is limited to GET HTTP method |
| //requests, and cannot handle formNodes. However, it has the advantage of being |
| //able to do cross-domain requests. |
| |
| return dojo.lang.inArray(["text/javascript", "text/json", "application/json"], (kwArgs["mimetype"].toLowerCase())) |
| && (kwArgs["method"].toLowerCase() == "get") |
| && !(kwArgs["formNode"] && dojo.io.formHasFile(kwArgs["formNode"])) |
| && (!kwArgs["sync"] || kwArgs["sync"] == false) |
| && !kwArgs["file"] |
| && !kwArgs["multipart"]; |
| } |
| |
| this.removeScripts = function(){ |
| //summary: Removes any script tags from the DOM that may have been added by ScriptSrcTransport. |
| //description: 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 (and probably 5.x), if you remove the script element while |
| //part of the response script is still executing, the browser might crash. |
| var scripts = document.getElementsByTagName("script"); |
| for(var i = 0; scripts && i < scripts.length; i++){ |
| var scriptTag = scripts[i]; |
| if(scriptTag.className == "ScriptSrcTransport"){ |
| var parent = scriptTag.parentNode; |
| parent.removeChild(scriptTag); |
| i--; //Set the index back one since we removed an item. |
| } |
| } |
| } |
| |
| this.bind = function(/*dojo.io.Request*/kwArgs){ |
| //summary: function that sends the request to the server. |
| //description: See the Dojo Book page on this transport for a full |
| //description of supported kwArgs properties and usage: |
| //http://manual.dojotoolkit.org/WikiHome/DojoDotBook/Book25 |
| |
| //START duplication from BrowserIO.js (some changes made) |
| var url = kwArgs.url; |
| var query = ""; |
| |
| if(kwArgs["formNode"]){ |
| var ta = kwArgs.formNode.getAttribute("action"); |
| if((ta)&&(!kwArgs["url"])){ url = ta; } |
| var tp = kwArgs.formNode.getAttribute("method"); |
| if((tp)&&(!kwArgs["method"])){ kwArgs.method = tp; } |
| query += dojo.io.encodeForm(kwArgs.formNode, kwArgs.encoding, kwArgs["formFilter"]); |
| } |
| |
| if(url.indexOf("#") > -1) { |
| dojo.debug("Warning: dojo.io.bind: stripping hash values from url:", url); |
| url = url.split("#")[0]; |
| } |
| |
| //Break off the domain/path of the URL. |
| var urlParts = url.split("?"); |
| if(urlParts && urlParts.length == 2){ |
| url = urlParts[0]; |
| query += (query ? "&" : "") + urlParts[1]; |
| } |
| |
| if(kwArgs["backButton"] || kwArgs["back"] || kwArgs["changeUrl"]){ |
| dojo.undo.browser.addToHistory(kwArgs); |
| } |
| |
| //Create an ID for the request. |
| var id = kwArgs["apiId"] ? kwArgs["apiId"] : "id" + this._counter++; |
| |
| //Fill out any other content pieces. |
| var content = kwArgs["content"]; |
| var jsonpName = kwArgs.jsonParamName; |
| if(kwArgs.sendTransport || jsonpName) { |
| if (!content){ |
| content = {}; |
| } |
| if(kwArgs.sendTransport){ |
| content["dojo.transport"] = "scriptsrc"; |
| } |
| |
| if(jsonpName){ |
| content[jsonpName] = "dojo.io.ScriptSrcTransport._state." + id + ".jsonpCall"; |
| } |
| } |
| |
| if(kwArgs.postContent){ |
| query = kwArgs.postContent; |
| }else if(content){ |
| query += ((query) ? "&" : "") + dojo.io.argsFromMap(content, kwArgs.encoding, jsonpName); |
| } |
| //END duplication from BrowserIO.js |
| |
| //START DSR |
| |
| //If an apiId is specified, then we want to make sure useRequestId is true. |
| if(kwArgs["apiId"]){ |
| kwArgs["useRequestId"] = true; |
| } |
| |
| //Set up the state for this request. |
| var state = { |
| "id": id, |
| "idParam": "_dsrid=" + id, |
| "url": url, |
| "query": query, |
| "kwArgs": kwArgs, |
| "startTime": (new Date()).getTime(), |
| "isFinishing": false |
| }; |
| |
| if(!url){ |
| //Error. An URL is needed. |
| this._finish(state, "error", {status: this.DsrStatusCodes.Error, statusText: "url.none"}); |
| return; |
| } |
| |
| //If this is a jsonp request, intercept the jsonp callback |
| if(content && content[jsonpName]){ |
| state.jsonp = content[jsonpName]; |
| state.jsonpCall = function(data){ |
| if(data["Error"]||data["error"]){ |
| if(dojo["json"] && dojo["json"]["serialize"]){ |
| dojo.debug(dojo.json.serialize(data)); |
| } |
| dojo.io.ScriptSrcTransport._finish(this, "error", data); |
| }else{ |
| dojo.io.ScriptSrcTransport._finish(this, "load", data); |
| } |
| }; |
| } |
| |
| //Only store the request state on the state tracking object if a callback |
| //is expected or if polling on a checkString will be done. |
| if(kwArgs["useRequestId"] || kwArgs["checkString"] || state["jsonp"]){ |
| this._state[id] = state; |
| } |
| |
| //A checkstring is a string that if evaled will not be undefined once the |
| //script src loads. Used as an alternative to depending on a callback from |
| //the script file. If this is set, then multipart is not assumed to be used, |
| //since multipart requires a specific callback. With checkString we will be doing |
| //polling. |
| if(kwArgs["checkString"]){ |
| state.checkString = kwArgs["checkString"]; |
| } |
| |
| //Constant params are parameters that should always be sent with each |
| //part of a multipart URL. |
| state.constantParams = (kwArgs["constantParams"] == null ? "" : kwArgs["constantParams"]); |
| |
| if(kwArgs["preventCache"] || |
| (this.preventCache == true && kwArgs["preventCache"] != false)){ |
| state.nocacheParam = "dojo.preventCache=" + new Date().valueOf(); |
| }else{ |
| state.nocacheParam = ""; |
| } |
| |
| //Get total length URL, if we were to do it as one URL. |
| //Add some padding, extra & separators. |
| var urlLength = state.url.length + state.query.length + state.constantParams.length |
| + state.nocacheParam.length + this._extraPaddingLength; |
| |
| if(kwArgs["useRequestId"]){ |
| urlLength += state.idParam.length; |
| } |
| |
| if(!kwArgs["checkString"] && kwArgs["useRequestId"] |
| && !state["jsonp"] && !kwArgs["forceSingleRequest"] |
| && urlLength > this.maxUrlLength){ |
| if(url > this.maxUrlLength){ |
| //Error. The URL domain and path are too long. We can't |
| //segment that, so return an error. |
| this._finish(state, "error", {status: this.DsrStatusCodes.Error, statusText: "url.tooBig"}); |
| return; |
| }else{ |
| //Start the multiple requests. |
| this._multiAttach(state, 1); |
| } |
| }else{ |
| //Send one URL. |
| var queryParams = [state.constantParams, state.nocacheParam, state.query]; |
| if(kwArgs["useRequestId"] && !state["jsonp"]){ |
| queryParams.unshift(state.idParam); |
| } |
| var finalUrl = this._buildUrl(state.url, queryParams); |
| |
| //Track the final URL in case we need to use that instead of api ID when receiving |
| //the load callback. |
| state.finalUrl = finalUrl; |
| |
| this._attach(state.id, finalUrl); |
| } |
| //END DSR |
| |
| this.startWatchingInFlight(); |
| } |
| |
| //Private properties/methods |
| this._counter = 1; |
| this._state = {}; |
| this._extraPaddingLength = 16; |
| |
| //Is there a dojo function for this already? |
| this._buildUrl = function(url, nameValueArray){ |
| var finalUrl = url; |
| var joiner = "?"; |
| for(var i = 0; i < nameValueArray.length; i++){ |
| if(nameValueArray[i]){ |
| finalUrl += joiner + nameValueArray[i]; |
| joiner = "&"; |
| } |
| } |
| |
| return finalUrl; |
| } |
| |
| this._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.className = "ScriptSrcTransport"; |
| document.getElementsByTagName("head")[0].appendChild(element); |
| } |
| |
| this._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){ |
| this._finish(state, "error", {status: this.DsrStatusCodes.Error, 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 = this.maxUrlLength - state.idParam.length |
| - state.constantParams.length - state.url.length |
| - state.nocacheParam.length - this._extraPaddingLength; |
| |
| //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 queryParams = [currentQuery, state.idParam, state.constantParams, state.nocacheParam]; |
| if(!isDone){ |
| queryParams.push("_part=" + part); |
| } |
| |
| var url = this._buildUrl(state.url, queryParams); |
| |
| this._attach(state.id + "_" + part, url); |
| } |
| |
| this._finish = function(state, callback, event){ |
| if(callback != "partOk" && !state.kwArgs[callback] && !state.kwArgs["handle"]){ |
| //Ignore "partOk" because that is an internal callback. |
| if(callback == "error"){ |
| state.isDone = true; |
| throw event; |
| } |
| }else{ |
| switch(callback){ |
| case "load": |
| var response = event ? event.response : null; |
| if(!response){ |
| response = event; |
| } |
| state.kwArgs[(typeof state.kwArgs.load == "function") ? "load" : "handle"]("load", response, event, state.kwArgs); |
| state.isDone = true; |
| break; |
| case "partOk": |
| var part = parseInt(event.response.part, 10) + 1; |
| //Update the constant params, if any. |
| if(event.response.constantParams){ |
| state.constantParams = event.response.constantParams; |
| } |
| this._multiAttach(state, part); |
| state.isDone = false; |
| break; |
| case "error": |
| state.kwArgs[(typeof state.kwArgs.error == "function") ? "error" : "handle"]("error", event.response, event, state.kwArgs); |
| state.isDone = true; |
| break; |
| default: |
| state.kwArgs[(typeof state.kwArgs[callback] == "function") ? callback : "handle"](callback, event, event, state.kwArgs); |
| state.isDone = true; |
| } |
| } |
| } |
| |
| dojo.io.transports.addTransport("ScriptSrcTransport"); |
| } |
| |
| //Define callback handler. |
| window.onscriptload = function(event){ |
| var state = null; |
| var transport = dojo.io.ScriptSrcTransport; |
| |
| //Find the matching state object for event ID. |
| if(transport._state[event.id]){ |
| state = transport._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 transport._state){ |
| tempState = transport._state[param]; |
| if(tempState.finalUrl && tempState.finalUrl == 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") == "ScriptSrcTransport" |
| && scriptTag.src == event.id){ |
| state = transport._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 = "error"; |
| switch(event.status){ |
| case dojo.io.ScriptSrcTransport.DsrStatusCodes.Continue: |
| //A part of a multipart request. |
| callbackName = "partOk"; |
| break; |
| case dojo.io.ScriptSrcTransport.DsrStatusCodes.Ok: |
| //Successful reponse. |
| callbackName = "load"; |
| break; |
| } |
| |
| transport._finish(state, callbackName, event); |
| }; |