1894 lines
54 KiB
JavaScript
1894 lines
54 KiB
JavaScript
/*
|
|
* Copyright 2009 Matthew Eernisse (mde@fleegix.org)
|
|
* and SitePoint Pty. Ltd, www.sitepoint.com
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS;
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*
|
|
*/
|
|
|
|
if (typeof fleegix == 'undefined') { var fleegix = {}; }
|
|
fleegix.xhr = new function () {
|
|
// Public vars
|
|
// ================================
|
|
// Maximum number of XHR objects to spawn to handle requests
|
|
// Moz/Safari seem to perform significantly better with XHR
|
|
// re-use, IE -- not so much
|
|
this.maxXhrs = 5;
|
|
// Used to increment request IDs -- these may be used for
|
|
// externally tracking or aborting specific requests
|
|
this.lastReqId = 0;
|
|
// Show exceptions for connection failures
|
|
this.debug = false;
|
|
// Default number of seconds before a request times out
|
|
this.defaultTimeoutSeconds = 300;
|
|
// If set to true, use the default err handler for sync requests
|
|
// If false, failures always hand back the whole request object
|
|
this.useDefaultErrHandlerForSync = true;
|
|
// Possible formats for the XHR response
|
|
var _formats = {
|
|
TXT: 'text',
|
|
XML: 'xml',
|
|
OBJ: 'object',
|
|
JSON: 'json'
|
|
};
|
|
this.formats = _formats;
|
|
this.contentTypes = {
|
|
'application/json': _formats.JSON,
|
|
'text/json': _formats.JSON,
|
|
'application/xml': _formats.XML,
|
|
'text/xml': _formats.XML
|
|
};
|
|
|
|
// Public methods
|
|
// ================================
|
|
this.get = function () {
|
|
var o = {};
|
|
var hand = null;
|
|
var args = Array.prototype.slice.apply(arguments);
|
|
if (typeof args[0] == 'function') {
|
|
o.async = true;
|
|
hand = args.shift();
|
|
}
|
|
else {
|
|
o.async = false;
|
|
}
|
|
var url = args.shift();
|
|
// Passing in keyword/obj after URL
|
|
if (typeof args[0] == 'object') {
|
|
var opts = args.shift();
|
|
for (var p in opts) {
|
|
o[p] = opts[p];
|
|
}
|
|
}
|
|
// Normal order-based params of URL, [format]
|
|
else {
|
|
o.format = args.shift() || 'text';
|
|
}
|
|
o.success = hand;
|
|
o.url = url;
|
|
return this.doReq(o);
|
|
};
|
|
this.doGet = function () {
|
|
return this.get.apply(this, arguments);
|
|
};
|
|
this.post = function () {
|
|
var o = {};
|
|
var hand = null;
|
|
var args = Array.prototype.slice.apply(arguments);
|
|
if (typeof args[0] == 'function') {
|
|
o.async = true;
|
|
hand = args.shift();
|
|
}
|
|
else {
|
|
o.async = false;
|
|
}
|
|
var url = args.shift();
|
|
var data = args.shift();
|
|
// Passing in keyword/obj after URL
|
|
if (typeof args[0] == 'object') {
|
|
var opts = args.shift();
|
|
for (var p in opts) {
|
|
o[p] = opts[p];
|
|
}
|
|
}
|
|
// Normal order-based params of URL, [format]
|
|
else {
|
|
o.format = args.shift() || 'text';
|
|
}
|
|
o.success = hand;
|
|
o.url = url;
|
|
o.data = data;
|
|
o.method = 'POST';
|
|
return this.doReq(o);
|
|
};
|
|
this.doPost = function () {
|
|
return this.post.apply(this, arguments);
|
|
};
|
|
this.doReq = function (opts) {
|
|
return this.send(opts);
|
|
};
|
|
this.send = function (o) {
|
|
var opts = o || {};
|
|
var req = new fleegix.xhr.Request();
|
|
var xhrId = null;
|
|
|
|
if (opts.contentType && this.contentTypes[opts.contentType]) {
|
|
opts.format = this.contentTypes[opts.contentType];
|
|
}
|
|
|
|
// Override default request opts with any specified
|
|
for (var p in opts) {
|
|
if (opts.hasOwnProperty(p)) {
|
|
req[p] = opts[p];
|
|
}
|
|
}
|
|
|
|
// HTTP req method all-caps
|
|
req.method = req.method.toUpperCase();
|
|
|
|
req.id = this.lastReqId;
|
|
this.lastReqId++; // Increment req ID
|
|
|
|
// Return request ID or response
|
|
// Async -- handle request or queue it up
|
|
// -------
|
|
if (req.async) {
|
|
// If we have an instantiated XHR we can use, let him handle it
|
|
if (_idleXhrs.length) {
|
|
xhrId = _idleXhrs.shift();
|
|
}
|
|
// No available XHRs -- spawn a new one if we're still
|
|
// below the limit
|
|
else if (_xhrs.length < this.maxXhrs) {
|
|
xhrId = _spawnXhr();
|
|
}
|
|
|
|
// If we have an XHR xhr to handle the request, do it
|
|
// xhrId should be a number (index of XHR obj in _xhrs)
|
|
if (xhrId !== null) {
|
|
_processReq(req, xhrId);
|
|
}
|
|
// No xhr available to handle the request -- queue it up
|
|
else {
|
|
// Uber-requests step to the front of the line, please
|
|
if (req.uber) {
|
|
_requestQueue.unshift(req);
|
|
}
|
|
// Normal queued requests are FIFO
|
|
else {
|
|
_requestQueue.push(req);
|
|
}
|
|
}
|
|
// Return request ID -- may be used for aborting,
|
|
// external tracking, etc.
|
|
return req.id;
|
|
}
|
|
// Sync -- do request inlne and return actual result
|
|
// -------
|
|
else {
|
|
return _processReq(req);
|
|
}
|
|
};
|
|
this.abort = function (reqId) {
|
|
var r = _processingMap[reqId];
|
|
var t = _xhrs[r.xhrId];
|
|
// Abort the req if it's still processing
|
|
if (t) {
|
|
// onreadystatechange can still fire as abort is executed
|
|
t.onreadystatechange = function () { };
|
|
t.abort();
|
|
r.aborted = true;
|
|
_cleanup(r);
|
|
return true;
|
|
}
|
|
else {
|
|
return false;
|
|
}
|
|
};
|
|
// All the goofy normalization and logic to determine
|
|
// what constitutes 'success'
|
|
this.isReqSuccessful = function (obj) {
|
|
var stat = obj.status;
|
|
if (!stat) { return false; }
|
|
// Handle stupid bogus URLMon "Operation Aborted"
|
|
// code in IE for 204 no-content
|
|
if (document.all && stat == 1223) {
|
|
stat = 204;
|
|
}
|
|
if ((stat > 199 && stat < 300) || stat == 304) {
|
|
return true;
|
|
}
|
|
else {
|
|
return false;
|
|
}
|
|
};
|
|
|
|
// Private vars
|
|
// ================================
|
|
var _this = this;
|
|
// Prog ID for specific versions of MSXML -- caches after
|
|
// initial req
|
|
var _msProgId = null;
|
|
// Used in response status test
|
|
var _UNDEFINED_VALUE;
|
|
// Array of XHR obj xhrs, spawned as needed up to
|
|
// maxXhrs ceiling
|
|
var _xhrs = [];
|
|
// Queued-up requests -- appended to when all XHR xhrs
|
|
// are in use -- FIFO list, XHR objs respond to waiting
|
|
// requests immediately as then finish processing the current one
|
|
var _requestQueue = [];
|
|
// List of free XHR objs -- xhrs sit here when not
|
|
// processing requests. If this is empty when a new request comes
|
|
// in, we try to spawn a request -- if we're already at max
|
|
// xhr number, we queue the request
|
|
var _idleXhrs = [];
|
|
// Hash of currently in-flight requests -- each string key is
|
|
// the request id of the request
|
|
// Used to abort processing requests
|
|
var _processingMap = {};
|
|
// Array of in-flight request for the watcher to iterate over
|
|
var _processingArray = [];
|
|
// The single XHR obj used for synchronous requests -- sync
|
|
// requests do not participate in the request pooling
|
|
var _syncXhr = null;
|
|
// The single request obj used for sync requests, same
|
|
// as above
|
|
var _syncRequest = null;
|
|
// The id for the setTimeout used in the the
|
|
// request timeout watcher
|
|
var _processingWatcherId = null;
|
|
|
|
// Private methods
|
|
// ================================
|
|
// The XHR object factory
|
|
var _spawnXhr = function (isSync) {
|
|
var t = [
|
|
'Msxml2.XMLHTTP.6.0',
|
|
'MSXML2.XMLHTTP.3.0',
|
|
'Microsoft.XMLHTTP'
|
|
];
|
|
var xhrObj = null;
|
|
if (window.XMLHttpRequest) {
|
|
xhrObj = new XMLHttpRequest();
|
|
}
|
|
else if (window.ActiveXObject) {
|
|
if (_msProgId) {
|
|
xhrObj = new ActiveXObject(_msProgId);
|
|
}
|
|
else {
|
|
for (var i = 0; i < t.length; i++) {
|
|
try {
|
|
xhrObj = new ActiveXObject(t[i]);
|
|
// Cache the prog ID, break the loop
|
|
_msProgId = t[i]; break;
|
|
}
|
|
catch(e) {}
|
|
}
|
|
}
|
|
}
|
|
// Instantiate XHR obj
|
|
if (xhrObj) {
|
|
if (isSync) { return xhrObj; }
|
|
else {
|
|
_xhrs.push(xhrObj);
|
|
var xhrId = _xhrs.length - 1;
|
|
return xhrId;
|
|
}
|
|
}
|
|
else {
|
|
throw new Error('Could not create XMLHttpRequest object.');
|
|
}
|
|
};
|
|
// This is the workhorse function that actually
|
|
// sets up and makes the XHR request
|
|
var _processReq = function (req, t) {
|
|
var xhrId = null;
|
|
var xhrObj = null;
|
|
var url = '';
|
|
var resp = null;
|
|
|
|
// Async mode -- grab an XHR obj from the pool
|
|
if (req.async) {
|
|
xhrId = t;
|
|
xhrObj = _xhrs[xhrId];
|
|
_processingMap[req.id] = req;
|
|
_processingArray.unshift(req);
|
|
req.xhrId = xhrId;
|
|
}
|
|
// Sync mode -- use single sync XHR
|
|
else {
|
|
if (!_syncXhr) { _syncXhr = _spawnXhr(true); }
|
|
xhrObj = _syncXhr;
|
|
_syncRequest = req;
|
|
}
|
|
|
|
// Defeat the evil power of the IE caching mechanism
|
|
if (req.preventCache) {
|
|
var dt = new Date().getTime();
|
|
url = req.url.indexOf('?') > -1 ? req.url + '&preventCache=' + dt :
|
|
req.url + '?preventCache=' + dt;
|
|
}
|
|
else {
|
|
url = req.url;
|
|
}
|
|
|
|
// Call 'abort' method in IE to allow reuse of the obj
|
|
if (document.all) {
|
|
xhrObj.abort();
|
|
}
|
|
|
|
// Set up the request
|
|
// ==========================
|
|
if (req.username && req.password) {
|
|
xhrObj.open(req.method, url, req.async, req.username, req.password);
|
|
}
|
|
else {
|
|
xhrObj.open(req.method, url, req.async);
|
|
}
|
|
// Override MIME type if necessary for Mozilla/Firefox & Safari
|
|
if (req.mimeType && navigator.userAgent.indexOf('MSIE') == -1) {
|
|
xhrObj.overrideMimeType(req.mimeType);
|
|
}
|
|
|
|
// Add any custom headers that are defined
|
|
var headers = req.headers;
|
|
for (var h in headers) {
|
|
if (headers.hasOwnProperty(h)) {
|
|
xhrObj.setRequestHeader(h, headers[h]);
|
|
}
|
|
}
|
|
// Otherwise set correct content-type for POST
|
|
if (req.method == 'POST' || req.method == 'PUT') {
|
|
// Backward-compatibility
|
|
req.data = req.data || req.dataPayload;
|
|
// Firefox throws out the content-length
|
|
// if data isn't present
|
|
if (!req.data) {
|
|
req.data = '';
|
|
}
|
|
/*
|
|
// Set content-length for picky servers, but *only*
|
|
// in nice browsers that allow it
|
|
if (!fleegix.isSafari) {
|
|
var contentLength = typeof req.data == 'string' ?
|
|
req.data.length : 0;
|
|
xhrObj.setRequestHeader('Content-Length', contentLength);
|
|
}
|
|
*/
|
|
// Set content-type to urlencoded if nothing
|
|
// else specified
|
|
if (typeof req.headers['Content-Type'] == 'undefined') {
|
|
xhrObj.setRequestHeader('Content-Type',
|
|
'application/x-www-form-urlencoded');
|
|
}
|
|
}
|
|
// Send the request, along with any POST/PUT data
|
|
// ==========================
|
|
xhrObj.send(req.data);
|
|
// ==========================
|
|
if (_processingWatcherId === null) {
|
|
_processingWatcherId = setTimeout(_watchProcessing, 10);
|
|
}
|
|
// Sync mode -- return actual result inline back to doReq
|
|
if (!req.async) {
|
|
// Blocks here
|
|
var ret = _handleResponse(xhrObj, req);
|
|
_syncRequest = null;
|
|
// Start the watcher loop back up again if need be
|
|
if (_processingArray.length) {
|
|
_processingWatcherId = setTimeout(_watchProcessing, 10);
|
|
}
|
|
// Otherwise stop watching
|
|
else {
|
|
_processingWatcherId = null;
|
|
}
|
|
return ret;
|
|
}
|
|
};
|
|
// Called in a setTimeout loop as long as requests are
|
|
// in-flight, and invokes the handler for each request
|
|
// as it returns
|
|
var _watchProcessing = function () {
|
|
var proc = _processingArray;
|
|
var d = new Date().getTime();
|
|
|
|
// Stop looping while processing sync requests
|
|
// after req returns, it will start the loop back up
|
|
if (_syncRequest !== null) {
|
|
return;
|
|
}
|
|
else {
|
|
for (var i = 0; i < proc.length; i++) {
|
|
var req = proc[i];
|
|
var xhrObj = _xhrs[req.xhrId];
|
|
var isTimedOut = ((d - req.startTime) > (req.timeoutSeconds*1000));
|
|
switch (true) {
|
|
// Aborted requests
|
|
case (req.aborted || !xhrObj.readyState):
|
|
_processingArray.splice(i, 1);
|
|
break;
|
|
// Timeouts
|
|
case isTimedOut:
|
|
_processingArray.splice(i, 1);
|
|
_timeout(req);
|
|
break;
|
|
// Actual responses
|
|
case (xhrObj.readyState == 4):
|
|
_processingArray.splice(i, 1);
|
|
_handleResponse.call(_this, xhrObj, req);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
clearTimeout(_processingWatcherId);
|
|
if (_processingArray.length) {
|
|
_processingWatcherId = setTimeout(_watchProcessing, 10);
|
|
}
|
|
else {
|
|
_processingWatcherId = null;
|
|
}
|
|
};
|
|
var _handleResponse = function (xhrObj, req) {
|
|
// Grab the desired response type
|
|
var resp;
|
|
switch(req.format) {
|
|
// JSON
|
|
case _formats.JSON:
|
|
if (typeof JSON != 'undefined') {
|
|
resp = JSON.parse(xhrObj.responseText);
|
|
}
|
|
else {
|
|
if (typeof fleegix.json == 'undefined') {
|
|
throw new Error('Need fleegix.json module to parse JSON.');
|
|
}
|
|
else {
|
|
resp = fleegix.json.parse(resp);
|
|
}
|
|
}
|
|
break;
|
|
// XML
|
|
case _formats.XML:
|
|
if (req.xmlDocFromResponseText && typeof fleegix.xml != 'undefined') {
|
|
resp = fleegix.xml.createDoc(xhrObj.responseText);
|
|
}
|
|
else {
|
|
resp = xhrObj.responseXML;
|
|
}
|
|
break;
|
|
// The object itself
|
|
case _formats.OBJ:
|
|
resp = xhrObj;
|
|
break;
|
|
// Text
|
|
case _formats.TXT:
|
|
default:
|
|
resp = xhrObj.responseText;
|
|
break;
|
|
}
|
|
// If we have a One True Event Handler, use that
|
|
// Best for odd cases such as Safari's 'undefined' status
|
|
// or 0 (zero) status from trying to load local files or chrome
|
|
if (req.all) {
|
|
req.all(resp, req.id);
|
|
}
|
|
// Otherwise hand to either success/failure
|
|
else {
|
|
try {
|
|
switch (true) {
|
|
// Request was successful -- execute response handler
|
|
case _this.isReqSuccessful(xhrObj):
|
|
if (req.async) {
|
|
// Make sure handler is defined
|
|
if (!req.success) {
|
|
throw new Error('No response handler defined ' +
|
|
'for this request');
|
|
}
|
|
else {
|
|
window.setTimeout(function () { req.success(resp, req.id) }, 0);
|
|
}
|
|
}
|
|
// Blocking requests return the result inline on success
|
|
else {
|
|
return resp;
|
|
}
|
|
break;
|
|
// Status of 0 -- in FF, user may have hit ESC while processing
|
|
case (xhrObj.status === 0):
|
|
if (_this.debug) {
|
|
throw new Error('XMLHttpRequest HTTP status is zero.');
|
|
}
|
|
break;
|
|
// Status of null or undefined -- yes, null == undefined
|
|
case (xhrObj.status == _UNDEFINED_VALUE):
|
|
// Squelch -- if you want to get local files or
|
|
// chrome, use 'all' above
|
|
if (_this.debug) {
|
|
throw new Error('XMLHttpRequest HTTP status not set.');
|
|
}
|
|
break;
|
|
// Request failed -- execute error handler or hand back
|
|
// raw request obj
|
|
default:
|
|
// Blocking requests that want the raw object returned
|
|
// on error, instead of letting the built-in handle it
|
|
if (!req.async && !_this.useDefaultErrHandlerForSync) {
|
|
return resp;
|
|
}
|
|
else {
|
|
if (req.error) {
|
|
req.error(resp, req.id);
|
|
}
|
|
else {
|
|
_errorDefault(xhrObj);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
// FIXME: Might be nice to try to catch NS_ERROR_NOT_AVAILABLE
|
|
// err in Firefox for broken connections
|
|
catch (e) {
|
|
throw e;
|
|
}
|
|
}
|
|
// Clean up, move immediately to respond to any
|
|
// queued up requests
|
|
if (req.async) {
|
|
_cleanup(req);
|
|
}
|
|
return true;
|
|
};
|
|
var _timeout = function (req) {
|
|
if (_this.abort.apply(_this, [req.id])) {
|
|
if (typeof req.timeout == 'function') {
|
|
req.timeout();
|
|
}
|
|
else {
|
|
alert('XMLHttpRequest to ' + req.url + ' timed out.');
|
|
}
|
|
}
|
|
};
|
|
var _cleanup = function (req) {
|
|
// Remove from list of xhrs currently in use
|
|
// this XHR can't be aborted until it's processing again
|
|
delete _processingMap[req.id];
|
|
|
|
// Requests queued up, grab one to respond to
|
|
if (_requestQueue.length) {
|
|
var nextReq = _requestQueue.shift();
|
|
// Reset the start time for the request for timeout purposes
|
|
nextReq.startTime = new Date().getTime();
|
|
_processReq(nextReq, req.xhrId);
|
|
}
|
|
// Otherwise this xhr is idle, waiting to respond
|
|
else {
|
|
_idleXhrs.push(req.xhrId);
|
|
}
|
|
};
|
|
var _errorDefault = function (r) {
|
|
var errorWin;
|
|
// Create new window and display error
|
|
try {
|
|
errorWin = window.open('', 'errorWin');
|
|
errorWin.document.body.innerHTML = r.responseText;
|
|
}
|
|
// If pop-up gets blocked, inform user
|
|
catch(e) {
|
|
alert('An error occurred, but the error message cannot be' +
|
|
' displayed because of your browser\'s pop-up blocker.\n' +
|
|
'Please allow pop-ups from this Web site.');
|
|
}
|
|
};
|
|
};
|
|
|
|
fleegix.xhr.Request = function () {
|
|
this.id = 0;
|
|
this.xhrId = null;
|
|
this.url = null;
|
|
this.status = null;
|
|
this.statusText = '';
|
|
this.method = 'GET';
|
|
this.async = true;
|
|
this.data = null;
|
|
this.readyState = null;
|
|
this.responseText = null;
|
|
this.responseXML = null;
|
|
this.success = null;
|
|
this.error = null;
|
|
this.all = null;
|
|
this.timeout = null;
|
|
this.contentType = null;
|
|
this.format = fleegix.xhr.formats.TXT; // TXT, XML, OBJ, JSON
|
|
this.xmlDocFromResponseText = false;
|
|
this.mimeType = null;
|
|
this.username = '';
|
|
this.password = '';
|
|
this.headers = [];
|
|
this.preventCache = false;
|
|
this.startTime = new Date().getTime();
|
|
this.timeoutSeconds = fleegix.xhr.defaultTimeoutSeconds; // Default to 30-sec timeout
|
|
this.uber = false;
|
|
this.aborted = false;
|
|
};
|
|
fleegix.xhr.Request.prototype.setRequestHeader = function (headerName, headerValue) {
|
|
this.headers.push(headerName + ': ' + headerValue);
|
|
};
|
|
|
|
/*
|
|
* Copyright 2009 Matthew Eernisse (mde@fleegix.org)
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*
|
|
*/
|
|
if (typeof fleegix == 'undefined') { var fleegix = {}; }
|
|
|
|
if (typeof $ == 'undefined') {
|
|
// Don't want hoisting -- don't declare with var
|
|
$ = function (s) { return document.getElementById(s); };
|
|
}
|
|
|
|
var $elem = function (s, o) {
|
|
var opts = o || {};
|
|
var elem = document.createElement(s);
|
|
for (var p in opts) {
|
|
elem[p] = opts[p];
|
|
}
|
|
return elem;
|
|
};
|
|
|
|
var $text = function (s) {
|
|
return document.createTextNode(s);
|
|
};
|
|
|
|
fleegix.bind = function (func, context) {
|
|
return function () {
|
|
func.apply(context, arguments);
|
|
};
|
|
};
|
|
|
|
fleegix.extend = function (/* Super-class constructor function */ superClass,
|
|
/* Sub-class constructor function */ subClass) {
|
|
return function () {
|
|
superClass.apply(this, arguments);
|
|
superClass.prototype.constructor.apply(this, arguments);
|
|
subClass.apply(this, arguments);
|
|
this.superClass = superClass;
|
|
this.subClass = subClass;
|
|
};
|
|
};
|
|
|
|
fleegix.mixin = function (/* Target obj */ target,
|
|
/* Obj of props or constructor */ mixin, /* Deep-copy flag */ recurse) {
|
|
// Create an instance if we get a constructor
|
|
var m;
|
|
if (typeof mixin == 'function') {
|
|
m = new mixin();
|
|
}
|
|
else {
|
|
m = mixin;
|
|
}
|
|
var baseObj = {};
|
|
for (var p in m) {
|
|
// Don't copy anything from Object.prototype
|
|
if (typeof baseObj[p] == 'undefined' || baseObjj[p] != m[p]) {
|
|
if (recurse && typeof m[p] == 'object' && m[p] !== null && !m[p] instanceof Array) {
|
|
fleegix.mixin(target[p], m[p], recurse);
|
|
}
|
|
else {
|
|
target[p] = m[p];
|
|
}
|
|
}
|
|
}
|
|
return target;
|
|
};
|
|
|
|
// Note this doesn't check for cyclical references
|
|
fleegix.clone = function (o) {
|
|
if (typeof o == 'object') {
|
|
var ret;
|
|
if (typeof o.constructor == 'function') {
|
|
ret = new o.constructor();
|
|
}
|
|
else {
|
|
ret = {};
|
|
}
|
|
for (var p in o) {
|
|
if (typeof o[p] == 'object' && o[p] !== null) {
|
|
ret[p] = fleegix.clone(o[p]);
|
|
}
|
|
else {
|
|
ret[p] = o[p];
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
ret = o;
|
|
}
|
|
return ret;
|
|
};
|
|
|
|
if (typeof navigator != 'undefined') {
|
|
fleegix.ua = new function () {
|
|
var ua = navigator.userAgent;
|
|
var majorVersion = function (ua, re) {
|
|
var m = re.exec(ua);
|
|
if (m && m.length > 1) {
|
|
m = m[1].substr(0, 1);
|
|
if (!isNaN(m)) {
|
|
return parseInt(m);
|
|
}
|
|
else {
|
|
return null;
|
|
}
|
|
}
|
|
return null;
|
|
};
|
|
// Layout engines
|
|
this.isWebKit= ua.indexOf("AppleWebKit") > -1;
|
|
this.isKHTML = ua.indexOf('KHTML') > -1;
|
|
this.isGecko = ua.indexOf('Gecko') > -1 &&
|
|
!this.isWebKit && !this.isKHTML;
|
|
|
|
// Browsers
|
|
this.isOpera = ua.indexOf("Opera") > -1;
|
|
this.isChrome = ua.indexOf("Chrome") > -1;
|
|
this.isSafari = ua.indexOf("Safari") > -1 && !this.isChrome;
|
|
// Firefox, Camino, 'Iceweasel/IceCat' for the freetards
|
|
this.isFF = ua.indexOf('Firefox') > -1 ||
|
|
ua.indexOf('Iceweasel') > -1 || ua.indexOf('IceCat') > -1;
|
|
this.isFirefox = this.isFF; // Alias
|
|
this.isIE = ua.indexOf('MSIE ') > -1 && !this.isOpera;
|
|
|
|
// Mobiles
|
|
this.isIPhone = ua.indexOf("iPhone") > -1;
|
|
this.isMobile = this.isIPhone || ua.indexOf("Opera Mini") > -1;
|
|
|
|
// OS's
|
|
this.isMac = ua.indexOf('Mac') > -1;
|
|
this.isUnix = ua.indexOf('Linux') > -1 ||
|
|
ua.indexOf('BSD') > -1 || ua.indexOf('SunOS') > -1;
|
|
this.isLinux = ua.indexOf('Linux') > -1;
|
|
this.isWindows = ua.indexOf('Windows') > -1 || ua.indexOf('Win');
|
|
|
|
// Major ua version
|
|
this.majorVersion = null;
|
|
var reList = {
|
|
FF: /Firefox\/([0-9\.]*)/,
|
|
Safari: /Version\/([0-9\.]*) /,
|
|
IE: /MSIE ([0-9\.]*);/,
|
|
Opera: /Opera\/([0-9\.]*) /,
|
|
Chrome: /Chrome\/([0-9\.]*)/
|
|
}
|
|
for (var p in reList) {
|
|
if (this['is' + p]) {
|
|
this.majorVersion = majorVersion(ua, reList[p]);
|
|
}
|
|
}
|
|
|
|
// Add to base fleegix obj for backward compat
|
|
for (var p in this) {
|
|
fleegix[p] = this[p];
|
|
}
|
|
};
|
|
}
|
|
|
|
|
|
fleegix.cookie = new function() {
|
|
this.set = function(name, value, optParam) {
|
|
var opts = optParam || {};
|
|
var path = '/';
|
|
var days = 0;
|
|
var hours = 0;
|
|
var minutes = 0;
|
|
var exp = '';
|
|
var t = 0;
|
|
if (typeof optParam == 'object') {
|
|
path = opts.path || '/';
|
|
days = opts.days || 0;
|
|
hours = opts.hours || 0;
|
|
minutes = opts.minutes || 0;
|
|
}
|
|
else {
|
|
path = optParam || '/';
|
|
}
|
|
t += days ? days*24*60*60*1000 : 0;
|
|
t += hours ? hours*60*60*1000 : 0;
|
|
t += minutes ? minutes*60*1000 : 0;
|
|
|
|
if (t) {
|
|
var dt = new Date();
|
|
dt.setTime(dt.getTime() + t);
|
|
exp = '; expires=' + dt.toGMTString();
|
|
}
|
|
else {
|
|
exp = '';
|
|
}
|
|
document.cookie = name + '=' + value +
|
|
exp + '; path=' + path;
|
|
};
|
|
this.get = function(name) {
|
|
var nameEq = name + '=';
|
|
var arr = document.cookie.split(';');
|
|
for(var i = 0; i < arr.length; i++) {
|
|
var c = arr[i];
|
|
while (c.charAt(0) == ' ') {
|
|
c = c.substring(1, c.length);
|
|
}
|
|
if (c.indexOf(nameEq) === 0) {
|
|
return c.substring(nameEq.length, c.length);
|
|
}
|
|
}
|
|
return null;
|
|
};
|
|
this.create = this.set;
|
|
this.destroy = function(name, path) {
|
|
var opts = {};
|
|
opts.minutes = -1;
|
|
if (path) { opts.path = path; }
|
|
this.set(name, '', opts);
|
|
};
|
|
};
|
|
|
|
|
|
fleegix.form = {};
|
|
/**
|
|
* Serializes the data from all the inputs in a Web form
|
|
* into a query-string style string.
|
|
* @param f -- Reference to a DOM node of the form element
|
|
* @param opts -- JS object of options for how to format
|
|
* the return string. See fleegix.url.objectToQS for usage.
|
|
* @returns query-string style String of variable-value pairs
|
|
*/
|
|
fleegix.form.serialize = function (f, opts) {
|
|
var h = fleegix.form.toObject(f, opts);
|
|
if (typeof fleegix.url == 'undefined') {
|
|
throw new Error('fleegix.form.serialize depends on the fleegix.url module.');
|
|
}
|
|
var str = fleegix.url.objectToQS(h, opts);
|
|
return str;
|
|
};
|
|
|
|
/**
|
|
* Converts the values in an HTML form into a JS object
|
|
* Elements with multiple values like sets of radio buttons
|
|
* become arrays
|
|
* @param f -- HTML form element to convert into a JS object
|
|
* @param o -- JS Object of options:
|
|
* pedantic: (Boolean) include the values of elements like
|
|
* button or image
|
|
* hierarchical: (Boolean) if the form is using Rails-/PHP-style
|
|
* name="foo[bar]" inputs, setting this option to
|
|
* true will create a hierarchy of objects in the
|
|
* resulting JS object, where some of the properties
|
|
* of the objects are sub-objects with values pulled
|
|
* from the form. Note: this only supports one level
|
|
* of nestedness
|
|
* hierarchical option code by Kevin Faulhaber, kjf@kjfx.net
|
|
* @returns JavaScript object representation of the contents
|
|
* of the form.
|
|
*/
|
|
fleegix.form.toObject= function (f, o) {
|
|
var opts = o || {};
|
|
var h = {};
|
|
function expandToArr(orig, val) {
|
|
if (orig) {
|
|
var r = null;
|
|
if (typeof orig == 'string') {
|
|
r = [];
|
|
r.push(orig);
|
|
}
|
|
else { r = orig; }
|
|
r.push(val);
|
|
return r;
|
|
}
|
|
else { return val; }
|
|
}
|
|
|
|
for (var i = 0; i < f.elements.length; i++) {
|
|
var elem = f.elements[i];
|
|
// Elements should have a name
|
|
if (elem.name) {
|
|
var st = elem.name.indexOf('[');
|
|
var sp = elem.name.indexOf(']');
|
|
var sb = '';
|
|
var en = '';
|
|
var c;
|
|
var n;
|
|
// Using Rails-/PHP-style name="foo[bar]"
|
|
// means you can go hierarchical if you want
|
|
if (opts.hierarchical && (st > 0) && (sp > 2)) {
|
|
sb = elem.name.substring(0, st);
|
|
en = elem.name.substring(st + 1, sp);
|
|
if (typeof h[sb] == 'undefined') { h[sb] = {}; }
|
|
c = h[sb];
|
|
n = en;
|
|
}
|
|
else {
|
|
c = h;
|
|
n = elem.name;
|
|
}
|
|
switch (elem.type) {
|
|
// Text fields, hidden form elements, etc.
|
|
case 'text':
|
|
case 'hidden':
|
|
case 'password':
|
|
case 'textarea':
|
|
case 'select-one':
|
|
c[n] = elem.value;
|
|
break;
|
|
// Multi-option select
|
|
case 'select-multiple':
|
|
for(var j = 0; j < elem.options.length; j++) {
|
|
var e = elem.options[j];
|
|
if(e.selected) {
|
|
c[n] = expandToArr(c[n], e.value);
|
|
}
|
|
}
|
|
break;
|
|
// Radio buttons
|
|
case 'radio':
|
|
if (elem.checked) {
|
|
c[n] = elem.value;
|
|
}
|
|
break;
|
|
// Checkboxes
|
|
case 'checkbox':
|
|
if (elem.checked) {
|
|
c[n] = expandToArr(c[n], elem.value);
|
|
}
|
|
break;
|
|
// Pedantic
|
|
case 'submit':
|
|
case 'reset':
|
|
case 'file':
|
|
case 'image':
|
|
case 'button':
|
|
if (opts.pedantic) { c[n] = elem.value; }
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
return h;
|
|
};
|
|
// Alias for backward compat
|
|
fleegix.form.toHash = fleegix.form.toObject;
|
|
|
|
fleegix.json = new function() {
|
|
// Escaping control chars, etc.
|
|
// Source: Crockford's excellent JSON2
|
|
// http://json.org/json2.js -- License: public domain
|
|
var _cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g;
|
|
var _escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g;
|
|
var _meta = { // table of character substitutions
|
|
'\b': '\\b',
|
|
'\t': '\\t',
|
|
'\n': '\\n',
|
|
'\f': '\\f',
|
|
'\r': '\\r',
|
|
'"' : '\\"',
|
|
'\\': '\\\\'
|
|
};
|
|
var _quote = function (str) {
|
|
_escapable.lastIndex = 0;
|
|
return _escapable.test(str) ?
|
|
'"' + str.replace(_escapable, function (a) {
|
|
var c = _meta[a];
|
|
return typeof c === 'string' ? c :
|
|
'\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
|
|
}) + '"' :
|
|
'"' + str + '"';
|
|
};
|
|
this.serialize = function(obj) {
|
|
var str = '';
|
|
switch (typeof obj) {
|
|
case 'object':
|
|
switch (true) {
|
|
// Null
|
|
case obj === null:
|
|
return 'null';
|
|
break;
|
|
// Arrays
|
|
case obj instanceof Array:
|
|
for (var i = 0; i < obj.length; i++) {
|
|
if (str) { str += ','; }
|
|
str += fleegix.json.serialize(obj[i]);
|
|
}
|
|
return '[' + str + ']';
|
|
break;
|
|
// Exceptions don't serialize correctly in Firefox
|
|
case (fleegix.isFF && obj instanceof DOMException):
|
|
str += '"' + obj.toString() + '"';
|
|
break;
|
|
// All other generic objects
|
|
case typeof obj.toString != 'undefined':
|
|
for (var i in obj) {
|
|
if (str) { str += ','; }
|
|
str += '"' + i + '":';
|
|
if (typeof obj[i] == 'undefined') {
|
|
str += '"undefined"';
|
|
}
|
|
else {
|
|
str += fleegix.json.serialize(obj[i]);
|
|
}
|
|
}
|
|
return '{' + str + '}';
|
|
break;
|
|
}
|
|
return str;
|
|
case 'unknown':
|
|
case 'undefined':
|
|
case 'function':
|
|
return '"undefined"';
|
|
case 'string':
|
|
str += _quote(obj);
|
|
return str;
|
|
default:
|
|
return String(obj);
|
|
}
|
|
};
|
|
// Credits: Crockford's excellent json2 parser
|
|
// http://json.org/json2.js -- License: public domain
|
|
this.parse = function (text, reviver) {
|
|
var j;
|
|
function walk(holder, key) {
|
|
var k, v, value = holder[key];
|
|
if (value && typeof value === 'object') {
|
|
for (k in value) {
|
|
if (Object.hasOwnProperty.call(value, k)) {
|
|
v = walk(value, k);
|
|
if (v !== undefined) {
|
|
value[k] = v;
|
|
} else {
|
|
delete value[k];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return reviver.call(holder, key, value);
|
|
}
|
|
_cx.lastIndex = 0;
|
|
if (_cx.test(text)) {
|
|
text = text.replace(_cx, function (a) {
|
|
return '\\u' +
|
|
('0000' + a.charCodeAt(0).toString(16)).slice(-4);
|
|
});
|
|
}
|
|
if (/^[\],:{}\s]*$/.
|
|
test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@').
|
|
replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']').
|
|
replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) {
|
|
j = eval('(' + text + ')');
|
|
return typeof reviver === 'function' ?
|
|
walk({'': j}, '') : j;
|
|
}
|
|
throw new SyntaxError('JSON not parseable.');
|
|
};
|
|
};
|
|
|
|
|
|
fleegix.string = new function () {
|
|
// Regexes used in trimming functions
|
|
var _LTR = /^\s+/;
|
|
var _RTR = /\s+$/;
|
|
var _TR = /^\s+|\s+$/g;
|
|
// From/to char mappings -- for the XML escape,
|
|
// unescape, and test for escapable chars
|
|
var _CHARS = {
|
|
'&': '&',
|
|
'<': '<',
|
|
'>': '>',
|
|
'"': '"',
|
|
'\'': '''
|
|
};
|
|
// Builds the escape/unescape methods using a common
|
|
// map of characters
|
|
var _buildEscapes = function (direction) {
|
|
return function (str) {
|
|
s = str;
|
|
var fr, to;
|
|
for (var p in _CHARS) {
|
|
fr = direction == 'to' ? p : _CHARS[p];
|
|
to = direction == 'to' ? _CHARS[p] : p;
|
|
s = s.replace(new RegExp(fr, 'gm'), to);
|
|
}
|
|
return s;
|
|
};
|
|
};
|
|
// Builds a method that tests for any of the escapable
|
|
// characters -- useful for avoiding double-escaping if
|
|
// you're not sure whether a string is already escaped
|
|
var _buildEscapeTest = function (direction) {
|
|
return function (s) {
|
|
var pat = '';
|
|
for (var p in _CHARS) {
|
|
pat += direction == 'to' ? p : _CHARS[p];
|
|
pat += '|';
|
|
}
|
|
pat = pat.substr(0, pat.length - 1);
|
|
pat = new RegExp(pat, "gm");
|
|
return pat.test(s);
|
|
};
|
|
};
|
|
// Escape special chars to entities
|
|
this.escapeXML = _buildEscapes('to');
|
|
// Unescape entities to special chars
|
|
this.unescapeXML = _buildEscapes('from');
|
|
// Test if a string includes special chars that
|
|
// require escaping
|
|
this.needsEscape = _buildEscapeTest('to');
|
|
this.needsUnescape = _buildEscapeTest('from');
|
|
this.toArray = function (str) {
|
|
var arr = [];
|
|
for (var i = 0; i < str.length; i++) {
|
|
arr[i] = str.substr(i, 1);
|
|
}
|
|
return arr;
|
|
};
|
|
this.reverse = function (str) {
|
|
return this.toArray(str).reverse().join('');
|
|
};
|
|
this.ltrim = function (str, chr) {
|
|
var pat = chr ? new RegExp('^' + chr + '+') : _LTR;
|
|
return str.replace(pat, '');
|
|
};
|
|
this.rtrim = function (str, chr) {
|
|
var pat = chr ? new RegExp(chr + '+$') : _RTR;
|
|
return str.replace(pat, '');
|
|
};
|
|
this.trim = function (str, chr) {
|
|
var pat = chr ? new RegExp('^' + chr + '+|' + chr + '+$', 'g') : _TR;
|
|
return str.replace(pat, '');
|
|
};
|
|
this.lpad = function (str, chr, width) {
|
|
var s = str;
|
|
while (s.length < width) {
|
|
s = chr + s;
|
|
}
|
|
return s;
|
|
};
|
|
this.rpad = function (str, chr, width) {
|
|
var s = str;
|
|
while (s.length < width) {
|
|
s = s + chr;
|
|
}
|
|
return s;
|
|
};
|
|
// Converts someVariableName to some_variable_name
|
|
this.toLowerCaseWithUnderscores = function (s) {
|
|
return s.replace(/([A-Z]+)/g, '_$1').toLowerCase().
|
|
replace(/^_/, '');
|
|
};
|
|
// Alias for above
|
|
this.deCamelize = function (s) {
|
|
return this.toLowerCaseWithUnderscores(s);
|
|
};
|
|
// Converts some_variable_name to someVariableName
|
|
this.toCamelCase = function (s) {
|
|
return s.replace(/_[a-z]{1}/g, function (s)
|
|
{ return s.replace('_', '').toUpperCase() });
|
|
};
|
|
// Alias for above
|
|
this.camelize = function (s) {
|
|
return this.toCamelCase(s);
|
|
};
|
|
this.capitalize = function (s) {
|
|
return s.substr(0, 1).toUpperCase() + s.substr(1);
|
|
};
|
|
};
|
|
|
|
|
|
|
|
fleegix.url = new function () {
|
|
// Private vars
|
|
var _this = this;
|
|
var _QS = '\\?|;';
|
|
var _QS_SIMPLE = new RegExp(_QS);
|
|
var _QS_CAPTURE = new RegExp('(' + _QS + ')');
|
|
|
|
// Private function, used by both getQSParam and setQSParam
|
|
var _changeQS = function (mode, str, name, val) {
|
|
var match = _QS_CAPTURE.exec(str);
|
|
var delim;
|
|
var base;
|
|
var query;
|
|
var obj;
|
|
var s = '';
|
|
// If there's a querystring delimiter, save it
|
|
// for reinsertion into the return value
|
|
if (match && match.length > 0) {
|
|
delim = match[0];
|
|
}
|
|
// Delimiter -- entire URL, need to decompose
|
|
if (delim) {
|
|
base = _this.getBase(str);
|
|
query = _this.getQS(str);
|
|
}
|
|
// Just a querystring passed
|
|
else {
|
|
query = str;
|
|
}
|
|
obj = _this.qsToObject(query, { arrayizeMulti: true });
|
|
if (mode == 'set') { obj[name] = val; }
|
|
else { delete obj[name]; }
|
|
if (base) {
|
|
s = base + delim;
|
|
}
|
|
s += _this.objectToQS(obj);
|
|
return s;
|
|
};
|
|
/**
|
|
* Convert the values in a query string (key=val&key=val) to
|
|
* an Object
|
|
* @param str -- A querystring
|
|
* @param o -- JS object of options, current only includes:
|
|
* arrayizeMulti: (Boolean) convert mutliple instances of
|
|
* the same key into an array of values instead of
|
|
* overriding. Defaults to false.
|
|
* @returns JavaScript key/val object with the values from
|
|
* the querystring
|
|
*/
|
|
this.qsToObject = function (str, o) {
|
|
var opts = o || {};
|
|
var d = {};
|
|
var arrayizeMulti = opts.arrayizeMulti || false;
|
|
if (str) {
|
|
var arr = str.split('&');
|
|
for (var i = 0; i < arr.length; i++) {
|
|
var pair = arr[i].split('=');
|
|
var name = pair[0];
|
|
var val = decodeURIComponent(pair[1]);
|
|
// "We've already got one!" -- arrayize if the flag
|
|
// is set
|
|
if (typeof d[name] != 'undefined' && arrayizeMulti) {
|
|
if (typeof d[name] == 'string') {
|
|
d[name] = [d[name]];
|
|
}
|
|
d[name].push(val);
|
|
}
|
|
// Otherwise just set the value
|
|
else {
|
|
d[name] = val;
|
|
}
|
|
}
|
|
}
|
|
return d;
|
|
};
|
|
/**
|
|
* Convert a JS Object to querystring (key=val&key=val).
|
|
* Value in arrays will be added as multiple parameters
|
|
* @param obj -- an Object containing only scalars and arrays
|
|
* @param o -- JS object of options for how to format
|
|
* the return string. Supported options:
|
|
* collapseMulti: (Boolean) take values from elements that
|
|
* can return multiple values (multi-select, checkbox groups)
|
|
* and collapse into a single, comman-delimited value
|
|
* (e.g., thisVar=asdf,qwer,zxcv)
|
|
* stripTags: (Boolean) strip markup tags from any values
|
|
* includeEmpty: (Boolean) include keys in the string for
|
|
* all elements, even if they have no value set (e.g.,
|
|
* even if elemB has no value: elemA=foo&elemB=&elemC=bar)
|
|
* pedantic: (Boolean) include the values of elements like
|
|
* button or image
|
|
* deCamelizeParams: (Boolean) change param names from
|
|
* camelCase to lowercase_with_underscores
|
|
* @returns A querystring containing the values in the
|
|
* Object
|
|
* NOTE: This is used by form.serialize
|
|
*/
|
|
this.objectToQS = function (obj, o) {
|
|
var opts = o || {};
|
|
var str = '';
|
|
var pat = opts.stripTags ? /<[^>]*>/g : null;
|
|
for (var n in obj) {
|
|
var s = '';
|
|
var v = obj[n];
|
|
if (v != undefined) {
|
|
// Multiple vals -- array
|
|
if (v.length && typeof v != 'string') {
|
|
var sep = '';
|
|
if (opts.collapseMulti) {
|
|
sep = ',';
|
|
str += n + '=';
|
|
}
|
|
else {
|
|
sep = '&';
|
|
}
|
|
for (var j = 0; j < v.length; j++) {
|
|
s = opts.stripTags ? v[j].replace(pat, '') : v[j];
|
|
s = (!opts.collapseMulti) ? n + '=' + encodeURIComponent(s) :
|
|
encodeURIComponent(s);
|
|
str += s + sep;
|
|
}
|
|
str = str.substr(0, str.length - 1);
|
|
}
|
|
// Single val -- string
|
|
else {
|
|
s = opts.stripTags ? v.replace(pat, '') : v;
|
|
str += n + '=' + encodeURIComponent(s);
|
|
}
|
|
str += '&';
|
|
}
|
|
else {
|
|
if (opts.includeEmpty) { str += n + '=&'; }
|
|
}
|
|
}
|
|
// Convert all the camelCase param names to Ruby/Python style
|
|
// lowercase_with_underscores
|
|
if (opts.deCamelizeParams) {
|
|
if (!fleegix.string) {
|
|
throw new Error(
|
|
'deCamelize option depends on fleegix.string module.');
|
|
}
|
|
var arr = str.split('&');
|
|
var arrItems;
|
|
str = '';
|
|
for (var i = 0; i < arr.length; i++) {
|
|
arrItems = arr[i].split('=');
|
|
if (arrItems[0]) {
|
|
str += fleegix.string.deCamelize(arrItems[0]) +
|
|
'=' + arrItems[1] + '&';
|
|
}
|
|
}
|
|
}
|
|
str = str.substr(0, str.length - 1);
|
|
return str;
|
|
};
|
|
this.objectToQs = this.objectToQS; // Case-insensitive alias
|
|
/**
|
|
* Retrieve the value of a parameter from a querystring
|
|
* @param str -- Either a querystring or an entire URL
|
|
* @param name -- The param to retrieve the value for
|
|
* @param o -- JS object of options, current only includes:
|
|
* arrayizeMulti: (Boolean) convert mutliple instances of
|
|
* the same key into an array of values instead of
|
|
* overriding. Defaults to false.
|
|
* @returns The string value of the specified param from
|
|
* the querystring
|
|
*/
|
|
this.getQSParam = function (str, name, o) {
|
|
var p = null;
|
|
var q = _QS_SIMPLE.test(str) ? _this.getQS(str) : str;
|
|
var opts = o || {};
|
|
if (q) {
|
|
var h = _this.qsToObject(q, opts);
|
|
p = h[name];
|
|
}
|
|
return p;
|
|
};
|
|
this.getQsParam = this.getQSParam; // Case-insensitive alias
|
|
/**
|
|
* Set the value of a parameter in a querystring
|
|
* @param str -- Either a querystring or an entire URL
|
|
* @param name -- The param to set
|
|
* @param val -- The value to set the param to
|
|
* @returns the URL or querystring, with the new value
|
|
* set -- if the param was not originally there, it adds it.
|
|
*/
|
|
this.setQSParam = function (str, name, val) {
|
|
return _changeQS('set', str, name, val);
|
|
};
|
|
this.setQsParam = this.setQSParam; // Case-insensitive alias
|
|
/**
|
|
* Remove a parameter in a querystring
|
|
* @param str -- Either a querystring or an entire URL
|
|
* @param name -- The param to remove
|
|
* @returns the URL or querystring, with the parameter
|
|
* removed
|
|
*/
|
|
this.removeQSParam = function (str, name) {
|
|
return _changeQS('remove', str, name, null);
|
|
};
|
|
this.removeQsParam = this.removeQSParam; // Case-insensitive alias
|
|
this.getQS = function (s) {
|
|
return s.split(_QS_SIMPLE)[1];
|
|
};
|
|
this.getQs = this.getQS; // Case-insensitive alias
|
|
this.getBase = function (s) {
|
|
return s.split(_QS_SIMPLE)[0];
|
|
};
|
|
|
|
this.getFileExtension = function (str) {
|
|
var ext = null;
|
|
var pathArr = str.split('.');
|
|
if (pathArr.length > 1) {
|
|
ext = pathArr[pathArr.length - 1];
|
|
}
|
|
return ext;
|
|
};
|
|
};
|
|
// Backward-compat shim
|
|
fleegix.uri = new function () {
|
|
this.getParamHash = fleegix.url.qsToObject;
|
|
// Params are reversed, and passed-in QS is
|
|
// optional -- defaults to local HREF for the
|
|
// page it's defined on
|
|
this.getParam = function (name, str) {
|
|
var s = str || fleegix.url.getQS(document.location.href);
|
|
return fleegix.url.getQSParam(s, name);
|
|
};
|
|
// Params are reversed, and passed-in QS is
|
|
// optional -- defaults to local HREF for the
|
|
// page it's defined on
|
|
this.setParam = function (name, val, str) {
|
|
var s = str || fleegix.url.getQS(document.location.href);
|
|
return fleegix.url.setQSParam(s, name, val);
|
|
};
|
|
this.getQuery = fleegix.url.getQS;
|
|
this.getBase = fleegix.url.getBase;
|
|
};
|
|
|
|
|
|
fleegix.html = new function () {
|
|
var _createElem = function (s) {
|
|
return document.createElement(s); };
|
|
var _createText = function (s) {
|
|
return document.createTextNode(s); };
|
|
this.createSelect = function (o) {
|
|
var sel = document.createElement('select');
|
|
var options = [];
|
|
var appendElem = null;
|
|
|
|
// createSelect({ name: 'foo', id: 'foo', multi: true,
|
|
// options: [{ text: 'Howdy', value: '123' },
|
|
// { text: 'Hey', value: 'ABC' }], className: 'fooFoo' });
|
|
appendElem = arguments[1];
|
|
options = o.options;
|
|
for (var p in o) {
|
|
if (p != 'options') {
|
|
sel[p] = o[p];
|
|
}
|
|
}
|
|
// Add the options for the select
|
|
if (options) {
|
|
this.setSelectOptions(sel, options);
|
|
}
|
|
return sel;
|
|
};
|
|
|
|
this.setSelectOptions = function (selectElement, options){
|
|
while (selectElement.firstChild){
|
|
selectElement.removeChild(selectElement.firstChild);
|
|
}
|
|
for (var i = 0; i < options.length; i++) {
|
|
var opt = _createElem('option');
|
|
opt.value = options[i].value || '';
|
|
opt.appendChild(_createText(options[i].text));
|
|
selectElement.appendChild(opt);
|
|
if (options[i].selected){
|
|
selectElement.selectedIndex = i;
|
|
}
|
|
}
|
|
};
|
|
|
|
this.setSelect = function (sel, val) {
|
|
var index = 0;
|
|
var opts = sel.options;
|
|
for (var i = 0; i < opts.length; i++) {
|
|
if (opts[i].value == val) {
|
|
index = i;
|
|
break;
|
|
}
|
|
}
|
|
sel.selectedIndex = index;
|
|
};
|
|
|
|
this.createInput = function (o) {
|
|
var input = null;
|
|
var str = '';
|
|
|
|
// createInput({ type: 'password', name: 'foo', id: 'foo',
|
|
// value: 'asdf', className: 'fooFoo' });
|
|
// Neither IE nor Safari 2 can handle DOM-generated radio
|
|
// or checkbox inputs -- they stupidly assume name and id
|
|
// attributes should be identical. The solution: kick it
|
|
// old-skool with conditional branching and innerHTML
|
|
if ((document.all || navigator.userAgent.indexOf('Safari/41') > -1) &&
|
|
(o.type == 'radio' || o.type == 'checkbox')) {
|
|
str = '<input type="' + o.type + '"' +
|
|
' name="' + o.name + '"' +
|
|
' id ="' + o.id + '"';
|
|
if (o.value) {
|
|
str += ' value="' + o.value + '"';
|
|
}
|
|
if (o.size) {
|
|
str += ' size="' + o.size + '"';
|
|
}
|
|
if (o.maxlength) {
|
|
str += ' maxlength="' + o.maxlength + '"';
|
|
}
|
|
if (o.checked) {
|
|
str += ' checked="checked"';
|
|
}
|
|
if (o.className) {
|
|
str += ' class="' + o.className + '"';
|
|
}
|
|
str += '/>';
|
|
|
|
var s = _createElem('span');
|
|
s.innerHTML = str;
|
|
input = s.firstChild;
|
|
s.removeChild(input);
|
|
}
|
|
// Standards-compliant browsers -- all form inputs
|
|
// IE/Safari 2 -- everything but radio button and checkbox
|
|
else {
|
|
input = document.createElement('input');
|
|
for (var p in o) {
|
|
input[p] = o[p];
|
|
}
|
|
|
|
}
|
|
return input;
|
|
};
|
|
|
|
};
|
|
/*
|
|
* Copyright 2009 Matthew Eernisse (mde@fleegix.org)
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*
|
|
*/
|
|
if (typeof fleegix == 'undefined') { fleegix = {}; }
|
|
|
|
fleegix.ejs = {};
|
|
|
|
fleegix.ejs.templateTextCache = {};
|
|
|
|
fleegix.ejs.Template = function (p) {
|
|
var UNDEF;
|
|
var params = p || {};
|
|
|
|
this.mode = null;
|
|
this.templateText = params.text ||
|
|
// If you don't want to use Fleegix.js,
|
|
// override getTemplateTextFromNode to use
|
|
// textarea node value for template text
|
|
this.getTemplateTextFromNode(params.node);
|
|
this.afterLoaded = params.afterLoaded;
|
|
this.source = '';
|
|
this.markup = UNDEF;
|
|
// Try to get from URL
|
|
if (typeof this.templateText == 'undefined') {
|
|
// If you don't want to use Fleegix.js,
|
|
// override getTemplateTextFromUrl to use
|
|
// files for template text
|
|
this.getTemplateTextFromUrl(params);
|
|
}
|
|
};
|
|
|
|
fleegix.ejs.Template.prototype = new function () {
|
|
var _REGEX = /(<%%)|(%%>)|(<%=)|(<%#)|(<%)|(%>\n)|(%>)|(\n)/;
|
|
this.modes = {
|
|
EVAL: 'eval',
|
|
OUTPUT: 'output',
|
|
APPEND: 'append',
|
|
COMMENT: 'comment',
|
|
LITERAL: 'literal'
|
|
};
|
|
this.getTemplateTextFromNode = function (node) {
|
|
// Requires the fleegix.xhr module
|
|
if (typeof fleegix.string == 'undefined') {
|
|
throw('Requires fleegix.string module.'); }
|
|
var ret;
|
|
if (node) {
|
|
ret = node.value;
|
|
ret = fleegix.string.unescapeXML(ret);
|
|
ret = fleegix.string.trim(ret);
|
|
}
|
|
return ret;
|
|
};
|
|
this.getTemplateTextFromUrl = function (params) {
|
|
// Requires the fleegix.xhr module
|
|
if (typeof fleegix.xhr == 'undefined') {
|
|
throw('Requires fleegix.xhr module.'); }
|
|
var _this = this;
|
|
var url = params.url;
|
|
var noCache = params.preventCache || false;
|
|
var text = fleegix.ejs.templateTextCache[url];
|
|
// Found text in cache, and caching is turned on
|
|
if (text && !noCache) {
|
|
this.templateText = text;
|
|
}
|
|
// Otherwise go grab the text
|
|
else {
|
|
// Callback for setting templateText and caching --
|
|
// used for both sync and async loading
|
|
var callback = function (s) {
|
|
_this.templateText = s;
|
|
fleegix.ejs.templateTextCache[url] = s;
|
|
// Use afterLoaded hook if set
|
|
if (typeof _this.afterLoaded == 'function') {
|
|
_this.afterLoaded();
|
|
}
|
|
};
|
|
var opts;
|
|
if (params.async) {
|
|
opts = {
|
|
url: url,
|
|
method: 'GET',
|
|
preventCache: noCache,
|
|
async: true,
|
|
handleSuccess: callback
|
|
};
|
|
// Get templ text asynchronously, wait for
|
|
// loading to exec the callback
|
|
fleegix.xhr.send(opts);
|
|
}
|
|
else {
|
|
opts = {
|
|
url: url,
|
|
method: 'GET',
|
|
preventCache: noCache,
|
|
async: false
|
|
};
|
|
// Get the templ text inline and pass directly to
|
|
// the callback
|
|
text = fleegix.xhr.send(opts);
|
|
callback(text);
|
|
}
|
|
}
|
|
};
|
|
|
|
this.process = function (p) {
|
|
var params = p || {};
|
|
this.data = params.data || {};
|
|
var domNode = params.domNode;
|
|
// Cache/reuse the generated template source for speed
|
|
this.source = this.source || '';
|
|
if (!this.source) { this.generateSource(); }
|
|
|
|
// Eval the template with the passed data
|
|
// Use 'with' to give local scoping to data obj props
|
|
// ========================
|
|
var _output = ''; // Inner scope var for eval output
|
|
console.log(this.source);
|
|
with (this.data) {
|
|
eval(this.source);
|
|
}
|
|
this.markup = _output;
|
|
|
|
if (domNode) {
|
|
domNode.innerHTML = this.markup;
|
|
}
|
|
return this.markup;
|
|
};
|
|
|
|
this.generateSource = function () {
|
|
var line = '';
|
|
var matches = this.parseTemplateText();
|
|
if (matches) {
|
|
for (var i = 0; i < matches.length; i++) {
|
|
line = matches[i];
|
|
if (line) {
|
|
this.scanLine(line);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
this.parseTemplateText = function() {
|
|
var str = this.templateText;
|
|
var pat = _REGEX;
|
|
var result = pat.exec(str);
|
|
var arr = [];
|
|
while (result) {
|
|
var firstPos = result.index;
|
|
var lastPos = pat.lastIndex;
|
|
if (firstPos !== 0) {
|
|
arr.push(str.substring(0, firstPos));
|
|
str = str.slice(firstPos);
|
|
}
|
|
arr.push(result[0]);
|
|
str = str.slice(result[0].length);
|
|
result = pat.exec(str);
|
|
}
|
|
if (str !== '') {
|
|
arr.push(str);
|
|
}
|
|
return arr;
|
|
};
|
|
|
|
this.scanLine = function (line) {
|
|
var li, _this = this;
|
|
var _addOutput = function () {
|
|
line = line.replace(/\n/g, '\\n').replace(/\r/g,
|
|
'\\r').replace(/"/g, '\\"');
|
|
_this.source += '_output += "' + line + '";';
|
|
};
|
|
switch (line) {
|
|
case '<%':
|
|
this.mode = this.modes.EVAL;
|
|
break;
|
|
case '<%=':
|
|
this.mode = this.modes.OUTPUT;
|
|
break;
|
|
case '<%#':
|
|
this.mode = this.modes.COMMENT;
|
|
break;
|
|
case '<%%':
|
|
this.mode = this.modes.LITERAL;
|
|
this.source += '_output += "' + line + '";';
|
|
break;
|
|
case '%>':
|
|
case '%>\n':
|
|
if (this.mode == this.modes.LITERAL) {
|
|
_addOutput();
|
|
}
|
|
this.mode = null;
|
|
break;
|
|
default:
|
|
// In script mode, depends on type of tag
|
|
if (this.mode) {
|
|
switch (this.mode) {
|
|
// Just executing code
|
|
case this.modes.EVAL:
|
|
this.source += line + ';';
|
|
break;
|
|
// Exec and output
|
|
case this.modes.OUTPUT:
|
|
// Add the exec'd result to the output
|
|
li = line.replace(/\s*;/, '');
|
|
this.source += '_output += (' + li + ' || "");';
|
|
break;
|
|
case this.modes.COMMENT:
|
|
// Do nothing
|
|
break;
|
|
// Literal <%% mode, append as raw output
|
|
case this.modes.LITERAL:
|
|
_addOutput();
|
|
break;
|
|
}
|
|
}
|
|
// In string mode, just add the output
|
|
else {
|
|
_addOutput();
|
|
}
|
|
}
|
|
};
|
|
};
|
|
|
|
|
|
|
|
fleegix.xml = new function () {
|
|
var pat = /^[\s\n\r\t]+|[\s\n\r\t]+$/g;
|
|
var expandToArr = function (orig, val) {
|
|
if (orig) {
|
|
var r = null;
|
|
if (orig instanceof Array == false) {
|
|
r = [];
|
|
r.push(orig);
|
|
}
|
|
else { r = orig; }
|
|
r.push(val);
|
|
return r;
|
|
}
|
|
else { return val; }
|
|
};
|
|
// Parses an XML doc or doc fragment into a JS obj
|
|
// Values for multiple same-named tags a placed in
|
|
// an array -- ideas for improvement to hierarchical
|
|
// parsing from Kevin Faulhaber (kjf@kjfx.net)
|
|
this.parse = function (node, tagName) {
|
|
var obj = {};
|
|
var kids = [];
|
|
var k;
|
|
var key;
|
|
var t;
|
|
var val;
|
|
if (tagName) {
|
|
kids = node.getElementsByTagName(tagName);
|
|
}
|
|
else {
|
|
kids = node.childNodes;
|
|
}
|
|
for (var i = 0; i < kids.length; i++) {
|
|
k = kids[i];
|
|
// Element nodes (blow by the stupid Mozilla linebreak nodes)
|
|
if (k.nodeType == 1) {
|
|
key = k.tagName;
|
|
// Tags with content
|
|
if (k.firstChild) {
|
|
// Node has only one child
|
|
if(k.childNodes.length == 1) {
|
|
t = k.firstChild.nodeType;
|
|
// Leaf nodes - text, CDATA, comment
|
|
if (t == 3 || t == 4 || t == 8) {
|
|
// Either set plain value, or if this is a same-named
|
|
// tag, start stuffing values into an array
|
|
val = k.firstChild.nodeValue.replace(pat, '');
|
|
obj[key] = expandToArr(obj[key], val);
|
|
}
|
|
// Node has a single child branch node, recurse
|
|
else if (t == 1) {
|
|
// Rinse and repeat
|
|
obj[key] = expandToArr(obj[key], this.parse(k.firstChild));
|
|
}
|
|
}
|
|
// Node has children branch nodes, recurse
|
|
else {
|
|
// Rinse and repeat
|
|
obj[key] = expandToArr(obj[key], this.parse(k));
|
|
}
|
|
}
|
|
// Empty tags -- create an empty entry
|
|
else {
|
|
obj[key] = expandToArr(obj[key], null);
|
|
}
|
|
}
|
|
}
|
|
return obj;
|
|
};
|
|
// Create an XML document from string
|
|
this.createDoc = function (str) {
|
|
// Mozilla
|
|
if (typeof DOMParser != "undefined") {
|
|
return (new DOMParser()).parseFromString(str, "application/xml");
|
|
}
|
|
// Internet Explorer
|
|
else if (typeof ActiveXObject != "undefined") {
|
|
var doc = XML.newDocument( );
|
|
doc.loadXML(str);
|
|
return doc;
|
|
}
|
|
else {
|
|
// Try loading the document from a data: URL
|
|
// Credits: Sarissa library (sarissa.sourceforge.net)
|
|
var url = "data:text/xml;charset=utf-8," + encodeURIComponent(str);
|
|
var request = new XMLHttpRequest();
|
|
request.open("GET", url, false);
|
|
request.send(null);
|
|
return request.responseXML;
|
|
}
|
|
};
|
|
// Returns a single, top-level XML document node
|
|
// Ideal for grabbing embedded XML data from a page
|
|
// (i.e., XML 'data islands')
|
|
this.getXMLDoc = function (id, tagName) {
|
|
var arr = [];
|
|
var doc = null;
|
|
if (document.all) {
|
|
var str = document.getElementById(id).innerHTML;
|
|
doc = new ActiveXObject("Microsoft.XMLDOM");
|
|
doc.loadXML(str);
|
|
doc = doc.documentElement;
|
|
}
|
|
// Moz/compat can access elements directly
|
|
else {
|
|
arr =
|
|
window.document.body.getElementsByTagName(tagName);
|
|
doc = arr[0];
|
|
}
|
|
return doc;
|
|
};
|
|
}; |