1- import Sortable from "sortablejs" ;
1+ import Sortable , { MultiDrag } from "sortablejs" ;
22import { Component } from "./component" ;
33import { htmlToDom } from "../services/dom" ;
44
@@ -37,22 +37,148 @@ const sortOperations = {
3737 } ,
3838} ;
3939
40+ /**
41+ * The available move actions.
42+ * The active function indicates if the action is possible for the given item.
43+ * The run function performs the move.
44+ * @type {{up: {active(Element, ?Element, Element): boolean, run(Element, ?Element, Element)}} }
45+ */
46+ const moveActions = {
47+ up : {
48+ active ( elem , parent , book ) {
49+ return ! ( elem . previousElementSibling === null && ! parent ) ;
50+ } ,
51+ run ( elem , parent , book ) {
52+ const newSibling = elem . previousElementSibling || parent ;
53+ newSibling . insertAdjacentElement ( 'beforebegin' , elem ) ;
54+ }
55+ } ,
56+ down : {
57+ active ( elem , parent , book ) {
58+ return ! ( elem . nextElementSibling === null && ! parent ) ;
59+ } ,
60+ run ( elem , parent , book ) {
61+ const newSibling = elem . nextElementSibling || parent ;
62+ newSibling . insertAdjacentElement ( 'afterend' , elem ) ;
63+ }
64+ } ,
65+ next_book : {
66+ active ( elem , parent , book ) {
67+ return book . nextElementSibling !== null ;
68+ } ,
69+ run ( elem , parent , book ) {
70+ const newList = book . nextElementSibling . querySelector ( 'ul' ) ;
71+ newList . prepend ( elem ) ;
72+ }
73+ } ,
74+ prev_book : {
75+ active ( elem , parent , book ) {
76+ return book . previousElementSibling !== null ;
77+ } ,
78+ run ( elem , parent , book ) {
79+ const newList = book . previousElementSibling . querySelector ( 'ul' ) ;
80+ newList . appendChild ( elem ) ;
81+ }
82+ } ,
83+ next_chapter : {
84+ active ( elem , parent , book ) {
85+ return elem . dataset . type === 'page' && this . getNextChapter ( elem , parent ) ;
86+ } ,
87+ run ( elem , parent , book ) {
88+ const nextChapter = this . getNextChapter ( elem , parent ) ;
89+ nextChapter . querySelector ( 'ul' ) . prepend ( elem ) ;
90+ } ,
91+ getNextChapter ( elem , parent ) {
92+ const topLevel = ( parent || elem ) ;
93+ const topItems = Array . from ( topLevel . parentElement . children ) ;
94+ const index = topItems . indexOf ( topLevel ) ;
95+ return topItems . slice ( index + 1 ) . find ( elem => elem . dataset . type === 'chapter' ) ;
96+ }
97+ } ,
98+ prev_chapter : {
99+ active ( elem , parent , book ) {
100+ return elem . dataset . type === 'page' && this . getPrevChapter ( elem , parent ) ;
101+ } ,
102+ run ( elem , parent , book ) {
103+ const prevChapter = this . getPrevChapter ( elem , parent ) ;
104+ prevChapter . querySelector ( 'ul' ) . append ( elem ) ;
105+ } ,
106+ getPrevChapter ( elem , parent ) {
107+ const topLevel = ( parent || elem ) ;
108+ const topItems = Array . from ( topLevel . parentElement . children ) ;
109+ const index = topItems . indexOf ( topLevel ) ;
110+ return topItems . slice ( 0 , index ) . reverse ( ) . find ( elem => elem . dataset . type === 'chapter' ) ;
111+ }
112+ } ,
113+ book_end : {
114+ active ( elem , parent , book ) {
115+ return parent || ( parent === null && elem . nextElementSibling ) ;
116+ } ,
117+ run ( elem , parent , book ) {
118+ book . querySelector ( 'ul' ) . append ( elem ) ;
119+ }
120+ } ,
121+ book_start : {
122+ active ( elem , parent , book ) {
123+ return parent || ( parent === null && elem . previousElementSibling ) ;
124+ } ,
125+ run ( elem , parent , book ) {
126+ book . querySelector ( 'ul' ) . prepend ( elem ) ;
127+ }
128+ } ,
129+ before_chapter : {
130+ active ( elem , parent , book ) {
131+ return parent ;
132+ } ,
133+ run ( elem , parent , book ) {
134+ parent . insertAdjacentElement ( 'beforebegin' , elem ) ;
135+ }
136+ } ,
137+ after_chapter : {
138+ active ( elem , parent , book ) {
139+ return parent ;
140+ } ,
141+ run ( elem , parent , book ) {
142+ parent . insertAdjacentElement ( 'afterend' , elem ) ;
143+ }
144+ } ,
145+ } ;
146+
40147export class BookSort extends Component {
41148
42149 setup ( ) {
43150 this . container = this . $el ;
44151 this . sortContainer = this . $refs . sortContainer ;
45152 this . input = this . $refs . input ;
46153
154+ Sortable . mount ( new MultiDrag ( ) ) ;
155+
47156 const initialSortBox = this . container . querySelector ( '.sort-box' ) ;
48157 this . setupBookSortable ( initialSortBox ) ;
49158 this . setupSortPresets ( ) ;
159+ this . setupMoveActions ( ) ;
50160
51- window . $events . listen ( 'entity-select-confirm ' , this . bookSelect . bind ( this ) ) ;
161+ window . $events . listen ( 'entity-select-change ' , this . bookSelect . bind ( this ) ) ;
52162 }
53163
54164 /**
55- * Setup the handlers for the preset sort type buttons.
165+ * Set up the handlers for the item-level move buttons.
166+ */
167+ setupMoveActions ( ) {
168+ // Handle move button click
169+ this . container . addEventListener ( 'click' , event => {
170+ if ( event . target . matches ( '[data-move]' ) ) {
171+ const action = event . target . getAttribute ( 'data-move' ) ;
172+ const sortItem = event . target . closest ( '[data-id]' ) ;
173+ this . runSortAction ( sortItem , action ) ;
174+ }
175+ } ) ;
176+
177+ this . updateMoveActionStateForAll ( ) ;
178+ }
179+
180+ /**
181+ * Set up the handlers for the preset sort type buttons.
56182 */
57183 setupSortPresets ( ) {
58184 let lastSort = '' ;
@@ -100,16 +226,19 @@ export class BookSort extends Component {
100226 const newBookContainer = htmlToDom ( resp . data ) ;
101227 this . sortContainer . append ( newBookContainer ) ;
102228 this . setupBookSortable ( newBookContainer ) ;
229+ this . updateMoveActionStateForAll ( ) ;
230+
231+ const summary = newBookContainer . querySelector ( 'summary' ) ;
232+ summary . focus ( ) ;
103233 } ) ;
104234 }
105235
106236 /**
107- * Setup the given book container element to have sortable items.
237+ * Set up the given book container element to have sortable items.
108238 * @param {Element } bookContainer
109239 */
110240 setupBookSortable ( bookContainer ) {
111- const sortElems = [ bookContainer . querySelector ( '.sort-list' ) ] ;
112- sortElems . push ( ...bookContainer . querySelectorAll ( '.entity-list-item + ul' ) ) ;
241+ const sortElems = Array . from ( bookContainer . querySelectorAll ( '.sort-list, .sortable-page-sublist' ) ) ;
113242
114243 const bookGroupConfig = {
115244 name : 'book' ,
@@ -125,22 +254,40 @@ export class BookSort extends Component {
125254 }
126255 } ;
127256
128- for ( let sortElem of sortElems ) {
129- new Sortable ( sortElem , {
257+ for ( const sortElem of sortElems ) {
258+ Sortable . create ( sortElem , {
130259 group : sortElem . classList . contains ( 'sort-list' ) ? bookGroupConfig : chapterGroupConfig ,
131260 animation : 150 ,
132261 fallbackOnBody : true ,
133262 swapThreshold : 0.65 ,
134- onSort : this . updateMapInput . bind ( this ) ,
263+ onSort : ( event ) => {
264+ this . ensureNoNestedChapters ( )
265+ this . updateMapInput ( ) ;
266+ this . updateMoveActionStateForAll ( ) ;
267+ } ,
135268 dragClass : 'bg-white' ,
136269 ghostClass : 'primary-background-light' ,
137270 multiDrag : true ,
138- multiDragKey : 'CTRL ' ,
271+ multiDragKey : 'Control ' ,
139272 selectedClass : 'sortable-selected' ,
140273 } ) ;
141274 }
142275 }
143276
277+ /**
278+ * Handle nested chapters by moving them to the parent book.
279+ * Needed since sorting with multi-sort only checks group rules based on the active item,
280+ * not all in group, therefore need to manually check after a sort.
281+ * Must be done before updating the map input.
282+ */
283+ ensureNoNestedChapters ( ) {
284+ const nestedChapters = this . container . querySelectorAll ( '[data-type="chapter"] [data-type="chapter"]' ) ;
285+ for ( const chapter of nestedChapters ) {
286+ const parentChapter = chapter . parentElement . closest ( '[data-type="chapter"]' ) ;
287+ parentChapter . insertAdjacentElement ( 'afterend' , chapter ) ;
288+ }
289+ }
290+
144291 /**
145292 * Update the input with our sort data.
146293 */
@@ -202,4 +349,38 @@ export class BookSort extends Component {
202349 }
203350 }
204351
352+ /**
353+ * Run the given sort action up the provided sort item.
354+ * @param {Element } item
355+ * @param {String } action
356+ */
357+ runSortAction ( item , action ) {
358+ const parentItem = item . parentElement . closest ( 'li[data-id]' ) ;
359+ const parentBook = item . parentElement . closest ( '[data-type="book"]' ) ;
360+ moveActions [ action ] . run ( item , parentItem , parentBook ) ;
361+ this . updateMapInput ( ) ;
362+ this . updateMoveActionStateForAll ( ) ;
363+ item . scrollIntoView ( { behavior : 'smooth' , block : 'nearest' } ) ;
364+ item . focus ( ) ;
365+ }
366+
367+ /**
368+ * Update the state of the available move actions on this item.
369+ * @param {Element } item
370+ */
371+ updateMoveActionState ( item ) {
372+ const parentItem = item . parentElement . closest ( 'li[data-id]' ) ;
373+ const parentBook = item . parentElement . closest ( '[data-type="book"]' ) ;
374+ for ( const [ action , functions ] of Object . entries ( moveActions ) ) {
375+ const moveButton = item . querySelector ( `[data-move="${ action } "]` ) ;
376+ moveButton . disabled = ! functions . active ( item , parentItem , parentBook ) ;
377+ }
378+ }
379+
380+ updateMoveActionStateForAll ( ) {
381+ const items = this . container . querySelectorAll ( '[data-type="chapter"],[data-type="page"]' ) ;
382+ for ( const item of items ) {
383+ this . updateMoveActionState ( item ) ;
384+ }
385+ }
205386}
0 commit comments