HEX
Server: Microsoft-IIS/8.5
System: Windows NT YDAWBH120 6.3 build 9600 (Windows Server 2012 R2 Standard Edition) AMD64
User: tentjecom_web (0)
PHP: 7.4.14
Disabled: NONE
Upload Files
File: D:/HostingSpaces/SBogers10/carrot.komma.pro/resources/js/kms/attributes/multiSelect.js
class MultiSelect {
    constructor(attributeWrapper) {
        this.attributeKey = attributeWrapper.dataset.key;
        this.value = attributeWrapper.querySelector('[name="'+this.attributeKey+'"]').value;

        /**Select all elements for easy access later on**/
        this.uiWidget = attributeWrapper.querySelector('.ui-widget');
        this.input = document.getElementById(this.attributeKey);
        this.openbutton = document.getElementById(this.attributeKey + "-open");
        this.autoCompleteSelectField = document.getElementById(this.attributeKey + "-fake");
        this.itemsWrapper = document.getElementById(this.attributeKey+"_items");

        this.autoSaveUrl = attributeWrapper.dataset.autosaveUrl;
        this.maxItemsToSelect = attributeWrapper.dataset.maxItemsToSelect;
        this.selectOptions = MultiSelect._buildSelectOptionsFromObject(JSON.parse(attributeWrapper.dataset.items));
        this.dataSet = MultiSelect._buildDataSetFromSelectOptions(this.selectOptions);

        this.sortable = attributeWrapper.dataset.sortable;

        if(this.sortable) {

            let self = this;
            this.dragElement = null;
            // Sorting starts
            this.itemsWrapper.addEventListener('dragstart', function (evt){
                self.dragElement = evt.target; // Remembering an element that will be moved

                // Limiting the movement type
                evt.dataTransfer.effectAllowed = 'move';
                evt.dataTransfer.setData('Text', self.dragElement.textContent);

                // Subscribing to the events at dnd
                self.itemsWrapper.addEventListener('dragover', self._onDragOver.bind(self), false);
                self.itemsWrapper.addEventListener('dragend', self._onDragEnd.bind(self), false);

                setTimeout(function () {
                    // If this action is performed without setTimeout, then
                    // the moved object will be of this class.
                    self.dragElement.classList.add('ghost');
                }, 0)
            }, false);
        }

        this._makeMultiSelect();
        this._addItems();
        this._updateRealInput();
        this._enableListeners(true);
        this._enableOpenButton();
    }

    static _buildSelectOptionsFromObject(data) {
        let selectOptions = [];

        let itemsCount = data.length;
        for(let index = 0; index < itemsCount; index++) {
            let selectOption = SelectOption.fromObject(data[index]);
            if(selectOption) selectOptions.push(selectOption);
        }

        return selectOptions;
    }

    /**
     * @param {SelectOption[]}selectOptions
     * @private
     */
    static _buildDataSetFromSelectOptions(selectOptions) {
        let dataset = [];
        let selectoptionsCount = selectOptions.length;
        for(let index = 0; index < selectoptionsCount; index++) {
            /** @var {SelectOption} selectOption **/
            dataset.push({
                id: selectOptions[index].value,
                value: selectOptions[index].htmlContent.replace(/&nbsp;/gi, '.')
            });
        }

        return dataset;
    }


    _enableListeners(enable) {
        this.listenersEnabled = enable;

        let length = this.itemsWrapper.children.length;

        //Add / remove listeners to items
        for (let i = 0; i < length; i++) {
            let item = this.itemsWrapper.children[i];
            let id = item.dataset.id;

            item.classList.remove('readonly');
            this.autoCompleteSelectField.classList.remove('hidden');

            if(!enable) {
                item.classList.add('readonly');
                this.autoCompleteSelectField.classList.add('hidden');
                this.openbutton.classList.add('hidden');
            }
        }
    }

    _addItems()
    {
        let valuesFromAttribute = this.value.split(',');
        let datasetLength = this.dataSet.length;
        for (let valuesIndex = 0; valuesIndex < valuesFromAttribute.length; valuesIndex++) {
            let idFromAttribute = parseInt(valuesFromAttribute[valuesIndex]);
            for (let itemIndex = 0; itemIndex < datasetLength; itemIndex++) {
                let item = this.dataSet[itemIndex];
                if (item.id === idFromAttribute) {
                    this._addItem(item.id, item.value);
                    break;
                }
            }
        }
    };

    /**
     * Makes the fake input field and autocomplete input field.
     * Focus method is triggered when you hover over items in the menu
     * select method is triggered when you click an item in the list.
     */
    _makeMultiSelect() {
        let self = this;

        let element = document.getElementById(this.attributeKey+"-fake");
        $(element).autocomplete({
            source: this.dataSet,
            minLength: 0,

            focus: function (event, ui) {
                return false;
            },
            select: function (event, ui) {
                $(this.autoCompleteSelectField).val("");
                self._addItem(ui.item.id, ui.item.value);
                self._updateRealInput();
                self._autosaveIfNeeded();
                self._enableListeners(true);
                return false;
            },
        });
    };


    /**
     * Adds a span tag containing the value given and the data-id property set to id given to
     * the p tag with class items.
     */
    _addItem(id, value) {
        let self = this;
        if(this._itemsCount() >= self.maxItemsToSelect) return;

        let itemElement = document.createElement('p');
        itemElement.classList.add('item');

        if(this.sortable) {
            itemElement.draggable = true;
            value = '<span class="drag-icon"></span><span class="text">'+value+'</span>';
        }

        itemElement.innerHTML = value;

        let removeButton = document.createElement('span');
        removeButton.classList.add('remove');
        itemElement.dataset.id = id;

        itemElement.dataset.sort_order = this._itemsCount()+1;

        removeButton.addEventListener('click', (function (itemToRemove) {
            return function() {
                if(!self.listenersEnabled) return;
                self._removeItem(itemToRemove);
            }
        })(id));

        this.itemsWrapper.appendChild(itemElement);
        itemElement.appendChild(removeButton);

        if(this._itemsCount() >= self.maxItemsToSelect) {
            this.uiWidget.style.display = "none";
        }

    };

    /**
     * Make sure we can open the autocomplete field dropdown with the open button
     */
    _enableOpenButton() {
        let self = this;

        this.openbutton.addEventListener('click', function(event) {
            $(self.autoCompleteSelectField).autocomplete("search", "");
        });
    };



    _itemsCount()
    {
        return this.itemsWrapper.childNodes.length-1;
    };

    /**
     * Iterates over the child elements of the p element with class items and removes the element that has
     * data-id set to the specified id. Then calls _updateRealInput to update the hidden input
     */
    _removeItem(id) {
        let items = this.itemsWrapper.children;
        for (let index = 0; index < items.length; index++) {
            let item = items[index];
            if (parseInt(item.dataset.id) === id) {
                this.itemsWrapper.removeChild(item);

                if(this._itemsCount() <= this.maxItemsToSelect) {
                    this.uiWidget.style.display = "block";
                }
                break;
            }
        }
        this._updateRealInput();
        this._autosaveIfNeeded();
    };

    /**
     * Iterates over the child elements of the p element with class items and concatenates their data-id
     * values into a string. After that it sets the hidden input with the id string
     */
    _updateRealInput() {
        let ids = [];
        let items = this.itemsWrapper.children;
        let length = this.itemsWrapper.children.length;
        for (let i = 0; i < length; i++) {
            let item = items[i];
            ids[ids.length] = item.dataset.id;
        }
        let idString = ids.join(',');

        this.input.setAttribute('value', idString);

        let changeEvent = document.createEvent("Event");
        changeEvent.initEvent("change", false, true);
        // args: string type, boolean bubbles, boolean cancelable
        this.input.dispatchEvent(changeEvent)
    };

    _autosaveIfNeeded() {
        if (!this.autoSaveUrl) return;
        let itemIds = this.input.getAttribute('value');
        let self = this;

        $.ajax({
            type: "POST",
            url: this.autoSaveUrl,
            data: {
                'itemIds': itemIds,
            },
            success: function (response, textStatus, xhr) {
                if (xhr.status === 200) {
                    console.info('autosave successfull to url: ' + self.autoSaveUrl)
                } else {
                    console.error('autosave failure to url (no code 200): ' + self.autoSaveUrl + '');
                    console.error(response)
                }
            },
            error: function (message) {
                console.error('autosave failure to url: ' + self.autoSaveUrl + '');
                console.error(message)
            },
            headers: {
                'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
            },
            dataType: 'json'
        });
    };

    _onDragOver(evt) {
        evt.preventDefault();
        evt.dataTransfer.dropEffect = 'move';

        let target = evt.target;
        if (target && target !== this.dragElement && target.nodeName === 'P') {
            // Sorting
            this.itemsWrapper.insertBefore(this.dragElement, target.nextSibling || target);
        }
    }

    _onDragEnd(evt){
        evt.preventDefault();

        this.dragElement.classList.remove('ghost');
        this.itemsWrapper.removeEventListener('dragover', this.onDragOver, false);
        this.itemsWrapper.removeEventListener('dragend', this.onDragEnd, false);
        this._updateRealInput();
    }
}


/**
 * Matches it's php twin functionality
 */
class SelectOption {
    constructor()
    {
        this._value = '';
        this._content = '';
        this._htmlContent = '';
        this.toJSON = this._toJson.bind(this);
    }

    _toJson()
    {
        return {
            value: this._value,
            content: this._content,
            htmlContent: this._htmlContent,
        }
    }

    static fromObject(data) {
        if(!this._objectRepresentsSelectOption(data)) return;

        let instance = new this;
        instance._value = data.value;
        instance._content = data.content;
        instance._htmlContent = data.htmlContent;

        return instance;
    }

    static _objectRepresentsSelectOption(data)
    {
        if (
            typeof data !== 'object' ||
            !data.hasOwnProperty('value') ||
            !data.hasOwnProperty('content') ||
            !data.hasOwnProperty('htmlContent')
        ) {
            console.error('SelectOption::_objectRepresentsSelectOption: This does not represent an select option. It must be an object containing properties: value, content and htmlContent', data); return false;
        }

        return true;
    }

    get value() {
        return this._value;
    }

    set value(value) {
        if(typeof value !== 'string') { console.error('SelectOption::Value must be a string'); return; }
        this._value = value;
    }

    get content() {
        return this._content;
    }

    set content(value) {
        if(typeof value !== 'string') { console.error('SelectOption::Content must be a string'); return; }
        this._content = value;
    }

    get htmlContent() {
        return this._htmlContent;
    }

    set htmlContent(value) {
        if(typeof value !== 'string') { console.error('SelectOption::htmlContent must be a string'); return }
        this._htmlContent = value;
    }
}