0

I add a custom admin filed in custom form, it's a multi-select category field. I just want to make this field required.

phtml

<div class="field required">
    <label class="label"><?php echo __('Product Category') ?>:</label>
    <?php
    if($product_hint_status && $helper->getProductHintCategory()){?>
        <img src="<?php echo $this->getViewFileUrl('Webkul_Marketplace::images/quest.png'); ?>" class='questimg' title="<?php echo $helper->getProductHintCategory() ?>"/>
    <?php
    } ?>
    <?php if ($helper->getIsAdminViewCategoryTree()) { ?>
        <div data-bind="scope: 'sellerCategory'">
            <!-- ko template: getTemplate() --><!-- /ko -->
        </div>
        <script type="text/x-magento-init">
            {
                "*": {
                    "Magento_Ui/js/core/app": {
                        "components": {
                            "sellerCategory": {
                                "component": "Webkul_Marketplace/js/product/seller-category-tree",
                                "template" : "Webkul_Marketplace/seller-category-tree",
                                "filterOptions": true,
                                "levelsVisibility": "1",
                                "options": <?php echo $block->getCategoriesTree()?>,
                                "value": <?php echo json_encode($data['product']['category_ids'])?>
                            }
                        }
                    }
                }
            }
        </script>
</div>

html

<div
class="admin__action-multiselect-wrap action-select-wrap _required"
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 _required"
    data-role="advanced-select"
    data-bind="
    css: {_active: multiselectFocus},
    click: function(data, event) {
        toggleListVisible(data, event)
    }
">
<div class="admin__action-multiselect-text _required"
     data-role="selected-option"
     data-bind="text: setCaption()">
</div>
</div>
<!-- /ko -->
<!-- ko if: chipsEnabled -->
<div
    class="action-select admin__action-multiselect _required"
    data-role="advanced-select"
    data-bind="
    css: {_active: multiselectFocus},
    click: function(data, event) {
        toggleListVisible(data, event)
    }
">
<div class="admin__action-multiselect-text _required"
     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>
    <input type="hidden" 
        name="product[category_ids][]" 
        data-bind="
        attr: {
            id: 'wk-cat-hide'+value,
            value: value
        }
    ">
<!-- /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 _required"
                    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>
</div>

js

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
            });
        }
    });
});

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.