Smile-SA/elasticsuite

View on GitHub
src/module-elasticsuite-core/view/frontend/web/js/form-mini.js

Summary

Maintainability
D
2 days
Test Coverage
/**
 * DISCLAIMER
 *
 * Do not edit or add to this file if you wish to upgrade Smile ElasticSuite to newer
 * versions in the future.
 *
 * @category  Smile
 * @package   Smile\ElasticsuiteCore
 * @author    Romain Ruaud <romain.ruaud@smile.fr>
 * @copyright 2020 Smile
 * @license   Open Software License ("OSL") v. 3.0
 */

/*jshint browser:true jquery:true*/
/*global alert*/

define([
    'ko',
    'jquery',
    'underscore',
    'mage/template',
    'Magento_Catalog/js/price-utils',
    'Magento_Ui/js/lib/knockout/template/loader',
    'Magento_Ui/js/modal/modal',
    'mage/translate',
    'Magento_Search/js/form-mini'
], function (ko, $, _, mageTemplate, priceUtil, templateLoader) {
    'use strict';

    $.widget('smileEs.quickSearch', $.mage.quickSearch, {
        options: {
            autocomplete: 'off',
            minSearchLength: 2,
            responseFieldElements: 'dl dd',
            selectClass: 'selected',
            submitBtn: 'button[type="submit"]',
            searchLabel: '[data-role=minisearch-label]'
        },

        /**
         * Overriden constructor to ensure templates initialization on load
         *
         * @private
         */
        _create: function () {
            this.templateCache = [];
            this.currentRequest = null;
            this._initTemplates();
            this._initTitleRenderer();
            this._super();
            this._blur();
        },

        /**
         * Init templates used for rendering when instantiating the widget
         *
         * @private
         */
        _initTemplates: function() {
            for (var template in this.options.templates) {
                if ({}.hasOwnProperty.call(this.options.templates, template)) {
                    this._loadTemplate(template);
                }
            }
        },

        /**
         * Init templates used for rendering when instantiating the widget
         *
         * @private
         */
        _initTitleRenderer: function() {
            this.titleRenderers = {};
            for (var typeIdentifier in this.options.templates) {
                if (this.options.templates[typeIdentifier]['titleRenderer']) {
                   require([this.options.templates[typeIdentifier]['titleRenderer']], function (renderer) {
                       this.component.titleRenderers[this.type] = renderer;
                   }.bind({component: this, type: typeIdentifier}));
                }
            }
        },

        /**
         * Load a renderer for title when configured for a type.
         *
         * @param type The type to render
         *
         * @private
         */
        _loadTemplate: function (type) {
            var templateFile = this.options.templates[type]['template'];
            templateLoader.loadTemplate(templateFile).done(function (renderer) {
                this.options.templates[type]['template'] = renderer;
            }.bind(this));
        },

        /**
         * Get rendering template for a given element. Will look into this.options.templates[element.type] for the renderer.
         * Returns an evaluated template for the given element's type.
         *
         * @param element The autocomplete result to display
         *
         * @returns function
         *
         * @private
         */
        _getTemplate: function (element) {
            var source = this.options.template; // Fallback to standard widget template
            var type   = element.type ? element.type : 'undefined';

            if (this.templateCache[type]) {
                return this.templateCache[type];
            }

            if (element.type && this.options.templates && this.options.templates[element.type]) {
                source = this.options.templates[element.type].template;
            }

            this.templateCache[type] = mageTemplate(source);

            return this.templateCache[type];
        },

        /**
         * Render an autocomplete item in the result list
         *
         * @param element The element : an autocomplete result
         * @param index   The element index
         *
         * @returns {*|jQuery|HTMLElement}
         *
         * @private
         */
        _renderItem: function (element, index) {
            var template = this._getTemplate(element);
            element.index = index;

            if (element.price && (!isNaN(element.price))) {
                element.price = priceUtil.formatPrice(element.price, this.options.priceFormat);
            }

            return template({
                data: element
            });
        },

        /**
         * Return the wrapper for all autocomplete results
         *
         * @returns {*|jQuery|HTMLElement}
         *
         * @private
         */
        _getResultWrapper: function () {
            return $('<div class="smile-elasticsuite-autocomplete-result"></div>');
        },

        /**
         * Return the header for an autocomplete result section
         *
         * @param type The type of element to display
         *
         * @returns {*|jQuery|HTMLElement}
         *
         * @private
         */
        _getSectionHeader: function(type, data) {
            var title = '';
            var header = $('<dl role="listbox" class="autocomplete-list"></dl>');

            if (type !== undefined) {
                title = this._getSectionTitle(type, data);
                header.append(title);
            }

            return header;
        },

        /**
         * Return the title for an autocomplete result section
         *
         * @param type
         *
         * @returns {*|jQuery|HTMLElement}
         *
         * @private
         */
        _getSectionTitle: function(type, data) {
            var title = '';

            if (this.titleRenderers && this.titleRenderers[type]) {
                title = $('<dt role="listbox" class="autocomplete-list-title title-' + type + '">' + this.titleRenderers[type].render(data) + '</dt>');
            } else if (this.options.templates && this.options.templates[type].title) {
                title = $('<dt role="listbox" class="autocomplete-list-title title-' + type + '">' + this.options.templates[type].title + '</dt>');
            }

            return title;
        },

        /**
         * Check wether the incoming string is not empty or if doesn't consist of spaces.
         *
         * @param {String} value - Value to check.
         *
         * @returns {Boolean}
         *
         * @private
         */
        _isEmpty : function(value) {
            return value === null || value.trim().length === 0;
        },

        /**
         * Executes when the value of the search input field changes. Executes a GET request
         * to populate a suggestion list based on entered text. Handles click (select), hover,
         * and mouseout events on the populated suggestion list dropdown.
         *
         * Overriden to :
         *  - move rendering of elements in a subfunction.
         *  - manage redirection when clicking a result having an href attribute.
         *
         * @private
         */
        _onPropertyChange: _.debounce(function () {
            var searchField = this.element,
                clonePosition = {
                    position: 'absolute',
                    // Removed to fix display issues
                    // left: searchField.offset().left,
                    // top: searchField.offset().top + searchField.outerHeight(),
                    width: searchField.outerWidth()
                },
                value = this.element.val();

            this.submitBtn.disabled = this._isEmpty(value);

            if (value.trim().length >= parseInt(this.options.minSearchLength, 10)) {
                this.searchForm.addClass('processing');
                this.currentRequest = $.ajax({
                    method: "GET",
                    url: this.options.url,
                    cache: true,
                    dataType: 'json',
                    data: {q: value},
                    // This function will ensure proper killing of the last Ajax call.
                    // In order to prevent requests of an old request to pop up later and replace results.
                    beforeSend: function() { if (this.currentRequest !== null) { this.currentRequest.abort(); }}.bind(this),
                    success: $.proxy(function (data) {
                        var self = this;
                        var lastElement = false;
                        var content = this._getResultWrapper();
                        var sectionDropdown = this._getSectionHeader();
                        $.each(data, function(index, element) {

                            if (!lastElement || (lastElement && lastElement.type !== element.type)) {
                                sectionDropdown = this._getSectionHeader(element.type, data);
                            }

                            var elementHtml = this._renderItem(element, index);

                            sectionDropdown.append(elementHtml);

                            if (!lastElement || (lastElement && lastElement.type !== element.type)) {
                                content.append(sectionDropdown);
                            }

                            lastElement = element;
                        }.bind(this));
                        this.responseList.indexList = this.autoComplete.html(content)
                            .css(clonePosition)
                            .show()
                            .find(this.options.responseFieldElements + ':visible');

                        this._resetResponseList(false);
                        this.element.removeAttr('aria-activedescendant');

                        if (this.responseList.indexList.length) {
                            this._updateAriaHasPopup(true);
                        } else {
                            this._updateAriaHasPopup(false);
                        }

                        this.responseList.indexList
                            .on('click vclick', function (e) {
                                self.responseList.selected = $(this);
                                if (self.responseList.selected.attr("href")) {
                                    window.location.href = self.responseList.selected.attr("href");
                                    e.stopPropagation();
                                    return false;
                                }
                                self.searchForm.trigger('submit');
                            })
                            .on('mouseenter', function (e) {
                                self.responseList.indexList.removeClass(self.options.selectClass);
                                $(this).addClass(self.options.selectClass);
                                self.responseList.selected = $(e.target);
                                self.element.attr('aria-activedescendant', $(e.target).attr('id'));
                            })
                            .on('mouseleave', function (e) {
                                $(this).removeClass(self.options.selectClass);
                                self._resetResponseList(false);
                            })
                            .on('mouseout', function () {
                                if (!self._getLastElement() && self._getLastElement().hasClass(self.options.selectClass)) {
                                    $(this).removeClass(self.options.selectClass);
                                    self._resetResponseList(false);
                                }
                            });
                    },this),
                    complete : $.proxy(function () {
                        this.searchForm.removeClass('processing');
                    }, this)
                });
            } else {
                this._resetResponseList(true);
                this.autoComplete.hide();
                this._updateAriaHasPopup(false);
                this.element.removeAttr('aria-activedescendant');
            }
        }, 250),

        /**
         * Executes when keys are pressed in the search input field. Performs specific actions
         * depending on which keys are pressed.
         *
         * @private
         * @param {Event} e - The key down event
         * @return {Boolean} Default return type for any unhandled keys
         */
        _onKeyDown: function (e) {
            var keyCode = e.keyCode || e.which;

            switch (keyCode) {
                case $.ui.keyCode.HOME:
                    this._selectElement(this._getFirstVisibleElement());
                    break;
                case $.ui.keyCode.END:
                    this._selectElement(this._getLastElement());
                    break;
                case $.ui.keyCode.ESCAPE:
                    this._resetResponseList(true);
                    this.autoComplete.hide();
                    break;
                case $.ui.keyCode.ENTER:
                    this._validateElement(e);
                    break;
                case $.ui.keyCode.DOWN:
                    this._navigateDown();
                    break;
                case $.ui.keyCode.UP:
                    this._navigateUp();
                    break;
                default:
                    return true;
            }
        },

        /**
         * Validate selection of an element (eg : when ENTER is pressed)
         *
         * @param event The keydown event
         *
         * @returns {boolean}
         *
         * @private
         */
        _validateElement: function(event) {
            var selected = this.responseList.selected;
            if (selected && selected.attr('href') !== undefined) {
                window.location = selected.attr('href');
                event.preventDefault();
                return false;
            }
            this.searchForm.trigger('submit');
        },

        /**
         * Process down navigation on autocomplete box
         *
         * @private
         */
        _navigateDown: function() {
            if (this.responseList.indexList) {
                if (!this.responseList.selected) {
                    this._getFirstVisibleElement().addClass(this.options.selectClass);
                    this.responseList.selected = this._getFirstVisibleElement();
                }
                else if (!this._getLastElement().hasClass(this.options.selectClass)) {
                    var nextElement = this._getNextElement();
                    this.responseList.selected.removeClass(this.options.selectClass);
                    this.responseList.selected = nextElement.addClass(this.options.selectClass);
                } else {
                    this.responseList.selected.removeClass(this.options.selectClass);
                    this._getFirstVisibleElement().addClass(this.options.selectClass);
                    this.responseList.selected = this._getFirstVisibleElement();
                }
                this._activateElement();
            }
        },

        /**
         * Process up navigation on autocomplete box
         *
         * @private
         */
        _navigateUp: function() {
            if (this.responseList.indexList !== null) {
                if (!this._getFirstVisibleElement().hasClass(this.options.selectClass)) {
                    var prevElement = this._getPrevElement();
                    this.responseList.selected.removeClass(this.options.selectClass);
                    this.responseList.selected = prevElement.addClass(this.options.selectClass);
                } else {
                    this.responseList.selected.removeClass(this.options.selectClass);
                    this._getLastElement().addClass(this.options.selectClass);
                    this.responseList.selected = this._getLastElement();
                }
                this._activateElement();
            }
        },

        /**
         * Toggles an element as currently selected
         *
         * @param {Element} e - The DOM element
         *
         * @private
         */
        _selectElement: function(element) {
            element.addClass(this.options.selectClass);
            this.responseList.selected = element;
        },

        /**
         * Toggles an element as active
         *
         * @param {Element} e - The DOM element
         *
         * @private
         */
        _activateElement: function() {
            this.element.val(this.responseList.selected.find('.qs-option-name').text());
            this.element.attr('aria-activedescendant', this.responseList.selected.attr('id'));
        },

        /**
         * Retrieve the next element when navigating through keyboard
         *
         * @private
         *
         * @return Element
         */
        _getNextElement: function() {
            var nextElement = this.responseList.selected.next('dd');
            if (nextElement.length === 0) {
                nextElement = this.responseList.selected.parent('dl').next('dl').find('dd').first();
            }

            return nextElement;
        },

        /**
         * Retrieve the previous element when navigating through keyboard
         *
         * @private
         *
         * @return Element
         */
        _getPrevElement: function() {
            var prevElement = this.responseList.selected.prev('dd');
            this.responseList.selected.removeClass(this.options.selectClass);
            if (prevElement.length === 0) {
                prevElement = this.responseList.selected.parent('dl').prev('dl').find('dd').last();
            }

            return prevElement;
        },

        /**
         * Handle blur event of search input item
         * @private
         */
        _blur: function() {
            this.element.on('blur', $.proxy(function () {
                if (!this.searchLabel.hasClass('active')) {
                    return;
                }
                setTimeout($.proxy(function () {
                    if (this.autoComplete.is(':hidden')) {
                        this.setActiveState(false);
                    } else {
                        this.element.trigger('focus');
                    }
                    this.autoComplete.hide();
                    $('#search').trigger('blur');
                    this._updateAriaHasPopup(false);
                }, this),250);
            }, this));
        }
    });

    return $.smileEs.quickSearch;
});