1/** 2 * @file 3 * Dynamic time difference formatting. 4 */ 5 6((Drupal, once) => { 7 /** 8 * @typedef {object} timeDiffValue 9 * 10 * @prop {number} [year] 11 * Years count. 12 * @prop {number} [month] 13 * Months count. 14 * @prop {number} [week] 15 * Weeks count. 16 * @prop {number} [day] 17 * Days count. 18 * @prop {number} [hour] 19 * Hours count. 20 * @prop {number} [minute] 21 * Minutes count. 22 * @prop {number} [second] 23 * Seconds count. 24 */ 25 26 /** 27 * @typedef {object} timeDiff 28 * 29 * @prop {string} formatted 30 * A translated string representation of the interval. 31 * @prop {timeDiffValue} value 32 * The elements composing the time difference interval. Example: { day: 2, 33 * hour: 2, minute: 32, second: 15 }. 34 */ 35 36 /** 37 * List of time intervals. 38 * 39 * @type {object} 40 * 41 * @prop {number} year 42 * Year duration in seconds. 43 * @prop {number} month 44 * Month duration in seconds. 45 * @prop {number} week 46 * Week duration in seconds. 47 * @prop {number} day 48 * Day duration in seconds. 49 * @prop {number} hour 50 * Hour duration in seconds. 51 * @prop {number} minute 52 * Minute duration in seconds. 53 * @prop {number} second 54 * One second. 55 */ 56 const intervals = { 57 year: 31536000, 58 month: 2592000, 59 week: 604800, 60 day: 86400, 61 hour: 3600, 62 minute: 60, 63 second: 1, 64 }; 65 66 /** 67 * List of available time intervals names. 68 * 69 * @type {string[]} 70 */ 71 const intervalsNames = Object.keys(intervals); 72 73 /** 74 * 75 * @type {WeakMap<HTMLElement, number>} 76 */ 77 const timers = new WeakMap(); 78 79 /** 80 * @namespace 81 */ 82 Drupal.timeDiff = { 83 /** 84 * Fills a HTML5 time element text with a computed time difference string. 85 * 86 * @param {Element} timeElement 87 * The time DOM element. 88 */ 89 show(timeElement) { 90 const timestamp = new Date( 91 timeElement.getAttribute('datetime'), 92 ).getTime(); 93 const timeDiffSettings = JSON.parse( 94 timeElement.getAttribute('data-drupal-time-diff'), 95 ); 96 97 const now = Date.now(); 98 const diff = Math.round((timestamp - now) / 1000); 99 const options = { granularity: timeDiffSettings.granularity }; 100 const timeDiff = Drupal.timeDiff.format(diff, options); 101 const format = diff > 0 ? 'future' : 'past'; 102 timeElement.textContent = Drupal.formatString( 103 timeDiffSettings.format[format], 104 { 105 '@interval': timeDiff.formatted, 106 }, 107 ); 108 109 if (timeDiffSettings.refresh > 0) { 110 const refreshInterval = Drupal.timeDiff.refreshInterval( 111 timeDiff.value, 112 timeDiffSettings.refresh, 113 timeDiffSettings.granularity, 114 ); 115 clearTimeout(timers.get(timeElement)); 116 timers.set( 117 timeElement, 118 setTimeout(Drupal.timeDiff.show, refreshInterval * 1000, timeElement), 119 ); 120 } 121 }, 122 123 /** 124 * Computes the refresh interval. 125 * 126 * There are cases when the refresh occurs even when it is not needed. For 127 * example if the refresh interval is '10 seconds', the granularity is 2 and 128 * the time difference is '1 hour 32 minutes', there's no need to refresh 129 * every 10 seconds but every 1 minute. This function optimizes the refresh 130 * interval to higher values, if the structure of the time difference 131 * doesn't require refreshing more often. 132 * 133 * @param {timeDiffValue} value 134 * The time difference object. 135 * @param {number} refresh 136 * The configured refresh interval in seconds. 137 * @param {number} granularity 138 * The time difference granularity. 139 * 140 * @return {number} 141 * The computed refresh interval in seconds. 142 */ 143 refreshInterval(value, refresh, granularity) { 144 const units = Object.keys(value); 145 const unitsCount = units.length; 146 const lastUnit = units.pop(); 147 148 // If the lowest unit of time difference is 'minute' or greater but the 149 // refresh interval is lower, do not refresh often than the duration of 150 // the lowest unit of time difference. 151 if (lastUnit !== 'second') { 152 // If the time difference value parts count equals the granularity and 153 // lowest unit duration is bigger than the refresh interval, use the 154 // interval duration. For example, if the refresh interval is 155 // '10 seconds', the granularity is 2 and the time difference is 156 // '1 hour 32 minutes', do not refresh every 10 seconds but every one 157 // minute (60 seconds). 158 if (unitsCount === granularity) { 159 intervalsNames.every((interval) => { 160 const duration = intervals[interval]; 161 if (interval === lastUnit) { 162 refresh = refresh < duration ? duration : refresh; 163 return false;
164 } 165 return true; 166 }); 167 return refresh; 168 } 169 // The time difference value parts count might be smaller than the 170 // granularity when the lowest part is missed because is 0. In this case 171 // the missed part interval duration is used as refresh. For example, if 172 // the refresh is '10 seconds', the granularity is 2 and the time 173 // difference is '59 minutes 59 seconds', on the next refresh the time 174 // difference will be '1 hour' (because minutes are 0, therefore are not 175 // shown) but we want the next refresh to occur, not in one hour, but in 176 // one minute. 177 const lastIntervalIndex = intervalsNames.indexOf(lastUnit); 178 const nextInterval = intervalsNames[lastIntervalIndex + 1]; 179 refresh = intervals[nextInterval]; 180 } 181 return refresh; 182 }, 183 184 /** 185 * Formats a time interval between two timestamps. 186 * 187 * @param {number} diff 188 * A UNIX timestamps difference in seconds. 189 * @param {object} [options] 190 * An optional object with additional options. 191 * @param {number} [options.granularity=2] 192 * An integer value that signals how many different units to display in the 193 * string. Defaults to 2. 194 * @param {boolean} [options.strict=false] 195 * A boolean value indicating whether or not, a negative diff should be 196 * rendered as "0 seconds". If the time difference is negative (i.e. the 197 * timestamp is in the past) and this option is false (default) the result 198 * string will be the formatted time difference. If the option is true the 199 * result string will be "0 seconds". 200 * 201 * @return {timeDiff} 202 * A time difference type object. 203 */ 204 format(diff, options = {}) { 205 // Provide appropriate defaults. 206 options = { granularity: 2, strict: false, ...options }; 207 208 if (options.strict && diff < 0) { 209 return { 210 formatted: Drupal.formatPlural(0, '1 second', '@count seconds'), 211 value: { second: 0 }, 212 }; 213 } 214 diff = Math.abs(diff); 215 216 const output = []; 217 const value = {}; 218 let units; 219 let { granularity } = options; 220 221 intervalsNames.every((interval) => { 222 const duration = intervals[interval]; 223 units = Math.floor(diff / duration); 224 if (units > 0) { 225 diff %= units * duration; 226 switch (interval) { 227 case 'year': 228 output.push(Drupal.formatPlural(units, '1 year', '@count years')); 229 break; 230 case 'month': 231 output.push( 232 Drupal.formatPlural(units, '1 month', '@count months'), 233 ); 234 break; 235 case 'week': 236 output.push(Drupal.formatPlural(units, '1 week', '@count weeks')); 237 break; 238 case 'day': 239 output.push(Drupal.formatPlural(units, '1 day', '@count days')); 240 break; 241 case 'hour': 242 output.push(Drupal.formatPlural(units, '1 hour', '@count hours')); 243 break; 244 case 'minute': 245 output.push( 246 Drupal.formatPlural(units, '1 minute', '@count minutes'), 247 ); 248 break; 249 default: 250 output.push( 251 Drupal.formatPlural(units, '1 second', '@count seconds'), 252 ); 253 } 254 value[interval] = units; 255 256 granularity -= 1; 257 if (granularity <= 0) { 258 // Limit the granularity of the output. 259 return false; 260 } 261 } else if (output.length > 0) { 262 // Exit if there was previous output but not any output at this level, 263 // to avoid skipping levels and getting output like "1 year 1 second". 264 return false; 265 } 266 return true; 267 }); 268 269 if (output.length === 0) { 270 return { 271 formatted: Drupal.formatPlural(0, '1 second', '@count seconds'), 272 value: { second: 0 }, 273 }; 274 } 275 return { formatted: output.join(' '), value }; 276 }, 277 }; 278 279 /** 280 * Fills all time[data-drupal-time-diff] elements with a refreshing time diff. 281 * 282 * @type {Drupal~behavior} 283 * 284 * @prop {Drupal~behaviorAttach} attach 285 * Initializes refresh of time differences. 286 * 287 * @prop {Drupal~behaviorDetach} detach 288 * Clear timers associated with time diff elements. 289 */ 290 Drupal.behaviors.timeDiff = { 291 attach(context) { 292 // Replace each <time> element text with a time difference representation. 293 once('time-diff', 'time[data-drupal-time-diff]', context).forEach( 294 Drupal.timeDiff.show, 295 ); 296 }, 297 detach(context, settings, trigger) { 298 if (trigger === 'unload') { 299 once 300 .remove('time-diff', 'time[data-drupal-time-diff]', context) 301 .forEach((timeElement) => clearTimeout(timers.get(timeElement))); 302 } 303 }, 304 }; 305})(Drupal, once);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.