toolshed-8.x-1.x-dev/assets/toolshed.es6.js
assets/toolshed.es6.js
// Define the Toolshed object scope.
Drupal.Toolshed = {
/**
* Check if this value is a string or not. Helps to encapsulate a safe way to
* test for string.
*
* @param {*} str
* The variable to check if this is a string.
*
* @return {bool}
* TRUE if the parameter is a string, FALSE otherwise.
*/
isString(str) {
return typeof str === 'string' || str instanceof String;
},
/**
* Simple escaping of RegExp strings.
*
* Does not handle advanced regular expressions, but will take care of
* most cases. Meant to be used when concatenating string to create a
* regular expressions.
*
* @param {string} str
* String to escape.
*
* @return {string}
* String with the regular expression special characters escaped.
*/
escapeRegex(str) {
return str.replace(/[\^$+*?[\]{}()\\]/g, '\\$&');
},
/**
* Helper function to uppercase the first letter of a string.
*
* @param {string} str
* String to transform.
*
* @return {string}
* String which has the first letter uppercased.
*/
ucFirst(str) {
return str.charAt(0).toUpperCase() + str.slice(1);
},
/**
* Transform a string into camel case. It will remove spaces, underscores
* and hyphens, and uppercase the letter directly following them.
*
* @param {string} str
* The string to try to transform into camel case.
*
* @return {string}
* The string transformed into camel case.
*/
camelCase(str) {
return str.replace(/(?:[ _-]+)([a-z])/g, (match, p1) => p1.toUpperCase());
},
/**
* Transforms a string into Pascal case. This is basically the same as
* camel case, except that it will upper case the first letter as well.
*
* @param {string} str
* The original string to transform into Pascal case.
*
* @return {string}
* The transformed string.
*/
pascalCase(str) {
return str.replace(/(?:^|[ _-]+)([a-z])/g, (match, p1) => p1.toUpperCase());
},
/**
* Gets the current page Drupal URL (excluding the query or base path).
*
* @return {string}
* The current internal path for Drupal. This would be the path
* without the "base path", however, it can still be a path alias.
*/
getCurrentPath() {
if (!this.getCurrentPath.path) {
this.getCurrentPath.path = null;
if (drupalSettings.path.baseUrl) {
const regex = new RegExp(`^${this.escapeRegex(drupalSettings.path.baseUrl)}`, 'i');
this.getCurrentPath.path = window.location.pathname.replace(regex, '');
}
else {
throw Error('Base path is unavailable. This usually occurs if getCurrentPath() is run before the DOM is loaded.');
}
}
return this.getCurrentPath.path;
},
/**
* Parse URL query paramters from a URL.
*
* @param {string} url
* Full URL including the query parameters starting with '?' and
* separated with '&' characters.
*
* @return {Object}
* JSON formatted object which has the property names as the query
* key, and the property value is the query value.
*/
getUrlParams(url) {
const params = {};
const [uri] = (url || window.location.search).split('#', 2);
const [, query = null] = uri.split('?', 2);
if (query) {
query.split('&').forEach((param) => {
const matches = /^([^=]+)=(.*)$/.exec(param);
if (matches) {
params[decodeURIComponent(matches[1])] = decodeURIComponent(matches[2]);
}
});
}
return params;
},
/**
* Build a URL based on a Drupal internal path. This function will test
* for the availability of clean URL's and prefer them if available.
* The URL components will be run through URL encoding.
*
* @param {string} rawUrl
* The URL to add the query parameters to. The URL can previously have
* query parameters already include. This will append additional params.
* @param {Object|string} params
* An object containing parameters to use as the URL query. Object
* property keys are the query variable names, and the object property
* value is the value to use for the query.
*
* @return {string}
* The valid Drupal URL based on values passed.
*/
buildUrl(rawUrl, params) {
let url = rawUrl || '';
// leave absolute URL's alone.
if (!(/^([a-z]{2,5}:)?\/\//i).test(url)) {
const baseUrl = (drupalSettings.path.baseUrl ? drupalSettings.path.baseUrl : '/');
url = url.replace(/^[/,\s]+|<front>|([/,\s]+$)/g, '');
url = `${baseUrl}${drupalSettings.path.pathPrefix}${url}`;
}
if (params) {
const paramConcat = (acc, entry) => `${acc}&${encodeURIComponent(entry[0])}=${encodeURIComponent(entry[1])}`;
const qry = this.isString(params) ? params : Object.entries(params).reduce(paramConcat, '').substring(1);
if (qry.length) {
const [base, fragment] = url.split('#', 2);
url = base + (base.indexOf('?') === -1 ? '?' : '&') + qry + (fragment ? `#${fragment}` : '');
}
}
return url;
},
/**
* Create a lambda that can make requests GET to a specified URI.
*
* The resulting high order function will return a Promise encapsulating a
* HTTP request to the URL with the parameters passed, and the XHR reference
* to allow aborting the request or listening for events.
*
* This allows for aborting the request, which at the time of this writing
* "fetch" does not support.
*
* @param {string} uri
* URI to create a request function for.
* @param {object} opts
* Options for how the request should be sent and the expected data
* results should appear.
*
* The following options are available:
* - method: The HTTP method to use when creating the request.
* - format: The expected response format (JSON, HTML, URL encoded, etc...)
* - encoding(optional): Encoding of content for POST and PUT requests.
*
* @return {function(query:object):Promise|function(content, query:object):Promise}
* A lambda that creates a request to specified URI with
* passed in URL query params.
*
* If the request method is POST or PUT then the resulting lambda expects
* a content and query parameter that contains the request body and
* aditional URI query parameters respectively. GET and other methods are
* not expecting to send data, and therefore exclude the content parameter.
*
* The lamba returns a Promise which can be used to process the
* results or errors.
*/
createRequester(uri, opts = {}) {
const ts = this;
opts = {
method: 'GET',
format: 'json',
encoding: 'urlencoded',
...opts,
};
// Only needed for legacy support because IE 11 and older does not handle
// the JSON response type correctly and just returns a text response.
const formatResponse = (resp) => (ts.isString(resp) && opts.format === 'json') ? JSON.parse(resp) : resp;
if (opts.method === 'POST' || opts.method === 'PUT') {
let bodyType;
let bodyFormatter;
// Default to the same data text encoding as the response format.
switch (opts.encoding) {
case 'json':
bodyType = 'application/json';
bodyFormatter = JSON.stringify;
break;
case 'html':
case 'urlencoded':
default:
bodyType = 'application/x-www-form-urlencoded';
bodyFormatter = (data) => Object.entries(data).reduce((acc, [key, val]) => `${acc}&${encodeURIComponent(key)}=${encodeURIComponent(val)}`, '').substring(1);
}
return (content, query) => {
const xhr = new XMLHttpRequest();
const promise = new Promise((resolve, reject) => {
xhr.open(opts.method, ts.buildUrl(uri, query), true);
xhr.responseType = opts.format;
xhr.onreadystatechange = function onStateChange() {
if (this.readyState === XMLHttpRequest.DONE) {
if (this.status === 200) resolve(formatResponse(this.response));
else reject(new Error(`${this.status}: ${this.statusText}`));
}
};
// Unable to contact the server or no response.
xhr.onerror = () => reject(new Error('Unable to connect'));
xhr.onabort = () => reject(new Error('Cancelled'));
xhr.ontimeout = () => reject(new Error('Timeout'));
// Convert parameters into URL encoded values for returning.
if (content instanceof FormData) {
xhr.send(content);
}
else {
xhr.setRequestHeader('Content-Type', bodyType);
xhr.send(ts.isString(content) ? content : bodyFormatter(content));
}
});
return { promise, xhr };
};
}
// Basic GET or HEAD HTTP requests.
return (query) => {
const xhr = new XMLHttpRequest();
const promise = new Promise((resolve, reject) => {
xhr.open(opts.method, ts.buildUrl(uri, query), true);
xhr.responseType = opts.format;
xhr.onload = () => {
if (xhr.status === 200) resolve(formatResponse(xhr.response));
else reject(new Error(`${xhr.status}: ${xhr.statusText}`));
};
// Unable to contact the server or no response.
xhr.onerror = () => reject(new Error('Unable to connect'));
xhr.onabort = () => reject(new Error('Cancelled'));
xhr.ontimeout = () => reject(new Error('Timeout'));
xhr.send();
});
return { promise, xhr };
};
},
/**
* Send a Request to URI with provided parameters and return Promise.
*
* @param {string} uri
* URI to build the request for.
* @param {array|null} params
* Parameters to include when making the request.
* @param {object} opts
* Same set of options as for createRequester() function.
*
* @return {Promise}
* Promise wrapping the HTTP request.
*
* @see Drupal.Toolshed.createRequester()
*/
sendRequest(uri, params, opts = { }) {
const { promise } = this.createRequester(uri, opts)(params);
return promise;
},
/**
* Utility function used to find an object based on a string name.
*
* @param {string} name
* Fully qualified name of the object to fetch.
*
* @return {Object}
* the object matching the name, or NULL if it cannot be found.
*/
getObject(name) {
if (!(name && name.split)) return null;
const fetchObj = (obj, items) => {
const part = items.shift();
if (obj[part]) return items.length ? fetchObj(obj[part], items) : obj[part];
return null;
};
return fetchObj(window, name.split('.'));
},
/**
* Apply a callback to DOM elements with a specified CSS class name.
*
* @param {Element} context
* The DOM Element which either has the class or should be searched within
* for elements with the class name.
* @param {string} className
* The class name to search for.
* @param {Function} callback
* The callback function to apply to all the matching elements. The
* callback should accept the DOM element as its single parameter.
* @param {string} [once=null]
* If a non-null string then use this string to as a class name to
* indicate if this callback has been called previously on these elemnts.
*/
walkByClass(context, className, callback, once = null) {
const items = context.classList && context.classList.contains(className)
? [context] : context.getElementsByClassName(className);
this._applyToElements(items, callback, once);
},
/**
* Apply a callback to all DOM elements that match a selector query.
*
* @param {Element} context
* The DOM Element which either matches the selector or should be searched
* within for elements which match the selector.
* @param {string} query
* The select query to use in order to locate elements to apply the
* callback function to.
* @param {Function} callback
* The callback function to apply to all the matching elements. The
* callback should accept the DOM element as its single parameter.
* @param {string} [once=null]
* If a non-null string then use this string to as a class name to
* indicate if this callback has been called previously on these elemnts.
*/
walkBySelector(context, query, callback, once = null) {
const items = context.matches && context.matches(query)
? [context] : context.querySelectorAll(query);
this._applyToElements(items, callback, once);
},
/**
* A browser compliant method for applying a function to an iterable object.
*
* NOTE: Though the underlying call to Array.foreach() method supports
* additional parameters such as the index and the iterable object, this
* method could be applying to either a NodeList or a HTMLCollection depending
* on how we got here and it shouldn't be assumed the index is meaningful or
* can be relied on, especially in cases where elements might get removed.
*
* @param {Iterable<Element>} elements
* An iterable list of HTML Elements to apply the callback to. If a value
* is assigned to once, the once class name is checked before applying
* the callback to the element.
* @param {Function} callback
* A callback to apply to each of the DOM elements.
* @param {[type]} [once=null]
* A class name to check for, and apply to the elements to ensure they
* do not get processed multiple times with the same "once" value.
*/
_applyToElements(elements, callback, once = null) {
if (once) {
const origFunc = callback;
callback = (item) => {
if (!item.classList.contains(once)) {
item.classList.add(once);
return origFunc(item);
}
};
}
Array.prototype.forEach.call(elements, callback);
},
};
