PageSourceSearch

https://binderim.com/js/autocomplete.j4bl94k5gi.js

js binderim.com collected 2026-09-24 14:40:21 UTC 20,857 bytes, 544 lines download raw bytes

1$(function () {
2    var languageStorageKey = 'search-language-id';
3
4    /**
5     * T-20260816-15/M1 (#1625 B): kullanici NAVIGASYON yolu aktif culture prefix'ini tasimali.
6     * PathBase sunucudan <body data-pathbase> ile gelir (TR'de bos string, /en'de "/en") --
7     * basket.js/marketplace-redesign.js ile AYNI kopru. BULUNAN HATA: /en'de arama onerisine
8     * tiklayan kullanici prefix'siz /Card/Detail/... adresine gidiyordu, yani sessizce TR'ye
9     * dusuyordu. fetch()/XHR URL'lerine DOKUNULMADI.
10     */
11    function pathBase() {
12        return (document.body && document.body.dataset && document.body.dataset.pathbase) || '';
13    }
14
15    // Oneri satirlarinin hedef URL'i. Gercek <a href> uretebilmek icin tiklama
16    // aninda degil, render aninda hesaplanir; boylece orta tus / Ctrl+tik ile
17    // yeni sekmede acma, "Baglantiyi yeni sekmede ac" ve link onizleme calisir.
18    function buildResultHref(item, languageId) {
19        if (!item || !item.id) {
20            return '';
21        }
22
23        var base = pathBase();
24
25        if (item.type === 'card') {
26            return base + '/Card/Detail/' + encodeURIComponent(item.id);
27        }
28
29        if (item.type === 'set') {
30            return base + '/UserSet/Detail/' + encodeURIComponent(item.id);
31        }
32
33        if (item.type === 'illustrator') {
34            return base + '/Home/Search?searchTerm=' + encodeURIComponent(item.label || '') +
35                '&languageId=' + encodeURIComponent(languageId);
36        }
37
38        return '';
39    }
40
41    // Combobox kalibi: <ul role="listbox"> icinde secilebilir oge role="option"
42    // olmali. Rolu <li>'ye degil <a>'ya veriyoruz — href korundugu icin orta tus /
43    // Ctrl+tik / "yeni sekmede ac" calismaya devam eder (bunlar tarayici
44    // seviyesinde href'e bagli, role'e degil), ekran okuyucu ise satiri combobox
45    // secenegi olarak duyurur. Odak input'ta kalir, aktif oge
46    // aria-activedescendant ile isaretlenir -> optionlar tabindex="-1".
47    function createResultItem(item, languageId, optionId) {
48        var $item = $('<li>')
49            .addClass('autocompleteItem')
50            .addClass('list-group-item list-group-item-action')
51            .attr('role', 'presentation')
52            .attr('data-id', item.id)
53            .attr('data-type', item.type);
54
55        var $link = $('<a>')
56            .addClass('ac-item-link')
57            .attr('id', optionId)
58            .attr('role', 'option')
59            .attr('aria-selected', 'false')
60            .attr('tabindex', '-1');
61
62        var href = buildResultHref(item, languageId);
63        if (href) {
64            $link.attr('href', href);
65        }
66
67        var $content = $("<div class='ac-item-content'></div>");
68
69        if (item.image) {
70            $content.append($("<div class='ac-card-img'><img src='" + item.image + "' width='50px'/></div>"));
71        } else {
72            var iconClass = item.type === 'illustrator' ? 'bi-brush'
73                : item.type === 'set' ? 'bi-collection'
74                : 'bi-image';
75            $content.append($("<div class='ac-card-img ac-no-image'><i class='bi " + iconClass + " ac-icon' aria-hidden='true'></i></div>"));
76        }
77
78        var $desc = $("<div class='ac-card-desc'></div>")
79            .append(
80                $('<span>').text(item.label),
81                $('<span>').text(item.description)
82            );
83
84        // Secenegin erisilebilir adi: "etiket, aciklama". Icerikten turetilse de
85        // acikca vermek, gorsel kutu/ikon gibi parcalarin ada karismasini onler.
86        var accessibleName = item.description
87            ? item.label + ', ' + item.description
88            : item.label;
89        $link.attr('aria-label', accessibleName);
90
91        $content.append($desc);
92        $link.append($content);
93        $item.append($link);
94        return $item;
95    }
96
97    function createCategoryHeader(categoryName, count) {
98        // Kategori basligi secilebilir bir oge degil; listbox'ta option olarak
99        // sayilmamasi icin role="presentation".
100        return $("<li class='autocompleteCategory list-group-item' role='presentation'></li>")
101            .append($('<strong>').text(categoryName + ' (' + count + ')'));
102    }
103
104    $('.search-box-wrap').each(function (wrapIndex) {
105        var $wrap = $(this);
106
107        // ---- i18n (Issue #1625 B, shared dilimi) ----------------------------
108        // Kullaniciya gorunen metinler artik SABIT DEGIL: _SearchBoxPartial.cshtml
109        // .search-box-wrap kokune data-i18n-* basar, degerler _SearchBoxPartial.resx(.en)
110        // dosyalarindan gelir. Ayni desen basket.js/destek.js (#1613). Attribute yoksa
111        // bugunku TR metne duseriz -- davranis DEGISMEZ.
112        // 🔴 Kok BU wrap'in KENDISI: arama kutusu layout'ta iki kez (masaustu + mobil)
113        // basiliyor, her ornek kendi attribute'larini okur.
114        function t(name, fallback) {
115            var value = $wrap.attr('data-i18n-' + name);
116            return (value === undefined || value === null || value === '') ? fallback : value;
117        }
118        /** '{0}' yer tutucusunu argumanlarla degistirir (basket.js ile ayni sozlesme). */
119        function fmt(template) {
120            var args = Array.prototype.slice.call(arguments, 1);
121            return String(template == null ? '' : template).replace(/\{(\d+)\}/g, function (match, index) {
122                var v = args[Number(index)];
123                return v === undefined ? match : v;
124            });
125        }
126
127        var $input = $wrap.find('.search-term-input');
128        var $list = $wrap.find('.autocomplete-list');
129        var $languageMenu = $wrap.find('.search-language-menu');
130        var $languageInput = $wrap.find('.search-language-id-input');
131        var $flag = $wrap.find('.active-language-flag');
132        var $globe = $wrap.find('.active-language-globe');
133        var debounceTimer = null;
134        var pendingRequest = null;
135        var latestRequestId = 0;
136        var responseCache = new Map();
137
138        // Arama kutusu layout'ta iki kez basiliyor (masaustu + mobil), bu yuzden
139        // id'ler ornek basina benzersiz olmali.
140        var listId = 'ac-list-' + wrapIndex;
141        var optionIdPrefix = 'ac-opt-' + wrapIndex + '-';
142        var activeIndex = -1;
143        // Aktif secenegi klavye mi fare mi belirledi? Fare listeden ciktiginda
144        // sadece farenin koydugu vurgu temizlensin, klavye secimi ayakta kalsin.
145        var activeFromPointer = false;
146
147        $list.attr({ id: listId, role: 'listbox' });
148        $input.attr({
149            role: 'combobox',
150            'aria-expanded': 'false',
151            'aria-controls': listId,
152            'aria-autocomplete': 'list',
153            'aria-haspopup': 'listbox'
154        });
155
156        // Ekran okuyucuya sonuc sayisini duyuran gorunmez bolge.
157        var $status = $('<div>')
158            .addClass('visually-hidden ac-status')
159            .attr({ role: 'status', 'aria-live': 'polite', 'aria-atomic': 'true' })
160            .appendTo($wrap);
161
162        function announce(message) {
163            $status.text(message);
164        }
165
166        function $options() {
167            return $list.find('a.ac-item-link[role="option"]');
168        }
169
170        function clearActiveOption() {
171            activeIndex = -1;
172            activeFromPointer = false;
173            $options().removeClass('ac-active').attr('aria-selected', 'false');
174            $input.removeAttr('aria-activedescendant');
175        }
176
177        function setActiveOption(index, fromPointer) {
178            var $all = $options();
179            if (!$all.length) {
180                clearActiveOption();
181                return;
182            }
183
184            // Uclarda basa/sona sar.
185            if (index < 0) {
186                index = $all.length - 1;
187            } else if (index >= $all.length) {
188                index = 0;
189            }
190
191            $all.removeClass('ac-active').attr('aria-selected', 'false');
192            activeIndex = index;
193            activeFromPointer = !!fromPointer;
194
195            var $active = $all.eq(index)
196                .addClass('ac-active')
197                .attr('aria-selected', 'true');
198
199            $input.attr('aria-activedescendant', $active.attr('id'));
200
201            // Aktif oge listede gorunur kalsin (klavyeyle asagi inerken).
202            // Fare ile gelen secimde kaydirma yapmayiz: zaten gorunur olan bir
203            // satirdir ve kaydirmak imlecin altindaki satiri degistirip geri
204            // besleme dongusu yaratir.
205            if (fromPointer) {
206                return;
207            }
208
209            var el = $active[0];
210            if (el && typeof el.scrollIntoView === 'function') {
211                el.scrollIntoView({ block: 'nearest' });
212            }
213        }
214
215        function showList() {
216            $list.show();
217            $input.attr('aria-expanded', 'true');
218        }
219
220        function hideList() {
221            $list.hide();
222            $input.attr('aria-expanded', 'false');
223            clearActiveOption();
224        }
225
226        function getSelectedLanguageId() {
227            var value = Number($languageInput.val());
228            if (!Number.isNaN(value) && value >= 0) {
229                return value;
230            }
231            return 0;
232        }
233
234        function abortPendingRequest() {
235            if (pendingRequest && pendingRequest.readyState !== 4) {
236                pendingRequest.abort();
237            }
238
239            pendingRequest = null;
240        }
241
242        function renderResults(data) {
243            $list.empty();
244            clearActiveOption();
245
246            // GEÇİCİ BAKIM (T-20260715-03): sunucu autocomplete'i devre dışı bırakıp
247            // maintenance=true döndürünce, kullanıcıya boş açılır liste yerine kısa bilgi göster.
248            if (data && data.maintenance) {
249                var message = data.maintenanceMessage || t('maintenance', 'Arama önerileri geçici olarak bakımda');
250                $list.append(
251                    $("<li class='autocompleteCategory list-group-item text-muted' role='presentation'></li>").text(message)
252                );
253                showList();
254                announce(message);
255                return;
256            }
257
258            var totalResults = (data.cards ? data.cards.length : 0) +
259                (data.illustrators ? data.illustrators.length : 0) +
260                (data.sets ? data.sets.length : 0);
261
262            if (totalResults === 0) {
263                hideList();
264                announce(t('no-results', 'Sonuç bulunamadı'));
265                return;
266            }
267
268            var languageId = getSelectedLanguageId();
269            var optionIndex = 0;
270
271            function appendGroup(title, items) {
272                if (!items || items.length === 0) {
273                    return;
274                }
275
276                $list.append(createCategoryHeader(title, items.length));
277                items.forEach(function (item) {
278                    $list.append(createResultItem(item, languageId, optionIdPrefix + optionIndex));
279                    optionIndex++;
280                });
281            }
282
283            appendGroup(t('group-cards', 'Kartlar'), data.cards);
284            appendGroup(t('group-illustrators', 'Illustratorler'), data.illustrators);
285            appendGroup(t('group-sets', 'Setler'), data.sets);
286
287            showList();
288            announce(fmt(t('results-announcement', '{0} sonuç bulundu. Gezinmek için yön tuşlarını kullanın.'), totalResults));
289        }
290
291        function updateLanguageUi(id) {
292            if (Number(id) === 0) {
293                $flag.addClass('d-none');
294                $globe.removeClass('d-none');
295                return;
296            }
297
298            var $item = $languageMenu.find(".search-language-item[data-lang-id='" + id + "']").first();
299            if (!$item.length) {
300                $flag.addClass('d-none');
301                $globe.removeClass('d-none');
302                return;
303            }
304
305            var code = ($item.data('flag-code') || 'gb').toString().toLowerCase();
306            $flag.attr('src', 'https://flagcdn.com/w20/' + code + '.png');
307            $flag.removeClass('d-none');
308            $globe.addClass('d-none');
309        }
310
311        function initializeLanguage() {
312            var selectedId = 0;
313            var stored = localStorage.getItem(languageStorageKey);
314            if (stored) {
315                var parsed = Number(stored);
316                if (!Number.isNaN(parsed) && parsed >= 0 && $languageMenu.find(".search-language-item[data-lang-id='" + parsed + "']").length > 0) {
317                    selectedId = parsed;
318                }
319            }
320
321            $languageInput.val(selectedId);
322            updateLanguageUi(selectedId);
323        }
324
325        function runAutocomplete(term) {
326            var languageId = getSelectedLanguageId();
327            var cacheKey = String(languageId) + '|' + term.toLowerCase();
328            var requestId = ++latestRequestId;
329
330            if (responseCache.has(cacheKey)) {
331                renderResults(responseCache.get(cacheKey));
332                return;
333            }
334
335            abortPendingRequest();
336
337            pendingRequest = $.ajax({
338                url: '/Home/Autocomplete',
339                method: 'GET',
340                data: {
341                    term: term,
342                    languageId: languageId
343                }
344            })
345                .done(function (data) {
346                    if (requestId !== latestRequestId) {
347                        return;
348                    }
349
350                    responseCache.set(cacheKey, data);
351                    renderResults(data);
352                })
353                .fail(function (_xhr, status) {
354                    if (status !== 'abort' && requestId === latestRequestId) {
355                        hideList();
356                    }
357                })
358                .always(function () {
359                    if (requestId === latestRequestId) {
360                        pendingRequest = null;
361                    }
362                });
363        }
364
365        initializeLanguage();
366
367        $languageMenu.on('click', '.search-language-item', function () {
368            var selectedId = Number($(this).data('lang-id'));
369            if (Number.isNaN(selectedId) || selectedId < 0) {
370                return;
371            }
372
373            $languageInput.val(selectedId);
374            localStorage.setItem(languageStorageKey, String(selectedId));
375            updateLanguageUi(selectedId);
376            responseCache.clear();
377
378            if ($input.val().length >= 2) {
379                $input.trigger('input');
380            }
381        });
382
383        $input.on('input', function () {
384            var term = $input.val().trim();
385
386            if (debounceTimer) {
387                clearTimeout(debounceTimer);
388            }
389
390            // Metin degisti: ekrandaki oneriler artik bayat. Vurguyu HEMEN
391            // dusuruyoruz, yoksa debounce (180ms) dolmadan Enter'a basildiginda
392            // eski listedeki secenegin href'ine gidilir — kullanicinin o an
393            // yazdigi terimle alakasiz bir sayfa. Vurgu yokken Enter serbest
394            // metin aramasina duser.
395            clearActiveOption();
396
397            if (term.length < 2) {
398                abortPendingRequest();
399                hideList();
400                return;
401            }
402
403            debounceTimer = setTimeout(function () {
404                runAutocomplete(term);
405            }, 180);
406        });
407
408        function runFreeTextSearch() {
409            var searchTerm = $input.val().trim();
410            if (searchTerm.length >= 2) {
411                // 🔴 QA (#1625 B AC4) BULGUSU: buradaki adres prefix'SIZ sabitti. Oneri
412                // satirlari buildResultHref() ile pathBase() aliyordu ama SERBEST METIN
413                // aramasi (vurgulu oneri yokken Enter / arama butonu) almiyordu -- /en'deki
414                // kullanici arama yaptigi anda sessizce TR arama sayfasina dusuyordu.
415                window.location.href = pathBase() + '/Home/Search?searchTerm=' + encodeURIComponent(searchTerm) +
416                    '&languageId=' + encodeURIComponent(getSelectedLanguageId());
417            }
418        }
419
420        // Klavye gezinmesi (WAI-ARIA combobox): odak input'ta kalir, aktif secenek
421        // aria-activedescendant ile isaretlenir. keypress ok tuslarini uretmedigi
422        // icin tum tuslar keydown'da ele alinir.
423        $input.on('keydown', function (e) {
424            // IME (Japonca/Korece aday secimi, Android GBoard) aday onaylarken de
425            // Enter uretir; eski keypress handler'i bunu gormezdi, keydown gorur.
426            // Onaylama Enter'ini arama tetikleyicisi saymayalim.
427            // DIKKAT: jQuery event nesnesi isComposing'i kopyalamaz (jQuery 3'un
428            // addProp listesinde yok) — originalEvent'ten okumak ZORUNLU, yoksa
429            // guard sessizce hicbir sey yapmaz. keyCode 229 Android GBoard yolu.
430            var nativeEvent = e.originalEvent || e;
431            if (nativeEvent.isComposing || e.which === 229 || e.keyCode === 229) {
432                return;
433            }
434
435            var key = e.key;
436            var listOpen = $list.is(':visible');
437            var optionCount = $options().length;
438
439            if (key === 'ArrowDown' || key === 'ArrowUp') {
440                if (!listOpen) {
441                    // Kapaliyken asagi ok, mevcut terim icin listeyi yeniden acar.
442                    if ($input.val().trim().length >= 2) {
443                        $input.trigger('input');
444                    }
445                    return;
446                }
447
448                if (!optionCount) {
449                    return;
450                }
451
452                e.preventDefault();
453                setActiveOption(activeIndex + (key === 'ArrowDown' ? 1 : -1));
454                return;
455            }
456
457            if ((key === 'Home' || key === 'End') && listOpen && optionCount) {
458                e.preventDefault();
459                setActiveOption(key === 'Home' ? 0 : optionCount - 1);
460                return;
461            }
462
463            if (key === 'Escape') {
464                if (listOpen) {
465                    e.preventDefault();
466                    hideList();
467                }
468                return;
469            }
470
471            if (key === 'Tab') {
472                // Odak kutudan cikiyorsa acik liste kalmasin.
473                if (listOpen) {
474                    hideList();
475                }
476                return;
477            }
478
479            if (key === 'Enter') {
480                e.preventDefault();
481
482                var $active = listOpen && activeIndex >= 0 ? $options().eq(activeIndex) : $();
483                var href = $active.length ? $active.attr('href') : '';
484
485                if (href) {
486                    hideList();
487                    window.location.href = href;
488                    return;
489                }
490
491                // Vurgulu secenek yoksa eski davranis: serbest metin aramasi.
492                runFreeTextSearch();
493            }
494        });
495
496        // Fare ile gezinirken klavye vurgusu ile cift vurgu olusmasin.
497        // mouseenter DEGIL mousemove dinliyoruz: klavyeyle asagi inerken liste
498        // sabit duran imlecin altinda kayar ve mouseenter tetiklenir; bu, aktif
499        // secimi imlecin altindaki satira sicratip Enter'i yanlis karta goturur.
500        // mousemove yalnizca fare gercekten hareket ettiginde gelir.
501        $list.on('mousemove', 'a.ac-item-link[role="option"]', function () {
502            var index = $options().index(this);
503            if (index >= 0 && index !== activeIndex) {
504                setActiveOption(index, true);
505            }
506        });
507
508        $list.on('mouseleave', function () {
509            // Klavyeyle secilmis bir secenek varsa fare listeden ciksa da kalsin.
510            if (activeFromPointer) {
511                clearActiveOption();
512            }
513        });
514
515        // Navigasyonu tarayici yapar (satirlar gercek <a href>). Burada sadece
516        // listeyi kapatiyoruz; yeni sekmede acan tiklamalarda (Ctrl/Cmd/Shift,
517        // orta tus -> 'click' hic tetiklenmez) liste acik kalsin ki kullanici
518        // ust uste birkac kart acabilsin.
519        $list.on('click', 'li.autocompleteItem a.ac-item-link', function (e) {
520            if (e.ctrlKey || e.metaKey || e.shiftKey || e.altKey) {
521                return;
522            }
523
524            hideList();
525        });
526
527        // Disari tiklaninca kapat. hideList kullaniyoruz ki aria-expanded ve
528        // aria-activedescendant da sifirlansin.
529        $(document).on('click', function (e) {
530            if (!$(e.target).closest($list).length && !$(e.target).closest($input).length) {
531                if ($list.is(':visible')) {
532                    hideList();
533                }
534            }
535        });
536    });
537
538    $(document).on('submit', '.search-form', function () {
539        var $input = $(this).find('.search-language-id-input');
540        if ($input.length && !$input.val()) {
541            $input.val('0');
542        }
543    });
544});

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.