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}">
</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;
}
}