Replaced jquery sortable with sortablejs

This commit is contained in:
Dan Brown
2019-06-06 13:09:58 +01:00
parent 2eba8c611e
commit d87eb277dd
8 changed files with 214 additions and 194 deletions

View File

@ -0,0 +1,204 @@
import Sortable from "sortablejs";
// Auto sort control
const sortOperations = {
name: function(a, b) {
const aName = a.getAttribute('data-name').trim().toLowerCase();
const bName = b.getAttribute('data-name').trim().toLowerCase();
return aName.localeCompare(bName);
},
created: function(a, b) {
const aTime = Number(a.getAttribute('data-created'));
const bTime = Number(b.getAttribute('data-created'));
return bTime - aTime;
},
updated: function(a, b) {
const aTime = Number(a.getAttribute('data-updated'));
const bTime = Number(b.getAttribute('data-updated'));
return bTime - aTime;
},
chaptersFirst: function(a, b) {
const aType = a.getAttribute('data-type');
const bType = b.getAttribute('data-type');
if (aType === bType) {
return 0;
}
return (aType === 'chapter' ? -1 : 1);
},
chaptersLast: function(a, b) {
const aType = a.getAttribute('data-type');
const bType = b.getAttribute('data-type');
if (aType === bType) {
return 0;
}
return (aType === 'chapter' ? 1 : -1);
},
};
class BookSort {
constructor(elem) {
this.elem = elem;
this.sortContainer = elem.querySelector('[book-sort-boxes]');
this.input = elem.querySelector('[book-sort-input]');
const initialSortBox = elem.querySelector('.sort-box');
this.setupBookSortable(initialSortBox);
this.setupSortPresets();
window.$events.listen('entity-select-confirm', this.bookSelect.bind(this));
}
/**
* Setup the handlers for the preset sort type buttons.
*/
setupSortPresets() {
let lastSort = '';
let reverse = false;
const reversibleTypes = ['name', 'created', 'updated'];
this.sortContainer.addEventListener('click', event => {
const sortButton = event.target.closest('.sort-box-options [data-sort]');
if (!sortButton) return;
event.preventDefault();
const sortLists = sortButton.closest('.sort-box').querySelectorAll('ul');
const sort = sortButton.getAttribute('data-sort');
reverse = (lastSort === sort) ? !reverse : false;
let sortFunction = sortOperations[sort];
if (reverse && reversibleTypes.includes(sort)) {
sortFunction = function(a, b) {
return 0 - sortOperations[sort](a, b)
};
}
for (let list of sortLists) {
const directItems = Array.from(list.children).filter(child => child.matches('li'));
directItems.sort(sortFunction).forEach(sortedItem => {
list.appendChild(sortedItem);
});
}
lastSort = sort;
this.updateMapInput();
});
}
/**
* Handle book selection from the entity selector.
* @param {Object} entityInfo
*/
bookSelect(entityInfo) {
const alreadyAdded = this.elem.querySelector(`[data-type="book"][data-id="${entityInfo.id}"]`) !== null;
if (alreadyAdded) return;
const entitySortItemUrl = entityInfo.link + '/sort-item';
window.$http.get(entitySortItemUrl).then(resp => {
const wrap = document.createElement('div');
wrap.innerHTML = resp.data;
const newBookContainer = wrap.children[0];
this.sortContainer.append(newBookContainer);
this.setupBookSortable(newBookContainer);
});
}
/**
* Setup the given book container element to have sortable items.
* @param {Element} bookContainer
*/
setupBookSortable(bookContainer) {
const sortElems = [bookContainer.querySelector('.sort-list')];
sortElems.push(...bookContainer.querySelectorAll('.entity-list-item + ul'));
const bookGroupConfig = {
name: 'book',
pull: ['book', 'chapter'],
put: ['book', 'chapter'],
};
const chapterGroupConfig = {
name: 'chapter',
pull: ['book', 'chapter'],
put: function(toList, fromList, draggedElem) {
return draggedElem.getAttribute('data-type') === 'page';
}
};
for (let sortElem of sortElems) {
new Sortable(sortElem, {
group: sortElem.classList.contains('sort-list') ? bookGroupConfig : chapterGroupConfig,
animation: 150,
fallbackOnBody: true,
swapThreshold: 0.65,
onSort: this.updateMapInput.bind(this),
dragClass: 'bg-white',
ghostClass: 'primary-background-light',
});
}
}
/**
* Update the input with our sort data.
*/
updateMapInput() {
const pageMap = this.buildEntityMap();
this.input.value = JSON.stringify(pageMap);
}
/**
* Build up a mapping of entities with their ordering and nesting.
* @returns {Array}
*/
buildEntityMap() {
const entityMap = [];
const lists = this.elem.querySelectorAll('.sort-list');
for (let list of lists) {
const bookId = list.closest('[data-type="book"]').getAttribute('data-id');
const directChildren = Array.from(list.children)
.filter(elem => elem.matches('[data-type="page"], [data-type="chapter"]'));
for (let i = 0; i < directChildren.length; i++) {
this.addBookChildToMap(directChildren[i], i, bookId, entityMap);
}
}
return entityMap;
}
/**
* Parse a sort item and add it to a data-map array.
* Parses sub0items if existing also.
* @param {Element} childElem
* @param {Number} index
* @param {Number} bookId
* @param {Array} entityMap
*/
addBookChildToMap(childElem, index, bookId, entityMap) {
const type = childElem.getAttribute('data-type');
const parentChapter = false;
const childId = childElem.getAttribute('data-id');
entityMap.push({
id: childId,
sort: index,
parentChapter: parentChapter,
type: type,
book: bookId
});
const subPages = childElem.querySelectorAll('[data-type="page"]');
for (let i = 0; i < subPages.length; i++) {
entityMap.push({
id: subPages[i].getAttribute('data-id'),
sort: i,
parentChapter: childId,
type: 'page',
book: bookId
});
}
}
}
export default BookSort;

View File

@ -24,6 +24,7 @@ import triLayout from "./tri-layout";
import breadcrumbListing from "./breadcrumb-listing";
import permissionsTable from "./permissions-table";
import customCheckbox from "./custom-checkbox";
import bookSort from "./book-sort";
const componentMapping = {
'dropdown': dropdown,
@ -52,6 +53,7 @@ const componentMapping = {
'breadcrumb-listing': breadcrumbListing,
'permissions-table': permissionsTable,
'custom-checkbox': customCheckbox,
'book-sort': bookSort,
};
window.components = {};

View File

@ -59,8 +59,11 @@
}
/*
* Entity background colors
* Standard & Entity background colors
*/
.bg-white {
background-color: #FFFFFF;
}
.bg-book {
background-color: $color-book;
}

View File

@ -16,16 +16,16 @@
<div class="grid left-focus gap-xl">
<div>
<div class="card content-wrap">
<div book-sort class="card content-wrap">
<h1 class="list-heading mb-l">{{ trans('entities.books_sort') }}</h1>
<div id="sort-boxes">
<div book-sort-boxes>
@include('books.sort-box', ['book' => $book, 'bookChildren' => $bookChildren])
</div>
<form action="{{ $book->getUrl('/sort') }}" method="POST">
{!! csrf_field() !!}
<input type="hidden" name="_method" value="PUT">
<input type="hidden" id="sort-tree-input" name="sort-tree">
<input book-sort-input type="hidden" name="sort-tree">
<div class="list text-right">
<a href="{{ $book->getUrl() }}" class="button outline">{{ trans('common.cancel') }}</a>
<button class="button primary" type="submit">{{ trans('entities.books_sort_save') }}</button>
@ -47,157 +47,3 @@
</div>
@stop
@section('scripts')
<script src="{{ baseUrl("/libs/jquery-sortable/jquery-sortable.min.js") }}"></script>
<script>
$(document).ready(function() {
const $container = $('#sort-boxes');
// Sortable options
const sortableOptions = {
group: 'serialization',
containerSelector: 'ul',
itemPath: '',
itemSelector: 'li',
onDrop: function ($item, container, _super) {
updateMapInput();
_super($item, container);
},
isValidTarget: function ($item, container) {
// Prevent nested chapters
return !($item.is('[data-type="chapter"]') && container.target.closest('li').attr('data-type') === 'chapter');
}
};
// Create our sortable group
let group = $('.sort-list').sortable(sortableOptions);
// Add book on selection confirm
window.$events.listen('entity-select-confirm', function(entityInfo) {
const alreadyAdded = $container.find(`[data-type="book"][data-id="${entityInfo.id}"]`).length > 0;
if (alreadyAdded) return;
const entitySortItemUrl = entityInfo.link + '/sort-item';
window.$http.get(entitySortItemUrl).then(resp => {
$container.append(resp.data);
group.sortable("destroy");
group = $('.sort-list').sortable(sortableOptions);
});
});
/**
* Update the input with our sort data.
*/
function updateMapInput() {
const pageMap = buildEntityMap();
$('#sort-tree-input').val(JSON.stringify(pageMap));
}
/**
* Build up a mapping of entities with their ordering and nesting.
* @returns {Array}
*/
function buildEntityMap() {
const entityMap = [];
const $lists = $('.sort-list');
$lists.each(function(listIndex) {
const $list = $(this);
const bookId = $list.closest('[data-type="book"]').attr('data-id');
const $directChildren = $list.find('> [data-type="page"], > [data-type="chapter"]');
$directChildren.each(function(directChildIndex) {
const $childElem = $(this);
const type = $childElem.attr('data-type');
const parentChapter = false;
const childId = $childElem.attr('data-id');
entityMap.push({
id: childId,
sort: directChildIndex,
parentChapter: parentChapter,
type: type,
book: bookId
});
$childElem.find('[data-type="page"]').each(function(pageIndex) {
const $chapterChild = $(this);
entityMap.push({
id: $chapterChild.attr('data-id'),
sort: pageIndex,
parentChapter: childId,
type: 'page',
book: bookId
});
});
});
});
return entityMap;
}
// Auto sort control
const sortOperations = {
name: function(a, b) {
const aName = a.getAttribute('data-name').trim().toLowerCase();
const bName = b.getAttribute('data-name').trim().toLowerCase();
return aName.localeCompare(bName);
},
created: function(a, b) {
const aTime = Number(a.getAttribute('data-created'));
const bTime = Number(b.getAttribute('data-created'));
return bTime - aTime;
},
updated: function(a, b) {
const aTime = Number(a.getAttribute('data-updated'));
const bTime = Number(b.getAttribute('data-updated'));
return bTime - aTime;
},
chaptersFirst: function(a, b) {
const aType = a.getAttribute('data-type');
const bType = b.getAttribute('data-type');
if (aType === bType) {
return 0;
}
return (aType === 'chapter' ? -1 : 1);
},
chaptersLast: function(a, b) {
const aType = a.getAttribute('data-type');
const bType = b.getAttribute('data-type');
if (aType === bType) {
return 0;
}
return (aType === 'chapter' ? 1 : -1);
},
};
let lastSort = '';
let reverse = false;
const reversibleTypes = ['name', 'created', 'updated'];
$container.on('click', '.sort-box-options [data-sort]', function(event) {
event.preventDefault();
const $sortLists = $(this).closest('.sort-box').find('ul');
const sort = $(this).attr('data-sort');
reverse = (lastSort === sort) ? !reverse : false;
let sortFunction = sortOperations[sort];
if (reverse && reversibleTypes.includes(sort)) {
sortFunction = function(a, b) {
return 0 - sortOperations[sort](a, b)
};
}
$sortLists.each(function() {
const $list = $(this);
$list.children('li').sort(sortFunction).appendTo($list);
});
lastSort = sort;
updateMapInput();
});
});
</script>
@stop