/**
*
* @param {HTMLElement} element
* @param {boolean} fadeIn
*/
window.fade = function (element, fadeIn = true) {
if (fadeIn) {
element.style.animation = 'fadeIn var(--transition-base)';
} else {
element.style.animation = 'fadeOut var(--transition-base)';
window.debouncer.schedule(
`remove-${element.dataset.id??element.id??element.className.replace(' ', '-')}`,
() => {
element.remove();
},
500
);
}
}
/**
* Format a time value as relative time (past or future)
* Handles both "X time ago" and "in X time" formats
*
* @param {string|Date} dateStr Date to format
* @returns {string} Formatted time string
*/
window.formatTimeAgo = function(dateStr, dateFormat = 'default') {
const date = dateStr instanceof Date ? dateStr : new Date(dateStr);
const now = new Date();
const diffMs = date - now;
const isPast = diffMs < 0;
// Work with absolute values for calculations
const seconds = Math.floor(Math.abs(diffMs) / 1000);
const minutes = Math.floor(seconds / 60);
const hours = Math.floor(minutes / 60);
const days = Math.floor(hours / 24);
// Just now (within 1 minute either way)
if (minutes === 0) {
return 'Just now';
}
// Format the time components
let timeStr = '';
if (seconds < 10) {
timeStr = 'a moment';
} else if (seconds < 60) {
timeStr = 'less than a minute'
} else if (minutes < 5) {
timeStr = 'a few minutes';
} else if (hours < 24) {
if (hours === 0) {
// Minutes only
timeStr = `${minutes} ${minutes === 1 ? 'minute' : 'minutes'}`;
} else {
// Hours
timeStr = `about ${hours} ${hours === 1 ? 'hour' : 'hours'}`;
}
} else if (days < 7) {
if (days === 1) {
return isPast ? 'yesterday' : 'tomorrow';
}
timeStr = `about ${days} days`;
// Days
timeStr = `${days} ${days === 1 ? 'day' : 'days'}`;
} else {
// More than a week - show the date based on format
if (dateFormat === 'default') {
return date.toLocaleDateString();
}
// Parse PHP-style format string
const formatMap = {
'Y': date.getFullYear(),
'y': String(date.getFullYear()).slice(-2),
'F': date.toLocaleDateString('en-CA', { month: 'long' }),
'M': date.toLocaleDateString('en-CA', { month: 'short' }),
'm': String(date.getMonth() + 1).padStart(2, '0'),
'n': date.getMonth() + 1,
'd': String(date.getDate()).padStart(2, '0'),
'j': date.getDate(),
'D': date.toLocaleDateString('en-CA', { weekday: 'short' }),
'l': date.toLocaleDateString('en-CA', { weekday: 'long' }),
'H': String(date.getHours()).padStart(2, '0'),
'i': String(date.getMinutes()).padStart(2, '0'),
's': String(date.getSeconds()).padStart(2, '0'),
'h': String(date.getHours() % 12 || 12).padStart(2, '0'),
'g': date.getHours() % 12 || 12,
'A': date.getHours() >= 12 ? 'PM' : 'AM',
'a': date.getHours() >= 12 ? 'pm' : 'am',
};
return dateFormat.replace(/[YyFMmnjDlHishgAa]/g, match => formatMap[match]);
}
// Add appropriate prefix/suffix based on past or future
return isPast ? `${timeStr} ago` : `in ${timeStr}`;
}
/**
* Capitalize first letter of a string
*
* @param {string} string String to capitalize
* @returns {string} Capitalized string
*/
window.uppercaseFirst = function(string) {
return string.charAt(0).toUpperCase() + string.slice(1);
}
/**
* Load HTML templates
*/
window.templates = new Map();
document.addEventListener('DOMContentLoaded', ()=> {
// // Default templates
// window.templates.set('replyButton', this.createReplyButtonTemplate());
// window.templates.set('commentsButton', this.createCommentsButtonTemplate());
// window.templates.set('voteButton', this.createVoteButtonsTemplate());
window.loadTemplates();
});
window.loadTemplates = function() {
document.querySelectorAll('template').forEach(template => {
const classes = Array.from(template.classList);
if (classes.length > 0) {
const item = template.content.cloneNode(true).firstElementChild;
classes.forEach(key => {
if (!window.templates.has(key)) {
window.templates.set(key, item);
}
});
}
});
}
/**
* Helper to load a template from
* @param template
* @returns {Node|ActiveX.IXMLDOMNode|boolean}
*/
window.getTemplate = function (template){
if (window.templates.size === 0) {
window.loadTemplates();
}
if(window.templates.has(template)){
return window.templates.get(template).cloneNode(true);
}
return false;
}
/**
* Load and instantiate HTML templates as lightweight components
*/
class TemplateRegistry {
constructor() {
this.templates = new Map(); // name ->
this.definitions = new Map(); // name -> component definition
}
/**
* Collect all elements
*/
registerAll(root = document) {
root.querySelectorAll('template').forEach(tpl => {
tpl.classList.forEach(name => {
if (!this.templates.has(name)) {
this.templates.set(name, tpl);
}
});
});
}
/**
* Define component behavior
*/
define(name, definition = {}, context = null) {
this.definitions.set(name, {
refs: definition.refs || null,
manyRefs: definition.manyRefs || null,
setup: definition.setup || null,
context: context
});
}
/**
* Create a component instance
*/
create(name, data = {}) {
const tpl = this.templates.get(name);
if (!tpl) {
console.warn(`[TemplateRegistry] Template "${name}" not found`);
return null;
}
const element = tpl.content.cloneNode(true).firstElementChild;
if (!element) return null;
const def = this.definitions.get(name);
const refs = def?.refs
? this.#collectRefs(element, def.refs)
: {};
const manyRefs = def?.manyRefs
? this.#collectRefs(element, def.manyRefs, false)
: {};
def?.setup?.({
el: element,
refs,
manyRefs,
data
});
return element;
}
/**
* Resolve refs declared in component definition
*/
#collectRefs(root, refMap, single = true) {
const refs = {};
for (const [key, value] of Object.entries(refMap)) {
let selector;
let required = false;
if (typeof value === 'string') {
selector = value;
} else {
selector = value.selector;
required = !!value.required;
}
const found = (single) ? root.querySelector(selector) : root.querySelectorAll(selector);
if (required) {
if (single && !found) {
console.warn(`[TemplateRegistry] Required ref "${key}" not found: ${selector}`);
}
if (!single && found.length === 0) {
console.warn(`[TemplateRegistry] Required manyRef "${key}" not found: ${selector}`);
}
}
refs[key] = single ? found : Array.from(found);
}
return refs;
}
}
window.jvbTemplates = new TemplateRegistry();
document.addEventListener('DOMContentLoaded', () => {
window.jvbTemplates.registerAll();
});
/**
* Gets a clone of an icon element if it exists for efficient DOM manipulation
* @param icon
* @returns {Node | ActiveX.IXMLDOMNode}
*/
window.icon = null;
window.getIcon = function getIcon(icon, style = ''){
if (typeof icon === 'undefined') {
return '';
}
if (!window.icon) {
window.icon = document.createElement('i');
window.icon.className = 'icon';
window.icon.ariaHidden = true;
}
let theIcon = window.icon.cloneNode(true);
if (style !== '' && ['regular', 'bold', 'duotone', 'fill', 'light', 'thin' ].includes('style')) {
style = `-${style.slice(0, 2)}`;
} else {
style = '';
}
theIcon.classList.add(`icon-${icon}${style}`);
return theIcon;
}
/**
* Format a number with comma separator (e.g., 1,234)
* @param {number} num - Number to format
* @returns {string} - Formatted number
*/
window.formatNumber = function(num) {
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
}
/**
* Format a price with currency symbol
* @param {number} price - Price to format
* @param {string} currency - Currency code (default: 'CAD')
* @returns {string} - Formatted price
*/
window.formatPrice = function(price, currency = 'CAD') {
return new Intl.NumberFormat('en-CA', {
style: 'currency',
currency: currency
}).format(price);
}
/**
* Escape HTML special characters to prevent XSS
* @param {string} text - Text to escape
* @returns {string} - Escaped text
*/
window.escapeHtml = function(text) {
if (!text) return '';
// Convert to string if it's not already a string
if (typeof text !== "string" && !(text instanceof String)) {
text = String(text);
}
return text
.replace(/&/g, "&")
.replace(//g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
/**
* Should be faster than setting innerHTML = ''
* @param node
*/
window.removeChildren = function(node) {
if(node.children.length === 0){
return;
}
while (node.firstChild) {
node.removeChild(node.firstChild);
}
}
/**
* Format a date range (e.g., "Jan 1 - Jan 5, 2023")
* @param {string} startDate - Start date ISO string
* @param {string} endDate - End date ISO string
* @returns {string} - Formatted date range
*/
window.formatDateRange = function(startDate, endDate) {
const start = new Date(startDate);
const end = new Date(endDate);
// If same day, just show one date
if (start.toDateString() === end.toDateString()) {
return start.toLocaleDateString('en-CA', {
year: 'numeric',
month: 'short',
day: 'numeric'
});
}
// If same month and year, show range with month once
if (start.getMonth() === end.getMonth() && start.getFullYear() === end.getFullYear()) {
return `${start.toLocaleDateString('en-CA', { month: 'short', day: 'numeric' })} - ${end.getDate()}, ${end.getFullYear()}`;
}
// If same year, show full range with year once
if (start.getFullYear() === end.getFullYear()) {
return `${start.toLocaleDateString('en-CA', { month: 'short', day: 'numeric' })} - ${end.toLocaleDateString('en-CA', { month: 'short', day: 'numeric' })}, ${end.getFullYear()}`;
}
// Different years, show full dates
return `${start.toLocaleDateString('en-CA', { month: 'short', day: 'numeric', year: 'numeric' })} - ${end.toLocaleDateString('en-CA', { month: 'short', day: 'numeric', year: 'numeric' })}`;
}
/**
* Throttle function to limit call frequency
* @param {Function} func - Function to throttle
* @param {number} limit - Time limit in milliseconds
* @returns {Function} - Throttled function
*/
window.throttle = function(func, limit = 300) {
let inThrottle;
return function(...args) {
if (!inThrottle) {
func.apply(this, args);
inThrottle = true;
setTimeout(() => inThrottle = false, limit);
}
};
}
window.chunkIt = async function(items, renderCallback, placementCallback, size = 10) {
const chunks = [];
for (let i = 0; i {
const element = renderCallback(item);
if (element) fragment.append(element);
});
placementCallback(fragment);
await new Promise(resolve => requestAnimationFrame(resolve));
}
}
window.prefixInput = function(input, prefix, wrapper = null, replace = false, name = false) {
if (!input) {
console.warn('prefixInput called with null/undefined input');
return;
}
// console.log('Prefixing input: ', input);
// console.log('With prefix: ', prefix);
// console.log('Wrapper: ', wrapper);
const oldId = input.id;
const newId = replace ? prefix : `${prefix}${input.name}`;
// console.log('Old ID: ', oldId);
// console.log('New ID: ', newId);
// Search for label within wrapper if provided, otherwise use existing logic
let label = null;
if (wrapper) {
// Most reliable: search within wrapper by old ID
label = wrapper.querySelector(`label[for="${oldId}"]`);
} else if (input.labels && input.labels.length > 0) {
// Fallback to input.labels if no wrapper provided
label = input.labels[0];
} else if (input.previousElementSibling?.tagName === 'LABEL') {
label = input.previousElementSibling;
} else if (input.nextElementSibling?.tagName === 'LABEL') {
label = input.nextElementSibling;
} else {
// Final fallback: search up the tree
label = input.closest('[data-field]')?.querySelector(`label[for="${oldId}"]`);
}
if (label) {
label.htmlFor = newId;
}
input.id = newId;
if (name) {
input.name = newId;
}
}
/**
* Makes first letter uppercase
* @param string
* @returns {string}
*/
window.uppercaseFirst = function(string){
return string.charAt(0).toUpperCase()
+ string.slice(1)
}
/**
* Sanitizes HTML
* @param text
* @returns {string}
*/
window.sanitizeHtml = function(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
window.generateID = function(prefix = 'jvb') {
return `${prefix}_${Date.now()}_${Math.random().toString(36).slice(2,9)}`;
}
window.showProgress = function(elements, current, total, message = '', icon = '') {
const show = current < total;
if (elements.progress && show) {
window.fade(elements.progress, true);
}
const percent = total > 0 ? (current / total) * 100 : 0;
if (elements.fill) elements.fill.style.width = `${percent}%`;
if (elements.details) elements.details.textContent = message;
if (elements.count) elements.count.textContent = `${current}/${total}`;
if (elements.icon) elements.icon.className = (icon === '') ? 'icon' : 'icon icon-'+icon;
if (elements.progress && current === total) {
window.fade(elements.progress, false);
}
}
/**
* Format a date string for display
* @param {string} dateString - ISO date string
* @returns {string} Formatted date
*/
window.formatDate = function(dateString) {
if (!dateString) return '';
const date = new Date(dateString);
const now = new Date();
const diffDays = Math.floor((now - date) / (1000 * 60 * 60 * 24));
if (diffDays < 1) {
return 'Today';
} else if (diffDays < 2) {
return 'Yesterday';
} else if (diffDays < 7) {
return `${diffDays} days ago`;
} else {
return date.toLocaleDateString();
}
}
/**
* Pluralizes the content
* @param content
* @returns {string|string}
*/
window.getPluralContent = function(content){
return (content === 'artwork') ? 'artwork' : content+'s';
}
/**
* Shortcut to add notification
* @param message
* @param type
* @param actions
*/
window.showToast = function(message, type='success', actions={}){
window.jvbNotifications.showToast(message, type, actions);
}
window.dateFormatter = new Intl.DateTimeFormat('en-CA', {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
timeZoneName: 'short'
});
window.formatDate = function(date) {
if (!(date instanceof Date && !isNaN(date))) {
date = new Date(date);
}
return window.dateFormatter.format(date);
}
/**
* Outputs the set text as if a typewriter were writing it
* @param container
* @param text
* @param speed
* @returns {Promise}
*/
window.typeText = function(container, text, speed = 50) {
return new Promise((resolve) => {
// Cancel any existing animation on this container
if (container._typeInterval) {
clearInterval(container._typeInterval);
delete container._typeInterval;
}
let index = 0;
container.textContent = '';
container._typeInterval = setInterval(() => {
if (index < text.length) {
container.textContent += text.charAt(index);
index++;
} else {
clearInterval(container._typeInterval);
delete container._typeInterval;
resolve();
}
}, speed);
});
}
/**
* Erases text like a keyboard would.
* @param container
* @param speed
* @returns {Promise}
*/
window.eraseText = function(container, speed = 10) {
return new Promise((resolve) => {
// Cancel any existing animation on this container
if (container._eraseInterval) {
clearInterval(container._eraseInterval);
delete container._eraseInterval;
}
let text = container.textContent;
let index = text.length;
container._eraseInterval = setInterval(() => {
if (index > 0) {
index--;
container.textContent = text.substring(0, index);
} else {
clearInterval(container._eraseInterval);
delete container._eraseInterval;
resolve();
}
}, speed);
});
}
/**
* Continuously types and erases text in a loop
* @param container - The DOM element to display text in
* @param text - The text to type and erase
* @param typeSpeed - Speed of typing (ms between characters)
* @param eraseSpeed - Speed of erasing (ms between character removals)
* @param pauseAfterType - Pause after typing completes (ms)
* @param pauseAfterErase - Pause after erasing completes (ms)
* @returns {Function} - Call this function to stop the loop
*/
window.typeLoop = function(container, text, typeSpeed = 50, eraseSpeed = 10, pauseAfterType = 1000, pauseAfterErase = 250) {
// Generate unique key for this container
const containerId = container.id || container.dataset.typeKey || `type-${Date.now()}`;
if (!container.dataset.typeKey) {
container.dataset.typeKey = containerId;
}
// Stop any existing loop immediately
if (container._stopTyping) {
container._stopTyping();
}
let isRunning = true;
async function loop() {
while (isRunning) {
await window.typeText(container, text, typeSpeed);
if (!isRunning) break;
await new Promise(resolve => setTimeout(resolve, pauseAfterType));
if (!isRunning) break;
await window.eraseText(container, eraseSpeed);
if (!isRunning) break;
await new Promise(resolve => setTimeout(resolve, pauseAfterErase));
}
}
const stopLoop = function() {
isRunning = false;
if (container._typeInterval) {
clearInterval(container._typeInterval);
delete container._typeInterval;
}
if (container._eraseInterval) {
clearInterval(container._eraseInterval);
delete container._eraseInterval;
}
};
container._stopTyping = stopLoop;
loop(); // Start immediately
return stopLoop;
};
window.toCamelCase = function (string) {
return string.replace(/-([a-z])/g, function (g) { return g[1].toUpperCase(); });
}
window.targetCheck = function (e, selector) {
if (Array.isArray(selector)) {
selector = selector.join(',');
}
if (typeof selector !== 'string') {
return false;
}
return (e.target.closest(selector))??false;
}
//Modified rom Stackoverflow: https://stackoverflow.com/a/8596559
window.getDifferences = {
VALUE_CREATED: "created",
VALUE_UPDATED: "updated",
VALUE_DELETED: "deleted",
VALUE_UNCHANGED: "unchanged",
map: function(oldData, newData) {
if (this.isFunction(oldData) || this.isFunction(newData)) {
throw "Invalid argument. Function given, object expected.";
}
if (this.isFile(oldData) || this.isFile(newData)) {
const changeType = this.compareFiles(oldData, newData);
return changeType === this.VALUE_UNCHANGED ? null : {
type: changeType,
data: oldData === undefined ? newData : oldData
};
}
if (this.isValue(oldData) || this.isValue(newData)) {
const changeType = this.compareValues(oldData, newData);
if (changeType === this.VALUE_UNCHANGED) return null;
let resultData;
switch (changeType) {
case this.VALUE_CREATED:
resultData = newData;
break;
case this.VALUE_DELETED:
resultData = this.getEmptyValue(oldData);
break;
case this.VALUE_UPDATED:
default:
resultData = newData;
}
return { type: changeType, data: resultData };
}
let changes = {};
let hasChanges = false;
// Check for modifications and deletions
for (let key in oldData) {
if (!this.isFunction(oldData[key])) {
let newValue = undefined;
if (newData && newData[key] !== undefined) {
newValue = newData[key];
}
const change = this.map(oldData[key], newValue);
if (change !== null) {
if (change.hasOwnProperty("type") && change.hasOwnProperty("data")) {
changes[key] = change.data;
} else {
changes[key] = change;
}
hasChanges = true;
}
}
}
// Check for additions
if (newData) {
for (let key in newData) {
if (!this.isFunction(newData[key]) && (oldData === undefined || oldData[key] === undefined)) {
const change = this.map(undefined, newData[key]);
if (change !== null) {
if (change.hasOwnProperty("type") && change.hasOwnProperty("data")) {
changes[key] = change.data;
} else {
changes[key] = change;
}
hasChanges = true;
}
}
}
}
return hasChanges ? changes : null;
},
/**
* Get appropriate empty value for a deleted field
*/
getEmptyValue: function(originalValue) {
if (this.isArray(originalValue)) {
return [];
}
if (this.isObject(originalValue)) {
return {};
}
if (typeof originalValue === 'number') {
return 0;
}
if (typeof originalValue === 'boolean') {
return false;
}
// For strings and other types, return empty string
return "";
},
compareValues: function(oldValue, newValue) {
return oldValue === newValue ||
(this.isDate(oldValue) && this.isDate(newValue) && oldValue.getTime() === newValue.getTime())
? this.VALUE_UNCHANGED
: oldValue === undefined
? this.VALUE_CREATED
: newValue === undefined
? this.VALUE_DELETED
: this.VALUE_UPDATED;
},
isFunction: function(value) {
return Object.prototype.toString.call(value) === "[object Function]";
},
isArray: function(value) {
return Object.prototype.toString.call(value) === "[object Array]";
},
isDate: function(value) {
return Object.prototype.toString.call(value) === "[object Date]";
},
isObject: function(value) {
return Object.prototype.toString.call(value) === "[object Object]";
},
isFile: function(value) {
return value instanceof File;
},
isValue: function(value) {
return !this.isObject(value) && !this.isArray(value);
},
compareFiles: function(oldFile, newFile) {
if (!this.isFile(oldFile) && this.isFile(newFile)) {
return this.VALUE_CREATED;
}
if (this.isFile(oldFile) && !this.isFile(newFile)) {
return this.VALUE_DELETED;
}
if (this.isFile(oldFile) && this.isFile(newFile)) {
return oldFile.name === newFile.name &&
oldFile.size === newFile.size &&
oldFile.type === newFile.type &&
oldFile.lastModified === newFile.lastModified
? this.VALUE_UNCHANGED
: this.VALUE_UPDATED;
}
return this.VALUE_UNCHANGED;
},
merge: function(oldData, newData) {
if (oldData == null) return newData;
if (newData == null) return oldData;
if (this.isFunction(oldData) || this.isFunction(newData)) return newData;
if (this.isFile(oldData) || this.isFile(newData)) return newData;
if (this.isValue(oldData) || this.isValue(newData) || this.isArray(oldData) || this.isArray(newData)) {
return newData;
}
if (this.isObject(oldData) && this.isObject(newData)) {
let result = {};
for (let key in oldData) {
if (!this.isFunction(oldData[key])) {
result[key] = oldData[key];
}
}
for (let key in newData) {
if (!this.isFunction(newData[key])) {
if (oldData[key] !== undefined) {
result[key] = this.merge(oldData[key], newData[key]);
} else {
result[key] = newData[key];
}
}
}
return result;
}
return newData;
}
};
window.deepMerge = function(oldData, newData) {
return window.getDifferences.merge(oldData, newData);
};
window.isInt = function(n) {
return !isNaN(parseInt(n)) && isFinite(n);
};
window.isNumeric = function(n) {
return !isNaN(parseFloat(n)) && isFinite(n);
};
/**
*
* @param {object} selectors
* @param {HTMLElement|null} parent
* @returns {object}
*/
window.uiFromSelectors = function(selectors, parent = null, all = false) {
let ui = {};
for (let [key, selector] of Object.entries(selectors)) {
if (typeof selector === 'object') {
ui[key] = window.uiFromSelectors(selector, parent);
}else {
if (!parent) {
ui[key] = (all) ? document.querySelectorAll(selector) : document.querySelector(selector);
} else {
ui[key] = (all) ? parent.querySelectorAll(selector) : parent.querySelector(selector);
}
}
}
return ui;
}
window.sleep = async function (ms = 50) {
return new Promise(resolve => setTimeout(resolve, ms));
};
class DebouncedActions {
constructor() {
this.timeouts = new Map();
window.addEventListener('beforeunload', () => this.cleanup());
}
schedule(key, callback, delay = 1000) {
this.cancel(key);
this.timeouts.set(key, setTimeout(() => {
callback();
this.timeouts.delete(key);
}, delay));
}
cancel(key) {
if (this.timeouts.has(key)) {
clearTimeout(this.timeouts.get(key));
this.timeouts.delete(key);
}
}
cleanup() {
for (let timeout of this.timeouts.values()) {
clearTimeout(timeout);
}
this.timeouts.clear();
}
}
window.debouncer = new DebouncedActions();
// -----------------------------------------------------
// Scroll direction + scroll progress
// -----------------------------------------------------
const body = document.body;
const docEl = document.documentElement;
const progressBar = document.querySelector('.scroll-progress .bar');
let lastY = window.scrollY || docEl.scrollTop || 0;
let direction = -1;
let ticking = false;
let maxScroll = 0;
function updateMaxScroll() {
maxScroll = Math.max(0, docEl.scrollHeight - window.innerHeight);
}
function updateScrollProgress(y) {
if (!progressBar) return;
const progress = maxScroll > 0 ? y / maxScroll : 0;
const clamped = Math.max(0, Math.min(1, progress));
progressBar.style.transform = `scaleX(${clamped})`;
}
function onScrollFrame() {
const y = window.scrollY || docEl.scrollTop || 0;
// Direction: 1 = down, -1 = up, keep existing if no movement
if (y > lastY) {
direction = 1;
} else if (y < lastY) {
direction = -1;
}
lastY = y;
// Only add scroll-up when actually below top & moving up
document.body.classList.toggle('scroll-up', direction < 0 && y > 0);
// Update progress bar
updateScrollProgress(y);
ticking = false;
}
// Throttled scroll listener
window.addEventListener(
'scroll',
() => {
if (!ticking) {
ticking = true;
requestAnimationFrame(onScrollFrame);
}
},
{ passive: true }
);
window.previousBGSize = 'Small';
window.bgSizes = {
Small: 500,
Med: 768,
Large: 1024
};
window.bgObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
let newSize = entry.target.dataset[`bg${window.previousBGSize}`];
entry.target.style.backgroundImage = `url(${newSize})`;
entry.target.dataset.bgImg = window.previousBGSize;
window.bgObserver.unobserve(entry.target);
}
})
},
{
root: null,
rootMargin: '0px 0px -100px 0px',
threshold: 0
});
function updateBG() {
let current = window.innerWidth;
let newWidth = getBGWidth(current);
if (newWidth) {
window.previousBGSize = newWidth;
document.querySelectorAll('[data-bg-img]:not([data-bg-img="'+window.previousBGSize+'"])').forEach(img => {
window.bgObserver.observe(img);
});
}
}
function getBGWidth(width) {
let prev = window.previousBGSize;
let check = {
Small: ['Med','Large'],
Med: ['Large'],
Large: false
};
if (!check[prev]) {
return false;
}
let next = 'Small';
check[prev].forEach(w => {
if (width => window.bgSizes[w]) {
next = w;
}
});
return next;
}
updateBG();
// Debounced resize to recalc scrollable height
window.addEventListener('resize', () => {
window.debouncer.schedule('recalc-max-scroll', () => {
updateMaxScroll();
updateScrollProgress(window.scrollY || docEl.scrollTop || 0);
}, 20);
window.debouncer.schedule('bg-resize', () => {
updateBG();
});
});
// Initial setup
updateMaxScroll();
updateScrollProgress(lastY);
window.decodeHTMLEntities = function(text) {
if (!window.decodeHelper) {
window.decodeHelper = document.createElement('textarea');
}
window.decodeHelper.innerHTML = text;
return window.decodeHelper.value;
}
window.focusNextElement = function() {
//add all elements we want to include in our selection
var focussableElements =
'a:not([disabled]), button:not([disabled]), input[type=text]:not([disabled]), [tabindex]:not([disabled]):not([tabindex="-1"])';
if (document.activeElement && document.activeElement.form) {
var focussable = Array.prototype.filter.call(
document.activeElement.form.querySelectorAll(focussableElements),
function (element) {
//check for visibility while always include the current activeElement
return (
element.offsetWidth > 0 ||
element.offsetHeight > 0 ||
element === document.activeElement
);
}
);
var index = focussable.indexOf(document.activeElement);
if (index > -1) {
var nextElement = focussable[index + 1] || focussable[0];
nextElement.focus();
}
}
}