//################################################################################################
/** @file Web Input/Output mixin for ish.js
* @mixin ish.io.web
* @author Nick Campbell
* @license MIT
* @copyright 2014-2023, Nick Campbell
*/ //############################################################################################
/*global module, define, global, require */ //# Enable Node globals for JSHint
/*jshint maxcomplexity:9 */ //# Enable max complexity warnings for JSHint
(function () {
'use strict'; //<MIXIN>
function init(core) {
var bServerside = core.config.ish().onServer, //# code-golf
_root = (bServerside ? global : window), //# code-golf
_document = (bServerside ? {} : document), //# code-golf
_null = null, //# code-golf
_undefined /*= undefined*/ //# code-golf
;
//################################################################################################
/** Collection of Web-based functionality.
* @namespace ish.io.web
* @ignore
*/ //############################################################################################
core.oop.partial(core.io, {
web: core.extend(
{
//################################################################################################
/** Collection of Cookie-based functionality.
* @namespace ish.io.web.cookie
* @ignore
*/ //############################################################################################
cookie: function () {
var fnIsomorphic = (bServerside ?
function (oSources) {
var oRequestHeaders = core.resolve(oSources, "request.headers"),
oResponseHeaders = core.resolve(oSources, "response.headers")
;
return {
get: function () {
//# If we have oRequestHeaders, return their .cookie's to the caller
if (core.type.obj.is(oRequestHeaders)) {
return core.type.arr.mk(oRequestHeaders.cookie); //# set-cookie?
}
//# Else we have no oRequestHeaders, so throw the error
else {
throw "io.web.cookie: `request` not provided in `options`.";
}
},
set: function (sName, sValue) {
var a_sCookies, i,
bFound /*= false*/
;
//# If we have oResponseHeaders
if (core.type.obj.is(oResponseHeaders)) {
//# If set-cookie isn't an array, set it as one then collect the a_sCookies
if (!core.type.arr.is(oResponseHeaders["set-cookie"])) {
oResponseHeaders["set-cookie"] = [];
}
a_sCookies = oResponseHeaders["set-cookie"];
//# Traverse the a_sCookies, looking for the passed sName
for (i = 0; i < a_sCookies.length; i++) {
//# If this is the a_sCookies we're looking for, reset it's value to the passed info, flip bFound and fall from the loop
if (cookieName(a_sCookies[i]) === sName) {
a_sCookies[i] = sName + "=" + sValue;
bFound = true;
break;
}
}
//# If the sName wasn't bFound in the a_sCookies, .push in the passed info as a new cookie
if (!bFound) {
a_sCookies.push(sName + "=" + sValue);
}
}
//# Else we have no oResponseHeaders, so throw the error
else {
throw "io.web.cookie: `response` not provided in `options`.";
}
}
};
} :
function () {
return {
get: function() {
return core.type.str.mk(_document.cookie).split(";");
},
set: function (sName, sValue) {
_document.cookie = sName + "=" + sValue;
}
};
}
);
//# Correctly decodes the cookie name from the passed sCookieData
function cookieName(sCookieData) {
var i = sCookieData.indexOf("=");
return sCookieData.substr(0, i).replace(/^\s+|\s+$/g, "");
} //# cookieName
//#########
/** Provides an interface to the cookie data of the passed value.
* @function ish.io.web.cookie
* @param {string} sName Value representing the cookie to retrieve.
* @param {object} [oDefault=undefined] Value representing if the default value of the cookie data if it hasn't been previously set.
* @param {object} [vOptions] Value representing the desired options:
* @param {boolean} [vOptions.path="/"] Value representing if the Querystring's delimiter is to be semicolons (<code>;</code>) rather than ampersands (<code>&</code>).
* @param {boolean} [vOptions.domain] Value representing the domain the cookie is related to.
* @param {boolean} [vOptions.maxAge] Value representing max age in milliseconds the cookie is to be valid for.
* @returns {object} =interface Value representing the following properties:
* @returns {string} =interface.name Value representing the cookie's name.
* @returns {object} =interface.original Value representing the cookie's original data.
* @returns {object} =interface.data Value representing the cookie's data.
* @returns {boolean} =interface.isNew Value representing if the cookie wasn't previously present in the browser's collection.
* @returns {function} =interface.stringify Returns a value representing the cookie data as JSON; <code>stringify()</code>.
* @returns {function} =interface.set Sets the cookie into the browser's collection; <code>set()</code>.
* @returns {function} =interface.rm Removes the cookie from the browser's collection; <code>rm()</code>.
*/ //#####
return function (sName, oDefault, oOptions) {
var oModel,
fnCookies = fnIsomorphic(oOptions),
$returnValue = {
//options: oOptions,
name: sName,
original: _undefined,
data: _undefined,
isNew: true,
stringify: function () {
return JSON.stringify($returnValue.data);
}, //# stringify
set: function () {
var dExpires, sDomain, sPath, sSameSite, iMaxAge;
//# Ensure the .path and .maxAge are valid or defaulted to root and .seconds7Days respectively
sPath = oOptions.path = core.type.str.mk(oOptions.path, "/");
sSameSite = oOptions.sameSite = core.type.str.mk(oOptions.sameSite, "Lax");
sDomain = oOptions.domain = core.type.str.mk(oOptions.domain);
iMaxAge = oOptions.maxAge = core.type.int.mk(oOptions.maxAge, (1000 * 60 * 60 * 24 * 7));
//# If this is not a session cookie, setup dExpires
if (iMaxAge > 0) {
dExpires = new Date();
dExpires.setSeconds(dExpires.getSeconds() + iMaxAge);
}
//# .encodeURIComponent the .string, set the max-age and path and toss it into the .cookie collection
//# NOTE: http://www.w3.org/Protocols/rfc2109/rfc2109 - The Domain attribute specifies the domain for which the cookie is valid. An explicitly specified domain must always start with a dot.
fnCookies.set(sName, encodeURIComponent($returnValue.stringify()) +
"; path=" + sPath +
(iMaxAge > 0 ? "; max-age=" + iMaxAge : "") +
(dExpires ? "; expires=" + dExpires.toUTCString() : "") +
(sDomain ? '; domain=.' + sDomain : "") +
(sSameSite ? '; SameSite=' + sSameSite : "") +
"; ");
//# Flip .isNew to false (as its now present on the browser)
$returnValue.isNew = false;
}, //# set
rm: function () {
//# Set the max-age and path and toss it into the .cookie collection
//$returnValue.data = _undefined;
fnCookies.set(sName, "; expires=Thu, 01 Jan 1970 00:00:01 GMT;" + " path=" + oOptions.path + "; ");
} //# rm
}
;
//# Locates the document.cookie for the passed sName
//# NOTE: This is placed in a function so that the a_sCookies array will be dropped after execution
function find() {
var i, j,
a_sCookies = fnCookies.get()
;
//# Loop thru the values in .cookie looking for the passed sName, setting $returnValue.original if it's found
for (i = 0; i < a_sCookies.length; i++) {
j = a_sCookies[i].indexOf("=");
if (cookieName(a_sCookies[i]) === sName) {
$returnValue.original = decodeURIComponent(a_sCookies[i].substr(j + 1));
oModel = core.type.obj.mk($returnValue.original);
return true;
}
}
return false;
} //# find
//#
oOptions = core.type.obj.mk(oOptions);
$returnValue.options = oOptions;
//# If a seemingly valid sName was passed
if (!core.type.str.is(sName) || sName === "") {
throw (core.config.ish().target + ".io.web.cookie: A [String] value must be provided for the cookie name.");
}
//# If we can .find a current value, reset .isNew to false
if (find()) {
$returnValue.isNew = false;
}
//# Else this .isNew cookie, so if the caller passed in a valid oDefault object, reset our oModel to it
else if (core.type.obj.is(oDefault)) {
oModel = oDefault;
}
//#
$returnValue.data = oModel;
return $returnValue;
};
}(), //# core.web.cookie
//################################################################################################
/** Collection of QueryString-based functionality.
* @namespace ish.io.web.queryString
* @ignore
*/ //############################################################################################
queryString: function () {
var $queryString,
bLoaded = false
;
//# serializes the passed oData (up to 3-dimensions)
function serialize(oData, vOptions) {
var key, value, subkey, i,
oOptions = (core.type.obj.is(vOptions) ?
vOptions :
{ useSemicolon: vOptions === true, encodeURI: true }
),
e = (oOptions.encodeURI === false ?
function (s) { return s; } :
encodeURIComponent
),
delimiter = (oOptions.useSemicolon ?
"; " :
"&"
),
returnVal = ''
;
//# Traverse each key within the passed oData
for (key in oData) {
if (oData.hasOwnProperty(key)) {
value = oData[key];
key = e(key.trim());
//# If the current value is null or undefined, record a null-string value
if (!core.type.is.value(value)) {
returnVal += key + "=" + delimiter;
}
//# Else if the current key is a pseudo-Array object
else if (core.type.obj.is(value)) {
//# Traverse each subkey in the current value
for (subkey in value) {
//# If the current subkey is an Array, traverse it's Array outputting each of its values individually
if (value[subkey] instanceof Array) {
for (i = 0; i < value[subkey].length; i++) {
returnVal += key + "[" + e(subkey) + "]=" + e(value[subkey][i]) + delimiter;
}
}
//# Else treat the current subkey as a string
else {
returnVal += key + "[" + e(subkey) + "]=" + e(value[subkey]) + delimiter;
}
}
}
//# Else if the current key is an Array, traverse it's Array outputting each of its values individually
else if (Array.isArray(value)) {
for (i = 0; i < value.length; i++) {
returnVal += key + "=" + e(value[i]) + delimiter;
}
}
//# Else if the current value isn't a function, treat the current key/value as a string
else if (core.type.fn.is(value)) {
returnVal += key + "=" + e(value) + delimiter;
}
}
}
return returnVal.trim().substr(0, returnVal.length - 1);
} //# serialize
//# Parses the passed valueToParse into a model (up to 3-dimensions deep)
//# Based on: http://jsbin.com/adali3/2/edit via http://stackoverflow.com/a/2880929/235704
//# NOTE: All keys are .toLowerCase'd to make them case-insensitive(-ish) with the exception of a sub-keys named 'length', which are .toUpperCase'd
//# Supports: key=val1;key=val2;newkey=val3;obj=zero;obj=one;obj[one]=1;obj[two]=2;obj[Length]=long;obj[2]=mytwo;obj=two;obj[2]=myothertwo;obj=three
function deserialize(valueToParse) {
//# Setup the required local variables (using annoyingly short names which are documented in the comments below)
//# NOTE: Per http://en.wikipedia.org/wiki/Query_string#Structure and the W3C both & and ; are legal delimiters for a query string, hence the RegExp below
var b, e, k, p, sk, v, r = {},
d = function (v) {
try {
return decodeURIComponent(v).replace(/\+/g, " ") + '';
} catch (e) {
return (v + '').replace(/\+/g, " ") + '';
}
}, //# d(ecode) the v(alue)
s = /([^&;=]+)=?([^&;]*)/g //# original regex that does not allow for ; as a delimiter: /([^&=]+)=?([^&]*)/g
;
//# p(ush) re-implementation
p = function (a) {
//# Traverse the passed arguments, skipping the first as it's our a(rray)
for (var i = 1, j = arguments.length; i < j; i++) {
//# If there is already an entry at the "new" index, .push the current arguments into the sk(sub-key)'s Array (or make one if it doesn't already exist)
if (a[a.length]) {
if (core.type.arr.is(a[a.length])) {
a[a.length].push(arguments[i]);
}
else {
a[a.length] = new Array(a[a.length], arguments[i]);
}
a.length++;
}
//# Else this is a new entry, so just set the arguments
else {
a[a.length++] = arguments[i];
}
}
};
//# While we still have key-value e(ntries) in the valueToParse via the s(earch regex)...
while ( (e = s.exec(valueToParse)) /* == truthy */) { //# while((e = s.exec(valueToParse)) !== _null) {
//# Collect the open b(racket) location (if any) then set the d(ecoded) v(alue) from the above split key-value e(ntry)
b = e[1].indexOf("[");
v = d(e[2]);
//# As long as this is NOT a hash[]-style key-value e(ntry)
if (b < 0) {
//# d(ecode) the simple k(ey)
k = d(e[1]).trim();
//# If the k(ey) already exists, we need to transform it into a standard Array
if (r[k]) {
//# If the k(ey) is already an Array, .push the v(alue) into it
if (r[k] instanceof Array) {
r[k].push(v);
}
//# Else if it is a pseudo-Array object, p(ush) the v(alue) in
else if (typeof r[k] === 'object') {
p(r[k], v);
}
//# Else the k(ey) must be a single value, so transform it into a standard Array
else {
r[k] = new Array(r[k], v);
}
}
//# Else this is a new k(ey), so just add the v(alue) into the r(eturn value) as a single value
else {
r[k] = (v ? v : _null);
}
}
//# Else we've got ourselves a hash[]-style key-value e(ntry)
else {
//# Collect the .trim'd and d(ecoded) k(ey) and sk(sub-key) based on the b(racket) locations
k = d(e[1].slice(0, b)).trim();
sk = d(e[1].slice(b + 1, e[1].indexOf("]", b))).trim();
//# If the sk(sub-key) is the reserved 'length', then .toUpperCase it and .warn the developer
if (sk === 'length') {
sk = sk.toUpperCase();
core.type.fn.call(core.resolve(core, "io.log.warn"), null, "'length' is a reserved name and cannot be used as a sub-key. Your sub-key has been changed to 'LENGTH'.");
}
//# If the k(ey) isn't already a pseudo-Array object
if (!core.type.obj.is(r[k]) || core.type.arr.is(r[k])) {
//# If the k(ey) is an Array
if (core.type.arr.is(r[k])) {
var i, a = r[k];
//# Reset the k(ey) as a pseudo-Array object, then copy the previous a(rray)'s values in at their numeric indexes
//# NOTE:
r[k] = { length: a.length };
for (i = 0; i < a.length; i++) {
r[k][i] = a[i];
}
}
//# Else the k(ey) is a single value so transform it into a pseudo-Array object with the current value at index 0
else {
r[k] = { 0: r[k], length: 1 };
}
}
//# If we have a sk(sub-key)
if (sk) {
//# If the sk(sub-key) already exists, .push the v(alue) into the sk(sub-key)'s Array (or make one if it doesn't already exist)
if (r[k][sk]) {
if (core.type.arr.is(r[k][sk])) {
r[k][sk].push(v);
}
else {
r[k][sk] = new Array(r[k][sk], v);
}
}
//# Else the sk(sub-key) is new, so just set the v(alue)
else { r[k][sk] = v; }
}
//# Else p(ush) the v(alue) into the k(ey)'s pseudo-Array object
else { p(r[k], v); }
}
}
//# Return the r(eturn value)
return r;
} //# deserialize
//#
function getQS() {
//#
if (bServerside) {
throw "No request.url has been provided.";
}
//#
else {
return _root.location.search.substr(1);
}
} //# getQS
//#
function get(sKey, bCaseInsensitive) {
var vReturnVal;
//# Ensure the cached $queryString is setup
if (!bLoaded) {
$queryString = deserialize(getQS());
}
//# If there were no arguments, return the cached $queryString
if (arguments.length === 0) {
vReturnVal = $queryString;
}
//# Else we need to .get the key from the $queryString
else {
vReturnVal = (bCaseInsensitive ? core.type.obj.get($queryString, sKey) : $queryString[sKey]);
}
return vReturnVal;
} //# get
//#########
/** Retrieves the passed value from the Querystring data.
* @$note If <code>ish.io.web.queryString.parse</code> was not called previously, the Querystring is implicitly parsed prior to the passed value being retrieved. This will result in an error on the server-side as the Querystring cannot be automatically collected.
* @function ish.io.web.queryString.!
* @$aka ish.io.web.queryString.get
* @param {string} sKey Value representing the Querystring key to retrieve.
* @param {boolean} [bCaseInsensitive=false] Value representing if the passed value is to be retrieved from the Querystring data in a case-insensitive manor.
* @returns {variant} Value representing the data for the passed value.
*/ //#####
return core.extend(get, {
//# TODO: Remove
ser: {
ize: serialize,
de: deserialize
}, //# ser
//# NOTE: AKA'd above
get: get,
//#########
/** Parses the passed value into a Javascript object representing the Querystring data.
* @$note All keys are .toLowerCase'd to make them case-insensitive(-ish) with the exception of a sub-keys named 'length', which are .toUpperCase'd.
* @$note Supports up to 3-dimensions: <code>key=val1;key=val2;newkey=val3;obj=zero;obj=one;obj[one]=1;obj[two]=2;obj[Length]=long;obj[2]=mytwo;obj=two;obj[2]=myothertwo;obj=three</code>
* @function ish.io.web.queryString:parse
* @param {string} [sUrl=location.search] Value representing the Querystring data to parse.
* @returns {object} Value representing the Querystring data.
* @see {@link http://stackoverflow.com/a/2880929/235704|StackOverflow.com}
* @see {@link http://jsbin.com/adali3/2/edit|JSBin.com}
*/ //#####
parse: function (sUrl) {
var oReturnVal, i;
//# Ensure the passed sUrl .is .str then locate the ?
sUrl = core.type.str.mk(
(arguments.length > 0 ? sUrl : getQS())
);
i = sUrl.indexOf("?");
//# If the sUrl has a query string, .deserialize it into our oReturnVal
if (i > -1) {
bLoaded = true;
oReturnVal = deserialize(sUrl.substr(i + 1));
}
$queryString = oReturnVal || {};
return $queryString;
}, //# parse
//#########
/** Converts the passed value to a Querystring.
* @function ish.io.web.queryString:stringify
* @param {object} oData Value representing the data to serialize into a Querystring.
* @param {object} [vOptions] Value representing the desired options:
* @param {boolean} [vOptions.useSemicolon=false] Value representing if the Querystring's delimiter is to be semicolons (<code>;</code>) rather than ampersands (<code>&</code>).
* @param {boolean} [vOptions.encodeURI=true] Value representing if <code>encodeURIComponent</code> is to be used.
* @returns {string} Value representing the Querystring data.
*/ //#####
stringify: serialize
});
}(), //# io.web.queryString
//#########
/** Parses the passed value into its URL components.
* @function ish.io.web.url
* @param {string} sUrl Value representing the URL to parse.
* @returns {object} Value representing the URL components.
* @see {@link https://www.sitepoint.com/url-parsing-isomorphic-javascript/|Sitepoint.com}
* @see {@link http://davidwalsh.name/essential-javascript-functions|David Walsh}
*/ //#####
url: function () {
var $lib, fnReturnVal;
//#
function consistentInterface(oLibResult) {
oLibResult = core.type.obj.mk(oLibResult);
return {
href: oLibResult.href,
protocol: oLibResult.protocol,
host: oLibResult.host,
hostname: oLibResult.hostname,
port: oLibResult.port,
pathname: oLibResult.pathname,
search: oLibResult.search,
hash: oLibResult.hash,
username: oLibResult.username,
password: oLibResult.password,
//auth: !!(oLibResult.username || oLibResult.password),
origin: oLibResult.origin
};
} //# consistentInterface
//# If we are running bServerside (or possibly have been required as a CommonJS module)
if (bServerside) {
$lib = require('url');
fnReturnVal = function (sUrl) {
return consistentInterface($lib.parse(sUrl));
};
}
//# Else we are running in the browser, so we need to setup the _document-based features
else {
//var matches = url.match(/^https?\:\/\/([^\/?#]+)(?:[\/?#]|$)/i);
//var domain = matches && matches[1];
fnReturnVal = function (sUrl) {
$lib = $lib || _document.createElement('a');
$lib.href = sUrl;
return consistentInterface($lib);
};
}
return fnReturnVal;
}() //# io.web.url
},
//# If we aren't running bServerside (or possibly have been required as a CommonJS module), add in the browser-specific stuff
(bServerside ? _null : {
//# Aliases to window.localstorage and window.sessionStorage with automajic stringification of non-string values
//################################################################################################
/** Collection of <code>window.*Storage</code>-based functionality.
* @namespace ish.io.web.storage
* @$clientsideonly
* @ignore
*/ //############################################################################################
storage: function () {
var window_localStorage = _root.localStorage, //# code-golf
window_sessionStorage = _root.sessionStorage //# code-golf
;
function get(sKey, bSession) {
var sValue = (bSession ? window_sessionStorage : window_localStorage).getItem(sKey);
return core.type.obj.mk(sValue, sValue);
}
//#########
/** Retrieves the passed value from the browser's <code>window.*Storage</code> data.
* @function ish.io.web.storage.!
* @$aka ish.io.web.storage.get
* @$clientsideonly
* @param {string} sKey Value representing the browser's <code>window.*Storage</code> key to retrieve.
* @param {boolean} [bSession=false] Value representing if the passed value is to be retrieved from the browser's <code>window.sessionStorage</code> data rather than <code>window.localStorage</code>.
* @returns {variant} Value representing the data for the passed value.
*/ //#####
return core.extend(get, {
//#########
/** Sets the passed value into the browser's <code>window.*Storage</code> data.
* @function ish.io.web.storage:set
* @$clientsideonly
* @param {string} sKey Value representing the key to set.
* @param {variant} vValue Value representing the value to set.
* @param {boolean} [bSession=false] Value representing if the passed value is to be set into the browser's <code>window.sessionStorage</code> data rather than <code>window.localStorage</code>.
*/ //#####
set: function (sKey, vValue, bSession) {
var sValue = (core.type.obj.is(vValue) ? JSON.stringify(vValue) : core.type.str.mk(vValue));
(bSession ? window_sessionStorage : window_localStorage).setItem(sKey, sValue);
},
//# NOTE: AKA'd above
get: get,
//#########
/** Removes the passed value from the browser's <code>window.*Storage</code> data.
* @function ish.io.web.storage:rm
* @$clientsideonly
* @param {string} sKey Value representing the key to remove.
* @param {boolean} [bSession=false] Value representing if the passed value is to be removed from the browser's <code>window.sessionStorage</code> data rather than <code>window.localStorage</code>.
*/ //#####
rm: function (sKey, bSession) {
(bSession ? window_sessionStorage : window_localStorage).removeItem(sKey);
},
//#########
/** Clears the browser's <code>window.*Storage</code> data.
* @function ish.io.web.storage:clear
* @$clientsideonly
* @param {boolean} [bSession=false] Value representing if the passed value is to be cleared from the browser's <code>window.sessionStorage</code> data rather than <code>window.localStorage</code>.
*/ //#####
clear: function (bSession) {
(bSession ? window_sessionStorage : window_localStorage).clear();
}
});
}() //# web.storage
})
)
}); //# core.io.web
//# .fire the plugin's loaded event
core.io.event.fire("ish.io.web");
//# Return core to allow for chaining
return core;
}
//# If we are running server-side
//# NOTE: Compliant with UMD, see: https://github.com/umdjs/umd/blob/master/templates/returnExports.js
//# NOTE: Does not work with strict CommonJS, but only CommonJS-like environments that support module.exports, like Node.
if (typeof module === 'object' && module.exports) { //if (typeof module !== 'undefined' && this.module !== module && module.exports) {
module.exports = init;
}
//# Else if we are running in an .amd environment, register as an anonymous module
else if (typeof define === 'function' && define.amd) {
define([], init);
}
//# Else we are running in the browser, so we need to setup the _document-based features
else {
return init(window.head.ish || document.querySelector("SCRIPT[ish]").ish);
}
//</MIXIN>
}());