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/vangogh.komma.pro/resources/js/kms/entities/sortable.js
/**
 * Fills an ul element with data retrieved from an api and makes the items draggable so that you can sort them.
 * Also updates the api with the new positions of the items. The data to and from the api has this structure for example.
 *
 * [
 *  {
 *      id:1
 *      title:false
 *      thumbnail:false
 *      status: "active"
 *      routes: []
 *      children: [
 *          {
 *              id: 12
 *              title: "Thuis"
 *              thumbnail: false
 *              routes: [{
 *                  40: "en/Homenew",
 *                  104: "nl/Thuisnew"
 *              }]
 *          }
 *      ]
 *  }
 * ]
 *
 */
class SortableController {
    constructor(wrapper) {
        if(!wrapper) {
            console.error('SortableController: Expected a wrapper but did not get any. Sortable controller stopped working');
            return;
        }
        this._wrapper = wrapper;

        this._list = this._wrapper.querySelector('ul');
        if(!this._list) {
            console.error('SortableController: Expected the wrapper to have an ul. But did not have any. Sortable controller stopped working');
            return;
        }

        if(!("slug" in this._list.dataset)) {
            console.error('Make sure that the wrapper contains the data-slug attribute that contains the slug. Sortable controller stopped working.');
            return;
        }
        this.slug = this._list.dataset.slug;

        this._apiUrl = '/kms/api/'+this.slug;
        this._editEntitiesUrl = '/kms/'+this.slug;

        this._disabled = true;
        this._listItemClass = 'entities-list-item';

        this._enableSortButton = this._wrapper.querySelector('.entities-order .sortable-button.enable-sortable');
        if(!this._enableSortButton) {
            console.error('SortableController: The sort button could not be found inside the wrapper.', this._wrapper, 'Selector: ".entities-order .sortable-button .enable-sortable". Sortable controller stopped working');
            return;
        }

        this._saveSortOrderButton = this._wrapper.querySelector('.entities-order .sortable-button.save-order');
        if(!this._saveSortOrderButton) {
            console.error('SortableController: The sort button could not be found inside the wrapper.', this._wrapper, 'Selector: ".entities-order .sortable-button .save-order". Sortable controller stopped working');
            return;
        }

        this._activeId = this._list.dataset.activeId;
        axios.defaults.headers.common["X-Requested-With"] = "XMLHttpRequest";

        this._updateVisibility(this._list);


        this.enableSortable = this.enableSortable.bind(this); //needed to set the correct "this" in the method
        this.disableSortable = this.disableSortable.bind(this); //needed to set the correct "this" in the method

        this._setSortButtonsEnabled(true);
    }

    /**
     * Enables or disables the sort buttons. The ones that you can
     * click to enable sorting, and saving it afterwards
     *
     * @param enabled
     * @private
     */
    _setSortButtonsEnabled(enabled)
    {
        this._enableSortButton.removeEventListener('click', this.enableSortable);
        this._saveSortOrderButton.removeEventListener('click', this.disableSortable);

        if(enabled) {
            this._enableSortButton.addEventListener('click', this.enableSortable);
            this._saveSortOrderButton.addEventListener('click', this.disableSortable);
        }
    }

    /**
     * Does look at the list, and loops over all list items that have sub lists.
     * When they have, it looks at the display property style state of those list items, and updates the ul children to
     * match that display property state
     *
     * {HTMLUListElement}
     * @private
     */
    _updateVisibility(list)
    {
        if(list.tagName !== 'UL') {
            console.error('sortableController: Could not update the unordered list diplay style state since the list is no <ul> tag')
        }

        let childrenThatHaveSubUl = list.querySelectorAll("li ul");
        const childrenThatHaveSubUlLength = childrenThatHaveSubUl.length;

        for(let i = 0; i < childrenThatHaveSubUlLength; i++){
            let child = childrenThatHaveSubUl[i];
            child.style.display = child.style.display !== 'none' ? 'none' : '';

            //Check if the children of this child must also be updated
            let childChildren = child.children;
            let childChildrenCount = childChildren.length;
            for(let x = 0; x < childChildrenCount; x++)
            {
                let childChild = child.children[x];
                if(childChild.tagName === 'UL') {
                    this._updateVisibility(childChild);
                }
            }
        }
    }

    /**
     * Makes sure that all items that have class .sortable are now also are sortable
     *
     * @private
     */
    _updateSortableJavascript()
    {
        // console.log("Updating all sortable elements with these selectors: '" + this.selector+" .sortable' AND '"+this.selector+"'");

        $('.sortable').sortable({
            connectWith: ".sortable",
            placeholder: "sortable-placeholder",
            disabled: this._disabled
        });
    }

    /**
     * Makes the rootUl sortable
     */
    enableSortable() {
        this._enableSortButton.classList.remove('show');
        this._saveSortOrderButton.classList.add('show');

        this._disabled = false;
        $(this._wrapper).sortable({
            disabled: this._disabled
        }).addClass('sorting');


        this._updateSortableJavascript();
    }

    /**
     * Disables the rootUl so that it cannot be sorted
     */
    disableSortable() {
        this._enableSortButton.classList.add('show');
        this._saveSortOrderButton.classList.remove('show');

        this._disabled = true;
        $(this._wrapper).sortable({
            disabled: this._disabled
        }).removeClass('sorting');

        this._save();
    }

    /**
     * Saves the item data to the api
     */
    _save() {
        let itemsJson = this._itemsToJson(this._list);

        let rootItem = {
          id: 1,
          routes: {},
          status: null,
          thumbnail: false,
          title: null,
          children: itemsJson
        };

        itemsJson = JSON.stringify(rootItem);
        let apiJson = {
          "tree": itemsJson
        };

        axios.post(this._apiUrl, apiJson).then((response) => {
            // console.log('Successfully stored the sort order to the api: ');
            // console.log(response);
        }).catch((error) => {
            // console.error('Could not save sortable sort order to api because an error occured: ');
            console.error(error);
        });
    }

    /**
     * Converts the rootUl back to json for saving it
     *
     * @param htmlElement HTMLElement Root ul
     * @return null|[] if something went wrong null, or an array containing json items if successful.
     * @private
     */
    _itemsToJson(htmlElement) {
        let jsonArray = [];
        let error = false;

        //find all child li items
        let listItems = htmlElement.querySelectorAll('li.'+this._listItemClass);

        const listItemsLength = listItems.length;
        for(let i = 0; i < listItemsLength; i++){
            let listItem = listItems[i];

            //Skip children of children because they will be processed in the recursive _itemsToJson call down here.
            if(listItem.parentElement !== htmlElement) continue;

            let elementJson = listItem.dataset.json;
            if(!elementJson)
            {
                console.error('One or more li HTMLElements with class "'+this._listItemClass+'" did not have data-json attribute set while it should.');
                error = true;
            }

            let currentItemJson = JSON.parse(listItem.dataset.json);
            let childUl = listItem.querySelector('ul');
            if(childUl)
            {
                currentItemJson.children = this._itemsToJson(childUl);
                if(!currentItemJson.children) error = true;
            } else {
                currentItemJson.children = [];
            }

            jsonArray.push(currentItemJson);
        }

        if(error) return null;

        return jsonArray;
    }

    /**
     * Returns a promise that resolves with the root ul that then will contain the items retrieved from the api
     * @private
     */
    load() {
        self = this;

        return new Promise(function (resolve, reject) {
            axios.get(self._apiUrl).then((response) => {
                if (!response.data || response.data.length == 0) {
                    reject('The sortable did not get any data from the api');
                    return;
                }

                const childrenLength = response.data.children.length;
                for(let i = 0; i < childrenLength; i++){
                    const itemObject = response.data.children[i];
                    self._list.appendChild( self._createHtmlElement(itemObject) );
                }

                self._updateSortableJavascript();

                resolve(self._list);
            }).catch((error) => {
                reject(error);
            });
        });
    }

    /**
     * Removes all children from the ul
     */
    clearList()
    {
        while(this._list.firstChild) this._list.removeChild(this._list.firstChild);
    }

    /**
     * Creates a menu item (HTMLElement) and sub menu items if necessary
     *
     * @param data
     * @returns {HTMLElement}
     * @private
     */
    _createHtmlElement(data)
    {
        self = this;

        let activeId = self._activeId;

        let id = data.id;
        let title = data.title || '-';
        let thumbnail = data.thumbnail;
        let children = data.children;
        let status = data.status;
        let routes = data.routes;

        //Generate json data representing that element
        let routesForElement = {};
        for (let routeProperty in routes) {
            if(!routes.hasOwnProperty(routeProperty)) continue;

            routesForElement[routeProperty] = routes[routeProperty];
        }
        if(routesForElement == {}) routesForElement = [];


        let elementJson = {
            id: data.id,
            title: data.title,
            thumbnail: data.thumbnail,
            routes: routesForElement,
            status: data.status
        };
        elementJson = JSON.stringify(elementJson);

        //Render all child html items first
        let childItems = [];
        let childrenLength = children.length;
        for(let index = 0; index < childrenLength; index++)
        {
            let obj = children[index];
            childItems.push(this._createHtmlElement(obj));
        }

        //Displays a red or green line in front of the item depending on if the status (class) is active or not
        let colorStatusHtml = `
            <span class="color-status" data-status-type="${status}">
                &nbsp;
            </span>
        `;

        //Displays the items icon thumbnail OR the first character of its title
        let iconHtml = `
            <div class="icon">
                ${thumbnail ? thumbnail : `<span>${title ? title.substring(0,1): ``}</span>`}
            </div>
        `;

        //The main item html that has all other items
        let listIem = `
            <li data-json='${elementJson}' class="${this._listItemClass} ${ id == activeId ? 'active': '' }"> 
                <a href="${this._editEntitiesUrl}/${id}">
                    ${colorStatusHtml}
                    ${iconHtml}
                    <p>${title}</p>
                </a>
            </li> 
        `;

        //Render it to a real html element
        let domParser = new DOMParser();
        let document = domParser.parseFromString(listIem, "text/html");
        let node = document.body.firstChild;

        const childItemsLength = childItems.length;

        //And add the children inside
        if(childItemsLength > 0)
        {
            let subList = document.createElement('ul');
            subList.className = 'sortable';

            // console.log('rendered childItems');
            // console.log(childItems);

            for(let index = 0; index < childItemsLength; index++)
            {
                let element = childItems[index];
                subList.appendChild(element);
            }

            node.appendChild(subList);
        }

        //and return it
        return node;
    }
}