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