]> BookStack Code Mirror - bookstack/blob - resources/js/components/book-sort.js
Updated a batch of JS components
[bookstack] / resources / js / components / book-sort.js
1 import Sortable from "sortablejs";
2 import {Component} from "./component";
3 import {htmlToDom} from "../services/dom";
4
5 // Auto sort control
6 const sortOperations = {
7     name: function(a, b) {
8         const aName = a.getAttribute('data-name').trim().toLowerCase();
9         const bName = b.getAttribute('data-name').trim().toLowerCase();
10         return aName.localeCompare(bName);
11     },
12     created: function(a, b) {
13         const aTime = Number(a.getAttribute('data-created'));
14         const bTime = Number(b.getAttribute('data-created'));
15         return bTime - aTime;
16     },
17     updated: function(a, b) {
18         const aTime = Number(a.getAttribute('data-updated'));
19         const bTime = Number(b.getAttribute('data-updated'));
20         return bTime - aTime;
21     },
22     chaptersFirst: function(a, b) {
23         const aType = a.getAttribute('data-type');
24         const bType = b.getAttribute('data-type');
25         if (aType === bType) {
26             return 0;
27         }
28         return (aType === 'chapter' ? -1 : 1);
29     },
30     chaptersLast: function(a, b) {
31         const aType = a.getAttribute('data-type');
32         const bType = b.getAttribute('data-type');
33         if (aType === bType) {
34             return 0;
35         }
36         return (aType === 'chapter' ? 1 : -1);
37     },
38 };
39
40 export class BookSort extends Component {
41
42     setup() {
43         this.container = this.$el;
44         this.sortContainer = this.$refs.sortContainer;
45         this.input = this.$refs.input;
46
47         const initialSortBox = this.container.querySelector('.sort-box');
48         this.setupBookSortable(initialSortBox);
49         this.setupSortPresets();
50
51         window.$events.listen('entity-select-confirm', this.bookSelect.bind(this));
52     }
53
54     /**
55      * Setup the handlers for the preset sort type buttons.
56      */
57     setupSortPresets() {
58         let lastSort = '';
59         let reverse = false;
60         const reversibleTypes = ['name', 'created', 'updated'];
61
62         this.sortContainer.addEventListener('click', event => {
63             const sortButton = event.target.closest('.sort-box-options [data-sort]');
64             if (!sortButton) return;
65
66             event.preventDefault();
67             const sortLists = sortButton.closest('.sort-box').querySelectorAll('ul');
68             const sort = sortButton.getAttribute('data-sort');
69
70             reverse = (lastSort === sort) ? !reverse : false;
71             let sortFunction = sortOperations[sort];
72             if (reverse && reversibleTypes.includes(sort)) {
73                 sortFunction = function(a, b) {
74                     return 0 - sortOperations[sort](a, b)
75                 };
76             }
77
78             for (let list of sortLists) {
79                 const directItems = Array.from(list.children).filter(child => child.matches('li'));
80                 directItems.sort(sortFunction).forEach(sortedItem => {
81                     list.appendChild(sortedItem);
82                 });
83             }
84
85             lastSort = sort;
86             this.updateMapInput();
87         });
88     }
89
90     /**
91      * Handle book selection from the entity selector.
92      * @param {Object} entityInfo
93      */
94     bookSelect(entityInfo) {
95         const alreadyAdded = this.container.querySelector(`[data-type="book"][data-id="${entityInfo.id}"]`) !== null;
96         if (alreadyAdded) return;
97
98         const entitySortItemUrl = entityInfo.link + '/sort-item';
99         window.$http.get(entitySortItemUrl).then(resp => {
100             const newBookContainer = htmlToDom(resp.data);
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.container.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 }