@myissue/vue-website-page-builder 3.3.11 → 3.3.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,11 @@
1
1
  // Type definitions
2
- import type { ComponentObject, ImageObject, PageBuilderConfig } from '../types'
2
+ import type {
3
+ BuilderResourceData,
4
+ ComponentObject,
5
+ ImageObject,
6
+ PageBuilderConfig,
7
+ StartBuilderResult,
8
+ } from '../types'
3
9
 
4
10
  import type { usePageBuilderStateStore } from '../stores/page-builder-state'
5
11
 
@@ -18,11 +24,7 @@ import { isEmptyObject } from '../helpers/isEmptyObject'
18
24
 
19
25
  export class PageBuilderService {
20
26
  // Class properties with types
21
- private nextTick: Promise<void>
22
- private containsPagebuilder: Element | null
23
- // private pageBuilder: Element | null
24
27
  private pageBuilderStateStore: ReturnType<typeof usePageBuilderStateStore>
25
- private getTextAreaVueModel: ComputedRef<string | null>
26
28
  private getLocalStorageItemName: ComputedRef<string | null>
27
29
  private getApplyImageToSelection: ComputedRef<ImageObject>
28
30
  private getHyberlinkEnable: ComputedRef<boolean>
@@ -37,17 +39,14 @@ export class PageBuilderService {
37
39
  private delay: (ms?: number) => Promise<void>
38
40
  private hasStartedEditing: boolean = false
39
41
  // Hold data from Database or Backend for updated post
40
- private originalComponents: string | null = null
42
+ private originalComponents: BuilderResourceData | undefined = undefined
41
43
  // Holds data to be mounted when #pagebuilder is not yet present in the DOM
42
- private pendingMountData: string | null = null
44
+ private pendingMountData: BuilderResourceData | null = null
43
45
 
44
46
  constructor(pageBuilderStateStore: ReturnType<typeof usePageBuilderStateStore>) {
45
- this.nextTick = nextTick()
46
47
  this.hasStartedEditing = false
47
- this.containsPagebuilder = document.querySelector('#contains-pagebuilder')
48
48
  this.pageBuilderStateStore = pageBuilderStateStore
49
49
 
50
- this.getTextAreaVueModel = computed(() => this.pageBuilderStateStore.getTextAreaVueModel)
51
50
  this.getLocalStorageItemName = computed(
52
51
  () => this.pageBuilderStateStore.getLocalStorageItemName,
53
52
  )
@@ -176,6 +175,82 @@ export class PageBuilderService {
176
175
  }
177
176
  }
178
177
 
178
+ #validateUserProvidedComponents(components: unknown) {
179
+ const formType =
180
+ this.pageBuilderStateStore.getPageBuilderConfig &&
181
+ this.pageBuilderStateStore.getPageBuilderConfig.updateOrCreate &&
182
+ this.pageBuilderStateStore.getPageBuilderConfig.updateOrCreate.formType
183
+
184
+ if (Array.isArray(components) && components.length === 0) {
185
+ return { error: false as const, message: 'No components provided (empty array).' }
186
+ }
187
+
188
+ if (
189
+ Array.isArray(components) &&
190
+ components.length >= 1 &&
191
+ formType === 'create' &&
192
+ components
193
+ ) {
194
+ return {
195
+ error: true as const,
196
+ warning:
197
+ 'You cannot set formType to create in your configuration while also passing a components data array to the Page Builder. Please set formType to update.',
198
+ status: 'validation_failed',
199
+ }
200
+ }
201
+ if (formType === 'create' && components) {
202
+ return {
203
+ error: true as const,
204
+ warning:
205
+ 'You cannot set formType to create in your configuration while also passing a components data array to the Page Builder. Please set formType to update.',
206
+ status: 'validation_failed',
207
+ }
208
+ }
209
+
210
+ // Must be an array
211
+ if (!Array.isArray(components)) {
212
+ return {
213
+ error: true as const,
214
+ reason: 'Components data must be an array.',
215
+ }
216
+ }
217
+
218
+ // Check that the first item looks like a component
219
+ const first = components[0]
220
+
221
+ // Check that the first item is not an empty object
222
+ if (isEmptyObject(first)) {
223
+ console.error(
224
+ 'The first object in the array is empty. Each component must be a non-empty object and include an html_code key.',
225
+ )
226
+ return {
227
+ error: true as const,
228
+ reason:
229
+ "The first object in the array is empty. Each component must be a non-empty object and include an 'html_code' key.",
230
+ }
231
+ }
232
+
233
+ if (first && 'html_code' in first && typeof first.html_code !== 'string') {
234
+ console.error("The 'html_code' property in the first object must be a string.")
235
+ return {
236
+ error: true as const,
237
+ reason: "The 'html_code' property in the first object must be a string.",
238
+ }
239
+ }
240
+
241
+ // Check that the first item has an 'html_code' key
242
+ if (!first || !('html_code' in first)) {
243
+ console.error("The first object in the array must include an 'html_code' key.")
244
+ return {
245
+ error: true as const,
246
+ reason: "The first object in the array must include an 'html_code' key.",
247
+ }
248
+ }
249
+
250
+ // No errors found
251
+ return { error: false as const }
252
+ }
253
+
179
254
  #validateConfig(config: PageBuilderConfig): void {
180
255
  const defaultConfigValues = {
181
256
  updateOrCreate: {
@@ -194,6 +269,96 @@ export class PageBuilderService {
194
269
  }
195
270
  }
196
271
 
272
+ #handlePageBuilderNotPresent(passedDataComponents: BuilderResourceData) {
273
+ this.pendingMountData = passedDataComponents
274
+ }
275
+
276
+ async #mountPassedComponentsToDOM(components?: BuilderResourceData): Promise<void> {
277
+ const config = this.pageBuilderStateStore.getPageBuilderConfig
278
+ const formType = config && config.updateOrCreate && config.updateOrCreate.formType
279
+ const localStorageData = this.loadStoredComponentsFromStorage()
280
+
281
+ let dataToPass: string
282
+ if (typeof components === 'string') {
283
+ dataToPass = components
284
+ } else if (components !== undefined) {
285
+ dataToPass = JSON.stringify(components)
286
+ } else {
287
+ dataToPass = ''
288
+ }
289
+
290
+ await this.#updateComponentsFromString(dataToPass)
291
+ }
292
+
293
+ async tryMountPendingComponents() {
294
+ this.pageBuilderStateStore.setIsLoadingGlobal(true)
295
+ await delay(200)
296
+ const config = this.pageBuilderStateStore.getPageBuilderConfig
297
+ const formType = config && config.updateOrCreate && config.updateOrCreate.formType
298
+ const localStorageData = this.loadStoredComponentsFromStorage()
299
+ //
300
+ if (!config) return
301
+ //
302
+ if (
303
+ config &&
304
+ formType === 'update' &&
305
+ localStorageData &&
306
+ typeof localStorageData === 'string' &&
307
+ this.pendingMountData
308
+ ) {
309
+ this.pageBuilderStateStore.setHasLocalDraftForUpdate(true)
310
+ }
311
+ //
312
+ //
313
+ //
314
+ //
315
+ if (config && formType === 'update') {
316
+ if (this.pendingMountData) {
317
+ this.#completeBuilderInitialization(this.pendingMountData)
318
+ return
319
+ }
320
+
321
+ // Pending data for mount is null at this stage
322
+ if (typeof localStorageData === 'string') {
323
+ await this.#updateComponentsFromString(localStorageData)
324
+ this.#completeBuilderInitialization()
325
+ return
326
+ }
327
+
328
+ //
329
+ //
330
+ //
331
+ //
332
+ // Wait for Vue to finish DOM updates before attaching event listeners. This ensure elements exist in the DOM.
333
+ await nextTick()
334
+ // Attach event listeners to all editable elements in the Builder
335
+ await this.#addListenersToEditableElements()
336
+
337
+ this.pageBuilderStateStore.setIsRestoring(false)
338
+ this.pageBuilderStateStore.setIsLoadingGlobal(false)
339
+ }
340
+
341
+ if (config && formType === 'create') {
342
+ // Pending data for mount is null at this stage
343
+ if (typeof localStorageData === 'string') {
344
+ await this.#updateComponentsFromString(localStorageData)
345
+ this.#completeBuilderInitialization()
346
+ return
347
+ }
348
+
349
+ //
350
+ //
351
+ //
352
+ //
353
+ // Wait for Vue to finish DOM updates before attaching event listeners. This ensure elements exist in the DOM.
354
+ await nextTick()
355
+ // Attach event listeners to all editable elements in the Builder
356
+ await this.#addListenersToEditableElements()
357
+
358
+ this.pageBuilderStateStore.setIsRestoring(false)
359
+ this.pageBuilderStateStore.setIsLoadingGlobal(false)
360
+ }
361
+ }
197
362
  /**
198
363
  * - Entry point for initializing the Page Builder.
199
364
  * - Sets the builder as started in the state store.
@@ -204,53 +369,103 @@ export class PageBuilderService {
204
369
  *
205
370
  * @param config - The configuration object for the Page Builder.
206
371
  */
207
- async startBuilder(config: PageBuilderConfig): Promise<void> {
372
+ async startBuilder(
373
+ config: PageBuilderConfig,
374
+ passedComponentsArray?: BuilderResourceData,
375
+ ): Promise<StartBuilderResult> {
208
376
  // Reactive flag signals to the UI that the builder has been successfully initialized
209
377
  // Prevents builder actions to prevent errors caused by missing DOM .
210
378
  this.pageBuilderStateStore.setBuilderStarted(true)
379
+ const pagebuilder = document.querySelector('#pagebuilder')
380
+ let validation
211
381
 
212
- // Show a global loading indicator while initializing
213
- this.pageBuilderStateStore.setIsLoadingGlobal(true)
214
-
215
- // Wait briefly to ensure UI updates and async processes settle
216
-
217
- // Store the provided config in the builder's state store
218
- this.pageBuilderStateStore.setPageBuilderConfig(config)
382
+ try {
383
+ this.originalComponents = passedComponentsArray
384
+ this.pageBuilderStateStore.setPageBuilderConfig(config)
385
+ // Validate and normalize the config (ensure required fields are present)
386
+ this.#validateConfig(config)
219
387
 
220
- // Validate and normalize the config (ensure required fields are present)
221
- this.#validateConfig(config)
388
+ validation = this.#validateUserProvidedComponents(passedComponentsArray)
222
389
 
223
- // Update the localStorage key name based on the config/resource
224
- this.#updateLocalStorageItemName()
390
+ // Update the localStorage key name based on the config/resource
391
+ this.#updateLocalStorageItemName()
225
392
 
226
- this.completeBuilderInitialization()
393
+ // Page Builder is not Present in the DOM but Components have been passed to the Builder
394
+ if (passedComponentsArray && !pagebuilder) {
395
+ this.#handlePageBuilderNotPresent(passedComponentsArray)
396
+ }
397
+ // Page Builder is Present in the DOM & Components have been passed to the Builder
398
+ if (pagebuilder) {
399
+ this.#completeBuilderInitialization(passedComponentsArray)
400
+ }
227
401
 
228
- const formType = config.updateOrCreate && config.updateOrCreate.formType
229
- if (formType === 'create') {
230
- await this.mountComponentsToDOM('')
402
+ // Return both the success message and validation info if present
403
+ return {
404
+ message: 'Page builder started successfully.',
405
+ ...(validation || {}),
406
+ }
407
+ } catch (err) {
408
+ console.error('Not able to start the Page Builder', err)
409
+ this.pageBuilderStateStore.setIsLoadingGlobal(false)
410
+ return {
411
+ error: true as const,
412
+ reason: 'Failed to start the Page Builder due to an unexpected error.',
413
+ }
231
414
  }
232
415
  }
233
416
 
234
- async completeBuilderInitialization() {
235
- const pagebuilder = document.querySelector('#pagebuilder')
236
- if (!pagebuilder) return
417
+ async #completeBuilderInitialization(passedComponentsArray?: BuilderResourceData): Promise<void> {
418
+ this.pageBuilderStateStore.setIsLoadingGlobal(true)
419
+ const localStorageData = this.loadStoredComponentsFromStorage()
420
+
421
+ await this.delay(300)
237
422
 
238
423
  // Deselect any selected or hovered elements in the builder UI
239
424
  await this.clearHtmlSelection()
240
- this.pageBuilderStateStore.setIsLoadingGlobal(true)
241
- await this.delay(300)
242
425
 
243
- // Hide the global loading indicator and mark the builder as started
244
- this.pageBuilderStateStore.setIsLoadingGlobal(false)
426
+ if (passedComponentsArray) {
427
+ // Prefer components from local storage if available for this resource
428
+ if (!this.pendingMountData && localStorageData && typeof localStorageData === 'string') {
429
+ await this.#updateComponentsFromString(localStorageData)
430
+ } else {
431
+ // If no local storage is found, use the components array provided by the user
432
+ await this.#mountPassedComponentsToDOM(passedComponentsArray)
433
+ this.pendingMountData = null
434
+ }
435
+ }
245
436
 
246
- if (await this.hasLocalDraftForUpdate()) {
247
- this.pageBuilderStateStore.setHasLocalDraftForUpdate(true)
437
+ //
438
+ //
439
+ //
440
+ if (!passedComponentsArray) {
441
+ // Prefer components from local storage if available for this resource
442
+ if (localStorageData && typeof localStorageData === 'string') {
443
+ await this.#updateComponentsFromString(localStorageData)
444
+ } else {
445
+ // If no local storage is found, use the components array provided by the user
446
+ await this.#mountPassedComponentsToDOM([])
447
+ }
248
448
  }
449
+ //
450
+ //
451
+ //
452
+ //
453
+ //
454
+ //
455
+ //
456
+ //
457
+ //
458
+ //
459
+ //
460
+ //
461
+ //
249
462
 
250
463
  // Wait for Vue to finish DOM updates before attaching event listeners. This ensure elements exist in the DOM.
251
464
  await nextTick()
252
465
  // Attach event listeners to all editable elements in the Builder
253
466
  await this.#addListenersToEditableElements()
467
+ // Show a global loading indicator while initializing
468
+ this.pageBuilderStateStore.setIsLoadingGlobal(false)
254
469
 
255
470
  // Clean up any old localStorage items related to previous builder sessions
256
471
  this.deleteOldPageBuilderLocalStorage()
@@ -437,8 +652,8 @@ export class PageBuilderService {
437
652
  this.pageBuilderStateStore.setIsSaving(true)
438
653
  // Deselect any selected or hovered elements in the builder UI
439
654
  //
440
- await this.saveComponentsLocalStorage()
441
- await this.delay(500)
655
+ this.#saveDomComponentsToLocalStorage()
656
+ await this.delay(300)
442
657
  } catch (err) {
443
658
  console.error('Error trying auto save.', err)
444
659
  } finally {
@@ -449,7 +664,7 @@ export class PageBuilderService {
449
664
  if (passedConfig && !passedConfig.userSettings) {
450
665
  try {
451
666
  this.pageBuilderStateStore.setIsSaving(true)
452
- await this.saveComponentsLocalStorage()
667
+ this.#saveDomComponentsToLocalStorage()
453
668
  await this.delay(300)
454
669
  } catch (err) {
455
670
  console.error('Error trying saving.', err)
@@ -475,7 +690,7 @@ export class PageBuilderService {
475
690
  passedConfig.userSettings.autoSave)
476
691
  ) {
477
692
  this.pageBuilderStateStore.setIsSaving(true)
478
- await this.saveComponentsLocalStorage()
693
+ this.#saveDomComponentsToLocalStorage()
479
694
  await this.delay(300)
480
695
 
481
696
  this.pageBuilderStateStore.setIsSaving(false)
@@ -483,7 +698,7 @@ export class PageBuilderService {
483
698
  }
484
699
  if (passedConfig && !passedConfig.userSettings) {
485
700
  this.pageBuilderStateStore.setIsSaving(true)
486
- await this.saveComponentsLocalStorage()
701
+ this.#saveDomComponentsToLocalStorage()
487
702
  await this.delay(300)
488
703
 
489
704
  this.pageBuilderStateStore.setIsSaving(false)
@@ -494,15 +709,16 @@ export class PageBuilderService {
494
709
  // Deep clone clone component
495
710
  const clonedComponent = { ...componentObject }
496
711
 
712
+ const pageBuilder = document.querySelector('#contains-pagebuilder')
497
713
  // scoll to top or bottom # end
498
- if (this.containsPagebuilder) {
714
+ if (pageBuilder) {
499
715
  if (
500
716
  this.getComponentArrayAddMethod.value === 'unshift' ||
501
717
  this.getComponentArrayAddMethod.value === 'push'
502
718
  ) {
503
719
  // push to top
504
720
  if (this.getComponentArrayAddMethod.value === 'unshift') {
505
- this.containsPagebuilder.scrollTo({
721
+ pageBuilder.scrollTo({
506
722
  top: 0,
507
723
  behavior: 'smooth',
508
724
  })
@@ -510,9 +726,8 @@ export class PageBuilderService {
510
726
 
511
727
  // push to bottom
512
728
  if (this.getComponentArrayAddMethod.value === 'push') {
513
- const maxHeight = this.containsPagebuilder.scrollHeight
514
- this.containsPagebuilder.scrollTo({
515
- top: maxHeight,
729
+ pageBuilder.scrollTo({
730
+ top: 0,
516
731
  behavior: 'smooth',
517
732
  })
518
733
  }
@@ -606,80 +821,6 @@ export class PageBuilderService {
606
821
  }
607
822
  }
608
823
 
609
- handleRemoveClasses(userSelectedClass: string): void {
610
- // remove selected class from element
611
- if (this.getElement.value?.classList.contains(userSelectedClass)) {
612
- this.getElement.value?.classList.remove(userSelectedClass)
613
-
614
- this.pageBuilderStateStore.setElement(this.getElement.value)
615
- this.pageBuilderStateStore.removeClass(userSelectedClass)
616
- }
617
- }
618
-
619
- handleDeleteElement() {
620
- // Get the element to be deleted
621
- const element = this.getElement.value
622
-
623
- if (!element) return
624
-
625
- if (!element?.parentNode) {
626
- this.pageBuilderStateStore.setComponent(null)
627
- this.pageBuilderStateStore.setElement(null)
628
- return
629
- }
630
-
631
- // Store the parent of the deleted element
632
- // if parent element tag is section remove the hole component
633
- if (element.parentElement?.tagName !== 'SECTION') {
634
- this.pageBuilderStateStore.setParentElement(element.parentNode as HTMLElement)
635
-
636
- // Store the outerHTML of the deleted element
637
- this.pageBuilderStateStore.setRestoredElement(element.outerHTML)
638
-
639
- // Store the next sibling of the deleted element
640
- this.pageBuilderStateStore.setNextSibling(element.nextSibling as HTMLElement | null)
641
- }
642
-
643
- // if parent element tag is section remove the hole component
644
- if (element.parentElement?.tagName === 'SECTION') {
645
- this.deleteSelectedComponent()
646
- }
647
-
648
- // Remove the element from the DOM
649
- element.remove()
650
- this.pageBuilderStateStore.setComponent(null)
651
- this.pageBuilderStateStore.setElement(null)
652
- }
653
-
654
- async handleRestoreElement() {
655
- // Get the stored deleted element and its parent
656
- if (this.getRestoredElement.value && this.getParentElement.value) {
657
- // Create a new element from the stored outerHTML
658
- const newElement = document.createElement('div')
659
- // Fixed type conversion issue
660
- newElement.innerHTML = this.getRestoredElement.value
661
-
662
- // Append the restored element to its parent
663
- // Insert the restored element before its next sibling in its parent
664
- if (newElement.firstChild && this.getParentElement.value) {
665
- // insertBefore can accept null as second parameter (will append to end)
666
- this.getParentElement.value.insertBefore(newElement.firstChild, this.getNextSibling.value)
667
- }
668
- }
669
-
670
- // Clear
671
- this.pageBuilderStateStore.setParentElement(null)
672
- this.pageBuilderStateStore.setRestoredElement(null)
673
- this.pageBuilderStateStore.setNextSibling(null)
674
- this.pageBuilderStateStore.setComponent(null)
675
- this.pageBuilderStateStore.setElement(null)
676
-
677
- // Wait for Vue to finish DOM updates before attaching event listeners. This ensure elements exist in the DOM.
678
- await nextTick()
679
- // Attach event listeners to all editable elements in the Builder
680
- await this.#addListenersToEditableElements()
681
- }
682
-
683
824
  handleFontWeight(userSelectedFontWeight?: string): void {
684
825
  this.#applyElementClassChanges(
685
826
  userSelectedFontWeight,
@@ -840,15 +981,40 @@ export class PageBuilderService {
840
981
  this.#applyElementClassChanges(opacity, tailwindOpacities.opacities, 'setOpacity')
841
982
  }
842
983
 
843
- deleteAllComponents() {
984
+ /**
985
+ * Removes all components from both the builder state and the DOM.
986
+ *
987
+ * - First clears the components array in the store.
988
+ * - Then, as a defensive measure, also manually removes all sections elements from the DOM.
989
+ *
990
+ * This manual DOM clearing is only optional
991
+ *
992
+ */
993
+
994
+ deleteAllComponentsFromDOM() {
995
+ // Clear the store
844
996
  this.pageBuilderStateStore.setComponents([])
997
+
998
+ // Also clear the DOM
999
+ const pagebuilder = document.querySelector('#pagebuilder')
1000
+ if (pagebuilder) {
1001
+ // Remove all section elements (assuming each component is a <section>)
1002
+ pagebuilder
1003
+ .querySelectorAll('section[data-componentid]')
1004
+ .forEach((section) => section.remove())
1005
+ }
845
1006
  }
846
1007
 
847
- async deleteSelectedComponent() {
848
- if (!this.getComponents.value || !this.getComponent.value) return
1008
+ async deleteComponentFromDOM() {
1009
+ this.#syncDomToStoreOnly()
1010
+ await nextTick()
1011
+
1012
+ const components = this.getComponents.value
1013
+
1014
+ if (!components) return
849
1015
 
850
1016
  // Find the index of the component to delete
851
- const indexToDelete = this.getComponents.value.findIndex(
1017
+ const indexToDelete = components.findIndex(
852
1018
  (component) => component.id === this.getComponent.value?.id,
853
1019
  )
854
1020
 
@@ -857,17 +1023,99 @@ export class PageBuilderService {
857
1023
  return
858
1024
  }
859
1025
 
860
- // Remove the component from the array
861
- this.getComponents.value.splice(indexToDelete, 1)
862
- this.pageBuilderStateStore.setComponents(this.getComponents.value)
1026
+ // Create a new array without the deleted component (avoid mutating original array)
1027
+ const newComponents = [
1028
+ ...components.slice(0, indexToDelete),
1029
+ ...components.slice(indexToDelete + 1),
1030
+ ]
863
1031
 
864
- // Wait for Vue to finish DOM updates before attaching event listeners. This ensure elements exist in the DOM.
1032
+ this.pageBuilderStateStore.setComponents(newComponents)
1033
+
1034
+ // Remove the section from the DOM as well
1035
+ const pagebuilder = document.querySelector('#pagebuilder')
1036
+ if (pagebuilder && this.getComponent.value?.id) {
1037
+ const section = pagebuilder.querySelector(
1038
+ `section[data-componentid="${this.getComponent.value.id}"]`,
1039
+ )
1040
+ if (section) section.remove()
1041
+ }
1042
+
1043
+ // Wait for Vue to finish DOM updates before attaching event listeners.
865
1044
  await nextTick()
866
- // Attach event listeners to all editable elements in the Builder
867
1045
  await this.#addListenersToEditableElements()
868
1046
 
869
1047
  this.pageBuilderStateStore.setComponent(null)
870
1048
  this.pageBuilderStateStore.setElement(null)
1049
+
1050
+ // Optional: auto-save after deletion
1051
+ await this.handleAutoSave()
1052
+ }
1053
+
1054
+ async deleteElementFromDOM() {
1055
+ const element = this.getElement.value
1056
+ if (!element) return
1057
+
1058
+ if (!element.parentNode) {
1059
+ this.pageBuilderStateStore.setComponent(null)
1060
+ this.pageBuilderStateStore.setElement(null)
1061
+ return
1062
+ }
1063
+
1064
+ // If the element is inside a section, but not the section itself, store info for undo/restore
1065
+ if (element.parentElement?.tagName !== 'SECTION') {
1066
+ this.pageBuilderStateStore.setParentElement(element.parentNode as HTMLElement)
1067
+ this.pageBuilderStateStore.setRestoredElement(element.outerHTML)
1068
+ this.pageBuilderStateStore.setNextSibling(element.nextSibling as HTMLElement | null)
1069
+ // Remove only the element itself from the DOM
1070
+ element.remove()
1071
+ } else {
1072
+ // If the element's parent is a section, remove the whole component (section)
1073
+ await this.deleteComponentFromDOM()
1074
+ // No need to call element.remove() here, as the section is already removed
1075
+ }
1076
+
1077
+ // Clear selection state
1078
+ this.pageBuilderStateStore.setComponent(null)
1079
+ this.pageBuilderStateStore.setElement(null)
1080
+ }
1081
+
1082
+ async restoreDeletedElementToDOM() {
1083
+ // Restore the previously deleted element to the DOM
1084
+ const restoredHTML = this.getRestoredElement.value
1085
+ const parent = this.getParentElement.value
1086
+ const nextSibling = this.getNextSibling.value
1087
+
1088
+ if (restoredHTML && parent) {
1089
+ // Create a container and parse the HTML
1090
+ const container = document.createElement('div')
1091
+ container.innerHTML = restoredHTML
1092
+
1093
+ // Insert the restored element before its next sibling (or append if null)
1094
+ if (container.firstChild) {
1095
+ parent.insertBefore(container.firstChild, nextSibling)
1096
+ }
1097
+ }
1098
+
1099
+ // Clear restore-related state
1100
+ this.pageBuilderStateStore.setParentElement(null)
1101
+ this.pageBuilderStateStore.setRestoredElement(null)
1102
+ this.pageBuilderStateStore.setNextSibling(null)
1103
+ this.pageBuilderStateStore.setComponent(null)
1104
+ this.pageBuilderStateStore.setElement(null)
1105
+
1106
+ // Wait for Vue to finish DOM updates before attaching event listeners
1107
+ await nextTick()
1108
+ await this.#addListenersToEditableElements()
1109
+ }
1110
+
1111
+ handleRemoveClasses(userSelectedClass: string): void {
1112
+ // remove selected class from element
1113
+ if (this.getElement.value?.classList.contains(userSelectedClass)) {
1114
+ this.getElement.value?.classList.remove(userSelectedClass)
1115
+
1116
+ this.pageBuilderStateStore.setElement(this.getElement.value)
1117
+ this.pageBuilderStateStore.removeClass(userSelectedClass)
1118
+ }
871
1119
  }
872
1120
 
873
1121
  // move component
@@ -932,7 +1180,7 @@ export class PageBuilderService {
932
1180
  }
933
1181
 
934
1182
  if (typeof this.getElement.value.innerHTML === 'string') {
935
- await this.nextTick
1183
+ await nextTick()
936
1184
 
937
1185
  // Update text content
938
1186
  this.getElement.value.textContent = textContentVueModel
@@ -1038,21 +1286,25 @@ export class PageBuilderService {
1038
1286
  }
1039
1287
 
1040
1288
  #updateLocalStorageItemName(): void {
1041
- const updateOrCreate =
1289
+ const formtype =
1042
1290
  this.pageBuilderStateStore.getPageBuilderConfig &&
1043
1291
  this.pageBuilderStateStore.getPageBuilderConfig.updateOrCreate &&
1044
1292
  this.pageBuilderStateStore.getPageBuilderConfig.updateOrCreate.formType
1045
1293
 
1046
- const resourceData = this.pageBuilderStateStore.getPageBuilderConfig?.resourceData
1294
+ const formname =
1295
+ this.pageBuilderStateStore.getPageBuilderConfig &&
1296
+ this.pageBuilderStateStore.getPageBuilderConfig.updateOrCreate &&
1297
+ this.pageBuilderStateStore.getPageBuilderConfig.updateOrCreate.formName
1047
1298
 
1048
- const resourceFormName =
1049
- this.pageBuilderStateStore.getPageBuilderConfig?.updateOrCreate?.formName
1299
+ const resourceData =
1300
+ this.pageBuilderStateStore.getPageBuilderConfig &&
1301
+ this.pageBuilderStateStore.getPageBuilderConfig.resourceData
1050
1302
 
1051
1303
  // Logic for create resource
1052
- if (updateOrCreate === 'create') {
1053
- if (resourceFormName && resourceFormName.length > 0) {
1304
+ if (formtype === 'create') {
1305
+ if (formname && formname.length > 0) {
1054
1306
  this.pageBuilderStateStore.setLocalStorageItemName(
1055
- `page-builder-create-resource-${this.sanitizeForLocalStorage(resourceFormName)}`,
1307
+ `page-builder-create-resource-${this.sanitizeForLocalStorage(formname)}`,
1056
1308
  )
1057
1309
  return
1058
1310
  }
@@ -1063,15 +1315,15 @@ export class PageBuilderService {
1063
1315
 
1064
1316
  // Logic for create
1065
1317
  // Logic for update and with resource form name
1066
- if (updateOrCreate === 'update') {
1067
- if (typeof resourceFormName === 'string' && resourceFormName.length > 0) {
1318
+ if (formtype === 'update') {
1319
+ if (typeof formname === 'string' && formname.length > 0) {
1068
1320
  //
1069
1321
  //
1070
1322
  if (resourceData && resourceData != null && !resourceData.title) {
1071
1323
  // Check if id is missing, null, undefined, or an empty string (after trimming)
1072
1324
  if (!resourceData.id || typeof resourceData.id === 'string') {
1073
1325
  this.pageBuilderStateStore.setLocalStorageItemName(
1074
- `page-builder-update-resource-${this.sanitizeForLocalStorage(resourceFormName)}`,
1326
+ `page-builder-update-resource-${this.sanitizeForLocalStorage(formname)}`,
1075
1327
  )
1076
1328
  return
1077
1329
  }
@@ -1086,7 +1338,7 @@ export class PageBuilderService {
1086
1338
  ) {
1087
1339
  if (!resourceData.id || typeof resourceData.id === 'string') {
1088
1340
  this.pageBuilderStateStore.setLocalStorageItemName(
1089
- `page-builder-update-resource-${this.sanitizeForLocalStorage(resourceFormName)}-${this.sanitizeForLocalStorage(resourceData.title)}`,
1341
+ `page-builder-update-resource-${this.sanitizeForLocalStorage(formname)}-${this.sanitizeForLocalStorage(resourceData.title)}`,
1090
1342
  )
1091
1343
  return
1092
1344
  }
@@ -1098,7 +1350,7 @@ export class PageBuilderService {
1098
1350
  if (!resourceData.title && typeof resourceData.title !== 'string') {
1099
1351
  if (resourceData.id || typeof resourceData.id === 'number') {
1100
1352
  this.pageBuilderStateStore.setLocalStorageItemName(
1101
- `page-builder-update-resource-${this.sanitizeForLocalStorage(resourceFormName)}-${this.sanitizeForLocalStorage(String(resourceData.id))}`,
1353
+ `page-builder-update-resource-${this.sanitizeForLocalStorage(formname)}-${this.sanitizeForLocalStorage(String(resourceData.id))}`,
1102
1354
  )
1103
1355
  return
1104
1356
  }
@@ -1114,7 +1366,7 @@ export class PageBuilderService {
1114
1366
  ) {
1115
1367
  if (resourceData.id || typeof resourceData.id === 'number') {
1116
1368
  this.pageBuilderStateStore.setLocalStorageItemName(
1117
- `page-builder-update-resource-${this.sanitizeForLocalStorage(resourceFormName)}-${this.sanitizeForLocalStorage(resourceData.title)}-${this.sanitizeForLocalStorage(String(resourceData.id))}`,
1369
+ `page-builder-update-resource-${this.sanitizeForLocalStorage(formname)}-${this.sanitizeForLocalStorage(resourceData.title)}-${this.sanitizeForLocalStorage(String(resourceData.id))}`,
1118
1370
  )
1119
1371
  return
1120
1372
  }
@@ -1122,11 +1374,8 @@ export class PageBuilderService {
1122
1374
  }
1123
1375
  }
1124
1376
 
1125
- // Logic for update without without resourceFormName
1126
- if (
1127
- !resourceFormName ||
1128
- (typeof resourceFormName === 'string' && resourceFormName.length === 0)
1129
- ) {
1377
+ // Logic for update without without formname
1378
+ if (!formname || (typeof formname === 'string' && formname.length === 0)) {
1130
1379
  //
1131
1380
  //
1132
1381
  if (resourceData && resourceData != null && !resourceData.title) {
@@ -1204,11 +1453,31 @@ export class PageBuilderService {
1204
1453
  }
1205
1454
 
1206
1455
  /**
1207
- * Components from DOM JS (not JS DOM). øøø
1208
- * Saving the current DOM state into JS this.getComponents (for example, before saving to localStorage).
1209
- * This function Only copies the current DOM HTML into JS this.getComponents (component.html_code).
1456
+ * Syncs the current DOM state into the in-memory store (getComponents),
1457
+ * but does NOT save to localStorage.
1458
+ */
1459
+ #syncDomToStoreOnly() {
1460
+ const pagebuilder = document.querySelector('#pagebuilder')
1461
+ if (!pagebuilder) return
1462
+
1463
+ const componentsToSave: { html_code: string; id: string | null; title: string }[] = []
1464
+
1465
+ pagebuilder.querySelectorAll('section[data-componentid]').forEach((section) => {
1466
+ const sanitizedSection = this.#cloneAndRemoveSelectionAttributes(section as HTMLElement)
1467
+ componentsToSave.push({
1468
+ html_code: sanitizedSection.outerHTML,
1469
+ id: sanitizedSection.getAttribute('data-componentid'),
1470
+ title: sanitizedSection.getAttribute('data-component-title') || 'Untitled Component',
1471
+ })
1472
+ })
1473
+
1474
+ this.pageBuilderStateStore.setComponents(componentsToSave)
1475
+ }
1476
+
1477
+ /**
1478
+ * Saves the current DOM state (components) to localStorage.
1210
1479
  */
1211
- #domToComponentsSync = async () => {
1480
+ #saveDomComponentsToLocalStorage() {
1212
1481
  const pagebuilder = document.querySelector('#pagebuilder')
1213
1482
  if (!pagebuilder) return
1214
1483
 
@@ -1240,34 +1509,23 @@ export class PageBuilderService {
1240
1509
  if (keyForSavingFromDomToLocal && typeof keyForSavingFromDomToLocal === 'string') {
1241
1510
  localStorage.setItem(keyForSavingFromDomToLocal, JSON.stringify(dataToSave))
1242
1511
  }
1243
-
1244
- // No DOM mutation here!
1245
- await new Promise<void>((resolve) => resolve())
1246
- }
1247
-
1248
- // save to local storage
1249
- async saveComponentsLocalStorage() {
1250
- await this.nextTick
1251
-
1252
- await this.#domToComponentsSync()
1253
1512
  }
1254
1513
 
1255
- async removeItemComponentsLocalStorage() {
1256
- await this.nextTick
1514
+ async removeCurrentComponentsFromLocalStorage() {
1515
+ await nextTick()
1257
1516
 
1258
- if (this.getLocalStorageItemName.value) {
1259
- localStorage.removeItem(this.getLocalStorageItemName.value)
1517
+ const key = this.getLocalStorageItemName.value
1518
+ if (key) {
1519
+ localStorage.removeItem(key)
1260
1520
  }
1261
1521
  }
1262
1522
 
1263
1523
  //
1264
1524
  deleteOldPageBuilderLocalStorage(): void {
1265
- if (
1266
- this.pageBuilderStateStore.getPageBuilderConfig &&
1267
- this.pageBuilderStateStore.getPageBuilderConfig.updateOrCreate &&
1268
- typeof this.pageBuilderStateStore.getPageBuilderConfig.updateOrCreate.formType === 'string' &&
1269
- this.pageBuilderStateStore.getPageBuilderConfig.updateOrCreate.formType === 'update'
1270
- ) {
1525
+ const config = this.pageBuilderStateStore.getPageBuilderConfig
1526
+ const formType = config && config.updateOrCreate && config.updateOrCreate.formType
1527
+
1528
+ if (formType === 'update') {
1271
1529
  let oldCountLocalStorages = 0
1272
1530
  const deletedItemsLog: { Number: number; Key: string; SavedAt: string }[] = []
1273
1531
 
@@ -1315,38 +1573,6 @@ export class PageBuilderService {
1315
1573
  }
1316
1574
  }
1317
1575
 
1318
- async hasLocalDraftForUpdate(): Promise<boolean> {
1319
- const pagebuilder = document.querySelector('#pagebuilder')
1320
- if (!pagebuilder) {
1321
- return true
1322
- }
1323
-
1324
- if (this.hasStartedEditing) return false
1325
-
1326
- if (
1327
- this.pageBuilderStateStore.getPageBuilderConfig &&
1328
- this.pageBuilderStateStore.getPageBuilderConfig.updateOrCreate &&
1329
- typeof this.pageBuilderStateStore.getPageBuilderConfig.updateOrCreate.formType === 'string' &&
1330
- this.pageBuilderStateStore.getPageBuilderConfig.updateOrCreate.formType === 'update'
1331
- ) {
1332
- const key = this.getLocalStorageItemName.value
1333
- if (typeof key === 'string') {
1334
- const draft = localStorage.getItem(key)
1335
- if (draft) {
1336
- try {
1337
- await this.delay(500)
1338
- this.pageBuilderStateStore.setHasLocalDraftForUpdate(false)
1339
- return true
1340
- } catch (err) {
1341
- console.error('Unable to mount components to DOM.', err)
1342
- return false
1343
- }
1344
- }
1345
- }
1346
- }
1347
- return false
1348
- }
1349
-
1350
1576
  // Call this when the user starts editing (e.g., on first change or when resuming a draft)
1351
1577
  startEditing() {
1352
1578
  this.hasStartedEditing = true
@@ -1354,40 +1580,48 @@ export class PageBuilderService {
1354
1580
 
1355
1581
  //
1356
1582
  async resumeEditingForUpdate() {
1357
- if (
1358
- this.pageBuilderStateStore.getPageBuilderConfig &&
1359
- this.pageBuilderStateStore.getPageBuilderConfig.updateOrCreate &&
1360
- typeof this.pageBuilderStateStore.getPageBuilderConfig.updateOrCreate.formType === 'string' &&
1361
- this.pageBuilderStateStore.getPageBuilderConfig.updateOrCreate.formType === 'update'
1362
- ) {
1363
- const key = this.getLocalStorageItemName.value
1583
+ const config = this.pageBuilderStateStore.getPageBuilderConfig
1584
+ const formType = config && config.updateOrCreate && config.updateOrCreate.formType
1364
1585
 
1365
- if (typeof key === 'string') {
1366
- const updateDraftFromLocalStorage = localStorage.getItem(key)
1586
+ if (formType !== 'update') return
1587
+ //
1588
+ //
1589
+ //
1367
1590
 
1368
- if (typeof updateDraftFromLocalStorage === 'string') {
1369
- this.pageBuilderStateStore.setIsLoadingResumeEditing(true)
1370
- await delay(500)
1371
- this.mountComponentsToDOM(updateDraftFromLocalStorage)
1372
- this.pageBuilderStateStore.setIsLoadingResumeEditing(false)
1373
- }
1591
+ const key = this.getLocalStorageItemName.value
1592
+
1593
+ if (typeof key === 'string') {
1594
+ const updateDraftFromLocalStorage = localStorage.getItem(key)
1595
+
1596
+ if (typeof updateDraftFromLocalStorage === 'string') {
1597
+ this.pageBuilderStateStore.setIsLoadingResumeEditing(true)
1598
+ localStorage.removeItem(key)
1599
+ await delay(300)
1600
+ await this.#updateComponentsFromString(updateDraftFromLocalStorage)
1601
+ this.pageBuilderStateStore.setIsLoadingResumeEditing(false)
1374
1602
  }
1375
1603
  }
1604
+
1605
+ // Wait for Vue to finish DOM updates before attaching event listeners. This ensure elements exist in the DOM.
1606
+ await nextTick()
1607
+ // Attach event listeners to all editable elements in the Builder
1608
+ await this.#addListenersToEditableElements()
1609
+ // set loading to false
1610
+ this.pageBuilderStateStore.setIsLoadingResumeEditing(false)
1376
1611
  }
1377
1612
 
1378
1613
  async restoreOriginalContent() {
1379
- if (
1380
- this.pageBuilderStateStore.getPageBuilderConfig &&
1381
- this.pageBuilderStateStore.getPageBuilderConfig.updateOrCreate &&
1382
- typeof this.pageBuilderStateStore.getPageBuilderConfig.updateOrCreate.formType === 'string' &&
1383
- this.pageBuilderStateStore.getPageBuilderConfig.updateOrCreate.formType === 'update'
1384
- ) {
1614
+ const config = this.pageBuilderStateStore.getPageBuilderConfig
1615
+ const formType = config && config.updateOrCreate && config.updateOrCreate.formType
1616
+
1617
+ if (formType === 'update') {
1385
1618
  this.pageBuilderStateStore.setIsRestoring(true)
1386
1619
  await this.delay(300)
1387
1620
 
1388
1621
  // Restore the original content if available
1389
- if (this.originalComponents) {
1390
- this.mountComponentsToDOM(this.originalComponents)
1622
+ if (Array.isArray(this.originalComponents)) {
1623
+ await this.#mountPassedComponentsToDOM(this.originalComponents)
1624
+ this.removeCurrentComponentsFromLocalStorage()
1391
1625
  }
1392
1626
 
1393
1627
  // Wait for Vue to finish DOM updates before attaching event listeners. This ensure elements exist in the DOM.
@@ -1441,7 +1675,7 @@ export class PageBuilderService {
1441
1675
 
1442
1676
  // Only apply if an image is staged
1443
1677
  if (this.getApplyImageToSelection.value && this.getApplyImageToSelection.value.src) {
1444
- await this.nextTick
1678
+ await nextTick()
1445
1679
  this.pageBuilderStateStore.setBasePrimaryImage(`${this.getApplyImageToSelection.value.src}`)
1446
1680
 
1447
1681
  await this.handleAutoSave()
@@ -1698,19 +1932,22 @@ export class PageBuilderService {
1698
1932
  * @param data - JSON string (e.g., '[{"html_code":"...","id":"123","title":"..."}]')
1699
1933
  * OR HTML string (e.g., '<section data-componentid="123">...</section>')
1700
1934
  */
1701
- async #setComponentsFromData(htmlString: string): Promise<void> {
1935
+ async #updateComponentsFromString(htmlString: string): Promise<void> {
1702
1936
  // Auto-detect if input is JSON or HTML
1703
1937
  const trimmedData = htmlString.trim()
1704
1938
 
1705
1939
  if (trimmedData.startsWith('[') || trimmedData.startsWith('{')) {
1706
1940
  // Looks like JSON - parse as JSON
1707
1941
  await this.#parseJSONComponents(trimmedData)
1708
- } else if (trimmedData.startsWith('<')) {
1942
+ return
1943
+ }
1944
+ if (trimmedData.startsWith('<')) {
1709
1945
  // Looks like HTML - parse as HTML
1710
1946
  await this.#parseHTMLComponents(trimmedData)
1711
- } else {
1712
- await this.#parseJSONComponents(trimmedData)
1947
+ return
1713
1948
  }
1949
+
1950
+ await this.#parseJSONComponents(trimmedData)
1714
1951
  }
1715
1952
 
1716
1953
  // Private method to parse JSON components and save pageBuilderContentSavedAt to localStorage
@@ -1772,7 +2009,7 @@ export class PageBuilderService {
1772
2009
  await this.#addListenersToEditableElements()
1773
2010
  } catch (error) {
1774
2011
  console.error('Error parsing JSON components:', error)
1775
- this.pageBuilderStateStore.setComponents([])
2012
+ this.deleteAllComponentsFromDOM()
1776
2013
  }
1777
2014
  }
1778
2015
  // Private method to parse HTML components
@@ -1827,111 +2064,7 @@ export class PageBuilderService {
1827
2064
  await this.#addListenersToEditableElements()
1828
2065
  } catch (error) {
1829
2066
  console.error('Error parsing HTML components:', error)
1830
- this.pageBuilderStateStore.setComponents([])
1831
- }
1832
- }
1833
-
1834
- /**
1835
- * Mount Components to DOM
1836
- * @param passedData - HTML/JSON string to inject (optional)
1837
- * @param preferLocalStorage - if true, always try localStorage first
1838
- */
1839
- async mountComponentsToDOM(passedData: string): Promise<void> {
1840
- const pagebuilder = document.querySelector('#pagebuilder')
1841
-
1842
- // If #pagebuilder is not present, cache the data and exit
1843
- if (!pagebuilder) {
1844
- // For 'create', set pendingMountData to '' (empty string)
1845
- const config = this.pageBuilderStateStore.getPageBuilderConfig
1846
- const formType = config && config.updateOrCreate && config.updateOrCreate.formType
1847
- if (formType === 'create') {
1848
- this.pendingMountData = ''
1849
- } else {
1850
- this.pendingMountData = passedData
1851
- }
1852
- return
1853
- }
1854
-
1855
- // Clear the cache if we are mounting now
1856
- this.pendingMountData = null
1857
-
1858
- this.pageBuilderStateStore.setComponents([])
1859
-
1860
- // On from type update Save Original Post
1861
- if (
1862
- this.pageBuilderStateStore.getPageBuilderConfig &&
1863
- this.pageBuilderStateStore.getPageBuilderConfig.updateOrCreate &&
1864
- typeof this.pageBuilderStateStore.getPageBuilderConfig.updateOrCreate.formType === 'string' &&
1865
- this.pageBuilderStateStore.getPageBuilderConfig.updateOrCreate.formType === 'update' &&
1866
- passedData &&
1867
- !this.originalComponents
1868
- ) {
1869
- this.originalComponents = passedData
1870
- }
1871
-
1872
- // Form type Update
1873
- if (
1874
- this.pageBuilderStateStore.getPageBuilderConfig &&
1875
- this.pageBuilderStateStore.getPageBuilderConfig.updateOrCreate &&
1876
- typeof this.pageBuilderStateStore.getPageBuilderConfig.updateOrCreate.formType === 'string' &&
1877
- this.pageBuilderStateStore.getPageBuilderConfig.updateOrCreate.formType === 'update'
1878
- ) {
1879
- if (passedData) {
1880
- await this.#setComponentsFromData(passedData)
1881
- return
1882
- }
1883
- }
1884
-
1885
- // Form type Create
1886
- const localStorageData = this.loadStoredComponentsFromStorage()
1887
-
1888
- if (
1889
- this.pageBuilderStateStore.getPageBuilderConfig &&
1890
- this.pageBuilderStateStore.getPageBuilderConfig.updateOrCreate &&
1891
- typeof this.pageBuilderStateStore.getPageBuilderConfig.updateOrCreate.formType === 'string' &&
1892
- this.pageBuilderStateStore.getPageBuilderConfig.updateOrCreate.formType === 'create'
1893
- ) {
1894
- if (localStorageData) {
1895
- await this.#setComponentsFromData(localStorageData)
1896
- return
1897
- }
1898
-
1899
- // If no localStorage, but passedData exists (for demo), use it
1900
- if (passedData) {
1901
- await this.#setComponentsFromData(passedData)
1902
- return
1903
- }
1904
- }
1905
- }
1906
-
1907
- async tryMountPendingData() {
1908
- const pagebuilder = document.querySelector('#pagebuilder')
1909
-
1910
- // Only run if #pagebuilder exists
1911
- if (!pagebuilder) return
1912
-
1913
- // If pendingMountData is a non-empty string (update or demo), always mount
1914
- if (this.pendingMountData && typeof this.pendingMountData === 'string') {
1915
- await this.mountComponentsToDOM(this.pendingMountData)
1916
- this.pendingMountData = null
1917
- this.completeBuilderInitialization()
1918
- return
1919
- }
1920
-
1921
- // If pendingMountData is exactly '', and formType is 'create', and no components are mounted, mount for create
1922
- const config = this.pageBuilderStateStore.getPageBuilderConfig
1923
- const formType = config && config.updateOrCreate && config.updateOrCreate.formType
1924
- const components = this.pageBuilderStateStore.getComponents
1925
-
1926
- if (
1927
- this.pendingMountData === '' &&
1928
- formType === 'create' &&
1929
- (!components || (Array.isArray(components) && components.length === 0))
1930
- ) {
1931
- await this.mountComponentsToDOM('')
1932
- this.pendingMountData = null
1933
- this.completeBuilderInitialization()
1934
- return
2067
+ this.deleteAllComponentsFromDOM()
1935
2068
  }
1936
2069
  }
1937
2070