1+ /*
2+ * This file is part of the Sylius CMS Plugin package.
3+ *
4+ * (c) Sylius Sp. z o.o.
5+ *
6+ * For the full copyright and license information, please view the LICENSE
7+ * file that was distributed with this source code.
8+ */
9+
10+ import { Controller } from '@hotwired/stimulus' ;
11+
12+ /* stimulusFetch: 'lazy' */
13+ export default class extends Controller {
14+ moveUp ( event ) {
15+ this . move ( event , 'up' ) ;
16+ }
17+
18+ moveDown ( event ) {
19+ this . move ( event , 'down' ) ;
20+ }
21+
22+ async move ( event , direction ) {
23+ const button = event . currentTarget ;
24+
25+ this . #hideTooltip( button ) ;
26+
27+ const entry = button . closest ( '.sortable-item' ) ;
28+ if ( ! entry ) {
29+ return ;
30+ }
31+
32+ const sibling = direction === 'up'
33+ ? this . findPreviousSibling ( entry )
34+ : this . findNextSibling ( entry ) ;
35+
36+ if ( ! sibling ) {
37+ return ;
38+ }
39+
40+ const entryIndex = this . extractIndex ( entry ) ;
41+ const siblingIndex = this . extractIndex ( sibling ) ;
42+
43+ if ( entryIndex < 0 || siblingIndex < 0 ) {
44+ return ;
45+ }
46+
47+ const parent = entry . parentElement ;
48+ if ( ! parent ) {
49+ return ;
50+ }
51+
52+ if ( direction === 'up' ) {
53+ parent . insertBefore ( entry , sibling ) ;
54+ } else {
55+ parent . insertBefore ( sibling , entry ) ;
56+ }
57+
58+ this . swapEntryIndexes ( entry , sibling , entryIndex , siblingIndex ) ;
59+
60+ await this . #syncLiveComponentModel( entryIndex , siblingIndex ) ;
61+
62+ this . updateButtonStates ( ) ;
63+ }
64+
65+ #hideTooltip( button ) {
66+ window . bootstrap ?. Tooltip ?. getInstance ( button ) ?. hide ( ) ;
67+ }
68+
69+ async #syncLiveComponentModel( indexA , indexB ) {
70+ let root = this . element . parentElement ;
71+
72+ while ( root && ! root . __component ) {
73+ root = root . parentElement ;
74+ }
75+
76+ if ( ! root ?. __component ) {
77+ return ;
78+ }
79+
80+ const input = this . element . querySelector ( '[name*="[contentElements]"]' ) ;
81+
82+ if ( ! input ) {
83+ return ;
84+ }
85+
86+ const formName = input . name . split ( '[' ) [ 0 ] ;
87+
88+ if ( ! formName ) {
89+ return ;
90+ }
91+
92+ const segments = [ ] ;
93+ const segmentRegex = / \[ ( [ ^ \] ] * ) ] / g;
94+
95+ let match ;
96+
97+ while ( ( match = segmentRegex . exec ( input . name ) ) !== null ) {
98+ segments . push ( match [ 1 ] ) ;
99+ }
100+
101+ let lastNumericPos = - 1 ;
102+
103+ for ( let i = segments . length - 1 ; i >= 0 ; i -- ) {
104+ if ( / ^ \d + $ / . test ( segments [ i ] ) ) {
105+ lastNumericPos = i ;
106+ break ;
107+ }
108+ }
109+
110+ if ( lastNumericPos < 0 ) {
111+ return ;
112+ }
113+
114+ const pathToArray = segments . slice ( 0 , lastNumericPos ) ;
115+
116+ let currentFormValues ;
117+
118+ try {
119+ currentFormValues = root . __component . getData ( formName ) ;
120+ } catch {
121+ return ;
122+ }
123+
124+ let formValues ;
125+
126+ try {
127+ formValues = structuredClone ( currentFormValues ) ;
128+ } catch {
129+ return ;
130+ }
131+
132+ let collection = formValues ;
133+
134+ for ( const key of pathToArray ) {
135+ if ( collection == null || typeof collection !== 'object' ) {
136+ return ;
137+ }
138+
139+ collection = collection [ key ] ;
140+ }
141+
142+ if ( ! Array . isArray ( collection ) ) {
143+ return ;
144+ }
145+
146+ if (
147+ collection [ indexA ] === undefined ||
148+ collection [ indexB ] === undefined
149+ ) {
150+ return ;
151+ }
152+
153+ [
154+ collection [ indexA ] ,
155+ collection [ indexB ] ,
156+ ] = [
157+ collection [ indexB ] ,
158+ collection [ indexA ] ,
159+ ] ;
160+
161+ const result = root . __component . set ( formName , formValues , false ) ;
162+
163+ if ( result instanceof Promise ) {
164+ await result ;
165+ }
166+ }
167+
168+ findPreviousSibling ( entry ) {
169+ let element = entry . previousElementSibling ;
170+
171+ while ( element ) {
172+ if ( element . classList . contains ( 'sortable-item' ) ) {
173+ return element ;
174+ }
175+
176+ element = element . previousElementSibling ;
177+ }
178+
179+ return null ;
180+ }
181+
182+ findNextSibling ( entry ) {
183+ let element = entry . nextElementSibling ;
184+
185+ while ( element ) {
186+ if ( element . classList . contains ( 'sortable-item' ) ) {
187+ return element ;
188+ }
189+
190+ element = element . nextElementSibling ;
191+ }
192+
193+ return null ;
194+ }
195+
196+ extractIndex ( entry ) {
197+ const input = entry . querySelector ( '[name*="[contentElements]"]' ) ;
198+
199+ if ( ! input ) {
200+ return - 1 ;
201+ }
202+
203+ const match = input . name . match ( / \[ c o n t e n t E l e m e n t s ] \[ ( \d + ) ] / ) ;
204+
205+ return match ? parseInt ( match [ 1 ] , 10 ) : - 1 ;
206+ }
207+
208+ swapEntryIndexes ( entryA , entryB , indexA , indexB ) {
209+ const tempIndex = `__TEMP_${ Date . now ( ) } __` ;
210+
211+ this . renameEntry ( entryA , indexA , tempIndex ) ;
212+ this . renameEntry ( entryB , indexB , indexA ) ;
213+ this . renameEntry ( entryA , tempIndex , indexB ) ;
214+ }
215+
216+ renameEntry ( entry , oldIndex , newIndex ) {
217+ if ( entry . id ) {
218+ entry . id = this . replaceInId ( entry . id , oldIndex , newIndex ) ;
219+ }
220+
221+ entry . querySelectorAll ( '[name]' ) . forEach ( element => {
222+ element . name = element . name . replaceAll (
223+ `[contentElements][${ oldIndex } ]` ,
224+ `[contentElements][${ newIndex } ]` ,
225+ ) ;
226+ } ) ;
227+
228+ entry . querySelectorAll ( '[id]' ) . forEach ( element => {
229+ element . id = this . replaceInId (
230+ element . id ,
231+ oldIndex ,
232+ newIndex ,
233+ ) ;
234+ } ) ;
235+
236+ entry . querySelectorAll ( '[for]' ) . forEach ( element => {
237+ element . htmlFor = this . replaceInId (
238+ element . htmlFor ,
239+ oldIndex ,
240+ newIndex ,
241+ ) ;
242+ } ) ;
243+ }
244+
245+ replaceInId ( value , oldIndex , newIndex ) {
246+ if ( ! value ) {
247+ return value ;
248+ }
249+
250+ return value . replace (
251+ new RegExp ( `_contentElements_${ oldIndex } (?=_|$)` , 'g' ) ,
252+ `_contentElements_${ newIndex } ` ,
253+ ) ;
254+ }
255+
256+ updateButtonStates ( ) {
257+ const entries = [
258+ ...this . element . querySelectorAll ( '.sortable-item' ) ,
259+ ] ;
260+
261+ entries . forEach ( ( entry , index ) => {
262+ const upButton = entry . querySelector (
263+ '[data-sort-direction="up"]' ,
264+ ) ;
265+
266+ const downButton = entry . querySelector (
267+ '[data-sort-direction="down"]' ,
268+ ) ;
269+
270+ if ( upButton ) {
271+ upButton . disabled = index === 0 ;
272+ }
273+
274+ if ( downButton ) {
275+ downButton . disabled = index === entries . length - 1 ;
276+ }
277+ } ) ;
278+ }
279+ }
0 commit comments