/** * * @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 ->