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/edwingovers.komma.pro/resources/assets/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(selector) {
        this.apiUrl = '';
        this.editEntitiesUrl = '';
        this.initialized = false;
        this.selector = selector;
        this.disabled = true;

        this.listItemClass = 'entities-list-item';

        let rootUl = document.querySelector(this.selector);
        this.siteSlug = rootUl.dataset.siteSlug;
        this.slug = rootUl.dataset.slug;
        this.activeId = rootUl.dataset.activeId;
    }

    /**
     * Initializes the controller so that it knows where it can get its data from,
     * where it needs to direct users when they click on an item and from which ul it
     * should create a sortable ul
     *
     * @param apiUrl A string
     * @param editEntitiesUrl A string
     */
    init(apiUrl, editEntitiesUrl) {
        this.apiUrl = apiUrl;
        this.editEntitiesUrl = editEntitiesUrl;

        let itemsThatHaveSubUl = document.querySelectorAll(this.selector+" li ul");
        itemsThatHaveSubUl.forEach((node, index) => {
            // console.log('clicked ul');
            node.style.display = node.style.display !== 'none' ? 'none' : '';
        });

        this.initialized = true;
    }

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

        let itemsThatHaveSubUl = document.querySelectorAll(this.selector+" li ul");
        itemsThatHaveSubUl.forEach((node, index) => {
            // console.log('clicked ul');
            // node.style.display = node.style.display !== 'none' ? 'none' : '';
        });
    }

    /**
     * Makes the rootUl sortable
     */
    enableSortable() {
        if (this.initialized === false) { console.error('Please initialize the controller with the init method first.'); return; }

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


        this.updateSortableJavascript();
    }

    /**
     * Diables the rootUl so that it cannot be sorted
     */
    disableSortable() {
        // if (this.initialized === false) { console.error('Please initialize the controller with the init method first.'); return; }

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

        this.save();
    }

    /**
     * Saves the item data to the api
     */
    save() {
        if (this.initialized === false) { console.error('Please initialize the controller with the init method first.'); return; }
        let itemsJson = this.itemsToJson(document.querySelector(this.selector));

        itemsJson = JSON.stringify(itemsJson);
        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 successfull.
     */
    itemsToJson(htmlElement) {
        if (this.initialized === false) { console.error('Please initialize the controller with the init method first.'); return; }

        let jsonArray = [];
        let error = false;

        //find all child li items
        let listItems = htmlElement.querySelectorAll(':scope > li.'+this.listItemClass);
        for(let listItem of listItems)
        {
            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
     */
    load() {
        if (this.initialized === false) { console.error('Please initialize the controller with the init method first.'); return; }

        self = this;


        return new Promise(function (resolve, reject) {
            if (self.initialized === false) { reject('Please initialize the controller with the init method first.') }

            axios.get(self.apiUrl).then((response) => {
                if (!response.data) {
                    reject('The sortable did not get any data from the api');
                    return;
                }

                /** @var HTMLElement[] menuItems*/
                let menuItems = [];
                response.data[0].children.forEach(function (itemObject) {
                    menuItems.push(self.createHtmlElement(itemObject));
                });

                menuItems.forEach(function (htmlElementItem) {
                    document.querySelector(self.selector).appendChild(htmlElementItem);
                });

                self.updateSortableJavascript();

                resolve(document.querySelector(self.selector));
            }).catch((error) => {
                reject(error);
            });
        });
    }

    /**
     * Removes all children from the ul
     */
    clearRootUl()
    {
        if (this.initialized === false) { console.error('Please initialize the controller with the init method first.'); return; }

        let rootUl = document.querySelector(this.selector);
        while(rootUl.firstChild) rootUl.removeChild(rootUl.firstChild);
    }

    /**
     * Creates a menu item (HTMLElement) and sub menu items if necessary
     *
     * @param data
     * @returns {HTMLElement}
     */
    createHtmlElement(data)
    {
        if (this.initialized === false) { console.error('Please initialize the controller with the init method first.'); return; }

        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 = [];
        for(let object of children)
        {
            childItems.push(this.createHtmlElement(object));
        }

        //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 ? `style="background-image: url('${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;

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

            // console.log('rendered childItems');
            // console.log(childItems);
            childItems.forEach(function(element) {
                subList.appendChild(element);
            });

            node.appendChild(subList);
        }

        //and return it
        return node;
    }
}