PageSourceSearch

https://cascadehills.com/wp-content/plugins/minervakb/assets/js/minerva-kb.js?ver=2.0.9

js cascadehills.com collected 2026-09-26 10:40:16 UTC 96,193 bytes, 2,894 lines download raw bytes

1/**
2 * Project: Minerva KB
3 * Copyright: 2015-2016 @KonstruktStudio
4 */
5(function ($) {
6
7    var GLOBAL_DATA = window.MinervaKB;
8    var ui = window.MinervaCommonUI;
9
10    var i18n = GLOBAL_DATA.i18n;
11    var platform = GLOBAL_DATA.platform;
12    var settings = GLOBAL_DATA.settings;
13    var info = GLOBAL_DATA.info;
14
15    var $body = $('body');
16
17    /**
18     * libs
19     */
20    if (!String.prototype.trim) {
21        String.prototype.trim = function () {
22            return this.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, '');
23        };
24    }
25
26    function simpleExtend(obj1, obj2) {
27        for (var attr in obj2) {
28            if (obj2.hasOwnProperty(attr)) obj1[attr] = obj2[attr];
29        }
30
31        return obj1;
32    }
33
34    /**
35     * Gets form data for ajax calls
36     * @param form
37     * @returns {}
38     */
39    function getFormData(form) {
40        return $(form).serializeArray().reduce(function(store, kv) {
41            store[kv.name] = kv.value;
42            return store;
43        }, {});
44    }
45
46    /**
47     * Debounces function execution
48     * TODO: make shared utils lib
49     * @param func
50     * @param wait
51     * @param immediate
52     * @returns {Function}
53     */
54    function debounce(func, wait, immediate) {
55        var timeout;
56        return function () {
57            var context = this, args = arguments;
58            var later = function () {
59                timeout = null;
60                if (!immediate) {
61                    func.apply(context, args);
62                }
63            };
64            var callNow = immediate && !timeout;
65            clearTimeout(timeout);
66            timeout = setTimeout(later, wait);
67            if (callNow) {
68                func.apply(context, args);
69            }
70        };
71    }
72
73    /**
74     * Throttles function execution. Based on Ben Alman implementation
75     * TODO: make shared utils lib
76     * @param delay
77     * @param noTrailing
78     * @param callback
79     * @param atBegin
80     * @returns {wrapper}
81     */
82    function throttle(delay, noTrailing, callback, atBegin) {
83        var timeoutId;
84        var lastExec = 0;
85
86        if (typeof noTrailing !== 'boolean') {
87            atBegin = callback;
88            callback = noTrailing;
89            noTrailing = undefined;
90        }
91
92        function wrapper() {
93            var elapsed = +new Date() - lastExec;
94            var args = arguments;
95
96            var exec = function _exec() {
97                lastExec = +new Date();
98                callback.apply(this, args );
99            }.bind(this);
100
101            function clear() {
102                timeoutId = undefined;
103            }
104
105            if (atBegin && !timeoutId) {
106                exec();
107            }
108
109            timeoutId && clearTimeout(timeoutId);
110
111            if (atBegin === undefined && elapsed > delay) {
112                exec();
113            } else if (noTrailing !== true) {
114                timeoutId = setTimeout(
115                    atBegin ?
116                        clear :
117                        exec,
118                    atBegin === undefined ?
119                    delay - elapsed :
120                        delay
121                );
122            }
123        }
124
125        return wrapper;
126    }
127
128    function addAjaxNonce(data) {
129        data['nonce_key'] = GLOBAL_DATA.nonce.nonceKey;
130        data['nonce_value'] = GLOBAL_DATA.nonce.nonce;
131
132        return data;
133    }
134
135    function addAjaxNonceToFormData(formData) {
136        formData.append('nonce_key',GLOBAL_DATA.nonce.nonceKey);
137        formData.append('nonce_value',GLOBAL_DATA.nonce.nonce);
138
139        return formData;
140    }
141
142    /**
143     * Sends Google Analytics event, if API available
144     * @param category
145     * @param action
146     * @param label
147     * @param value
148     */
149    function trackGoogleAnalytics(category, action, label, value) {
150        if (window.ga && typeof window.ga === 'function') {
151            window.ga('send', 'event', category, action, label, value, {
152                nonInteraction: true
153            });
154        }
155    }
156
157    // theme
158    var ajaxUrl = GLOBAL_DATA.ajaxUrl;
159    var $kbSearch = $('.kb-search__input');
160    var NO_RESULTS_CLASS = 'kb-search__input-wrap--no-results';
161    var HAS_CONTENT_CLASS = 'kb-search__input-wrap--has-content';
162    var HAS_RESULTS_CLASS = 'kb-search__input-wrap--has-results';
163    var REQUEST_CLASS = 'kb-search__input-wrap--request';
164    var hasResults = false;
165    var resultsCount = 0;
166    var activeResult = -1;
167    var ESC = 27;
168    var ENTER = 13;
169    var ARROW_LEFT = 37;
170    var ARROW_UP = 38;
171    var ARROW_RIGHT = 39;
172    var ARROW_DOWN = 40;
173    var $doc = $('html, body');
174    var $adminBar = $('#wpadminbar');
175    var adminOffset = $adminBar.length ? $adminBar.height() : 0;
176    var searchMode = settings['search_mode'];
177    var searchNeedleLength = parseInt(settings['search_needle_length']);
178    var searchRequestsCount = 0;
179    var searchCache = {};
180    var trackingCache = {};
181    var onPageFAQItems = {};
182    var onPageGlossaryItems = {};
183
184    function setupExtraSearchGroupsOnPageTargets() {
185        // 1. FAQ
186        if (settings['active_search_groups'].includes('faq')) {
187            var hasOnPageFAQItems = false;
188
189            var $faqContainer = $('.fn-kb-faq-container');
190
191            if (!$faqContainer.length) {
192                return;
193            }
194
195            $faqContainer.each(function(wrapIndex, item) {
196                var $wrap = $(item);
197
198                $wrap.find('.fn-kb-faq-link').each(function(index, item) {
199                    var id = item.dataset.id;
200
201                    if (!onPageFAQItems[id]) {
202                        onPageFAQItems[id] = {
203                            el: item,
204                            wrapIndex: wrapIndex
205                        };
206                        hasOnPageFAQItems = true;
207                    }
208                });
209            });
210
211            if (hasOnPageFAQItems) {
212                $body.on('click', '.js-mkb-search-results-group-faq a', function(e) {
213                    var id = e.currentTarget.dataset.id;
214
215                    if (onPageFAQItems[id]) {
216                        e.preventDefault();
217                        e.stopImmediatePropagation();
218
219                        scrollToFAQItem(id);
220                    }
221                });
222            }
223        }
224
225        // 2. Glossary
226        if (settings['active_search_groups'].includes('glossary')) {
227            var hasOnPageGlossaryItems = false;
228            var $glossaryContainer = $('.js-mkb-glossary-list');
229
230            if (!$glossaryContainer.length) {
231                return;
232            }
233
234            $glossaryContainer.each(function(wrapIndex, item) {
235                var $wrap = $(item);
236
237                $wrap.find('.js-mkb-glossary-term-entry').each(function(index, item) {
238                    var id = item.dataset.id;
239
240                    if (!onPageGlossaryItems[id]) {
241                        onPageGlossaryItems[id] = {
242                            el: item,
243                            wrapIndex: wrapIndex
244                        };
245                        hasOnPageGlossaryItems = true;
246                    }
247                });
248            });
249
250            if (hasOnPageGlossaryItems) {
251                $body.on('click', '.js-mkb-search-results-group-glossary a', function(e) {
252                    var id = e.currentTarget.dataset.id;
253
254                    if (onPageGlossaryItems[id]) {
255                        e.preventDefault();
256                        e.stopImmediatePropagation();
257
258                        scrollToGlossaryItem(id);
259                    }
260                });
261            }
262        }
263    }
264
265    function scrollToFAQItem(id) {
266        var faqItem = onPageFAQItems[id];
267
268        if (faqItem) {
269            var $question = $(faqItem.el);
270            var scrollOffset = parseInt(settings['faq_scroll_offset']['size']);
271
272            $body.trigger({
273                type: 'minerva_faq_toggle_answer_' + faqItem.wrapIndex,
274                id: id
275            });
276
277            setTimeout(function() {
278                $doc.animate({
279                    scrollTop: $question.offset().top - scrollOffset
280                }, 300);
281            }, 110); // programmatic faq open is 100ms
282        }
283    }
284
285    function scrollToGlossaryItem(id) {
286        var glossaryItem = onPageGlossaryItems[id];
287
288        if (glossaryItem) {
289            var scrollOffset = parseInt(settings['glossary_scroll_offset']['size']);
290
291            $doc.animate({
292                scrollTop: $(glossaryItem.el).offset().top - scrollOffset
293            }, 300);
294        }
295    }
296
297    /**
298     * Live search result handler
299     * @param $search
300     * @param response
301     */
302    function handleSearchResultsReceive($search, response) {
303        var $wrap = $search.parents('.kb-search__input-wrap');
304        var $summary = $wrap.find('.kb-summary-text-holder');
305        var $results = $wrap.find('.kb-search__results');
306        var results = response.result;
307        var extraResults = response.extraResults || {};
308        var faqResults = (extraResults.faq || []).filter(function(result) {
309            return settings['faq_enable_pages'] || onPageFAQItems[result.id];
310        });
311        var glossaryResults = (extraResults.glossary || []).filter(function(result) {
312            return settings['glossary_enable_pages'] || onPageGlossaryItems[result.id];
313        });
314        var topicsResults = extraResults.topics || [];
315        var extraResultsCount = faqResults.length + glossaryResults.length + topicsResults.length;
316        var totalResults = resultsCount = results.length + extraResultsCount;
317        var searchNeedle = response.search;
318        var resultsContent;
319        var resultsInfoHtml = response.results_info || '';
320        var searchShowTopics = $search.data('show-results-topic') === 1;
321        var showTopicsLabel = $search.data('topic-label');
322        var useCustomTopicColors = Boolean($search.data('custom-topic-colors'));
323        var showExcerpt = settings['live_search_show_excerpt'];
324
325        if (searchMode === 'nonblocking') {
326            var needle = $search.val() && $search.val().trim();
327
328            if (!needle || needle.length < searchNeedleLength) {
329                // in nonblocking mode user could have already removed the typed string
330
331                results = [];
332                hasResults = false;
333                resultsCount = 0;
334                activeResult = -1;
335                $wrap.removeClass(HAS_RESULTS_CLASS).removeClass(NO_RESULTS_CLASS);
336                $summary.html('');
337                $results.html('');
338
339                return;
340            }
341        }
342
343        $results.html('');
344
345        /**
346         * 0. No results
347         */
348        if (!results && !results.length && !faqResults.length && !glossaryResults.length) {
349            if (settings['track_search_without_results']) {
350                trackGoogleAnalytics(
351                    settings['ga_bad_search_category'],
352                    settings['ga_bad_search_action'],
353                    searchNeedle,
354                    settings['ga_bad_search_value'] || 0
355                );
356            }
357
358            hasResults = false;
359            resultsCount = 0;
360            activeResult = -1;
361            $wrap.removeClass(HAS_RESULTS_CLASS).addClass(NO_RESULTS_CLASS);
362            $summary.html(i18n['no-results']);
363        }
364
365        function getResultsGroupHTML(label, count, groupId) {
366            return $(
367                '<ul class="js-mkb-search-results-group-' + groupId + '">' +
368                    '<li>' +
369                        '<span class="kb-search__results-group">' +
370                            '<span class="kb-search__results-group-title">' + label + '</span>' +
371                            '<span class="kb-search__results-group-count">' +
372                            + count + ' ' +
373                            (count === 1 ? i18n['result'] : i18n['results']) +
374                            '</span>' +
375                        '</span>' +
376                    '</li>' +
377                '</ul>'
378            )
379        }
380
381        hasResults = true;
382        activeResult = -1;
383
384        $wrap.removeClass(NO_RESULTS_CLASS).addClass(HAS_RESULTS_CLASS);
385        $summary.html(totalResults + ' ' + (totalResults === 1 ? i18n['result'] : i18n['results']));
386
387        /**
388         * 1. KB results
389         */
390        if (results.length) {
391            if (settings['track_search_with_results']) {
392                trackGoogleAnalytics(
393                    settings['ga_good_search_category'],
394                    settings['ga_good_search_action'],
395                    searchNeedle,
396                    settings['ga_good_search_value'] || 0
397                );
398            }
399
400            // TODO: regroup product label for grouped / standalone results
401            // TODO: maybe switch to FE label
402            var $kbResultsInitialHTML = extraResultsCount ?
403                getResultsGroupHTML(i18n['search_group_kb'], results.length, 'kb') :
404                $('<ul></ul>');
405
406            $kbResultsInitialHTML.append(resultsInfoHtml);
407
408            resultsContent = results.reduce(function ($el, result) {
409                var isTopicPresent = Boolean(result.topics[0]);
410                var isProductArticle = Boolean(result.product);
411                var firstTopic = result.topics[0];
412                var topicColorStyle = isTopicPresent && useCustomTopicColors ?
413                    'style="background-color: ' + firstTopic.color + '"' :
414                    '';
415
416                return $el.append(
417                    '<li>' +
418                        '<a href="' + result.link + '">' +
419                            '<span class="kb-search__result-header">' +
420                                '<span class="kb-search__result-title">' + result.title + '</span>' +
421                                (searchShowTopics && isTopicPresent ?
422                                    '<span class="kb-search__result-topic">' +
423                                        '<span class="kb-search__result-topic-label">' + showTopicsLabel + '</span>' +
424                                        '<span class="kb-search__result-topic-name" ' + topicColorStyle + '>' +
425                                            (isProductArticle && result.product !== firstTopic.name ?
426                                                result.product + ' / ' : '') + (firstTopic.name)  +
427                                        '</span>' +
428                                    '</span>' :
429                                    '') +
430                            '</span>' +
431                            (showExcerpt ? '<span class="kb-search__result-excerpt">' + result.excerpt + ' ...</span>' : '') +
432                        '</a>' +
433                    '</li>'
434                );
435            }, $kbResultsInitialHTML);
436        }
437
438        /**
439         * 2. FAQ results
440         */
441        if (faqResults.length) {
442            var faqContent = faqResults.reduce(function($el, result) {
443                return $el.append('<li><a href="' + result.link + '" data-id="' + result.id + '">' + result.title + '</a></li>');
444            }, getResultsGroupHTML(i18n['search_group_faq'], faqResults.length, 'faq'));
445        }
446
447        /**
448         * 3. Glossary results
449         */
450        if (glossaryResults.length) {
451            var glossaryContent = glossaryResults.reduce(function($el, result) {
452                return $el.append('<li><a href="' + result.link + '" data-id="' + result.id + '">' + result.title + '</a></li>');
453            }, getResultsGroupHTML(i18n['search_group_glossary'], glossaryResults.length, 'glossary'));
454        }
455
456        /**
457         * 4. Topics results
458         */
459        if (topicsResults.length) {
460            var topicsContent = topicsResults.reduce(function($el, result) {
461                return $el.append('<li><a href="' + result.link + '">' + result.title + '</a></li>');
462            }, getResultsGroupHTML(i18n['search_group_kb_topics'], topicsResults.length, 'topics'));
463        }
464
465        var resultGroups = settings['active_search_groups'];
466
467        resultGroups.forEach(function(groupId) {
468            switch(groupId) {
469                case 'kb':
470                    $results.append(resultsContent);
471                    break;
472
473                case 'faq':
474                    $results.append(faqContent);
475                    break;
476
477                case 'glossary':
478                    $results.append(glossaryContent);
479                    break;
480
481                case 'topics':
482                    $results.append(topicsContent);
483                    break;
484
485                default:
486                    break;
487            }
488        });
489    }
490
491    function focusInput() {
492        $kbSearch.filter('[data-autofocus="1"]').focus();
493    }
494
495    function nextSearchResult() {
496        var $resultItems = $('.kb-search__results li a');
497
498        activeResult = activeResult + 1 >= resultsCount ? 0 : activeResult + 1;
499        $resultItems.eq(activeResult).focus();
500    }
501
502    function prevSearchResult() {
503        var $resultItems = $('.kb-search__results li a');
504
505        activeResult = activeResult - 1 < 0 ? resultsCount - 1 : activeResult - 1;
506        $resultItems.eq(activeResult).focus();
507    }
508
509    /**
510     * Live search keypress handler
511     * @param e
512     */
513    function onSearchKeyPress(e) {
514
515        if (!$(".kb-search__input").is(":focus") && !$(".kb-search__results a").is(":focus")) {
516            return; //we do not to mess with keypress unless search is in focus
517        }
518
519        var $search = $(".kb-search__input:focus");
520
521        switch (e.keyCode) {
522            case ESC:
523                focusInput();
524                break;
525
526            case ARROW_UP:
527                prevSearchResult();
528                break;
529
530            case ARROW_DOWN:
531                nextSearchResult();
532                break;
533
534            case ENTER:
535                if ($search.length && !$search.val().trim()) {
536                    e.preventDefault();
537                    e.stopImmediatePropagation();
538                }
539                return;
540
541            default:
542                return;
543        }
544
545        e.preventDefault(); // prevent the default action (scroll / move caret)
546    }
547
548    function serializeRequestParams(params) {
549        var serialized = "";
550
551        for (var key in params) {
552            if (serialized !== "") {
553                serialized += "&";
554            }
555
556            serialized += key + "=" + encodeURIComponent(params[key]);
557        }
558
559        return serialized;
560    }
561
562
563
564    /**
565     * Search request stats is saved separately for non-blocking search
566     */
567    function trackSearchRequest(searchParams) {
568
569        searchParams.trackResults = true;
570
571        return $.ajax({
572            method: 'POST',
573            url: ajaxUrl,
574            dataType: 'json',
575            data: searchParams
576        });
577    }
578
579    /**
580     * Waits for a timeout untill tracking results
581     * @param searchParams
582     */
583    var trackingTimerId = null;
584
585    function handleNonBlockingTracking(searchParams) {
586        var serializedParams = serializeRequestParams(searchParams);
587
588        if (trackingTimerId) {
589            clearTimeout(trackingTimerId);
590        }
591
592        if (trackingCache[serializedParams]) {
593            return;
594        }
595
596        trackingTimerId = setTimeout(function() {
597            trackSearchRequest(searchParams).then(function() {
598                trackingCache[serializedParams] = true;
599            });
600        }, 1000);
601    }
602
603    /**
604     * Lice search type handler
605     * @param $search
606     */
607    function onSearchType($search) {
608        var $wrap = $search.parents('.kb-search__input-wrap');
609        var needle = $search.val() && $search.val().trim();
610        var $topics = $wrap.find('input[name="topics"]');
611        var topics = $topics.length ? $topics.val() : null;
612        var $kbId = $wrap.find('input[name="kb_id"]');
613        var $lang = $wrap.find('input[name="lang"]');
614        var kbId = $kbId.length ? parseInt($kbId.val()) : null;
615        var searchParams = {
616            action: 'mkb_kb_search',
617            search: needle,
618            mode: searchMode
619        };
620
621        if (kbId) {
622            searchParams.kb_id = kbId;
623        }
624
625        if (topics) {
626            searchParams.topics = topics;
627        }
628
629        if ($lang.length && $lang.val()) {
630            searchParams.lang = $lang.val();
631        }
632
633        var serializedParams = serializeRequestParams(searchParams);
634
635        if (needle) {
636            $wrap.addClass(HAS_CONTENT_CLASS);
637        } else {
638            $wrap.removeClass(HAS_CONTENT_CLASS);
639        }
640
641        // check cache for response
642        if (settings['search_request_fe_cache'] && searchCache[serializedParams]) {
643            // track cached result, if it wasn tracked before
644            if (searchMode === 'nonblocking') {
645                handleNonBlockingTracking(searchParams);
646            }
647
648            return handleSearchResultsReceive.call(this, $search, searchCache[serializedParams]);
649        }
650
651        if (!needle || needle.length < searchNeedleLength) {
652            hasResults = false;
653            resultsCount = 0;
654            activeResult = -1;
655            $wrap.removeClass(HAS_RESULTS_CLASS).removeClass(NO_RESULTS_CLASS);
656
657            if (searchMode === 'nonblocking' && needle.length > 0) {
658                fakeRequest($wrap); // progress indicator for input to be more responsive
659            }
660
661            return;
662        }
663
664        if (searchMode === 'nonblocking') {
665            handleNonBlockingTracking(searchParams);
666        }
667
668        if (searchMode === 'blocking') {
669            $search.attr('disabled', 'disabled');
670        }
671
672        $wrap.addClass(REQUEST_CLASS);
673        ++searchRequestsCount;
674
675        $.ajax({
676            method: settings['live_search_use_post'] ? 'POST' : 'GET',
677            url: ajaxUrl,
678            dataType: 'json',
679            data: searchParams
680        })
681            .then(function(response) {
682                if (settings['search_request_fe_cache']) {
683                    searchCache[serializedParams] = response;
684                }
685
686                return handleSearchResultsReceive.call(this, $search, response);
687            }.bind(this))
688            .always(function () {
689                if (searchMode === 'blocking') {
690                    $search
691                        .attr('disabled', false)
692                        .focus();
693                }
694
695                --searchRequestsCount;
696
697                if (searchRequestsCount === 0) {
698                    $wrap.removeClass(REQUEST_CLASS);
699                }
700            });
701    }
702
703    /**
704     * Progress indicator for short requests
705     * @param $wrap
706     */
707    function fakeRequest($wrap) {
708        $wrap.addClass(REQUEST_CLASS);
709        ++searchRequestsCount;
710
711        setTimeout(function() {
712            --searchRequestsCount;
713            if (searchRequestsCount === 0) {
714                $wrap.removeClass(REQUEST_CLASS);
715            }
716        }, 500);
717    }
718
719    /**
720     * Article pageview tracking
721     */
722    function trackArticleView() {
723        var $tracking_meta = $('.mkb-article-extra__tracking-data');
724
725        if (!$tracking_meta.length) {
726            return;
727        }
728
729        var $id = $tracking_meta.data('article-id');
730
731        if (!$id) {
732            return;
733        }
734
735        $.ajax({
736            method: 'POST',
737            url: ajaxUrl,
738            dataType: 'json',
739            data: addAjaxNonce({
740                action: 'mkb_article_pageview',
741                id: $id
742            })
743        });
744    }
745
746    /**
747     * Article like
748     * @param e
749     */
750    function handleArticleLike(e) {
751        e.preventDefault();
752
753        var $likeBtn = $(e.currentTarget);
754        var id = $likeBtn.data('article-id');
755        var title = $likeBtn.data('article-title');
756        var $count = $('.mkb-article-extra__stats-likes');
757        var likes = parseInt($count.text(), 10);
758
759        if (!id || $likeBtn.hasClass('mkb-voted') || $likeBtn.hasClass('mkb-disabled')) {
760            return;
761        }
762
763        $likeBtn.addClass('mkb-voted');
764        $('.mkb-article-extra__dislike').addClass('mkb-disabled');
765        $count.text(++likes);
766
767        $.ajax({
768            method: 'POST',
769            url: ajaxUrl,
770            dataType: 'json',
771            data: addAjaxNonce({
772                action: 'mkb_article_like',
773                id: id
774            })
775        }).done(function() {
776            if (settings['track_article_likes']) {
777                trackGoogleAnalytics(
778                    settings['ga_like_category'],
779                    settings['ga_like_action'],
780                    settings['ga_like_label'] === 'article_title' ? title : id,
781                    settings['ga_like_value'] || 0
782                );
783            }
784
785            if (settings['show_like_message']) {
786                $('.fn-rating-likes-block')
787                    .html($('<div class="mkb-article-extra__message">' + i18n['like_message_text'] + '</div>'));
788            }
789
790            if (settings['enable_feedback'] &&
791                (settings['feedback_mode'] === 'like' || settings['feedback_mode'] === 'any')) {
792                addFeedbackForm();
793            }
794        });
795    }
796
797    /**
798     * Article dislike
799     * @param e
800     */
801    function handleArticleDislike(e) {
802        e.preventDefault();
803
804        var $dislikeBtn = $(e.currentTarget);
805        var id = $dislikeBtn.data('article-id');
806        var title = $dislikeBtn.data('article-title');
807        var $count = $('.mkb-article-extra__stats-dislikes');
808        var dislikes = parseInt($count.text(), 10);
809
810        if (!id || $dislikeBtn.hasClass('mkb-voted') || $dislikeBtn.hasClass('mkb-disabled')) {
811            return;
812        }
813
814        $dislikeBtn.addClass('mkb-voted');
815        $('.mkb-article-extra__like').addClass('mkb-disabled');
816        $count.text(++dislikes);
817
818        $.ajax({
819            method: 'POST',
820            url: ajaxUrl,
821            dataType: 'json',
822            data: addAjaxNonce({
823                action: 'mkb_article_dislike',
824                id: id
825            })
826        }).done(function() {
827            if (settings['track_article_dislikes']) {
828                trackGoogleAnalytics(
829                    settings['ga_dislike_category'],
830                    settings['ga_dislike_action'],
831                    settings['ga_dislike_label'] === 'article_title' ? title : id,
832                    settings['ga_dislike_value'] || 0
833                );
834            }
835
836            if (settings['show_dislike_message']) {
837                $('.fn-rating-likes-block')
838                    .html($('<div class="mkb-article-extra__message">' + i18n['dislike_message_text'] + '</div>'));
839            }
840
841            if (settings['enable_feedback'] &&
842                (settings['feedback_mode'] === 'dislike' || settings['feedback_mode'] === 'any')) {
843                addFeedbackForm();
844            }
845        });
846    }
847
848    /**
849     * Renders feedback form if configured
850     */
851    function addFeedbackForm() {
852        $('.fn-article-feedback-container').append($(
853            '<div class="mkb-article-extra__feedback-form mkb-article-extra__feedback-form--no-content fn-feedback-form">' +
854
855                (settings['feedback_email_on'] ?
856                    (
857                        '<div class="mkb-article-extra__feedback-form-email-title">' + i18n['feedback_email_label'] + '</div>' +
858                        '<input type="email" name="mkb_feedback_email" class="mkb-article-extra__feedback-form-email js-mkb-feedback-email">'
859                    ) :
860                '') +
861
862                '<div class="mkb-article-extra__feedback-form-title">' +
863                i18n['feedback_label'] +
864                '</div>' +
865                '<div class="mkb-article-extra__feedback-form-message">' +
866                '<textarea class="mkb-article-extra__feedback-form-message-area js-mkb-feedback-message" rows="5"></textarea>' +
867                (i18n['feedback_info_text'] ?
868                    ('<div class="mkb-article-extra__feedback-info">' + i18n['feedback_info_text'] + '</div>') :
869                    '') +
870                '</div>' +
871                '<div class="mkb-article-extra__feedback-form-submit">' +
872                '<a href="#">' + i18n['feedback_submit_label'] + '</a>' +
873                '</div>' +
874            '</div>'
875        ));
876    }
877
878    /**
879     * Sends article feedback to server
880     * @param e
881     */
882    function handleFeedbackSubmit(e) {
883        var $trackingMeta = $('.mkb-article-extra__tracking-data');
884
885        e.preventDefault();
886
887        if (!$trackingMeta.length) {
888            return;
889        }
890
891        var id = $trackingMeta.data('article-id');
892        var title = $trackingMeta.data('article-title');
893        var $btn = $(e.target);
894        var $content = $('.js-mkb-feedback-message');
895        var $email = $('.js-mkb-feedback-email');
896        var email = Boolean($email.length) && $email.val().trim();
897
898        if (!id || !$content.val()) {
899            return;
900        }
901
902        $btn
903            .text(i18n['feedback_submit_request_label'])
904            .attr('disabled', 'disabled');
905
906        var formData = {
907            action: 'mkb_article_feedback',
908            id: id,
909            content: $content.val()
910        };
911
912        if (email) {
913            formData.email = email;
914        }
915
916        $.ajax({
917            method: 'POST',
918            url: ajaxUrl,
919            dataType: 'json',
920            data: addAjaxNonce(formData)
921        }).done(function() {
922            if (settings['track_article_feedback']) {
923                trackGoogleAnalytics(
924                    settings['ga_feedback_category'],
925                    settings['ga_feedback_action'],
926                    settings['ga_feedback_label'] === 'article_title' ? title : id,
927                    settings['ga_feedback_value'] || 0
928                );
929            }
930
931            $('.fn-article-feedback-container').html(
932                '<div class="mkb-article-extra__feedback-sent-message">' +
933                i18n['feedback_sent_text'] +
934                '</div>'
935            );
936        });
937    }
938
939    /**
940     * Toggle submit available
941     * @param e
942     */
943    function handleFeedbackType(e) {
944        $('.fn-feedback-form').toggleClass('mkb-article-extra__feedback-form--no-content', Boolean($(e.currentTarget).val() < 1));
945    }
946
947    /**
948     * Back to top in articles
949     */
950    function handleArticleBackToTop() {
951        var $container = $('.mkb-container');
952
953        $container.on('click', '.mkb-back-to-top', function (e) {
954            e.preventDefault();
955
956            $doc.animate({
957                scrollTop: settings['back_to_site_top'] ? 0 : $container.offset().top - adminOffset
958            }, 300);
959
960            window.location.hash = '';
961        });
962    }
963
964    /**
965     * Article Table of Contents
966     */
967    function handleArticleTOC() {
968        var $entryContent = $('.mkb-article-text');
969        var $tocList = $('.mkb-anchors-list');
970        var scrollOffset = parseInt(settings['toc_scroll_offset']['size']);
971        var headingsExclude = settings['toc_headings_exclude'].trim().toLowerCase();
972        var $headings;
973        var isScrollSpy = settings['scrollspy_switch'] && settings['toc_in_content_disable'] &&
974            platform === 'desktop' &&
975            window.outerWidth >= parseInt(settings['article_sidebar_sticky_min_width']['size'], 10);
976
977        // dynamic TOC
978        if ($tocList.hasClass('mkb-anchors-list--dynamic')) {
979            var headingsPool = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'];
980
981            if (headingsExclude) {
982                var excluded = headingsExclude.split(',').map(function(heading) {
983                    return heading.trim();
984                }).filter(function(heading) {
985                    return Boolean(heading);
986                });
987
988                headingsPool = headingsPool.filter(function(heading) {
989                    return excluded.indexOf(heading) === -1;
990                });
991            }
992
993            $headings = $entryContent.find(headingsPool.join(', '));
994
995            if (settings['show_back_to_top']) {
996                wrapTOCAnchors($headings);
997            }
998
999        } else { // via shortcodes
1000            $headings = $('.mkb-anchor__title');
1001        }
1002
1003        // scroll
1004        var headingOffsets = Array.prototype.map.call($headings, function(heading) {
1005            return $(heading).offset().top;
1006        });
1007        var $links = $tocList.find('.mkb-anchors-list__item-link');
1008        var linksArr = Array.prototype.map.call($links, function(link) {
1009            return link;
1010        }).filter(function(link) {
1011            return link.dataset.index !== '-1'; // other pages links
1012        });
1013
1014        function recalculateMetrics() {
1015            headingOffsets = Array.prototype.map.call($headings, function(heading) {
1016                return $(heading).offset().top;
1017            });
1018        }
1019
1020        $(window).on('load', recalculateMetrics);
1021        $(window).on('resize', debounce(recalculateMetrics, 1000));
1022
1023        setTimeout(recalculateMetrics, 3000); // in case load fails
1024
1025        var navScrolling = false;
1026
1027        function navigateToChapter(index) {
1028            navScrolling = true;
1029
1030            recalculateMetrics();
1031
1032            if (settings['toc_url_update']) { // TODO: replace with browserHistory instead
1033                window.location.hash = 'ch_' + (index + 1);
1034            }
1035
1036            $doc.animate({
1037                scrollTop: headingOffsets[index] - scrollOffset - 10
1038            }, 300, 'swing', function() {
1039
1040
1041                if (isScrollSpy && linksArr) {
1042                    $(linksArr).removeClass('active');
1043                    $(linksArr[index]).addClass('active');
1044                }
1045
1046                setTimeout(function() {
1047                    navScrolling = false;
1048                }, 300);
1049            });
1050        }
1051
1052        // link click handlers
1053        $tocList.on('click', '.mkb-anchors-list__item-link', function (e) {
1054            var $item = $(e.currentTarget);
1055
1056            if ($item.attr('href') !== '#') {
1057                return; // normal link, just navigate
1058            }
1059
1060            var index = parseInt($item.data('index'), 10);
1061
1062            e.preventDefault();
1063
1064            if (isScrollSpy) {
1065                $links.removeClass('active');
1066                $item.addClass('active');
1067            }
1068
1069            navigateToChapter(index);
1070        });
1071
1072        var chapterHash = window.location.hash && window.location.hash.replace(/^#/, '');
1073
1074        if (chapterHash && chapterHash.indexOf('ch_') !== -1) {
1075            var chapterIndex = parseInt(chapterHash.replace(/^ch_/, '')) - 1;
1076
1077            if (chapterIndex && chapterIndex > 0) {
1078                setTimeout(function() {
1079                    navigateToChapter(chapterIndex);
1080                }, 300);
1081            } else if (isScrollSpy && chapterIndex === 0) {
1082                $(linksArr[0]).addClass('active');
1083            }
1084        } else if (isScrollSpy) {
1085            $(linksArr[0]).addClass('active');
1086        }
1087
1088        if (!isScrollSpy) {
1089            return;
1090        }
1091
1092        // ScrollSpy
1093        var win = window;
1094        var doc = document.documentElement;
1095
1096        $(win).on('scroll', throttle(150, function() {
1097            if (navScrolling) { return; }
1098
1099            var top = win.pageYOffset || doc.scrollTop;
1100
1101            linksArr.forEach(function(item, index) {
1102                var curr = index === 0 ? 0 : headingOffsets[index] - scrollOffset - 1;
1103                var next = (index === headingOffsets.length - 1 ? 9999999 : headingOffsets[index + 1]) - scrollOffset - 1;
1104
1105                $(item).toggleClass('active', top >= curr && top < next);
1106            });
1107        }));
1108    }
1109
1110    /**
1111     * Wraps headings n back to top containers when necessary
1112     * @param $headings
1113     */
1114    function wrapTOCAnchors($headings) {
1115        $headings.each(function(index, el) {
1116            var $heading = $(el);
1117
1118            $heading.wrap('<div class="mkb-anchor mkb-clearfix mkb-back-to-top-' +
1119            settings['back_to_top_position'] +
1120            '"></div>');
1121
1122            $heading.addClass('mkb-anchor__title');
1123
1124            $heading.parent().append('<a href="#" class="mkb-back-to-top" title="' +
1125            settings['back_to_top_text'] + '">' +
1126            settings['back_to_top_text'] +
1127            (
1128                settings['show_back_to_top_icon'] ?
1129                '<i class="mkb-back-to-top-icon fa ' + settings['back_to_top_icon'] + '"></i>' :
1130                    ''
1131            ) +
1132            '</a>');
1133        });
1134    }
1135
1136    /**
1137     * Articles fancy box
1138     */
1139    function initArticlesFancyBox() {
1140        if (!$.fn.fancybox) {
1141            return;
1142        }
1143
1144        // with captions
1145        $('figure[id^="attachment"] a').each(function (index, item) {
1146            var $item = $(item);
1147            var text = $item.parent().find('.wp-caption-text').text();
1148
1149            $item.fancybox({
1150                titlePosition: 'over',
1151                title: text
1152            });
1153        });
1154
1155        // no captions
1156        $('.mkb-single-content img').each(function(i, img) {
1157            var $img = $(img);
1158            var $link = $img.parent();
1159
1160            if ($img.parents('figure.wp-caption').length || !$link.attr('href')) {
1161                return;
1162            }
1163
1164            $link.fancybox({
1165                titlePosition: 'none',
1166                title: ''
1167            });
1168        });
1169    }
1170
1171    /**
1172     * Search clear
1173     * @param e
1174     */
1175    function handleSearchClear(e) {
1176        e.preventDefault();
1177
1178        $(e.currentTarget)
1179            .parents('.kb-search__input-wrap')
1180            .find('.kb-search__input')
1181            .val('')
1182            .trigger('input')
1183            .focus();
1184    }
1185
1186    function initSearchInputs() {
1187        $kbSearch.each(function (index, el) {
1188            var $search = $(el);
1189
1190            var searchHandler = searchMode === 'blocking' ?
1191                debounce(onSearchType.bind(this, $search), parseInt(settings['search_delay'], 10) || 1000, false) :
1192                throttle(parseInt(settings['search_delay'], 10) || 300, true, onSearchType.bind(this, $search), true);
1193
1194            $search.on('input', searchHandler);
1195
1196            if (settings['live_search_prevent_submit']) {
1197                $search.parents('form.kb-search__form').on('submit', function(e) {
1198                    e.preventDefault();
1199                });
1200            }
1201        });
1202    }
1203
1204    /**
1205     * Detects if live search disabled for current platform
1206     * @returns {boolean}
1207     */
1208    function isSearchDisabled() {
1209        return Boolean(settings['live_search_disable_' + platform]);
1210    }
1211
1212    /**
1213     * Sticky sidebar
1214     */
1215    function setupArticleStickySidebar() {
1216
1217        if (settings['single_template'] === 'theme' ||
1218            !settings['article_sidebar_sticky'] ||
1219            !info.isSingle ||
1220            platform !== 'desktop' ||
1221            window.outerWidth < parseInt(settings['article_sidebar_sticky_min_width']['size'], 10)) {
1222
1223            // sticky sidebar not enabled or not allowed
1224            return;
1225        }
1226
1227        var sticky = false;
1228        var sidebarPosition = settings['article_sidebar'];
1229        var atBottom = false;
1230        var $sidebar = $('.mkb-sidebar');
1231        var sidebarHeight = $sidebar.outerHeight();
1232        var $root = $sidebar.parents('.mkb-root');
1233        var rootHeight = $root.outerHeight();
1234        var rootHeightInner = $root.height();
1235        var rootPad = rootHeight - rootHeightInner;
1236        var rootTop = $root.offset().top;
1237        var triggerPos = rootTop - rootPad / 2;
1238        var winHeight = window.innerHeight;
1239        var bottomOffset = winHeight > sidebarHeight + rootPad ? winHeight - sidebarHeight - rootPad : 0;
1240        var width = $sidebar.outerWidth();
1241        var win = window;
1242        var doc = document.documentElement;
1243
1244        function recalculateMetrics() {
1245            rootHeight = $root.outerHeight();
1246            rootHeightInner = $root.height();
1247            rootPad = rootHeight - rootHeightInner;
1248            rootTop = $root.offset().top;
1249            triggerPos = rootTop - rootPad / 2;
1250            winHeight = window.innerHeight;
1251            sidebarHeight = $sidebar.outerHeight();
1252            bottomOffset = winHeight > sidebarHeight + rootPad ? winHeight - sidebarHeight - rootPad : 0;
1253        }
1254
1255        $(win).on('load', recalculateMetrics);
1256        $(win).on('resize', debounce(recalculateMetrics, 1000));
1257
1258        setInterval(recalculateMetrics, 500); // sometimes content height changes dynamically
1259
1260        function updateSidebarLeftPosition() {
1261            var left = $root.get(0).getBoundingClientRect().left;
1262            $sidebar.css('left', left + parseInt($root.css('padding-left')) + 'px');
1263        }
1264
1265        function handleScroll() {
1266            var top = win.pageYOffset || doc.scrollTop;
1267            var bottom = top + winHeight - bottomOffset;
1268
1269            if (bottom > rootHeight + rootTop && !atBottom || bottom <= rootHeight + rootTop && atBottom) {
1270                atBottom = !atBottom;
1271                $sidebar.toggleClass('mkb-fixed-bottom', atBottom);
1272
1273                if (sidebarPosition === 'left') {
1274                    if (atBottom) {
1275                        $sidebar.css('left', '');
1276                    } else {
1277                        updateSidebarLeftPosition();
1278                    }
1279                }
1280            }
1281
1282            if (sticky && top >= triggerPos || !sticky && top < triggerPos) {
1283                return;
1284            }
1285
1286            sticky = !sticky;
1287            $sidebar.toggleClass('mkb-fixed', sticky);
1288            $sidebar.css('max-width', sticky ? width + 'px' : 'none');
1289
1290            if (sidebarPosition === 'left') {
1291                if (sticky) {
1292                    if (!atBottom) {
1293                        updateSidebarLeftPosition();
1294                    }
1295
1296                    $(window).on('resize', updateSidebarLeftPosition);
1297                } else {
1298                    $sidebar.css('left', 0);
1299                    $(window).off('resize', updateSidebarLeftPosition);
1300                }
1301            }
1302        }
1303
1304        $(win).on('scroll', handleScroll);
1305    }
1306
1307    /**
1308     * FAQ
1309     */
1310    function setupFaq() {
1311        var $faqContainer = $('.fn-kb-faq-container');
1312
1313        if (!$faqContainer.length) {
1314            return;
1315        }
1316
1317        function getHashFaqId() {
1318            var currentHash = window.location.hash.replace('#', '');
1319            return /^qa_/.test(currentHash) && currentHash.replace('qa_', '') || false;
1320        }
1321
1322        function setHashFaq(id) {
1323            history.replaceState(null, '',
1324                window.location.origin + window.location.pathname + window.location.search +
1325                '#qa_' + id);
1326        }
1327
1328        function clearHash() {
1329            history.replaceState(null, '',
1330                window.location.origin + window.location.pathname + window.location.search);
1331        }
1332
1333        $faqContainer.each(function(index, item) {
1334            var $container = $(item);
1335            var FAQ_HIDDEN_CLASS = 'mkb-faq-item-hidden';
1336            var FAQ_SECTION_HIDDEN_CLASS = 'mkb-faq-section-hidden';
1337            var $filterForm = $container.find('.fn-kb-faq-filter');
1338            var $filter = $filterForm.find('.fn-kb-faq-filter-input');
1339            var $sections = $container.find('.fn-kb-faq-section');
1340            var $noResults = $container.find('.fn-kb-faq-no-results');
1341
1342            // FAQ sections
1343            var sections = [].map.call($sections, function(section) {
1344                var $section = $(section);
1345                var $count = $section.find('.fn-kb-faq-section-count');
1346                var $items = [].map.call($section.find('.fn-kb-faq-item'), function(item) {
1347                    var $item = $(item);
1348
1349                    return {
1350                        $el: $item,
1351                        question: $item.find('.fn-kb-faq-question').text().trim().toLowerCase(),
1352                        answer: $item.find('.fn-kb-faq-answer').text().trim().toLowerCase(),
1353                        isVisible: true
1354                    };
1355                });
1356
1357                return {
1358                    $el: $section,
1359                    $countEl: $count.length ? $count : null,
1360                    items: $items,
1361                    visible: [].map.call($items, function(item) { return $(item) }),
1362                    hidden: []
1363                };
1364            });
1365
1366            /**
1367             * FAQ Filter
1368             */
1369            var currentFilter;
1370
1371            function updateFilter(visCheck) {
1372                visCheck = visCheck || checkVisibility;
1373
1374                var totalVisible = 0;
1375
1376                // check visibility
1377                sections.forEach(function(section) {
1378                    section.visible = [];
1379                    section.hidden = [];
1380                    section.items.forEach(function(item) {
1381                        section[(item.isVisible = visCheck(item)) ? 'visible' : 'hidden'].push(item);
1382                    });
1383                    totalVisible += section.visible.length;
1384                });
1385
1386                $noResults.toggleClass('mkb-hidden', totalVisible > 0);
1387
1388                sections.forEach(function(section) {
1389                    section.visible.forEach(function(item) {
1390                        item.$el.removeClass(FAQ_HIDDEN_CLASS);
1391                    });
1392                    section.hidden.forEach(function(item) {
1393                        item.$el.addClass(FAQ_HIDDEN_CLASS);
1394                    });
1395                    section.$countEl && section.$countEl.html(section.visible.length +
1396                    ' ' + (section.visible.length === 1 ? i18n['question'] : i18n['questions'])
1397                    );
1398                    section.$el.toggleClass(FAQ_SECTION_HIDDEN_CLASS, !section.visible.length);
1399                    section.visible = [];
1400                    section.hidden = [];
1401                });
1402
1403                if (settings['faq_filter_open_single'] && totalVisible === 1) {
1404                    var $onlyItem = $container.find('.fn-kb-faq-item:not(.' + FAQ_HIDDEN_CLASS + ')');
1405
1406                    if ($onlyItem.length && !$onlyItem.hasClass('kb-faq__questions-list-item--open')) {
1407                        toggleAnswer($onlyItem);
1408                    }
1409                }
1410            }
1411
1412            var resetFilter = updateFilter.bind(this, function() { return true; });
1413
1414            function checkVisibility(item) {
1415                return item.question.indexOf(currentFilter) !== -1 || item.answer.indexOf(currentFilter) !== -1;
1416            }
1417
1418            function handleFilterChange(e) {
1419                var needle = (e.currentTarget.value || '').trim();
1420
1421                if (needle.length < 3) {
1422                    $filterForm.addClass('kb-faq__filter--empty');
1423                    currentFilter = '';
1424                    resetFilter();
1425
1426                    $filter.focus();
1427                    return
1428                }
1429
1430                $filterForm.removeClass('kb-faq__filter--empty');
1431                currentFilter = needle.toLowerCase();
1432                updateFilter();
1433            }
1434
1435            function handleFilterClear(e) {
1436                e.preventDefault();
1437
1438                $filter.val('').trigger('input');
1439            }
1440
1441            if ($filterForm.length) {
1442                $container.on('input', '.fn-kb-faq-filter-input', handleFilterChange);
1443                $container.on('click', '.fn-kb-faq-filter-clear', handleFilterClear);
1444            }
1445
1446            var OPEN_SPEED = settings['faq_slow_animation'] ? 400 : 100;
1447
1448            function getMaxHeight(el) {
1449                return Array.prototype.reduce.call(el.childNodes, function(store, current) {
1450                    return store + (current.offsetHeight || 0);
1451                }, 0);
1452            }
1453
1454            /**
1455             * FAQ Toggle
1456             */
1457            function toggleAnswer($item, customToggleSpeed) {
1458                customToggleSpeed = customToggleSpeed || OPEN_SPEED;
1459
1460                var $answer = $item.find('.fn-kb-faq-answer');
1461                var $link = $item.find('.fn-kb-faq-link');
1462                var answerEl = $answer.get(0);
1463                var maxHeight = getMaxHeight(answerEl);
1464
1465                if ($item.hasClass('kb-faq__questions-list-item--open')) {
1466                    $answer.css('max-height', maxHeight);
1467                    $answer.animate({maxHeight: 0}, customToggleSpeed, 'swing', function() {
1468                        $item.removeClass('kb-faq__questions-list-item--open');
1469                    });
1470
1471                    if (settings['faq_url_update']) {
1472                        var hashFaqId = getHashFaqId();
1473
1474                        if (hashFaqId && hashFaqId == $link.data('id')) {
1475                            clearHash();
1476                        }
1477                    }
1478                } else {
1479                    if (settings['faq_toggle_mode']) {
1480                        sections.forEach(function(section) {
1481                            section.items.forEach(function(item) {
1482                                closeAnswer(item.$el, customToggleSpeed);
1483                            });
1484                        });
1485                    }
1486
1487                    $answer.animate({maxHeight: maxHeight}, customToggleSpeed, 'swing', function() {
1488                        $answer.css('max-height', 'none');
1489                        $item.addClass('kb-faq__questions-list-item--open');
1490                    });
1491
1492                    if (settings['faq_url_update']) {
1493                        setHashFaq($link.data('id'));
1494                    }
1495                }
1496            }
1497
1498            function openAnswer($item) {
1499                var $answer = $item.find('.fn-kb-faq-answer');
1500                var answerEl = $answer.get(0);
1501                var maxHeight = getMaxHeight(answerEl);
1502
1503                if (!$item.hasClass('kb-faq__questions-list-item--open')) {
1504                    $answer.animate({maxHeight: maxHeight}, OPEN_SPEED, 'swing', function() {
1505                        $answer.css('max-height', 'none');
1506                        $item.addClass('kb-faq__questions-list-item--open');
1507                    });
1508                }
1509            }
1510
1511            function closeAnswer($item, customCloseSpeed) {
1512                customCloseSpeed = customCloseSpeed || OPEN_SPEED;
1513
1514                var $answer = $item.find('.fn-kb-faq-answer');
1515                var answerEl = $answer.get(0);
1516                var maxHeight = getMaxHeight(answerEl);
1517
1518                if ($item.hasClass('kb-faq__questions-list-item--open')) {
1519                    $answer.css('max-height', maxHeight);
1520                    $answer.animate({maxHeight: 0}, customCloseSpeed, 'swing', function() {
1521                        $item.removeClass('kb-faq__questions-list-item--open');
1522                    });
1523                }
1524            }
1525
1526            var trackedQuestionViews = {};
1527
1528            function handleQuestionClick (e) {
1529                e.preventDefault();
1530
1531                var $link = $(e.currentTarget);
1532                var id = Number($link.data('id'));
1533                var $item = $link.parent();
1534                var isOpen = $item.hasClass('kb-faq__questions-list-item--open');
1535
1536                toggleAnswer($item);
1537
1538                if (!isOpen && id && !isNaN(id) && !trackedQuestionViews[id]) {
1539                    trackQuestionView(id);
1540                    trackedQuestionViews[id] = true;
1541                }
1542            }
1543
1544            function trackQuestionView(id) {
1545                $.ajax({
1546                    method: 'POST',
1547                    url: ajaxUrl,
1548                    dataType: 'json',
1549                    data: addAjaxNonce({
1550                        action: 'mkb_faq_view',
1551                        id: id
1552                    })
1553                });
1554            }
1555
1556            function handleToggleAllClick (e) {
1557                e.preventDefault();
1558
1559                var $link = $(e.currentTarget);
1560                var isOpen = $link.hasClass('kb-faq__toggle-all-link--open');
1561
1562                sections.forEach(function(section) {
1563                    section.items.forEach(function(item) {
1564                        isOpen ? closeAnswer(item.$el) : openAnswer(item.$el);
1565                    });
1566                });
1567
1568                $link.toggleClass('kb-faq__toggle-all-link--open');
1569            }
1570
1571            $container.on('click', '.fn-kb-faq-link', handleQuestionClick);
1572            $container.on('click', '.fn-kb-faq-toggle-all', handleToggleAllClick);
1573
1574            // global API
1575            $body.on('minerva_faq_toggle_answer_' + index, function(options) {
1576                var id = options.id;
1577
1578                var $link = $container.find('.fn-kb-faq-link[data-id="' + id + '"]');
1579
1580                if (!$link.length) {
1581                    return;
1582                }
1583
1584                var $item = $link.parent();
1585
1586                if (!$item.hasClass('kb-faq__questions-list-item--open')) {
1587                    toggleAnswer($item, 100);
1588                }
1589            })
1590        });
1591
1592        if (settings['faq_url_update']) {
1593            // navigate to faq from hash
1594            var hashFaqId = getHashFaqId();
1595            var $faqLink = hashFaqId && $('.fn-kb-faq-link[data-id="' + hashFaqId + '"]');
1596            var scrollOffset = parseInt(settings['faq_scroll_offset']['size']);
1597
1598            if ($faqLink && $faqLink.length) {
1599                $faqLink.trigger('click');
1600
1601                setTimeout(function() {
1602                    $doc.animate({
1603                        scrollTop: $faqLink.offset().top - scrollOffset
1604                    }, 300);
1605                }, 800);
1606            }
1607        }
1608    }
1609
1610    /**
1611     * Content Tree
1612     */
1613    function setupContentTreeWidgets() {
1614        var $contentTree = $('.mkb-widget-content-tree__list');
1615        var openActiveBranch = settings['content_tree_widget_open_active_branch'];
1616
1617        function setListMaxHeight(index, list) {
1618            var $list = $(list);
1619            $list.animate({'max-height': list.scrollHeight}, 200, function() {
1620                $list.css('max-height', 'none');
1621            });
1622        }
1623
1624        $contentTree.each(function(index, tree) {
1625            var $tree  = $(tree);
1626            var $topics = $tree.find('.mkb-widget-content-tree__topic');
1627
1628            $tree.on('click', '.mkb-widget-content-tree__topic-name', function(e) {
1629                var topicName = e.currentTarget;
1630                var $topic = $(topicName).parent();
1631                var topic = $topic.get(0);
1632
1633                if ($topic.hasClass('topic-open')) {
1634                    $topic.removeClass('topic-open');
1635                    $topic.find('>ul').css('max-height', '0');
1636                } else {
1637                    var activeBranch = [topic.dataset.id];
1638                    var $parents = $topic.parents('.mkb-widget-content-tree__topic');
1639
1640                    $parents.each(function(index, parent) {
1641                        activeBranch.push(parent.dataset.id);
1642                    });
1643
1644                    // hide topics that are not in current branch
1645                    $topics.each(function(index, item) {
1646                        if (activeBranch.indexOf(item.dataset.id) !== -1) {
1647                            return;
1648                        }
1649
1650                        var $item = $(item);
1651
1652                        $item.removeClass('topic-open');
1653                        $item.find('>ul').css('max-height', '0');
1654                    });
1655
1656                    $topic.find('>ul').each(setListMaxHeight);
1657                    $topic.addClass('topic-open');
1658
1659                    $parents.each(function(index, item) {
1660                        var $parentTopic = $(item);
1661
1662                        if ($parentTopic.hasClass('topic-open')) {
1663                            return;
1664                        }
1665
1666                        $parentTopic.find('>ul').each(setListMaxHeight);
1667                        $parentTopic.addClass('topic-open');
1668                    });
1669                }
1670
1671                e.preventDefault();
1672                e.stopImmediatePropagation();
1673            });
1674
1675            if (openActiveBranch) {
1676                setTimeout(function() {
1677                    $tree.find('.mkb-widget-content-tree__article--active')
1678                        .closest('.mkb-widget-content-tree__topic')
1679                        .find('>.mkb-widget-content-tree__topic-name')
1680                        .trigger('click');
1681                }, 300);
1682            }
1683        });
1684    }
1685
1686    /**
1687     * Client submission form
1688     */
1689    function setupSubmissionForm() {
1690        var $submisionContainers = $('.js-mkb-client-submission');
1691
1692        $submisionContainers.each(function(index, container) {
1693            var $container = $(container);
1694            var $form = $container.find('.js-mkb-client-submission-form');
1695
1696            var $title = $container.find('.js-mkb-submission-title');
1697            var $topic = $container.find('.js-mkb-submission-topic');
1698            var $content = $container.find('#mkb-client-editor');
1699            var $antispamAnswer = $container.find('.js-mkb-real-human-answer');
1700            var $messagesContainer = $container.find('.js-mkb-form-messages');
1701
1702            var toolbarOptions = [
1703                [{ 'header': [1, 2, 3, 4, 5, 6, false] }],
1704                ['bold', 'italic', 'underline', 'strike'],
1705                [{ 'list': 'ordered'}, { 'list': 'bullet' }],
1706                ['link'],
1707                ['blockquote', 'code-block'],
1708                ['clean']
1709            ];
1710
1711            var quillEditor = new Quill('#mkb-client-editor', {
1712                modules: {
1713                    toolbar: toolbarOptions,
1714                    history: {
1715                        delay: 2000,
1716                        maxStack: 500,
1717                        userOnly: true
1718                    }
1719                },
1720                theme: settings.submit_content_editor_skin
1721            });
1722
1723            function getData() {
1724                return {
1725                    action: 'mkb_save_client_submission',
1726                    title: $title.val().trim(),
1727                    content: $content.find('.ql-editor').get(0).innerHTML.trim(),
1728                    topic: $topic.length ? $topic.val().trim() : '',
1729                    antispam: $antispamAnswer.length ? $antispamAnswer.val().trim() : ''
1730                }
1731            }
1732
1733            function validateData(data) {
1734                var status = 0;
1735                var errors = [];
1736
1737                if (!data.title) {
1738                    status = 1;
1739                    errors.push(i18n.submission_empty_title);
1740                }
1741
1742                if (!data.content
1743                        .replace('<p>', '')
1744                        .replace('</p>', '')
1745                        .replace('<br>', '')) {
1746
1747                    status = 1;
1748                    errors.push(i18n.submission_empty_content);
1749                }
1750
1751                if (!data.antispam && $antispamAnswer.length) {
1752                    status = 1;
1753                    errors.push(settings.antispam_failed_message);
1754                }
1755
1756                return {
1757                    status: status,
1758                    errors: errors
1759                }
1760            }
1761
1762            function renderMessages(messages) {
1763                $messagesContainer.html(messages.reduce(function(html, err) {
1764                    return html + '<div class="mkb-form-message">' + err + '</div>'
1765                }), '');
1766            }
1767
1768            function showMessages(messageClass, noScroll) {
1769                noScroll = noScroll || false;
1770
1771                $messagesContainer.addClass(messageClass).removeClass('mkb-hidden');
1772
1773                if (!noScroll) {
1774                    $doc.animate({
1775                        scrollTop: $messagesContainer.offset().top - adminOffset - parseInt(settings['global_scroll_offset']['size'], 10)
1776                    }, 100);
1777                }
1778            }
1779
1780            function showErrorMessages(noScroll) {
1781                showMessages('mkb-form-error', noScroll);
1782            }
1783
1784            function showSuccessMessages(noScroll) {
1785                showMessages('mkb-form-success', noScroll);
1786            }
1787
1788            $container.on('click', '.js-mkb-client-submission-send', function(e) {
1789                var requestData = getData();
1790                var $btn = $(e.currentTarget);
1791
1792                if ($btn.hasClass('mkb-disabled')) {
1793                    return;
1794                }
1795
1796                var validationResult = validateData(requestData);
1797
1798                if (validationResult.status !== 0) {
1799                    renderMessages(validationResult.errors);
1800                    showErrorMessages();
1801
1802                    $doc.animate({
1803                        scrollTop: $messagesContainer.offset().top - adminOffset - parseInt(settings['global_scroll_offset']['size'], 10)
1804                    }, 100);
1805
1806                    return;
1807                }
1808
1809                $btn.addClass('mkb-disabled');
1810
1811                $.ajax({
1812                    method: 'POST',
1813                    url: ajaxUrl,
1814                    dataType: 'json',
1815                    data: addAjaxNonce(requestData)
1816                }).done(function(response) {
1817
1818                    $btn.removeClass('mkb-disabled');
1819
1820                    if (response.status == 1) {
1821                        renderMessages([response.error]);
1822                        showErrorMessages();
1823                    } else {
1824                        renderMessages([settings.submit_success_message]);
1825                        showSuccessMessages(true);
1826
1827                        $form.html('');
1828
1829                        $doc.animate({
1830                            scrollTop: $container.offset().top - adminOffset - parseInt(settings['global_scroll_offset']['size'], 10)
1831                        }, 100);
1832                    }
1833                });
1834            });
1835        });
1836    }
1837
1838    /**
1839     * KB floating helper
1840     */
1841    function setupHelper() {
1842        var $helper = $('.js-mkb-floating-helper');
1843
1844        if (!$helper.length) {
1845            return;
1846        }
1847
1848        var $searchInput = $helper.find('.kb-search__input');
1849
1850        $helper.on('click', '.js-mkb-floating-helper-btn', function() {
1851            $helper.addClass('mkb-floating-helper-wrap--open');
1852            $searchInput.get(0).focus();
1853        });
1854
1855        $helper.on('click', '.js-mkb-floating-helper-close', function() {
1856            $helper.removeClass('mkb-floating-helper-wrap--open');
1857        });
1858
1859        if (settings.fh_display_mode === 'auto') {
1860            setTimeout(function() {
1861                $helper.addClass('mkb-floating-helper-wrap--ready');
1862            }, settings.fh_show_delay);
1863        } else if (settings.fh_display_mode === 'js_click') {
1864            $('body').on('click', '.js-mkb-helper-open', function() {
1865                $helper.addClass('mkb-floating-helper-wrap--ready');
1866            });
1867        }
1868    }
1869
1870    /**
1871     * Attachments
1872     */
1873    function setupAttachments() {
1874        // download tracking
1875        $body.on('click', '.js-mkb-attachment-link', function(e) {
1876            var link = e.currentTarget;
1877            var id = link.dataset.id;
1878
1879            if (!id) {
1880                return;
1881            }
1882
1883            $.ajax({
1884                method: 'POST',
1885                url: ajaxUrl,
1886                dataType: 'json',
1887                data: addAjaxNonce({
1888                    action: 'mkb_track_attachment_download',
1889                    id: id
1890                })
1891            })
1892        });
1893    }
1894
1895    /**
1896     * GLOSSARY TOOLTIPS
1897     */
1898
1899    /**
1900     * Glossary helper
1901     * @param char
1902     * @returns {boolean}
1903     */
1904    function isDelimiter(char) {
1905        return [',', '.', ':', ';', '"', '\''].includes(char) || /\s/.test(char);
1906    }
1907
1908    /**
1909     * Glossary helper
1910     * @param matchedText
1911     * @param wholeText
1912     * @param offset
1913     * @returns {boolean}
1914     */
1915    function checkMatchDelimiters(matchedText, wholeText, offset) {
1916        var validBefore = true;
1917        var validAfter = true;
1918
1919        // check before char
1920        if (offset > 0) {
1921            validBefore = isDelimiter(wholeText[offset - 1]);
1922        }
1923
1924        // check after char
1925        if (offset < wholeText.length - matchedText.length) {
1926            validAfter = isDelimiter(wholeText[offset + matchedText.length]);
1927        }
1928
1929        return validBefore && validAfter;
1930    }
1931
1932    /**
1933     * Match text helper for glossary
1934     * @param node
1935     * @param regex
1936     * @param callback
1937     * @param excludeElements
1938     * @returns {*}
1939     */
1940    var glossaryHighlightLimit = parseInt(settings.glossary_highlight_limit, 10);
1941    var glossaryHighlightCache = {};
1942    var glossaryExcludeElements = [
1943        'script',
1944        'style',
1945        'iframe',
1946        'canvas',
1947        'h1',
1948        'h2',
1949        'h3',
1950        'h4',
1951        'h5',
1952        'h6'
1953    ];
1954
1955    glossaryHighlightLimit = !isNaN(glossaryHighlightLimit) && glossaryHighlightLimit > 0
1956        ? glossaryHighlightLimit
1957        : 0;
1958
1959    /**
1960     *
1961     * @param node
1962     * @param regex {RegExp}
1963     * @param termId {Number}
1964     * @param createWrappedText {Function}
1965     * @returns {*}
1966     */
1967    function matchGlossaryText(node, regex, termId, createWrappedText) {
1968        if (glossaryHighlightLimit && glossaryHighlightCache[termId] >= glossaryHighlightLimit) {
1969            return;
1970        }
1971
1972        var child = node.firstChild;
1973
1974        while (child) {
1975            switch (child.nodeType) {
1976                case 1: // An Element node like <p> or <div>.
1977                    if (glossaryExcludeElements.indexOf(child.tagName.toLowerCase()) > -1) {
1978                        break;
1979                    }
1980
1981                    // note, recursion here, actual replace only in text nodes
1982                    matchGlossaryText(child, regex, termId, createWrappedText);
1983                    break;
1984
1985                case 3: // The actual Text inside an Element.
1986                    var bk = 0; // position back offset
1987
1988                    child.data.replace(regex, function glossaryTextNodeReplacerFn(matchedText) {
1989                        // NOTE: we need this additional check here, because replace may match multiple substrings
1990                        // in a single text node
1991                        if (glossaryHighlightLimit && glossaryHighlightCache[termId] >= glossaryHighlightLimit) {
1992                            return;
1993                        }
1994
1995                        var args = [].slice.call(arguments);
1996                        var offset = args[args.length - 2];
1997                        var wholeText = args[args.length - 1];
1998
1999                        if (!checkMatchDelimiters(matchedText, wholeText, offset)) {
2000                            return; // TODO: why different return type? maybe matchedText
2001                        }
2002
2003                        var newTextNode = child.splitText(offset + bk), tag;
2004
2005                        bk -= child.data.length + matchedText.length;
2006
2007                        newTextNode.data = newTextNode.data.substr(matchedText.length);
2008
2009                        tag = createWrappedText.apply(window, [child].concat(args));
2010
2011                        child.parentNode.insertBefore(tag, newTextNode);
2012                        child = newTextNode;
2013                    });
2014                    regex.lastIndex = 0;
2015                    break;
2016            }
2017
2018            child = child.nextSibling;
2019        }
2020
2021        return node;
2022    }
2023
2024    /**
2025     * Glossary tooltips
2026     */
2027    function setupGlossaryTooltips() {
2028        if (platform !== 'desktop' && settings['glossary_mobile_mode'] === 'none') {
2029            return;
2030        }
2031
2032        if (info.postId && settings['glossary_highlight_exclude_ids'].includes(String(info.postId))) {
2033            return;
2034        }
2035
2036        var selectors = ['.js-mkb-glossary-content-wrap'];
2037
2038        if (settings['glossary_highlight_post_types'].includes('product')) {
2039            selectors.push('.woocommerce-product-details__short-description');
2040        }
2041
2042        if (settings['glossary_highlight_post_types'].includes('page') && info.isPage) {
2043            selectors.push('.elementor');
2044        }
2045
2046        var selector = selectors.join(', ');
2047        var $container = $(selector);
2048
2049        if (!$container.length) {
2050            return;
2051        }
2052
2053        var glossaryList = window.MinervaKB.glossary || [];
2054
2055        if (!glossaryList.length) {
2056            return;
2057        }
2058
2059        var glossaryById = glossaryList.reduce(function(glossary, current) {
2060            glossary[current.id] = current;
2061            return glossary;
2062        }, {});
2063
2064        glossaryList.forEach(function(glossaryItem) {
2065            var synonyms = (glossaryItem.synonyms || '')
2066                .split(',')
2067                .map(function(s) {
2068                    return s.trim().toLowerCase();
2069                })
2070                .filter(Boolean);
2071            var synonymPart = synonyms.length ? synonyms.join('|') + '|' : '';
2072            var glossaryTermRE = new RegExp(synonymPart + glossaryItem.title.toLowerCase(), 'gi');
2073
2074            glossaryHighlightCache[glossaryItem.id] = 0;
2075
2076            $container.each(function(index, glossarySection) {
2077                matchGlossaryText(glossarySection, glossaryTermRE, glossaryItem.id, function(node, match, offset) {
2078                    var wrappedEl = document.createElement(platform === 'desktop' ? 'span': 'a');
2079
2080                    wrappedEl.className = "mkb-glossary-term js-mkb-glossary-term";
2081                    wrappedEl.textContent = match;
2082                    wrappedEl.dataset.id = glossaryItem.id;
2083
2084                    if (platform !== 'desktop') {
2085                        wrappedEl.setAttribute('href', glossaryItem.permalink);
2086
2087                        if (settings['glossary_mobile_mode'] === 'link_new') {
2088                            wrappedEl.setAttribute('target', '_blank');
2089                        }
2090                    }
2091
2092                    if (glossaryHighlightLimit) {
2093                        ++glossaryHighlightCache[glossaryItem.id];
2094                    }
2095
2096                    return wrappedEl;
2097                });
2098            });
2099        });
2100
2101        function showTooltip(e) {
2102            e.preventDefault();
2103
2104            var el = e.currentTarget;
2105            var isDesktop = platform === 'desktop';
2106            var termRect = el.getBoundingClientRect();
2107            var leftOffset = termRect.left + el.offsetWidth / 2;
2108            var rightOffset = document.documentElement.clientWidth - termRect.left - el.offsetWidth / 2;
2109            var tooltipHalfWidth = 160; // half of tooltip, hardcoded
2110            var isLeftCropped = isDesktop && leftOffset < tooltipHalfWidth;
2111            var isRightCropped = isDesktop && rightOffset < tooltipHalfWidth;
2112            var id = el.dataset.id;
2113            var loadingHTML = '<div class="mkb-glossary-loader"><i class="fa ' + settings['glossary_loader_icon'] + ' fa-spin fa-fw"></i></div>';
2114            var content = glossaryById[id].html;
2115            var $tooltipInner = $(
2116                '<div class="js-mkb-glossary-tooltip mkb-glossary-tooltip' +
2117                    (isLeftCropped ? ' mkb-glossary-tooltip--left' : '') +
2118                    (isRightCropped ? ' mkb-glossary-tooltip--right' : '') +
2119                '">' +
2120                    (platform !== 'desktop' ?
2121                        '<a href="#" class="js-mkb-glossary-tooltip-close mkb-glossary-tooltip-close"><i class="fa fa-times"></i></a>' :
2122                    '') +
2123                    '<div class="js-mkb-glossary-tooltip-inner mkb-glossary-tooltip-inner">' +
2124                        (content || loadingHTML) +
2125                    '</div>' +
2126                '</div>'
2127            );
2128
2129            $(el).append($tooltipInner);
2130
2131            if (!content) {
2132                $.ajax({
2133                    method: 'GET',
2134                    url: ajaxUrl,
2135                    dataType: 'json',
2136                    data: addAjaxNonce({
2137                        action: 'mkb_get_glossary_term_content',
2138                        id: id
2139                    })
2140                }).done(function(response) {
2141                    if (response && response.html) {
2142                        glossaryById[id].html = response.html;
2143
2144                        $tooltipInner.find('.js-mkb-glossary-tooltip-inner').html(glossaryById[id].html);
2145                    }
2146                });
2147            }
2148
2149            if (platform !== 'desktop') {
2150                $body.addClass('mkb-scroll-lock');
2151            }
2152
2153            setTimeout(function() {
2154                $tooltipInner.addClass('state--animated');
2155            }, 10);
2156        }
2157
2158        function removeTooltip(e) {
2159            var el = e.currentTarget;
2160
2161            $(el).find('.js-mkb-glossary-tooltip').remove();
2162        }
2163
2164        function removeTooltipOnClose(e) {
2165            e.preventDefault();
2166            e.stopImmediatePropagation();
2167
2168            var el = e.currentTarget;
2169
2170            $body.removeClass('mkb-scroll-lock');
2171
2172            $(el).parents('.js-mkb-glossary-tooltip').remove();
2173        }
2174
2175        if (platform === 'desktop') {
2176            $('.js-mkb-glossary-term').hover(showTooltip, removeTooltip);
2177        } else if (settings['glossary_mobile_mode'] === 'popup') {
2178            $body.on('click', '.js-mkb-glossary-term', showTooltip);
2179            $body.on('click', '.js-mkb-glossary-term .js-mkb-glossary-tooltip-close', removeTooltipOnClose);
2180        }
2181    }
2182
2183    function setupGlossary() {
2184        var $glossaryContainer = $('.js-mkb-glossary-list');
2185
2186        if (!$glossaryContainer.length) {
2187            return;
2188        }
2189
2190        $glossaryContainer.each(function(wrapIndex, item) {
2191            var $wrap = $(item);
2192
2193            $wrap.on('click', '.js-mkb-glossary-list-toc a', function(e) {
2194                e.preventDefault();
2195                e.stopImmediatePropagation();
2196
2197                var link = e.currentTarget;
2198
2199                $doc.animate({
2200                    scrollTop: $(link.getAttribute('href')).offset().top - parseInt(settings['glossary_scroll_offset']['size'], 10)
2201                }, 300);
2202            });
2203        });
2204    }
2205
2206    /**
2207     * Simple check. Main check is on server side
2208     */
2209    function checkIfNeedToPassReCaptcha($form) {
2210        $responseControl = $form.find('.g-recaptcha-response');
2211
2212        if (!$responseControl.length || $responseControl.val()) {
2213            return false;
2214        }
2215
2216        return true;
2217    }
2218
2219    /**
2220     * Login Form
2221     */
2222    function setupLoginForm() {
2223        var $loginForm = $('.js-mkb-support-account-login-form');
2224
2225        if (!$loginForm.length) {
2226            return;
2227        }
2228
2229        var loginForm = new ui.Form($loginForm.get(0), {});
2230
2231        $loginForm.on('submit', function(e) {
2232            e.preventDefault();
2233
2234            if (loginForm.isLoading()) {
2235                return;
2236            }
2237
2238            if (checkIfNeedToPassReCaptcha($loginForm)) {
2239                alert(i18n['recaptcha_user_message']);
2240
2241                return;
2242            }
2243
2244            var formData = loginForm.serialize();
2245
2246            loginForm.clearMessages();
2247            loginForm.startLoading();
2248
2249            $.ajax({
2250                method: 'POST',
2251                url: ajaxUrl,
2252                dataType: 'json',
2253                data: addAjaxNonce(formData)
2254            }).done(function(response) {
2255                if (response.status == 1) {
2256                    loginForm.showMessages(response.error || i18n['form_error_general_text'], 'error');
2257                } else {
2258                    window.location.reload();
2259                }
2260            }).always(function() {
2261                loginForm.endLoading();
2262            });
2263        });
2264    }
2265
2266    /**
2267     * Register Support Account
2268     */
2269    function setupRegisterForm() {
2270        var $createSupportAccountForm = $('.js-mkb-create-support-account-form');
2271
2272        if (!$createSupportAccountForm.length) {
2273            return;
2274        }
2275
2276        var createSupportAccountForm = new ui.Form($createSupportAccountForm.get(0), {});
2277
2278        $createSupportAccountForm.on('submit', function(e) {
2279            e.preventDefault();
2280
2281            if (createSupportAccountForm.isLoading()) {
2282                return;
2283            }
2284
2285            if (checkIfNeedToPassReCaptcha($createSupportAccountForm)) {
2286                alert(i18n['recaptcha_user_message']);
2287
2288                return;
2289            }
2290
2291            var formData = createSupportAccountForm.serialize();
2292
2293            createSupportAccountForm.clearMessages();
2294            createSupportAccountForm.startLoading();
2295
2296            $.ajax({
2297                method: 'POST',
2298                url: ajaxUrl,
2299                dataType: 'json',
2300                data: addAjaxNonce(formData)
2301            }).done(function(response) {
2302                if (response.status == 1) {
2303                    // TODO: process errors
2304                    createSupportAccountForm.showMessages(response.error || i18n['form_error_general_text'], 'error');
2305                } else {
2306                    if (response.message) {
2307                        createSupportAccountForm.showMessages(response.message, 'success');
2308                    }
2309
2310                    if (response.loggedIn) {
2311                        window.location.reload();
2312                    }
2313                }
2314            }).always(function() {
2315                createSupportAccountForm.endLoading();
2316            });
2317        });
2318    }
2319
2320    /**
2321     * Create user/guest ticket form
2322     */
2323    function setupCreateTicketForm() {
2324        var $createTicketForm = $('.js-mkb-create-ticket');
2325
2326        if (!$createTicketForm.length) {
2327            return;
2328        }
2329
2330        var $ticketMessageEl = $createTicketForm.find('.js-mkb-ticket-message');
2331        var quillEditor = ui.setupQuillEditor({
2332            $form: $createTicketForm,
2333            editorSelector: '.js-mkb-ticket-message',
2334            placeholder: $ticketMessageEl.data('placeholder'),
2335            theme: settings.submit_content_editor_skin,
2336            fileUpload: info['userCanAttachFiles']
2337        });
2338
2339        var createTicketForm = new ui.Form($createTicketForm.get(0), {
2340            quillEditors: [quillEditor]
2341        });
2342
2343        $createTicketForm.on('submit', function(e) {
2344            e.preventDefault();
2345
2346            if (createTicketForm.isLoading()) {
2347                return;
2348            }
2349
2350            if (checkIfNeedToPassReCaptcha($createTicketForm)) {
2351                alert(i18n['recaptcha_user_message']);
2352
2353                return;
2354            }
2355
2356            var formData = new FormData(this);
2357            var replyAsText = quillEditor.getText().trim();
2358            var isGuest = formData.get('action') === 'mkb_create_guest_support_ticket';
2359
2360            if (!replyAsText) {
2361                return alert(i18n['ticket_create_error_empty_message_text']);
2362            }
2363
2364            formData.append('message', $createTicketForm.find('.ql-editor').get(0).innerHTML.trim());
2365
2366            createTicketForm.clearMessages();
2367            createTicketForm.startLoading();
2368
2369            $.ajax({
2370                url: ajaxUrl,
2371                method: 'POST',
2372                contentType: false,
2373                processData: false,
2374                data: addAjaxNonceToFormData(formData)
2375            }).done(function(response) {
2376                if (typeof response === 'string') {
2377                    // fix for response with file upload
2378                    response = JSON.parse(response);
2379                }
2380
2381                if (response.status == 1) {
2382                    // TODO: errors
2383                    createTicketForm.showMessages(response.error || i18n['form_error_general_text'], 'error');
2384                } else {
2385                    if (isGuest) {
2386                        var $response = $(
2387                            '<div class="mkb-form-server-response status--success">' +
2388                                '<div class="mkb-create-ticket-response__icon"><i class="fa fa-check"></i></div>' +
2389                                '<div class="mkb-create-ticket-response__heading">' +
2390                                    i18n['ticket_create_success_message_heading_text'] +
2391                                '</div>' +
2392                                '<div class="mkb-create-ticket-response__top-message">' +
2393                                    (formData.get('email') ?
2394                                        (response.emailSent ?
2395                                            i18n['ticket_create_success_email_sent_text'] :
2396                                            i18n['ticket_create_success_email_not_sent_text']
2397                                        ) + ' ' :
2398                                        '') +
2399                                    i18n['ticket_create_success_link_message_text'] +
2400                                '</div>' +
2401                                '<div class="mkb-create-ticket-response__link-wrap"><a href="' + response.ticketUrl + '" target="_blank">' + formData.get('title') + '</a></div>' +
2402                            '</div>'
2403                        );
2404
2405                        $createTicketForm.after($response);
2406
2407                        $createTicketForm.remove();
2408
2409                        $doc.animate({
2410                            scrollTop: $response.offset().top - parseInt(settings['global_scroll_offset']['size'])
2411                        }, 300);
2412                    } else {
2413                        window.location.href = response.ticketUrl;
2414                    }
2415                }
2416            }).always(function() {
2417                createTicketForm.endLoading();
2418            });
2419        });
2420
2421        // admin area setup
2422        var $otherUserSelect = $('.js-mkb-ticket-other-user-select');
2423
2424        if ($otherUserSelect.length) {
2425            $otherUserSelect.select2();
2426
2427            // TODO: remove this hack after updating to next stable Select2 version
2428            $(document).on('select2:open', function() {
2429                document.querySelector(".select2-container--open .select2-search__field").focus()
2430            });
2431        }
2432    }
2433
2434    /**
2435     * Reply to ticket
2436     */
2437    function setupReplyToTicketForm() {
2438        var $replyToTicketForm = $('.js-mkb-reply-to-ticket');
2439
2440        if (!$replyToTicketForm.length) {
2441            return;
2442        }
2443
2444        if (window.sessionStorage.getItem('mkbTicketNewReplyAdded')) {
2445            setTimeout(function() {
2446                $doc.animate({
2447                    scrollTop: $('.js-mkb-ticket-discussion').offset().top - parseInt(settings['global_scroll_offset']['size'])
2448                }, 300);
2449            }, 1000);
2450
2451            window.sessionStorage.removeItem('mkbTicketNewReplyAdded');
2452        }
2453
2454        // TODO: scroll top maybe
2455        var quillEditor = ui.setupQuillEditor({
2456            $form: $replyToTicketForm,
2457            editorSelector: '.js-mkb-ticket-reply-content',
2458            placeholder: i18n['ticket_reply_field_placeholder_text'],
2459            theme: settings.submit_content_editor_skin,
2460            fileUpload: info['userCanAttachFiles']
2461        });
2462
2463        var replyForm = new ui.Form($replyToTicketForm.get(0), {
2464            quillEditors: [quillEditor]
2465        });
2466
2467        $replyToTicketForm.on('submit', function(e) {
2468            e.preventDefault();
2469
2470            if (replyForm.isLoading()) {
2471                return;
2472            }
2473
2474            // TODO: maybe move formData with files to Form as well
2475            var formData = new FormData(this);
2476            var replyAsText = quillEditor.getText().trim();
2477
2478            if (!replyAsText) {
2479                replyForm.showMessages(i18n['ticket_reply_error_empty_text'], 'error');
2480                return;
2481            }
2482
2483            formData.append('reply', $replyToTicketForm.find('.ql-editor').get(0).innerHTML.trim());
2484            formData.append('action', 'mkb_reply_to_ticket');
2485
2486            replyForm.clearMessages();
2487            replyForm.startLoading();
2488
2489            $.ajax({
2490                url: ajaxUrl,
2491                method: 'POST',
2492                contentType: false,
2493                processData: false,
2494                data: addAjaxNonceToFormData(formData)
2495            }).done(function(response) {
2496                if (typeof response === 'string') {
2497                    // fix for response with file upload
2498                    response = JSON.parse(response);
2499                }
2500
2501                if (response.status == 1) {
2502                    replyForm.showMessages(i18n['form_error_general_text'], 'error');
2503                } else {
2504                    var message = i18n['ticket_reply_success_message_text'];
2505
2506                    if (response.fileUploadErrors && response.fileUploadErrors.length) {
2507                        message += ' ' + i18n['ticket_reply_error_files_not_added_text'];
2508                    }
2509
2510                    replyForm.showMessages(message, 'success');
2511
2512                    window.sessionStorage.setItem('mkbTicketNewReplyAdded', true);
2513
2514                    window.location.reload();
2515                }
2516            }).always(function() {
2517                replyForm.endLoading();
2518            });
2519        });
2520    }
2521
2522    /**
2523     * Reopen ticket
2524     */
2525    function setupReopenTicketForm() {
2526        var $reopenTicketForm = $('.js-mkb-reopen-ticket-form');
2527
2528        if (!$reopenTicketForm.length) {
2529            return;
2530        }
2531
2532        var reopenTicketForm = new ui.Form($reopenTicketForm.get(0), {});
2533
2534        $reopenTicketForm.on('submit', function(e) {
2535            e.preventDefault();
2536
2537            if (reopenTicketForm.isLoading()) {
2538                return;
2539            }
2540
2541            var formData = reopenTicketForm.serialize();
2542
2543            formData.action = 'mkb_reopen_ticket';
2544
2545            reopenTicketForm.clearMessages();
2546            reopenTicketForm.startLoading();
2547
2548            $.ajax({
2549                url: ajaxUrl,
2550                method: 'POST',
2551                data: addAjaxNonce(formData)
2552            }).done(function(response) {
2553                if (response.status == 1) {
2554                    // TODO: errors
2555                    reopenTicketForm.showMessages(i18n['form_error_general_text'], 'error');
2556                } else {
2557                    reopenTicketForm.showMessages(i18n['ticket_reopen_success_message_text'], 'success');
2558
2559                    window.location.reload();
2560                }
2561            }).always(function() {
2562                reopenTicketForm.endLoading();
2563            });
2564        });
2565    }
2566
2567    /**
2568     * Ticket credentials
2569     */
2570    function setupTicketCredentialsForm() {
2571        var $credentialsForm = $('.js-mkb-provide-ticket-credentials');
2572
2573        if (!$credentialsForm.length) {
2574            return;
2575        }
2576
2577        var credentialsForm = new ui.Form($credentialsForm.get(0), {});
2578
2579        $credentialsForm.on('click', '.js-mkb-credentials-show', function(e) {
2580            e.preventDefault();
2581
2582            $credentialsForm.addClass('state--open');
2583        });
2584
2585        $credentialsForm.on('click', '.js-mkb-credentials-hide', function(e) {
2586            e.preventDefault();
2587
2588            $credentialsForm.removeClass('state--open');
2589        });
2590
2591        /**
2592         * Provide credentials
2593         */
2594        $credentialsForm.on('submit', function(e) {
2595            e.preventDefault();
2596
2597            if (credentialsForm.isLoading()) {
2598                return;
2599            }
2600
2601            var formData = credentialsForm.serialize();
2602
2603            formData.ticket_credentials = formData.ticket_credentials && formData.ticket_credentials.trim() || '';
2604
2605            if (!formData.ticket_credentials) {
2606                credentialsForm.showMessages(i18n['ticket_credentials_error_empty_text'], 'error');
2607
2608                return;
2609            }
2610
2611            formData.action = 'mkb_provide_ticket_credentials';
2612
2613            credentialsForm.clearMessages();
2614            credentialsForm.startLoading();
2615
2616            $.ajax({
2617                url: ajaxUrl,
2618                method: 'POST',
2619                data: addAjaxNonce(formData)
2620            }).done(function(response) {
2621                if (response.status == 1) {
2622                    // TODO: errors
2623                    credentialsForm.showMessages(i18n['form_error_general_text'], 'error');
2624                } else {
2625                    credentialsForm.showMessages(i18n['ticket_credentials_s
2625aved_text'], 'success');
2626
2627                    $('.js-mkb-delete-ticket-credentials').removeClass('mkb-hidden');
2628                }
2629            }).always(function() {
2630                credentialsForm.endLoading();
2631            });
2632        });
2633
2634        /**
2635         * Delete credentials
2636         */
2637        $('.js-mkb-delete-ticket-credentials').on('click', function(e) {
2638            e.preventDefault();
2639
2640            if (credentialsForm.isLoading()) {
2641                return;
2642            }
2643
2644            var $btn = $(e.currentTarget);
2645            var formData = credentialsForm.serialize();
2646
2647            formData.action = 'mkb_delete_ticket_credentials';
2648
2649            credentialsForm.clearMessages();
2650            credentialsForm.startLoading();
2651
2652            $.ajax({
2653                url: ajaxUrl,
2654                method: 'POST',
2655                data: addAjaxNonce(formData)
2656            }).done(function(response) {
2657                if (response.status == 1) {
2658                    // TODO: errors
2659                    credentialsForm.showMessages(i18n['form_error_general_text'], 'error');
2660                } else {
2661                    credentialsForm.showMessages(i18n['ticket_credentials_deleted_text'], 'success');
2662
2663                    $btn.addClass('mkb-hidden');
2664                    $('#ticket_credentials').val('');
2665                }
2666            }).always(function() {
2667                credentialsForm.endLoading();
2668            });
2669        });
2670    }
2671
2672    /**
2673     * Elapsed time tickers
2674     */
2675    function setupTicketElapsedTimeTickers() {
2676        if (!window.moment) {
2677            return;
2678        }
2679
2680        var locale = $('html').attr('lang') || window.navigator.userLanguage || window.navigator.language;
2681        moment.locale(locale);
2682
2683        $('.js-mkb-human-readable-time').each(function(index, item) {
2684            var $item = $(item);
2685            var timestamp = item.dataset.timestamp * 1000;
2686            var ONCE_A_MINUTE = 1000 * 60;
2687
2688            $item.html(moment.utc(timestamp).fromNow());
2689
2690            setInterval(function() {
2691                $item.html(moment(timestamp).fromNow());
2692            }, ONCE_A_MINUTE);
2693        });
2694    }
2695
2696    function setTicketViewedFlags() {
2697        $.ajax({
2698            url: ajaxUrl,
2699            method: 'POST',
2700            data: addAjaxNonce({
2701                action: 'mkb_ticket_viewed_by_customer',
2702                ticketId: info.postId
2703            })
2704        });
2705    }
2706
2707    /**
2708     *
2709     */
2710    function setupTickets() {
2711        setupLoginForm();
2712        setupRegisterForm();
2713
2714        setupCreateTicketForm();
2715        setupReplyToTicketForm();
2716        setupTicketCredentialsForm();
2717        setupReopenTicketForm();
2718        setupTicketElapsedTimeTickers();
2719
2720        if (info.isTicket) {
2721            setTicketViewedFlags();
2722        }
2723    }
2724
2725    function isElementorPreviewScreen() {
2726        if (window.URLSearchParams) {
2727            var urlParams = new URLSearchParams(window.location.search);
2728            return Boolean(urlParams.get('elementor-preview'));
2729        }
2730
2731        return false;
2732    }
2733
2734    function setupFeatureRequests() {
2735        $body.on('click', '.js-mkb-vote-for-feature', function(e) {
2736            e.preventDefault();
2737
2738            var btn = e.currentTarget;
2739            var $btn = $(btn);
2740            var id = btn.dataset.id;
2741            var $wrap = $btn.parent();
2742            var $votesCount = $wrap.find('.js-mkb-fr-votes-count');
2743            var $votesCountText = $wrap.find('.js-mkb-fr-votes-count-text');
2744            var currentVotes = Number($votesCount.text().trim());
2745
2746            if ($btn.hasClass('state--pending') || $btn.hasClass('state--voted')) {
2747                return;
2748            }
2749
2750            $btn.addClass('state--pending');
2751
2752            $.ajax({
2753                method: 'POST',
2754                url: ajaxUrl,
2755                dataType: 'json',
2756                data: addAjaxNonce({
2757                    action: 'mkb_feature_vote',
2758                    id: id
2759                })
2760            }).done(function() {
2761                ++currentVotes;
2762
2763                $votesCount.html(currentVotes);
2764                $votesCountText.html(currentVotes === 1 ? 'vote' : 'votes');
2765
2766                $btn.addClass('state--voted');
2767            }).always(function() {
2768                $btn.removeClass('state--pending');
2769            });
2770        });
2771    }
2772
2773    /**
2774     * Feature Request Submit form
2775     */
2776    function setupFeatureRequestSubmitForm() {
2777        var $featureRequestSubmitForm = $('.js-mkb-feature-request-submit-form');
2778
2779        if (!$featureRequestSubmitForm.length) {
2780            return;
2781        }
2782
2783        var featureRequestSubmitForm = new ui.Form($featureRequestSubmitForm.get(0));
2784
2785        $featureRequestSubmitForm.on('submit', function(e) {
2786            e.preventDefault();
2787
2788            if (featureRequestSubmitForm.isLoading()) {
2789                return;
2790            }
2791
2792            if (checkIfNeedToPassReCaptcha($featureRequestSubmitForm)) {
2793                alert(i18n['recaptcha_user_message']);
2794
2795                return;
2796            }
2797
2798            var formData = featureRequestSubmitForm.serialize();
2799
2800            featureRequestSubmitForm.clearMessages();
2801            featureRequestSubmitForm.startLoading();
2802
2803            $.ajax({
2804                url: ajaxUrl,
2805                method: 'POST',
2806                data: addAjaxNonce(formData)
2807            }).done(function(response) {
2808                if (response.status == 1) {
2809                    // TODO: errors
2810                    featureRequestSubmitForm.showMessages(response.error || i18n['form_error_general_text'], 'error');
2811                } else {
2812                    featureRequestSubmitForm.showMessages(i18n['feature_request_submit_success_message'], 'success');
2813
2814                    setTimeout(function() {
2815                        featureRequestSubmitForm.lock();
2816                    }, 0);
2817
2818                    $doc.animate({
2819                        scrollTop: $featureRequestSubmitForm.offset().top - parseInt(settings['global_scroll_offset']['size'])
2820                    }, 300);
2821                }
2822            }).always(function() {
2823                featureRequestSubmitForm.endLoading();
2824            }).fail(function() {
2825                featureRequestSubmitForm.showMessages(i18n['form_error_general_text'], 'error');
2826            });
2827        });
2828    }
2829
2830    /**
2831     * Main plugin startup
2832     */
2833    function init() {
2834        // for future use
2835        var isElementorPreview = isElementorPreviewScreen();
2836
2837        $adminBar = $('#wpadminbar');
2838        adminOffset = $adminBar.length ? $adminBar.height() : 0;
2839
2840        if ($kbSearch.length && !isSearchDisabled()) {
2841            initSearchInputs();
2842            $body.on('keydown', onSearchKeyPress);
2843            $body.on('click', '.kb-search__clear', handleSearchClear);
2844            focusInput();
2845            onSearchType($kbSearch.eq(0)); // restore previous search
2846
2847            setupExtraSearchGroupsOnPageTargets();
2848        }
2849
2850        // FAQ items
2851        setupFaq();
2852
2853        // Glossary items
2854        setupGlossary();
2855
2856        // Feature Requests
2857        setupFeatureRequests();
2858        setupFeatureRequestSubmitForm();
2859
2860        // article related code
2861        if (info.isSingle) {
2862            $body.on('click', '.mkb-article-extra__like', handleArticleLike);
2863            $body.on('click', '.mkb-article-extra__dislike', handleArticleDislike);
2864            $body.on('click', '.mkb-article-extra__feedback-form-submit', handleFeedbackSubmit);
2865            $body.on('input', '.mkb-article-extra__feedback-form-message-area', handleFeedbackType);
2866
2867            handleArticleBackToTop();
2868            handleArticleTOC();
2869
2870            if (settings['article_fancybox']) {
2871                initArticlesFancyBox();
2872            }
2873
2874            trackArticleView();
2875
2876            // sticky articles sidebar
2877            setupArticleStickySidebar();
2878
2879            setupAttachments();
2880        }
2881
2882        setupGlossaryTooltips();
2883        setupContentTreeWidgets();
2884        setupSubmissionForm();
2885        setupHelper();
2886
2887        // todo: on ticket page only
2888        setupTickets();
2889    }
2890
2891    // start
2892    $(document).ready(init);
2893
2894})(window.jQuery);

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.