// Package WCom.Util
if (!window.WCom) window.WCom = {};
WCom.Util = (function() {
const _esc = encodeURIComponent;
const _createQueryString = function(obj, traditional = true) {
if (!obj) return '';
return Object.entries(obj)
.filter(([key, val]) => val)
.reduce((acc, [k, v]) => {
if (traditional && Array.isArray(v))
return acc.concat(v.map(i => `${_esc(k)}=${_esc(i)}`));
return acc.concat(`${_esc(k)}=${_esc(v)}`);
}, []).join('&');
};
const _createURL = function(url, args, query = {}, options = {}) {
for (const arg of args) url = url.replace(/\*/, arg);
const q = _createQueryString(
Object.entries(query).reduce((acc, [key, val]) => {
if (key && (val && val !== '')) acc[key] = val;
return acc;
}, {})
);
if (q.length) url += `?${q}`;
const base = options.requestBase;
if (!base) return url.replace(/^\//, '');
return base.replace(/\/+$/, '') + '/' + url.replace(/^\//, '');
};
const _events = [
'onchange', 'onclick', 'ondragenter', 'ondragleave', 'ondragover',
'ondragstart', 'ondrop', 'oninput', 'onkeypress', 'onmousedown',
'onmouseenter', 'onmouseleave', 'onmousemove', 'onmouseover', 'onsubmit'
];
const _htmlProps = [
'disabled', 'readonly', 'required'
];
const _styleProps = [
'height', 'width'
];
const _typeof = function(x) {
if (!x) return;
const type = typeof x;
if ((type == 'object') && (x.nodeType == 1)
&& (typeof x.style == 'object')
&& (typeof x.ownerDocument == 'object')) return 'element';
if (type == 'object' && Array.isArray(x)) return 'array';
return type;
};
const _ucfirst = function(s) {
return s && s[0].toUpperCase() + s.slice(1) || '';
};
class Bitch {
_newHeaders() {
const headers = new Headers();
headers.set('X-Requested-With', 'XMLHttpRequest');
return headers;
}
_setHeaders(options) {
if (!options.headers) options.headers = this._newHeaders();
if (!(options.headers instanceof Headers)) {
const headers = options.headers;
options.headers = this._newHeaders();
for (const [k, v] of Object.entries(headers))
options.headers.set(k, v);
}
}
async blows(url, options = {}) {
let want = options.response || 'text'; delete options.response;
this._setHeaders(options);
if (options.form) {
const form = options.form; delete options.form;
const data = new FormData(form);
data.set('_submit', form.getAttribute('submitter'));
const type = options.enctype
|| form.getAttribute('enctype')
|| 'application/x-www-form-urlencoded';
delete options.enctype;
if (type == 'multipart/form-data') {
const files = options.files; delete options.files;
if (files && files[0]) data.append('file', files[0]);
options.body = data;
}
else {
options.headers.set('Content-Type', type);
const params = new URLSearchParams(data);
options.body = params.toString();
}
}
if (options.json) {
options.headers.set('Content-Type', 'application/json');
options.body = options.json; delete options.json;
want = 'object';
}
options.method ||= 'POST';
if (options.method == 'POST') {
options.cache ||= 'no-store';
options.credentials ||= 'same-origin';
}
const response = await fetch(url, options);
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.statusText}`);
}
const headers = response.headers;
const location = headers.get('location');
if (location) {
const reload_header = headers.get('x-force-reload');
const reload = reload_header == 'true' ? true : false;
return { location, reload, status: 302 };
}
if (want == 'object') return {
object: await response.json(), status: response.status
};
if (want == 'text') return {
status: response.status, text: await response.text()
};
return { response };
}
async sucks(url, options = {}) {
const want = options.response || 'object'; delete options.response;
this._setHeaders(options);
options.method ||= 'GET';
const response = await fetch(url, options);
if (!response.ok) {
if (want == 'object') {
console.warn(`HTTP error! Status: ${response.statusText}`);
return { object: false, status: response.status };
}
throw new Error(`HTTP error! Status: ${response.statusText}`);
}
const headers = response.headers;
const location = headers.get('location');
if (location) return { location, status: 302 };
if (want == 'blob') {
const key = 'content-disposition';
const filename = headers.get(key).split('filename=')[1];
const blob = await response.blob();
return { blob, filename, status: response.status };
}
if (want == 'object') return {
object: await response.json(), status: response.status
};
if (want == 'text') return {
status: response.status,
text: await new Response(await response.blob()).text()
};
return { response };
}
}
class HtmlTiny {
_frag(content) {
return document.createRange().createContextualFragment(content);
}
_tag(tag, attr, content) {
const el = document.createElement(tag);
const type = _typeof(attr);
if (type == 'object') {
for (const prop of Object.keys(attr)) {
if (_events.includes(prop)) {
el.addEventListener(prop.replace(/^on/, ''), attr[prop]);
}
else if (_htmlProps.includes(prop)) {
el.setAttribute(prop, prop);
}
else if (_styleProps.includes(prop)) el.style[prop] = attr[prop];
else el[prop] = attr[prop];
}
}
else if (type == 'array') { content = attr; }
else if (type == 'element') { content = [attr]; }
else if (type == 'string') { content = [attr]; }
if (!content) return el;
if (_typeof(content) != 'array') content = [content];
for (const child of content) {
const childType = _typeof(child);
if (!childType) continue;
if (childType == 'number' || childType == 'string') {
el.append(document.createTextNode(child));
}
else { el.append(child); }
}
return el;
}
typeOf(x) { return _typeof(x) }
a(attr, content) { return this._tag('a', attr, content) }
canvas(attr, content) { return this._tag('canvas', attr, content) }
caption(attr, content) { return this._tag('caption', attr, content) }
div(attr, content) { return this._tag('div', attr, content) }
fieldset(attr, content) { return this._tag('fieldset', attr, content) }
figure(attr, content) { return this._tag('figure', attr, content) }
form(attr, content) { return this._tag('form', attr, content) }
h1(attr, content) { return this._tag('h1', attr, content) }
h2(attr, content) { return this._tag('h2', attr, content) }
h3(attr, content) { return this._tag('h3', attr, content) }
h4(attr, content) { return this._tag('h4', attr, content) }
h5(attr, content) { return this._tag('h5', attr, content) }
img(attr) { return this._tag('img', attr) }
input(attr, content) { return this._tag('input', attr, content) }
label(attr, content) { return this._tag('label', attr, content) }
legend(attr, content) { return this._tag('legend', attr, content) }
li(attr, content) { return this._tag('li', attr, content) }
nav(attr, content) { return this._tag('nav', attr, content) }
optgroup(attr, content) { return this._tag('optgroup', attr, content) }
option(attr, content) { return this._tag('option', attr, content) }
select(attr, content) { return this._tag('select', attr, content) }
span(attr, content) { return this._tag('span', attr, content) }
strong(attr, content) { return this._tag('strong', attr, content) }
table(attr, content) { return this._tag('table', attr, content) }
tbody(attr, content) { return this._tag('tbody', attr, content) }
td(attr, content) { return this._tag('td', attr, content) }
textarea(attr, content) { return this._tag('textarea', attr, content) }
th(attr, content) { return this._tag('th', attr, content) }
thead(attr, content) { return this._tag('thead', attr, content) }
tr(attr, content) { return this._tag('tr', attr, content) }
ul(attr, content) { return this._tag('ul', attr, content) }
upload(attr, content) { return this._tag('upload', attr, content) }
button(attr, content) {
if (_typeof(attr) == 'object') attr['type'] ||= 'submit';
else {
content = attr;
attr = { type: 'submit' };
}
return this._tag('button', attr, content);
}
checkbox(attr) {
attr['type'] = 'checkbox';
return this._tag('input', attr);
}
file(attr) {
attr['type'] = 'file';
return this._tag('input', attr);
}
hidden(attr) {
attr['type'] = 'hidden';
return this._tag('input', attr);
}
icon(attr) {
const {
attrs = {}, className, height = 20, icons, name, onclick,
presentational = true, title, width = 20
} = attr;
if (Array.isArray(className)) className = `${className.join(' ')}`;
const newAttrs = {
'aria-hidden': presentational ? 'true' : null,
class: className, height, width, ...attrs
};
const svg = `
<svg ${Object.keys(newAttrs).filter(attr => newAttrs[attr]).map(attr => `${attr}="${newAttrs[attr]}"`).join(' ')}>
<use href="${icons}#icon-${name}"></use>
</svg>`;
const wrapperAttr = { className: 'icon-wrapper' };
if (onclick) wrapperAttr.onclick = onclick;
if (title) wrapperAttr.title = title;
return this.span(wrapperAttr, this._frag(svg.trim()));
}
radio(attr) {
attr['type'] = 'radio';
return this._tag('input', attr);
}
text(attr) {
attr['type'] = 'text';
return this._tag('input', attr);
}
cumulativeOffset(el) {
let valueT = 0;
let valueL = 0;
if (el.parentNode) {
do {
valueT += el.offsetTop || 0;
valueL += el.offsetLeft || 0;
el = el.offsetParent;
} while (el);
}
return { left: Math.round(valueL), top: Math.round(valueT) };
}
elementOffset(el, stopEl) {
let valueT = 0;
let valueL = 0;
do {
if (el) {
valueT += el.offsetTop || 0;
valueL += el.offsetLeft || 0;
el = el.offsetParent;
if (stopEl && el == stopEl) break;
}
} while (el);
return { left: Math.round(valueL), top: Math.round(valueT) };
}
getDimensions(el) {
if (!el) return { height: 0, width: 0 };
const style = el.style || {};
if (style.display && style.display !== 'none') {
return { height: el.offsetHeight, width: el.offsetWidth };
}
const originalStyles = {
display: style.display,
position: style.position,
visibility: style.visibility
};
const newStyles = { display: 'block', visibility: 'hidden' }
if (originalStyles.position !== 'fixed')
newStyles.position = 'absolute';
for (const p in newStyles) style[p] = newStyles[p];
const dimensions = { height: el.offsetHeight, width: el.offsetWidth };
for (const p in newStyles) style[p] = originalStyles[p];
return dimensions;
}
getCoords(event, coordKey = 'page') {
const x = `${coordKey}X`;
const y = `${coordKey}Y`;
return {
x: x in event ? event[x] : event.pageX,
y: y in event ? event[y] : event.pageY,
};
}
getOffset(el) {
const rect = el.getBoundingClientRect();
return {
left: Math.round(rect.left + window.scrollX),
top: Math.round(rect.top + window.scrollY)
};
}
}
const registeredOnloadCallbacks = [];
const registeredOnunloadCallbacks = [];
return {
Bitch: {
bitch: new Bitch(),
},
Event: {
onReady: function(callback) {
if (document.readyState != 'loading') callback();
else if (document.addEventListener)
document.addEventListener('DOMContentLoaded', callback);
else document.attachEvent('onreadystatechange', function() {
if (document.readyState == 'complete') callback();
});
},
onloadCallbacks: function() {
return registeredOnloadCallbacks;
},
onunloadCallbacks: function() {
return registeredOnunloadCallbacks;
},
registerOnload: function(callback) {
registeredOnloadCallbacks.push(callback);
return registeredOnloadCallbacks.length;
},
registerOnunload: function(callback) {
registeredOnunloadCallbacks.push(callback);
return registeredOnunloadCallbacks.length;
},
unregisterOnload: function(index) {
registeredOnloadCallbacks.splice(index - 1, 1);
},
unregisterOnunload: function(index) {
registeredOnunloadCallbacks.splice(index - 1, 1);
}
},
Markup: { // A role
animateButtons: function(container, selector = 'button') {
for (const el of container.querySelectorAll(selector)) {
if (el.getAttribute('movelistener')) continue;
el.setAttribute('movelistener', true);
el.addEventListener('mousemove', function(event) {
const rect = el.getBoundingClientRect();
const x = Math.floor(
event.pageX - (rect.left + window.scrollX)
);
const y = Math.floor(
event.pageY - (rect.top + window.scrollY)
);
el.style.setProperty('--x', x + 'px');
el.style.setProperty('--y', y + 'px');
});
}
},
appendValue: function(obj, key, newValue) {
let existingValue = obj[key] || '';
if (existingValue) existingValue += ' ';
obj[key] = existingValue + newValue;
},
display: function(container, attribute, obj) {
if (this[attribute] && container.contains(this[attribute])) {
container.replaceChild(obj, this[attribute]);
}
else { container.append(obj) }
return obj;
},
h: new HtmlTiny(),
isHTML: function(value) {
if (typeof value != 'string') return false;
if (value.match(new RegExp('^<'))) return true;
return false;
},
isHTMLOfClass: function(value, className) {
if (typeof value != 'string') return false;
if (!value.match(new RegExp(`class="${className}"`))) return false;
return true;
}
},
Modifiers: { // Another role
applyTraits: function(obj, namespace, traits, args) {
for (const trait of traits) {
if (!namespace[trait]) {
throw new Error(namespace + `: Unknown trait ${trait}`);
}
const initialiser = namespace[trait]['initialise'];
if (initialiser) initialiser.bind(obj)(args);
for (const method of Object.keys(namespace[trait].around)) {
obj.around(method, namespace[trait].around[method]);
}
}
},
around: function(method, modifier) {
const isBindable = func => func.hasOwnProperty('prototype');
if (!this[method]) {
throw new Error(`Around no method: ${method}`);
}
const original = this[method].bind(this);
const around = isBindable(modifier)
? modifier.bind(this) : modifier;
this[method] = function(args1, args2, args3, args4, args5) {
return around(original, args1, args2, args3, args4, args5);
};
},
resetModifiers: function(methods) {
for (const method of Object.keys(methods)) delete methods[method];
}
},
String: {
capitalise: function(s = '') {
const words = [];
for (const word of s.split(' ')) words.push(_ucfirst(word));
return words.join(' ');
},
guid: function() {
// https://jsfiddle.net/briguy37/2MVFd/
let date = new Date().getTime();
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
const r = ((date + Math.random()) * 16) % 16 | 0;
date = Math.floor(date / 16);
return (c === 'x' ? r : ((r & 0x3) | 0x8)).toString(16);
});
},
padString: function(string, padSize, pad) {
string = string.toString();
pad = pad.toString() || ' ';
const size = padSize - string.length;
if (size < 1) return string;
return pad.repeat(size) + string;
},
ucfirst: _ucfirst
},
URL: {
createURL: _createURL
}
};
})();