|
// Responsive & Accessible App UI/UX Framework |
|
// ------------------------------------------- |
|
// UI: Title, inputs, dialogs, notifications, dropdowns, swipe / drag actions |
|
// Compatibility: FF, Chrome, IE 9+, WAI ARIA W3C (a11ya) rich internet applications |
|
// recommendations for maximum accessibility. No dependencies whatsoever. |
|
// Language: Internationalisation (i18n) with language object |
|
// Last change: 2021.11.13, 21:20h |
|
// Note: Adapted Bootstrap 3/4 css selectors to run without it's own css |
|
|
|
function UI(i18n, icons) { |
|
'use strict'; |
|
// Monkey patching / adding some functions to Element prototypes |
|
var w = window, d = w.document, n = navigator; |
|
var raf = w.requestAnimationFrame || w.webkitRequestAnimationFrame || w.mozRequestAnimationFrame || function(c){ setTimeout(c, 16, Date.now()); return true;}; |
|
var ep = Element.prototype; |
|
if(!ep.matches) ep.matches = ep.matchesSelector || ep.msMatchesSelector || ep.webkitMatchesSelector || ep.mozMatchesSelector; |
|
if(!ep.closest) ep.closest = function(s){ var el = this; while (el.matches && !el.matches(s)) el = el.parentElement; return el.matches ? el : null; }; |
|
if(!ep.setAttributes) ep.setAttributes = function(o) { for(var key in o) this.setAttribute(key, o[key]); }; |
|
// Passive listener feature detection |
|
var hasPassive = false |
|
try { w.addEventListener('test', null, Object.defineProperty({}, 'passive', { get: function() { hasPassive = { passive: true }; } })); } |
|
catch(err) {} |
|
// UI class settings / i18n / SVGs |
|
i18n = i18n ? i18n : { |
|
ok: 'ok', |
|
cancel: 'cancel', |
|
close: 'close', |
|
dismiss: 'dismiss', |
|
alert: 'alert', |
|
warning: 'warning', |
|
confirm: 'confirm', |
|
prompt: 'prompt', |
|
error: 'error', |
|
errors: 'errors', |
|
success: 'success', |
|
permisssion: 'permmission', |
|
unload: 'Leave this page?', |
|
willnotbesaved: 'Changes will not be saved.', |
|
plus: 'plus', |
|
minus: 'minus', |
|
reveal: 'reveal', |
|
hide: 'hide', |
|
clear: 'clear', |
|
offline: 'The network is not reachable. Your data will be cached.', |
|
online: 'The network connection was re-established.' |
|
}; |
|
icons = icons ? icons : { |
|
ok: '<path fill="none" stroke="currentColor" d="M8 1L4 8 1 6"/>', |
|
warn: '<path d="M4.5,0.5L0.3,8h8.5L4.5,0.5z M5,7.3H4V6.5h1V7.3z M4,6V3h1v3H4z"/>', |
|
note: '<path d="M4.5 8.5c-.5 0-.7-.4-.7-.9h1.5c0 .5-.4.9-.8.9zM7 6l.8 1v.3H1.3V7L2 6V4c0-1.3.8-2.2 2-2.5V1c0-.3.2-.5.5-.5s.5.2.5.5v.5c1.2.3 2 1.3 2 2.5v2z"/>', |
|
close: '<path d="M8.4 1.4L7.6.6 4.5 3.8 1.4.6l-.8.8 3.2 3.1L.6 7.6l.8.8 3.1-3.2 3.1 3.2.8-.8-3.2-3.1z"/>', |
|
reveal: '<path d="M4.5,4.2c-0.7,0-1.4,0.6-1.4,1.4S3.7,7,4.5,7s1.4-0.6,1.4-1.4S5.2,4.2,4.5,4.2z M4.5,2C2,2,0,3.6,0,5.6h1C1,4.1,2.5,3,4.5,3C6.4,3,8,4.1,8,5.6h1C9,3.6,7,2,4.5,2z"/>', |
|
hide: '<path d="m7.4 2.9-.7.7C7.5 4 8 4.8 8 5.6h1c0-1.1-.6-2.1-1.6-2.7zM3.6 6.7c.2.2.5.3.9.3.8 0 1.4-.6 1.4-1.4 0-.3-.1-.6-.3-.9l-2 2zM4.5 3h.2l.9-.9C5.3 2 4.9 2 4.5 2 2 2 0 3.6 0 5.6h1C1 4.1 2.5 3 4.5 3zM.68 7.75 7.75.68l.57.57-7.07 7.07z"/>', |
|
copy: '<path d="M6.8 6.8V3.5h1.5v4.8H3.5V6.8h3.3zm-1-1V1.3H1.3v4.5h4.5zm.5.5H.8V.8h5.5v5.5z"/>', |
|
offline: '<path d="M6.8 6.8V3.5h1.5v4.8H3.5V6.8h3.3zm-1-1V1.3H1.3v4.5h4.5zm.5.5H.8V.8h5.5v5.5z"/>', |
|
notallow: '<path d="m2.2 5.9 3.7-3.7c-.4-.2-.9-.4-1.4-.4C3 1.8 1.8 3 1.8 4.5c0 .5.2 1 .4 1.4zm4.6-2.8L3.1 6.8c.4.2.9.4 1.4.4C6 7.2 7.2 6 7.2 4.5c0-.5-.2-1-.4-1.4zM4.5.5c2.2 0 4 1.8 4 4s-1.8 4-4 4-4-1.8-4-4 1.8-4 4-4z"/>' |
|
}; |
|
// Keyboard Key Codes |
|
var keys = { backspace:8, tab:9, return:13, shift:16, pause:19, esc:27, space:32, pgup:33, pgdown:34, end:35, home:36, left:37, up:38, right:39, down:40, add:107, substract:109 }; |
|
// Register changes in document inputs |
|
var hasChanged = changed(d, 'hasChanged'); |
|
// Global audio setting |
|
var allowAudio = true; |
|
var allowVibration = true; |
|
var allowNotifications = false; |
|
// Make UI functions available in window namespace |
|
w.allowUIAudio = function(s){ allowAudio = s || allowAudio; return allowAudio; }; |
|
w.allowUIVibration = function(s){ allowVibration = s || allowVibration; return allowVibration; }; |
|
w.allowUINotifications = function(s){ allowNotifications = s || allowNotifications; return allowNotifications; }; |
|
w.smoothUIScroll = function(l, r, ms){ smoothScroll(l, r, ms); }; |
|
w.switchScheme = function(el){ Scheme(el); } |
|
// Check for duplicate IDs |
|
checkDuplicateIds(); |
|
// Native UI overwrites |
|
w.alert = function(m, o, c) { new Alert(m, o, c); }; |
|
w.confirm = function(m, o, c) {new Confirm(m, o, c); }; |
|
w.prompt = function(m, v, o, c) { new Prompt(m, v, o, c); }; |
|
w.dialog = function(m, v, o, c) { new Dialog(m, v, o, c); }; |
|
w.notify = function(m, t, o, c) { new Notify(m, t, o, c); }; |
|
w.toast = function(m, t, o) { new Toast(m, t, o); }; |
|
// On unload message |
|
w.addEventListener('beforeunload', Unload, true); |
|
// Try hiding address bar |
|
w.addEventListener('load', function(e) { |
|
setTimeout(function(){ w.scrollTo(0, 1); }, 0); |
|
}, hasPassive); |
|
// Check DOM for ui element / add functionality to inline trigger |
|
w.addEventListener('DOMContentLoaded', function(e) { |
|
// Mobile viewport hack |
|
setViewport(e); |
|
// Native title Overwrite |
|
Tooltips(e); |
|
// Inline modal dialog triggers |
|
[].slice.call(d.querySelectorAll('[data-toggle="modal"]')).forEach(function(mt){ |
|
var mo = mt.hash ? d.getElementById(mt.hash.substring(1)) : d.getElementById(mt.getAttribute('data-target')); |
|
if (mo) mt.addEventListener('click', function(e){ new Modal(mo, {}); }, false); |
|
}); |
|
// Dropdown togglers |
|
[].slice.call(d.querySelectorAll('[data-toggle="dropdown"]')).forEach(function(dd){ |
|
dd.addEventListener('click', function(e){ new Dropdown(e); }, false); |
|
}); |
|
// Tabs |
|
[].slice.call(d.querySelectorAll('[role="tablist"]')).forEach(function(tl){ |
|
new Tabs(tl); |
|
}); |
|
// Ripples |
|
[].slice.call(d.querySelectorAll('.ripple')).forEach(function(rp){ |
|
rp.addEventListener('click', function(e){ new Ripple(e); }, false); |
|
}); |
|
// Collapse togglers |
|
[].slice.call(d.querySelectorAll('[data-toggle="collapse"]')).forEach(function(cl){ |
|
cl.addEventListener('click', function(e){ new Collapse(e); }, false); |
|
}); |
|
// Range inputs |
|
[].slice.call(d.querySelectorAll('[type="range"]')).forEach(function(ri){ |
|
new rangeInput(ri); |
|
}); |
|
// Number inputs |
|
[].slice.call(d.querySelectorAll('[type="number"]')).forEach(function(ni){ |
|
new numberInput(ni); |
|
}); |
|
// Password inputs |
|
[].slice.call(d.querySelectorAll('[type="password"]')).forEach(function(pi){ |
|
new passwordInput(pi); |
|
}); |
|
// Search inputs |
|
[].slice.call(d.querySelectorAll('[type="search"]')).forEach(function(si){ |
|
new searchInput(si); |
|
}); |
|
// Form validation |
|
[].slice.call(d.querySelectorAll('form[data-validate="true"]')).forEach(function(fm){ |
|
new Validation(fm); |
|
}); |
|
// Tests |
|
// Toast(getRootVar('--ui-font')); |
|
}, false); |
|
|
|
// Public Methods: |
|
// Alert modal dialog |
|
function Alert(message, options, callback) { |
|
new Modal('alert', options, message, null, callback); |
|
} |
|
// Confirm modal dialog |
|
function Confirm(message, options, callback) { |
|
new Modal('confirm', options, message, null, callback); |
|
} |
|
// Prompt modal dialog |
|
function Prompt(message, value, options, callback) { |
|
new Modal('prompt', options, message, value, callback); |
|
} |
|
// Generic modal dialog |
|
function Dialog(content, value, options, callback) { |
|
new Modal('dialog', options, content, null, callback); |
|
} |
|
// Notifications |
|
function Notify(message, type, options, callback) { |
|
new Note(type, message, options, callback); |
|
} |
|
// Before unload modal dialog |
|
function Unload(e) { |
|
if (hasChanged) { |
|
new Modal('confirm unload', null, i18n.willnotbesaved, null); |
|
w.removeEventListener('beforeunload', Unload, true); |
|
} |
|
} |
|
|
|
// UI Methods: |
|
// Modal dialogs |
|
function Modal (type, options, message, value, callback) { |
|
// Constructor |
|
var TYPE = typeof type !== 'string' ? 'custom' : type, |
|
MODAL = typeof type === 'string' ? create(type, options, message, value) : type, |
|
CALLBACK = typeof callback === 'function' ? callback : null, |
|
EVENT = w.event || d.event, |
|
TRIGGER = EVENT ? EVENT.target : null, |
|
isStatic = (MODAL.getAttribute('data-backdrop') === 'static') || !TYPE.match(/(custom|alert)/gi); |
|
if (EVENT) EVENT.preventDefault(); |
|
open(); |
|
// Methods: |
|
// Create ui modal dialog |
|
function create(type, options, message, value) { |
|
switch(type) { |
|
case 'alert': i18n.title = 'alert'; break; |
|
case 'confirm': i18n.title = 'confirm'; break; |
|
case 'prompt': i18n.title = 'prompt'; break; |
|
case 'confirm unload': i18n.title = i18n.unload; break; |
|
default: i18n.title = 'notification'; |
|
} |
|
i18n = options ? merge(i18n, options) : i18n; |
|
var tpl = [ |
|
'<div class="modal-dialog ui-dialog '+ type +'" role="document">', |
|
'<div class="modal-content"><div class="modal-header">', |
|
'<h3 id="'+ type +'Title">'+ i18n.title +'</h3></div><div class="modal-body">', |
|
(type.match(/prompt/gi)) ? '<label for="'+ type +'Input">'+ message +'</label>' : message, |
|
(type.match(/prompt/gi)) ? '<input type="text" class="form-control" id="'+ type +'Input" value="'+ value +'" autofocus/>' : '', |
|
'</div><div class="modal-footer">', |
|
(type.match(/alert|info/gi)) ? '' : '<button type="button" class="btn btn-secondary" data-dismiss="modal">'+ i18n.cancel +'</button>', |
|
'<button type="button" class="btn btn-primary">'+ i18n.ok +'</button>', |
|
'</div></div></div>' |
|
].join(''); |
|
var modal = d.createElement('div'); |
|
modal.id = 'ui-'+ type; |
|
modal.className = 'modal ui-modal'; |
|
modal.setAttributes({ |
|
'tabindex': '-1', |
|
'aria-hidden': true, |
|
'aria-labelledby': type +'Title' |
|
}); |
|
modal.innerHTML = tpl; |
|
d.body.appendChild(modal); |
|
return modal; |
|
} |
|
// Open modal |
|
function open() { |
|
scrollbar(); |
|
MODAL.removeAttribute('aria-hidden'); |
|
MODAL.setAttribute('aria-modal', true); |
|
MODAL.setAttribute('open', ''); |
|
// Event listeners |
|
if (isStatic) MODAL.addEventListener('click', makestatic, false); |
|
else MODAL.addEventListener('click', closeclick, false) |
|
MODAL.addEventListener('blur', trapFocus, true); |
|
var cls = MODAL.querySelectorAll('[data-dismiss="modal"], .modal-footer button, [type="submit"]'); |
|
[].slice.call(cls).forEach(function(cl) { |
|
cl.addEventListener('click', function(e){ close(MODAL, EVENT, CALLBACK); }, false); |
|
}) |
|
MODAL.firstChild.addEventListener('click', nopropagate, false); |
|
w.addEventListener('keydown', keylistener, false); |
|
// Timeout needed for smooth CSS transitions & effects |
|
setTimeout(function() { |
|
if(!TYPE.match(/(prompt)/gi)) cls[cls.length - 1].focus(); |
|
else MODAL.querySelector('input').select(); |
|
setTimeout(function() { sound(); }, 160); |
|
MODAL.className += ' show'; |
|
dispatchEvent('ui.modal.open', MODAL); |
|
}, 1); |
|
} |
|
// Methods: |
|
// Close / hide modal dialog |
|
function close () { |
|
MODAL.removeAttribute('aria-modal'); |
|
MODAL.removeAttribute('open'); |
|
MODAL.setAttribute('aria-hidden', true); |
|
MODAL.className = MODAL.className.replace(/\bshow\b/g, '').trim(); |
|
w.removeEventListener('keydown', keylistener, false); |
|
scrollbar(); |
|
// Dispatcher ? |
|
dispatchEvent('ui.modal.close', MODAL); |
|
if (CALLBACK) CALLBACK.call(); |
|
if (MODAL.id.match(/confirm/gi)) { |
|
CALLBACK = TRIGGER.getAttribute('onclick') || TRIGGER.onclick; |
|
if (typeof CALLBACK === 'function') TRIGGER.onclick = false; |
|
else TRIGGER.removeAttribute('onclick'); |
|
TRIGGER.click(); |
|
if (typeof CALLBACK === 'function') TRIGGER.onclick = CALLBACK; |
|
else TRIGGER.setAttribute('onclick', CALLBACK); |
|
} |
|
smoothScroll(TRIGGER); |
|
if ('focus' in TRIGGER) TRIGGER.focus(); |
|
if (!TYPE.match(/custom/gi)) remove(MODAL); |
|
} |
|
// Remove Modal from DOM |
|
function remove (el) { |
|
setTimeout(function(){ |
|
el.parentNode.removeChild(el); |
|
dispatchEvent('ui.modal.destroy', el); |
|
}, 500); |
|
} |
|
// Hide / disable scrollbars if any |
|
function scrollbar() { |
|
var width = parseInt(w.innerWidth - d.documentElement.clientWidth, 10); |
|
if(width > 0) { |
|
d.body.style.paddingRight = width +'px'; |
|
d.body.className = d.body.className + ' modal-open'.trim() |
|
} else if (d.body.className.match(/(modal-open)/gi)) { |
|
d.body.style.paddingRight = ''; |
|
d.body.className = d.body.className.replace('modal-open','').trim(); |
|
} |
|
} |
|
// Make modal static |
|
function makestatic (e){ |
|
var el = this; |
|
el.className += ' modal-static'; |
|
vibration([500]); |
|
setTimeout(function(){ |
|
el.className = el.className.replace(/\b(modal-static)\b/g, '').trim(); |
|
el.querySelector('button:first-of-type').focus(); |
|
}, 500); |
|
} |
|
// Click on modal only closes dialog |
|
function closeclick (e){ |
|
if(!isTarget(e, MODAL.firstChild)) close(); |
|
} |
|
} |
|
|
|
// Notifications: |
|
function Note (type, message, options, callback) { |
|
var WRAP = d.getElementById('ui-notifications') ? d.getElementById('ui-notifications') : wrap(), |
|
TYPE = type ? type.toLowerCase() : '', |
|
IMG = options ? (options.image ? options.image : null) : null, |
|
MSG = message, |
|
NOTE = create(); |
|
show(); |
|
// Methods: |
|
// Create wrapper |
|
function wrap () { |
|
var el = d.createElement('div'); |
|
el.id = 'ui-notifications'; |
|
el.className = 'ui-notifications'; |
|
el.setAttributes({'tabindex': '-1', 'aria-live': 'polite', 'aria-relevant': 'additions removals'}); |
|
el.innerHTML = '<a href="#!" class="counter" role="region" aria-live="polite"></a>' |
|
d.body.appendChild(el); |
|
return el; |
|
} |
|
// Create notification |
|
function create () { |
|
i18n.title = TYPE.match(/(error)/gi) ? 'error' : TYPE.match(/(warn)/gi) ? 'warning' : TYPE.match(/(success)/gi) ? 'success' : 'notification'; |
|
var ico = icons[TYPE.match(/(error)/gi) ? 'close' : TYPE.match(/(warning)/gi) ? 'warn' : TYPE.match(/(success)/gi) ? 'ok' : 'note'] ; |
|
var fig = IMG ? '<img src="'+ IMG +'"/>' : '<svg viewbox="0 0 9 9">'+ ico +'</svg>'; |
|
var tpl = [ |
|
'<figure aria-label="icon '+ TYPE +'">'+ fig +'</figure>', |
|
'<p><b>'+ i18n.title +'</b> '+ MSG +'</p>', |
|
'<a href="#!" class="close" role="button">'+ i18n.close +'</a>', |
|
'<time timestamp="'+ Date.now() +'"></time>', |
|
'<div class="progress"><i role="progressbar" aria-valuemin="0" aria-valuemax="1"></i></div>' |
|
].join(''), |
|
note = d.createElement('div'); |
|
note.id = 'note-'+ Math.random().toString(36).substr(2, 8); |
|
note.className = 'note' + (TYPE ? ' note-'+ TYPE : ''); |
|
note.setAttributes({ |
|
'role': 'alert', |
|
'aria-live': 'assertive', |
|
'aria-atomic': true, |
|
'tabindex': 0, |
|
'draggable': '' |
|
}); |
|
note.innerHTML = tpl; |
|
return note; |
|
} |
|
// Show notification |
|
function show () { |
|
var progress = NOTE.querySelector('.progress i'), |
|
close = NOTE.querySelector('.close'), |
|
timed = TYPE.match(/(timed)/gi), |
|
time = NOTE.querySelector('time'), |
|
img = NOTE.querySelector('figure svg, figure img'); |
|
ago(time); |
|
close.addEventListener('click', remove, false); |
|
if(timed) { |
|
NOTE.className += ' timed'; |
|
addPrefixedListener(progress, 'AnimationEnd', remove); |
|
} |
|
WRAP.insertAdjacentElement('afterbegin', NOTE); |
|
// Timeout needed for smoootth CSS transitions & effects |
|
setTimeout(function() { |
|
NOTE.className += ' show'; |
|
NOTE.focus(); |
|
sound(); |
|
new Swipe(NOTE, 'x', WRAP, function(gesture){ |
|
if(gesture === 'left') remove(); |
|
if(gesture === 'right') hide(); |
|
}, true); |
|
// vibration([400]); |
|
if (!d.hasFocus()) notification(TYPE, MSG, img ? (img.src ? img.src : png64(img)) : null, null, audiopool.pop ); |
|
dispatchEvent('ui.notification.show', NOTE); |
|
}, 1); |
|
} |
|
// Hide notification after timeout |
|
function hide () { |
|
WRAP.className = NOTE.className.replace(/\bshow\b/g, '').trim(); |
|
dispatchEvent('ui.notification.hide', NOTE); |
|
} |
|
// Delete notification |
|
function remove () { |
|
NOTE.className = NOTE.className.replace(/\bshow\b/g, '').trim(); |
|
setTimeout(function() { |
|
WRAP.removeChild(NOTE); |
|
dispatchEvent('ui.notification.destroy', NOTE); |
|
}, 500); |
|
} |
|
} |
|
|
|
// Dropdown |
|
function Dropdown(e) { |
|
var TRIGGER = e.target || w.event.target, |
|
PARENT = getClosest(TRIGGER, '.dropdown'), |
|
MENU = d.querySelector('[aria-labelledby="'+ TRIGGER.id +'"]'), |
|
isPersistent = TRIGGER.getAttribute('data-persist') === 'true', |
|
isExpanded = TRIGGER.getAttribute('aria-expanded') === 'true'; |
|
if(MENU){ |
|
prevent(e); |
|
d.addEventListener('scroll', position, hasPassive); |
|
w.addEventListener('resize', position, false); |
|
w.addEventListener('orientationchange', position, false); |
|
if(!isPersistent) d.addEventListener('click', hide, true); |
|
if(!isPersistent) MENU.addEventListener('click', hide, false); |
|
toggle(e); |
|
} |
|
// Methods: |
|
function toggle (e) { |
|
isExpanded ? hide(e) : show(e); |
|
} |
|
function show (e) { |
|
TRIGGER.setAttribute('aria-expanded', true); |
|
PARENT.className += ' show'; |
|
MENU.className += ' show'; |
|
position(e); |
|
dispatchEvent('ui.dropdown.show', MENU); |
|
} |
|
function hide (e) { |
|
d.removeEventListener('scroll', position, hasPassive); |
|
w.removeEventListener('resize', position, false); |
|
w.removeEventListener('orientationchange', position, false); |
|
if(!isPersistent) d.removeEventListener('click', hide, true); |
|
TRIGGER.setAttribute('aria-expanded', false); |
|
PARENT.className = PARENT.className.replace('show','').trim() |
|
MENU.className = MENU.className.replace('show','').trim(); |
|
dispatchEvent('ui.dropdown.hide', MENU); |
|
} |
|
function position (e) { |
|
var s = w.getComputedStyle(TRIGGER), |
|
align = TRIGGER.getAttribute('data-align') || '', |
|
offset = parseInt(TRIGGER.getAttribute('data-offset'),10) || 5, |
|
margin = { |
|
top: parseInt(s.marginTop,10), |
|
right: parseInt(s.marginRight,10), |
|
bottom: parseInt(s.marginBottom,10), |
|
left: parseInt(s.marginLeft,10) |
|
}, |
|
vh = d.documentElement.clientHeight, vw = d.body.clientWidth, |
|
tH = TRIGGER.offsetHeight, tT = TRIGGER.offsetTop, tL = TRIGGER.offsetLeft, |
|
mH = MENU.offsetHeight, mW = MENU.offsetWidth, |
|
hT, hR, hB, hL; |
|
MENU.style.top = tH + margin.top + offset +'px'; |
|
MENU.style.right = 'auto'; |
|
MENU.style.left = 0; |
|
if(!align || align.match(/(top|bottom)/gi)) { |
|
if(align.match(/top/gi)) MENU.style.top = -(mH + offset - margin.top) +'px'; |
|
} |
|
if(align && align.match(/(right|left)/gi)) { |
|
if(align.match(/left/gi)) MENU.style.left = tL - offset +'px'; |
|
if(align.match(/right/gi)) {MENU.style.left = 'auto'; MENU.style.right = 0} |
|
} |
|
} |
|
} |
|
|
|
/// Collapse |
|
function Collapse(e) { |
|
var TRIGGER = e.target || w.event.target, |
|
TARGETS = [], |
|
BACKDROP = d.getElementById('backdrop'), |
|
TEXT = TRIGGER.getAttribute('data-expanded'), |
|
isExpanded = TRIGGER.getAttribute('aria-expanded') === 'true'; |
|
if(TEXT && !TRIGGER.hasAttribute('data-original-text')) TRIGGER.setAttribute('data-original-text', TRIGGER.textContent); |
|
if(BACKDROP) BACKDROP.addEventListener('click', hide, false); |
|
if(TRIGGER.hash) TARGETS.push(d.getElementById(TRIGGER.hash.substr(1))); |
|
if(TRIGGER.getAttribute('aria-controls') !== null) { |
|
var ctrls = TRIGGER.getAttribute('aria-controls').trim().replace(' ',',').split(','); |
|
for(var i = 0; i < ctrls.length; i++) { |
|
if(d.getElementById(ctrls[i])) TARGETS.push(d.getElementById(ctrls[i])); |
|
} |
|
} |
|
prevent(e); |
|
if (isExpanded) hide(); |
|
else { |
|
if(TEXT) TRIGGER.textContent = TEXT; |
|
for(var i = 0; i < TARGETS.length; i++) show(TARGETS[i]); |
|
} |
|
// Methods: |
|
function show (el){ |
|
var dismiss = el.querySelectorAll('[data-dismiss="drawer"]'); |
|
TRIGGER.setAttribute('aria-expanded', true); |
|
el.className += el.className.match(/\bshow\b/gi) ? '' : ' show'; |
|
if(dismiss) { |
|
for(var i = 0; i < dismiss.length; i++) dismiss[i].addEventListener('click', hide, false); |
|
} |
|
dispatchEvent('ui.collapse.show', el); |
|
} |
|
function hide (){ |
|
if(TEXT) TRIGGER.textContent = TRIGGER.getAttribute('data-original-text'); |
|
for(var i = 0; i < TARGETS.length; i++){ |
|
TARGETS[i].className = TARGETS[i].className.replace(/\bshow\b/gi,'').trim(); |
|
dispatchEvent('ui.collapse.hide', TARGETS[i]); |
|
} |
|
TRIGGER.setAttribute('aria-expanded', false); |
|
} |
|
} |
|
|
|
/// Tabs |
|
function Tabs (el) { |
|
var LIST = el, |
|
TABS = LIST.querySelectorAll('[aria-controls]'), |
|
CURR = LIST.hasAttribute('data-current') ? current(LIST.getAttribute('data-current')) : false, |
|
STEPS = LIST.className.match(/\bstep-list\b/gi) && d.querySelector('[data-steplist="'+ LIST.id +'"]'), |
|
SCROLL = LIST.hasAttribute('data-scroll'), |
|
TIND = LIST.querySelector('.indicator'), |
|
PANELS = [], SWIPE = false, PIND = false; |
|
if(TABS) getPanels(TABS); |
|
if(SCROLL) mousescroll(); |
|
// Global Listeners: |
|
if(TIND) d.addEventListener('scroll', indicateTab, hasPassive); |
|
if(TIND) w.addEventListener('resize', indicateTab, false); |
|
if(TIND) w.addEventListener('orientationchange', indicateTab, false); |
|
w.addEventListener('hashchange', init, false); |
|
w.addEventListener('DOMContentLoaded', init, false); |
|
// Methods: |
|
function init () { |
|
var tab = LIST.querySelector('[href="'+ w.location.hash +'"]'), |
|
active = tab ? !tab.className.match(/\bactive\b/ig) && tab.getAttribute('aria-selected') != 'true' : false; |
|
if(active) tab.click(); |
|
} |
|
function getPanels (tabs) { |
|
if(!tabs) return; |
|
[].slice.call(tabs).forEach(function(tab){ |
|
var panel = tab.hash ? d.getElementById(tab.hash.substr(1)) : d.querySelector('[aria-labelledby="'+ tab.getAttribute('aria-controls') +'"]'); |
|
if(!panel) return; |
|
PANELS.push(panel); |
|
if(!SWIPE) SWIPE = panel.closest('.tab-panels').hasAttribute('data-swipe'); |
|
set(tab, panel); |
|
}); |
|
PIND = PANELS[0] && PANELS[0].closest('.tab-panels').getAttribute('data-indicate') === 'true' ? indicator() : false; |
|
} |
|
function set (tab, panel) { |
|
tab.addEventListener('click', function(e){ e.preventDefault(); show(tab, panel); }, false); |
|
if(!STEPS) { |
|
var i = [].indexOf.call(TABS, tab), |
|
first = TABS[0], |
|
last = TABS[TABS.length-1], |
|
prev = i === 0 ? last : TABS[i-1], |
|
next = i < TABS.length-1 ? TABS[i+1] : first; |
|
onKey('right', tab, function(){ next.click(); next.focus(); }); |
|
onKey('left', tab, function(){ prev.click(); prev.focus(); }); |
|
onKey('space', tab, function(){ tab.click(); }); |
|
onKey('home', tab, function(){ first.click(); first.focus(); }); |
|
onKey('end', tab, function(){ last.click(); last.focus(); }); |
|
} |
|
if(tab.getAttribute('aria-selected') === 'true') tab.click(); |
|
} |
|
function show (tab, panel) { |
|
// Reset all tabs and panels |
|
reset(); |
|
if(!STEPS) { |
|
var i = [].indexOf.call(PANELS, panel), |
|
next = i < PANELS.length-1 ? PANELS[i+1] : PANELS[0], |
|
prev = i === 0 ? PANELS[PANELS.length - 1] : PANELS[i-1]; |
|
prev.className += ' previous'; |
|
next.className += ' next'; |
|
if(SWIPE) new Swipe(panel, panel.parentElement.getAttribute('data-swipe'), panel, function(swipe) { |
|
var i = [].indexOf.call(TABS, tab), |
|
prev = i === 0 ? TABS[TABS.length - 1] : TABS[i-1], |
|
next = i < TABS.length-1 ? TABS[i+1] : TABS[0]; |
|
if(swipe.direction === 'right') { prev.click(); return; } |
|
else if(swipe.direction === 'left') { next.click(); return; } |
|
}, true); // false = do not remove listeners after swipe is completed. |
|
} |
|
else navigation(tab, panel); |
|
// Set tab & panel active |
|
tab.className += ' active'; |
|
tab.setAttribute('aria-selected', true); |
|
tab.setAttribute('tabindex', '-1'); |
|
panel.className += ' active'; |
|
panel.setAttribute('tabindex', 0); |
|
setTimeout(function(){ panel.className += ' show'; }, 10); |
|
// Set hash / history /**/ |
|
if (w.history.pushState) w.history.pushState({}, d.title +' - '+ tab.textContent, '#'+ panel.id); |
|
else w.location.hash = '#' + panel.id; |
|
if(SCROLL) position(tab); |
|
if(CURR) tab.appendChild(CURR); |
|
if(TIND) indicateTab(); |
|
if(PIND && !STEPS) indicatePanel(panel.id); |
|
dispatchEvent('ui.tabs.show', panel); |
|
} |
|
function reset () { |
|
PANELS.forEach(function(p){ |
|
p.setAttribute('tabindex', '-1'); |
|
p.className = p.className.replace(/\bactive|show|previous|next\b/gi,'').trim(); |
|
dispatchEvent('ui.tabs.hide', p); |
|
}); |
|
[].slice.call(TABS).forEach(function(t){ |
|
t.setAttribute('aria-selected', false); |
|
t.setAttribute('tabindex', 0); |
|
t.setAttribute('draggable', false); |
|
t.className = t.className.replace(/\bactive\b/gi,'').trim(); |
|
}); |
|
} |
|
function validate (tab, panel, next) { |
|
panel.className = panel.className.replace(/\bvalid\b/gi,'').trim(); |
|
tab.className = tab.className.replace(/\bdone\b/gi,'').trim(); |
|
if (Validation(panel)) { |
|
panel.className += ' valid'; |
|
tab.className += ' done'; |
|
next.click(); |
|
} |
|
} |
|
function navigation (tab, panel) { |
|
var i = [].indexOf.call(TABS, tab), |
|
next = i < (TABS.length-1) ? TABS[i+1] : false, |
|
prev = i === 0 ? false : TABS[i-1], |
|
v = panel.closest('form').contains(panel) && panel.closest('form').matches('[data-validate="true"]'), |
|
done = tab.className.match(/\bdone\b/gi) && panel.className.match(/\bvalid\b/gi), |
|
btnPrev = STEPS.querySelector('button:first-child'), |
|
btnNext = STEPS.querySelector('button:last-child'); |
|
btnPrev.disabled = true; |
|
btnPrev.hidden = true; |
|
btnNext.type = 'button'; |
|
if(!prev) { |
|
btnNext.onclick = function(e){ |
|
if(v && !done) validate(tab, panel, next); |
|
else { tab.className += ' done'; next.click(); } |
|
}; |
|
} |
|
else if (!next) { |
|
btnPrev.disabled = false; |
|
btnPrev.hidden = false; |
|
btnNext.type = 'submit'; |
|
btnPrev.onclick = function(e){ prev.click(); }; |
|
} |
|
else { |
|
btnPrev.disabled = false; |
|
btnPrev.hidden = false; |
|
btnPrev.onclick = function(e){ prev.click(); }; |
|
btnNext.onclick = function(e){ |
|
if(v && !done) validate(tab, panel, next); |
|
else { tab.className += ' done'; next.click(); } |
|
}; |
|
} |
|
} |
|
function position (tab) { |
|
LIST.setAttribute('scrolling',''); |
|
LIST.scrollLeft = tab.offsetLeft; |
|
LIST.removeAttribute('scrolling'); |
|
} |
|
function current (string) { |
|
var i = d.createElement('i'); |
|
i.innerHTML = string; |
|
i.className = 'visually-hidden'; |
|
return i; |
|
} |
|
function indicateTab () { |
|
var active = LIST.querySelector('[aria-selected="true"]'); |
|
TIND.style.width = active.offsetWidth +'px'; |
|
TIND.style.left = active.offsetLeft +'px'; |
|
} |
|
function indicatePanel (id) { |
|
var all = PIND.querySelectorAll('a'), |
|
active = PIND.querySelector('[href="#'+ id +'"]'); |
|
[].slice.call(all).forEach(function(a){ a.className = ''; }); |
|
active.className = 'active'; |
|
(active.previousElementSibling || all[all.length-1]).className = 'previous'; |
|
(active.nextElementSibling || all[0]).className = 'next'; |
|
} |
|
function indicator () { |
|
var wrap = d.createElement('div'); |
|
wrap.className = 'tab-panel-indicator'; |
|
PANELS[0].parentElement.appendChild(wrap); |
|
PANELS.forEach(function(p){ |
|
var a = d.createElement('a'), |
|
active = p.className.match(/\bactive\b/gi); |
|
a.setAttributes({ |
|
'href': '#'+ p.id, |
|
'id': p.id +'-indicator', |
|
'aria-controls': p.id, |
|
'aria-selected': active ? true : false, |
|
'tabindex': '-1' |
|
}); |
|
wrap.appendChild(a); |
|
}); |
|
return wrap; |
|
} |
|
function mousescroll () { |
|
var x, left, down = false; |
|
// Event listeners: |
|
LIST.addEventListener('mousedown', start, false); |
|
LIST.addEventListener('mousemove', move, false); |
|
LIST.addEventListener('mouseup', stop, false); |
|
LIST.addEventListener('mouseleave', stop, false); |
|
// Methods: |
|
function start (e) { |
|
LIST.setAttribute('drag', ''); |
|
down = true; |
|
x = e.pageX - LIST.offsetLeft; |
|
left = LIST.scrollLeft; |
|
}; |
|
function stop (e) { |
|
prevent(e); |
|
LIST.removeAttribute('drag'); |
|
LIST.removeAttribute('dragging'); |
|
LIST.removeAttribute('dragging-horizontal'); |
|
down = false; |
|
}; |
|
function move (e) { |
|
if(!down) return; |
|
prevent(e); |
|
LIST.removeAttribute('drag'); |
|
LIST.setAttribute('dragging', ''); |
|
LIST.setAttribute('dragging-horizontal', ''); |
|
var pX = e.pageX - LIST.offsetLeft, |
|
scroll = pX - x; |
|
LIST.scrollLeft = left - scroll; |
|
} |
|
} |
|
} |
|
|
|
// Tooltips |
|
function Tooltips (options) { |
|
var TIPS = d.querySelectorAll('[title]'), |
|
OFFSET = 9; |
|
[].slice.call(TIPS).forEach(function(tip){ |
|
if (tip.getAttribute('title') !== null) { |
|
tip.addEventListener('mouseenter', show, false); |
|
tip.addEventListener('mouseleave', hide, false); |
|
tip.addEventListener('focus', show, false); |
|
tip.addEventListener('blur', hide, false); |
|
} |
|
}); |
|
// Methods: |
|
function remove(e){ |
|
var trigger = this || (e || w.event).target, |
|
tip = d.getElementById(trigger.getAttribute('aria-describedby')); |
|
if(tip){ |
|
d.body.removeChild(tip); |
|
d.removeEventListener('scroll', function(){ position(trigger, tip); }, hasPassive); |
|
d.removeEventListener('resize', function(){ position(trigger, tip); }, false); |
|
d.removeEventListener('orientationchange', function(){ position(trigger, tip); }, false); |
|
trigger.setAttribute('title', trigger.getAttribute('data-original-title')); |
|
trigger.removeAttribute('data-original-title'); |
|
dispatchEvent('ui.tooltip.destroy', tip); |
|
} |
|
} |
|
function hide (e){ |
|
var trigger = this || (e || w.event).target, |
|
tip = d.getElementById(trigger.getAttribute('aria-describedby')); |
|
if(tip) tip.setAttribute('aria-hidden', true); |
|
dispatchEvent('ui.tooltip.hide', tip); |
|
} |
|
function show (e){ |
|
var trigger = this || (e || w.event).target, |
|
tip = d.getElementById(trigger.getAttribute('aria-describedby')); |
|
if(!tip) tip = create(trigger); |
|
position(trigger, tip); |
|
tip.setAttribute('aria-hidden', false); |
|
d.addEventListener('scroll', function(){ position(trigger, tip); }, hasPassive); |
|
w.addEventListener('resize', function(){ position(trigger, tip); }, false); |
|
w.addEventListener('orientationchange', function(){ position(trigger, tip); }, false); |
|
dispatchEvent('ui.tooltip.show', tip); |
|
} |
|
// Helper methods |
|
function position(trigger, tip){ |
|
var align = trigger.getAttribute('data-tooltip'), |
|
offset = trigger.hasAttribute('data-tooltip-offset') ? trigger.getAttribute('data-tooltip-offset') : OFFSET, |
|
vh = d.documentElement.clientHeight, vw = d.body.clientWidth, |
|
sY = w.scrollY || w.pageYOffset, sX = w.scrollX || w.pageXOffset, |
|
rect = trigger.getBoundingClientRect(), |
|
tW = rect.width, tH = rect.height, |
|
tL = rect.left + sX, tT = rect.top + sY, |
|
X = tL + tW/2, Y = tT + tH/2, |
|
W = tip.offsetWidth, H = tip.offsetHeight; |
|
var css = tip.style, icss = tip.querySelector('.indicator').style; |
|
var hT, hB, hR, hL; |
|
tip.className = 'ui-tooltip'+ ((align) ? ' '+ align : ''); |
|
css.top = (tT - H - offset) +'px'; |
|
css.left = (X - W/2) +'px'; |
|
if(!align || align.match(/(top|bottom)/gi)) { |
|
hT = (tT - sY) - H <= 0; |
|
hB = (tT - sY) + (tH + offset + H) >= vh; |
|
hR = (X + W/2) >= vw; |
|
hL = (X - W/2) <= 0; |
|
if(align === 'bottom') { css.top = (tT + tH + offset) +'px'; } |
|
if((hT && hB) || (hL && hR)) return; |
|
if(hT) { tip.className = 'ui-tooltip bottom'; css.top = (tT + tH + offset) +'px'; } |
|
if(hB) { tip.className = 'ui-tooltip'; css.top = (tT - tH - offset) +'px'; } |
|
if(hL) { css.left = offset +'px'; icss.left = tW/2 +'px'; } |
|
if(hR) { css.left = vw - (offset + W) +'px'; icss.left = 'auto'; icss.right = tW/2 + offset +'px'; } |
|
} |
|
if(align && align.match(/(right|left)/gi)) { |
|
hR = (tL + tW + W + offset) >= vw ; |
|
hL = tL - (W + offset) <= 0; |
|
if((hL && hR)) return tip.className = 'ui-tooltip'; |
|
css.top = (Y - H/2) +'px'; |
|
if(align === 'right') css.left = (tL + tW + offset) +'px'; |
|
if(align === 'left') css.left = (tL - W - offset) +'px'; |
|
if(hR) { tip.className = 'ui-tooltip left'; css.left = (tL - W - offset) +'px'; } |
|
if(hL) { tip.className = 'ui-tooltip right'; css.left = (tL + tW + offset) +'px'; } |
|
} |
|
} |
|
function create (trigger){ |
|
var tip = d.createElement('div'), |
|
content = trigger.getAttribute('title'), |
|
sanitized = d.createTextNode(content); |
|
tip.id = 'tooltip-'+ Math.random().toString(36).substr(2, 8); |
|
tip.setAttribute('role', 'tooltip'); |
|
tip.className = 'ui-tooltip'; |
|
tip.innerHTML = '<i class="indicator" aria-hidden="true"></i>'; |
|
tip.appendChild(sanitized); |
|
tip.setAttribute('aria-hidden', true); |
|
trigger.setAttribute('aria-describedby', tip.id); |
|
trigger.removeAttribute('title'); |
|
trigger.setAttribute('data-original-title', sanitized.textContent); |
|
d.body.insertAdjacentElement('beforeend', tip); |
|
return tip; |
|
} |
|
} |
|
|
|
// Toasts |
|
function Toast(message, type, ms) { |
|
var MS = ms ? parseInt(ms, 10) : 10000, |
|
TOAST = d.getElementById('ui-toast'), |
|
TYPE = type ? type : '', |
|
TXT = message ? message.message || message.toString() : i18n.unknown; |
|
if(!TOAST) { TOAST = d.createElement('div'); TOAST.id = 'ui-toast'; } |
|
clearTimeout(w.toastTimeout); |
|
TOAST.className = TYPE; |
|
TOAST.innerHTML = TXT; |
|
TOAST.addEventListener('click', close, false); |
|
d.body.appendChild(TOAST); |
|
TOAST.className += ' visible'; |
|
dispatchEvent('ui.toast.show', TOAST); |
|
// Autohide error / success toast |
|
if(TYPE.match(/(error|success)/gi)) w.toastTimeout = setTimeout(close, MS); |
|
// Method: |
|
// Close Toast |
|
function close (e){ |
|
TOAST.className += ' closing'; |
|
dispatchEvent('ui.toast.hide', TOAST); |
|
setTimeout(function() { TOAST.removeAttribute('class'); }, 500); |
|
clearTimeout(w.toastTimeout); |
|
} |
|
} |
|
|
|
// Password Reveal Input |
|
function passwordInput(el) { |
|
var reveal = d.createElement('button'), |
|
wrap = d.createElement('div'), |
|
label = el.parentNode.querySelector('label'), |
|
float = el.parentNode.className.match(/form-float-label/gi); |
|
wrap.className = 'form-password'; |
|
el.setAttribute('tabindex', 0); |
|
reveal.setAttributes({'class':'btn-reveal', 'type':'button', 'tabindex':-1, 'aria-label':i18n.reveal}); |
|
reveal.innerHTML = '<svg viewBox="0 0 9 9">'+ icons.reveal +'</svg>'; |
|
el.parentNode.insertBefore(wrap, el); |
|
wrap.appendChild(el); |
|
wrap.insertBefore(reveal, el.nextSibling); |
|
if(label && float) wrap.insertBefore(label, reveal.nextSibling); |
|
reveal.onclick = toggle; |
|
// Check for changes |
|
observe(el, set, {attributes: true}); |
|
// Methods: |
|
function toggle () { |
|
if(el.type != 'password') { |
|
el.type = 'password'; |
|
reveal.innerHTML = '<svg viewBox="0 0 9 9">'+ icons.reveal +'</svg>'; |
|
reveal.setAttribute('aria-label', i18n.reveal); |
|
} else { |
|
el.type ='text'; |
|
reveal.innerHTML = '<svg viewBox="0 0 9 9">'+ icons.hide +'</svg>'; |
|
reveal.setAttribute('aria-label', i18n.hide); |
|
} |
|
el.focus(); |
|
} |
|
function set () { |
|
el.disabled ? reveal.setAttribute('disabled', true) : reveal.removeAttribute('disabled'); |
|
} |
|
} |
|
|
|
// Search Clear Input |
|
function searchInput(el) { |
|
var clear = d.createElement('button'), |
|
wrap = d.createElement('div'), |
|
label = el.parentNode.querySelector('label'), |
|
float = el.parentNode.className.match(/form-float-label/gi); |
|
wrap.className = 'form-search'; |
|
el.setAttribute('tabindex', 0); |
|
clear.setAttributes({'class':'btn-clear', 'type':'button', 'tabindex':-1, 'aria-label':i18n.clear}); |
|
clear.innerHTML = '<svg viewBox="0 0 9 9">'+ icons.close +'</svg>'; |
|
el.parentNode.insertBefore(wrap, el); |
|
wrap.appendChild(el); |
|
wrap.insertBefore(clear, el.nextSibling); |
|
if(label && float) wrap.insertBefore(label, clear.nextSibling); |
|
clear.onclick = function(){ el.value = ''; el.focus(); }; |
|
// Check for changes |
|
observe(el, set, {attributes: true}); |
|
// Method: |
|
function set(){ |
|
el.disabled ? clear.setAttribute('disabled', true) : clear.removeAttribute('disabled'); |
|
} |
|
} |
|
|
|
// Input Number Spinner |
|
function numberInput(el) { |
|
var wrap = d.createElement('div'), |
|
plus = d.createElement('button'), |
|
minus = d.createElement('button'), |
|
step = parseFloat(el.getAttribute('step')); |
|
plus.innerHTML = '+'; |
|
minus.innerHTML = '−'; |
|
wrap.className = 'form-number'; |
|
el.setAttribute('tabindex', 0); |
|
plus.setAttributes({'class':'btn', 'type':'button', 'tabindex':-1, 'aria-label':i18n.plus}); |
|
minus.setAttributes({'class':'btn', 'type':'button', 'tabindex':-1, 'aria-label':i18n.minus}); |
|
plus.onclick = function(){ el.stepUp(step); }; |
|
minus.onclick = function(){ el.stepDown(step); }; |
|
el.parentNode.insertBefore(wrap, el); |
|
wrap.appendChild(el); |
|
wrap.insertBefore(minus, el); |
|
wrap.insertBefore(plus, el.nextSibling); |
|
// Check for changes |
|
observe(el, set, {attributes: true}); |
|
// Method: |
|
function set(){ |
|
if (el.disabled) { |
|
plus.disabled = true; |
|
minus.disabled = true; |
|
wrap.setAttribute('disabled', true); |
|
} |
|
else { |
|
plus.disabled = false; |
|
minus.disabled = false; |
|
wrap.removeAttribute('disabled'); |
|
} |
|
} |
|
} |
|
|
|
// Range Input |
|
function rangeInput(el) { |
|
var output = el.parentNode.querySelector('output'), |
|
unit = el.getAttribute('data-unit'), |
|
ini = el.defaultValue || el.value || 0; |
|
if(!output) { |
|
output = d.createElement('output'); |
|
el.parentNode.appendChild(output); |
|
}; |
|
el.addEventListener('input', set, false); |
|
el.form.addEventListener('reset', set, false); |
|
set(); |
|
// Method: |
|
function set(){ |
|
var min = parseFloat(el.min) || 0, |
|
max = parseFloat(el.max) || 1, |
|
value = el.value || ini, |
|
r = 100/(max-min); |
|
el.style.setProperty('--rangeValue', (value-min)*r); |
|
output.setAttribute('aria-live', 'assertive'); |
|
output.innerHTML = (value-min)*r + (unit ? unit : ''); |
|
} |
|
} |
|
|
|
// Swipe |
|
function Swipe (target, axis, zone, callback, remove, minPx) { |
|
if(!target) return alert('No swipe target defined. Aborting...'); |
|
var self = this, root = d.documentElement; |
|
remove = remove ? remove : false; // Remove initial listener from target? |
|
axis = axis.match(/(x|h|hor|horiz|horizontal)/gi) ? 'h' : 'v'; |
|
zone = zone ? zone : target; |
|
var ratio = w.devicePixelRatio || 1; |
|
var x, y, sX = 0, sY = 0, eX = 0, eY = 0, dX, dY, deg, degV = 70, degH = 30, |
|
minPx = minPx ? minPx*ratio : 35*ratio, minMs = 200, sT = null, eT = null, |
|
distance, direction, duration, dragging = false; |
|
target.removeAttribute('style'); |
|
// Event listeners |
|
zone.addEventListener('mousedown', start, false); |
|
zone.addEventListener('touchstart', start, hasPassive); |
|
// Methods: |
|
function start(e) { |
|
if(e.target.tagName.match(/(input|textarea|select|button)/gi)) return; |
|
if(dragging) end(e); |
|
e.preventDefault(); |
|
var touches = e.changedTouches || e.touches; |
|
sX = touches ? touches[0].screenX : e.clientX; |
|
sY = touches ? touches[0].screenY : e.clientY; |
|
eT = null; sT = e.timeStamp; |
|
target.style = ''; |
|
dragging = true; |
|
zone.setAttribute(axis === 'h' ? 'pan-x' : 'pan-y', ''); |
|
// zone.setAttribute('swiping', ''); |
|
// root.setAttribute('manipulating', ''); |
|
// target.setAttribute('noanimate', ''); |
|
// Event mouse listeners |
|
zone.addEventListener('mousemove', move, false); |
|
root.addEventListener('mouseup', end, false); |
|
zone.addEventListener('mouseleave', end, false); |
|
// Event touch listeners |
|
zone.addEventListener('touchmove', move, hasPassive); |
|
root.addEventListener('touchend', end, false); |
|
zone.addEventListener('touchcancel', end, false); |
|
} |
|
function move(e) { |
|
if(!dragging || !sT) return; |
|
var touches = e.changedTouches || e.touches; |
|
eX = touches ? touches[0].screenX : e.clientX; |
|
eY = touches ? touches[0].screenY : e.clientY; |
|
x = eX - sX; |
|
y = eY - sY; |
|
dX = Math.abs(x); |
|
dY = Math.abs(y); |
|
deg = Math.atan2(dY, dX)*(180/Math.PI); |
|
if (axis === 'h' && deg > degH) return; |
|
else if (axis === 'v' && deg < degV) return; |
|
else raf(transform); |
|
} |
|
function end(e) { |
|
e.preventDefault(); |
|
dragging = false; |
|
eT = e.timeStamp; |
|
dispatch(e); |
|
target.removeAttribute('style'); |
|
zone.removeAttribute('swiping'); |
|
root.removeAttribute('manipulating');// |
|
zone.removeAttribute(axis === 'h' ? 'pan-x' : 'pan-y'); |
|
// target.removeAttribute('noanimate'); |
|
sT = null; eT = null; deg = null; |
|
distance = 0, direction = null; |
|
// Remove mouse event listener |
|
zone.removeEventListener('mousemove', move, false); |
|
root.removeEventListener('mouseup', end, false); |
|
zone.removeEventListener('mouseleave', end, false); |
|
// Remove touch event listener |
|
zone.removeEventListener('touchmove', move, hasPassive); |
|
root.removeEventListener('touchend', end, false); |
|
zone.removeEventListener('touchcancel', end, false); |
|
} |
|
function dispatch (e) { |
|
duration = eT - sT; |
|
var xy = Math.abs(x/y), yx = Math.abs(y/x); |
|
if(Math.abs(x) + Math.abs(y) <= minPx/3) { |
|
if(duration < minMs) callback({direction: 'tab', distance: 0, duration: duration}); |
|
else callback({direction: 'press', distance: 0, duration: duration}); |
|
return; |
|
} |
|
if(axis === 'h') { |
|
if (eX <= sX && deg < degH) { distance = sX - eX; direction = 'left'; } |
|
if (eX >= sX && deg < degH) { distance = eX - sX; direction = 'right'; } |
|
} else { |
|
if (eY <= sY && deg > degV) { distance = sY - eY; direction = 'up'; } |
|
if (eY >= sY && deg > degV) { distance = eY - sY; direction = 'down'; } |
|
} |
|
if (distance > minPx && duration > minMs && direction) { |
|
act(direction, distance, duration, e); |
|
} |
|
} |
|
function act (direction, distance, duration, e) { |
|
callback({ direction: direction, distance: distance, duration: duration }); |
|
if (remove) { |
|
zone.removeEventListener('mousedown', start, false); |
|
zone.removeEventListener('touchstart', start, hasPassive); |
|
} |
|
} |
|
function transform() { |
|
axis === 'h' ? y = 0 : x = 0; |
|
target.style.webkitTransform = 'translate('+ x +'px,'+ y +'px)'; |
|
target.style.mozTransform = 'translate('+ x +'px,'+ y +'px)'; |
|
target.style.msTransform = 'translate('+ x +'px,'+ y +'px)'; |
|
target.style.oTransform = 'translate('+ x +'px,'+ y +'px)'; |
|
target.style.transform = 'translate('+ x +'px,'+ y +'px)'; |
|
} |
|
} |
|
|
|
// Form validation |
|
function Validation (el){ |
|
var ERRORS = [], |
|
RX = { |
|
email: /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/, |
|
url: '[(http(s)?):\/\/(www\.)?a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)', |
|
date: '\d{2,4}(\.|-|\/)\d{2}(\.|-|\/)\d{2,4}$', |
|
time: '^((([0-1]?[0-9])|([2][0-3])):)?(([0-5][0-9]):)?([0-5][0-9])(\.\d{1,3})?$|^\d+(\.\d{1,3})?$', |
|
datetime: '(0?[1-9]|[12][0-9]|3[01])[\/\-](0?[1-9]|1[012])[\/\-]\d{4}(\s\d{1,2}[:-]\d{2}([:-]\d{2,3})*)?', |
|
number: '(^\d{1,10}$)', |
|
password: '', |
|
tel: '([+(\d]{1})(([\d+() -.]){5,20})([+(\d]{1})', |
|
color: '^((rgb|hsl)(a?)\(([\d\.\-\s,]{5,})\)$)|^((#)(([0-9A-F]{3})$|([0-9A-F]{6})$))', |
|
clearhtml: '(<script(\s|\S)*?<\/script>)|(<style(\s|\S)*?<\/style>)|(<!--(\s|\S)*?-->)|(<\/?(\s|\S)*?>)' |
|
}, |
|
FORM = el.tagName.match(/form/gi), |
|
SET = el instanceof Element ? el : d.querySelector(el); |
|
if(FORM) { |
|
SET.setAttribute('novalidate', true); |
|
SET.addEventListener('submit', function(e) { e.preventDefault(); validate(SET); }, false); |
|
} |
|
else return validate(SET); |
|
// Methods: |
|
// Validate form / sets / form parts |
|
function validate (set) { |
|
var inputs = set.querySelectorAll('[required]:not(:disabled), [pattern]:not(:disabled)'); |
|
[].slice.call(inputs).forEach(function(i){ checkInput(i, set); }); |
|
if(ERRORS.length) scrollToError(set); |
|
else if(FORM) set.submit(); |
|
else return true; |
|
} |
|
// Input validation |
|
function checkInput(input, set) { |
|
var group = input.closest('.form-group') || set, |
|
label = input.closest('label') ? input.closest('label').innerText : input.getAttribute('placeholder') || input.id, |
|
type = input.type.split('-')[0].toLowerCase(), |
|
value = input.value.trim(), |
|
error = input.getAttribute('data-error') || i18n.error +': '+ label, |
|
pattern = input.getAttribute('pattern'), |
|
min = input.getAttribute('min') || input.getAttribute('minlength'), |
|
max = input.getAttribute('max') || input.getAttribute('maxlength'), |
|
len = input.getAttribute('type') === ('number' || 'range') ? input.value : input.value.length; |
|
if (value === '') setError(input, error, group, set); |
|
else if (pattern && !RegExp(pattern, 'g').test(value)) setError(input, error, group, set); |
|
else if (!pattern && !RegExp(RX[type] || '', 'g').test(value)) setError(input, error, group, set); |
|
else if (min && min > len) setError(input, error, group, set); |
|
else if (max && max < len) setError(input, error, group, set); |
|
else removeError(input, group, set); |
|
} |
|
// Set error |
|
function setError (input, error, group, set) { |
|
var hasError = group.className.match(/\bhas-error\b/gi), |
|
hasHint = group.querySelector('.hint'); |
|
if (!hasError) { |
|
group.className += ' has-error'; |
|
group.setAttribute('title', error); |
|
} |
|
input.setAttribute('aria-invalid', true); |
|
if(hasHint) input.setAttribute('aria-describedby', hasHint.id); |
|
ERRORS.push({ |
|
'set': '#'+ set.id, |
|
'input': '#'+ input.id, |
|
'message': error |
|
}); |
|
input.addEventListener('change', function(){ checkInput(input, set); }, hasPassive); |
|
// input.addEventListener('keyup', function(){ checkInput(input, set); }, hasPassive); |
|
if(FORM) group.form.addEventListener('reset', function(e) { removeError(input, group, set); }, false); |
|
setTimeout(function(){ displayErrorBadge(set); }, 5); |
|
} |
|
// Remove error |
|
function removeError(input, group, set) { |
|
ERRORS.splice(ERRORS.indexOf('#'+ input.id), 1); |
|
group.className = group.className.replace(/\bhas-error\b/gi,'').trim(); |
|
group.removeAttribute('title'); |
|
input.removeAttribute('aria-invalid'); |
|
input.className = input.className.replace(/is-error/gi,'').trim(); |
|
input.removeEventListener('change', function(){ checkInput(input, set); }, hasPassive); |
|
// input.removeEventListener('keyup', function(){ checkInput(input, set); }, hasPassive); |
|
if(FORM) group.form.removeEventListener('reset', function(e){ removeError(input, group, set); }, false); |
|
setTimeout(function(){ displayErrorBadge(set); }, 5); |
|
} |
|
// Scroll to first error in set |
|
function scrollToError (set){ |
|
var first = set.querySelector('.has-error'), |
|
scroll = scrollParent(first.parentNode); // Scroll to label |
|
if(scroll) smoothScroll(first, scroll); |
|
first.querySelector('input, select, textarea').focus(); |
|
} |
|
// Display badge in tab |
|
function displayErrorBadge (set){ |
|
var isTabbed = set.closest('[role="tabpanel"]').contains(el); |
|
if (!isTabbed) return; |
|
var count = 0; |
|
ERRORS.forEach(function(e){ if(e.set === '#'+set.id) count++; }); |
|
var panel = set.closest('[role="tabpanel"]'), |
|
tab = d.querySelector('a[href="#'+ panel.id +'"]'), |
|
badge = tab.querySelector('.badge'); |
|
if(tab) { |
|
if(count <= 0) { |
|
if(badge) tab.removeChild(badge); |
|
return; |
|
} |
|
else if(!badge) { |
|
badge = d.createElement('b'); |
|
tab.appendChild(badge); |
|
} |
|
badge.setAttributes({ |
|
'class': 'badge badge-circle badge-danger scale', |
|
'aria-live': 'assertive', |
|
'title': count +' '+ (count > 1 ? i18n.errors : i18n.error), |
|
'data-tooltip': 'top' |
|
}); |
|
badge.innerHTML = count; |
|
} |
|
} |
|
} |
|
|
|
// Backdrop |
|
function Backdrop (el, color) { |
|
var DROP = d.getElementById('backdrop'); |
|
if(!DROP) { DROP = d.createElement('div'); DROP.id = 'backdrop'; } |
|
DROP.className = 'visible'; |
|
theme(w.getComputedStyle(DROP).backgroundColor); |
|
// Methods: |
|
function theme (bgcolor) { |
|
var metas = ['theme-color', 'msapplication-navbutton-color', 'apple-mobile-web-app-status-bar-style'], |
|
head = d.getElementByTagName('head')[0], |
|
icon = head.querySelector('link[rel*=icon]'), |
|
color = head.querySelector('meta["'+ metas[0] +'"]').getAttribute('content'), |
|
style = head.querySelector('meta["'+ metas[2] +'"]').getAttribute('content'), |
|
black = bgcolor.replace(' ','').match(/(#000|#000000|0,0,0)/gi) || style.match(/(black)/gi); |
|
function set () { |
|
metas.forEach(function(meta){ |
|
var entry = head.querySelector('meta["'+ meta +'"]'); |
|
if(!entry) head.insertAdjacentElement('beforeend', d.createElement('meta')); |
|
entry.setAttribute('name', meta); |
|
if(meta.match(/(status-bar)/gi)) entry.setAttribute('content', 'translucent' + black ? '-black' : ''); |
|
else entry.setAttribute('content', bgcolor); |
|
}); |
|
} |
|
function reset(e){ |
|
metas.forEach(function(meta){ |
|
var entry = head.querySelector('meta["'+ meta +'"]'); |
|
if(meta.match(/(status-bar)/gi)) entry.setAttribute('content', style); |
|
else entry.setAttribute('content', color); |
|
}); |
|
} |
|
} |
|
} |
|
|
|
// Sccroll reveal when in viewport |
|
function scrollReveal(selectors, reverse) { |
|
var items = d.querySelectorAll(selectors); |
|
// Listeners |
|
w.addEventListener('orientationchange', set); |
|
w.addEventListener('resize', set); |
|
w.addEventListener('scroll', set); |
|
set(); |
|
// Method |
|
function set() { |
|
[].slice.call(items).forEach(function(item){ |
|
var cn = item.className.replace(/(is-hidden|in-view)/g,'').trim(); |
|
if (inViewport(item)) item.className = (cn +' in-view').trim(); |
|
else item.className = (cn +' is-hidden').trim(); |
|
}); |
|
} |
|
} |
|
|
|
// Smooth scrolling |
|
function smoothScroll(selectors, root, ms){ |
|
var MS = ms ? ms : 500, |
|
ROOT = root && isElement(root) ? root : d.documentElement, |
|
qubicFn = function (t) { return t*t*t; }, |
|
isNative = 'scroll-behavior' in ROOT.style, |
|
isSingle = isElement(selectors); |
|
if(!isSingle) { |
|
[].slice.call(d.querySelectorAll(selectors)).forEach(function(el) { |
|
ROOT = scrollParent(el); |
|
el.addEventListener('click', isNative ? native : scroll, false); |
|
}); |
|
} |
|
else { |
|
ROOT = scrollParent(selectors); |
|
selectors.addEventListener('click', isNative ? native : scroll, false); |
|
} |
|
// API method, requires css scroll-behavior support |
|
function native (e) { |
|
var hash = this.hash || this.getAttribute('aria-controls') || this.getAttribute('data-target'), |
|
target = hash ? d.getElementById(hash.replace(/#/g,'').trim()) : null, |
|
offset = this.getAttribute('data-scrolloffset') || (target ? w.getComputedStyle(target, null).scrollMarginTop : 0); |
|
if(target) { |
|
e.preventDefault(); |
|
ROOT.style.scrollPadding = parseInt(offset, 10) +'px'; |
|
ROOT.setAttribute('scrolling',''); |
|
setTimeout(function(){ |
|
ROOT.removeAttribute('scrolling'); |
|
// Trigger scroll eventlistener |
|
w.scroll(0, w.scrollY + 1); |
|
w.scroll(0, w.scrollY - 1); |
|
}, MS); |
|
} |
|
if(hash) w.location.hash = hash; |
|
} |
|
// Native scroll method, interval frame ease animation |
|
function scroll (e) { |
|
var hash = this.hash || this.getAttribute('aria-controls') || this.getAttribute('data-target'), |
|
target = hash ? d.getElementById(hash.replace(/#/g,'').trim()) : null, |
|
offset = this.getAttribute('data-scrolloffset') || (target ? w.getComputedStyle(target, null).scrollMarginTop : 0); |
|
if(target) { |
|
e.preventDefault(); |
|
raf(function () { |
|
var T = new Date().getTime(), sT = T, sY = w.pageYOffset, |
|
top = target.getBoundingClientRect().top - parseInt(offset, 10); |
|
frame(sT, T, MS, top, sY, hash); |
|
}); |
|
} |
|
} |
|
function frame (sT, T, ms, top, sY, hash) { |
|
var time = T - sT, ease = qubicFn(time/ms); |
|
w.scroll(0, sY + (top * ease)); |
|
if(time < ms) raf(function() { frame(sT, new Date().getTime(), ms, top, sY, hash); }); |
|
else if(hash) w.location.hash = hash; |
|
} |
|
} |
|
|
|
// Notifications API: Send notification (IE 11 and lower isn't supported.) |
|
function notification (title, body, icon, img, sound) { |
|
if (!('Notification' in w) || !allowNotifications) return false; |
|
var title = title || 'Undefined title', |
|
badge = d.querySelector('link[rel="mask-icon"]'), |
|
options = { |
|
body: body || 'Undefined content', |
|
icon: icon ? icon : null, |
|
badge: badge ? badge.href : null, |
|
image: img ? img : null, |
|
vibrate: [400], |
|
sound: audiopool.pop |
|
}; |
|
if (Notification.permission === 'granted') return new Notification(title, options); |
|
else { Notification.requestPermission(permission => { |
|
if (permission === 'granted') return new Notification(title, options); |
|
else return alert('Permission '+ permission +'.', {title: 'Notification API'}); |
|
}); |
|
} |
|
} |
|
|
|
// Color scheme |
|
function Scheme (toggler) { |
|
var ROOT = d.documentElement, |
|
HEAD = d.querySelector('head'), |
|
BODY = d.body || ROOT.body, |
|
userPrefers, mediaQuery, darkScheme, saved; |
|
saved = w.localStorage.getItem('UiColorScheme'); |
|
userPrefers = saved || w.getComputedStyle(ROOT, null).getPropertyValue('color-scheme').replace(/"/g, ''); |
|
darkScheme = BODY.getAttribute('data-scheme') === 'dark'; |
|
if(w.matchMedia) mediaQuery = w.matchMedia('(prefers-color-scheme: light)'); |
|
if(mediaQuery) mediaQuery.onchange = toggle; |
|
if(userPrefers === 'light' && darkScheme) toggle; |
|
// Init Scheme or Element Listeners: |
|
toggler = toggler instanceof Element ? toggler : d.querySelector(toggler); |
|
if(toggler) { |
|
toggler.onchange = toggle; |
|
toggler.checked = darkScheme ? true : false; |
|
} |
|
else toggle; |
|
// Methods: |
|
function toggle (e) { |
|
darkScheme = toggler ? toggler.checked : !darkScheme; |
|
BODY.setAttribute('data-scheme', darkScheme ? 'dark' : 'light'); |
|
// Wait for transitions to end. |
|
BODY.addEventListener('transitionend', set, false); |
|
} |
|
function set () { |
|
var theme = HEAD.querySelector('meta[name="theme-color"]'), |
|
iosbar = HEAD.querySelector('meta[name="apple-mobile-web-app-status-bar-style"]'), |
|
scheme = HEAD.querySelector('meta[name="color-scheme"]'), |
|
color = w.getComputedStyle(BODY).getPropertyValue('background-color'); |
|
if(!theme) theme = createMeta('theme-color'); |
|
if(!scheme) scheme = createMeta('color-scheme'); |
|
if(!iosbar) iosbar = createMeta('apple-mobile-web-app-status-bar-style'); |
|
BODY.removeEventListener('transitionend', set, false); |
|
ROOT.style.setProperty('color-scheme', darkScheme ? 'dark' : 'light'); |
|
scheme.content = darkScheme ? 'dark' : 'light'; |
|
iosbar.content = darkScheme ? 'black-translucent' : 'default'; |
|
theme.content = color; |
|
ROOT.offsetWidth; // force reflow, resolves scrollbar style bug? |
|
// Save preference and clean-up |
|
w.localStorage.setItem('UIColorScheme', darkScheme ? 'dark' : 'light'); |
|
BODY.removeEventListener('transitionend', set, false); |
|
// Method: Create and append <meta/> |
|
function createMeta (name) { |
|
var m = d.createElement('meta'); |
|
m.name = name; HEAD.appendChild(m); |
|
return m; |
|
} |
|
} |
|
} |
|
|
|
// Ripple effects |
|
function Ripple (e) { |
|
var trigger = e.target || w.event.target, |
|
touch = e.changedTouches || e.touches, |
|
ink = trigger.querySelector('.ink'), x, y; |
|
if(!ink) { |
|
ink = d.createElement('i'); |
|
trigger.appendChild(ink); |
|
ink.setAttribute('aria-hidden', true); |
|
} |
|
ink.className = 'ink'; |
|
if(!ink.offsetHeight && !ink.offsetWidth) { |
|
var dia = Math.max(trigger.offsetWidth, trigger.offsetHeight); |
|
ink.style.cssText = 'height:'+ dia +'px; width:'+ dia +'px;'; |
|
} |
|
x = (touch ? touch[0].screenX : e.clientX) - trigger.offsetLeft - ink.offsetWidth/2; |
|
y = (touch ? touch[0].screenY : e.clientY) - trigger.offsetTop - ink.offsetHeight/2; |
|
ink.style.cssText = 'top:'+ y +'px; left:'+ x +'px;'; |
|
ink.className += ' animating'; |
|
setTimeout(function(){ |
|
ink.className = 'ink'; |
|
ink.removeAttribute('style'); |
|
}, 20000); |
|
} |
|
|
|
// Network Status |
|
function Online () { |
|
var msg = d.createElement('div'), |
|
status = check; |
|
msg.id = 'network-status'; |
|
msg.className = 'network-status'; |
|
msg.setAttributes({ |
|
'role': 'alert', |
|
'aria-live': 'assertive', |
|
'aria-atomic': true, |
|
'tabindex': '-1', |
|
'draggable': '' |
|
}); |
|
// Listeners |
|
w.addEventListener('ui.online', update, false); |
|
// Methods; |
|
function update () { |
|
status = check; |
|
if(!status) { |
|
|
|
} |
|
} |
|
function check () { |
|
var xhr = new XMLHttpRequest(); |
|
xhr.open('HEAD', '//'+ w.location.hostname +'/?r='+ Date.now(), false); |
|
try { xhr.send(); return (xhr.status >= 200) && (xhr.status < 300 || xhr.status === 304); } |
|
catch (error) { return false; } |
|
} |
|
} |
|
|
|
// Helpers: |
|
// Get closest element with specific selector |
|
function getClosest (el, selector) { |
|
for ( ; el && el !== d; el = el.parentNode ) { |
|
if (el.matches(selector)) return el; // Need el.matches() support |
|
} |
|
return null; |
|
}; |
|
// Set global viewport height CSS variable |
|
function setViewport (){ |
|
w.addEventListener('resize', set, false); |
|
w.addEventListener('orientationchange', set, false); |
|
set(); |
|
function set(){ |
|
d.documentElement.style.setProperty('--vh', (w.innerHeight * 0.01) +'px'); |
|
} |
|
} |
|
// Check if element is partially in viewport |
|
function inViewport(el) { |
|
var r = el.getBoundingClientRect(), |
|
x = r.left, y = r.top, |
|
W = el.clientWidth, H = el.clientHeight, |
|
vW = Math.max(d.documentElement.clientWidth, w.innerWidth || 0), |
|
vH = Math.max(d.documentElement.clientHeight, w.innerHeight || 0); |
|
return (y < vH && y+H > 0) && (x < vW && x+W > 0); |
|
} |
|
// Focus trap |
|
function trapFocus (e) { |
|
if (!e.relatedTarget || !this.contains(e.relatedTarget)) { |
|
var f = this.querySelectorAll('button,[href],input,select,textarea,[tabindex]:not([tabindex="-1"])'); |
|
if(f) setTimeout(function() { f[0].focus(); }, 1); |
|
} |
|
} |
|
// Check if event happens inside an element |
|
function isTarget (e, el) { |
|
var target = e.target; |
|
do { if (target == el) return true; target = target.parentNode; } |
|
while (target); |
|
return false; |
|
} |
|
// Get next overflow scroll parent |
|
function scrollParent(node) { |
|
if (!node || node.nodeType != 1) return null; |
|
var oY = w.getComputedStyle(node).overflowY, |
|
isScroll = oY !== 'visible' && oY !== 'hidden'; |
|
if (isScroll && node.scrollHeight > node.clientHeight) return node; |
|
else return scrollParent(node.parentNode); |
|
} |
|
// Url parts |
|
function urlVars() { |
|
var vars = {}; |
|
w.location.href.replace(/[?&]+([^=&]+)=([^&]*)/gi, function(m, k, v) { vars[k] = v; } ); |
|
return vars; |
|
} |
|
// Create and append or modify <meta/> |
|
function setMeta (name, content) { |
|
var exists = d.querySelector('meta[name='+name+']'), |
|
m = exists ? exists : d.createElement('meta'); |
|
m.name = name; m.content = content; |
|
if(!exists) d.querySelector('head').appendChild(m); |
|
} |
|
// Get value of css var |
|
function rootVar(name){ |
|
var root = d.querySelector(':root'); |
|
return w.getComputedStyle(root).getPropertyValue(name); |
|
} |
|
// Format bytes |
|
function formatBytes (bytes, dec) { |
|
var s = ['Bytes','KB','MB','GB','TB','PB','EB','ZB','YB'], |
|
dec = dec ? dec : 2; |
|
for(var i = 0, r = bytes, b = 1024; r > b; i++) r /= b; |
|
return parseFloat(r.toFixed(dec)) +' '+ s[i]; |
|
} |
|
// Color conversion |
|
function rgb2hex(string) { |
|
var parts = (string || '').match(/\d+/g); |
|
return "#" + (16777216 | b | (g << 8) | (r << 16)).toString(16).slice(1); |
|
} |
|
// First letter upper case |
|
function ucFirst(string) { |
|
return string.charAt(0).toUpperCase() + string.slice(1); |
|
} |
|
// Color contrast |
|
function contrast(color) { |
|
var r, g, b, hsp, threshold = 127.5; |
|
if (color.match(/^rgb/)) { |
|
color = color.match(/^rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*(\d+(?:\.\d+)?))?\)$/); |
|
r = color[1]; |
|
g = color[2]; |
|
b = color[3]; |
|
} else { |
|
color = +('0x' + color.slice(1).replace(color.length < 5 && /./g, '$&$&')); |
|
r = color >> 16; |
|
g = color >> 8 & 255; |
|
b = color & 255; |
|
} |
|
hsp = Math.sqrt(0.299 * (r * r) + 0.587 * (g * g) + 0.114 * (b * b)); |
|
if (hsp > threshold) return 'light'; |
|
else return 'dark'; |
|
} |
|
function contrast4hex(hex){ |
|
var r = parseInt(hex.substr(0,2),16), g = parseInt(hex.substr(2,2),16), b = parseInt(hex.substr(4,2),16), |
|
yiq = ((r*299)+(g*587)+(b*114))/1000; |
|
return (yiq >= 128) ? 'dark' : 'light'; |
|
} |
|
// Add multiple event listeners to element |
|
function addListeners (el, events, fn) { |
|
var evts = events.split(' '); |
|
for (var i = 0; i < evts.length; i++) { |
|
el.addEventListener(evts[i], fn, false); |
|
} |
|
} |
|
// Remove listenners from element by type |
|
function removeListeners (el, event){ |
|
var evts = getEventListeners(el)[event]; |
|
for(var i = 0; i < evts.length; i++){ |
|
el.removeEventListener(event, evts[i].listener); |
|
} |
|
} |
|
// Prefixed JS CSS event listener |
|
function addPrefixedListener (el, type, callback) { |
|
var prefix = ['webkit', 'moz', 'MS', 'o', '']; |
|
for (var p = 0; p < prefix.length; p++) { |
|
if (!prefix[p]) type = type.toLowerCase(); |
|
el.addEventListener(prefix[p]+type, callback, false); |
|
} |
|
} |
|
// Replace / refresh / update listenner |
|
function replaceListener (event, fn, passive, el) { |
|
el.removeEventListener(event, fn, passive); |
|
el.addEventListener(event, fn, passive); |
|
} |
|
// Observe changes in DOM Elements |
|
function observe (el, call, opts){ |
|
var MutationObserver = w.MutationObserver || w.WebKitMutationObserver; |
|
if( !el || el.nodeType != 1 ) return; // validation |
|
if( MutationObserver ){ |
|
var o = new MutationObserver(call); |
|
o.observe( el, opts || {attributes: true, childList: true, subtree: true}); |
|
return o; |
|
} |
|
else if( w.addEventListener ){ |
|
el.addEventListener('DOMNodeInserted', call, false); |
|
el.addEventListener('DOMNodeRemoved', call, false); |
|
} |
|
} |
|
// Add event observe |
|
function dispatchEvent(name, el, data) { |
|
var e = d.createEvent('Event'); |
|
e.initEvent(name, true, true); |
|
e.data = data || {}; |
|
if(el.nodeType === 1) e.data.target = el; |
|
w.dispatchEvent(e); |
|
} |
|
// Check if object is a dom node element |
|
function isElement (o) { |
|
return (o instanceof Element || o instanceof HTMLDocument) && o instanceof Node; |
|
} |
|
// Prevent event bubbling |
|
function prevent (e) { |
|
e = (e) ? e : w.event; |
|
e.preventDefault(); |
|
e.stopPropagation(); |
|
} |
|
// Prevent event propagation |
|
function nopropagate (e) { |
|
e = (e) ? e : w.event; |
|
e.stopPropagation(); |
|
} |
|
// Merge |
|
function merge (o1, o2) { |
|
for (var key in o2) o1[key] = o2[key]; |
|
return o1; |
|
} |
|
function mergeArr (a1, a2) { |
|
return a1.concat(a2.filter(function (m) { return a1.indexOf(m) === -1; })); |
|
} |
|
// Listen to changes in element / document |
|
function changed (el, val){ |
|
var inputs = el.querySelectorAll('input, select, [contenteditable], textarea'), |
|
value = val ? [val] : false; |
|
for(var i = 0; i < inputs; i++){ |
|
inputs[i].addEventListener('change', function(e){ [value] = true; }, false); |
|
}; |
|
return value; |
|
} |
|
// Fullscreen |
|
function toggleFullScreen(e) { |
|
var dc = w.document, dEl = dc.documentElement, |
|
rFS = dEl.requestFullscreen || dEl.mozRequestFullScreen || dEl.webkitRequestFullScreen || dEl.msRequestFullscreen, |
|
cFS = d.exitFullscreen || dc.mozCancelFullScreen || dc.webkitExitFullscreen || dc.msExitFullscreen; |
|
if(!dc.fullscreenElement && !dc.mozFullScreenElement && !dc.webkitFullscreenElement && !dc.msFullscreenElement) rFS.call(dEl); |
|
else cFS.call(dc); |
|
} |
|
// Create SVG |
|
function svg (paths, viewbox, label) { |
|
var el = d.createElementNS('http://www.w3.org/2000/svg','svg'); |
|
el.setAttribute('viewBox', viewbox ? viewbox : '0 0 9 9'); |
|
if(paths instanceof Array) paths.forEach(function(p){ el.innerHTML += '<path d="'+ p +'"/>'; }); |
|
else if(paths) el.innerHTML = '<path d="'+ paths +'"/>'; |
|
if(!label) el.setAttribute('aria-hidden', true); |
|
else el.setAttribute('aria-label', label); |
|
return el; |
|
} |
|
// SVG to PNG |
|
function png64(svg) { |
|
var fill = w.getComputedStyle(svg).fill || null, |
|
stroke = w.getComputedStyle(svg).stroke || null; |
|
if(fill) svg.setAttribute('fill', fill); |
|
if(stroke) svg.setAttribute('stroke', stroke); |
|
var data = new XMLSerializer().serializeToString(svg), |
|
canvas = d.createElement('canvas'), |
|
ratio = w.devicePixelRatio, |
|
size = svg.getBoundingClientRect(), |
|
ctx = canvas.getContext('2d'), |
|
img = new Image(); |
|
canvas.width = size.width * ratio; |
|
canvas.height = size.height * ratio; |
|
ctx.scale(ratio, ratio); |
|
var dataurl = 'data:image/svg+xml,'+ encodeURIComponent(svg); |
|
img.setAttribute('src', dataurl); |
|
img.onload = function() { |
|
setTimeout(function(){ |
|
ctx.drawImage(img, 0, 0); |
|
},5) |
|
}; |
|
return canvas.toDataURL('image/png'); |
|
} |
|
// IE 9 fix |
|
/MSIE|Trident/.test(n.userAgent) && d.addEventListener('DOMContentLoaded', function(){ |
|
[].forEach.call(d.querySelectorAll('svg'), function (svg) { |
|
var use = svg.querySelector('use'); |
|
if (use) { |
|
var obj = d.createElement('object'); |
|
obj.data = use.getAttribute('xlink:href'); |
|
obj.className = svg.getAttribute('class'); |
|
svg.parentNode.replaceChild(obj, svg); |
|
} |
|
}); |
|
}); |
|
// Key listener |
|
function onKey (name, el, callback, once) { |
|
if(!el || !name || !callback) return; |
|
var fn = function(e) { |
|
var key = e.keyCode ? e.keyCode : e.which; |
|
if (key === keys[name]) { |
|
e.preventDefault(); |
|
callback(); |
|
} |
|
if(once) el.removeEventListener('keyup', fn, false); |
|
}; |
|
el.addEventListener('keyup', fn, false); |
|
} |
|
// Keyboard listener |
|
function keylistener (e) { |
|
// console.log(e.target) |
|
var target = e.target || w.event.target; |
|
// if(!target) return; |
|
nopropagate(e); |
|
var key = e.keyCode ? e.keyCode : e.which; |
|
if (key === 13) { // Enter |
|
console.log(key) |
|
} |
|
else if (key === 27) { // Esc |
|
console.log(key) |
|
} |
|
} |
|
// Format milliseconds to hours, minutes and seconds |
|
// Note: Hours display needs to be tested... |
|
function formatMs (n) { |
|
var h = Math.floor(n/3600), |
|
m = Math.floor((n-(h*3600))/60), |
|
s = Math.floor(n-(h*3600)-(m*60)); |
|
if (h >= 1) h = h +':'; else h = ''; |
|
if (m < 10) m = '0'+ m; |
|
if (s < 10) s = '0'+ s; |
|
return h + m +':'+ s; |
|
} |
|
// Format dates |
|
function formatDate(date, format) { |
|
var z = { M: date.getMonth() + 1, d: date.getDate(), h: date.getHours(), m: date.getMinutes(), s: date.getSeconds() }; |
|
format = date.replace(/(M+|d+|h+|m+|s+)/g, function(v) { |
|
return ((v.length > 1 ? '0' : '') + z[v.slice(-1)]).slice(-2); |
|
}); |
|
return format.replace(/(format+)/g, function(v) { |
|
return date.getFullYear().toString().slice(-v.length) |
|
}); |
|
} |
|
// Display human readable n time ago |
|
function ago(el, int) { |
|
var instance, interval = int ? int : 10000, |
|
timestamp = parseInt(el.getAttribute('timestamp'), 10), |
|
units = i18n.ago ? i18n.ago : [ |
|
{sec: 0, text: 'just now'}, |
|
{sec: 1, text: '%n second%s ago'}, |
|
{sec: 60, text: '%n minute%s ago'}, |
|
{sec: 3600, text: '%n hour%s ago'}, |
|
{sec: 86400, text: '%n day%s ago'}, |
|
{sec: 604800, text: '%n week%s ago'}, |
|
{sec: 2592000, text: '%n month%s ago'}, |
|
{sec: 31536000, text: '%n year%s ago'}, |
|
{sec: 315360000, text: '%n decade%s ago'} |
|
]; |
|
// Clear previous interval on element; |
|
clearInterval(instance); // if(instance) |
|
instance = setInterval(timer, interval); |
|
timer(); |
|
// Refresh date / time |
|
function timer () { |
|
var delta = Math.floor((new Date().getTime() - timestamp)/1000); |
|
for (var i = 0; i < units.length; i++) { |
|
if (delta < units[i].sec) { |
|
if (i === 0) el.innerHTML = units[0].text; |
|
else { |
|
var dif = Math.round(delta/units[i-1].sec); |
|
el.innerHTML = units[i-1].text.replace('%n', dif).replace(/%(\w+)/g, (dif <= 1 ? '' : '$1') ); |
|
} |
|
return; |
|
} |
|
} |
|
} |
|
} |
|
// Check for duplicate ids |
|
function checkDuplicateIds () { |
|
var ids = {}, dp = {}, str, |
|
nodes = d.querySelectorAll('[id]'); |
|
for(var i = 0; i < nodes.length; i++) { |
|
var c = nodes[i].id ? nodes[i].id : 'undefined'; |
|
if(isNaN(ids[c])) ids[c] = 0; |
|
ids[c]++; |
|
} |
|
for (var key in ids) if (ids[key] > 1) dp[key] = ids[key]; |
|
str = Object.keys(dp).map(function(k){ return '#'+k+": "+(dp[k]-1)+' Duplicate(s)';}).join("\n"); |
|
if(Object.keys(dp).length > 0) console.log('Non-unique IDs in the document: \n'+str+'\n'); |
|
} |
|
// Vibrate |
|
function vibration (pattern) { |
|
pattern = pattern ? pattern : [300, 100, 300]; |
|
w.navigator.vibrate(pattern); |
|
} |
|
// Play sound via HTML element (IE9 support) |
|
function sound(url){ |
|
if(!allowAudio) return; |
|
url = url ? url : audiopool.pop; |
|
var audio = d.createElement('audio'); |
|
audio.style.display = 'none'; |
|
audio.src = url; |
|
audio.autoplay = true; |
|
audio.onended = function(){ d.body.removeChild(audio);}; |
|
d.body.appendChild(audio); |
|
} |
|
// Mp3 audiopool for ui |
|
var audiopool = { pop: 'data:audio/mpeg;base64,SUQzBAAAAAABAlRYWFgAAAAKAAAAAENPTU1FTlRTVFNTRQAAAA4AAABMYXZmNTguMTIuMTAwVERSQwAAAAUAAAAyMDEzAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/80hkAAaQNRYvoYgBBvgR8AFDEAABlG23ePr8RKhOBvESoVd4EEAA5Uc4PvKBjwxlAQ4gd4gdE7yH/Lv9Rz/8H3w//rB9+XP5BECAAYE4eBAEHdYPl4g///8pBwEAx///4gOFAwrdzrgXqaWQgt2h3mYZtQ4CCGedJDIthbIIEBUAhQAoJEIQ+QDBjCILwNz/80hkKgxBE14AzFAADNBy9l+YKQIWAYACdCLAJFAZk4ACRDGN+TpbKzKIVel+gnrUpJX/3QZJkf//UtbJuzv///6BoT6CTGKFf//KmpdlKGwIAAwOAMABgAAOdZ4hQQlZpLGSU+8954f/PQovOnA5xNYS+j/PFjX/zd////rLCGpRpKENtvuABSVbLO1TmbX/80hEDweoU2Ef4xgBDcCqrV/GYAJc4jD5bV6hvV8rzJxIBAJLlSzTCziwCuLFTsSkvUQ9A85U+Y56CygobGU1pN3aLlbYBAAAUAkY056ONRamOw5HPmwkdTclj6BCtVaVgbADAGH7eJAqUeDk+0WnhLQj///8kRDq+mpSNtA9tvsAO3lA7Uy3RJE1LILCI9D/80hkFQlwe1MfGEaRCtBysl4IwFIHCjmOwq8N80c1bxBqfcy8yDJfsloNIa5hl73AAHR4BDTHmcNBwyF1jKSIMblyoaoa++NGaHanJW0wbtv+ABcaVsTiqzGuepZlQijwlBXPEn89rG/1b5up7bfZs7NZ4WIUJmvkQpJJbZxGpxrk7RcX4KReAxwlEzSyBwn/80hkGQgIW0LfCSYFCHiiTABj0uANlUxLj5gok1qyVoIE5h6o7Qv79Viv/o2c6olRYGzLFERIj6H7//oGq63Fix+3dMLvfClPUqk5iZJJoYjgaIxVD07oaA11E2WF1T1abdsj4IyfqoVCryqciUk+c7cOJdwWK9WiHAMaqyWzJeg0JH+g89ditUJNFoVo+rr/80hkMQboRzR/BSYRCKCSNABJnjDdX9/X//rImy05CIdZaWuyTFEi6Slrp1xzVXDCMaErs9hZWgotlH6noQG79t8BhURBUi9KQ1IhpYCERIjU9CStHLHsjfU0ke87rcrtqPfyX/zv/4a8t6JaZPjckk4IkssVAp1DIafW1/iL0VEtaCP2fU//qPfEO3//PFX/80hkUgW4BxQrCCMBCngGNZ4IRgLZXfK8j0Iv+A7TTT////TTTRVVVf//6KqqqqaaaaKqqqrdppo+qqq1NNNFVVVX///yqqqmmmGIf//9NNNv5UqH////4qLCws3+oWFxUVFRX/6hakxBTUUzLjk5LjW=', |
|
click: 'data:audio/mpeg;base64,//NIZAAFEADy36CIAAiA1bgBQRAAJKTlzcrT4Bxcgp0H8ufEBmIJcMQffy8P7S/f/+GCnkKw+Xfzn+c/+Q+Yx8gTGOMf//nyZ3qc7oQAIUOF3///+GCn///8Pl//+oEF/SA/DLk1Qt5Evm78et8/Q3hviABfB2jxHd2CoDAhVxLTIex0y9wkZUCRiOAoB8kR//NIZDAMfX9iAMe0AA0K/uI/gSgCihzQrn+BLx8HIbGkBmhOTy2V/b////1v///9rWW6bmht/X1//8kCUNiUNEGTZ72qJIxNVAkOBMMAEACASgQKADouaELHYf9P///5f7///o3///3Rs7f1T///+un5f//ZyN/5QkAoqmtu2fwKgs0R+XgVwg1PyaGmW46u//NIZBMKeO1iAMfAAAqYmtWVgRAAF1A+Q9chw5xSbh7gr4j8AsIwNXCNgvT1sI8KjoMKKrmT+///9BqDK+l/6EuFw0K6CDqSUZDLEyau/IDnBj6v/6IgwAgAAAAjDwA8YfF+XRsxvIToGO+V////6gG/U538+kgFnf/1OuKHPg0qLktrRl2k24CwqRBpr/3J//NIZA8KiFdlL+SMAQmoAq4/wRgCE01K1UKHbVQ55IkRRmPqgLMfQokY4GgaBo9wVPKDqgqGsGv8GgVdER4GgaUDR07/4iPFjxUFQa8GnwaiI8VcGoKgqeWGiABAgolUsAKu/EX6j1T/xE3+DR3+VxLVwa//6j2CriwMnSoKu/w7KcSbjdt1uvBPStKh6WhR//NIRA8FpAMZLwQjAQpwBh2eCEYC8ZQFYr1C0t1pliPan14Td+r1ixr8Z/rKpQz/GN/0lXdnJGpJwc69TJEYLzISNICuKYs2kBCzdIS9nH+v/FPxZEf/F///+pnUTEFNRTMuMTAwqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq//NIZDMAAAGkAAAAAAAAA0gAAAAAqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq'}; |
|
} |