PageSourceSearch

https://www.stockfeel.com.tw/wp-content/themes/stockfeel_2016_theme/js/header/search.js?v=202609090726

js stockfeel.com.tw collected 2026-09-24 18:26:14 UTC 19,096 bytes, 603 lines download raw bytes

1const searchModule = () => {
2	const data = {
3		suggestData: null,
4		suggestResult: {
5			stocks: [],
6			extend: [],
7			history: [],
8		},
9		hotKeyword: '',
10	};
11	const eventHandlers = {};
12	const getEventHandler = (handleSelector) => {
13		if (!eventHandlers[handleSelector]) {
14			eventHandlers[handleSelector] = {
15				keypress(evt) {
16					let event = new CustomEvent('keypressInSearchInput', {
17						detail: {
18							target: evt.target,
19							key: evt.key,
20						},
21					});
22					if (evt.key == 'Enter') evt.preventDefault();
23					document.querySelector(handleSelector).dispatchEvent(event);
24				},
25				keydown(evt) {
26					if (evt.key == 'Enter') return; // Enter 不理 keydown
27
28					// 上下鍵只處理選擇
29					if (evt.key == 'ArrowUp' || evt.key == 'ArrowDown') {
30						// 避免游標跑掉
31						evt.preventDefault();
32						let event = new CustomEvent('keydownInSearchInput', {
33							detail: {
34								target: evt.target,
35								key: evt.key,
36							},
37						});
38						document.querySelector(handleSelector).dispatchEvent(event);
39						return;
40					}
41
42					// 其它的也丟去 keypress
43					this.keypress(evt);
44				},
45			};
46		}
47		return eventHandlers[handleSelector];
48	};
49	const setSuggestData = (obj) => {
50		data.suggestData = obj;
51	};
52	const setSuggestResult = (obj) => {
53		data.suggestResult = obj;
54	};
55	const setHotKeyword = (str) => {
56		data.hotKeyword = str;
57	};
58	const getData = () => data;
59	return {
60		getData,
61		setSuggestData,
62		setSuggestResult,
63		setHotKeyword,
64		getEventHandler,
65	};
66};
67
68const searchApis = {
69	getSearchSuggestData: (apiurl, site_url) => {
70		return fetch(apiurl + '/api/getsuggest_cacheable.php')
71			.then((res) => res.json())
72			.then((data) => {
73				let searchSuggest = JSON.parse(data['searchsuggest']) || [];
74				let autocompleteIndex = JSON.parse(data['autocompleteindex']) || [];
75				let searchStock = JSON.parse(data['searchstock']) || [];
76				return {
77					searchSuggest,
78					autocompleteIndex,
79					searchStock,
80				};
81			})
82			.catch((err) => {
83				console.log('連線失敗 suggest');
84			});
85	},
86	getAccountData: (action) => {
87		let formData = new FormData();
88		formData.append('action', action);
89
90		return fetch(window.admin_url, {
91			method: 'POST',
92			body: formData,
93			headers: {
94				'User-Agent':
95					'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.79 Safari/537.36',
96			},
97		}).then((res) => res.text());
98	},
99	getSearchRank: () => {
100		return fetch(
101			`${window.template_url}/api/getsearchsuggest_cacheable.php?v=` + Math.floor(new Date().getTime() / 1800000)
102		)
103			.then((res) => res.json())
104			.then((data) => {
105				sfUtils.shuffle(data);
106				return data;
107			});
108	},
109};
110
111const searchControls = {
112	// _suggestData: null,
113	// setSuggestData: function (v) {
114	// 	this._suggestData = v;
115	// },
116	// getStockIndexes: function () {
117	// 	if (this._suggestData && this._suggestData.searchstock)
118	// 		return this._suggestData.searchstock;
119	// 	return [];
120	// },
121	// getCurrentStockIfAny: function () {
122	// 	let query = new URLSearchParams(window.location.search).get('query');
123	// 	let stocksIndex = this.getStockIndexes();
124	// 	if (!query || !stocksIndex[0]) return null;
125	// 	return stocksIndex.filter(item => {
126	// 		return item.stock_num.toLowerCase() === query
127	// 			|| item.stock_name.toLowerCase() === query;
128	// 	})[0];
129	// },
130	// suggestDataReady: function () {
131	// 	this.isSuggestDataReady = true;
132	// },
133	// waitForSuggestDataReady: async function () {
134	// 	return new Promise((resolve, reject)=>{
135	// 		let cnt = 0;
136	// 		let tmer = setInterval(()=>{
137	// 			cnt++;
138	// 			if (this.isSuggestDataReady) {
139	// 				resolve();
140	// 				clearInterval(tmer);
141	// 			}
142	// 			if (cnt > 10) {
143	// 				reject();
144	// 				clearInterval(tmer);
145	// 			}
146	// 		}, 200);
147	// 	});
148	// },
149	// callMenu 內 function 重寫
150	getSearchHistory: function () {
151		let row = getCookie('_sfSearchHistory');
152		let history = JSON.parse(row) || [];
153		return history;
154	},
155	setSearchHistory: function (query, history) {
156		let cookieArr = Array.from(new Set(history.unShift(query))).slice(0, 6);
157		let cookieQuery = JSON.stringify(cookieArr);
158		document.cookie = `_sfSearchHistory=${cookieQuery}; expires=Fri, 1 Jan 2038 12:00:00 UTC; path=/`;
159	},
160	checkSearchType: function (str) {
161		if (str.match(/^[0-9]+$/g)) return 'NUM';
162		else if (str.match(/^[A-Za-z]+$/g)) return 'EN';
163		else return 'CH';
164	},
165	searchSuggestQuery: function (query, { autocompleteIndex }) {
166		return autocompleteIndex.filter((item) => item.indexOf(query) === 0).slice(0, 6);
167	},
168	searchSuggestTerms: function (query, { searchSuggest }) {
169		const key = Object.keys(searchSuggest).find((item) => item.indexOf(query) === 0);
170		return searchSuggest[key] || [];
171	},
172	searchSuggestStock: function (query, options, { searchStock }) {
173		const NUM_FLAG = 2,
174			NAME_FLAG = 1;
175		return searchStock
176			.filter((item) => {
177				let isNum, isName;
178				if (options & NUM_FLAG) {
179					isNum = item.stock_num.toLowerCase().indexOf(query.toLowerCase()) === 0;
180				}
181				if (options & NAME_FLAG) {
182					isName = item.stock_name.toLowerCase().indexOf(query.toLowerCase()) === 0;
183				}
184				return isNum || isName;
185			})
186			.slice(0, 3);
187	},
188	searchMatch: function (query, data) {
189		let stocks = [],
190			extend = [];
191		const { searchSuggestQuery, checkSearchType, searchSuggestTerms, searchSuggestStock } = this;
192		if (query.length === 1) {
193			extend = searchSuggestQuery(query, data);
194		}
195		if (query.length >= 2) {
196			const type = checkSearchType(query, data);
197			extend = searchSuggestTerms(query, data);
198			switch (type) {
199				case 'NUM':
200					stocks = searchSuggestStock(query, 2, data);
201					break;
202				case 'EN':
203					stocks = searchSuggestStock(query, 3, data);
204					break;
205				case 'CH':
206				default:
207					stocks = searchSuggestStock(query, 1, data);
208					break;
209			}
210		}
211		return { stocks, extend };
212	},
213};
214let getSearchAutocomplete = (autoSelector) => {
215	let autoComp = {
216		_selector: autoSelector,
217		_listeners: {},
218		listen(eventName, handler) {
219			if (!this._listeners[eventName]) {
220				this._listeners[eventName] = [];
221			}
222			this._listeners[eventName].push(handler);
223		},
224		fire(eventName) {
225			let handlers = this._listeners[eventName];
226			let args = Array.prototype.slice.call(arguments, 1);
227			if (!handlers) return;
228			handlers.forEach((handler) => {
229				handler(...args);
230			});
231		},
232		renderStocks: function (stocks) {
233			if (stocks.length === 1) {
234				let stock = stocks[0];
235				let mrrStyle =
236					stock['mov_range'] > 0
237						? 'float: right; color: #ef685f;'
238						: stock['mov_range'] < 0
239						? 'float: right; color: #2ace43;'
240						: 'float: right;';
241				return `
242						<li class="auto__option pv2 ph3" data-value="${stock['stock_name']}"
243							data-strvalue="${stock['stock_name']}" data-numvalue="${stock['stock_num']}">
244							<span class="text-gray-1 f5">${stock['stock_num']}</span>
245							<div>
246								<div class="dib v-mid text-gray-1" style="float: right;">${
247									stock['country'] === 'TWO' ? '上櫃' : stock['country'] === 'emerging' ? '興櫃' : '上市'
248								}</div>
249								<div class="text-gray-3 f4 fw9"> ${stock['stock_name']} </div>
250								<div class="text-gray-3 f5 fw9" style="${mrrStyle}"> ${Number(stock['mov_range']).toFixed(2) + '%'} </div>
251								<div class="text-gray-3 f5 fw9"> ${Number(stock['close']).toFixed(2)} </div>
252							</div>
253						</li>
254					`;
255			} else if (stocks.length > 1) {
256				return stocks.reduce(
257					(acc, stock) =>
258						(acc += `
259						<li class="auto__option text-gray-2 hover-gray-0" data-value="${stock['stock_name']}"
260							data-strvalue="${stock['stock_name']}" data-numvalue="${stock['stock_num']}">
261							<div class="auto__stock dib pv2 ph3 fw6 v-mid">${stock['stock_num']} ${stock['stock_name']}</div>
262							<div class="dib v-mid text-gray-1">${
263								stock['country'] === 'TWO' ? '上櫃' : stock['country'] === 'emerging' ? '興櫃' : '上市'
264							}</div>
265						</li>
266					`),
267					''
268				);
269			}
270			return '';
271		},
272		renderOptions: function (data, titleStr) {
273			let html = `<div class="text-primary-0 pv2 ph3">${titleStr}</div>`;
274			if (typeof data === 'string') {
275				html += `<li class="auto__option text-gray-2 hover-gray-0" data-value="${data}">
276					<div class="pv2 ph3">${data}</div>
277				</li>`;
278			}
279			if (typeof data === 'object') {
280				html += data.reduce(
281					(acc, item) =>
282						(acc += `
283						<li class="auto__option text-gray-2 hover-gray-0" data-value="${item}">
284							<div class="pv2 ph3">${item}</div>
285						</li>
286					`),
287					''
288				);
289			}
290			return html;
291		},
292		handleKeyRender: function (query, { stocks, extend, history }) {
293			const { renderStocks, renderOptions } = this;
294			const autoEle = document.querySelector(autoSelector);
295            autoEle.innerHTML = renderOptions(history, '搜尋紀錄');
296		},
297		handleClickRender: function (history) {
298			const autoEle = document.querySelector(autoSelector);
299			if (autoEle.classList.contains('is-show')) return;
300			autoEle.classList.add('is-show');
301			autoEle.innerHTML = searchRender.renderOptions(history, '搜尋紀錄');
302		},
303		select(ele) {
304			const autoEle = document.querySelector(autoSelector);
305			let selected = autoEle.querySelector('.is-selected');
306			if (selected) {
307				selected.classList.remove('is-selected');
308			}
309			ele.classList.add('is-selected');
310			this.fire('onSelect', ele);
311		},
312		selectNext() {
313			const autoEle = document.querySelector(autoSelector);
314			let options = autoEle.querySelectorAll('li.auto__option');
315			let selected = autoEle.querySelector('.is-selected');
316			let next = null;
317			if (selected) {
318				selected.classList.remove('is-selected');
319				next = Array.prototype.filter.call(options, (e, i) => {
320					return options[i - 1] == selected;
321				})[0];
322			} else {
323				next = options[0];
324			}
325			if (next) {
326				next.classList.add('is-selected');
327			}
328		},
329		selectPrevious() {
330			const autoEle = document.querySelector(autoSelector);
331			let options = autoEle.querySelectorAll('li.auto__option');
332			let selected = autoEle.querySelector('.is-selected');
333			let previous = null;
334			if (selected) {
335				selected.classList.remove('is-selected');
336				previous = Array.prototype.filter.call(options, (e, i) => {
337					return options[i + 1] == selected;
338				})[0];
339			} else {
340				if (options[0]) {
341					previous = options[options.length - 1];
342				}
343			}
344			if (previous) {
345				previous.classList.add('is-selected');
346			}
347		},
348		getSelectedValue() {
349			const autoEle = document.querySelector(autoSelector);
350			let selected = autoEle.querySelector('.is-selected');
351			if (!selected)
352				return {
353					value: '',
354					strValue: '',
355					numValue: '',
356				};
357			return {
358				value: selected.dataset.value,
359				strValue: selected.dataset.strvalue,
360				numValue: selected.dataset.numvalue,
361			};
362		},
363		initEvents() {
364			document.querySelector(autoSelector).addEventListener('click', (evt) => {
365				let option = evt.target.closest('.auto__option');
366				if (option) {
367					window.location = `${$site_url}/isearch/?query=${option.dataset.value}`;
368				}
369			});
370			document.querySelector(autoSelector).addEventListener('mouseover', (evt) => {
371				if (!evt.target) return;
372				let ele = evt.target.closest('li.auto__option');
373				if (!ele) return;
374				this.select(ele);
375			});
376		},
377	};
378	autoComp.initEvents();
379	return autoComp;
380};
381const searchRender = getSearchAutocomplete('.search__auto');
382
383(async function initSearch() {
384	let search = (window.searchModuleInstance = searchModule());
385	let searchData = search.getData();
386	let history = searchControls.getSearchHistory();
387	try {
388		window.stockfeel.search = new Promise(async (resolve, reject) => {
389			const suggestData = await searchApis.getSearchSuggestData(window.template_url, window.site_url);
390			search.setSuggestData(suggestData);
391
392			const rankData = await searchApis.getSearchRank();
393			search.setHotKeyword(rankData[0]['query']);
394			searchData = search.getData();
395			search.setSuggestResult({ ...searchData.suggestResult, history });
396			document.querySelector('.search__input').setAttribute('placeholder', searchData.hotKeyword);
397
398			resolve({ ...suggestData, rankData });
399			reject({});
400		});
401
402		// searchControls.suggestDataReady();
403		// if(suggestData.searchstock && window.location.pathname.match(/isearch/)) {
404		// 	const query = "<?php echo $_GET['query']; ?>".toLowerCase();
405		// 	if(query) {
406
407		// 		window.stockfeel.search.setSuggestData(suggestData);
408		// 		// 開頭只存資料,之後再 render
409		// 		// let stock = window.stockfeel.search.getCurrentStockIfAny();
410		// 		// if(stock) renderStock(stock.stock_num);
411		// 	}
412		// }
413
414		// document.querySelectorAll(".form-inline").forEach(node => {
415		// 	node.querySelector(".search-input-nav")
416		// 		.setAttribute("placeholder", hotkeywordData);
417		// 	node.querySelector(".search-input-hotkey").value = hotkeywordData;
418		// });
419	} catch (err) {
420		console.log(err);
421	}
422	let getSearchKeyStrokes = (args) => {
423		let keyStrokes = {
424			datas: {
425				realInput: '',
426				selectedValue: '',
427			},
428			_searchInputKeypress(evt) {
429				let query = evt.detail.target.value;
430				if (query != this.datas.realInput && query != this.datas.selectedValue) {
431					this.datas.realInput = query;
432				}
433				if (query) {
434					const { extend, stocks }
434 = searchControls.searchMatch(query, searchData.suggestData);
435					search.setSuggestResult({
436						...searchData.suggestResult,
437						extend,
438						stocks,
439					});
440					if (evt.detail.key === 'Enter') {
441						window.location = `${$site_url}/isearch/?query=${query}`;
442					}
443				} else if (!query && evt.detail.key === 'Enter') {
444					window.location = `${$site_url}/isearch/?query=${searchData.hotKeyword}`;
445				}
446				args.autoEle.handleKeyRender(query, searchData.suggestResult);
447			},
448			searchInputKeypress(evt) {
449				if (this.searchInputKeypressTimer) {
450					clearTimeout(this.searchInputKeypressTimer);
451					this.searchInputKeypressTimer = null;
452				}
453				this.searchInputKeypressTimer = setTimeout(() => {
454					this._searchInputKeypress(evt);
455					this.searchInputKeypressTimer = null;
456				}, 10);
457			},
458			// 因為上下鍵不發 keypress
459			searchInputKeydown(evt) {
460				if (evt.detail.key != 'ArrowUp' && evt.detail.key != 'ArrowDown') return;
461				if (evt.detail.key == 'ArrowUp') {
462					args.autoEle.selectPrevious();
463				}
464				if (evt.detail.key == 'ArrowDown') {
465					args.autoEle.selectNext();
466				}
467				this.updateSelectedValue();
468				evt.detail.target.value = this.getDisplayValue();
469			},
470			updateSelectedValue() {
471				let selectedValue = args.autoEle.getSelectedValue();
472				if (
473					this.datas.realInput &&
474					searchControls.checkSearchType(this.datas.realInput) == 'NUM' &&
475					selectedValue.numValue
476				) {
477					this.datas.selectedValue = selectedValue.numValue;
478				} else {
479					this.datas.selectedValue = selectedValue.strValue || selectedValue.value;
480				}
481			},
482			getDisplayValue() {
483				return this.datas.selectedValue || this.datas.realInput;
484			},
485		};
486		return keyStrokes;
487	};
488	let headerSearchKeyStroke = getSearchKeyStrokes({ autoEle: searchRender });
489
490	// header 綁實際處理方法
491	document.querySelector('header').addEventListener('keypressInSearchInput', (evt) => {
492		headerSearchKeyStroke.searchInputKeypress(evt);
493	});
494	document.querySelector('header').addEventListener('keydownInSearchInput', (evt) => {
495		headerSearchKeyStroke.searchInputKeydown(evt);
496	});
497	searchRender.listen('onSelect', (e) => {
498		headerSearchKeyStroke.updateSelectedValue();
499		// 搜尋頁多 search bar 同步
500		[...document.querySelectorAll('.search__input')].forEach(
501			(ele) => (ele.value = headerSearchKeyStroke.getDisplayValue())
502		);
503		[...document.querySelectorAll('.search-and-member input')].forEach(
504			(ele) => (ele.value = headerSearchKeyStroke.getDisplayValue())
505		);
506	});
507	// search auto complete
508	document.querySelector('input.search__input').addEventListener('click', (evt) => {
509		const { history } = searchData.suggestResult;
510		searchRender.handleClickRender(history);
511		headerSearchKeyStroke.searchInputKeypress({
512			detail: {
513				target: {
514					value: evt.target.value,
515				},
516				key: evt.key,
517			},
518		});
519	});
520	document.querySelector('input.search__input').addEventListener('keypress', (evt) => {
521		window.searchModuleInstance.getEventHandler('header').keypress(evt);
522	});
523	document.querySelector('input.search__input').addEventListener('keydown', (evt) => {
524		window.searchModuleInstance.getEventHandler('header').keydown(evt);
525	});
526	if (document.querySelector('.header__search div img')) {
527		document.querySelector('.header__search div img').addEventListener('click', (evt) => {
528			let query = document.querySelector('input.search__input').value;
529			if (query) window.location = `${$site_url}/isearch/?query=${query}`;
530		});
531	}
532})();
533
534function getCookie(name) {
535	let nameEQ = name + '=';
536	let ca = document.cookie.split(';');
537	for (let i = 0; i < ca.length; i++) {
538		let c = ca[i];
539		while (c.charAt(0) == ' ') c = c.substring(1, c.length);
540		if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length, c.length);
541	}
542	return null;
543}
544
545// 搜尋頁面的協同
546
547// //active header nav on current page
548// $('.nav-bottom-col.link').each(function() {
549// 	if (window.location.href.match($(this).prop('href') + '[a-zA-Z0-9./_]+')) {
550// 		$(this).toggleClass('actived');
551// 	}
552// });
553
554// $('.nav-bottom-col.menu').click(function() {
555// 	$(this).toggleClass('actived');
556// });
557
558// document.querySelectorAll(".form-inline .search-input-nav")
559// 	.forEach(node => {
560// 		node.addEventListener("keypress", (evt) => {
561// 			if(evt.keyCode === 13) {
562// 				const isSpacingEmpty = node.value.replace(/\s/g, '').length === 0 ? true : false;
563// 				if(isSpacingEmpty || node.value === "") {
564// 					const query = node.parentNode.querySelector(".search-input-hotkey").value;
565// 					node.value = query;
566// 					window.location.assign(`${$site_url}/isearch/?query=${query}`);
567// 				} else {
568// 					const query = node.value;
569// 					window.location.assign(`${$site_url}/isearch/?query=${query}`);
570// 				}
571// 			}
572// 		})
573// 	})
574
575// document.querySelectorAll(".form-inline button")
576// 	.forEach(node => {
577// 		node.addEventListener("click", (evt) => {
578// 			const isSpacingEmpty = node.value.replace(/\s/g, '').length === 0 ? true : false;
579// 			if(isSpacingEmpty || node.value === "") {
580// 				const query = node.parentNode.querySelector(".search-input-nav").value;
581// 				node.value = query;
582// 				window.location.assign(`${$site_url}/isearch/?query=${query}`);
583// 			} else {
584// 				const query = node.querySelector(".search-input-nav").value;
585// 				window.location.assign(`${$site_url}/isearch/?query=${query}`);
586// 			}
587// 		})
588// 	})
589
590// document.querySelectorAll(".form-inline").forEach(node => {
591// 	node.addEventListener("submit", (evt) => {
592// 		evt.preventDefault();
593// 	})
594// })
595if (document.querySelector('.member__search-dummy')) {
596	document.querySelector('.member__search-dummy').classList.add('pointer');
597	document.querySelector('.member__search-dummy').addEventListener('click', () => {
598		let inp = document.querySelector('.search-block input');
599		if (inp && inp.value) {
600			window.location = `${$site_url}/isearch/?query=${inp.value}`;
601		}
602	});
603}

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.