1/** 2 * Front end for the Lönestatistik widget. 3 * 4 * The filter form is already in the DOM, rendered server-side with real 5 * options. This only fetches one year's aggregate and renders the figures for 6 * whatever the visitor has selected. 7 * 8 * Exposes a global `bricksLonestatistik()` because Bricks calls every function 9 * named in an element's $scripts array â on DOM ready and again after AJAX 10 * updates in the builder. It must therefore be safe to run repeatedly. 11 */ 12( function () { 13 'use strict'; 14 15 var strings = window.lonestatistikStrings || {}; 16 var numberFormat = new Intl.NumberFormat( 'sv-SE', { maximumFractionDigits: 0 } ); 17 var cache = {}; 18 19 function t( key, fallback ) { 20 return strings[ key ] || fallback || ''; 21 } 22 23 function el( tag, className, text ) { 24 var node = document.createElement( tag ); 25 26 if ( className ) { 27 node.className = className; 28 } 29 30 if ( undefined !== text && null !== text ) { 31 node.textContent = String( text ); 32 } 33 34 return node; 35 } 36 37 function money( value, currency ) { 38 if ( null === value || undefined === value || isNaN( value ) ) { 39 return 'â'; 40 } 41 42 return numberFormat.format( value ) + ( currency ? ' ' + currency : '' ); 43 } 44 45 /** 46 * One widget instance. 47 * 48 * @param {HTMLElement} root Container carrying data-lonestatistik. 49 */ 50 function Widget( root ) { 51 this.root = root; 52 this.form = root.querySelector( '[data-lonestatistik-form]' ); 53 this.results = root.querySelector( '[data-lonestatistik-results]' ); 54 55 this.config = JSON.parse( root.getAttribute( 'data-lonestatistik' ) || '{}' ); 56 this.figures = this.config.figures || {}; 57 this.payload = null; 58 this.selection = {}; 59 } 60 61 Widget.prototype.start = function () { 62 var self = this; 63 64 if ( ! this.form || ! this.results || ! this.config.endpoint ) { 65 return; 66 } 67 68 this.bind(); 69 70 load( this.config.endpoint ) 71 .then( function ( payload ) { 72 self.payload = payload; 73 self.update(); 74 } ) 75 .catch( function () { 76 self.results.innerHTML = ''; 77 self.results.appendChild( 78 el( 'p', 'lonestatistik__hint', t( 'loadFailed', 'Kunde inte hämta lönestatistiken.' ) ) 79 ); 80 } ); 81 }; 82 83 Widget.prototype.bind = function () { 84 var self = this; 85 86 this.form.querySelectorAll( '[data-dimension]' ).forEach( function ( select ) { 87 select.addEventListener( 'change', function () { 88 self.selection[ select.getAttribute( 'data-dimension' ) ] = select.value; 89 self.update(); 90 } ); 91 92 // Browsers restore select values on soft reload; seed from the DOM 93 // so a restored selection is reflected instead of silently ignored. 94 if ( select.value ) { 95 self.selection[ select.getAttribute( 'data-dimension' ) ] = select.value; 96 } 97 } ); 98 99 this.form.addEventListener( 'reset', function () { 100 // The reset applies after this event, so clear on the next tick. 101 window.setTimeout( function () { 102 self.selection = {}; 103 self.update(); 104 }, 0 ); 105 } ); 106 }; 107 108 /** 109 * Build the stats key for the current selection. 110 * 111 * @return {string|null} Null when the required dimension is unset. 112 */ 113 Widget.prototype.key = function () { 114 var payload = this.payload; 115 var parts = []; 116 var self = this; 117 var missing = false; 118 119 payload.order.forEach( function ( dimension ) { 120 var chosen = self.selection[ dimension ]; 121 122 if ( ! chosen ) { 123 if ( dimension === payload.required ) { 124 missing = true; 125 } 126 127 parts.push( dimension === payload.required ? '' : payload.any ); 128 return; 129 } 130 131 var index = ( payload.dimensions[ dimension ] || [] ).indexOf( chosen ); 132 133 if ( -1 === index ) { 134 missing = true; 135 return; 136 } 137 138 parts.push( String( index ) ); 139 } ); 140 141 return missing ? null : parts.join( '|' ); 142 }; 143 144 /** 145 * @return {Object|null} Field name => value. 146 */ 147 Widget.prototype.stats = function () { 148 var key = this.key(); 149 150 if ( null === key ) { 151 return null; 152 } 153 154 var tuple = ( this.payload.stats || {} )[ key ]; 155 156 if ( ! tuple ) { 157 return null; 158 } 159 160 var out = {}; 161 162 ( this.payload.fields || [] ).forEach( function ( field, position ) { 163 out[ field ] = undefined === tuple[ position ] ? null : tuple[ position ]; 164 } ); 165 166 return out; 167 }; 168 169 Widget.prototype.update = function () { 170 if ( ! this.payload ) { 171 return; 172 } 173 174 this.results.innerHTML = ''; 175 176 if ( ! this.selection[ this.payload.required ] ) { 177 this.results.appendChild( 178 el( 'p', 'lonestatistik__hint', t( 'chooseFirst', 'Välj en titel.' ) ) 179 ); 180 return; 181 } 182 183 var stats = this.stats(); 184 185 if ( ! stats ) { 186 this.results.appendChild( 187 el( 'p', 'lonestatistik__hint', t( 'tooFew', 'För fÃ¥ uppgifter.' ) ) 188 ); 189 return; 190 } 191 192 this.renderFigures( stats ); 193 }; 194 195 /** 196 * @param {Object} stats Figures for the current selection. 197 */ 198 Widget.prototype.renderFigures = function ( stats ) { 199 var currency = this.payload.currency || ''; 200 var self = this; 201 202 if ( this.figures.count ) { 203 this.results.appendChild( 204 el( 205 'p', 206 'lonestatistik__basis', 207 t( 'basis', 'Bygger pÃ¥ %s uppgifter.' ).replace( '%s', numberFormat.format( stats.n ) ) 208 ) 209 ); 210 } 211 212 var list = el( 'dl', 'lonestatistik__figures' ); 213 214 function add( label, value ) { 215 if ( null === value || undefined === value ) { 216 return; 217 } 218 219 list.appendChild( el( 'dt', null, label ) ); 220 list.appendChild( el( 'dd', null, money( value, currency ) ) ); 221 } 222 223 if ( this.figures.average ) { 224 add( t( 'average', 'Genomsnitt' ), stats.avg ); 225 } 226 227 if ( this.figures.median ) { 228 add( t( 'median', 'Median' ), stats.p50 ); 229 } 230 231 if ( this.figures.minmax ) { 232 add( t( 'lowest', 'Lägsta' ), stats.min ); 233 add( t( 'highest', 'Högsta' ), stats.max ); 234 } 235 236 if ( list.childNodes.length ) { 237 this.results.appendChild( list ); 238 } 239 240 if ( ! this.figures.percentiles ) { 241 return; 242 } 243 244 var rows = []; 245 246 ( this.payload.fields || [] ).forEach( function ( field ) { 247 var match = /^p(\d+)$/.exec( field ); 248 249 // p50 is already shown as the median; repeating it as a percentile 250 // would print the same number twice. 251 if ( ! match || ( '50' === match[ 1 ] && self.figures.median ) ) { 252 return; 253 } 254 255 if ( null === stats[ field ] || undefined === stats[ field ] ) { 256 return; 257 } 258 259 rows.push( { label: match[ 1 ] + ' %', value: stats[ field ] } ); 260 } ); 261 262 if ( ! rows.length ) { 263 return; 264 } 265 266 this.results.appendChild( 267 el( 'h3', 'lonestatistik__subheading', t( 'percentiles', 'Percentiler' ) ) 268 ); 269 270 var plist = el( 'dl', 'lonestatistik__figures lonestatistik__figures--percentiles' ); 271 272 rows.forEach( function ( row ) { 273 plist.appendChild( el( 'dt', null, row.label ) ); 274 plist.appendChild( el( 'dd', null, money( row.value, currency ) ) ); 275 } ); 276 277 this.results.appendChild( plist ); 278 }; 279 280 /** 281 * Fetch one year's aggregate, sharing the promise between widgets that 282 * point at the same endpoint. 283 * 284 * @param {string} endpoint REST URL. 285 * @return {Promise<Object>} 286 */ 287 function load( endpoint ) { 288 if ( ! cache[ endpoint ] ) {
289 cache[ endpoint ] = fetch( endpoint, { credentials: 'same-origin' } ).then( function ( response ) { 290 if ( ! response.ok ) { 291 throw new Error( 'HTTP ' + response.status ); 292 } 293 294 return response.json(); 295 } ); 296 297 // Do not cache a rejection: a later retry should be able to succeed. 298 cache[ endpoint ].catch( function () { 299 delete cache[ endpoint ]; 300 } ); 301 } 302 303 return cache[ endpoint ]; 304 } 305 306 /** 307 * Initialise every widget that has not been initialised yet. 308 * 309 * Idempotent: Bricks re-runs this after AJAX updates in the builder, and 310 * re-binding would double every change listener. 311 */ 312 function init() { 313 document.querySelectorAll( '[data-lonestatistik]' ).forEach( function ( root ) { 314 if ( root.dataset.lonestatistikReady ) { 315 return; 316 } 317 318 root.dataset.lonestatistikReady = '1'; 319 new Widget( root ).start(); 320 } ); 321 } 322 323 window.bricksLonestatistik = init; 324 325 if ( 'loading' === document.readyState ) { 326 document.addEventListener( 'DOMContentLoaded', init ); 327 } else { 328 init(); 329 } 330}() );
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.