forked from sent/waves
1000 lines
43 KiB
JavaScript
1000 lines
43 KiB
JavaScript
// jshint forin:true, noarg:true, noempty:true, eqeqeq:true, bitwise:true, onevar:true, strict:true, undef:true, unused:strict, curly:true, browser:true, evil:true, node:true
|
|
|
|
/* global Cookies */
|
|
|
|
var G = (function ()
|
|
{
|
|
"use strict";
|
|
|
|
var G = {
|
|
remove: function remove(arr, i, order_irrelevant)
|
|
{
|
|
var len = arr.length;
|
|
|
|
/// Handle negative numbers.
|
|
if (i < 0) {
|
|
i = len + i;
|
|
}
|
|
|
|
/// If the last element is to be removed, then all we need to do is pop it off.
|
|
///NOTE: This is always the fastest method and it is orderly too.
|
|
if (i === len - 1) {
|
|
arr.pop();
|
|
/// If the second to last element is to be removed, we can just pop off the last one and replace the second to last one with it.
|
|
///NOTE: This is always the fastest method and it is orderly too.
|
|
/// Or can use we the faster (but unorderly) method?
|
|
} else if (i === len - 2 || order_irrelevant) {
|
|
if (i >= 0 && i < len) {
|
|
/// This works by popping off the last array element and using that to replace the element to be removed.
|
|
arr[i] = arr.pop();
|
|
}
|
|
} else {
|
|
/// The first element can be quickly shifted off.
|
|
///NOTE: This is faster than splice() but slower than pop().
|
|
if (i === 0) {
|
|
arr.shift();
|
|
/// Ignore numbers that are still negative.
|
|
///NOTE: By default, if a number is below the total array count (e.g., G.remove([0,1], -3)), splice() will remove the first element.
|
|
/// This behavior is undesirable because it is unexpected.
|
|
} else if (i > 0) {
|
|
/// Use the orderly, but slower, splice method.
|
|
arr.splice(i, 1);
|
|
}
|
|
}
|
|
},
|
|
|
|
convert_currency: function convert_currency(money, currency, rates)
|
|
{
|
|
rates = rates || G.exchange_rates;
|
|
|
|
///NOTE: USD is the default.
|
|
if (currency && currency.abr !== "USD" && rates && rates[currency.abr]) {
|
|
/// Add the 2% exchange fee.
|
|
money = (money * 1.02) * rates[currency.abr];
|
|
}
|
|
|
|
return money;
|
|
},
|
|
|
|
/**
|
|
* cents (integer) The amount of money in cents
|
|
* free (string) (optional) The response if 0 (e.g., "FREE!")
|
|
* currenct (object) (optional) An object describing the currency (e.g., {abr: "USD", sym_before: "$", sym_after: ""})
|
|
*/
|
|
format_money: function format_money(cents, free, currency, rates)
|
|
{
|
|
var money = cents / 100;
|
|
|
|
rates = rates || G.exchange_rates;
|
|
|
|
if (currency && rates && rates[currency.abr]) {
|
|
money = G.convert_currency(money, currency, rates);
|
|
} else {
|
|
/// Clear currency so that that we make sure to default back to USD ($).
|
|
currency = null;
|
|
}
|
|
|
|
if (isNaN(money)) {
|
|
money = 0;
|
|
}
|
|
|
|
if (money % 1 !== 0) {
|
|
money = money.toFixed(2);
|
|
}
|
|
|
|
if (money === 0 && typeof free !== "undefined") {
|
|
return free;
|
|
}
|
|
|
|
if (currency) {
|
|
return currency.sym_before + money + currency.sym_after;
|
|
} else {
|
|
return "$" + money;
|
|
}
|
|
},
|
|
|
|
rand: function rand(min, max)
|
|
{
|
|
return Math.floor(Math.random() * (max - min + 1)) + min;
|
|
},
|
|
|
|
is_object: function is_object(mixed)
|
|
{
|
|
return mixed instanceof Object && !(mixed instanceof Array);
|
|
},
|
|
|
|
/**
|
|
* Safely parse JSON.
|
|
*/
|
|
parse_json: function parse_json(str)
|
|
{
|
|
try {
|
|
return JSON.parse(str);
|
|
} catch (e) {}
|
|
},
|
|
loop: function loop(arr, done, oneach, always_async)
|
|
{
|
|
var len,
|
|
fakesetImmediate_count = 0,
|
|
delay_func = typeof setImmediate === "function" ? setImmediate : function fakesetImmediate(func)
|
|
{
|
|
fakesetImmediate_count += 1;
|
|
if (always_async || fakesetImmediate_count >= 200) {
|
|
fakesetImmediate_count = 0;
|
|
setTimeout(func, 0);
|
|
} else {
|
|
func();
|
|
}
|
|
};
|
|
|
|
/// Optionally take an object like {oneach: function (el, next, i) {}, done: function (err) {}}
|
|
if (done && typeof done === "object") {
|
|
oneach = done.oneach;
|
|
always_async = done.always_async;
|
|
///NOTE: This must be last.
|
|
done = done.done;
|
|
}
|
|
|
|
if (!Array.isArray(arr)) {
|
|
if (done) {
|
|
done(new Error("Not an array."));
|
|
}
|
|
return;
|
|
}
|
|
|
|
len = arr.length;
|
|
|
|
delay_func(function ()
|
|
{
|
|
(function loop(i)
|
|
{
|
|
if (i >= len) {
|
|
if (done) {
|
|
return done();
|
|
}
|
|
return;
|
|
}
|
|
|
|
oneach(arr[i], function next()
|
|
{
|
|
delay_func(function ()
|
|
{
|
|
loop(i + 1);
|
|
});
|
|
}, i);
|
|
}(0));
|
|
});
|
|
},
|
|
escape_html: function escape_html(str)
|
|
{
|
|
return String(str).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
},
|
|
supplant: function supplant(obj, template)
|
|
{
|
|
/// Match all matching curly brackets, and send them to the function.
|
|
return template.replace(/{([^{}]+)}/g, function onreplace(whole, inside)
|
|
{
|
|
var data = obj[inside];
|
|
return typeof data !== "undefined" ? data : whole;
|
|
});
|
|
}
|
|
|
|
};
|
|
///TODO: Depriciate and remove.
|
|
G.get_random_int = G.rand;
|
|
///TODO: Depriciate and remove.
|
|
G.array_remove = G.remove;
|
|
///TODO: Depriciate and remove.
|
|
G.async_loop = G.loop;
|
|
|
|
/// Is this (probably) a browser?
|
|
if (typeof window === "object") {
|
|
/// Is there a DOM?
|
|
if (typeof window.document === "object") {
|
|
G.cde = function create_dom_el(type, properties, events, children)
|
|
{
|
|
var el = (!type || type === "documentFragment") ? document.createDocumentFragment() : document.createElement(type),
|
|
prop_sub = {
|
|
c: "className",
|
|
t: "textContent",
|
|
};
|
|
|
|
/// Make properties and events optional.
|
|
if (Array.isArray(properties) && !events && !children) {
|
|
children = properties;
|
|
properties = undefined;
|
|
events = undefined;
|
|
} else if (Array.isArray(events) && !children) {
|
|
children = events;
|
|
events = undefined;
|
|
}
|
|
|
|
if (G.is_object(properties)) {
|
|
Object.keys(properties).forEach(function (prop)
|
|
{
|
|
var prop_name = prop_sub[prop] || prop;
|
|
try {
|
|
if (prop === "nofollow" || prop === "noreferrer" || prop === "colspan" || prop === "list" || prop === "for" || prop.indexOf(":") > -1) {
|
|
el.setAttribute(prop_name, properties[prop]);
|
|
} else {
|
|
el[prop_name] = properties[prop];
|
|
}
|
|
} catch (e) {
|
|
console.log(prop);
|
|
console.log(e);
|
|
}
|
|
});
|
|
}
|
|
|
|
if (G.is_object(events)) {
|
|
Object.keys(events).forEach(function (prop)
|
|
{
|
|
/// A psuedo event
|
|
if (prop === "all_on_changes") {
|
|
el.addEventListener("change", events[prop]);
|
|
el.addEventListener("keypress", events[prop]);
|
|
el.addEventListener("keyup", events[prop]);
|
|
} else {
|
|
el.addEventListener(prop, events[prop]);
|
|
}
|
|
});
|
|
}
|
|
|
|
if (Array.isArray(children)) {
|
|
children.forEach(function (child)
|
|
{
|
|
if (typeof child === "object") {
|
|
el.appendChild(child);
|
|
} else {
|
|
el.appendChild(document.createTextNode(child));
|
|
}
|
|
});
|
|
}
|
|
|
|
return el;
|
|
};
|
|
|
|
/// Detect and label mobile devices.
|
|
G.mobile = (function is_it_mobile()
|
|
{
|
|
var mobile = false,
|
|
regex = /android|ipad|iphone|mobi|tablet|linux\sarmv7l/i; /// Android often has "Linux armv7l" as it's platform.
|
|
|
|
if (window.navigator) {
|
|
mobile = regex.test(navigator.userAgent) || regex.test(navigator.platform);
|
|
}
|
|
|
|
/// Add a flag to the HTML class for CSS detection.
|
|
if (mobile && document.documentElement) {
|
|
document.documentElement.classList.add("mobile");
|
|
}
|
|
|
|
return mobile;
|
|
}());
|
|
|
|
|
|
/// Identify the browser.
|
|
G.browser = window.opera ? "o" : /// Test for Opera first since it also is now webkit/blink.
|
|
window.chrome || window.navigator.userAgent.indexOf("WebKit/") >= 0 ? "webkit" :
|
|
window.navigator.userAgent.indexOf("Firefox/") >= 0 ? "moz" :
|
|
/MSIE|Trident/.test(navigator.userAgent) ? "ms" : "";
|
|
|
|
/// Mark for CSS.
|
|
if (G.browser && document.documentElement) {
|
|
document.documentElement.classList.add(G.browser);
|
|
}
|
|
|
|
/// **************************
|
|
/// * End of window.document *
|
|
/// **************************
|
|
}
|
|
|
|
try {
|
|
G.has_localStorage = typeof localStorage.getItem === "function";
|
|
} catch (e) {}
|
|
|
|
G.normalize_mouse_buttons = function normalize_mouse_buttons(e)
|
|
{
|
|
if (e) {
|
|
if (typeof e.which !== "undefined") {
|
|
return e.which;
|
|
}
|
|
if (typeof e.button !== "undefined") {
|
|
if (e.button === 0) {
|
|
return 1;
|
|
} else if (e.button === 1) {
|
|
return 4;
|
|
} else {
|
|
return e.button;
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
if (window.XMLHttpRequest) {
|
|
/**
|
|
* Options
|
|
* method (GET)
|
|
* message
|
|
* timeout (30000)
|
|
* retry (false)
|
|
* retry_interval (2000)
|
|
* csrf (defaults to the _csrf cookie on non-GET requests)
|
|
* headers (an object or array of objects)
|
|
* [{name: "value"}]
|
|
*/
|
|
G.ajax = (function eac()
|
|
{
|
|
function parse_if_json(ajax)
|
|
{
|
|
var res,
|
|
type;
|
|
|
|
if (ajax.readyState === 4) {
|
|
///NOTE: If the request returned a blob, accesing the responseText property throws an error.
|
|
if (ajax.responseType === "blob") {
|
|
res = ajax.response;
|
|
} else {
|
|
/// Chrome throws an error if this is called before it's ready.
|
|
type = ajax.getResponseHeader("Content-Type");
|
|
///NOTE: The header may include a charset.
|
|
if (type && type.indexOf("application/json") > -1) {
|
|
res = G.parse_json(ajax.responseText);
|
|
} else {
|
|
res = ajax.responseText;
|
|
}
|
|
}
|
|
}
|
|
|
|
return res;
|
|
}
|
|
|
|
return function ajax_func(path, options, cb)
|
|
{
|
|
var aborted,
|
|
ajax = new window.XMLHttpRequest(),
|
|
retrying,
|
|
retry_timer,
|
|
query_timer,
|
|
tried_new_csrf_token;
|
|
|
|
/// Make options optional.
|
|
if (typeof cb === "undefined" && typeof options === "function") {
|
|
cb = options;
|
|
options = {};
|
|
}
|
|
|
|
ajax.is_busy = function is_busy()
|
|
{
|
|
return retrying || Boolean(ajax.readyState % 4);
|
|
};
|
|
|
|
ajax.orig_abort = ajax.abort;
|
|
|
|
ajax.abort = function better_abort()
|
|
{
|
|
if (retrying) {
|
|
clearTimeout(retry_timer);
|
|
retrying = false;
|
|
}
|
|
|
|
if (ajax.is_busy()) {
|
|
/// Stop it from retrying from a timeout.
|
|
clearTimeout(query_timer);
|
|
aborted = true;
|
|
ajax.orig_abort();
|
|
}
|
|
|
|
/// Make sure a callback is called.
|
|
if (ajax.onerror) {
|
|
ajax.onerror();
|
|
}
|
|
};
|
|
|
|
function query()
|
|
{
|
|
var method = typeof options.method === "string" ? options.method.toUpperCase() : "GET",
|
|
message = typeof options.message === "object" ? G.make_params(options.message) : options.message,
|
|
timeout = options.timeout || 30000, /// Default to 30 seconds.
|
|
headers = options.headers || [],
|
|
csrf_token,
|
|
post_message,
|
|
csrf_cookie = options.csrf_cookie || "_csrf";
|
|
|
|
aborted = false;
|
|
|
|
function onload()
|
|
{
|
|
var err;
|
|
///NOTE: Really any 200 level request is good, but I don't think anyone ever uses other codes.
|
|
if (ajax.status !== 200) {
|
|
/// Was their (probably) a CRSF token failure and we are handleing CSRF?
|
|
if (ajax.status === 403 && !options.csrf && !tried_new_csrf_token) {
|
|
/// Make sure we don't try this more than once because that won't help.
|
|
tried_new_csrf_token = true;
|
|
/// First, clear the current bad cookie.
|
|
Cookies.expire(csrf_cookie);
|
|
/// Next, try to get another cookie and retry the request.
|
|
///TODO: Make an API or something for this.
|
|
G.ajax("/get_csrf", {}, query);
|
|
/// Stop processing anything else and wait to see if getting a new CSRF token cookie fixes things.
|
|
return;
|
|
}
|
|
|
|
err = {status: ajax.status, aborted: aborted};
|
|
}
|
|
|
|
if (err && options.retry && !aborted) {
|
|
retry_timer = setTimeout(query, options.retry_interval || 2000);
|
|
return;
|
|
}
|
|
|
|
if (cb) {
|
|
/// query() is sent back to let the caller retry if desired.
|
|
/// ajax is also sent back because it can offer extra data, like ajax.status.
|
|
cb(err, parse_if_json(ajax), query, ajax);
|
|
/// Make sure it's not called twice. For example, if both onerror and onload are called.
|
|
cb = null;
|
|
}
|
|
}
|
|
|
|
if (method.toUpperCase() === "GET") {
|
|
/// GET requests need the message appended to the path.
|
|
ajax.open(method, path + (message ? "?" + message : ""));
|
|
} else {
|
|
/// POST requests send the message later on (with .send()).
|
|
ajax.open(method, path);
|
|
post_message = message;
|
|
}
|
|
|
|
if (options.responseType) {
|
|
ajax.responseType = options.responseType;
|
|
}
|
|
|
|
/// Prepare headers.
|
|
if (!Array.isArray(headers)) {
|
|
headers = [headers];
|
|
}
|
|
|
|
/// Set default header.
|
|
headers.push({name: "Content-Type", value: "application/x-www-form-urlencoded"});
|
|
|
|
/// Set CSRF token.
|
|
if (!options.csrf && method !== "GET") {
|
|
csrf_token = Cookies.get(csrf_cookie);
|
|
} else {
|
|
csrf_token = options.csrf;
|
|
}
|
|
if (csrf_token) {
|
|
headers.push({name: "x-csrf-token", value: csrf_token});
|
|
}
|
|
|
|
headers.forEach(function oneach(header)
|
|
{
|
|
ajax.setRequestHeader(header.name, header.value);
|
|
});
|
|
|
|
|
|
ajax.onerror = onload;
|
|
ajax.onload = onload;
|
|
|
|
ajax.send(post_message);
|
|
|
|
if (timeout) {
|
|
query_timer = setTimeout(function timeout()
|
|
{
|
|
ajax.abort();
|
|
}, timeout);
|
|
}
|
|
}
|
|
|
|
options = options || {};
|
|
|
|
query();
|
|
|
|
return ajax;
|
|
};
|
|
}());
|
|
|
|
///TODO: Depriciate and remove.
|
|
G.easy_ajax = G.ajax;
|
|
|
|
/**
|
|
* Load some Javascript and optionally send it some variables from the closure.
|
|
*
|
|
* @example include("/path/to/script.js", {needed_var: var_from_the_closure}, function () {}, 20000, false);
|
|
* @param path (string) The location of the JavaScript to load.
|
|
* @param context (object) (optional) The variable to send to the included JavaScript.
|
|
* @param cb (function) (optional) A function to call after the code has been loaded.
|
|
* @param timeout (number) (optional) How long to wait before giving up on the script to load (in milliseconds).
|
|
* A falsey value (such as 0 or FALSE) disables timing out. (Default is 10,000 milliseconds.)
|
|
* @param retry (boolean) (optional) Whether or not to retry loading the script if a timeout occurs. (Default is TRUE.)
|
|
* @return NULL. Executes code.
|
|
* @todo If the code has already been loaded, simply run the script without re-downloading anything.
|
|
* @todo Determine if it would be better to use a callback function rather than passing context.
|
|
*/
|
|
G.include = (function ()
|
|
{
|
|
/// Store the "this" variable to let the other functions access it.
|
|
var that = this;
|
|
|
|
/**
|
|
* Eval code in a neutral scope.
|
|
*
|
|
* @param code (string) The string to eval.
|
|
* @return The result of the eval'ed code.
|
|
* @note Called when the Ajax request returns successfully.
|
|
* @note This function is used to prevent included code from having access to the variables inside of the function's scope.
|
|
*/
|
|
this.evaler = function (code)
|
|
{
|
|
///NOTE: Since the eval'ed code has access to the variables in this closure, we need to clear out the code variable both as a security caution and
|
|
/// to prevent memory leaks. The following code does just that: (code = ""). However, this also messes up Firebug's debugger.
|
|
return eval(code + (code = ""));
|
|
};
|
|
|
|
/// Prevent any eval'ed code from being able to modify the evaler() function.
|
|
Object.freeze(this);
|
|
|
|
function include_one(path, cb, context, timeout, retry)
|
|
{
|
|
var clean_path = path.replace(/(\?[^?]+)?(\#[^#]+)?$/, "").toLowerCase(),
|
|
fallback;
|
|
|
|
function done()
|
|
{
|
|
clearTimeout(fallback);
|
|
if (cb) {
|
|
cb();
|
|
/// Make sure it can't be called twice.
|
|
cb = null;
|
|
}
|
|
}
|
|
|
|
/// Not all browsers support onload (like Android).
|
|
fallback = setTimeout(done, 3000);
|
|
|
|
if (clean_path.slice(-4) === ".css") {
|
|
///TODO: Check other link tags to see if this has already been added.
|
|
document.getElementsByTagName("head")[0].appendChild(G.cde("link", {
|
|
href: path,
|
|
rel: "stylesheet"
|
|
}, {load: done}
|
|
));
|
|
} else { /// JS
|
|
G.ajax(path, function (err, res)
|
|
{
|
|
/// Evaluate the code in a safe environment.
|
|
/// Before evaluation, add the sourceURL so that debuggers can debug properly be matching the code to the correct file.
|
|
/// See https://blog.getfirebug.com/2009/08/11/give-your-eval-a-name-with-sourceurl/.
|
|
var code = that.evaler(res + "//# sourceURL=" + path);
|
|
|
|
if (err) {
|
|
console.log(err);
|
|
}
|
|
|
|
/// If the eval'ed code is a function, send it the context.
|
|
if (typeof code === "function") {
|
|
code(context);
|
|
}
|
|
|
|
if (cb) {
|
|
cb();
|
|
}
|
|
}, timeout, retry);
|
|
}
|
|
}
|
|
|
|
return function include(path, cb, context, timeout, retry)
|
|
{
|
|
var len;
|
|
|
|
if (!Array.isArray(path)) {
|
|
path = [path];
|
|
}
|
|
|
|
len = path.length;
|
|
|
|
(function loop(i)
|
|
{
|
|
if (i === len) {
|
|
if (cb) {
|
|
cb();
|
|
}
|
|
return;
|
|
}
|
|
|
|
include_one(path[i], function next()
|
|
{
|
|
loop(i + 1);
|
|
}, context, timeout, retry);
|
|
}(0));
|
|
};
|
|
///NOTE: Since this anonymous function would have an undefined "this" variable, we need to use the call() function to specify an empty "this" object.
|
|
/// The "this" object is used to "secure" the code from the eval'ed code using Object.freeze().
|
|
}).call({});
|
|
}
|
|
|
|
G.get_params = function get_params()
|
|
{
|
|
var sep1 = location.search.split(/\&|\?/g),
|
|
sep2,
|
|
params = {},
|
|
i,
|
|
len;
|
|
|
|
len = sep1.length;
|
|
|
|
if (len > 1) {
|
|
///NOTE: Skip the first empty element (it's empty because URL's start with a slash).
|
|
for (i = 1; i < len; i += 1) {
|
|
sep2 = sep1[i].split(/=/);
|
|
sep2[0] = decodeURIComponent(sep2[0]);
|
|
if (sep2[1]) {
|
|
sep2[1] = decodeURIComponent(sep2[1]);
|
|
}
|
|
if (typeof params[sep2[0]] === "undefined") {
|
|
params[sep2[0]] = sep2[1];
|
|
} else {
|
|
if (typeof params[sep2[0]] !== "object") {
|
|
params[sep2[0]] = [params[sep2[0]]];
|
|
}
|
|
params[sep2[0]].push(sep2[1]);
|
|
}
|
|
}
|
|
}
|
|
|
|
return params;
|
|
};
|
|
|
|
G.get_data_from_form = function get_data_from_form(form)
|
|
{
|
|
var data = {arr: [], obj: {}},
|
|
i,
|
|
len,
|
|
value;
|
|
|
|
if (form && form.elements && form.elements.length) {
|
|
///NOTE: HTMLCollections are not real arrays, so there is no forEach().
|
|
len = form.elements.length;
|
|
|
|
for (i = 0; i < len; i += 1) {
|
|
/// Only elements with a name should be retreaved.
|
|
///NOTE: This does store an element with a space (" ") as a name. Is that good?
|
|
if (form.elements[i].name) {
|
|
if (form.elements[i].type === "checkbox") {
|
|
value = form.elements[i].checked ? true : false;
|
|
} else {
|
|
value = form.elements[i].value;
|
|
}
|
|
/// If the element already exists, turn it into an array.
|
|
if (typeof data.obj[form.elements[i].name] === "undefined") {
|
|
data.obj[form.elements[i].name] = value;
|
|
} else {
|
|
if (!Array.isArray(data.obj[form.elements[i].name])) {
|
|
data.obj[form.elements[i].name] = [data.obj[form.elements[i].name]];
|
|
}
|
|
data.obj[form.elements[i].name].push(value);
|
|
}
|
|
data.arr[i] = form.elements[i];
|
|
}
|
|
}
|
|
}
|
|
|
|
return data;
|
|
};
|
|
|
|
G.make_params = function (params)
|
|
{
|
|
var str = "";
|
|
if (params) {
|
|
Object.keys(params).forEach(function oneach(key, i)
|
|
{
|
|
if (i > 0) {
|
|
str += "&";
|
|
}
|
|
str += encodeURIComponent(key);
|
|
if (typeof params[key] !== "undefined") {
|
|
str += "=" + encodeURIComponent(params[key]);
|
|
}
|
|
});
|
|
}
|
|
return str;
|
|
};
|
|
|
|
///NOTE: Even though this doesn't need to be client-side only, Node already has an event system.
|
|
G.events = (function ()
|
|
{
|
|
var func_list = {};
|
|
|
|
return {
|
|
/**
|
|
* Add one or more events to the event cue.
|
|
*
|
|
* @example G.event.attach("contentAddedAbove", function (e) {});
|
|
* @example G.event.attach("contentAddedAbove", function (e) {}, true);
|
|
* @example G.event.attach(["contentAddedAbove", "contentRemovedAbove"], function (e) {});
|
|
* @example G.event.attach(["contentAddedAbove", "contentRemovedAbove"], function (e) {}, true);
|
|
* @example G.event.attach(["contentAddedAbove", "contentRemovedAbove"], function (e) {}, [true, false]);
|
|
* @param name (string || array) The name of the event or an array of names of events.
|
|
* @param func (function) The function to call when the event it triggered.
|
|
* @param once (boolean || array) (optional) Whether or not to detach this function after being executed once. If "name" is an array, then "once" can also be an array of booleans.
|
|
* @return NULL
|
|
* @note If func(e) calls e.stopPropagation(), it will stop further event propagation.
|
|
* @todo Determine the value of adding a run_once property that removes function after the first run.
|
|
*/
|
|
attach: function attach(name, func, once)
|
|
{
|
|
var arr_len,
|
|
i;
|
|
|
|
/// Should the function be attached to multiple events?
|
|
if (name instanceof Array) {
|
|
arr_len = name.length;
|
|
for (i = 0; i < arr_len; i += 1) {
|
|
/// If "once" is an array, then use the elements of the array.
|
|
/// If "once" is not an array, then just send the "once" variable each time.
|
|
this.attach(name[i], func, once instanceof Array ? once[i] : once);
|
|
}
|
|
} else {
|
|
if (typeof func === "function") {
|
|
/// Has a function been previously attached to this event? If not, create a function to handle them.
|
|
if (!func_list[name]) {
|
|
func_list[name] = [];
|
|
}
|
|
/// Since we may remove events while calling them, it's easiest to store the array in reverse.
|
|
func_list[name].unshift({
|
|
func: func,
|
|
once: once
|
|
});
|
|
}
|
|
}
|
|
},
|
|
/**
|
|
* Remove an event from the event cue.
|
|
*
|
|
* @example G.event.detach("contentAddedAbove", function (e) {});
|
|
* @example G.event.detach(["contentAddedAbove", "contentRemovedAbove"], function (e) {}, [true, false]);
|
|
* @example G.event.detach(["contentAddedAbove", "contentRemovedAbove"], function (e) {}, true);
|
|
* @param name (string || array) The name of the event or an array of names of events.
|
|
* @param func (function) The function that was attached to the specified event.
|
|
* @param once (boolean || array) (optional) Whether or not to detach this function after being executed once. If "name" is an array, then "once" can also be an array of booleans.
|
|
*/
|
|
detach: function detach(name, func, once)
|
|
{
|
|
var i;
|
|
|
|
/// Are there multiple events to remove?
|
|
if (name instanceof Array) {
|
|
for (i = name.length - 1; i >= 0; i -= 1) {
|
|
/// If "once" is an array, then use the elements of the array.
|
|
/// If "once" is not an array, then just send the "once" variable each time.
|
|
this.detach(name[i], func, once instanceof Array ? once[i] : once);
|
|
}
|
|
} else if (func_list[name]) {
|
|
for (i = func_list[name].length - 1; i >= 0; i -= 1) {
|
|
///NOTE: Both func and once must match.
|
|
if (func_list[name][i].func === func && func_list[name][i].once === once) {
|
|
G.remove(func_list[name], i);
|
|
/// Since only one event should be removed at a time, we can end now.
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
},
|
|
/**
|
|
* Trigger the functions attached to an event.
|
|
*
|
|
* @param name (string) The name of the event to trigger.
|
|
* @param e (object) The event object sent to the called functions.
|
|
* @return NULL
|
|
*/
|
|
trigger: function trigger(name, e)
|
|
{
|
|
var i,
|
|
stop_propagation;
|
|
|
|
/// Does this event have any functions attached to it?
|
|
if (func_list[name]) {
|
|
if (!G.is_object(e)) {
|
|
/// If the event object was not specificed, it needs to be created in order to attach stopPropagation() to it.
|
|
e = {};
|
|
}
|
|
|
|
/// If an attached function runs this function, it will stop calling other functions.
|
|
e.stopPropagation = function ()
|
|
{
|
|
stop_propagation = true;
|
|
};
|
|
|
|
/// Execute the functions in reverse order so that we can remove them without throwing the order off.
|
|
for (i = func_list[name].length - 1; i >= 0; i -= 1) {
|
|
///NOTE: It would be a good idea to use a try/catch to prevent errors in events from preventing the code that called the
|
|
/// event from firing. However, there would need to be some sort of error handling. Sending a message back to the
|
|
/// server would be a good feature.
|
|
/// Check to make sure the function actually exists.
|
|
if (func_list[name][i]) {
|
|
func_list[name][i].func(e);
|
|
}
|
|
|
|
/// Is this function only supposed to be executed once?
|
|
if (!func_list[name][i] || func_list[name][i].once) {
|
|
G.remove(func_list[name], i);
|
|
}
|
|
|
|
/// Was e.stopPropagation() called?
|
|
if (stop_propagation) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
};
|
|
}());
|
|
|
|
|
|
/**
|
|
* Cookies.js - 1.1.0
|
|
* https:///github.com/ScottHamper/Cookies
|
|
*
|
|
* This is free and unencumbered software released into the public domain.
|
|
*
|
|
* Streamed lined by Greenfield Education.
|
|
*/
|
|
(function (undefined)
|
|
{
|
|
if (typeof window.document !== "object") {
|
|
throw new Error("Cookies.js requires a `window` with a `document` object");
|
|
}
|
|
|
|
var Cookies = function (key, value, options)
|
|
{
|
|
return arguments.length === 1 ?
|
|
Cookies.get(key) : Cookies.set(key, value, options);
|
|
};
|
|
|
|
/// Allows for setter injection in unit tests
|
|
Cookies._document = window.document;
|
|
|
|
/// Used to ensure cookie keys do not collide with
|
|
/// built-in `Object` properties
|
|
Cookies._cacheKeyPrefix = "cookey.";
|
|
|
|
Cookies.defaults = {
|
|
secure: location.protocol === "https:",
|
|
expires: 60 * 60 * 24 * 30 * 9, /// 9 months in milliseconds
|
|
path: "/"
|
|
};
|
|
|
|
Cookies.get = function (key)
|
|
{
|
|
if (Cookies._cachedDocumentCookie !== Cookies._document.cookie) {
|
|
Cookies._renewCache();
|
|
}
|
|
|
|
return Cookies._cache[Cookies._cacheKeyPrefix + key];
|
|
};
|
|
|
|
///NOTE: options.expires is in seconds.
|
|
Cookies.set = function (key, value, options)
|
|
{
|
|
options = Cookies._getExtendedOptions(options);
|
|
options.expires = Cookies._getExpiresDate(value === undefined ? -1 : options.expires);
|
|
|
|
Cookies._document.cookie = Cookies._generateCookieString(key, value, options);
|
|
|
|
return Cookies;
|
|
};
|
|
|
|
Cookies.expire = function (key, options)
|
|
{
|
|
return Cookies.set(key, undefined, options);
|
|
};
|
|
|
|
Cookies._getExtendedOptions = function (options)
|
|
{
|
|
return {
|
|
path: options && options.path || Cookies.defaults.path,
|
|
domain: options && options.domain || Cookies.defaults.domain,
|
|
expires: options && options.expires || Cookies.defaults.expires,
|
|
secure: options && options.secure !== undefined ? options.secure : Cookies.defaults.secure
|
|
};
|
|
};
|
|
|
|
Cookies._isValidDate = function (date)
|
|
{
|
|
return Object.prototype.toString.call(date) === "[object Date]" && !isNaN(date.getTime());
|
|
};
|
|
|
|
Cookies._getExpiresDate = function (expires, now)
|
|
{
|
|
now = now || new Date();
|
|
switch (typeof expires) {
|
|
case "number": expires = new Date(now.getTime() + expires * 1000); break;
|
|
case "string": expires = new Date(expires); break;
|
|
}
|
|
|
|
if (expires && !Cookies._isValidDate(expires)) {
|
|
throw new Error("`expires` parameter cannot be converted to a valid Date instance");
|
|
}
|
|
|
|
return expires;
|
|
};
|
|
|
|
Cookies._generateCookieString = function (key, value, options)
|
|
{
|
|
key = key.replace(/[^#$&+\^`|]/g, encodeURIComponent);
|
|
key = key.replace(/\(/g, "%28").replace(/\)/g, "%29");
|
|
value = (value + "").replace(/[^!#$&-+\--:<-\[\]-~]/g, encodeURIComponent);
|
|
options = options || {};
|
|
|
|
var cookieString = key + "=" + value;
|
|
cookieString += options.path ? ";path=" + options.path : "";
|
|
cookieString += options.domain ? ";domain=" + options.domain : "";
|
|
cookieString += options.expires ? ";expires=" + options.expires.toUTCString() : "";
|
|
cookieString += options.secure ? ";secure" : "";
|
|
|
|
return cookieString;
|
|
};
|
|
|
|
Cookies._getCacheFromString = function (documentCookie)
|
|
{
|
|
var cookieCache = {};
|
|
var cookiesArray = documentCookie ? documentCookie.split("; ") : [];
|
|
|
|
for (var i = 0; i < cookiesArray.length; i += 1) {
|
|
var cookieKvp = Cookies._getKeyValuePairFromCookieString(cookiesArray[i]);
|
|
|
|
if (cookieCache[Cookies._cacheKeyPrefix + cookieKvp.key] === undefined) {
|
|
cookieCache[Cookies._cacheKeyPrefix + cookieKvp.key] = cookieKvp.value;
|
|
}
|
|
}
|
|
|
|
return cookieCache;
|
|
};
|
|
|
|
Cookies._getKeyValuePairFromCookieString = function (cookieString)
|
|
{
|
|
/// "=" is a valid character in a cookie value according to RFC6265, so cannot `split("=")`
|
|
var separatorIndex = cookieString.indexOf("=");
|
|
|
|
/// IE omits the "=" when the cookie value is an empty string
|
|
separatorIndex = separatorIndex < 0 ? cookieString.length : separatorIndex;
|
|
|
|
return {
|
|
key: decodeURIComponent(cookieString.substr(0, separatorIndex)),
|
|
value: decodeURIComponent(cookieString.substr(separatorIndex + 1))
|
|
};
|
|
};
|
|
|
|
Cookies._renewCache = function ()
|
|
{
|
|
Cookies._cache = Cookies._getCacheFromString(Cookies._document.cookie);
|
|
Cookies._cachedDocumentCookie = Cookies._document.cookie;
|
|
};
|
|
|
|
Cookies._areEnabled = function ()
|
|
{
|
|
var testKey = "cookies.js";
|
|
var areEnabled = Cookies.set(testKey, 1).get(testKey) === "1";
|
|
Cookies.expire(testKey);
|
|
return areEnabled;
|
|
};
|
|
|
|
/// Removed enabled check. If you want to know, run Cookies._areEnabled().
|
|
|
|
window.Cookies = Cookies;
|
|
})();
|
|
|
|
/// *****************
|
|
/// * End of window *
|
|
/// *****************
|
|
}
|
|
|
|
return G;
|
|
}());
|
|
|
|
if (typeof module !== "undefined") {
|
|
module.exports = G;
|
|
}
|