DownloadBuilder.js | |
---|---|
/* DownloadBuilder.js - v0.4.0 - 2012-08-15
* http://www.gregfranko.com/downloadBuilder.js/
* Copyright (c) 2012 Greg Franko; Licensed MIT */
| |
Immediately-Invoked Function Expression (IIFE) Ben Alman Blog Post that calls another IIFE that contains all of the plugin logic. I use this pattern so that anyone viewing this code does not have to scroll to the bottom of the page to view the local parameters that are passed into the main IIFE. |
(function (downloadBuilder) {
|
ECMAScript 5 Strict Mode: John Resig Blog Post |
"use strict";
|
Calls the second IIFE and locally passes in the global window, and document objects |
downloadBuilder(window, document);
}
|
Locally passes in the |
(function (window, document, undefined) {
|
ECMAScript 5 Strict Mode: John Resig Blog Post |
"use strict";
|
Function object constructor |
var DownloadBuilder = function(obj) {
|
Sets the library options |
this.options = {
|
Location option (Defaults to an local) |
"location": (obj && obj.location) || "local",
|
Github author name option |
"author": (obj && obj.author) || "",
|
Github repo name option |
"repo": (obj && obj.repo) || "",
|
Github branch option |
"branch": (obj && obj.branch) || ""
};
|
Cache option (defaults to an hour (in milliseconds)) |
if(obj && obj.cache !== undefined) {
this.options.cache = +obj.cache;
}
else {
this.options.cache = 3600000;
}
|
Sets up the plugin |
this._create();
};
|
Adds methods to the downloadBuilder prototype object that are shared between all instances |
DownloadBuilder.prototype = {
|
Library Version Number |
VERSION: "0.4.0",
|
Github Rate Limit URL |
githubRateLimitUrl: "https://api.github.com/rate_limit",
|
String that will be used to hold the file content |
file: "",
|
Number of the current checkbox that is being traversed |
currentCheckbox: 0,
|
Checkbox HTML elements |
checkboxes: [],
|
Callback function that is called when the fileURL creation process is complete |
callback: function() {},
|
_Create |
_create: function() {
|
Determines if the current browser supports the HTML5 Filesystem API and sets up some global variables needed for the HTML5 filesystem to work |
this.supportsFilesystem = this.fileSystemSupport();
|
Instance variable (object) that determines if a user is trying to use a Github resource and how many Github resources are being used |
this.github = this._isUsingGithub();
|
If one or more Github resources are trying to be used |
if(this.github.isUsing) {
|
Checks to make sure the user is not over the Github rate limiting capacity and sets an instance property to the number of remaining available Github requests |
this._checkGithubRateLimit();
}
|
Maintains chainability |
return this;
},
|
_fileSystemSetup |
fileSystemSupport: function() {
|
HTML5 Filesystem API Variables |
window.requestFileSystem = window.requestFileSystem || window.webkitRequestFileSystem || window.mozRequestFileSystem;
window.URL = window.URL || window.webkitURL || window.mozURL;
window.storageInfo = window.storageInfo || window.webkitStorageInfo || window.mozStorageInfo;
|
If any of the global HTML5 Filesystem variables are not available |
if(!window.requestFileSystem || !window.Blob || !window.URL || !window.storageInfo) {
|
The HTML5 Filesystem API is not supported |
return false;
}
|
The HTML5 Filesystem API is supported |
return true;
},
|
_isUsingGithub |
_isUsingGithub: function() {
|
Grabs all of the elements on the page with the data-location='github' HTML5 data attribute |
var githubSelector = document.querySelectorAll('[data-location="github"]');
|
If there is one element or more that are trying to use Github resources |
if(githubSelector.length || this.options.location === "github") {
|
Returns an object |
return { "isUsing": true, "length": githubSelector.length };
}
|
If there are no elements that are trying to use Github resources |
else {
|
Returns an object |
return { "isUsing": false, "length": githubSelector.length };
}
},
|
_checkGithubRateLimit |
_checkGithubRateLimit: function() {
|
Saves the Library object context in the this variable |
var self = this;
|
Calls the custom JSONP method (created by Oscar Goodson) |
this.JSONP(this.githubRateLimitUrl, function(data){
|
Sets an instance variable that determines if the Github rate limit has been reached or not |
self.rateLimit = data.data.rate.remaining;
});
|
Maintains chainability |
return this;
},
|
JSONP |
JSONP: function(url,method,callback) {
|
Set the defaults |
url = url || "";
method = method || "";
callback = callback || function(){};
var jsonpScript,
generatedFunction;
|
If no method was set and they used the callback param in place of the method param instead, we say method is callback and set a default method of "callback" |
if(typeof method === "function") {
callback = method;
method = "callback";
}
|
This randomizes a function name for generated script tag at the bottom to call example: jsonp958653 |
generatedFunction = "jsonp" + Math.round(Math.random()*1000001);
|
Generate the temp JSONP function using the name above First, call the function the user defined in the callback param [callback(json)] Then delete the generated function from the window [delete window[generatedFunction]] |
window[generatedFunction] = function(json){
callback(json);
|
A try/catch block is used because IE8 does not support deleting global window properties |
try {
delete window[generatedFunction];
}
catch (e) {
window[generatedFunction] = undefined;
}
};
|
Check if the user set their own params, and if not add a ? to start a list of params If in fact they did we add a & to add onto the params example1: url = http://url.com THEN http://url.com?callback=X example2: url = http://url.com?example=param THEN http://url.com?example=param&callback=X |
if(url.indexOf("?") === -1) {
url = url+"?";
}
else {
url = url+"&";
}
|
This generates the script tag |
jsonpScript = document.createElement("script");
jsonpScript.setAttribute("src", url + method + "=" + generatedFunction);
document.getElementsByTagName("head")[0].appendChild(jsonpScript);
},
|
buildURL |
buildURL: function(checkboxes, fileName, lang, callback) {
|
If there is at least one checkbox element |
if(checkboxes.length) {
|
LOCAL VARIABLES |
var self = this,
time = new Date().getTime(),
currentElem,
location,
repoAuthor,
repoName,
repoBranch,
localPath,
x,
lastElem;
|
If the callback parameter is a function |
if(typeof callback === "function") {
|
Saves the callback function as an object instance property |
self.callback = callback;
}
if(self.currentCheckbox === 0 && window.sessionStorage && !window.sessionStorage.getItem("cache-time")) {
window.sessionStorage.setItem("cache-time", time);
}
|
Saves the checkbox elements as an object instance property |
self.checkboxes = checkboxes;
|
Determines if the currently traversed checkbox element is the last checkbox element in the list |
lastElem = (self.currentCheckbox === checkboxes.length -1) || false;
|
Stores the currently traversed checkbox element in a variable |
currentElem = checkboxes[self.currentCheckbox];
|
Stores the HTML5 data attribute, data-location, inside of a variable (Defaulting to local) |
location = currentElem.getAttribute("data-location") || self.options.location || "";
|
Stores the HTML5 data attribute, data-author, inside of a variable (Defaulting to an empty string) |
repoAuthor = currentElem.getAttribute("data-author") || self.options.author || "";
|
Stores the HTML5 data attribute, data-repo, inside of a variable (Defaulting to an empty string) |
repoName = currentElem.getAttribute("data-repo") || self.options.repo || "";
|
Stores the HTML5 data attribute, data-branch, inside of a variable (Defaulting to an empty string) |
repoBranch = currentElem.getAttribute("data-branch") || self.options.branch || "";
|
Stores the HTML5 data attribute, data-localpath, inside of a variable (Defaulting to an empty string) |
localPath = currentElem.getAttribute("data-localpath") || "";
|
If the current browser supports Session Storage |
if(window.sessionStorage) {
|
If there are already items stored in the session |
if(window.sessionStorage.getItem(currentElem.value) && window.sessionStorage.getItem("cache-time")) {
|
If the caching time has expired |
if((time - window.sessionStorage.getItem("cache-time")) >= self.options.cache) {
|
Removes the session data |
window.sessionStorage.removeItem(currentElem.value);
|
Removes the session data |
window.sessionStorage.removeItem("cache-time");
|
Requests new data |
self._sendRequest({ "location": location, "repoAuthor": repoAuthor, "repoName": repoName, "repoBranch": repoBranch, "localPath": localPath, "currentElem": currentElem, "time": time, "lastElem": lastElem, "fileName": fileName, "lang": lang });
}
|
If the caching time has not expired |
else {
|
Appends to the file instance property with whatever text is already in the session |
self.file += window.sessionStorage.getItem(currentElem.value);
|
Checks to the see if the file URL is ready to be created |
self._fileStatus({ "lastElem": lastElem, "lang": lang, "fileName": fileName });
}
}
|
If there are no items in the session |
else {
|
Requests new data |
self._sendRequest({ "location": location, "repoAuthor": repoAuthor, "repoName": repoName, "repoBranch": repoBranch, "localPath": localPath, "currentElem": currentElem, "time": time, "lastElem": lastElem, "fileName": fileName, "lang": lang });
}
}
}
else {
|
Calls the callback function that was ininitially passed into the buildURL method |
callback.call(window, { "content": "", "fileName": fileName, "lang": lang });
}
|
Maintains chainability |
return this;
},
|
_localRequest |
_localRequest: function(obj) {
|
LOCAL VARIABLES |
var self = this,
xhr,
text;
|
Ajax Request wrapped in a try/catch block in case of an error |
try {
|
Constructs a new XMLHttpRequest object instance |
xhr = new XMLHttpRequest();
|
Creates a GET request with the relative url that is passed |
xhr.open("GET", obj.url, true);
|
Ajax event handler to check if the request is finished |
xhr.onreadystatechange = function(e) {
|
If the Ajax request is complete and it is successful |
if (this.readyState === 4 && this.status === 200) {
|
Save the text response in a local variable |
text = this.responseText || this.response || "";
|
Append the text response to the file instance property |
self.file += text;
|
Sets necessary session data |
window.sessionStorage.setItem(obj.elem.value, text || "");
|
Checks to the see if the file URL should be created yet |
self._fileStatus({ "lastElem": obj.lastElem, "lang": obj.lang, "fileName": obj.fileName });
}
};
|
Sends the Ajax request |
xhr.send();
} catch(error) {
}
|
Maintains chainability |
return this;
},
|
_thirdPartyRequest |
_thirdPartyRequest: function(obj) {
|
Stores the local object context in the self local variable |
var self = this,
text;
|
Calls the custom JSONP method (created by Oscar Goodson) |
this.JSONP(obj.url, function(data) {
|
If the request is a Github request |
if(obj.type === "github") {
|
Appends the parsed Github text response to the file instance property |
text = self._parseGithubResponse({ "elem": obj.elem, "data": data }) || "";
self.file += text;
|
Stores all the necessary data inside of the session |
window.sessionStorage.setItem(obj.elem.value, text || "");
|
Checks to the see if the file URL is ready to be created |
self._fileStatus({ "lastElem": obj.lastElem, "lang": obj.lang, "fileName": obj.fileName });
}
});
},
|
_sendRequest |
_sendRequest: function(obj) {
|
If the request is a Github request |
if(obj.location === "github") {
|
If the Github rate limit has not been met |
if(this.rateLimit !== 0) {
|
Decreases the rate limit capacity amount |
this.rateLimit -= 1;
|
Sends a third party file request |
this._thirdPartyRequest({ "type": obj.location, "url": "https://api.github.com/repos/" + obj.repoAuthor + "/" + obj.repoName + "/contents/?ref=" + obj.repoBranch + "&path=" + obj.currentElem.value, "elem": obj.currentElem, "time": obj.time, "lastElem": obj.lastElem, "lang": obj.lang, "fileName": obj.fileName });
}
|
If the Github rate limit has been met |
else {
|
If a backup local path is provided |
if(obj.localPath) {
|
Sends a local file request |
this._localRequest({ "url": obj.localpath, "elem": obj.currentElem, "time": obj.time, "lastElem": obj.lastElem, "lang": obj.lang, "fileName": obj.fileName });
}
}
}
|
If the request is not a Github request |
else {
|
Sends a local file request |
this._localRequest({ "url": obj.currentElem.value, "elem": obj.currentElem, "time": obj.time, "lastElem": obj.lastElem, "lang": obj.lang, "fileName": obj.fileName });
}
},
|
_parseGithubResponse |
_parseGithubResponse: function(obj) {
|
LOCAL VARIABLES |
var self = this,
base64EncodedContent,
content,
contentArray,
text;
|
If Github found content and the global |
if (obj.data.data.content !== undefined && window.atob) {
|
If the encoding is base64 |
if (obj.data.data.encoding === "base64") {
|
Stores the content property inside of the local variable |
base64EncodedContent = obj.data.data.content;
|
Removes all new lines |
base64EncodedContent = base64EncodedContent.replace(/\n/g, "");
|
Stores the base64 encoded content inside of a local variable |
content = window.atob(base64EncodedContent);
|
Splits the content up into an array |
contentArray = content.split("\n");
|
Grabs all of the content from the array and stores the text in a local variable |
text = contentArray.slice(0, contentArray.length).join("\n") || "";
}
}
|
Maintains chainability |
return text;
},
|
createURL |
createURL: function(obj) {
|
LOCAL VARIABLES |
var self = this,
blob;
|
Request temporary storage data (even though we are not creating any files this step is necessary) |
window.storageInfo.requestQuota(window.TEMPORARY, 1024*1024, function(grantedBytes) {
|
The callback function that uses the granted bytes requested in the previous callback |
window.requestFileSystem(self.storageType, grantedBytes, function(fs) {
|
Creates a file (the name of the file is passed into the createURL method) |
fs.root.getFile(obj.fileName, { create: true }, function(fileEntry) {
|
Create a FileWriter object for our FileEntry |
fileEntry.createWriter(function(fileWriter) {
|
Create a new Blob |
blob = new window.Blob([obj.data], { type: "text/" + obj.lang });
|
Calls the callback function that was passed in, and constructs then passes the url that references the recently created blob |
obj.callback.call(window, window.URL.createObjectURL(blob));
});
});
});
});
|
Maintains chainability |
return this;
},
|
_fileStatus |
_fileStatus: function(obj) {
|
LOCAL VARIABLES |
var self = this,
url;
|
If the last checkbox is being process |
if(obj.lastElem) {
|
Resets the currently traversed checkbox back to zero |
this.currentCheckbox = 0;
|
If the current browser supports the HTML5 Filesystem API |
if(self.supportsFilesystem) {
|
Creates the file URL |
self.createURL({ "lang": obj.lang, "fileName": obj.fileName, "data": self.file, "callback": function(url) {
|
Saves the url inside of the session |
window.sessionStorage.setItem("bloburl", url);
|
Calls the callback function that was ininitially passed into the buildURL method |
self.callback.call(window, { "url": url, "content": self.file, "fileName": obj.fileName, "lang": obj.lang });
|
Removes the contents of the file instance property (that was used to create the file URL) |
self.file = "";
}});
}
|
If the current borwser does not support the HTML5 Filesystem API |
else {
|
Calls the callback function that was ininitially passed into the buildURL method |
self.callback.call(window, { "content": self.file, "fileName": obj.fileName, "lang": obj.lang });
|
Removes the contents of the file instance property (that was used to create the file URL) |
self.file = "";
}
}
|
If the last checkbox is not being processed |
else {
|
Increment the currently traversed checkbox number |
this.currentCheckbox += 1;
|
Call the buildURL method again |
self.buildURL(self.checkboxes, obj.fileName, obj.lang, self.callback);
}
|
Maintains chainability |
return this;
}
};
|
Makes the local downloadBuilder object global |
window.DownloadBuilder = DownloadBuilder;
})); // End of Library
|