PageSourceSearch

https://upphandling24.se/wp-content/plugins/lonestatistik/assets/js/lonestatistik.js?ver=2.5.1

js upphandling24.se collected 2026-09-25 04:52:09 UTC 7,988 bytes, 330 lines download raw bytes

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.