]> BookStack Code Mirror - bookstack/blob - resources/js/components/book-sort.js
Merge branch 'master' of https://github.com/jasonhoule/BookStack into jasonhoule...
[bookstack] / resources / js / components / book-sort.js
1 import Sortable from "sortablejs";
2
3 // Auto sort control
4 const sortOperations = {
5     name: function(a, b) {
6         const aName = a.getAttribute('data-name').trim().toLowerCase();
7         const bName = b.getAttribute('data-name').trim().toLowerCase();
8         return aName.localeCompare(bName);
9     },
10     created: function(a, b) {
11         const aTime = Number(a.getAttribute('data-created'));
12         const bTime = Number(b.getAttribute('data-created'));
13         return bTime - aTime;
14     },
15     updated: function(a, b) {
16         const aTime = Number(a.getAttribute('data-updated'));
17         const bTime = Number(b.getAttribute('data-updated'));
18         return bTime - aTime;
19     },
20     chaptersFirst: function(a, b) {
21         const aType = a.getAttribute('data-type');
22         const bType = b.getAttribute('data-type');
23         if (aType === bType) {
24             return 0;
25         }
26         return (aType === 'chapter' ? -1 : 1);
27     },
28     chaptersLast: function(a, b) {
29         const aType = a.getAttribute('data-type');
30         const bType = b.getAttribute('data-type');
31         if (aType === bType) {
32             return 0;
33         }
34         return (aType === 'chapter' ? 1 : -1);
35     },
36 };
37
38 class BookSort {
39
40     constructor(elem) {
41         this.elem = elem;
42         this.sortContainer = elem.querySelector('[book-sort-boxes]');
43         this.input = elem.querySelector('[book-sort-input]');
44
45         const initialSortBox = elem.querySelector('.sort-box');
46         this.setupBookSortable(initialSortBox);
47         this.setupSortPresets();
48
49         window.$events.listen('entity-select-confirm', this.bookSelect.bind(this));
50     }
51
52     /**
53      * Setup the handlers for the preset sort type buttons.
54      */
55     setupSortPresets() {
56         let lastSort = '';
57         let reverse = false;
58         const reversibleTypes = ['name', 'created', 'updated'];
59
60         this.sortContainer.addEventListener('click', event => {
61             const sortButton = event.target.closest('.sort-box-options [data-sort]');
62             if (!sortButton) return;
63
64             event.preventDefault();
65             const sortLists = sortButton.closest('.sort-box').querySelectorAll('ul');
66             const sort = sortButton.getAttribute('data-sort');
67
68             reverse = (lastSort === sort) ? !reverse : false;
69             let sortFunction = sortOperations[sort];
70             if (reverse && reversibleTypes.includes(sort)) {
71                 sortFunction = function(a, b) {
72                     return 0 - sortOperations[sort](a, b)
73                 };
74             }
75
76             for (let list of sortLists) {
77                 const directItems = Array.from(list.children).filter(child => child.matches('li'));
78                 directItems.sort(sortFunction).forEach(sortedItem => {
79                     list.appendChild(sortedItem);
80                 });
81             }
82
83             lastSort = sort;
84             this.updateMapInput();
85         });
86     }
87
88     /**
89      * Handle book selection from the entity selector.
90      * @param {Object} entityInfo
91      */
92     bookSelect(entityInfo) {
93         const alreadyAdded = this.elem.querySelector(`[data-type="book"][data-id="${entityInfo.id}"]`) !== null;
94         if (alreadyAdded) return;
95
96         const entitySortItemUrl = entityInfo.link + '/sort-item';
97         window.$http.get(entitySortItemUrl).then(resp => {
98             const wrap = document.createElement('div');
99             wrap.innerHTML = resp.data;
100             const newBookContainer = wrap.children[0];
101             this.sortContainer.append(newBookContainer);
102             this.setupBookSortable(newBookContainer);
103         });
104     }
105
106     /**
107      * Setup the given book container element to have sortable items.
108      * @param {Element} bookContainer
109      */
110     setupBookSortable(bookContainer) {
111         const sortElems = [bookContainer.querySelector('.sort-list')];
112         sortElems.push(...bookContainer.querySelectorAll('.entity-list-item + ul'));
113
114         const bookGroupConfig = {
115             name: 'book',
116             pull: ['book', 'chapter'],
117             put: ['book', 'chapter'],
118         };
119
120         const chapterGroupConfig = {
121             name: 'chapter',
122             pull: ['book', 'chapter'],
123             put: function(toList, fromList, draggedElem) {
124                 return draggedElem.getAttribute('data-type') === 'page';
125             }
126         };
127
128         for (let sortElem of sortElems) {
129             new Sortable(sortElem, {
130                 group: sortElem.classList.contains('sort-list') ? bookGroupConfig : chapterGroupConfig,
131                 animation: 150,
132                 fallbackOnBody: true,
133                 swapThreshold: 0.65,
134                 onSort: this.updateMapInput.bind(this),
135                 dragClass: 'bg-white',
136                 ghostClass: 'primary-background-light',
137                 multiDrag: true,
138                 multiDragKey: 'CTRL',
139                 selectedClass: 'sortable-selected',
140             });
141         }
142     }
143
144     /**
145      * Update the input with our sort data.
146      */
147     updateMapInput() {
148         const pageMap = this.buildEntityMap();
149         this.input.value = JSON.stringify(pageMap);
150     }
151
152     /**
153      * Build up a mapping of entities with their ordering and nesting.
154      * @returns {Array}
155      */
156     buildEntityMap() {
157         const entityMap = [];
158         const lists = this.elem.querySelectorAll('.sort-list');
159
160         for (let list of lists) {
161             const bookId = list.closest('[data-type="book"]').getAttribute('data-id');
162             const directChildren = Array.from(list.children)
163                 .filter(elem => elem.matches('[data-type="page"], [data-type="chapter"]'));
164             for (let i = 0; i < directChildren.length; i++) {
165                 this.addBookChildToMap(directChildren[i], i, bookId, entityMap);
166             }
167         }
168
169         return entityMap;
170     }
171
172     /**
173      * Parse a sort item and add it to a data-map array.
174      * Parses sub0items if existing also.
175      * @param {Element} childElem
176      * @param {Number} index
177      * @param {Number} bookId
178      * @param {Array} entityMap
179      */
180     addBookChildToMap(childElem, index, bookId, entityMap) {
181         const type = childElem.getAttribute('data-type');
182         const parentChapter = false;
183         const childId = childElem.getAttribute('data-id');
184
185         entityMap.push({
186             id: childId,
187             sort: index,
188             parentChapter: parentChapter,
189             type: type,
190             book: bookId
191         });
192
193         const subPages = childElem.querySelectorAll('[data-type="page"]');
194         for (let i = 0; i < subPages.length; i++) {
195             entityMap.push({
196                 id: subPages[i].getAttribute('data-id'),
197                 sort: i,
198                 parentChapter: childId,
199                 type: 'page',
200                 book: bookId
201             });
202         }
203     }
204
205 }
206
207 export default BookSort;