PageSourceSearch

https://traderfox.de/lib/main.js?time=1790254740

js traderfox.de collected 2026-09-24 12:59:08 UTC 26,061 bytes, 888 lines download raw bytes

1String.prototype.nf = function( decimals, min_decimals ) {
2	if( typeof min_decimals != 'number' ){ min_decimals = decimals }
3
4	var number = parseFloat(this);
5	if( isNaN(number) ){
6		return "";
7	}
8
9	return number.nf(decimals, min_decimals);
10
11}
12Number.prototype.nf = function( decimals, min_decimals ) {
13	if( typeof min_decimals != 'number' ){ min_decimals = decimals }
14
15	var number = this;
16
17	decimals = Math.round(decimals);
18	decimals = Math.min(10,decimals);
19	decimals = Math.max(0,decimals);
20
21	if( isNaN(number) || number == null ){
22		return "";
23	}
24
25	var nStr = number.toFixed( decimals ) + '';
26
27	var x = nStr.split('.');
28	var x1 = x[0];
29
30	if( x.length > 1 && min_decimals < decimals ){
31		while( x[1].charAt(x[1].length - 1) == '0' && min_decimals < decimals ){
32			x[1] = x[1].slice(0,-1);
33			decimals--;
34		}
35	}
36
37	var x2 = (x.length > 1 && x[1])? (','+x[1]) : '';
38	var rgx = /(\d+)(\d{3})/;
39	while (rgx.test(x1)) {
40		x1 = x1.replace(rgx, '$1' + '.' + '$2');
41	}
42	return x1 + x2;
43
44}
45
46String.prototype.parse_nf = function( ){
47	// get the numeric value
48	var signum = (this.indexOf('-') > -1)? '-' : '';
49	var value = this.split('.').join("");
50	var value_parts = value.match( /[0-9,]+/gi );
51	if( value_parts && value_parts.length ){
52		value = value_parts[0];
53	}else{
54		return null;
55	}
56	value_parts = value.split(',');
57	if(value_parts.length > 2){
58		// multiple commas
59		var last = value_parts.pop();
60		value = signum+value_parts.join('',last)+'.'+last;
61	}else if(value_parts.length == 2){
62		value = signum+value_parts[0]+'.'+value_parts[1];
63	}else{
64		value = signum+value_parts[0];
65	}
66	var value_num = parseFloat(value);
67	if( isNaN(value_num) ){
68		value_num = null;
69	}
70	return value_num;
71}
72Number.prototype.parse_nf = function(){
73	return this;
74}
75
76String.prototype.nf_financial = function( decimals, unit ) {
77
78	var number = parseFloat(this);
79	if( isNaN(number) ){
80		return "";
81	}
82
83	return number.nf_financial(decimals, unit);
84
85}
86Number.prototype.nf_financial = function( decimals, unit ){
87	var units_zeroes = [ 0, 3, 6, 9, 12, 15];
88	var units_names = [ '', 'Tsd.', 'Mio.', 'Mrd.', 'Bio.', 'Brd.'];
89
90	if( unit ){
91		var unit_index = units_names.indexOf(unit);
92		if( unit_index < 0 ){
93			unit_index = units_zeroes.indexOf(unit);
94		}
95		if( unit_index < 0 ){
96			unit_index = 0;
97			unit = '';
98		}else{
99			unit = ' '+units_names[unit_index];
100		}
101	}else{
102		unit_index = 0;
103		unit = '';
104	}
105
106	var number = this;
107	if( isNaN(number) ){
108		return "";
109	}
110
111	while( number > 1000 ){
112		if( units_zeroes.length <= unit_index+1 ){
113			break;
114		}
115		number = number/1000;
116		unit_index = unit_index+1;
117		unit = ' '+units_names[unit_index];
118	}
119	return number.nf(decimals)+unit;
120}
121
122function createToast(options) {
123	if (!options || typeof options != 'object') { throw new IllegalArgument('options expects Object'); }
124	if (!options.hasOwnProperty('header') || typeof options.header != 'string') { throw new IllegalArgument('options.header expects String'); }
125	if (!options.hasOwnProperty('message') || typeof options.message != 'string') { throw new IllegalArgument('options.message expects String'); }
126
127	let className = (options.hasOwnProperty('className') && typeof options.className == 'string')? options.className : '';
128	let fa = (options.hasOwnProperty('fa') && typeof options.fa == 'string')? options.fa : 'fas fa-info-circle';
129	let small = (options.hasOwnProperty('small') && typeof options.small == 'string')? options.small : '';
130
131	let parts1 = options.header.split(' ');
132	let parts2 = options.message.split(' ');
133
134	let delay = 0;
135	if (options.hasOwnProperty('delay') && typeof options.delay == 'number') {
136		delay = options.delay;
137	} else {
138		delay = Math.ceil(parts1.length + parts2.length / 4) * 1000;
139		delay = Math.max(delay,2000);
140	}
141
142	let toastElem = document.createElement('div');
143	toastElem.className = 'toast '+className;
144	toastElem.setAttribute('data-bs-delay',delay);
145	toastElem.setAttribute('data-bs-autohide','true');
146
147	toastElem.innerHTML =
148		'<div class="toast-header">'+
149			'<i class="'+fa+' me-2"></i>'+
150			'<strong class="me-auto">'+options.header+'</strong>'+
151			'<small class="text-body-secondary">
151'+small+'</small>'+
152			'<button type="button" class="btn-close" data-bs-dismiss="toast"></button>'+
153		'</div>'+
154		'<div class="toast-body">'+options.message+'</div>';
155
156	let toastContainer = document.getElementById('toast-container');
157	toastContainer.appendChild(toastElem);
158
159	let toast = bootstrap.Toast.getOrCreateInstance(toastElem);
160	toastElem.addEventListener('hidden.bs.toast',disposeToast);
161	toast.show();
162
163	return toast;
164}
165function disposeToast(event) {
166	let toastElem = event.target;
167	let toast = bootstrap.Toast.getInstance(toastElem);
168	if (toast) {
169		toast.dispose();
170		toast = null;
171	}
172	toastElem.removeEventListener('hidden.bs.toast',disposeToast);
173	toastElem.remove();
174	toastElem = null;
175}
176
177function initUpdateElements(updateHtmlElements){
178	for (let i = 0; i < updateHtmlElements.length; i++) {
179		let id = updateHtmlElements[i].getAttribute('data-id');
180		let type = updateHtmlElements[i].getAttribute('data-type');
181		if (type == 'last') {
182			let prefix = updateHtmlElements[i].getAttribute('data-prefix');
183			if (!prefix) {prefix = '';}
184			let postfix = updateHtmlElements[i].getAttribute('data-postfix');
185			if (!postfix) {postfix = '';}
186			let perfElement = new StockElement({
187				element : updateHtmlElements[i],
188				language : language,
189				stocksManager : stocksManager,
190				updateValues : ['last'],
191				configuration : {
192					key: 'last',
193					dtype: 'float',
194					defaultValue: '-',
195					precision: 2,
196					showSignum: false,
197					isBlinking: true,
198					isColored: false,
199					prefix: prefix,
200					postfix: postfix
201				},
202				data : {
203					id : id,
204					stock_id : id,
205					change : null
206				}
207			});
208			updateElements.push(perfElement);
209		}
210		else if (type == 'change') {
211			let perfElement = new StockElement({
212				element : updateHtmlElements[i],
213				language : language,
214				stocksManager : stocksManager,
215				updateValues : ['change'],
216				configuration : {
217					key: 'change',
218					dtype: 'float',
219					defaultValue: '-',
220					precision: 2,
221					showSignum: true,
222					isBlinking: true,
223					isColored: true,
224					prefix: '',
225					postfix: ' % <small>1T</small>'
226				},
227				data : {
228					id : id,
229					stock_id : id,
230					change : null
231				}
232			});
233			updateElements.push(perfElement);
234		}
235		else if (type == 'perf_1w') {
236			let perf1wElement = new StockDataElement({
237				element : updateHtmlElements[i],
238				language : language,
239				stockDataManager : stockDataManager,
240				updateValues : ['perf_1w'],
241				configuration : {
242					key: 'perf_1w',
243					dtype: 'float',
244					defaultValue: '-',
245					precision: 2,
246					showSignum: true,
247					isBlinking: true,
248					isColored: true,
249					prefix: '',
250					postfix: ' % <small>1W</small>'
251				},
252				data : {
253					id : id,
254					stock_id : id,
255					perf_1w : null
256				}
257			});
258		}
259		else if (type == 'date') {
260			let dateElement = new StockElement({
261				element : updateHtmlElements[i],
262				language : language,
263				stocksManager : stocksManager,
264				updateValues : ['lasttime'],
265				configuration : {
266					key: 'lasttime',
267					dtype: 'date',
268					defaultValue: '-',
269					formatOptions: {year: "numeric",month: "long",day:"2-digit",hour: '2-digit',minute:'2-digit'},
270					prefix: '',
271					postfix: ''
272				},
273				data : {
274					id : id,
275					stock_id : id,
276					lasttime : null
277				}
278			});
279			updateElements.push(dateElement);
280		}
281	}
282}
283
284class Game {
285	constructor(options) {
286		if ( !options || typeof options != 'object' ) { throw new IllegalArgument('options expects Object'); }
287		if ( !options.hasOwnProperty('element') || !(options.element instanceof Element) ) { throw new IllegalArgument('options.element expects DOM Element'); }
288		if ( !options.hasOwnProperty('baseStock') || typeof options.baseStock != 'object' ) { throw new IllegalArgument('options.baseStock expects Object'); }
289		if ( !options.hasOwnProperty('baseStockContainer') || !(options.baseStockContainer instanceof Element) ) { throw new IllegalArgument('options.baseStockContainer expects DOM Element'); }
290		if ( !options.hasOwnProperty('baseStockCardData') || typeof options.baseStock != 'object' ) { throw new IllegalArgument('options.baseStockCardData expects Object'); }
291
292		this._getSuggestionsBind = this._getSuggestions.bind(this);
293		this._resetBind = this._reset.bind(this);
294		this._selectCardBind = this._selectCard.bind(this);
295
296		this.element = options.element;
297
298		this.typeControls = this.element.querySelectorAll('input[name="type"]');
299		for (let i = 0; i < this.typeControls.length; i++) {
300			this.typeControls[i].addEventListener('change',this._getSuggestionsBind);
301		}
302
303		this.buttonGo = this.element.querySelector('button[name="go"]');
304		this.buttonReset = this.element.querySelector('button[name="reset"]');
305
306		this.buttonGo.addEventListener('click',this._getSuggestionsBind);
307		this.buttonReset.addEventListener('click',this._resetBind);
308
309		this._xhr = null;
310
311		// stored for reset.
312		// if the data of the current baseCard is needed, get it from the baseCard
313		this.baseStock = options.baseStock;
314		this.baseStockContainer = options.baseStockContainer;
315		this.baseStockCardData = options.baseStockCardData;
316
317		this.baseCard = new GameCard({
318			'element' : (this.baseStockContainer.children.length ? this.baseStockContainer.children[0] : null),
319			'container' : this.baseStockContainer,
320			'stock' : this.baseStock,
321			'cardData' : this.baseStockCardData,
322		});
323
324		this.suggestionContainers = this.element.querySelectorAll('.cardgame-card-container.cardgame-suggestion');
325		this.suggestionCards = [];
326
327		this.suggestionTitles = this.element.querySelectorAll('.cardgame-suggestion-title');
328	}
329	_reset() {
330		let baseStockId = this.baseCard.getStockId();
331		if (baseStockId == this.baseStock.id) {
332			this.buttonReset.classList.add('disabled');
333			return;
334		}
335
336		for(let i = 0; i < this.suggestionCards.length; i++) {
337			this.suggestionCards[i].remove();
338		}
339		this.suggestionCards = [];
340
341		this.baseCard.remove();
342		this.baseCard = new GameCard({
343			'element' : null,
344			'container' : this.baseStockContainer,
345			'stock' : this.baseStock,
346			'cardData' : this.baseStockCardData,
347		});
348
349		this.buttonReset.classList.add('disabled');
350	}
351	_selectCard(card) {
352		let baseStockId = this.baseCard.getStockId();
353		let stockId = card.getStockId();
354		if (baseStockId == stockId) {
355			return;
356		}
357
358		this.baseCard.remove();
359		this.baseCard = new GameCard({
360			'element' : null,
361			'container' : this.baseStockContainer,
362			'stock' : card.stock,
363			'cardData' : card.cardData,
364		});
365
366		for(let i = 0; i < this.suggestionCards.length; i++) {
367			let iStockId = this.suggestionCards[i].getStockId();
368			if (iStockId == stockId) {
369				this.suggestionCards.splice(i,1);
370				break;
371			}
372		}
373		card.remove();
374
375		this.buttonReset.classList.remove('disabled');
376
377		this._getSuggestions();
378	}
379
380	_getFilters() {
381		let filters = [];
382		for (let i = 0; i < this.typeControls.length; i++) {
383			if (this.typeControls[i].checked) {
384				let filter = {
385					id : this.typeControls[i].value,
386					min : this.baseCard.getValue(this.typeControls[i].value),
387					max : null,
388				}
389				filters.push(filter);
390			}
391		}
392		return filters;
393	}
394	_getFilterTitle(id) {
395		if (id == 'quality') {
396			return 'mit besserem Qualitäts-Check';
397		} else if (id == 'dividend_yield') {
398			return 'mit höherer Dividendenrendite';
399		} else if (id == 'dauerlaeufer_score') {
400			return 'mit höherer Dauerläufer-Score';
401		} else if (id == 'relative_strength') {
402			return 'mit höherer Relativen Stärke';
403		} else if (id == 'geo_performance_10y') {
404			return 'mit höherer durchschnittlichen Jahresperformance';
405		} else if (id == 'revenue_growth_ttm') {
406			return 'mit höherem Umsatzwachstum';
407		} else if (id == 'eps_growth_ttm') {
408			return 'mit höherem Gewinnwachstum';
409		} else {
410			return '';
411		}
412	}
413	_getFilterLink(id) { // TODO
414		if (id == 'quality') {
415			return '';
416		} else if (id == 'dividend_yield') {
417			return '';
418		} else if (id == 'dauerlaeufer_score') {
419			return '';
420		} else if (id == 'relative_strength') {
421			return '';
422		} else if (id == 'geo_performance_10y') {
423			return '';
424		} else if (id == 'revenue_growth_ttm') {
425			return '';
426		} else if (id == 'eps_growth_ttm') {
427			return '';
428		} else {
429			return '';
430		}
431	}
432
433	_getSuggestions() {
434
435		let filters = this._getFilters();
436
437		let title = 'Wähle ein Kriterium, um bessere Aktien angezeigt zu bekommen.';
438		if (filters.length > 1) {
439			title =
440				'<div class="text-start d-inline-block">'+
441					'<div class="mb-3">Zufällig ausgewählte Aktien</div>'+
442					'<ul class="fa-ul fs-110 fw-medium">';
443			for(let i = 0; i < filters.length; i++) {
444				let filterTitle = this._getFilterTitle(filters[i].id);
445				let filterLink = this._getFilterLink(filters[i].id);
446				if (filterLink) {
447					filterTitle =
448						'<a href="'+filterLink+'" title="Alle Aktien '+filterTitle+' anzeigen" class="text-reset">'+filterTitle+'</a>';
449				}
450				title = title +
451					'<li class="my-2"><i class="fa-li fas fa-check text-data"></i>'+filterTitle+'</li>';
452			}
453			title = title+
454					'</ul>'+
455				'</div>';
456		}
457		else if (filters.length > 0) {
458			let filterTitle = this._getFilterTitle(filters[0].id);
459			let filterLink = this._getFilterLink(filters[0].id);
460			title =
461				'<div class="row align-items-center justify-content-end">'+
462					'<div class="col">Aktien '+filterTitle+'<small class="d-block">Zufallsauswahl</small></div>';
463			if (filterLink) {
464				title = title+
465					'<div class="col-auto"><a href="'+filterLink+'" class="text-reset">alle anzeigen</a></div>';
466			}
467			title = title +
468				'</div>';
469		}
470		for (let i = 0; i < this.suggestionTitles.length; i++) {
471			this.suggestionTitles[i].innerHTML = title;
472		}
473
474		if (!filters.length) {
475			createToast({
476				header : 'Achtung',
477				message : 'Bitte wählen Sie mindestens einen Filter aus.',
478				className : 'toast-warning',
479				fa : 'fas fa-exclamation-triangle'
480			});
481			return;
482		}
483
484		if( this._xhr ){
485			createToast({
486				header : 'Achtung',
487				message : 'Bitte haben Sie einen Moment Geduld, während die Vorschläge geladen werden.',
488				className : 'toast-warning',
489				fa : 'fas fa-exclamation-triangle'
490			});
491			return;
492		}
493
494		this.disableEditing();
495
496		let data = "num="+(this.suggestionContainers.length+1);
497		for(let i = 0; i < filters.length; i++) {
498			let num = i+1;
499			data = data+'&type'+num+'='+filters[i].id;
500			if (filters[i].min !== null) {
501				data = data+'&min'+num+'='+filters[i].min;
502			}
503			if (filters[i].max !== null) {
504				data = data+'&min'+num+'='+filters[i].max;
505			}
506		}
507
508		this._xhr = new XMLHttpRequest();
509		this._xhr.open("POST", "/ajax/getCardGameSuggestions.php",true);
510		this._xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
511		this._xhr.onreadystatechange = function() {
512			if (this._xhr.readyState == 4) {
513
514				for(let i = 0; i < this.suggestionCards.length; i++) {
515					this.suggestionCards[i].remove();
516				}
517				this.suggestionCards = [];
518
519				if (this._xhr.status == 200) {
520					let data = JSON.parse(this._xhr.responseText);
521
522					if (
523						data.hasOwnProperty('data') &&
524						data.data.hasOwnProperty('stocks') && data.data.hasOwnProperty('cards') &&
525						!TraderFoxUtils.isObjectEmpty(data.data.stocks) && !TraderFoxUtils.isObjectEmpty(data.data.cards)
526					) {
527						let baseStockId = this.baseCard.getStockId();
528						let i = 0;
529						for (let stockId in data.data.cards) {
530							if (i >= this.suggestionContainers.length) {
531								break;
532							}
533							if (stockId == baseStockId) {
534								continue;
535							}
536							if (data.data.stocks.hasOwnProperty(stockId)) {
537								let card = new GameCard({
538									'element' : null,
539									'container' : this.suggestionContainers[i],
540									'stock' : data.data.stocks[stockId],
541									'cardData' : data.data.cards[stockId],
542									'onSelect' : this._selectCardBind
543								});
544								this.suggestionCards.push(card);
545								i++;
546							}
547						}
548
549						if (i < this.suggestionContainers.length - 1) {
550							for (let j = i; j < this.suggestionContainers.length; j++) {
551								this.suggestionContainers[j].innerHTML = '<div class="card"><div class="card-header py-5">Keine weiteren Aktien erfüllen alle Kriterien</div></div>';
552							}
553						}
554					} else {
555						createToast({
556							header : 'Achtung',
557							message : 'Ein Fehler ist aufgetreten. Bitte versuchen Sie es später noc
557h einmal',
558							className : 'toast-warning',
559							fa : 'fas fa-exclamation-triangle'
560						});
561					}
562				}
563
564				this._xhr = null;
565				this.enableEditing();
566			}
567		}.bind(this);
568		this._xhr.send(data);
569	}
570
571	disableEditing () {
572		for(let i = 0; i < this.suggestionCards.length; i++) {
573			this.suggestionCards[i].disableEditing();
574		}
575		for (let i = 0; i < this.typeControls.length; i++) {
576			this.typeControls[i].disabled = true;
577		}
578	}
579	enableEditing () {
580		for(let i = 0; i < this.suggestionCards.length; i++) {
581			this.suggestionCards[i].enableEditing();
582		}
583		for (let i = 0; i < this.typeControls.length; i++) {
584			this.typeControls[i].removeAttribute('disabled');
585		}
586	}
587}
588class GameCard {
589	constructor(options) {
590		if ( !options || typeof options != 'object' ) { throw new IllegalArgument('options expects Object'); }
591		if ( !options.hasOwnProperty('container') || !(options.container instanceof Element) ) { throw new IllegalArgument('options.container expects DOM Element'); }
592		if ( !options.hasOwnProperty('stock') || typeof options.stock != 'object' ) { throw new IllegalArgument('options.stock expects Object'); }
593		if ( !options.hasOwnProperty('cardData') || typeof options.cardData != 'object' ) { throw new IllegalArgument('options.cardData expects Object'); }
594
595		this._onFlipBind = this._onFlip.bind(this);
596		this._onSelectBind = this._onSelect.bind(this);
597
598		this.stock = options.stock;
599		this.cardData = options.cardData;
600
601		this.onSelectCallback = null;
602		if (options.hasOwnProperty('onSelect') && typeof options.onSelect == 'function') {
603			this.onSelectCallback = options.onSelect;
604		}
605
606		if (options.hasOwnProperty('element') && options.element instanceof Element) {
607			this.element = options.element;
608		} else {
609			this.element = this._createElement();
610			options.container.innerHTML = '';
611			options.container.appendChild(this.element);
612		}
613
614		this.buttonSelect = null;
615		if (this.onSelectCallback) {
616			this.buttonSelect = this.element.querySelector('button[name="select"]');
617			if (this.buttonSelect) {
618				this.buttonSelect.addEventListener('click',this._onSelectBind);
619			}
620		}
621
622		this.fliptoggles = this.element.querySelectorAll('.flip-card-toggle');
623		for(let i = 0; i < this.fliptoggles.length; i++) {
624			this.fliptoggles[i].addEventListener('click',this._onFlipBind);
625		}
626
627	}
628	getStockId() {
629		return this.stock.id;
630	}
631	destroy() {
632		if (this.buttonSelect) {
633			this.buttonSelect.removeEventListener('click',this._onSelectBind);
634		}
635		for(let i = 0; i < this.fliptoggles.length; i++) {
636			this.fliptoggles[i].removeEventListener('click',this._onFlipBind);
637		}
638	}
639	remove() {
640		this.destroy();
641
642		let parent = this.element.parentNode;
643
644		this.element = null;
645		this.stock = null;
646		this.cardData = null;
647		this.fliptoggles = null;
648		this.buttonSelect = null;
649
650		parent.innerHTML = '<div class="card cover"></div>';
651	}
652	disableEditing () {
653		if (this.buttonSelect) {
654			this.buttonSelect.disabled = true;
655		}
656	}
657	enableEditing () {
658		if (this.buttonSelect) {
659			this.buttonSelect.removeAttribute('disabled');
660		}
661	}
662	getValue(field) {
663		let value = null;
664		if (this.cardData.hasOwnProperty(field)) {
665			value = parseFloat(this.cardData[field]);
666			if (isNaN(value)) {
667				value = null;
668			}
669		}
670		return value;
671	}
672
673	_createElement () {
674		let element = document.createElement('div');
675		element.className = 'flip-card cardgame-flip-card mx-auto';
676
677		let qualityHtml = '-';
678		if (this.cardData.hasOwnProperty('quality')) {
679			let value = parseFloat(this.cardData.quality);
680			if (!isNaN(value)) {
681				qualityHtml = value.nf(0);
682			}
683		}
684		let dividendYieldHtml = '-';
685		if (this.cardData.hasOwnProperty('dividend_yield')) {
686			let value = parseFloat(this.cardData.dividend_yield);
687			if (!isNaN(value)) {
688				value = value * 100;
689				dividendYieldHtml = value.nf(0)+' %';
690			}
691		}
692		let dauerlaeuferScoreHtml = '-';
693		if (this.cardData.hasOwnProperty('dauerlaeufer_score')) {
694			let value = parseFloat(this.cardData.dauerlaeufer_score);
695			if (!isNaN(value)) {
696				value = value * 100;
697				dauerlaeuferScoreHtml = value.nf(0);
698			}
699		}
700		let RelativeStrengthHtml = '-';
701		if (this.cardData.hasOwnProperty('relative_strength')) {
702			let value = parseFloat(this.cardData.relative_strength);
703			if (!isNaN(value)) {
704				value = value * 100;
705				RelativeStrengthHtml = value.nf(0);
706			}
707		}
708		let geoPerformance10yHtml = '-';
709		if (this.cardData.hasOwnProperty('geo_performance_10y')) {
710			let value = parseFloat(this.cardData.geo_performance_10y);
711			if (!isNaN(value)) {
712				value = value * 100;
713				geoPerformance10yHtml = value.nf(0)+' %';
714			}
715		}
716		let revenueGrowthTTMHtml = '-';
717		if (this.cardData.hasOwnProperty('revenue_growth_ttm')) {
718			let value = parseFloat(this.cardData.revenue_growth_ttm);
719			if (!isNaN(value)) {
720				value = value * 100;
721				revenueGrowthTTMHtml = value.nf(0)+' %';
722			}
723		}
724		let epsGrowthTTMHtml = '-';
725		if (this.cardData.hasOwnProperty('eps_growth_ttm')) {
726			let value = parseFloat(this.cardData.eps_growth_ttm);
727			if (!isNaN(value)) {
728				value = value * 100;
729				epsGrowthTTMHtml = value.nf(0)+' %';
730			}
731		}
732		let descriptionCardHtml = '';
733		let flipCardMenuHtml = '';
734		if (this.cardData.hasOwnProperty('description') && this.cardData.description) {
735			flipCardMenuHtml =
736				'<div class="flip-card-menu">'+
737					'<button type="button" class="flip-card-toggle clickable" data-card="front"><i></i><i></i></button>'+
738				'</div>';
739			descriptionCardHtml =
740				'<div class="flip-card-back">'+
741					'<div class="card">'+
742						'<div class="card-body">'+this.cardData.description+'</div>'+
743					'</div>'+
744					flipCardMenuHtml+
745				'</div>';
746		}
747
748		let header = '';
749		if (this.onSelectCallback) {
750			header =
751				'<div class="card-header has-button">'+
752					'<strong class="title">'+this.stock.nice_name+'</strong>'+
753					'<button name="select" type="button" class="btn btn-card-game-icon" title="Suche bessere Aktien als '+this.stock.nice_name+'"><img src="/img/cardgame/click.png"></button>'+
754				'</div>';
755		} else {
756			header = '<div class="card-header"><strong class="title">'+this.stock.nice_name+'</strong></div>';
757		}
758
759		element.innerHTML =
760			'<div class="flip-card-inner shadow">'+
761				'<div class="flip-card-front">'+
762					'<div class="card">'+
763						header+
764						'<img src="https://charts.traderfox.com/chart/time_range-3650/adjustment-full/stock_id-'+this.stock.id+'/width-410/height-205/class-aktienquartett" class="card-img">'+
765						'<div class="card-body p-0 flex-grow-0"><div>'+
766							'<table class="table table-sm table-light"><tbody>'+
767								'<tr><th>Qualitätscheck</th><td>'+qualityHtml+'</td></tr>'+
768								'<tr><th>Dividendenrendite</th><td>'+dividendYieldHtml+'</td></tr>'+
769								'<tr><th>Dauerläufer Score</th><td>'+dauerlaeuferScoreHtml+'</td></tr>'+
770								'<tr><th>Relative Stärke Score</th><td>'+RelativeStrengthHtml+'</td></tr>'+
771								'<tr><th>Performance 10 J</th><td>'+geoPerformance10yHtml+'</td></tr>'+
772								'<tr><th>Umsatzw. (TTM)</th><td>'+revenueGrowthTTMHtml+'</td></tr>'+
773								'<tr><th>Gewinnw. (TTM)</th><td>'+epsGrowthTTMHtml+'</td></tr>'+
774							'</tbody></table>'+
775						'</div></div>'+
776					'</div>'+
777					flipCardMenuHtml+
778				'</div>'+
779				descriptionCardHtml
780			'</div>';
781
782		return element;
783	}
784
785	_onFlip (event) {
786		this.element.classList.toggle('flipped');
787	}
788	_onSelect(event) {
789		if (!this.onSelectCallback) {
790			return;
791		}
792		this.onSelectCallback(this);
793	}
794}
795
796
797class TFSearch {
798	constructor (element, options) {
799		if (!element || !(element instanceof Element)) { throw new IllegalArgument('element expects DOM Element'); }
800		if (!options || typeof options != 'object') { options = {}; }
801
802		this.id = element.id;
803		if (!this.id) {
804			this.id = (''+Math.random()).replace(/\D/g,"");
805			element.id = this.id;
806		}
807
808		if (!options.hasOwnProperty('callback')) {
809			options.callback = TFSearch.onSelected;
810		}
811		if (!options.hasOwnProperty('request')) {
812			options.request = 'full';
813		}
814
815		options.dataSrc = this._dataSrc.bind(this);
816		options.displayField = 'display';
817		options.valueField = 'value';
818
819		this.request = options.request;
820		this.xhr = null;
821
822		this.autocomplete = new Autocomplete(element,options);
823
824	}
825	destroy () {
826		if( this.xhr ){
827			this.xhr.abort();
828			this.xhr = null;
829		}
830		this.autocomplete.destroy();
831		this.autocomplete = null;
832	}
833
834	async _dataSrc (term) {
835		if( this.xhr ){
836			this.xhr.abort();
837			this.xhr = null;
838		}
839		let items = await new Promise(resolve => {
840
841			this.xhr = new XMLHttpRequest();
842			if (this.request == 'stock') {
843				this.xhr.open('GET', '/ajax/searchStocks.php?query='+term, true);
844			} else {
845				this.xhr.open('GET', '/ajax/searchTerms.php?query='+term, true);
846			}
847			this.xhr.onreadystatechange = function() {
848				if (this.xhr.readyState === XMLHttpRequest.DONE){
849					if (this.xhr.status === 200) {
850						let data = JSON.parse(this.xhr.responseText);
851						let items = [];
852						if( data && data.length ){
853							for( var i = 0; i < data.length; i++ ){
854
855								let display = data[i].hasOwnProperty('display') ? data[i].display : data[i].value;
856
857								if (data[i].type == 'title') {
858									display = '<div class="search-results-title">'+display+'</div>';
859								}
860								data[i]['display'] = display;
861								items.push(data[i]);
862							}
863						}
864						this.xhr = null;
865						resolve(items);
866					}else{
867						resolve([]);
868					}
869				}
870			}.bind(this)
871
872			this.xhr.send();
873
874		});
875
876		return items
877	}
878
879	static onSelected (selected) {
880		if (selected && typeof selected == 'object') {
881			if (selected.hasOwnProperty('link')) {
882				location.href = selected.link;
883			} else if (selected.hasOwnProperty('stock') && typeof selected.stock == 'object' && selected.stock.hasOwnProperty('link')) {
884				location.href = selected.stock.link;
885			}
886		}
887	}
888}

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.