Skip to content

Commit 9ade72a

Browse files
committed
FEAT: [Admin] 100 add order functionality for content elements
1 parent 768a4d3 commit 9ade72a

11 files changed

Lines changed: 520 additions & 10 deletions

File tree

UPGRADE-1.2.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,3 +50,26 @@ The default remains `trix`, so no action is required to keep the previous behavi
5050

5151
See the documentation for setup and configuration details:
5252
https://docs.sylius.com/cms-plugin/development/wysiwyg-editors
53+
54+
### New Stimulus Controller: `ContentElementsOrderController`
55+
56+
This controller allows ordering content elements.
57+
58+
In your end application, run the following command to add a new dependency to `package.json` file:
59+
60+
```bash
61+
yarn add @sylius-cms-plugin/admin@file:vendor/sylius/cms-plugin/assets/admin
62+
```
63+
64+
And add the following to your `controllers.json` file:
65+
66+
```json
67+
{
68+
"@sylius-cms-plugin/admin": {
69+
"content-elements-order": {
70+
"enabled": true,
71+
"fetch": "lazy"
72+
}
73+
}
74+
}
75+
```

assets/admin/controllers.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@
66
"webpackMode": "lazy",
77
"fetch": "lazy",
88
"enabled": true
9+
},
10+
"content-elements-order": {
11+
"main": "controllers/ContentElementsOrderController.js",
12+
"webpackMode": "lazy",
13+
"fetch": "lazy",
14+
"enabled": true
915
}
1016
},
1117
"@ehyiah/ux-quill": {
Lines changed: 279 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,279 @@
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(/\[contentElements]\[(\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+
}

assets/admin/package.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,12 @@
1010
"webpackMode": "lazy",
1111
"fetch": "lazy",
1212
"enabled": true
13+
},
14+
"content-elements-order": {
15+
"main": "controllers/ContentElementsOrderController.js",
16+
"webpackMode": "lazy",
17+
"fetch": "lazy",
18+
"enabled": true
1319
}
1420
}
1521
},
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
@managing_pages
2+
Feature: Sorting content elements on a page
3+
In order to manage the order of content on a page
4+
As an Administrator
5+
I want to be able to reorder content elements
6+
7+
Background:
8+
Given I am logged in as an administrator
9+
And the store operates on a single channel in "United States"
10+
11+
@ui @javascript
12+
Scenario: Moving a content element down
13+
When I go to the create page page
14+
And I fill the code with "sort-test-page"
15+
And I fill the name with "Sort Test Page"
16+
And I fill the slug with "sort-test-page"
17+
And I add a heading content element with type "h1" and "My Title" content
18+
And I add a textarea content element with "My body text" content
19+
When I move the 1st content element down
20+
Then the 1st content element should be a "Textarea" element
21+
And the 2nd content element should be a "Heading" element
22+
23+
@ui @javascript
24+
Scenario: Moving a content element up
25+
When I go to the create page page
26+
And I fill the code with "sort-test-page"
27+
And I fill the name with "Sort Test Page"
28+
And I fill the slug with "sort-test-page"
29+
And I add a heading content element with type "h1" and "My Title" content
30+
And I add a textarea content element with "My body text" content
31+
When I move the 2nd content element up
32+
Then the 1st content element should be a "Textarea" element
33+
And the 2nd content element should be a "Heading" element
34+
35+
@ui @javascript
36+
Scenario: The first content element cannot be moved up
37+
When I go to the create page page
38+
And I fill the code with "sort-test-page"
39+
And I fill the name with "Sort Test Page"
40+
And I fill the slug with "sort-test-page"
41+
And I add a heading content element with type "h1" and "My Title" content
42+
And I add a textarea content element with "My body text" content
43+
Then the move up button of the 1st content element should be disabled
44+
45+
@ui @javascript
46+
Scenario: The last content element cannot be moved down
47+
When I go to the create page page
48+
And I fill the code with "sort-test-page"
49+
And I fill the name with "Sort Test Page"
50+
And I fill the slug with "sort-test-page"
51+
And I add a heading content element with type "h1" and "My Title" content
52+
And I add a textarea content element with "My body text" content
53+
Then the move down button of the 2nd content element should be disabled

0 commit comments

Comments
 (0)