Skip to content

Commit 63c7c94

Browse files
committed
feat(editor): improve section/block lifecycle handling and add toggleClass liveUpdate support
- Replace `section:updating` and `section:updated` events with `section:unload` and `section:load` - Add support for `section:select`, `section:deselect`, `block:select`, and `block:deselect` events - Introduce `toggleClass` live update type in `LiveUpdatesBuilder` - Improve image setting handling with support for absolute URLs
1 parent 0c9e305 commit 63c7c94

File tree

7 files changed

+98
-31
lines changed

7 files changed

+98
-31
lines changed

docs/building-theme/adding-sections/integrating-editor.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,10 @@ When a section is updated in the editor, its DOM is replaced. Any interactive Ja
1818

1919
Bagisto Visual emits events during this lifecycle:
2020

21-
| Event | Timing | Use case |
22-
| ------------------------- | ----------------------------- | ------------------------ |
23-
| `visual:section:updating` | Before section is removed | Save UI state |
24-
| `visual:section:updated` | After new section is inserted | Reinitialize JS behavior |
21+
| Event | Timing | Use case |
22+
| ----------------------- | ---------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
23+
| `visual:section:load` | After section is added or re-rendered | Re-run any necessary JavaScript to ensure the section functions and displays correctly, as if the page were freshly loaded. You may also want to restore the section state. |
24+
| `visual:section:unload` | Before section is removed or re-rendered | Make sure to clean up event listeners, variables, and anything else to prevent issues when interacting with the page and avoid memory leaks. Also, save the section state. |
2525

2626
Each event exposes:
2727

@@ -38,13 +38,13 @@ event.detail = {
3838
@visual_design_mode
3939
@pushOnce('scripts')
4040
<script>
41-
document.addEventListener('visual:section:updating', (event) => {
41+
document.addEventListener('visual:section:unload', (event) => {
4242
if (event.detail.section.type === '{{ $section->type }}') {
4343
// Save scroll position, destroy carousels, etc.
4444
}
4545
});
4646
47-
document.addEventListener('visual:section:updated', (event) => {
47+
document.addEventListener('visual:section:load', (event) => {
4848
if (event.detail.section.type === '{{ $section->type }}') {
4949
// Reinitialize interactivity: sliders, modals, listeners
5050
}

resources/assets/editor/composables/useIframeRpc.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,12 @@ type IframeMessage =
55
| { type: 'reordering'; data: { order: string[]; sectionId: string } }
66
| { type: 'setting:updated'; data: any }
77
| { type: 'section:updating'; data: any }
8-
| { type: 'section:updated'; data: any }
98
| { type: 'section:highlight'; data: string }
109
| { type: 'section:unhighlight'; data: string }
11-
| { type: 'section:selected'; data: string }
12-
| { type: 'block:selected'; data: { sectionId: string; blockId: string } }
10+
| { type: 'section:select'; data: string }
11+
| { type: 'section:deselect'; data: string }
12+
| { type: 'block:select'; data: { sectionId: string; blockId: string } }
13+
| { type: 'block:deselect'; data: { sectionId: string; blockId: string } }
1314
| { type: 'sectionsOrder'; data: string[] }
1415
| { type: 'section:removed'; data: any }
1516
| { type: 'section:added'; data: any };

resources/assets/editor/injected.ts

Lines changed: 55 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,13 @@ const EVENTS = {
4444
SETTING_UPDATED: 'setting:updated',
4545
SECTION_HIGHLIGHT: 'section:highlight',
4646
SECTION_UNHIGHLIGHT: 'section:unhighlight',
47-
SECTION_SELECTED: 'section:selected',
48-
BLOCK_SELECTED: 'block:selected',
47+
SECTION_SELECT: 'section:select',
48+
SECTION_DESELECT: 'section:deselect',
49+
SECTION_LOAD: 'section:load',
50+
SECTION_UNLOAD: 'section:unload',
51+
52+
BLOCK_SELECT: 'block:select',
53+
BLOCK_DESELECT: 'block:deselect',
4954
SECTION_ADDED: 'section:added',
5055
SECTION_REMOVED: 'section:removed',
5156
SECTIONS_REORDERED: 'sectionsOrder',
@@ -130,6 +135,8 @@ class VisualObject {
130135
(targetEl.style as any)[config.style] = value;
131136
} else if (config.attr) {
132137
targetEl.setAttribute(config.attr, value);
138+
} else {
139+
return;
133140
}
134141

135142
skipRefresh();
@@ -158,8 +165,10 @@ class ThemeEditor {
158165

159166
private messageHandlers: Record<string, (data: any, messageId?: string) => void> = {
160167
[EVENTS.SECTION_HIGHLIGHT]: (data) => this.handleSectionHighlight(data),
161-
[EVENTS.SECTION_SELECTED]: (data) => this.handleSectionSelected(data),
162-
[EVENTS.BLOCK_SELECTED]: (data) => this.handleBlockSelected(data),
168+
[EVENTS.SECTION_SELECT]: (data) => this.handleSectionSelected(data),
169+
[EVENTS.SECTION_DESELECT]: (data) => this.handleSectionDeselected(data),
170+
[EVENTS.BLOCK_SELECT]: (data) => this.handleBlockSelected(data),
171+
[EVENTS.BLOCK_DESELECT]: (data) => this.handleBlockDeselected(data),
163172
[EVENTS.SECTION_ADDED]: (data) => this.handleSectionAdded(data),
164173
[EVENTS.SECTION_REMOVED]: (data) => this.handleSectionRemoved(data),
165174
[EVENTS.SECTION_UNHIGHLIGHT]: () => this.handleUnhighlightSection(),
@@ -262,6 +271,7 @@ class ThemeEditor {
262271
}
263272

264273
private handleWindowResize() {
274+
console.log('Window resized, focusing on active section if exists', this.activeSectionId);
265275
if (this.activeSectionId) {
266276
const activeSection = document.querySelector(`[${ATTRS.SectionId}="${this.activeSectionId}"]`) as HTMLElement;
267277

@@ -315,17 +325,42 @@ class ThemeEditor {
315325
this.handleSectionHighlight(id);
316326
el.scrollIntoView({ behavior: 'smooth', block: 'start' });
317327

318-
window.Visual._dispatch(EVENTS.SECTION_SELECTED, {
328+
window.Visual._dispatch(EVENTS.SECTION_SELECT, {
319329
section: {
320-
id: el.dataset.sectionId,
330+
id,
321331
type: el.dataset.sectionType,
322332
},
323333
});
334+
335+
window.Visual._dispatch(EVENTS.SECTION_SELECT + `:${id}`, {});
336+
}
337+
338+
private handleSectionDeselected(id: string) {
339+
if (this.activeSectionId === id) {
340+
this.clearActiveSection();
341+
this.activeSectionId = null;
342+
}
343+
344+
const el = document.querySelector(`[${ATTRS.SectionId}="${id}"]`) as HTMLElement;
345+
if (!el) {
346+
return;
347+
}
348+
349+
el.removeAttribute(ATTRS.VisualHighlighted);
350+
351+
window.Visual._dispatch(EVENTS.SECTION_DESELECT, { section: { id, type: el.dataset.sectionType } });
352+
window.Visual._dispatch(EVENTS.SECTION_DESELECT + `:${id}`, {});
324353
}
325354

326355
private handleBlockSelected(data: { sectionId: string; blockId: string }) {
327356
this.handleSectionSelected(data.sectionId);
328-
window.Visual._dispatch(EVENTS.BLOCK_SELECTED, data);
357+
window.Visual._dispatch(EVENTS.BLOCK_SELECT, data);
358+
window.Visual._dispatch(EVENTS.BLOCK_SELECT + `:${data.blockId}`, {});
359+
}
360+
361+
private handleBlockDeselected(data: { sectionId: string; blockId: string }) {
362+
window.Visual._dispatch(EVENTS.BLOCK_DESELECT, data);
363+
window.Visual._dispatch(EVENTS.BLOCK_DESELECT + `:${data.blockId}`, {});
329364
}
330365

331366
private handleSectionAdded({ section }: { section: SectionData }) {
@@ -399,7 +434,7 @@ class ThemeEditor {
399434
const el = document.querySelector(`[${ATTRS.SectionId}="${this.activeSectionId}"]`) as HTMLElement;
400435
if (el) {
401436
this.focusOnSection(el);
402-
el.scrollIntoView({ behavior: 'smooth', block: 'start' });
437+
// el.scrollIntoView({ behavior: 'smooth', block: 'start' });
403438
}
404439
}
405440

@@ -448,9 +483,9 @@ class ThemeEditor {
448483
return false;
449484
}
450485

451-
elements.forEach((el) => {
486+
for (const el of elements) {
452487
const type = el.getAttribute(attrName);
453-
const [updateType, updateKey] = type?.split(':') ?? ['text', undefined];
488+
const [updateType, updateKey] = type?.split(/:(.+)/) ?? ['text', undefined];
454489

455490
switch (updateType) {
456491
case 'text':
@@ -464,6 +499,10 @@ class ThemeEditor {
464499
break;
465500
case 'attr':
466501
if (!settingValue) {
502+
if (el.tagName.toLowerCase() === 'img' && updateKey === 'src') {
503+
return false;
504+
}
505+
467506
el.removeAttribute(updateKey as string);
468507
} else {
469508
el.setAttribute(updateKey as string, settingValue);
@@ -476,10 +515,13 @@ class ThemeEditor {
476515
(el as HTMLElement).style.setProperty(updateKey as string, settingValue);
477516
}
478517
break;
518+
case 'toggleClass':
519+
el.classList.toggle(updateKey as string);
520+
break;
479521
default:
480522
console.warn(`Unknown live update type: ${updateType}`);
481523
}
482-
});
524+
}
483525

484526
return true;
485527
}
@@ -539,6 +581,7 @@ class ThemeEditor {
539581
const newEl = newDoc.querySelector(`[data-section-id="${sectionId}"]`);
540582

541583
if (oldEl && newEl) {
584+
window.Visual._dispatch(EVENTS.SECTION_UNLOAD, context);
542585
this.patchNode(oldEl, newEl);
543586
} else if (!oldEl && newEl) {
544587
const sections = document.querySelectorAll(`[${ATTRS.SectionType}]`);
@@ -560,7 +603,7 @@ class ThemeEditor {
560603
return;
561604
}
562605

563-
window.Visual._dispatch('section:updated', context);
606+
window.Visual._dispatch(EVENTS.SECTION_LOAD, context);
564607
});
565608
}
566609

resources/assets/editor/store.ts

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -346,7 +346,7 @@ export const useStore = defineStore('main', () => {
346346

347347
switch (setting.type) {
348348
case 'image': {
349-
return `${window.ThemeEditor.imagesBaseUrl()}/${value}`;
349+
return String(value).startsWith('http') ? value : `${window.ThemeEditor.imagesBaseUrl()}/${value}`;
350350
}
351351

352352
case 'product': {
@@ -587,9 +587,7 @@ export const useStore = defineStore('main', () => {
587587
delete section.blocks[blockId];
588588
section.blocks_order = section.blocks_order.filter((id) => id !== blockId);
589589

590-
await previewIframe.call('section:updating', { section: toRaw(section), block: null }, 0);
591590
await persistThemeData();
592-
await previewIframe.call('section:updated', { section: toRaw(section), block: null }, 0);
593591
}
594592

595593
function activateSection(sectionId: string) {
@@ -604,12 +602,22 @@ export const useStore = defineStore('main', () => {
604602

605603
function selectSection(sectionId: string) {
606604
activeSectionId.value = sectionId;
607-
previewIframe.call('section:selected', sectionId, 0);
605+
previewIframe.call('section:select', sectionId, 0);
606+
}
607+
608+
function deselectSection(sectionId: string) {
609+
activeSectionId.value = null;
610+
previewIframe.call('section:deselect', sectionId, 0);
608611
}
609612

610613
function selectBlock(sectionId: string, blockId: string) {
611614
activeSectionId.value = sectionId;
612-
previewIframe.call('block:selected', { sectionId, blockId });
615+
previewIframe.call('block:select', { sectionId, blockId }, 0);
616+
}
617+
618+
function deselectBlock(sectionId: string, blockId: string) {
619+
activeSectionId.value = sectionId;
620+
previewIframe.call('block:deselect', { sectionId, blockId }, 0);
613621
}
614622

615623
function setContentSectionsOrder(order: string[]) {
@@ -756,7 +764,9 @@ export const useStore = defineStore('main', () => {
756764
activateSection,
757765
deactivateSection,
758766
selectSection,
767+
deselectSection,
759768
selectBlock,
769+
deselectBlock,
760770
setContentSectionsOrder,
761771
moveSectionUp,
762772
moveSectionDown,

resources/assets/editor/views/[section].[block].vue

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@
1616
store.selectBlock(route.params.section, route.params.block);
1717
});
1818
19+
onBeforeRouteLeave(() => {
20+
store.deselectBlock(route.params.section, route.params.block);
21+
});
22+
1923
function goBack() {
2024
router.back();
2125
}

resources/assets/editor/views/[section].vue

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,10 @@
4242
store.selectSection(route.params.section);
4343
});
4444
45+
onBeforeRouteLeave(() => {
46+
store.deselectSection(route.params.section);
47+
});
48+
4549
function goBack() {
4650
router.back();
4751
}
@@ -202,4 +206,4 @@
202206
</button>
203207
</footer>
204208
</div>
205-
</template>
209+
</template>

src/Sections/Support/LiveUpdatesBuilder.php

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -59,12 +59,17 @@ public function outerHtml(string $settingId): self
5959

6060
public function attr(string $settingId, string $attr): self
6161
{
62-
return $this->add($settingId, 'attr:'.$attr);
62+
return $this->add($settingId, 'attr:' . $attr);
6363
}
6464

6565
public function style(string $settingId, string $style): self
6666
{
67-
return $this->add($settingId, 'style:'.$style);
67+
return $this->add($settingId, 'style:' . $style);
68+
}
69+
70+
public function toggleClass(string $settingId, string $class): self
71+
{
72+
return $this->add($settingId, 'toggleClass:' . $class);
6873
}
6974

7075
public function toHtml(): string
@@ -79,9 +84,9 @@ public function __toString(): string
7984
}
8085

8186
return collect($this->updates)->map(function ($update) {
82-
$attr = 'data-live-update-'.$update['key'];
87+
$attr = 'data-live-update-' . $update['key'];
8388

84-
return $attr.'="'.htmlspecialchars($update['type'], ENT_QUOTES, 'UTF-8').'"';
89+
return $attr . '="' . htmlspecialchars($update['type'], ENT_QUOTES, 'UTF-8') . '"';
8590
})->implode(' ');
8691
}
8792
}

0 commit comments

Comments
 (0)