Back to Top

Magento2 Create Multi Select UI Field on Frontend as Admin

Updated 23 February 2024

Here we learn how to add or create Multi select field using the Ui Component on frontend.
I am going to take an example of categories multi select files which looks like the below
image in admin->catalog New or Edit Product section.

Select UI

Let’s start step by step.
You must have installed your custom Module on which you are going to implement this.

1.) Create a xml file which loads your Block and Phtml file.

For Example: Webkul/Custom/view/frontend/layout/custom_test_index.xml

<?xml version="1.0"?>
<!--
/**
 * Webkul Software.
 *
 * @category  Webkul
 * @package   Webkul_Custom
 * @author    Webkul
 * @copyright Webkul Software Private Limited (https://webkul.com)
 * @license   https://store.webkul.com/license.html
 */
-->
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
    <update handle="customer_account"/>
    <body>
        <referenceBlock name="page.main.title">
            <action method="setPageTitle">
                <argument translate="true" name="title" xsi:type="string">Ui Multi Select</argument>
            </action>
        </referenceBlock>
        <referenceContainer name="content">
            <block class="Webkul\Custom\Block\Test" name="ui_multiselect" template="test.phtml" cacheable="false"/>
        </referenceContainer>
    </body>
</page>

Now, create a Controller file for this.
File: Webkul/Custom/Controller/Test/Index.php

Searching for an experienced
Magento 2 Company ?
Find out More
<?php
namespace Webkul\Custom\Controller\Test;

use Magento\Framework\App\Action\Context;
use Magento\Framework\View\Result\PageFactory;
use Magento\Framework\App\RequestInterface;
use Magento\Customer\Model\Url as CustomerUrl;

class Index extends \Magento\Customer\Controller\AbstractAccount
{
    /**
     * @var \Magento\Customer\Model\Session
     */
    protected $customerSession;

    /**
     * @var PageFactory
     */
    protected $resultPageFactory;

    /**
     * @var CustomerUrl
     */
    protected $customerUrl;

   /**
    * Construct
    *
    * @param Context $context
    * @param PageFactory $resultPageFactory
    * @param \Magento\Customer\Model\Session $customerSession
    * @param CustomerUrl $customerUrl
    */
    public function __construct(
        Context $context,
        PageFactory $resultPageFactory,
        \Magento\Customer\Model\Session $customerSession,
        CustomerUrl $customerUrl
    ) {
        $this->customerSession = $customerSession;
        $this->resultPageFactory = $resultPageFactory;
        $this->customerUrl = $customerUrl;
        parent::__construct($context);
    }

    /**
     * Check customer authentication.
     *
     * @param RequestInterface $request
     *
     * @return \Magento\Framework\App\ResponseInterface
     */
    public function dispatch(RequestInterface $request)
    {
        $loginUrl = $this->customerUrl->getLoginUrl();

        if (!$this->customerSession->authenticate($loginUrl)) {
            $this->_actionFlag->set('', self::FLAG_NO_DISPATCH, true);
        }

        return parent::dispatch($request);
    }

    /**
     * Execute Action
     *
     * @return void
     */
    public function execute()
    {
        $resultPage = $this->resultPageFactory->create();
        $resultPage->getConfig()->getTitle()->set(__('Ui Multi Select'));

        return $resultPage;
    }
}

2.) Now Create the respective Block.
Webkul/Custom/Block/Test.php

<?php
/**
 * Webkul Software.
 *
 * @category  Webkul
 * @package   Webkul_Custom
 * @author    Webkul
 * @copyright Webkul Software Private Limited (https://webkul.com)
 * @license   https://store.webkul.com/license.html
 */
namespace Webkul\Custom\Block;

use Magento\Store\Model\StoreManagerInterface;
use Magento\Framework\App\Filesystem\DirectoryList;

class Test extends \Magento\Framework\View\Element\Template
{
    /**
     * @var \Magento\Framework\Data\Form\FormKey
     */
    protected $formKey;

    /**
     * @var \Magento\Framework\App\Config\ScopeConfigInterface
     */
    protected $_scopeConfig;

    /**
     * @var \Magento\Framework\UrlInterface
     */
    protected $_urlBuilder;

    /**
     * @param \Magento\Backend\Block\Template\Context $context
     * @param \Magento\Framework\View\Model\PageLayout\Config\BuilderInterface $pageLayoutBuilder
     * @param \Magento\Framework\ObjectManagerInterface $objectManager
     * @param array $data
     */
    public function __construct(
        \Magento\Backend\Block\Template\Context $context,
        \Magento\Framework\View\Model\PageLayout\Config\BuilderInterface $pageLayoutBuilder,
        \Magento\Framework\ObjectManagerInterface $objectManager,
        array $data = []
    ) {
        $this->_objectManager = $objectManager;
        $this->formKey = $context->getFormKey();
        parent::__construct($context, $data);
    }

    /**
     * Prepare layout
     *
     * @return this
     */
    public function _prepareLayout()
    {
        return parent::_prepareLayout();
    }
    
    public function getCategoriesTree()
    {
        $categories = $this->_objectManager->create(
            'Magento\Catalog\Ui\Component\Product\Form\Categories\Options'
        )->toOptionArray();
        return json_encode($categories);
    }
}

In above Block file getCategoriesTree() Method loads the all Categories in array form. Its just
an example, you can pass your custom array to display in the field.

3.) Create the phtml template file.
Webkul/Custom/view/frontend/templates/test.phtml

<form action="<?php echo $block->getUrl('custom/manage/save', ['_secure' => $this->getRequest()->isSecure()]) ?>" enctype="multipart/form-data" method="post"  data-mage-init='{"validation":{}}'>
    <fieldset class="fieldset">
        <div class="field required">
            <label class="label"><?php echo __('Select Category') ?>:</label>
            <div data-bind="scope: 'customCategory'">
                <!-- ko template: getTemplate() --><!-- /ko -->
            </div>
            <script type="text/x-magento-init">
                {
                    "*": {
                        "Magento_Ui/js/core/app": {
                            "components": {
                                "customCategory": {
                                    "component": "Magento_Ui/js/form/element/ui-select",
                                    "template" : "Webkul_Custom/ui-select",
                                    "filterOptions": true,
                                    "levelsVisibility": "1",
                                    "multiple": true,
                                    "showCheckbox": true,
                                    "options": <?php echo $block->getCategoriesTree()?>
                                }
                            }
                        }
                    }
                }
            </script>
        </div>
    </fieldset>
</form>

How to Create Multi Select UI Field on Frontend as Admin

In above template file, we have instantiated ui-select.js component and its default values.
You can customize this ui-select fields according to your choice. Like you can disable multiple, showCheckbox etc.

For more default options of ui-select you can view this file:
Magento_Ui/js/form/element/ui-select.js

For Now let’s create the ui-select.html template which we have used in customCategory component.
Webkul/Custom/view/frontend/web/template/ui-select.html

<!-- ko ifnot: disableLabel -->
<label class="admin__form-field-label" data-bind="attr: {for: uid}
">
    <span translate="label"></span>
</label>
<!-- /ko -->
<div class="admin__action-multiselect-wrap action-select-wrap" tabindex="0" data-bind="
attr: {
    id: uid
},
css: {
    _active: listVisible,
    'admin__action-multiselect-tree': isTree()
},
event: {
    focusin: onFocusIn,
    focusout: onFocusOut,
    keydown: keydownSwitcher
},
outerClick: outerClick.bind($data)
">
    <!-- ko ifnot: chipsEnabled -->
    <div class="action-select admin__action-multiselect" data-role="advanced-select" data-bind="
    css: {_active: multiselectFocus},
    click: function(data, event) {
        toggleListVisible(data, event)
    }
">
        <div class="admin__action-multiselect-text" data-role="selected-option" data-bind="text: setCaption()">
        </div>
        <!-- ko foreach: { data: getSelected(), as: 'option'}  -->
        <input type="hidden" name="category_id" data-bind="
            attr: {
                id: 'wk-cat-hide'+value,
                value: value
            }
        ">
        <!-- /ko -->
    </div>
    <!-- /ko -->
    <!-- ko if: chipsEnabled -->
    <div class="action-select admin__action-multiselect" data-role="advanced-select" data-bind="
    css: {_active: multiselectFocus},
    click: function(data, event) {
        toggleListVisible(data, event)
    }
">
        <div class="admin__action-multiselect-text" data-bind="
        visible: !hasData(),
        i18n: selectedPlaceholders.defaultPlaceholder
">
        </div>
        <!-- ko foreach: { data: getSelected(), as: 'option'}  -->
        <span class="admin__action-multiselect-crumb">
            <span data-bind="text: label">
            </span>
            <button class="action-close" type="button" data-action="remove-selected-item" tabindex="-1" data-bind="click: $parent.removeSelected.bind($parent, value)
        ">
                <span class="action-close-text" translate="'Close'"></span>
            </button>
        </span>
        <!-- /ko -->
    </div>
    <!-- /ko -->
    <div class="action-menu" data-bind="css: { _active: listVisible }
">
        <!-- ko if: filterOptions -->
        <div class="admin__action-multiselect-search-wrap">
            <input class="admin__control-text admin__action-multiselect-search" data-role="advanced-select-text" type="text" data-bind="
        event: {
            keydown: filterOptionsKeydown
        },
        attr: {id: uid+2},
        valueUpdate: 'afterkeydown',
        value: filterInputValue,
        hasFocus: filterOptionsFocus
        ">
            <label class="admin__action-multiselect-search-label" data-action="advanced-select-search" data-bind="attr: {for: uid+2}
    ">
            </label>
            <div if="itemsQuantity" data-bind="text: itemsQuantity" class="admin__action-multiselect-search-count">
            </div>
        </div>
        <!-- /ko -->
        <ul class="admin__action-multiselect-menu-inner _root" data-bind="
        event: {
            mousemove: function(data, event){onMousemove($data, event)}
        }
    ">
            <!-- ko foreach: { data: options, as: 'option'}  -->
            <li class="admin__action-multiselect-menu-inner-item _root" data-bind="css: { _parent: $data.optgroup }" data-role="option-group">
                <div class="action-menu-item" data-bind="
                css: {
                    _selected: $parent.isSelected(option.value),
                    _hover: $parent.isHovered(option, $element),
                    _expended: $parent.getLevelVisibility($data),
                    _unclickable: $parent.isLabelDecoration($data),
                    _last: $parent.addLastElement($data),
                    '_with-checkbox': $parent.showCheckbox
                },
                click: function(data, event){
                    $parent.toggleOptionSelected($data, event);
                },
                clickBubble: false
        ">
                    <!-- ko if: $data.optgroup && $parent.showOpenLevelsActionIcon-->
                    <div class="admin__action-multiselect-dropdown" data-bind="
                    click: function(event){
                        $parent.openChildLevel($data, $element, event);
                    },
                    clickBubble: false
                 ">
                    </div>
                    <!-- /ko-->
                    <!--ko if: $parent.showCheckbox-->
                    <input class="admin__control-checkbox" type="checkbox" tabindex="-1" data-bind="attr: { 'checked': $parent.isSelected(option.value) }">
                    <!-- /ko-->
                    <label class="admin__action-multiselect-label">
                        <span data-bind="text: option.label"></span>
                        <span if="$parent.getPath(option)" class="admin__action-multiselect-item-path" data-bind="text: $parent.getPath(option)"></span>
                    </label>
                </div>
                <!-- ko if: $data.optgroup -->
                <!-- ko template: {name: $parent.optgroupTmpl, data: {root: $parent, current: $data}} -->
                <!-- /ko -->
                <!-- /ko-->
            </li>
            <!-- /ko -->
        </ul>
        <!-- ko if: $data.closeBtn -->
        <div class="admin__action-multiselect-actions-wrap">
            <button class="action-default" data-action="close-advanced-select" type="button" data-bind="click: outerClick">
                <span translate="closeBtnLabel"></span>
            </button>
        </div>
        <!-- /ko -->
    </div>
</di

<strong>Also,</strong> you can use your own  ui-select.js in component as: <strong>Webkul_Custom/js/form/element/ui-select.js</strong>
define([
    'underscore',
    'Magento_Ui/js/form/element/abstract',
    'Magento_Ui/js/lib/key-codes',
    'mage/translate',
    'ko',
    'jquery',
    'Magento_Ui/js/lib/view/utils/async'
], function (_, Abstract, keyCodes, $t, ko, $) {
    'use strict';

    var isTouchDevice = typeof document.ontouchstart !== 'undefined';

    /**
     * Processing options list
     *
     * @param {Array} array - Property array
     * @param {String} separator - Level separator
     * @param {Array} created - list to add new options
     *
     * @return {Array} Plain options list
     */
    function flattenCollection(array, separator, created)
    {
        var i = 0,
            length,
            childCollection;

        array = _.compact(array);
        length = array.length;
        created = created || [];

        for (i; i < length; i++) {
            created.push(array[i]);

            if (array[i].hasOwnProperty(separator)) {
                childCollection = array[i][separator];
                delete array[i][separator];
                flattenCollection.call(this, childCollection, separator, created);
            }
        }

        return created;
    }

    /**
     * Set levels to options list
     *
     * @param {Array} array - Property array
     * @param {String} separator - Level separator
     * @param {Number} level - Starting level
     * @param {String} path - path to root
     *
     * @returns {Array} Array with levels
     */
    function setProperty(array, separator, level, path)
    {
        var i = 0,
            length,
            nextLevel,
            nextPath;

        array = _.compact(array);
        length = array.length;
        level = level || 0;
        path = path || '';

        for (i; i < length; i++) {
            if (array[i]) {
                _.extend(array[i], {
                    level: level,
                    path: path
                });
            }

            if (array[i].hasOwnProperty(separator)) {
                nextLevel = level + 1;
                nextPath = path ? path + '.' + array[i].label : array[i].label;
                setProperty.call(this, array[i][separator], separator, nextLevel, nextPath);
            }
        }

        return array;
    }

    /**
     * Preprocessing options list
     *
     * @param {Array} nodes - Options list
     *
     * @return {Object} Object with property - options(options list)
     *      and cache options with plain and tree list
     */
    function parseOptions(nodes)
    {
        var caption,
            value,
            cacheNodes,
            copyNodes;

        nodes = setProperty(nodes, 'optgroup');
        copyNodes = JSON.parse(JSON.stringify(nodes));
        cacheNodes = flattenCollection(copyNodes, 'optgroup');

        nodes = _.map(nodes, function (node) {
            value = node.value;

            if (value == null || value === '') {
                if (_.isUndefined(caption)) {
                    caption = node.label;
                }
            } else {
                return node;
            }
        });

        return {
            options: _.compact(nodes),
            cacheOptions: {
                plain: _.compact(cacheNodes),
                tree: _.compact(nodes)
            }
        };
    }

    return Abstract.extend({
        defaults: {
            options: [],
            listVisible: false,
            value: [],
            filterOptions: false,
            chipsEnabled: true,
            itemsQuantity: '',
            filterInputValue: '',
            filterOptionsFocus: false,
            multiselectFocus: false,
            multiple: true,
            selectType: 'tree',
            lastSelectable: false,
            showFilteredQuantity: true,
            showCheckbox: true,
            levelsVisibility: true,
            openLevelsAction: true,
            showOpenLevelsActionIcon: true,
            optgroupLabels: false,
            closeBtn: true,
            showPath: true,
            labelsDecoration: false,
            disableLabel: false,
            filterRateLimit: 500,
            closeBtnLabel: $t('Done'),
            optgroupTmpl: 'ui/grid/filters/elements/ui-select-optgroup',
            quantityPlaceholder: $t('options'),
            hoverClass: '_hover',
            rootListSelector: 'ul.admin__action-multiselect-menu-inner._root',
            visibleOptionSelector: 'li.admin__action-multiselect-menu-inner-item:visible',
            actionTargetSelector: '.action-menu-item',
            selectedPlaceholders: {
                defaultPlaceholder: $t('Select...'),
                lotPlaceholders: $t('Selected')
            },
            separator: 'optgroup',
            listens: {
                listVisible: 'cleanHoveredElement',
                filterInputValue: 'filterOptionsList',
                options: 'checkOptionsList'
            },
            presets: {
                single: {
                    showCheckbox: false,
                    chipsEnabled: false,
                    closeBtn: false
                },
                optgroup: {
                    showCheckbox: false,
                    lastSelectable: true,
                    optgroupLabels: true,
                    openLevelsAction: false,
                    labelsDecoration: true,
                    showOpenLevelsActionIcon: false
                }
            }
        },

        /**
         * Initializes UISelect component.
         *
         * @returns {UISelect} Chainable.
         */
        initialize: function () {
            this._super();

            $.async(
                this.rootListSelector,
                this,
                this.onRootListRender.bind(this)
            );

            return this;
        },

        /**
         * Parses options and merges the result with instance
         * Set defaults according to mode and levels configuration
         *
         * @param  {Object} config
         * @returns {Object} Chainable.
         */
        initConfig: function (config) {
            var result = parseOptions(config.options),
                defaults = this.constructor.defaults,
                multiple = _.isBoolean(config.multiple) ? config.multiple : defaults.multiple,
                type = config.selectType || defaults.selectType,
                showOpenLevelsActionIcon = _.isBoolean(config.showOpenLevelsActionIcon) ?
                    config.showOpenLevelsActionIcon :
                    defaults.showOpenLevelsActionIcon,
                openLevelsAction = _.isBoolean(config.openLevelsAction) ?
                    config.openLevelsAction :
                    defaults.openLevelsAction;

            multiple = !multiple ? 'single' : false;
            config.showOpenLevelsActionIcon = showOpenLevelsActionIcon && openLevelsAction;
            _.extend(config, result, defaults.presets[multiple], defaults.presets[type]);
            this._super();

            return this;
        },

        /**
         * Check child optgroup
         */
        hasChildList: function () {
            return _.find(this.options(), function (option) {
                return !!option[this.separator];
            }, this);
        },

        /**
         * Check tree mode
         */
        isTree: function () {
            return this.hasChildList() && this.selectType !== 'optgroup';
        },

        /**
         * Add option to lastOptions array
         *
         * @param {Object} data
         * @returns {Boolean}
         */
        addLastElement: function (data) {
            if (!data.hasOwnProperty(this.separator)) {
                !this.cacheOptions.lastOptions ? this.cacheOptions.lastOptions = [] : false;

                if (!_.findWhere(this.cacheOptions.lastOptions, { value: data.value })) {
                    this.cacheOptions.lastOptions.push(data);
                }

                return true;
            }

            return false;
        },

        /**
         * Check options length and set to cache
         * if some options is added
         *
         * @param {Array} options - ui select options
         */
        checkOptionsList: function (options) {
            if (options.length > this.cacheOptions.plain.length) {
                this.cacheOptions.plain = options;
                this.setCaption();
            }
        },

        /**
         * Check label decoration
         */
        isLabelDecoration: function (data) {
            return data.hasOwnProperty(this.separator) && this.labelsDecoration;
        },

        /**
         * Calls 'initObservable' of parent, initializes 'options' and 'initialOptions'
         *     properties, calls 'setOptions' passing options to it
         *
         * @returns {Object} Chainable.
         */
        initObservable: function () {
            this._super();
            this.observe([
                'listVisible',
                'placeholder',
                'multiselectFocus',
                'options',
                'itemsQuantity',
                'filterInputValue',
                'filterOptionsFocus'
            ]);

            this.filterInputValue.extend({
                rateLimit: this.filterRateLimit
            });

            return this;
        },

        /**
         * object with key - keyname and value - handler function for this key
         *
         * @returns {Object} Object with handlers function name.
         */
        keyDownHandlers: function () {
            return {
                enterKey: this.enterKeyHandler,
                escapeKey: this.escapeKeyHandler,
                spaceKey: this.enterKeyHandler,
                pageUpKey: this.pageUpKeyHandler,
                pageDownKey: this.pageDownKeyHandler
            };
        },

        /**
         * Processing level visibility for levels
         *
         * @param {Object} data - element data
         *
         * @returns {Boolean} level visibility.
         */
        showLevels: function (data) {
            var curLevel = ++data.level,
                isVisible;

            if (data.visible) {
                isVisible = data.visible();
            } else {
                isVisible = !!data.hasOwnProperty(this.separator) &&
                    _.isBoolean(this.levelsVisibility) &&
                    this.levelsVisibility ||
                    data.hasOwnProperty(this.separator) && parseInt(this.levelsVisibility, 10) >= curLevel;

                data.visible = ko.observable(isVisible);
                data.isVisited = isVisible;
            }

            return isVisible;
        },

        /**
         * Processing level visibility for levels
         *
         * @param {Object} data - element data
         *
         * @returns {Boolean} level visibility.
         */
        getLevelVisibility: function (data) {
            if (data.visible) {
                return data.visible();
            }

            return this.showLevels(data);
        },

        /**
         * Set option to options array.
         *
         * @param {Object} option
         * @param {Array} options
         */
        setOption: function (option, options) {
            var copyOptionsTree;

            options = options || this.cacheOptions.tree;

            _.each(options, function (opt) {
                if (opt.value == option.parent) { //eslint-disable-line eqeqeq
                    delete  option.parent;
                    opt[this.separator] ? opt[this.separator].push(option) : opt[this.separator] = [option];
                    copyOptionsTree = JSON.parse(JSON.stringify(this.cacheOptions.tree));
                    this.cacheOptions.plain = flattenCollection(copyOptionsTree, this.separator);
                    this.options(this.cacheOptions.tree);
                } else if (opt[this.separator]) {
                    this.setOption(option, opt[this.separator]);
                }
            }, this);
        },

        /**
         * Handler outerClick event. Closed options list
         */
        outerClick: function () {
            this.listVisible() ? this.listVisible(false) : false;

            if (isTouchDevice) {
                this.multiselectFocus(false);
            }
        },

        /**
         * Handler keydown event to filter options input
         *
         * @returns {Boolean} Returned true for emersion events
         */
        filterOptionsKeydown: function (data, event) {
            var key = keyCodes[event.keyCode];

            !this.isTabKey(event) ? event.stopPropagation() : false;

            if (key === 'pageDownKey' || key === 'pageUpKey') {
                event.preventDefault();
                this.filterOptionsFocus(false);
                this.cacheUiSelect.focus();
            }

            this.keydownSwitcher(data, event);

            return true;
        },

        /**
         * Filtered options list by value from filter options list
         */
        filterOptionsList: function () {
            var value = this.filterInputValue().trim().toLowerCase(),
                array = [];

            if (value && value.length < 2) {
                return false;
            }

            this.cleanHoveredElement();

            if (!value) {
                this.renderPath = false;
                this.options(this.cacheOptions.tree);
                this._setItemsQuantity(false);

                return false;
            }

            this.showPath ? this.renderPath = true : false;

            if (this.filterInputValue()) {
                array = this.selectType === 'optgroup' ?
                    this._getFilteredArray(this.cacheOptions.lastOptions, value) :
                    this._getFilteredArray(this.cacheOptions.plain, value);

                if (!value.length) {
                    this.options(this.cacheOptions.plain);
                    this._setItemsQuantity(this.cacheOptions.plain.length);
                } else {
                    this.options(array);
                    this._setItemsQuantity(array.length);
                }

                return false;
            }

            this.options(this.cacheOptions.plain);
        },

        /**
         * Filtered options list by value from filter options list
         *
         * @param {Array} list - option list
         * @param {String} value
         *
         * @returns {Array} filters result
         */
        _getFilteredArray: function (list, value) {
            var i = 0,
                array = [],
                curOption;

            for (i; i < list.length; i++) {
                curOption = list[i].label.toLowerCase();

                if (curOption.indexOf(value) > -1) {
                    array.push(list[i]); /*eslint max-depth: [2, 4]*/
                }
            }

            return array;
        },

        /**
         * Get path to current option
         *
         * @param {Object} data - option data
         * @returns {String} path
         */
        getPath: function (data) {
            var pathParts,
                createdPath = '';

            if (this.renderPath) {
                pathParts = data.path.split('.');
                _.each(pathParts, function (curData) {
                    createdPath = createdPath ? createdPath + ' / ' + curData : curData;
                });

                return createdPath;
            }
        },

        /**
         * Set filtered items quantity
         *
         * @param {Object} data - option data
         */
        _setItemsQuantity: function (data) {
            if (this.showFilteredQuantity) {
                data || parseInt(data, 10) === 0 ?
                    this.itemsQuantity(data + ' ' + this.quantityPlaceholder) :
                    this.itemsQuantity('');
            }
        },

        /**
         * Remove element from selected array
         */
        removeSelected: function (value, data, event) {
            event ? event.stopPropagation() : false;
            this.value.remove(value);
        },

        /**
         * Checked key name
         *
         * @returns {Boolean}
         */
        isTabKey: function (event) {
            return keyCodes[event.keyCode] === 'tabKey';
        },

        /**
         * Clean hoveredElement variable
         *
         * @returns {Object} Chainable
         */
        cleanHoveredElement: function () {
            if (this.hoveredElement) {
                $(this.hoveredElement)
                    .children(this.actionTargetSelector)
                    .removeClass(this.hoverClass);

                this.hoveredElement = null;
            }

            return this;
        },

        /**
         * Check selected option
         *
         * @param {String} value - option value
         * @return {Boolean}
         */
        isSelected: function (value) {
            return this.multiple ? _.contains(this.value(), value) : this.value() === value;
        },

        /**
         * Check optgroup label
         *
         * @param {Object} data - element data
         * @return {Boolean}
         */
        isOptgroupLabels: function (data) {
            return data.hasOwnProperty(this.separator) && this.optgroupLabels;
        },

        /**
         * Check hovered option
         *
         * @param {Object} data - element data
         * @return {Boolean}
         */
        isHovered: function (data) {
            var element = this.hoveredElement,
                elementData;

            if (!element) {
                return false;
            }

            elementData = ko.dataFor(this.hoveredElement);

            return data.value === elementData.value;
        },

        /**
         * Toggle list visibility
         *
         * @returns {Object} Chainable
         */
        toggleListVisible: function () {
            this.listVisible(!this.listVisible());

            return this;
        },

        /**
         * Get selected element labels
         *
         * @returns {Array} array labels
         */
        getSelected: function () {
            var selected = this.value();

            return this.cacheOptions.plain.filter(function (opt) {
                return _.isArray(selected) ?
                    _.contains(selected, opt.value) :
                selected == opt.value;//eslint-disable-line eqeqeq
            });
        },

        /**
         * Toggle activity list element
         *
         * @param {Object} data - selected option data
         * @returns {Object} Chainable
         */
        toggleOptionSelected: function (data) {
            var isSelected = this.isSelected(data.value);

            if (this.lastSelectable && data.hasOwnProperty(this.separator)) {
                return this;
            }

            if (!this.multiple) {
                if (!isSelected) {
                    this.value(data.value);
                }
                this.listVisible(false);
            } else {
                if (!isSelected) { /*eslint no-lonely-if: 0*/
                    this.value.push(data.value);
                } else {
                    this.value(_.without(this.value(), data.value));
                }
            }

            return this;
        },

        /**
         * Change visibility to child level
         *
         * @param {Object} data - element data
         */
        openChildLevel: function (data) {
            var contextElement = data,
                isVisible;

            if (this.openLevelsAction && data.hasOwnProperty(this.separator) && _.isBoolean(this.levelsVisibility) || this.openLevelsAction && data.hasOwnProperty(this.separator) && parseInt(this.levelsVisibility, 10) <= data.level) {
                isVisible = !contextElement.visible();

                if (isVisible && !contextElement.isVisited) {
                    contextElement.isVisited = true;
                }

                contextElement.visible(isVisible);
            }
        },

        /**
         * Check selected elements
         *
         * @returns {Boolean}
         */
        hasData: function () {
            if (!this.value()) {
                this.value([]);
            }

            return this.value() ? !!this.value().length : false;
        },

        /**
         * @deprecated
         */
        onMousemove: function () {},

        /**
         * Handles hover on list items.
         *
         * @param {Object} event - mousemove event
         */
        onDelegatedMouseMouve: function (event) {
            var target = $(event.currentTarget).closest(this.visibleOptionSelector)[0];

            if (this.isCursorPositionChange(event) || this.hoveredElement === target) {
                return;
            }

            this._hoverTo(target);
            this.setCursorPosition(event);
        },

        /**
         * Get option index
         *
         * @param {Object} data - object with data about this element
         *
         * @returns {Number}
         */
        getOptionIndex: function (data) {
            var index;

            _.each(this.cacheOptions.plain, function (opt, id) {
                if (data.value === opt.value) {
                    index = id;
                }
            });

            return index;
        },

        /**
         * Set X and Y cursor position
         *
         * @param {Object} event - mousemove event
         */
        setCursorPosition: function (event) {
            this.cursorPosition = {
                x: event.pageX,
                y: event.pageY
            };
        },

        /**
         * Check previous and current cursor position
         *
         * @param {Object} event - mousemove event
         * @returns {Boolean}
         */
        isCursorPositionChange: function (event) {
            return this.cursorPosition &&
                this.cursorPosition.x === event.pageX &&
                this.cursorPosition.y === event.pageY;
        },

        /**
         * Set true to observable variable multiselectFocus
         * @param {Object} ctx
         * @param {Object} event - focus event
         */
        onFocusIn: function (ctx, event) {
            !this.cacheUiSelect ? this.cacheUiSelect = event.target : false;
            this.multiselectFocus(true);
        },

        /**
         * Set false to observable variable multiselectFocus
         * and close list
         */
        onFocusOut: function () {
            this.multiselectFocus(false);
        },

        /**
         * Handler enter key, if select list is closed - open select,
         * if select list is open toggle selected current option
         */
        enterKeyHandler: function () {

            if (this.filterOptionsFocus()) {
                return false;
            }

            if (this.listVisible()) {
                if (this.hoveredElement) {
                    this.toggleOptionSelected(ko.dataFor(this.hoveredElement));
                }
            } else {
                this.setListVisible(true);
            }
        },

        /**
         * Handler escape key, if select list is open - closes it,
         */
        escapeKeyHandler: function () {
            this.listVisible() ? this.setListVisible(false) : false;
        },

        /**
         * Handler pageDown key, selected next option in list, if current option is last
         * selected first option in list
         */
        pageDownKeyHandler: function () {
            this._setHoverToElement(1);
        },

        /**
         * Get jQuery element by option data
         *
         * @param {Object} data - option data
         *
         * @returns {Object} jQuery element
         */
        _getElemByData: function (data) {
            var i = 0,
                list = $(this.cacheUiSelect).find('li'),
                length = this.options().length,
                result;

            for (i; i < length; i++) {
                if (this.options()[i].value === data.value) {
                    result = $(list[i]);
                }
            }

            return result;
        },

        /**
         * Set hover to visible element
         *
         * @param {Number} direction - iterator
         */
        _setHoverToElement: function (direction) {
            var element;

            if (direction ===  1) {
                element = this._getNextElement();
            } else if (direction === -1) {
                element = this._getPreviousElement();
            }

            if (element) {
                this._hoverTo(element);
                this._scrollTo(element);
            }
        },

        /**
         * Find current hovered element
         * and change scroll position
         *
         * @param {Number} element - element index
         */
        _scrollTo: function (element) {
            var curEl = $(element).children(this.actionTargetSelector),
                wrapper = $(this.rootList),
                curElPos = {},
                wrapperPos = {};

            curElPos.start = curEl.offset().top;
            curElPos.end = curElPos.start + curEl.outerHeight();

            wrapperPos.start = wrapper.offset().top;
            wrapperPos.end = wrapperPos.start + wrapper.height();

            if (curElPos.start < wrapperPos.start) {
                wrapper.scrollTop(wrapper.scrollTop() - (wrapperPos.start - curElPos.start));
            } else if (curElPos.end > wrapperPos.end) {
                wrapper.scrollTop(wrapper.scrollTop() + curElPos.end - wrapperPos.end);
            }
        },

        /**
         * Handler pageUp key, selected previous option in list, if current option is first -
         * selected last option in list
         */
        pageUpKeyHandler: function () {
            this._setHoverToElement(-1);
        },

        /**
         * Switcher to parse keydown event and delegate event to needful method
         *
         * @param {Object} data - element data
         * @param {Object} event - keydown event
         * @returns {Boolean} if handler for this event doesn't found return true
         */
        keydownSwitcher: function (data, event) {
            var keyName = keyCodes[event.keyCode];

            if (this.isTabKey(event)) {
                if (!this.filterOptionsFocus() && this.listVisible() && this.filterOptions) {
                    this.cacheUiSelect.blur();
                    this.filterOptionsFocus(true);
                    this.cleanHoveredElement();

                    return false;
                }
                this.listVisible(false);

                return true;
            }

            if (this.keyDownHandlers().hasOwnProperty(keyName)) {
                this.keyDownHandlers()[keyName].apply(this, arguments);
            } else {
                return true;
            }
        },

        /**
         * Set caption
         */
        setCaption: function () {
            var length;

            if (!_.isArray(this.value()) && this.value()) {
                length = 1;
            } else if (this.value()) {
                length = this.value().length;
            } else {
                this.value([]);
                length = 0;
            }

            if (length > 1) {
                this.placeholder(length + ' ' + this.selectedPlaceholders.lotPlaceholders);
            } else if (length && this.getSelected().length) {
                this.placeholder(this.getSelected()[0].label);
            } else {
                this.placeholder(this.selectedPlaceholders.defaultPlaceholder);
            }

            return this.placeholder();
        },

        /**
         * Set list status, open or close
         *
         * @param {Boolean} value - variable for set list visible status
         */
        setListVisible: function (value) {
            this.listVisible(value);
        },

        /**
         * Processes preview for option by it's value, and sets the result
         * to 'preview' observable
         *
         * @returns {String}
         */
        getPreview: function () {
            var selected = this.getSelected();

            return selected.map(function (option) {
                return option.label;
            }).join(', ');
        },

        /**
         * Defines previous option element to
         * the one that is currently hovered.
         *
         * @returns {Element}
         */
        _getPreviousElement: function () {
            var currentElement = this.hoveredElement,
                lastElement    = this._getLastIn(this.rootList),
                previousElement;

            if (!currentElement) {
                return lastElement;
            }

            previousElement = $(currentElement).prev()[0];

            return this._getLastIn(previousElement) ||
                previousElement ||
                this._getFirstParentOf(currentElement) ||
                lastElement;
        },

        /**
         * Defines next option element to
         * the one that is currently hovered.
         *
         * @returns {Element}
         */
        _getNextElement: function () {
            var currentElement = this.hoveredElement,
                firstElement   = this._getFirstIn(this.rootList);

            if (!currentElement) {
                return firstElement;
            }

            return this._getFirstIn(currentElement) ||
                $(currentElement).next()[0] ||
                this._getParentsOf(currentElement).next()[0] ||
                firstElement;
        },

        /**
         * Returns first option element in provided scope.
         *
         * @param {Element} scope
         * @returns {Element}
         */
        _getFirstIn: function (scope) {
            return $(scope).find(this.visibleOptionSelector)[0];
        },

        /**
         * Returns last descendant option element in provided scope.
         *
         * @param {Element} scope
         * @returns {Element}
         */
        _getLastIn: function (scope) {
            return $(scope).find(this.visibleOptionSelector).last()[0];
        },

        /**
         * Returns a collection of parent option elements.
         *
         * @param {Element} scope
         * @returns {jQueryCollection}
         */
        _getParentsOf: function (scope) {
            return $(scope).parents(this.visibleOptionSelector);
        },

        /**
         * Returns first parent option element.
         *
         * @param {Element} scope
         * @returns {Element}
         */
        _getFirstParentOf: function (scope) {
            return this._getParentsOf(scope)[0];
        },

        /**
         * Sets hover class to provided option element.
         *
         * @param {Element} element
         */
        _hoverTo: function (element) {
            if (this.hoveredElement) {
                $(this.hoveredElement)
                    .children(this.actionTargetSelector)
                    .removeClass(this.hoverClass);
            }

            $(element)
                .children(this.actionTargetSelector)
                .addClass(this.hoverClass);

            this.hoveredElement = element;
        },

        /**
         * Callback which fires when root list element is rendered.
         *
         * @param {Element} element
         */
        onRootListRender: function (element) {
            var targetSelector = 'li > ' + this.actionTargetSelector;

            this.rootList = element;

            $(this.rootList).on(
                'mousemove',
                targetSelector,
                this.onDelegatedMouseMouve.bind(this)
            );
        }
    });
});


Now, Flush the Cache, and see the result like below image.

Note: You will have to get the CSS from Admin for this field, otherwise it will not look as Admin.

Select UI

Thanks.

. . .

Leave a Comment

Your email address will not be published. Required fields are marked*


6 comments

  • Rav
    • Mahesh Singh (Moderator)
  • vishal
  • Vikas mehar
    • Mahesh Singh (Moderator)
      • Vikas mehar
  • Back to Top

    Message Sent!

    If you have more details or questions, you can reply to the received confirmation email.

    Back to Home