@myissue/vue-website-page-builder 3.4.20 → 3.4.22

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.
Files changed (89) hide show
  1. package/README.md +22 -24
  2. package/dist/vue-website-page-builder.js +1 -1
  3. package/dist/vue-website-page-builder.umd.cjs +1 -1
  4. package/package.json +2 -3
  5. package/src/App.vue +0 -28
  6. package/src/Components/Homepage/Footer.vue +0 -32
  7. package/src/Components/Homepage/Navbar.vue +0 -25
  8. package/src/Components/Layouts/FullWidthElement.vue +0 -34
  9. package/src/Components/Loaders/GlobalLoader.vue +0 -18
  10. package/src/Components/Modals/BuilderComponents.vue +0 -53
  11. package/src/Components/Modals/DynamicModalBuilder.vue +0 -209
  12. package/src/Components/Modals/MediaLibraryModal.vue +0 -61
  13. package/src/Components/Modals/ModalBuilder.vue +0 -117
  14. package/src/Components/PageBuilder/EditorMenu/Editables/BackgroundColorEditor.vue +0 -121
  15. package/src/Components/PageBuilder/EditorMenu/Editables/BorderRadius.vue +0 -188
  16. package/src/Components/PageBuilder/EditorMenu/Editables/Borders.vue +0 -176
  17. package/src/Components/PageBuilder/EditorMenu/Editables/ClassEditor.vue +0 -112
  18. package/src/Components/PageBuilder/EditorMenu/Editables/ComponentTopMenu.vue +0 -108
  19. package/src/Components/PageBuilder/EditorMenu/Editables/EditGetElement.vue +0 -541
  20. package/src/Components/PageBuilder/EditorMenu/Editables/HTMLEditor.vue +0 -203
  21. package/src/Components/PageBuilder/EditorMenu/Editables/ImageEditor.vue +0 -96
  22. package/src/Components/PageBuilder/EditorMenu/Editables/LinkEditor.vue +0 -315
  23. package/src/Components/PageBuilder/EditorMenu/Editables/ManageBackgroundOpacity.vue +0 -107
  24. package/src/Components/PageBuilder/EditorMenu/Editables/ManageOpacity.vue +0 -106
  25. package/src/Components/PageBuilder/EditorMenu/Editables/Margin.vue +0 -116
  26. package/src/Components/PageBuilder/EditorMenu/Editables/OpacityEditor.vue +0 -19
  27. package/src/Components/PageBuilder/EditorMenu/Editables/Padding.vue +0 -117
  28. package/src/Components/PageBuilder/EditorMenu/Editables/StyleEditor.vue +0 -123
  29. package/src/Components/PageBuilder/EditorMenu/Editables/TextColorEditor.vue +0 -123
  30. package/src/Components/PageBuilder/EditorMenu/Editables/Typography.vue +0 -237
  31. package/src/Components/PageBuilder/EditorMenu/EditorAccordion.vue +0 -46
  32. package/src/Components/PageBuilder/EditorMenu/RightSidebarEditor.vue +0 -368
  33. package/src/Components/PageBuilder/Settings/AdvancedPageBuilderSettings.vue +0 -476
  34. package/src/Components/PageBuilder/Settings/PageBuilderSettings.vue +0 -581
  35. package/src/Components/PageBuilder/ToolbarOption/ToolbarOption.vue +0 -248
  36. package/src/Components/PageBuilder/UndoRedo/UndoRedo.vue +0 -90
  37. package/src/Components/TipTap/TipTap.vue +0 -25
  38. package/src/Components/TipTap/TipTapInput.vue +0 -341
  39. package/src/PageBuilder/PageBuilder.vue +0 -1025
  40. package/src/PageBuilder/Preview.vue +0 -64
  41. package/src/composables/builderInstance.ts +0 -47
  42. package/src/composables/delay.ts +0 -5
  43. package/src/composables/extractCleanHTMLFromPageBuilder.ts +0 -59
  44. package/src/composables/preloadImage.ts +0 -8
  45. package/src/composables/useDebounce.ts +0 -8
  46. package/src/composables/usePageBuilderModal.ts +0 -25
  47. package/src/composables/useTranslations.ts +0 -28
  48. package/src/css/dev-global.css +0 -24
  49. package/src/css/style.css +0 -600
  50. package/src/helpers/isEmptyObject.ts +0 -5
  51. package/src/index.ts +0 -28
  52. package/src/locales/ar.json +0 -170
  53. package/src/locales/de.json +0 -171
  54. package/src/locales/en.json +0 -171
  55. package/src/locales/es.json +0 -171
  56. package/src/locales/fr.json +0 -171
  57. package/src/locales/hi.json +0 -172
  58. package/src/locales/ja.json +0 -171
  59. package/src/locales/pt.json +0 -171
  60. package/src/locales/ru.json +0 -171
  61. package/src/locales/zh-Hans.json +0 -171
  62. package/src/main.ts +0 -14
  63. package/src/plugin.ts +0 -16
  64. package/src/services/LocalStorageManager.ts +0 -25
  65. package/src/services/PageBuilderService.ts +0 -3191
  66. package/src/stores/page-builder-state.ts +0 -498
  67. package/src/stores/shared-store.ts +0 -13
  68. package/src/tailwind-safelist.html +0 -3
  69. package/src/tests/DefaultComponents/DefaultBuilderComponents.vue +0 -284
  70. package/src/tests/DefaultComponents/DefaultMediaLibraryComponent.vue +0 -10
  71. package/src/tests/PageBuilderTest.vue +0 -82
  72. package/src/tests/TestComponents/DemoBuilderComponentsTest.vue +0 -5
  73. package/src/tests/TestComponents/DemoMediaLibraryComponentTest.vue +0 -8
  74. package/src/tests/TestComponents/DemoUnsplash.vue +0 -389
  75. package/src/tests/componentsArray.test.json +0 -62
  76. package/src/tests/pageBuilderService.test.ts +0 -165
  77. package/src/types/global.d.ts +0 -11
  78. package/src/types/index.ts +0 -306
  79. package/src/utils/builder/html-doc-declaration-with-components.ts +0 -7
  80. package/src/utils/builder/tailwaind-colors.ts +0 -503
  81. package/src/utils/builder/tailwind-border-radius.ts +0 -67
  82. package/src/utils/builder/tailwind-border-style-width-color.ts +0 -272
  83. package/src/utils/builder/tailwind-font-sizes.ts +0 -75
  84. package/src/utils/builder/tailwind-font-styles.ts +0 -56
  85. package/src/utils/builder/tailwind-opacities.ts +0 -45
  86. package/src/utils/builder/tailwind-padding-margin.ts +0 -159
  87. package/src/utils/html-elements/component.ts +0 -485
  88. package/src/utils/html-elements/componentHelpers.ts +0 -98
  89. package/src/utils/html-elements/themes.ts +0 -85
@@ -1,3191 +0,0 @@
1
- import { LocalStorageManager } from './LocalStorageManager'
2
- import type {
3
- BuilderResourceData,
4
- ComponentObject,
5
- ImageObject,
6
- PageBuilderConfig,
7
- PageSettings,
8
- StartBuilderResult,
9
- } from '../types'
10
- import type { usePageBuilderStateStore } from '../stores/page-builder-state'
11
-
12
- import tailwindFontSizes from '../utils/builder/tailwind-font-sizes'
13
- import tailwindColors from '../utils/builder/tailwaind-colors'
14
- import tailwindOpacities from '../utils/builder/tailwind-opacities'
15
- import tailwindFontStyles from '../utils/builder/tailwind-font-styles'
16
- import tailwindPaddingAndMargin from '../utils/builder/tailwind-padding-margin'
17
- import tailwindBorderRadius from '../utils/builder/tailwind-border-radius'
18
- import tailwindBorderStyleWidthPlusColor from '../utils/builder/tailwind-border-style-width-color'
19
- import { computed, ref, nextTick } from 'vue'
20
- import type { ComputedRef } from 'vue'
21
- import { v4 as uuidv4 } from 'uuid'
22
- import { delay } from '../composables/delay'
23
- import { isEmptyObject } from '../helpers/isEmptyObject'
24
- import { extractCleanHTMLFromPageBuilder } from '../composables/extractCleanHTMLFromPageBuilder'
25
-
26
- // Define available languages as a type and an array for easy iteration and type safety
27
- export type AvailableLanguage =
28
- | 'en'
29
- | 'zh-Hans'
30
- | 'fr'
31
- | 'ja'
32
- | 'ru'
33
- | 'es'
34
- | 'pt'
35
- | 'de'
36
- | 'ar'
37
- | 'hi'
38
-
39
- export const AVAILABLE_LANGUAGES: AvailableLanguage[] = [
40
- 'en',
41
- 'zh-Hans',
42
- 'fr',
43
- 'ja',
44
- 'ru',
45
- 'es',
46
- 'pt',
47
- 'de',
48
- 'ar',
49
- 'hi',
50
- ]
51
-
52
- export class PageBuilderService {
53
- // Class properties with types
54
- private fontSizeRegex =
55
- /^(sm:|md:|lg:|xl:|2xl:)?pbx-text-(xs|sm|base|lg|xl|2xl|3xl|4xl|5xl|6xl|7xl|8xl|9xl)$/
56
-
57
- protected pageBuilderStateStore: ReturnType<typeof usePageBuilderStateStore>
58
- private getLocalStorageItemName: ComputedRef<string | null>
59
- private getApplyImageToSelection: ComputedRef<ImageObject>
60
- private getHyberlinkEnable: ComputedRef<boolean>
61
- private getComponents: ComputedRef<ComponentObject[] | null>
62
- private getComponent: ComputedRef<ComponentObject | null>
63
- private getElement: ComputedRef<HTMLElement | null>
64
- private getComponentArrayAddMethod: ComputedRef<string | null>
65
- private NoneListernesTags: string[]
66
- private hasStartedEditing: boolean = false
67
- // Hold data from Database or Backend for updated post
68
- private originalComponents: BuilderResourceData | undefined = undefined
69
- // Holds data to be mounted when pagebuilder is not yet present in the DOM
70
- private savedMountComponents: BuilderResourceData | null = null
71
- private pendingMountComponents: BuilderResourceData | null = null
72
- private isPageBuilderMissingOnStart: boolean = false
73
-
74
- // Add a class-level WeakMap to track elements and their listeners
75
- // Use class-level WeakMap from being a local variable inside addListenersToEditableElements to a private class-level property.
76
- // This ensures that the map persists across multiple calls to the method and retains knowledge of
77
- // which elements already have listeners.
78
- // This prevents multiple event listeners being attached to the same HTML elements
79
- private elementsWithListeners = new WeakMap<
80
- Element,
81
- { click: EventListener; mouseover: EventListener; mouseleave: EventListener }
82
- >()
83
-
84
- constructor(pageBuilderStateStore: ReturnType<typeof usePageBuilderStateStore>) {
85
- this.hasStartedEditing = false
86
- this.pageBuilderStateStore = pageBuilderStateStore
87
- this.getApplyImageToSelection = computed(
88
- () => this.pageBuilderStateStore.getApplyImageToSelection,
89
- )
90
- this.getLocalStorageItemName = computed(
91
- () => this.pageBuilderStateStore.getLocalStorageItemName,
92
- )
93
- this.getHyberlinkEnable = computed(() => this.pageBuilderStateStore.getHyberlinkEnable)
94
- this.getComponents = computed(() => this.pageBuilderStateStore.getComponents)
95
-
96
- this.getComponent = computed(() => this.pageBuilderStateStore.getComponent)
97
-
98
- this.getElement = computed(() => this.pageBuilderStateStore.getElement)
99
-
100
- this.getComponentArrayAddMethod = computed(
101
- () => this.pageBuilderStateStore.getComponentArrayAddMethod,
102
- )
103
-
104
- this.NoneListernesTags = [
105
- 'P',
106
- 'H1',
107
- 'H2',
108
- 'H3',
109
- 'H4',
110
- 'H5',
111
- 'H6',
112
- 'IFRAME',
113
- 'UL',
114
- 'OL',
115
- 'LI',
116
- 'EM',
117
- 'STRONG',
118
- 'B',
119
- 'A',
120
- 'SPAN',
121
- 'BLOCKQUOTE',
122
- 'BR',
123
- 'PRE',
124
- 'CODE',
125
- 'MARK',
126
- 'DEL',
127
- 'INS',
128
- 'U',
129
- 'FIGURE',
130
- 'FIGCAPTION',
131
- ]
132
- }
133
-
134
- /**
135
- * Returns an array of available languages.
136
- * @returns {AvailableLanguage[]} An array of available language codes.
137
- */
138
- public availableLanguage(): AvailableLanguage[] {
139
- return AVAILABLE_LANGUAGES
140
- }
141
-
142
- /**
143
- * Sets the current language in the page builder state.
144
- * @param {string} lang - The language code to set.
145
- */
146
- public changeLanguage(lang: string) {
147
- this.pageBuilderStateStore.setCurrentLanguage(lang)
148
- }
149
- /**
150
- * Deselects any selected or hovered elements in the builder UI.
151
- * @returns {Promise<void>}
152
- */
153
- async clearHtmlSelection(): Promise<void> {
154
- this.pageBuilderStateStore.setComponent(null)
155
- this.pageBuilderStateStore.setElement(null)
156
- await this.removeHoveredAndSelected()
157
- }
158
-
159
- /**
160
- * Ensures that the `updateOrCreate` configuration is valid and sets default values if necessary.
161
- * @param {PageBuilderConfig} config - The page builder configuration.
162
- * @private
163
- */
164
- private ensureUpdateOrCreateConfig(config: PageBuilderConfig): void {
165
- // Case A: updateOrCreate is missing or an empty object
166
- if (!config.updateOrCreate || (config.updateOrCreate && isEmptyObject(config.updateOrCreate))) {
167
- const updatedConfig = {
168
- ...config,
169
- updateOrCreate: {
170
- formType: 'create',
171
- formName: 'post',
172
- },
173
- } as const
174
-
175
- this.pageBuilderStateStore.setPageBuilderConfig(updatedConfig)
176
- return
177
- }
178
-
179
- // Case B: formType is valid ('create' or 'update'), but formName is missing or an empty string
180
- if (
181
- (config.updateOrCreate &&
182
- typeof config.updateOrCreate.formType === 'string' &&
183
- (config.updateOrCreate.formType === 'create' ||
184
- config.updateOrCreate.formType === 'update') &&
185
- typeof config.updateOrCreate.formName !== 'string') ||
186
- (typeof config.updateOrCreate.formName === 'string' &&
187
- config.updateOrCreate.formName.length === 0)
188
- ) {
189
- const updatedConfig = {
190
- ...config,
191
- updateOrCreate: {
192
- formType: config.updateOrCreate.formType,
193
- formName: 'post',
194
- },
195
- } as const
196
- this.pageBuilderStateStore.setPageBuilderConfig(updatedConfig)
197
- }
198
-
199
- // Case C: formType is missing or not a valid string like ('create' or 'update') but formName is valid string
200
- if (
201
- (config.updateOrCreate && typeof config.updateOrCreate.formType !== 'string') ||
202
- (typeof config.updateOrCreate.formType === 'string' &&
203
- config.updateOrCreate.formType !== 'create' &&
204
- config.updateOrCreate.formType !== 'update' &&
205
- typeof config.updateOrCreate.formName === 'string' &&
206
- config.updateOrCreate.formName.length !== 0)
207
- ) {
208
- const updatedConfig = {
209
- ...config,
210
- updateOrCreate: {
211
- formType: 'create',
212
- formName: config.updateOrCreate.formName,
213
- },
214
- } as const
215
-
216
- this.pageBuilderStateStore.setPageBuilderConfig(updatedConfig)
217
- return
218
- }
219
-
220
- // Case D: formType exists but is not 'create' or 'update', and formName is missing or invalid
221
- if (
222
- config.updateOrCreate &&
223
- typeof config.updateOrCreate.formType === 'string' &&
224
- config.updateOrCreate.formType !== 'create' &&
225
- config.updateOrCreate.formType !== 'update' &&
226
- typeof config.formName !== 'string'
227
- ) {
228
- const updatedConfig = {
229
- ...config,
230
- updateOrCreate: {
231
- formType: 'create',
232
- formName: 'post',
233
- },
234
- } as const
235
-
236
- this.pageBuilderStateStore.setPageBuilderConfig(updatedConfig)
237
- }
238
- }
239
-
240
- /**
241
- * Validates the user-provided components array.
242
- * @param {unknown} components - The components data to validate.
243
- * @returns {{error: true, warning: string, status: string} | {error: true, reason: string} | undefined} An error object if validation fails, otherwise undefined.
244
- * @private
245
- */
246
- private validateUserProvidedComponents(components: unknown) {
247
- const formType =
248
- this.pageBuilderStateStore.getPageBuilderConfig &&
249
- this.pageBuilderStateStore.getPageBuilderConfig.updateOrCreate &&
250
- this.pageBuilderStateStore.getPageBuilderConfig.updateOrCreate.formType
251
-
252
- if (
253
- Array.isArray(components) &&
254
- components.length >= 1 &&
255
- formType === 'create' &&
256
- components
257
- ) {
258
- return {
259
- error: true as const,
260
- warning:
261
- '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.',
262
- status: 'validation_failed',
263
- }
264
- }
265
-
266
- // Must be an array
267
- if (!Array.isArray(components)) {
268
- return {
269
- error: true as const,
270
- reason: 'Components data must be an array.',
271
- }
272
- }
273
-
274
- // Check that the first item looks like a component
275
- const first = components[0]
276
-
277
- if (first && 'html_code' in first && typeof first.html_code !== 'string') {
278
- return {
279
- error: true as const,
280
- reason: "The 'html_code' property in the first object must be a string.",
281
- }
282
- }
283
-
284
- // Check that the first item has an 'html_code' key
285
- if (Array.isArray(components) && components.length >= 1) {
286
- if (!first || !('html_code' in first)) {
287
- return {
288
- error: true as const,
289
- reason: "The first object in the array must include an 'html_code' key.",
290
- }
291
- }
292
- }
293
-
294
- // No errors found
295
- return
296
- }
297
-
298
- /**
299
- * Ensures that the language configuration is valid and sets default values if necessary.
300
- * @param {PageBuilderConfig} config - The page builder configuration.
301
- * @private
302
- */
303
- private ensureLanguage(config: PageBuilderConfig): void {
304
- // Set default language config if missing, empty, or language missing/empty
305
- const defaultLang = 'en'
306
- const defaultEnable = ['en', 'zh-Hans', 'fr', 'ja', 'ru', 'es', 'pt', 'de', 'ar', 'hi'] as const
307
-
308
- let needsDefault = false
309
- const userSettings = config.userSettings
310
- const language = userSettings && userSettings.language
311
-
312
- if (!userSettings || isEmptyObject(userSettings)) {
313
- needsDefault = true
314
- } else if (!language || isEmptyObject(language)) {
315
- needsDefault = true
316
- }
317
-
318
- if (needsDefault) {
319
- const updatedLanguage = {
320
- ...config,
321
- userSettings: {
322
- ...userSettings,
323
- language: {
324
- default: defaultLang,
325
- enable: defaultEnable as typeof defaultEnable,
326
- },
327
- },
328
- } as const
329
- this.pageBuilderStateStore.setPageBuilderConfig(updatedLanguage)
330
- return
331
- }
332
-
333
- // Ensure default is in enable array
334
- if (language && Array.isArray(language.enable) && language.default) {
335
- if (!language.enable.includes(language.default)) {
336
- const updatedEnable = [...language.enable, language.default]
337
- const updatedLanguage = {
338
- ...config,
339
- userSettings: {
340
- ...userSettings,
341
- language: {
342
- ...language,
343
- enable: updatedEnable,
344
- },
345
- },
346
- } as const
347
- this.pageBuilderStateStore.setPageBuilderConfig(updatedLanguage)
348
- }
349
- }
350
- }
351
-
352
- /**
353
- * Validates the entire page builder configuration.
354
- * @param {PageBuilderConfig} config - The page builder configuration.
355
- * @private
356
- */
357
- private validateConfig(config: PageBuilderConfig): void {
358
- const defaultConfigValues = {
359
- updateOrCreate: {
360
- formType: 'create',
361
- formName: 'post',
362
- },
363
- } as const
364
-
365
- // Set config for page builder if not set by user
366
- if (!config || (config && Object.keys(config).length === 0 && config.constructor === Object)) {
367
- this.pageBuilderStateStore.setPageBuilderConfig(defaultConfigValues)
368
- }
369
-
370
- if (config && Object.keys(config).length !== 0 && config.constructor === Object) {
371
- this.ensureUpdateOrCreateConfig(config)
372
- }
373
-
374
- this.ensureLanguage(config)
375
- }
376
-
377
- /**
378
- * Saves user settings to local storage.
379
- * @param {string} newLang - The new language to save.
380
- */
381
- public saveUserSettingsStorage(newLang: string) {
382
- localStorage.setItem(
383
- 'userSettingsPageBuilder',
384
- JSON.stringify({ userSettings: { lang: newLang } }),
385
- )
386
- }
387
-
388
- /**
389
- * Initializes the Page Builder.
390
- * @param {PageBuilderConfig} config - The configuration object for the Page Builder.
391
- * @param {BuilderResourceData} [passedComponentsArray] - Optional array of components to load.
392
- * @returns {Promise<StartBuilderResult>} A result object indicating success or failure.
393
- */
394
- async startBuilder(
395
- config: PageBuilderConfig,
396
- passedComponentsArray?: BuilderResourceData,
397
- ): Promise<StartBuilderResult> {
398
- // Reactive flag signals to the UI that the builder has been successfully initialized
399
- // Prevents builder actions to prevent errors caused by missing DOM .
400
- this.pageBuilderStateStore.setBuilderStarted(true)
401
- const pagebuilder = document.querySelector('#pagebuilder')
402
-
403
- let validation
404
- try {
405
- this.originalComponents = passedComponentsArray
406
- this.pageBuilderStateStore.setPageBuilderConfig(config)
407
- // Validate and normalize the config (ensure required fields are present)
408
- this.validateConfig(config)
409
-
410
- validation = this.validateUserProvidedComponents(passedComponentsArray)
411
-
412
- // Update the localStorage key name based on the config/resource
413
- this.updateLocalStorageItemName()
414
- this.initializeHistory()
415
-
416
- if (passedComponentsArray) {
417
- this.savedMountComponents = passedComponentsArray
418
- }
419
- // Page Builder is not Present in the DOM but Components have been passed to the Builder
420
- if (!pagebuilder) {
421
- this.isPageBuilderMissingOnStart = true
422
- }
423
- if (passedComponentsArray && !pagebuilder) {
424
- this.pendingMountComponents = passedComponentsArray
425
- }
426
- // Page Builder is Present in the DOM & Components have been passed to the Builder
427
- if (pagebuilder) {
428
- this.completeBuilderInitialization(passedComponentsArray)
429
- }
430
-
431
- // result to end user
432
- const result: StartBuilderResult = {
433
- message: 'Page builder started successfully.',
434
- }
435
-
436
- if (validation) {
437
- result.validation = validation
438
- }
439
-
440
- // PassedComponentsArray
441
- if (Array.isArray(passedComponentsArray) && passedComponentsArray.length >= 0) {
442
- result.passedComponentsArray = passedComponentsArray
443
- }
444
-
445
- // Return messages, validation info if present etc.
446
- return result
447
- } catch (err) {
448
- console.error('Not able to start the Page Builder', err)
449
- return {
450
- error: true as const,
451
- reason: 'Failed to start the Page Builder due to an unexpected error.',
452
- }
453
- }
454
- }
455
-
456
- /**
457
- * Completes the builder initialization process once the DOM is ready.
458
- * @param {BuilderResourceData} [passedComponentsArray] - Optional array of components to load.
459
- * @returns {Promise<void>}
460
- */
461
- async completeBuilderInitialization(passedComponentsArray?: BuilderResourceData): Promise<void> {
462
- this.pageBuilderStateStore.setIsLoadingGlobal(true)
463
- await delay(400)
464
-
465
- // Always clear DOM and store before mounting new resource
466
- this.deleteAllComponentsFromDOM()
467
-
468
- const config = this.pageBuilderStateStore.getPageBuilderConfig
469
- const formType = config && config.updateOrCreate && config.updateOrCreate.formType
470
-
471
- const localStorageData = this.getSavedPageHtml()
472
-
473
- // Deselect any selected or hovered elements in the builder UI
474
- await this.clearHtmlSelection()
475
-
476
- if (formType === 'update' || formType === 'create') {
477
- // Page Builder is initially present in the DOM
478
- if (!this.pendingMountComponents) {
479
- if (!passedComponentsArray && this.isPageBuilderMissingOnStart && localStorageData) {
480
- await this.completeMountProcess(localStorageData)
481
- return
482
- }
483
- if (passedComponentsArray && !localStorageData) {
484
- const htmlString = this.renderComponentsToHtml(passedComponentsArray)
485
- await this.completeMountProcess(htmlString, true)
486
- this.saveDomComponentsToLocalStorage()
487
- return
488
- }
489
-
490
- if (passedComponentsArray && localStorageData) {
491
- const htmlString = this.renderComponentsToHtml(passedComponentsArray)
492
- await this.completeMountProcess(htmlString, true)
493
- await delay(500)
494
- this.pageBuilderStateStore.setHasLocalDraftForUpdate(true)
495
- return
496
- }
497
-
498
- if (!passedComponentsArray && localStorageData && !this.savedMountComponents) {
499
- await this.completeMountProcess(localStorageData)
500
- return
501
- }
502
- if (!passedComponentsArray && this.savedMountComponents && localStorageData) {
503
- const htmlString = this.renderComponentsToHtml(this.savedMountComponents)
504
- await this.completeMountProcess(htmlString)
505
- return
506
- }
507
-
508
- if (!passedComponentsArray && !localStorageData && this.isPageBuilderMissingOnStart) {
509
- const htmlString = this.renderComponentsToHtml([])
510
- await this.completeMountProcess(htmlString)
511
-
512
- return
513
- }
514
-
515
- if (!this.isPageBuilderMissingOnStart && !localStorageData && !passedComponentsArray) {
516
- const htmlString = this.renderComponentsToHtml([])
517
- await this.completeMountProcess(htmlString)
518
- return
519
- }
520
- }
521
-
522
- // Page Builder is not initially present in the DOM
523
- if (this.pendingMountComponents) {
524
- if (localStorageData && this.isPageBuilderMissingOnStart) {
525
- const htmlString = this.renderComponentsToHtml(this.pendingMountComponents)
526
- await this.completeMountProcess(htmlString, true)
527
- await delay(500)
528
- this.pageBuilderStateStore.setHasLocalDraftForUpdate(true)
529
- this.pendingMountComponents = null
530
- return
531
- }
532
- if (!localStorageData && passedComponentsArray && this.isPageBuilderMissingOnStart) {
533
- const htmlString = this.renderComponentsToHtml(this.pendingMountComponents)
534
- await this.completeMountProcess(htmlString, true)
535
- this.saveDomComponentsToLocalStorage()
536
- return
537
- }
538
-
539
- if (!passedComponentsArray && !localStorageData && this.isPageBuilderMissingOnStart) {
540
- const htmlString = this.renderComponentsToHtml(this.pendingMountComponents)
541
- await this.completeMountProcess(htmlString, true)
542
- this.saveDomComponentsToLocalStorage()
543
- return
544
- }
545
- }
546
- }
547
- }
548
-
549
- /**
550
- * Converts an array of ComponentObject into a single HTML string.
551
- *
552
- * @returns {string} A single HTML string containing all components.
553
- */
554
- private renderComponentsToHtml(componentsArray: BuilderResourceData): string {
555
- // If the componentsArray is empty or invalid, return a default HTML structure
556
- if (!componentsArray || (Array.isArray(componentsArray) && componentsArray.length === 0)) {
557
- return `<div id="pagebuilder" class="pbx-text-black pbx-font-sans"></div>`
558
- }
559
-
560
- const sectionsHtml = componentsArray
561
- .map((component) => {
562
- return component.html_code // Fallback in case section is not found
563
- })
564
- .join('\n')
565
-
566
- // Return the combined HTML string
567
- return sectionsHtml
568
- }
569
-
570
- /**
571
- * Completes the mounting process by loading components into the DOM and setting up listeners.
572
- * @param {string} html - The HTML string of components to mount.
573
- * @param {boolean} [useConfigPageSettings] - Whether to use page settings from the passed data.
574
- * @private
575
- */
576
- private async completeMountProcess(html: string, useConfigPageSettings?: boolean) {
577
- await this.mountComponentsToDOM(html, useConfigPageSettings)
578
-
579
- // Clean up any old localStorage items related to previous builder sessions
580
- this.deleteOldPageBuilderLocalStorage()
581
- this.pageBuilderStateStore.setIsRestoring(false)
582
- this.pageBuilderStateStore.setIsLoadingGlobal(false)
583
- }
584
-
585
- /**
586
- * Applies CSS class changes to the currently selected element.
587
- * @param {string | undefined} cssUserSelection - The user's CSS class selection.
588
- * @param {string[]} CSSArray - The array of possible CSS classes for this property.
589
- * @param {string} mutationName - The name of the store mutation to call.
590
- * @returns {string | undefined} The previously applied CSS class.
591
- * @private
592
- */
593
- private applyElementClassChanges(
594
- cssUserSelection: string | undefined,
595
- CSSArray: string[],
596
- mutationName: string,
597
- ): string | undefined {
598
- const currentHTMLElement = this.getElement.value
599
-
600
- if (!currentHTMLElement) return
601
-
602
- const currentCSS = CSSArray.find((CSS) => {
603
- return currentHTMLElement.classList.contains(CSS)
604
- })
605
-
606
- // set to 'none' if undefined
607
- let elementClass = currentCSS || 'none'
608
-
609
- // If cssUserSelection is undefined, just set the current state and return
610
- if (cssUserSelection === undefined) {
611
- if (typeof mutationName === 'string' && mutationName.length > 2) {
612
- // Use a type-safe approach to handle mutationName
613
- if (
614
- mutationName in this.pageBuilderStateStore &&
615
- typeof this.pageBuilderStateStore[
616
- mutationName as keyof typeof this.pageBuilderStateStore
617
- ] === 'function'
618
- ) {
619
- const mutationFunction = this.pageBuilderStateStore[
620
- mutationName as keyof typeof this.pageBuilderStateStore
621
- ] as (arg: string) => void
622
- mutationFunction(elementClass)
623
- }
624
- }
625
- return currentCSS
626
- }
627
-
628
- // cssUserSelection examples: bg-zinc-200, px-10, rounded-full etc.
629
- if (typeof cssUserSelection === 'string' && cssUserSelection !== 'none') {
630
- if (elementClass && currentHTMLElement.classList.contains(elementClass)) {
631
- currentHTMLElement.classList.remove(elementClass)
632
- }
633
-
634
- currentHTMLElement.classList.add(cssUserSelection)
635
- elementClass = cssUserSelection
636
- } else if (
637
- typeof cssUserSelection === 'string' &&
638
- cssUserSelection === 'none' &&
639
- elementClass
640
- ) {
641
- currentHTMLElement.classList.remove(elementClass)
642
- elementClass = cssUserSelection
643
- }
644
-
645
- // Only call store mutations after all DOM manipulation is complete
646
- if (typeof mutationName === 'string' && mutationName.length > 2) {
647
- // Use a type-safe approach to handle mutationName
648
- if (
649
- mutationName in this.pageBuilderStateStore &&
650
- typeof this.pageBuilderStateStore[
651
- mutationName as keyof typeof this.pageBuilderStateStore
652
- ] === 'function'
653
- ) {
654
- const mutationFunction = this.pageBuilderStateStore[
655
- mutationName as keyof typeof this.pageBuilderStateStore
656
- ] as (arg: string) => void
657
- mutationFunction(elementClass)
658
- this.pageBuilderStateStore.setElement(currentHTMLElement)
659
- }
660
- }
661
-
662
- return currentCSS
663
- }
664
-
665
- /**
666
- * Removes all CSS classes from the main page builder container.
667
- * @returns {Promise<void>}
668
- */
669
- public async clearClassesFromPage() {
670
- const pagebuilder = document.querySelector('#pagebuilder')
671
- if (!pagebuilder) return
672
-
673
- pagebuilder.removeAttribute('class')
674
-
675
- this.initializeElementStyles()
676
- await nextTick()
677
- }
678
- /**
679
- * Removes all inline styles from the main page builder container.
680
- * @returns {Promise<void>}
681
- */
682
- public async clearInlineStylesFromPage() {
683
- const pagebuilder = document.querySelector('#pagebuilder')
684
- if (!pagebuilder) return
685
-
686
- pagebuilder.removeAttribute('style')
687
-
688
- this.initializeElementStyles()
689
- await nextTick()
690
- }
691
-
692
- /**
693
- * Selects the main page builder container for global styling.
694
- * @returns {Promise<void>}
695
- */
696
- public async globalPageStyles() {
697
- const pagebuilder = document.querySelector('#pagebuilder')
698
- if (!pagebuilder) return
699
-
700
- // Deselect any selected or hovered elements in the builder UI
701
- await this.clearHtmlSelection()
702
- //
703
- // Set the element in the store
704
- this.pageBuilderStateStore.setElement(pagebuilder as HTMLElement)
705
-
706
- // Add the data attribute for styling
707
- pagebuilder.setAttribute('data-global-selected', 'true')
708
-
709
- await nextTick()
710
- }
711
-
712
- /**
713
- * Handles changes to the font weight of the selected element.
714
- * @param {string} [userSelectedFontWeight] - The selected font weight class.
715
- */
716
- public handleFontWeight(userSelectedFontWeight?: string): void {
717
- this.applyElementClassChanges(
718
- userSelectedFontWeight,
719
- tailwindFontStyles.fontWeight,
720
- 'setFontWeight',
721
- )
722
- }
723
-
724
- /**
725
- * Handles changes to the base font size of the selected element.
726
- * @param {string} [userSelectedFontSize] - The selected font size class.
727
- */
728
- public handleFontSizeBase(userSelectedFontSize?: string): void {
729
- this.applyElementClassChanges(userSelectedFontSize, tailwindFontSizes.fontBase, 'setFontBase')
730
- }
731
-
732
- /**
733
- * Handles changes to the desktop font size of the selected element.
734
- * @param {string} [userSelectedFontSize] - The selected font size class for desktop.
735
- */
736
- public handleFontSizeDesktop(userSelectedFontSize?: string): void {
737
- const currentHTMLElement = this.getElement.value
738
- if (!currentHTMLElement) return
739
-
740
- // Hardcoded mapping: selected => base
741
- const fontSizeBaseMap: Record<string, string> = {
742
- 'pbx-text-9xl': 'pbx-text-6xl',
743
- 'pbx-text-8xl': 'pbx-text-5xl',
744
- 'pbx-text-7xl': 'pbx-text-4xl',
745
- 'pbx-text-6xl': 'pbx-text-3xl',
746
- 'pbx-text-5xl': 'pbx-text-3xl',
747
- 'pbx-text-4xl': 'pbx-text-2xl',
748
- 'pbx-text-3xl': 'pbx-text-1xl',
749
- 'pbx-text-2xl': 'pbx-text-lg',
750
- 'pbx-text-xl': 'pbx-text-base',
751
- 'pbx-text-lg': 'pbx-text-sm',
752
- 'pbx-text-base': 'pbx-text-xs',
753
- 'pbx-text-sm': 'pbx-text-xs',
754
- 'pbx-text-xs': 'pbx-text-xs',
755
- }
756
-
757
- if (userSelectedFontSize) {
758
- // Remove all existing font size classes first
759
- Array.from(currentHTMLElement.classList).forEach((cls) => {
760
- if (this.fontSizeRegex.test(cls)) {
761
- currentHTMLElement.classList.remove(cls)
762
- }
763
- })
764
-
765
- // Extract the font size class (remove 'lg:' if present)
766
- const fontSizeClass = userSelectedFontSize.replace(/^lg:/, '')
767
-
768
- const baseClass = fontSizeBaseMap[fontSizeClass] || fontSizeClass
769
- const lgClass = `lg:${fontSizeClass}`
770
-
771
- if (baseClass !== fontSizeClass) {
772
- currentHTMLElement.classList.add(baseClass, lgClass)
773
- } else {
774
- currentHTMLElement.classList.add(baseClass)
775
- }
776
- }
777
-
778
- const currentCSS = tailwindFontSizes.fontDesktop.find((CSS) => {
779
- return currentHTMLElement.classList.contains(CSS)
780
- })
781
-
782
- if (!userSelectedFontSize) {
783
- this.pageBuilderStateStore.setFontDesktop('none')
784
- }
785
-
786
- if (currentCSS && !userSelectedFontSize) {
787
- this.pageBuilderStateStore.setFontDesktop(currentCSS)
788
- }
789
- }
790
-
791
- /**
792
- * Applies helper CSS classes to elements, such as wrapping them or adding responsive text classes.
793
- * @param {HTMLElement} element - The element to process.
794
- * @private
795
- */
796
- private applyHelperCSSToElements(element: HTMLElement): void {
797
- this.wrapElementInDivIfExcluded(element)
798
-
799
- // If this is a DIV and its only/main child is a heading, apply font size classes to the DIV
800
- if (
801
- element.tagName === 'DIV' &&
802
- element.children.length === 1 &&
803
- ['H1', 'H2', 'H3', 'H4', 'H5', 'H6'].includes(element.children[0].tagName)
804
- ) {
805
- const heading = element.children[0] as HTMLElement
806
-
807
- // Only add default font size classes if none exist
808
- const hasFontSizeClass = Array.from(element.classList).some((cls) =>
809
- this.fontSizeRegex.test(cls),
810
- )
811
-
812
- if (!hasFontSizeClass) {
813
- // Apply responsive font size classes based on heading type
814
- if (heading.tagName === 'H2') {
815
- element.classList.add('pbx-text-2xl', 'lg:pbx-text-4xl', 'pbx-font-medium')
816
- }
817
- if (heading.tagName === 'H3') {
818
- element.classList.add('pbx-text-1xl', 'lg:pbx-text-3xl', 'pbx-font-medium')
819
- }
820
- }
821
- }
822
- }
823
-
824
- /**
825
- * Toggles the visibility of the TipTap modal for rich text editing.
826
- * @param {boolean} status - Whether to show or hide the modal.
827
- * @returns {Promise<void>}
828
- */
829
- public async toggleTipTapModal(status: boolean): Promise<void> {
830
- this.pageBuilderStateStore.setShowModalTipTap(status)
831
-
832
- // Wait for Vue to finish DOM updates before attaching event listeners. This ensure elements exist in the DOM.
833
- await nextTick()
834
- // Attach event listeners to all editable elements in the Builder
835
- await this.addListenersToEditableElements()
836
-
837
- if (!status) {
838
- await this.handleAutoSave()
839
- }
840
- }
841
-
842
- /**
843
- * Wraps an element in a div if it's an excluded tag and adjacent to an image.
844
- * @param {HTMLElement} element - The element to potentially wrap.
845
- * @private
846
- */
847
- private wrapElementInDivIfExcluded(element: HTMLElement): void {
848
- if (!element) return
849
-
850
- if (
851
- this.NoneListernesTags.includes(element.tagName) &&
852
- ((element.previousElementSibling && element.previousElementSibling.tagName === 'IMG') ||
853
- (element.nextElementSibling && element.nextElementSibling.tagName === 'IMG'))
854
- ) {
855
- const divWrapper = document.createElement('div')
856
- element.parentNode?.insertBefore(divWrapper, element)
857
- divWrapper.appendChild(element)
858
- }
859
- }
860
-
861
- /**
862
- * Handles the mouseover event for editable elements, showing a hover state.
863
- * @param {Event} e - The mouse event.
864
- * @param {HTMLElement} element - The element being hovered over.
865
- * @private
866
- */
867
- private handleMouseOver = (e: Event, element: HTMLElement): void => {
868
- e.preventDefault()
869
- e.stopPropagation()
870
-
871
- const pagebuilder = document.querySelector('#pagebuilder')
872
-
873
- if (!pagebuilder) return
874
-
875
- const hoveredElement = pagebuilder.querySelector('[hovered]')
876
- if (hoveredElement) {
877
- hoveredElement.removeAttribute('hovered')
878
- }
879
-
880
- if (!element.hasAttribute('selected')) {
881
- element.setAttribute('hovered', '')
882
- }
883
- }
884
-
885
- /**
886
- * Handles the mouseleave event for editable elements, removing the hover state.
887
- * @param {Event} e - The mouse event.
888
- * @private
889
- */
890
- private handleMouseLeave = (e: Event): void => {
891
- e.preventDefault()
892
- e.stopPropagation()
893
-
894
- const pagebuilder = document.querySelector('#pagebuilder')
895
- if (!pagebuilder) return
896
-
897
- const hoveredElement = pagebuilder.querySelector('[hovered]')
898
- if (hoveredElement) {
899
- hoveredElement.removeAttribute('hovered')
900
- }
901
- }
902
-
903
- /**
904
- * Checks if an element is editable based on its tag name.
905
- * @param {Element | null} el - The element to check.
906
- * @returns {boolean} True if the element is editable, false otherwise.
907
- */
908
- public isEditableElement(el: Element | null): boolean {
909
- if (!el) return false
910
- return !this.NoneListernesTags.includes(el.tagName)
911
- }
912
-
913
- /**
914
- * Attaches click, mouseover, and mouseleave event listeners to all editable elements in the page builder.
915
- * @private
916
- */
917
- private addListenersToEditableElements = async () => {
918
- const pagebuilder = document.querySelector('#pagebuilder')
919
- if (!pagebuilder) return
920
-
921
- // Wait for the next DOM update cycle to ensure all elements are rendered.
922
- await nextTick()
923
-
924
- pagebuilder.querySelectorAll('section *').forEach((element) => {
925
- if (this.isEditableElement(element)) {
926
- const htmlElement = element as HTMLElement
927
-
928
- // If the element already has listeners, remove them to avoid duplicates.
929
- if (this.elementsWithListeners.has(htmlElement)) {
930
- const listeners = this.elementsWithListeners.get(htmlElement)
931
- if (listeners) {
932
- htmlElement.removeEventListener('click', listeners.click)
933
- htmlElement.removeEventListener('mouseover', listeners.mouseover)
934
- htmlElement.removeEventListener('mouseleave', listeners.mouseleave)
935
- }
936
- }
937
-
938
- // Define new listener functions.
939
- const clickListener = (e: Event) => this.handleElementClick(e, htmlElement)
940
- const mouseoverListener = (e: Event) => this.handleMouseOver(e, htmlElement)
941
- const mouseleaveListener = (e: Event) => this.handleMouseLeave(e)
942
-
943
- // Add the new event listeners.
944
- htmlElement.addEventListener('click', clickListener)
945
- htmlElement.addEventListener('mouseover', mouseoverListener)
946
- htmlElement.addEventListener('mouseleave', mouseleaveListener)
947
-
948
- // Store the new listeners in the WeakMap to track them.
949
- this.elementsWithListeners.set(htmlElement, {
950
- click: clickListener,
951
- mouseover: mouseoverListener,
952
- mouseleave: mouseleaveListener,
953
- })
954
- }
955
- })
956
- }
957
-
958
- /**
959
- * Handles the click event for editable elements, setting the element as selected.
960
- * @param {Event} e - The click event.
961
- * @param {HTMLElement} element - The clicked element.
962
- * @private
963
- */
964
- private handleElementClick = async (e: Event, element: HTMLElement): Promise<void> => {
965
- e.preventDefault()
966
- e.stopPropagation()
967
-
968
- const pagebuilder = document.querySelector('#pagebuilder')
969
-
970
- if (!pagebuilder) return
971
-
972
- this.pageBuilderStateStore.setMenuRight(true)
973
-
974
- const selectedElement = pagebuilder.querySelector('[selected]')
975
- if (selectedElement) {
976
- selectedElement.removeAttribute('selected')
977
- }
978
-
979
- element.removeAttribute('hovered')
980
-
981
- element.setAttribute('selected', '')
982
-
983
- this.pageBuilderStateStore.setElement(element)
984
-
985
- await this.handleAutoSave()
986
- }
987
-
988
- private getHistoryBaseKey(): string | null {
989
- return this.getLocalStorageItemName.value
990
- }
991
-
992
- private initializeHistory() {
993
- const baseKey = this.getHistoryBaseKey()
994
- if (baseKey) {
995
- const history = LocalStorageManager.getHistory(baseKey)
996
- this.pageBuilderStateStore.setHistoryIndex(history.length - 1)
997
- this.pageBuilderStateStore.setHistoryLength(history.length)
998
- }
999
- }
1000
-
1001
- /**
1002
- * Triggers an auto-save of the current page builder content to local storage if enabled.
1003
- */
1004
- public handleAutoSave = async () => {
1005
- this.startEditing()
1006
- const passedConfig = this.pageBuilderStateStore.getPageBuilderConfig
1007
-
1008
- // Check if config is set
1009
- if (passedConfig && passedConfig.userSettings) {
1010
- //
1011
- // Enabled auto save
1012
- if (
1013
- typeof passedConfig.userSettings.autoSave === 'boolean' &&
1014
- passedConfig.userSettings.autoSave !== false
1015
- ) {
1016
- if (this.pageBuilderStateStore.getIsSaving) return
1017
-
1018
- try {
1019
- this.pageBuilderStateStore.setIsSaving(true)
1020
- // Deselect any selected or hovered elements in the builder UI
1021
- //
1022
- this.saveDomComponentsToLocalStorage()
1023
- await delay(400)
1024
- } catch (err) {
1025
- console.error('Error trying auto save.', err)
1026
- } finally {
1027
- this.pageBuilderStateStore.setIsSaving(false)
1028
- }
1029
- }
1030
- }
1031
- if (passedConfig && !passedConfig.userSettings) {
1032
- try {
1033
- this.pageBuilderStateStore.setIsSaving(true)
1034
- this.saveDomComponentsToLocalStorage()
1035
- await delay(400)
1036
- } catch (err) {
1037
- console.error('Error trying saving.', err)
1038
- } finally {
1039
- this.pageBuilderStateStore.setIsSaving(false)
1040
- }
1041
- }
1042
- }
1043
-
1044
- /**
1045
- * Manually saves the current page builder content to local storage.
1046
- */
1047
- public handleManualSave = async (doNoClearHTML?: boolean) => {
1048
- this.pageBuilderStateStore.setIsSaving(true)
1049
- if (!doNoClearHTML) {
1050
- this.clearHtmlSelection()
1051
- }
1052
- this.startEditing()
1053
- this.saveDomComponentsToLocalStorage()
1054
- await delay(300)
1055
- this.pageBuilderStateStore.setIsSaving(false)
1056
- }
1057
-
1058
- /**
1059
- * Clones a component object and prepares it for insertion into the DOM by adding unique IDs and prefixes.
1060
- * @param {ComponentObject} componentObject - The component object to clone.
1061
- * @returns {ComponentObject} The cloned and prepared component object.
1062
- */
1063
- public cloneCompObjForDOMInsertion(componentObject: ComponentObject): ComponentObject {
1064
- // Deep clone clone component
1065
- const clonedComponent = { ...componentObject }
1066
-
1067
- const pageBuilderWrapper = document.querySelector('#page-builder-wrapper')
1068
- // scoll to top or bottom
1069
- if (pageBuilderWrapper) {
1070
- // push to top
1071
- if (this.getComponentArrayAddMethod.value === 'unshift') {
1072
- pageBuilderWrapper.scrollTo({
1073
- top: 0,
1074
- behavior: 'smooth',
1075
- })
1076
- }
1077
- }
1078
-
1079
- // Create a DOMParser instance
1080
- const parser = new DOMParser()
1081
-
1082
- // Parse the HTML content of the clonedComponent using the DOMParser
1083
- const doc = parser.parseFromString(clonedComponent.html_code || '', 'text/html')
1084
-
1085
- // Selects all elements within the HTML document, including elements like:
1086
- const elements = doc.querySelectorAll('*')
1087
-
1088
- elements.forEach((element) => {
1089
- this.applyHelperCSSToElements(element as HTMLElement)
1090
- })
1091
-
1092
- // Add the component id to the section element
1093
- const section = doc.querySelector('section')
1094
- if (section) {
1095
- // Prefix all classes inside the section
1096
- section.querySelectorAll('[class]').forEach((el) => {
1097
- el.setAttribute(
1098
- 'class',
1099
- this.addTailwindPrefixToClasses(el.getAttribute('class') || '', 'pbx-'),
1100
- )
1101
- })
1102
-
1103
- // Generate a unique ID using uuidv4() and assign it to the section
1104
- section.dataset.componentid = uuidv4()
1105
-
1106
- // Set the title attribute if present
1107
- if (clonedComponent.title) {
1108
- section.setAttribute('data-component-title', clonedComponent.title)
1109
- }
1110
-
1111
- // Update the clonedComponent id with the newly generated unique ID
1112
- clonedComponent.id = section.dataset.componentid
1113
-
1114
- // Update the HTML content of the clonedComponent with the modified HTML
1115
- clonedComponent.html_code = section.outerHTML
1116
- }
1117
-
1118
- // return to the cloned element to be dropped
1119
- return clonedComponent
1120
- }
1121
-
1122
- /**
1123
- * Removes the 'hovered' and 'selected' attributes from all elements in the page builder.
1124
- * @private
1125
- */
1126
- private async removeHoveredAndSelected() {
1127
- const pagebuilder = document.querySelector('#pagebuilder')
1128
- if (!pagebuilder) return
1129
-
1130
- const hoveredElement = pagebuilder.querySelector('[hovered]')
1131
- if (hoveredElement) {
1132
- hoveredElement.removeAttribute('hovered')
1133
- }
1134
-
1135
- const selectedElement = pagebuilder.querySelector('[selected]')
1136
-
1137
- if (selectedElement) {
1138
- selectedElement.removeAttribute('selected')
1139
- }
1140
- }
1141
-
1142
- /**
1143
- * Syncs the CSS classes of the currently selected element to the state store.
1144
- * @private
1145
- */
1146
- private async syncCurrentClasses() {
1147
- // convert classList to array
1148
- const classListArray = Array.from(this.getElement.value?.classList || [])
1149
-
1150
- // commit array to store
1151
- this.pageBuilderStateStore.setCurrentClasses(classListArray)
1152
- }
1153
-
1154
- /**
1155
- * Syncs the inline styles of the currently selected element to the state store.
1156
- * @private
1157
- */
1158
- private async syncCurrentStyles() {
1159
- const style = this.getElement.value?.getAttribute('style')
1160
- if (style) {
1161
- const stylesObject = this.parseStyleString(style)
1162
- this.pageBuilderStateStore.setCurrentStyles(stylesObject)
1163
- } else {
1164
- this.pageBuilderStateStore.setCurrentStyles({})
1165
- }
1166
- }
1167
-
1168
- /**
1169
- * Adds a CSS class to the currently selected element.
1170
- * @param {string} userSelectedClass - The class to add.
1171
- */
1172
- public handleAddClasses(userSelectedClass: string): void {
1173
- if (
1174
- typeof userSelectedClass === 'string' &&
1175
- userSelectedClass.trim() !== '' &&
1176
- !userSelectedClass.includes(' ') &&
1177
- // Check if class (with prefix) already exists
1178
- !this.getElement.value?.classList.contains('pbx-' + userSelectedClass.trim())
1179
- ) {
1180
- const cleanedClass = userSelectedClass.trim()
1181
-
1182
- // Add prefix if missing
1183
- const prefixedClass = cleanedClass.startsWith('pbx-') ? cleanedClass : 'pbx-' + cleanedClass
1184
-
1185
- this.getElement.value?.classList.add(prefixedClass)
1186
-
1187
- this.pageBuilderStateStore.setElement(this.getElement.value)
1188
- this.pageBuilderStateStore.setClass(prefixedClass)
1189
- }
1190
- }
1191
-
1192
- /**
1193
- * Adds or updates an inline style property on the currently selected element.
1194
- * @param {string} property - The CSS property to add/update.
1195
- * @param {string} value - The value of the CSS property.
1196
- */
1197
- public handleAddStyle(property: string, value: string): void {
1198
- const element = this.getElement.value
1199
- if (!element || !property || !value) return
1200
-
1201
- element.style.setProperty(property, value)
1202
- this.pageBuilderStateStore.setElement(element)
1203
- }
1204
-
1205
- /**
1206
- * Removes an inline style property from the currently selected element.
1207
- * @param {string} property - The CSS property to remove.
1208
- */
1209
- public handleRemoveStyle(property: string): void {
1210
- const element = this.getElement.value
1211
- if (!element || !property) return
1212
-
1213
- element.style.removeProperty(property)
1214
- this.pageBuilderStateStore.setElement(element)
1215
- }
1216
-
1217
- /**
1218
- * Handles changes to the font family of the selected element.
1219
- * @param {string} [userSelectedFontFamily] - The selected font family class.
1220
- */
1221
- public handleFontFamily(userSelectedFontFamily?: string): void {
1222
- this.applyElementClassChanges(
1223
- userSelectedFontFamily,
1224
- tailwindFontStyles.fontFamily,
1225
- 'setFontFamily',
1226
- )
1227
- }
1228
- /**
1229
- * Handles changes to the font style of the selected element.
1230
- * @param {string} [userSelectedFontStyle] - The selected font style class.
1231
- */
1232
- public handleFontStyle(userSelectedFontStyle?: string): void {
1233
- this.applyElementClassChanges(
1234
- userSelectedFontStyle,
1235
- tailwindFontStyles.fontStyle,
1236
- 'setFontStyle',
1237
- )
1238
- }
1239
- /**
1240
- * Handles changes to the vertical padding of the selected element.
1241
- * @param {string} [userSelectedVerticalPadding] - The selected vertical padding class.
1242
- */
1243
- public handleVerticalPadding(userSelectedVerticalPadding?: string): void {
1244
- this.applyElementClassChanges(
1245
- userSelectedVerticalPadding,
1246
- tailwindPaddingAndMargin.verticalPadding,
1247
- 'setFontVerticalPadding',
1248
- )
1249
- }
1250
- /**
1251
- * Handles changes to the horizontal padding of the selected element.
1252
- * @param {string} [userSelectedHorizontalPadding] - The selected horizontal padding class.
1253
- */
1254
- public handleHorizontalPadding(userSelectedHorizontalPadding?: string): void {
1255
- this.applyElementClassChanges(
1256
- userSelectedHorizontalPadding,
1257
- tailwindPaddingAndMargin.horizontalPadding,
1258
- 'setFontHorizontalPadding',
1259
- )
1260
- }
1261
-
1262
- /**
1263
- * Handles changes to the vertical margin of the selected element.
1264
- * @param {string} [userSelectedVerticalMargin] - The selected vertical margin class.
1265
- */
1266
- public handleVerticalMargin(userSelectedVerticalMargin?: string): void {
1267
- this.applyElementClassChanges(
1268
- userSelectedVerticalMargin,
1269
- tailwindPaddingAndMargin.verticalMargin,
1270
- 'setFontVerticalMargin',
1271
- )
1272
- }
1273
- /**
1274
- * Handles changes to the horizontal margin of the selected element.
1275
- * @param {string} [userSelectedHorizontalMargin] - The selected horizontal margin class.
1276
- */
1277
- public handleHorizontalMargin(userSelectedHorizontalMargin?: string): void {
1278
- this.applyElementClassChanges(
1279
- userSelectedHorizontalMargin,
1280
- tailwindPaddingAndMargin.horizontalMargin,
1281
- 'setFontHorizontalMargin',
1282
- )
1283
- }
1284
-
1285
- /**
1286
- * Handles changes to the border style of the selected element.
1287
- * @param {string} [borderStyle] - The selected border style class.
1288
- */
1289
- public handleBorderStyle(borderStyle?: string): void {
1290
- this.applyElementClassChanges(
1291
- borderStyle,
1292
- tailwindBorderStyleWidthPlusColor.borderStyle,
1293
- 'setBorderStyle',
1294
- )
1295
- }
1296
- /**
1297
- * Handles changes to the border width of the selected element.
1298
- * @param {string} [borderWidth] - The selected border width class.
1299
- */
1300
- public handleBorderWidth(borderWidth?: string): void {
1301
- this.applyElementClassChanges(
1302
- borderWidth,
1303
- tailwindBorderStyleWidthPlusColor.borderWidth,
1304
- 'setBorderWidth',
1305
- )
1306
- }
1307
- /**
1308
- * Handles changes to the border color of the selected element.
1309
- * @param {string} [borderColor] - The selected border color class.
1310
- */
1311
- public handleBorderColor(borderColor?: string): void {
1312
- this.applyElementClassChanges(
1313
- borderColor,
1314
- tailwindBorderStyleWidthPlusColor.borderColor,
1315
- 'setBorderColor',
1316
- )
1317
- }
1318
- // border color, style & width / end
1319
-
1320
- /**
1321
- * Handles changes to the background color of the selected element.
1322
- * @param {string} [color] - The selected background color class.
1323
- */
1324
- public handleBackgroundColor(color?: string): void {
1325
- this.applyElementClassChanges(
1326
- color,
1327
- tailwindColors.backgroundColorVariables,
1328
- 'setBackgroundColor',
1329
- )
1330
- }
1331
-
1332
- /**
1333
- * Handles changes to the text color of the selected element.
1334
- * @param {string} [color] - The selected text color class.
1335
- */
1336
- public handleTextColor(color?: string): void {
1337
- this.applyElementClassChanges(color, tailwindColors.textColorVariables, 'setTextColor')
1338
- }
1339
-
1340
- /**
1341
- * Handles changes to the global border radius of the selected element.
1342
- * @param {string} [borderRadiusGlobal] - The selected global border radius class.
1343
- */
1344
- handleBorderRadiusGlobal(borderRadiusGlobal?: string): void {
1345
- this.applyElementClassChanges(
1346
- borderRadiusGlobal,
1347
- tailwindBorderRadius.roundedGlobal,
1348
- 'setBorderRadiusGlobal',
1349
- )
1350
- }
1351
- /**
1352
- * Handles changes to the top-left border radius of the selected element.
1353
- * @param {string} [borderRadiusTopLeft] - The selected top-left border radius class.
1354
- */
1355
- handleBorderRadiusTopLeft(borderRadiusTopLeft?: string): void {
1356
- this.applyElementClassChanges(
1357
- borderRadiusTopLeft,
1358
- tailwindBorderRadius.roundedTopLeft,
1359
- 'setBorderRadiusTopLeft',
1360
- )
1361
- }
1362
- /**
1363
- * Handles changes to the top-right border radius of the selected element.
1364
- * @param {string} [borderRadiusTopRight] - The selected top-right border radius class.
1365
- */
1366
- handleBorderRadiusTopRight(borderRadiusTopRight?: string): void {
1367
- this.applyElementClassChanges(
1368
- borderRadiusTopRight,
1369
- tailwindBorderRadius.roundedTopRight,
1370
- 'setBorderRadiusTopRight',
1371
- )
1372
- }
1373
- /**
1374
- * Handles changes to the bottom-left border radius of the selected element.
1375
- * @param {string} [borderRadiusBottomleft] - The selected bottom-left border radius class.
1376
- */
1377
- handleBorderRadiusBottomleft(borderRadiusBottomleft?: string): void {
1378
- this.applyElementClassChanges(
1379
- borderRadiusBottomleft,
1380
- tailwindBorderRadius.roundedBottomLeft,
1381
- 'setBorderRadiusBottomleft',
1382
- )
1383
- }
1384
- /**
1385
- * Handles changes to the bottom-right border radius of the selected element.
1386
- * @param {string} [borderRadiusBottomRight] - The selected bottom-right border radius class.
1387
- */
1388
- handleBorderRadiusBottomRight(borderRadiusBottomRight?: string): void {
1389
- this.applyElementClassChanges(
1390
- borderRadiusBottomRight,
1391
- tailwindBorderRadius.roundedBottomRight,
1392
- 'setBorderRadiusBottomRight',
1393
- )
1394
- }
1395
- // border radius / end
1396
-
1397
- /**
1398
- * Handles changes to the tablet font size of the selected element.
1399
- * @param {string} [userSelectedFontSize] - The selected font size class for tablet.
1400
- */
1401
- handleFontSizeTablet(userSelectedFontSize?: string): void {
1402
- this.applyElementClassChanges(
1403
- userSelectedFontSize,
1404
- tailwindFontSizes.fontTablet,
1405
- 'setFontTablet',
1406
- )
1407
- }
1408
- /**
1409
- * Handles changes to the mobile font size of the selected element.
1410
- * @param {string} [userSelectedFontSize] - The selected font size class for mobile.
1411
- */
1412
- handleFontSizeMobile(userSelectedFontSize?: string): void {
1413
- this.applyElementClassChanges(
1414
- userSelectedFontSize,
1415
- tailwindFontSizes.fontMobile,
1416
- 'setFontMobile',
1417
- )
1418
- }
1419
-
1420
- /**
1421
- * Handles changes to the background opacity of the selected element.
1422
- * @param {string} [opacity] - The selected background opacity class.
1423
- */
1424
- handleBackgroundOpacity(opacity?: string): void {
1425
- this.applyElementClassChanges(
1426
- opacity,
1427
- tailwindOpacities.backgroundOpacities,
1428
- 'setBackgroundOpacity',
1429
- )
1430
- }
1431
- /**
1432
- * Handles changes to the opacity of the selected element.
1433
- * @param {string} [opacity] - The selected opacity class.
1434
- */
1435
- handleOpacity(opacity?: string): void {
1436
- this.applyElementClassChanges(opacity, tailwindOpacities.opacities, 'setOpacity')
1437
- }
1438
-
1439
- /**
1440
- * Removes all components from both the builder state and the DOM.
1441
- * @private
1442
- */
1443
- private deleteAllComponentsFromDOM() {
1444
- // Clear the store
1445
- this.pageBuilderStateStore.setComponents([])
1446
-
1447
- // Also clear the DOM
1448
- const pagebuilder = document.querySelector('#pagebuilder')
1449
- if (pagebuilder) {
1450
- // Remove all section elements (assuming each component is a <section>)
1451
- pagebuilder
1452
- .querySelectorAll('section[data-componentid]')
1453
- .forEach((section) => section.remove())
1454
- }
1455
- }
1456
-
1457
- public async undo() {
1458
- this.pageBuilderStateStore.setIsLoadingGlobal(true)
1459
- await delay(300)
1460
- const baseKey = this.getHistoryBaseKey()
1461
- if (!baseKey) return
1462
-
1463
- const history = LocalStorageManager.getHistory(baseKey)
1464
- if (history.length > 1 && this.pageBuilderStateStore.getHistoryIndex > 0) {
1465
- this.pageBuilderStateStore.setHistoryIndex(this.pageBuilderStateStore.getHistoryIndex - 1)
1466
- const data = history[this.pageBuilderStateStore.getHistoryIndex]
1467
- const htmlString = this.renderComponentsToHtml(data.components)
1468
- await this.mountComponentsToDOM(htmlString, false, data.pageSettings)
1469
- }
1470
- this.pageBuilderStateStore.setIsLoadingGlobal(false)
1471
- }
1472
-
1473
- public async redo() {
1474
- this.pageBuilderStateStore.setIsLoadingGlobal(true)
1475
- await delay(300)
1476
- const baseKey = this.getHistoryBaseKey()
1477
- if (!baseKey) return
1478
-
1479
- const history = LocalStorageManager.getHistory(baseKey)
1480
- if (history.length > 0 && this.pageBuilderStateStore.getHistoryIndex < history.length - 1) {
1481
- this.pageBuilderStateStore.setHistoryIndex(this.pageBuilderStateStore.getHistoryIndex + 1)
1482
- const data = history[this.pageBuilderStateStore.getHistoryIndex]
1483
- const htmlString = this.renderComponentsToHtml(data.components)
1484
- await this.mountComponentsToDOM(htmlString, false, data.pageSettings)
1485
- }
1486
- this.pageBuilderStateStore.setIsLoadingGlobal(false)
1487
- }
1488
-
1489
- private hasVisibleContent(element: HTMLElement): boolean {
1490
- if (!element) return false
1491
-
1492
- // Check for meaningful elements
1493
- const meaningfulContentSelector =
1494
- 'img, video, iframe, input, button, a, h1, h2, h3, h4, h5, h6, p, li, blockquote, pre, code, table'
1495
- if (element.querySelector(meaningfulContentSelector)) return true
1496
-
1497
- // Check for non-empty text nodes
1498
- const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT, null)
1499
- while (walker.nextNode()) {
1500
- if (walker.currentNode.nodeValue && walker.currentNode.nodeValue.trim() !== '') {
1501
- return true
1502
- }
1503
- }
1504
-
1505
- return false
1506
- }
1507
-
1508
- private isSectionEmpty(section: HTMLElement): boolean {
1509
- return !this.hasVisibleContent(section)
1510
- }
1511
-
1512
- /**
1513
- * Duplicate the currently selected component from the DOM and the state.
1514
- * @returns {Promise<void>}
1515
- */
1516
- public async duplicateComponent() {
1517
- // Sync latest DOM changes to the store
1518
- this.syncDomToStoreOnly()
1519
- await nextTick()
1520
-
1521
- const components = this.pageBuilderStateStore.getComponents
1522
- const selectedComponent = this.getComponent.value
1523
-
1524
- if (!components || !selectedComponent) return
1525
-
1526
- // Find the index of the selected component
1527
- const index = components.findIndex(
1528
- (component: ComponentObject) => component.id === selectedComponent.id,
1529
- )
1530
- if (index === -1) return
1531
-
1532
- // Clone the component and generate a new id
1533
- const clonedComponent = this.cloneCompObjForDOMInsertion(components[index])
1534
-
1535
- // Insert the cloned component right after the selected one
1536
- const newComponents = [
1537
- ...components.slice(0, index + 1),
1538
- clonedComponent,
1539
- ...components.slice(index + 1),
1540
- ]
1541
-
1542
- this.pageBuilderStateStore.setComponents(newComponents)
1543
-
1544
- // Wait for DOM update and re-attach listeners
1545
- await nextTick()
1546
- await this.addListenersToEditableElements()
1547
-
1548
- // Optionally, select the new duplicated component
1549
- this.pageBuilderStateStore.setComponent(clonedComponent)
1550
- this.pageBuilderStateStore.setElement(null)
1551
-
1552
- // Auto-save after duplication
1553
- await this.handleAutoSave()
1554
- }
1555
- /**
1556
- * Deletes the currently selected component from the DOM and the state.
1557
- * @returns {Promise<void>}
1558
- */
1559
- public async deleteComponentFromDOM() {
1560
- this.syncDomToStoreOnly()
1561
- await nextTick()
1562
-
1563
- const components = this.pageBuilderStateStore.getComponents
1564
-
1565
- if (!components) return
1566
-
1567
- // Find the index of the component to be deleted.
1568
- const indexToDelete = components.findIndex((component: ComponentObject) =>
1569
- this.getComponent.value ? component.id === this.getComponent.value.id : false,
1570
- )
1571
-
1572
- if (indexToDelete === -1) {
1573
- // If the component is not found, do nothing.
1574
- return
1575
- }
1576
-
1577
- // Create a new array excluding the component to be deleted.
1578
- const newComponents = [
1579
- ...components.slice(0, indexToDelete),
1580
- ...components.slice(indexToDelete + 1),
1581
- ]
1582
-
1583
- this.pageBuilderStateStore.setComponents(newComponents)
1584
-
1585
- // Wait for the DOM to update before re-attaching event listeners.
1586
- await nextTick()
1587
- await this.addListenersToEditableElements()
1588
-
1589
- this.pageBuilderStateStore.setComponent(null)
1590
- this.pageBuilderStateStore.setElement(null)
1591
-
1592
- // Trigger an auto-save after deletion.
1593
- await this.handleAutoSave()
1594
- }
1595
-
1596
- /**
1597
- * Deletes the currently selected element from the DOM and stores it for potential restoration.
1598
- * @returns {Promise<void>}
1599
- */
1600
- public async deleteElementFromDOM() {
1601
- const element = this.getElement.value
1602
- if (!element) return
1603
-
1604
- // Remove the 'selected' attribute before deletion to avoid visual artifacts.
1605
- element.removeAttribute('selected')
1606
-
1607
- if (!element.parentNode) {
1608
- this.pageBuilderStateStore.setComponent(null)
1609
- this.pageBuilderStateStore.setElement(null)
1610
- return
1611
- }
1612
-
1613
- const parentSection = element.closest('section')
1614
-
1615
- // If the element to be deleted is the section itself
1616
- if (element.tagName === 'SECTION') {
1617
- await this.deleteComponentFromDOM()
1618
- } else {
1619
- // If the element is inside a section
1620
- element.remove()
1621
- // --- Sync DOM to store after DOM mutation ---
1622
- this.syncDomToStoreOnly()
1623
-
1624
- if (parentSection && this.isSectionEmpty(parentSection)) {
1625
- const componentId = parentSection.getAttribute('data-componentid')
1626
- if (componentId) {
1627
- const components = this.pageBuilderStateStore.getComponents
1628
- if (components) {
1629
- const indexToDelete = components.findIndex((c: ComponentObject) => c.id === componentId)
1630
- if (indexToDelete !== -1) {
1631
- const newComponents = [
1632
- ...components.slice(0, indexToDelete),
1633
- ...components.slice(indexToDelete + 1),
1634
- ]
1635
- this.pageBuilderStateStore.setComponents(newComponents)
1636
- parentSection.remove() // Directly remove from DOM
1637
- // --- Sync DOM to store again after removing section ---
1638
- this.syncDomToStoreOnly()
1639
- // Save after removing the section
1640
- this.saveDomComponentsToLocalStorage()
1641
- }
1642
- }
1643
- }
1644
- } else if (parentSection) {
1645
- // If the section is not empty, update its HTML content in the store
1646
- const componentId = parentSection.getAttribute('data-componentid')
1647
- if (componentId) {
1648
- const components = this.pageBuilderStateStore.getComponents
1649
- if (components) {
1650
- const componentIndex = components.findIndex(
1651
- (c: ComponentObject) => c.id === componentId,
1652
- )
1653
- if (componentIndex !== -1) {
1654
- const updatedComponent = {
1655
- ...components[componentIndex],
1656
- html_code: parentSection.outerHTML,
1657
- }
1658
- const newComponents = [
1659
- ...components.slice(0, componentIndex),
1660
- updatedComponent,
1661
- ...components.slice(componentIndex + 1),
1662
- ]
1663
- this.pageBuilderStateStore.setComponents(newComponents)
1664
- // --- Sync DOM to store after updating section ---
1665
- this.syncDomToStoreOnly()
1666
- // Save after updating the section
1667
- this.saveDomComponentsToLocalStorage()
1668
- }
1669
- }
1670
- }
1671
- } else {
1672
- // If no parentSection, still sync and save
1673
- this.syncDomToStoreOnly()
1674
- this.saveDomComponentsToLocalStorage()
1675
- }
1676
- }
1677
-
1678
- // Clear the selection state.
1679
- this.pageBuilderStateStore.setComponent(null)
1680
- this.pageBuilderStateStore.setElement(null)
1681
-
1682
- // Deselect any selected or hovered elements in the builder UI.
1683
- await this.clearHtmlSelection()
1684
- // Wait for the DOM to update before re-attaching event listeners.
1685
- await nextTick()
1686
- // Re-attach event listeners to all editable elements.
1687
- await this.addListenersToEditableElements()
1688
- }
1689
-
1690
- /**
1691
- * Removes a CSS class from the currently selected element.
1692
- * @param {string} userSelectedClass - The class to remove.
1693
- */
1694
- public handleRemoveClasses(userSelectedClass: string): void {
1695
- // remove selected class from element
1696
- if (this.getElement.value?.classList.contains(userSelectedClass)) {
1697
- this.getElement.value?.classList.remove(userSelectedClass)
1698
-
1699
- this.pageBuilderStateStore.setElement(this.getElement.value)
1700
- this.pageBuilderStateStore.removeClass(userSelectedClass)
1701
- }
1702
- }
1703
-
1704
- /**
1705
- * Reorders the currently selected component up or down in the component list.
1706
- * @param {number} direction - The direction to move the component (-1 for up, 1 for down).
1707
- */
1708
- public async reorderComponent(direction: number): Promise<void> {
1709
- if (!this.getComponents.value || !this.getComponent.value) return
1710
-
1711
- if (this.getComponents.value.length <= 1) return
1712
-
1713
- // Find the component to move.
1714
- const componentToMove = this.getComponent.value
1715
-
1716
- // Determine the current index of the component.
1717
- const currentIndex = this.getComponents.value.findIndex(
1718
- (component) => component.id === componentToMove.id,
1719
- )
1720
-
1721
- if (currentIndex === -1) {
1722
- // Component not found in the array.
1723
- return
1724
- }
1725
-
1726
- const newIndex = currentIndex + direction
1727
-
1728
- // Ensure the new index is within the bounds of the array.
1729
- if (newIndex < 0 || newIndex >= this.getComponents.value.length) {
1730
- return
1731
- }
1732
-
1733
- // Move the component to the new position in the array.
1734
- this.getComponents.value.splice(currentIndex, 1)
1735
- this.getComponents.value.splice(newIndex, 0, componentToMove)
1736
-
1737
- // Wait for the DOM to update after reordering
1738
- await nextTick()
1739
-
1740
- // Scroll to the moved component
1741
- const pageBuilderWrapper = document.querySelector('#page-builder-wrapper') as HTMLElement | null
1742
- const movedComponentElement = pageBuilderWrapper?.querySelector(
1743
- `section[data-componentid="${componentToMove.id}"]`,
1744
- ) as HTMLElement
1745
-
1746
- if (movedComponentElement) {
1747
- // Apply highlight to the moved element
1748
- movedComponentElement.classList.add('pbx-reorder-highlight')
1749
-
1750
- // Highlight its new neighbors (if they exist)
1751
- const prevSibling = movedComponentElement.previousElementSibling as HTMLElement
1752
- const nextSibling = movedComponentElement.nextElementSibling as HTMLElement
1753
-
1754
- if (prevSibling && prevSibling.tagName === 'SECTION') {
1755
- prevSibling.classList.add('pbx-sibling-highlight')
1756
- }
1757
- if (nextSibling && nextSibling.tagName === 'SECTION') {
1758
- nextSibling.classList.add('pbx-sibling-highlight')
1759
- }
1760
-
1761
- if (pageBuilderWrapper) {
1762
- // Scroll to the moved component
1763
- const topPos = movedComponentElement.offsetTop - pageBuilderWrapper.offsetTop
1764
- pageBuilderWrapper.scrollTop = topPos - pageBuilderWrapper.clientHeight / 2
1765
-
1766
- // Remove highlights after a delay
1767
- setTimeout(() => {
1768
- movedComponentElement.classList.remove('pbx-reorder-highlight')
1769
- if (prevSibling && prevSibling.tagName === 'SECTION') {
1770
- prevSibling.classList.remove('pbx-sibling-highlight')
1771
- }
1772
- if (nextSibling && nextSibling.tagName === 'SECTION') {
1773
- nextSibling.classList.remove('pbx-sibling-highlight')
1774
- }
1775
- }, 200)
1776
- }
1777
- }
1778
- }
1779
-
1780
- /**
1781
- * Checks if the currently selected component can be moved up.
1782
- * @returns {boolean} True if the component can be moved up, false otherwise.
1783
- */
1784
- public canMoveUp(): boolean {
1785
- if (!this.getComponents.value || !this.getComponent.value) return false
1786
- const currentIndex = this.getComponents.value.findIndex(
1787
- (component) => component.id === this.getComponent.value?.id,
1788
- )
1789
- return currentIndex > 0
1790
- }
1791
-
1792
- /**
1793
- * Checks if the currently selected component can be moved down.
1794
- * @returns {boolean} True if the component can be moved down, false otherwise.
1795
- */
1796
- public canMoveDown(): boolean {
1797
- if (!this.getComponents.value || !this.getComponent.value) return false
1798
- const currentIndex = this.getComponents.value.findIndex(
1799
- (component) => component.id === this.getComponent.value?.id,
1800
- )
1801
- return currentIndex < this.getComponents.value.length - 1
1802
- }
1803
-
1804
- /**
1805
- * Ensures that a text area element has content, adding a visual indicator if it's empty.
1806
- */
1807
- public ensureTextAreaHasContent = () => {
1808
- if (!this.getElement.value) return
1809
-
1810
- // text content
1811
- if (typeof this.getElement.value.innerHTML !== 'string') {
1812
- return
1813
- }
1814
- const element = this.getElement.value
1815
- const elementTag = element.tagName
1816
-
1817
- if (
1818
- ['DIV'].includes(elementTag) &&
1819
- element.tagName.toLowerCase() !== 'img' &&
1820
- element.textContent &&
1821
- Number(element.textContent.length) === 0
1822
- ) {
1823
- element.classList.add('h-6')
1824
- element.classList.add('bg-red-50')
1825
- } else {
1826
- element.classList.remove('h-6')
1827
- element.classList.remove('bg-red-50')
1828
- }
1829
- }
1830
-
1831
- /**
1832
- * Handles text input for an element, updating its content.
1833
- * @param {string} textContentVueModel - The new text content from the Vue model.
1834
- * @returns {Promise<void>}
1835
- */
1836
- public handleTextInput = async (textContentVueModel: string): Promise<void> => {
1837
- if (typeof this.getElement.value?.innerHTML !== 'string') {
1838
- return
1839
- }
1840
-
1841
- if (typeof this.getElement.value.innerHTML === 'string') {
1842
- await nextTick()
1843
-
1844
- // Update text content
1845
- this.getElement.value.textContent = textContentVueModel
1846
-
1847
- this.pageBuilderStateStore.setTextAreaVueModel(this.getElement.value.innerHTML)
1848
-
1849
- this.getElement.value.innerHTML = textContentVueModel
1850
- }
1851
-
1852
- this.ensureTextAreaHasContent()
1853
- }
1854
-
1855
- /**
1856
- * Checks if the selected element or its first child is an iframe.
1857
- * @returns {boolean} True if it is an iframe, false otherwise.
1858
- */
1859
- public ElOrFirstChildIsIframe() {
1860
- if (
1861
- this.getElement.value?.tagName === 'IFRAME' ||
1862
- this.getElement.value?.firstElementChild?.tagName === 'IFRAME'
1863
- ) {
1864
- return true
1865
- } else {
1866
- return false
1867
- }
1868
- }
1869
- /**
1870
- * Checks if the selected element is a valid text container (i.e., does not contain images or divs).
1871
- * @returns {boolean | undefined} True if it's a valid text element, otherwise undefined.
1872
- */
1873
- public isSelectedElementValidText() {
1874
- let reachedElseStatement = false
1875
-
1876
- // Get all child elements of the parentDiv
1877
- const childElements = this.getElement.value?.children
1878
- if (
1879
- this.getElement.value?.tagName === 'IMG' ||
1880
- this.getElement.value?.firstElementChild?.tagName === 'IFRAME'
1881
- ) {
1882
- return
1883
- }
1884
- if (!childElements) {
1885
- return
1886
- }
1887
-
1888
- Array.from(childElements).forEach((element) => {
1889
- if (element?.tagName === 'IMG' || element?.tagName === 'DIV') {
1890
- reachedElseStatement = false
1891
- } else {
1892
- reachedElseStatement = true
1893
- }
1894
- })
1895
-
1896
- return reachedElseStatement
1897
- }
1898
-
1899
- /**
1900
- * Generates a preview of the current page design.
1901
- */
1902
- public previewCurrentDesign() {
1903
- this.pageBuilderStateStore.setElement(null)
1904
-
1905
- const pagebuilder = document.querySelector('#pagebuilder')
1906
- if (!pagebuilder) return
1907
-
1908
- if (pagebuilder) {
1909
- // Get cleaned HTML from entire builder
1910
- const cleanedHTML = extractCleanHTMLFromPageBuilder(
1911
- pagebuilder as HTMLElement,
1912
- this.pageBuilderStateStore.getPageBuilderConfig
1913
- ? this.pageBuilderStateStore.getPageBuilderConfig
1914
- : undefined,
1915
- )
1916
-
1917
- // Store as array with one string (as your preview expects an array)
1918
- const previewData = JSON.stringify([cleanedHTML])
1919
-
1920
- this.pageBuilderStateStore.setCurrentLayoutPreview(previewData)
1921
- }
1922
- }
1923
- /**
1924
- * Sanitizes a string to be used as a key in local storage.
1925
- * @param {string} input - The string to sanitize.
1926
- * @returns {string} The sanitized string.
1927
- */
1928
- public sanitizeForLocalStorage(input: string): string {
1929
- return input
1930
- .trim() // Remove leading/trailing spaces
1931
- .toLowerCase() // Convert to lowercase
1932
- .replace(/\s+/g, '-') // Replace one or more spaces with single hyphen
1933
- .replace(/[^a-z0-9-]/g, '') // Remove all non-alphanumeric characters except hyphens
1934
- .replace(/-+/g, '-') // Replace multiple consecutive hyphens with single hyphen
1935
- .replace(/^-+|-+$/g, '') // Remove leading/trailing hyphens
1936
- }
1937
-
1938
- /**
1939
- * Clones an element and removes selection-related attributes from the clone.
1940
- * @param {HTMLElement} element - The element to clone.
1941
- * @returns {HTMLElement} The sanitized clone.
1942
- * @private
1943
- */
1944
- private cloneAndRemoveSelectionAttributes(element: HTMLElement): HTMLElement {
1945
- // Deep clone the element
1946
- const clone = element.cloneNode(true) as HTMLElement
1947
-
1948
- // Remove [hovered] and [selected] from the clone and all descendants
1949
- clone.querySelectorAll('[hovered]').forEach((el) => el.removeAttribute('hovered'))
1950
- clone.querySelectorAll('[selected]').forEach((el) => el.removeAttribute('selected'))
1951
- // Also remove from the root element itself if present
1952
- clone.removeAttribute('hovered')
1953
- clone.removeAttribute('selected')
1954
-
1955
- return clone
1956
- }
1957
-
1958
- /**
1959
- * Syncs the current DOM state of components to the in-memory store.
1960
- * @private
1961
- */
1962
- public syncDomToStoreOnly() {
1963
- const pagebuilder = document.querySelector('#pagebuilder')
1964
- if (!pagebuilder) return
1965
-
1966
- const componentsToSave: { html_code: string; id: string | null; title: string }[] = []
1967
-
1968
- pagebuilder.querySelectorAll('section[data-componentid]').forEach((section) => {
1969
- const sanitizedSection = this.cloneAndRemoveSelectionAttributes(section as HTMLElement)
1970
- componentsToSave.push({
1971
- html_code: sanitizedSection.outerHTML,
1972
- id: sanitizedSection.getAttribute('data-componentid'),
1973
- title: sanitizedSection.getAttribute('data-component-title') || 'Untitled Component',
1974
- })
1975
- })
1976
-
1977
- this.pageBuilderStateStore.setComponents(componentsToSave)
1978
- }
1979
-
1980
- public async generateHtmlFromComponents(): Promise<string> {
1981
- this.syncDomToStoreOnly()
1982
- await nextTick()
1983
-
1984
- const components = this.pageBuilderStateStore.getComponents
1985
-
1986
- if (!Array.isArray(components)) {
1987
- return ''
1988
- }
1989
-
1990
- // Wait for Vue to finish DOM updates before attaching event listeners. This ensure elements exist in the DOM.
1991
- await nextTick()
1992
- // Attach event listeners to all editable elements in the Builder
1993
- await this.addListenersToEditableElements()
1994
-
1995
- return components
1996
- .map((comp) => {
1997
- return comp.html_code
1998
- .replace(/data-componentid="[^"]*"/g, '') // remove data-componentid
1999
- .replace(/\s{2,}/g, ' ') // optional: clean up excess spaces
2000
- })
2001
- .join('\n')
2002
- }
2003
-
2004
- /**
2005
- * Saves the current DOM state of components to local storage.
2006
- * @private
2007
- */
2008
- private saveDomComponentsToLocalStorage() {
2009
- this.updateLocalStorageItemName()
2010
- const pagebuilder = document.querySelector('#pagebuilder')
2011
- if (!pagebuilder) return
2012
-
2013
- const hoveredElement = pagebuilder.querySelector('[hovered]')
2014
- if (hoveredElement) {
2015
- hoveredElement.removeAttribute('hovered')
2016
- }
2017
-
2018
- const componentsToSave: { html_code: string; title: string }[] = []
2019
-
2020
- pagebuilder.querySelectorAll('section[data-componentid]').forEach((section) => {
2021
- const sanitizedSection = this.cloneAndRemoveSelectionAttributes(section as HTMLElement)
2022
-
2023
- // Remove the data-componentid attribute
2024
- sanitizedSection.removeAttribute('data-componentid')
2025
-
2026
- componentsToSave.push({
2027
- html_code: sanitizedSection.outerHTML,
2028
- title: sanitizedSection.getAttribute('data-component-title') || 'Untitled Component',
2029
- })
2030
- })
2031
-
2032
- const pageSettings = {
2033
- classes: pagebuilder.className || '',
2034
- style: pagebuilder.getAttribute('style') || (pagebuilder as HTMLElement).style.cssText || '',
2035
- }
2036
-
2037
- const dataToSave = {
2038
- components: componentsToSave,
2039
- pageBuilderContentSavedAt: new Date().toISOString(),
2040
- pageSettings,
2041
- }
2042
-
2043
- const baseKey = this.getHistoryBaseKey()
2044
-
2045
- if (baseKey) {
2046
- const currentDataRaw = localStorage.getItem(baseKey)
2047
-
2048
- if (!currentDataRaw) {
2049
- localStorage.setItem(baseKey, JSON.stringify(dataToSave))
2050
- }
2051
- if (currentDataRaw) {
2052
- const currentData = JSON.parse(currentDataRaw)
2053
-
2054
- // Compare components
2055
- const currentComponents = currentData.components || []
2056
- const newComponents = dataToSave.components || []
2057
-
2058
- const hasChanges =
2059
- newComponents.length !== currentComponents.length ||
2060
- newComponents.some((newComponent, index) => {
2061
- const currentComponent = currentComponents[index]
2062
- return (
2063
- // New component added
2064
- !currentComponent ||
2065
- // Component HTML changed
2066
- currentComponent.html_code !== newComponent.html_code
2067
- )
2068
- })
2069
-
2070
- // Compare pageSettings
2071
- const hasPageSettingsChanges =
2072
- (currentData.pageSettings &&
2073
- currentData.pageSettings.classes !== dataToSave.pageSettings.classes) ||
2074
- (currentData.pageSettings &&
2075
- currentData.pageSettings.style !== dataToSave.pageSettings.style)
2076
-
2077
- // Only save to local storage if there's a difference between the existing saved data and the current DOM data
2078
- if (hasChanges || hasPageSettingsChanges) {
2079
- localStorage.setItem(baseKey, JSON.stringify(dataToSave))
2080
- let history = LocalStorageManager.getHistory(baseKey)
2081
-
2082
- const lastState = history[history.length - 1]
2083
- if (lastState) {
2084
- const lastComponents = JSON.stringify(lastState.components)
2085
- const newComponents = JSON.stringify(dataToSave.components)
2086
- const lastSettings = JSON.stringify(lastState.pageSettings)
2087
- const newSettings = JSON.stringify(dataToSave.pageSettings)
2088
- if (lastComponents === newComponents && lastSettings === newSettings) {
2089
- return // Do not save duplicate state
2090
- }
2091
- }
2092
-
2093
- if (this.pageBuilderStateStore.getHistoryIndex < history.length - 1) {
2094
- history = history.slice(0, this.pageBuilderStateStore.getHistoryIndex + 1)
2095
- }
2096
- history.push(dataToSave)
2097
- if (history.length > 10) {
2098
- history = history.slice(history.length - 10)
2099
- }
2100
- localStorage.setItem(baseKey + '-history', JSON.stringify(history))
2101
- this.pageBuilderStateStore.setHistoryIndex(history.length - 1)
2102
- this.pageBuilderStateStore.setHistoryLength(history.length)
2103
- return
2104
- }
2105
- }
2106
- }
2107
- }
2108
- /**
2109
- * Removes the current page's components from local storage.
2110
- * @private
2111
- */
2112
- private async removeCurrentComponentsFromLocalStorage() {
2113
- this.updateLocalStorageItemName()
2114
- await nextTick()
2115
-
2116
- const key = this.getLocalStorageItemName.value
2117
- if (key) {
2118
- localStorage.removeItem(key)
2119
- }
2120
- }
2121
-
2122
- /**
2123
- * Handles the form submission process, clearing local storage and the DOM.
2124
- * @returns {Promise<void>}
2125
- */
2126
- public async handleFormSubmission() {
2127
- await this.removeCurrentComponentsFromLocalStorage()
2128
- this.deleteAllComponentsFromDOM()
2129
- this.pageBuilderStateStore.setComponents([])
2130
- }
2131
-
2132
- /**
2133
- * Parses a CSS style string into a key-value object.
2134
- * @param {string} style - The style string to parse.
2135
- * @returns {Record<string, string>} The parsed style object.
2136
- * @private
2137
- */
2138
- private parseStyleString(style: string): Record<string, string> {
2139
- return style
2140
- .split(';')
2141
- .map((s) => s.trim())
2142
- .filter(Boolean)
2143
- .reduce(
2144
- (acc, rule) => {
2145
- const [key, value] = rule.split(':').map((str) => str.trim())
2146
- if (key && value) acc[key] = value
2147
- return acc
2148
- },
2149
- {} as Record<string, string>,
2150
- )
2151
- }
2152
-
2153
- /**
2154
- * Deletes old page builder data from local storage (older than 2 weeks).
2155
- */
2156
- deleteOldPageBuilderLocalStorage(): void {
2157
- const config = this.pageBuilderStateStore.getPageBuilderConfig
2158
- const formType = config && config.updateOrCreate && config.updateOrCreate.formType
2159
-
2160
- if (formType === 'update') {
2161
- let oldCountLocalStorages = 0
2162
- const deletedItemsLog: { Number: number; Key: string; SavedAt: string }[] = []
2163
-
2164
- // const pastTime = new Date(Date.now() - 1 * 60 * 1000) // 1 minute
2165
- const pastTime = new Date(Date.now() - 14 * 24 * 60 * 60 * 1000) // 2 weeks
2166
-
2167
- for (let i = 0; i < localStorage.length; i++) {
2168
- const key = localStorage.key(i)
2169
-
2170
- if (!key) continue
2171
- if (!key.startsWith('page-builder-update-resource-')) continue
2172
-
2173
- try {
2174
- const storeComponents = localStorage.getItem(key)
2175
- if (!storeComponents) continue
2176
-
2177
- const storeComponentsParsed = JSON.parse(storeComponents)
2178
- const savedAt = storeComponentsParsed.pageBuilderContentSavedAt
2179
- if (savedAt) {
2180
- const savedAtDate = new Date(savedAt)
2181
-
2182
- if (savedAtDate < pastTime) {
2183
- oldCountLocalStorages++
2184
- deletedItemsLog.push({
2185
- Number: oldCountLocalStorages,
2186
- Key: key,
2187
- SavedAt: savedAt,
2188
- })
2189
-
2190
- // Delete old items
2191
- localStorage.removeItem(key)
2192
- }
2193
- }
2194
- } catch {
2195
- // Ignore parse errors for unrelated keys
2196
- }
2197
- }
2198
-
2199
- if (deletedItemsLog.length > 0) {
2200
- console.info(
2201
- `Deleted ${deletedItemsLog.length} localStorage item(s) older than ${pastTime} days:`,
2202
- )
2203
- console.table(deletedItemsLog)
2204
- }
2205
- }
2206
- }
2207
-
2208
- /**
2209
- * Sets a flag to indicate that the user has started editing.
2210
- */
2211
- public startEditing() {
2212
- this.hasStartedEditing = true
2213
- }
2214
-
2215
- /**
2216
- * Resumes editing from a draft saved in local storage.
2217
- * @returns {Promise<void>}
2218
- */
2219
- public async resumeEditingFromDraft() {
2220
- this.updateLocalStorageItemName()
2221
-
2222
- const localStorageData = this.getSavedPageHtml()
2223
-
2224
- if (localStorageData) {
2225
- await delay(400)
2226
- this.pageBuilderStateStore.setIsLoadingResumeEditing(true)
2227
- await this.mountComponentsToDOM(localStorageData)
2228
- this.pageBuilderStateStore.setIsLoadingResumeEditing(false)
2229
- }
2230
-
2231
- // Wait for Vue to finish DOM updates before attaching event listeners. This ensure elements exist in the DOM.
2232
- await nextTick()
2233
- // Attach event listeners to all editable elements in the Builder
2234
- await this.addListenersToEditableElements()
2235
- // set loading to false
2236
- this.pageBuilderStateStore.setIsLoadingResumeEditing(false)
2237
- }
2238
-
2239
- /**
2240
- * Restores the original content that was loaded when the builder started.
2241
- * @returns {Promise<void>}
2242
- */
2243
- public async restoreOriginalContent() {
2244
- this.updateLocalStorageItemName()
2245
-
2246
- this.pageBuilderStateStore.setIsRestoring(true)
2247
- await delay(400)
2248
-
2249
- // Restore the original content if available
2250
- if (Array.isArray(this.originalComponents)) {
2251
- await this.clearClassesFromPage()
2252
- await this.clearInlineStylesFromPage()
2253
- const htmlString = this.renderComponentsToHtml(this.originalComponents)
2254
- await this.mountComponentsToDOM(htmlString)
2255
- this.removeCurrentComponentsFromLocalStorage()
2256
- }
2257
-
2258
- // Wait for Vue to finish DOM updates before attaching event listeners. This ensure elements exist in the DOM.
2259
- await nextTick()
2260
- // Attach event listeners to all editable elements in the Builder
2261
- await this.addListenersToEditableElements()
2262
-
2263
- this.pageBuilderStateStore.setIsRestoring(false)
2264
- }
2265
-
2266
- public async returnLatestComponents() {
2267
- this.syncDomToStoreOnly()
2268
- // Wait for Vue to finish DOM updates before attaching event listeners. This ensure elements exist in the DOM.
2269
- await nextTick()
2270
- // Attach event listeners to all editable elements in the Builder
2271
- await this.addListenersToEditableElements()
2272
-
2273
- return this.pageBuilderStateStore.getComponents
2274
- }
2275
- /**
2276
- * Gets the local storage key for the current resource.
2277
- * @returns {string | null} The local storage key.
2278
- */
2279
- public getStorageItemNameForResource(): string | null {
2280
- return this.getLocalStorageItemName.value
2281
- }
2282
-
2283
- /**
2284
- * Retrieves the saved page HTML from local storage.
2285
- * @returns {string | false} The HTML string or false if not found.
2286
- */
2287
- public getSavedPageHtml() {
2288
- if (!this.getLocalStorageItemName.value) return false
2289
-
2290
- const key = this.getLocalStorageItemName.value
2291
- if (!key) return false
2292
-
2293
- const raw = localStorage.getItem(key)
2294
- if (!raw) return false
2295
-
2296
- const parsed = JSON.parse(raw)
2297
-
2298
- // Object with components and pageSettings
2299
- if (parsed && Array.isArray(parsed.components)) {
2300
- const classes = (parsed.pageSettings && parsed.pageSettings.classes) || ''
2301
- const style = (parsed.pageSettings && parsed.pageSettings.style) || ''
2302
-
2303
- const sectionsHtml = parsed.components
2304
- .map((c: ComponentObject) => {
2305
- const parser = new DOMParser()
2306
- const doc = parser.parseFromString(c.html_code, 'text/html')
2307
- const section = doc.querySelector('section')
2308
-
2309
- if (section) {
2310
- section.removeAttribute('data-componentid') // Remove the data-componentid attribute
2311
- return section.outerHTML
2312
- }
2313
-
2314
- return c.html_code // Fallback in case section is not found
2315
- })
2316
- .join('\n')
2317
-
2318
- return `<div id="pagebuilder" class="${classes}" style="${style}">\n${sectionsHtml}\n</div>`
2319
- }
2320
-
2321
- return false
2322
- }
2323
-
2324
- /**
2325
- * Applies a selected image to the current element.
2326
- * @param {ImageObject} image - The image object to apply.
2327
- * @returns {Promise<void>}
2328
- */
2329
- public async applySelectedImage(image: ImageObject): Promise<void> {
2330
- this.pageBuilderStateStore.setApplyImageToSelection(image)
2331
-
2332
- if (!this.getElement.value) return
2333
-
2334
- // Only apply if an image is staged
2335
- if (this.getApplyImageToSelection.value && this.getApplyImageToSelection.value.src) {
2336
- await nextTick()
2337
- this.pageBuilderStateStore.setBasePrimaryImage(`${this.getApplyImageToSelection.value.src}`)
2338
-
2339
- await this.handleAutoSave()
2340
- }
2341
- }
2342
-
2343
- /**
2344
- * Sets the base primary image from the currently selected element if it's an image.
2345
- * @private
2346
- */
2347
- private setBasePrimaryImageFromSelectedElement() {
2348
- if (!this.getElement.value) return
2349
-
2350
- const currentImageContainer = document.createElement('div')
2351
- currentImageContainer.innerHTML = this.getElement.value.outerHTML
2352
-
2353
- // Get all img and div within the current image container
2354
- const imgElements = currentImageContainer.getElementsByTagName('img')
2355
- const divElements = currentImageContainer.getElementsByTagName('div')
2356
-
2357
- // If exactly one img and no div, set as base primary image
2358
- if (imgElements.length === 1 && divElements.length === 0) {
2359
- this.pageBuilderStateStore.setBasePrimaryImage(imgElements[0].src)
2360
- return
2361
- }
2362
-
2363
- // Otherwise, clear the base primary image
2364
- this.pageBuilderStateStore.setBasePrimaryImage(null)
2365
- }
2366
-
2367
- /**
2368
- * Adds or removes a hyperlink from the selected element.
2369
- * @param {boolean} hyperlinkEnable - Whether to enable or disable the hyperlink.
2370
- * @param {string | null} urlInput - The URL for the hyperlink.
2371
- * @param {boolean} openHyperlinkInNewTab - Whether the link should open in a new tab.
2372
- * @private
2373
- */
2374
- private addHyperlinkToElement(
2375
- hyperlinkEnable: boolean,
2376
- urlInput: string | null,
2377
- openHyperlinkInNewTab: boolean,
2378
- ) {
2379
- if (!this.getElement.value) return
2380
-
2381
- // Check if element is a proper DOM element and has closest method
2382
- if (
2383
- !(this.getElement.value instanceof HTMLElement) ||
2384
- typeof this.getElement.value.closest !== 'function'
2385
- )
2386
- return
2387
-
2388
- const parentHyperlink = this.getElement.value.closest('a')
2389
- const hyperlink = this.getElement.value.querySelector('a')
2390
-
2391
- this.pageBuilderStateStore.setHyperlinkError(null)
2392
-
2393
- // url validation
2394
- const urlRegex = /^https?:\/\//
2395
-
2396
- const isValidURL = ref(true)
2397
-
2398
- if (hyperlinkEnable === true && urlInput !== null) {
2399
- isValidURL.value = urlRegex.test(urlInput)
2400
- }
2401
-
2402
- if (isValidURL.value === false) {
2403
- this.pageBuilderStateStore.setHyperlinkMessage(null)
2404
-
2405
- this.pageBuilderStateStore.setHyperlinkError('URL is not valid')
2406
- return
2407
- }
2408
-
2409
- if (hyperlinkEnable === true && typeof urlInput === 'string') {
2410
- // check if element contains child hyperlink tag
2411
- // updated existing url
2412
- if (hyperlink !== null && urlInput.length !== 0) {
2413
- hyperlink.href = urlInput
2414
-
2415
- // Conditionally set the target attribute if openHyperlinkInNewTab is true
2416
- if (openHyperlinkInNewTab === true) {
2417
- hyperlink.target = '_blank'
2418
- }
2419
- if (openHyperlinkInNewTab === false) {
2420
- hyperlink.removeAttribute('target')
2421
- }
2422
-
2423
- hyperlink.textContent = this.getElement.value.textContent
2424
-
2425
- this.pageBuilderStateStore.setHyperlinkMessage('Succesfully updated element hyperlink')
2426
-
2427
- this.pageBuilderStateStore.setElementContainsHyperlink(true)
2428
-
2429
- return
2430
- }
2431
-
2432
- // check if element contains child a tag
2433
- if (hyperlink === null && urlInput.length !== 0) {
2434
- // add a href
2435
- if (parentHyperlink === null) {
2436
- const link = document.createElement('a')
2437
- link.href = urlInput
2438
-
2439
- // Conditionally set the target attribute if openHyperlinkInNewTab is true
2440
- if (openHyperlinkInNewTab === true) {
2441
- link.target = '_blank'
2442
- }
2443
-
2444
- link.textContent = this.getElement.value.textContent
2445
- this.getElement.value.textContent = ''
2446
- this.getElement.value.appendChild(link)
2447
-
2448
- this.pageBuilderStateStore.setHyperlinkMessage('Successfully added hyperlink to element')
2449
-
2450
- this.pageBuilderStateStore.setElementContainsHyperlink(true)
2451
-
2452
- return
2453
- }
2454
- }
2455
- //
2456
- }
2457
-
2458
- if (hyperlinkEnable === false && urlInput === 'removeHyperlink') {
2459
- // To remove the added hyperlink tag
2460
- const originalText = this.getElement.value.textContent || ''
2461
- const textNode = document.createTextNode(originalText)
2462
- this.getElement.value.textContent = ''
2463
- this.getElement.value.appendChild(textNode)
2464
-
2465
- this.pageBuilderStateStore.setHyberlinkEnable(false)
2466
- this.pageBuilderStateStore.setElementContainsHyperlink(false)
2467
- }
2468
- }
2469
-
2470
- /**
2471
- * Checks if the selected element contains a hyperlink and updates the state accordingly.
2472
- * @private
2473
- */
2474
- private checkForHyperlink() {
2475
- if (!this.getElement.value) return
2476
-
2477
- const hyperlink = this.getElement.value.querySelector('a')
2478
- if (hyperlink !== null) {
2479
- this.pageBuilderStateStore.setHyberlinkEnable(true)
2480
- this.pageBuilderStateStore.setElementContainsHyperlink(true)
2481
- this.pageBuilderStateStore.setHyperlinkInput(hyperlink.href)
2482
- this.pageBuilderStateStore.setHyperlinkMessage(null)
2483
- this.pageBuilderStateStore.setHyperlinkError(null)
2484
-
2485
- if (hyperlink.target === '_blank') {
2486
- this.pageBuilderStateStore.setOpenHyperlinkInNewTab(true)
2487
- }
2488
- if (hyperlink.target !== '_blank') {
2489
- this.pageBuilderStateStore.setOpenHyperlinkInNewTab(false)
2490
- }
2491
-
2492
- return
2493
- }
2494
-
2495
- this.pageBuilderStateStore.setElementContainsHyperlink(false)
2496
- this.pageBuilderStateStore.setHyperlinkInput('')
2497
- this.pageBuilderStateStore.setHyperlinkError(null)
2498
- this.pageBuilderStateStore.setHyperlinkMessage(null)
2499
- this.pageBuilderStateStore.setHyberlinkEnable(false)
2500
- }
2501
-
2502
- /**
2503
- * Handles all hyperlink-related actions for the selected element.
2504
- * @param {boolean} [hyperlinkEnable] - Whether to enable or disable the hyperlink.
2505
- * @param {string | null} [urlInput] - The URL for the hyperlink.
2506
- * @param {boolean} [openHyperlinkInNewTab] - Whether the link should open in a new tab.
2507
- */
2508
- public handleHyperlink(
2509
- hyperlinkEnable?: boolean,
2510
- urlInput?: string | null,
2511
- openHyperlinkInNewTab?: boolean,
2512
- ): void {
2513
- this.pageBuilderStateStore.setHyperlinkAbility(true)
2514
-
2515
- if (!this.getElement.value) return
2516
-
2517
- // Check if element is a proper DOM element and has closest method
2518
- if (
2519
- !(this.getElement.value instanceof HTMLElement) ||
2520
- typeof this.getElement.value.closest !== 'function'
2521
- )
2522
- return
2523
-
2524
- const parentHyperlink = this.getElement.value.closest('a')
2525
-
2526
- // handle case where parent element already has an a href tag
2527
- // when clicking directly on a hyperlink
2528
- if (parentHyperlink !== null) {
2529
- this.pageBuilderStateStore.setHyperlinkAbility(false)
2530
- }
2531
- const elementTag = this.getElement.value?.tagName.toUpperCase()
2532
-
2533
- if (
2534
- elementTag !== 'P' &&
2535
- elementTag !== 'H1' &&
2536
- elementTag !== 'H2' &&
2537
- elementTag !== 'H3' &&
2538
- elementTag !== 'H4' &&
2539
- elementTag !== 'H5' &&
2540
- elementTag !== 'H6'
2541
- ) {
2542
- this.pageBuilderStateStore.setHyperlinkAbility(false)
2543
- }
2544
-
2545
- if (hyperlinkEnable === undefined) {
2546
- this.checkForHyperlink()
2547
- return
2548
- }
2549
-
2550
- this.addHyperlinkToElement(hyperlinkEnable, urlInput || null, openHyperlinkInNewTab || false)
2551
- }
2552
-
2553
- public async addTheme(components: string): Promise<void> {
2554
- if (components) {
2555
- this.validateMountingHTML(components)
2556
- await this.mountComponentsToDOM(components)
2557
- }
2558
- await this.handleAutoSave()
2559
- }
2560
-
2561
- /**
2562
- * Adds a new component to the page builder.
2563
- * @param {ComponentObject} componentObject - The component to add.
2564
- * @returns {Promise<void>}
2565
- */
2566
- public async addComponent(componentObject: ComponentObject): Promise<void> {
2567
- try {
2568
- const clonedComponent = this.cloneCompObjForDOMInsertion({
2569
- html_code: componentObject.html_code,
2570
- id: componentObject.id,
2571
- title: componentObject.title,
2572
- })
2573
-
2574
- this.pageBuilderStateStore.setPushComponents({
2575
- component: clonedComponent,
2576
- componentArrayAddMethod: this.getComponentArrayAddMethod.value
2577
- ? this.getComponentArrayAddMethod.value
2578
- : 'push',
2579
- })
2580
-
2581
- const pageBuilderWrapper = document.querySelector('#page-builder-wrapper')
2582
- // scoll to top or bottom
2583
- if (pageBuilderWrapper) {
2584
- // push to bottom
2585
- if (this.getComponentArrayAddMethod.value === 'push') {
2586
- pageBuilderWrapper.scrollTo({
2587
- top: pageBuilderWrapper.scrollHeight + 50,
2588
- behavior: 'smooth',
2589
- })
2590
- }
2591
- }
2592
-
2593
- // Wait for Vue to finish DOM updates before attaching event listeners. This ensure elements exist in the DOM.
2594
- await nextTick()
2595
- // Attach event listeners to all editable elements in the Builder
2596
- await this.addListenersToEditableElements()
2597
-
2598
- await this.handleAutoSave()
2599
- } catch (error) {
2600
- console.error('Error adding component:', error)
2601
- }
2602
- }
2603
-
2604
- /**
2605
- * Adds a prefix to Tailwind CSS classes in a string.
2606
- * @param {string} classList - The string of classes.
2607
- * @param {string} [prefix='pbx-'] - The prefix to add.
2608
- * @returns {string} The prefixed class string.
2609
- * @private
2610
- */
2611
- private addTailwindPrefixToClasses(classList: string, prefix = 'pbx-'): string {
2612
- return classList
2613
- .split(/\s+/)
2614
- .map((cls) => {
2615
- if (!cls || cls.startsWith(prefix)) return cls
2616
- const parts = cls.split(':')
2617
- const base = parts.pop()!
2618
- if (base.startsWith(prefix)) return cls
2619
- // Always prefix if not already prefixed
2620
- return [...parts, prefix + base].join(':')
2621
- })
2622
- .join(' ')
2623
- }
2624
-
2625
- /**
2626
- * Converts a style object to a CSS string.
2627
- * @param {string | Record<string, string> | null | undefined} styleObj - The style object.
2628
- * @returns {string} The CSS style string.
2629
- * @private
2630
- */
2631
- private convertStyleObjectToString(
2632
- styleObj: string | Record<string, string> | null | undefined,
2633
- ): string {
2634
- if (!styleObj) return ''
2635
- if (typeof styleObj === 'string') return styleObj
2636
-
2637
- return Object.entries(styleObj)
2638
- .map(([key, value]) => {
2639
- const kebabKey = key.replace(/([A-Z])/g, '-$1').toLowerCase()
2640
- return `${kebabKey}: ${value};`
2641
- })
2642
- .join(' ')
2643
- }
2644
-
2645
- /**
2646
- * Parses a string of HTML and extracts builder components and global page settings.
2647
- * - This method expects an **HTML string** containing one or more `<section>...</section>` elements (such as the output from `getSavedPageHtml()` or a previously saved builder HTML string).
2648
- * - **Do NOT pass a JSON string** (such as the result of `JSON.stringify(componentsArray)`) to this method. Passing a JSON string to `DOMParser.parseFromString(..., 'text/html')` will not produce valid DOM nodes. Instead, it will treat the JSON as plain text, resulting in a `<html><head></head><body>{...json...}</body></html>` structure, not real HTML elements.
2649
- * - If you pass a JSON string, you will see lots of `\n` and strange HTML, because the parser is just wrapping your JSON in a `<body>` tag as text.
2650
- *
2651
- * Why only HTML?
2652
- * - It enforces a single source of truth for builder state (HTML).
2653
- * - It prevents misuse (e.g., passing JSON to a DOM parser, which is always a bug).
2654
- * - It makes your documentation and support much simpler.
2655
- *
2656
- * @param htmlString - The HTML string to parse (must contain `<section>...</section>` elements, not JSON).
2657
- * @returns An object with `components` (array of builder components) and `pageSettings` (global styles for the page).
2658
- */
2659
- public parsePageBuilderHTML(htmlString: string): {
2660
- components: ComponentObject[]
2661
- pageSettings: PageSettings
2662
- } {
2663
- const parser = new DOMParser()
2664
- const doc = parser.parseFromString(htmlString, 'text/html')
2665
-
2666
- // Prefix all classes in the document
2667
- doc.querySelectorAll('[class]').forEach((element) => {
2668
- const currentClasses = element.getAttribute('class') || ''
2669
- const prefixedClasses = this.addTailwindPrefixToClasses(currentClasses)
2670
- element.setAttribute('class', prefixedClasses)
2671
- })
2672
-
2673
- const pagebuilderDiv = doc.querySelector('#pagebuilder')
2674
- let pageSettings: PageSettings = {
2675
- classes: '',
2676
- style: {},
2677
- }
2678
-
2679
- if (pagebuilderDiv) {
2680
- const rawStyle = pagebuilderDiv.getAttribute('style') || ''
2681
- pageSettings = {
2682
- classes: pagebuilderDiv.className || '',
2683
- style: this.parseStyleString(rawStyle),
2684
- }
2685
- }
2686
-
2687
- // Always assign sectionNodes before use
2688
- let sectionNodes: NodeListOf<HTMLElement> = doc.querySelectorAll('section')
2689
- if (pagebuilderDiv) {
2690
- sectionNodes = pagebuilderDiv.querySelectorAll('section')
2691
- }
2692
-
2693
- // Only use top-level (non-nested) sections as components
2694
- const topLevelSections = Array.from(sectionNodes).filter(
2695
- (section) =>
2696
- !section.parentElement || section.parentElement.tagName.toLowerCase() !== 'section',
2697
- )
2698
-
2699
- let components: ComponentObject[] = []
2700
-
2701
- if (topLevelSections.length > 0) {
2702
- components = topLevelSections.map((section) => ({
2703
- id: null,
2704
- html_code: section.outerHTML.trim(),
2705
- title: section.getAttribute('data-component-title') || 'Untitled Component',
2706
- }))
2707
- }
2708
- if (topLevelSections.length === 0) {
2709
- // No <section> found: treat each first-level child as a component, wrapped in a section
2710
- const parent = pagebuilderDiv || doc.body
2711
- const children = Array.from(parent.children)
2712
- if (children.length > 0) {
2713
- components = children.map((child) => {
2714
- // Wrap in a section with data-componentid and data-component-title
2715
- const section = doc.createElement('section')
2716
- section.setAttribute('data-component-title', 'Untitled Component')
2717
- // Optionally: generate a uuid for data-componentid if needed
2718
- // section.setAttribute('data-componentid', uuidv4())
2719
- section.innerHTML = child.outerHTML.trim()
2720
- return {
2721
- id: null,
2722
- html_code: section.outerHTML.trim(),
2723
- title: 'Untitled Component',
2724
- }
2725
- })
2726
- }
2727
- if (children.length === 0) {
2728
- // No children: wrap the entire content in a <section> as a single component
2729
- const section = doc.createElement('section')
2730
- section.setAttribute('data-component-title', 'Untitled Component')
2731
- section.innerHTML = parent.innerHTML.trim()
2732
- components = [
2733
- {
2734
- id: null,
2735
- html_code: section.outerHTML.trim(),
2736
- title: 'Untitled Component',
2737
- },
2738
- ]
2739
- }
2740
- }
2741
-
2742
- return {
2743
- components,
2744
- pageSettings,
2745
- }
2746
- }
2747
-
2748
- /**
2749
- * Applies modified components by mounting them to the DOM and attaching listeners.
2750
- * @param htmlString - The HTML string to apply
2751
- * @returns {Promise<string | null>} - Returns error message if failed, otherwise null
2752
- */
2753
- public async applyModifiedHTML(htmlString: string): Promise<string | null> {
2754
- if (!htmlString || (typeof htmlString === 'string' && htmlString.length === 0)) {
2755
- return 'No HTML content was provided. Please ensure a valid HTML string is passed.'
2756
- }
2757
-
2758
- // Check if the htmlString contains any <section> tags
2759
- if (/<section[\s>]/i.test(htmlString)) {
2760
- return 'Error: The <section> tag cannot be used as it is already included inside this component.'
2761
- }
2762
-
2763
- const tempDiv = document.createElement('div')
2764
- tempDiv.innerHTML = htmlString.trim()
2765
-
2766
- const parsedElement = tempDiv.firstElementChild as HTMLElement | null
2767
-
2768
- if (!parsedElement) {
2769
- return 'Could not parse element from HTML string.'
2770
- }
2771
-
2772
- // Replace the actual DOM element
2773
- const oldElement = this.pageBuilderStateStore.getElement
2774
-
2775
- if (oldElement && oldElement.parentElement) {
2776
- oldElement.replaceWith(parsedElement)
2777
-
2778
- // Update the element in the store (now referencing the new one)
2779
- this.pageBuilderStateStore.setElement(parsedElement)
2780
- }
2781
-
2782
- await this.addListenersToEditableElements()
2783
- await nextTick()
2784
- return null
2785
- }
2786
-
2787
- private validateMountingHTML(
2788
- htmlString: string,
2789
- options?: { logError?: boolean },
2790
- ): string | null {
2791
- // Trim HTML string
2792
- const trimmedData = htmlString.trim()
2793
- const openingSectionMatches = htmlString.match(/<section\b[^>]*>/gi) || []
2794
- const closingSectionMatches = htmlString.match(/<\/section>/gi) || []
2795
-
2796
- if (!htmlString || htmlString.trim().length === 0) {
2797
- const error = 'No HTML content was provided. Please ensure a valid HTML string is passed.'
2798
- if (options && options.logError) {
2799
- console.error(error)
2800
- // Behavior
2801
- return error
2802
- }
2803
- // default behavior
2804
- return error
2805
- }
2806
-
2807
- if (openingSectionMatches.length !== closingSectionMatches.length) {
2808
- const error =
2809
- 'Uneven <section> tags detected in the provided HTML. Each component must be wrapped in its own properly paired <section>...</section>. ' +
2810
- 'Ensure that all <section> tags have a matching closing </section> tag.'
2811
-
2812
- if (options && options.logError) {
2813
- console.error(error)
2814
- return error
2815
- }
2816
-
2817
- return error
2818
- }
2819
-
2820
- const tempDiv = document.createElement('div')
2821
- tempDiv.innerHTML = trimmedData
2822
- const nestedSection = tempDiv.querySelector('section section')
2823
- if (nestedSection) {
2824
- const error =
2825
- 'Nested <section> tags are not allowed. Please ensure that no <section> is placed inside another <section>.'
2826
- if (options && options.logError) {
2827
- console.error(error)
2828
- return error
2829
- }
2830
- return error
2831
- }
2832
-
2833
- // Return error since JSON data has been passed to mount HTML to DOM
2834
- if (trimmedData.startsWith('[') || trimmedData.startsWith('{')) {
2835
- const error =
2836
- 'Brackets [] or curly braces {} are not valid HTML. They are used for data formats like JSON.'
2837
- if (options && options.logError) {
2838
- console.error(error)
2839
- return error
2840
- }
2841
-
2842
- return error
2843
- }
2844
-
2845
- return null
2846
- }
2847
-
2848
- /**
2849
- * Applies modified components by mounting them to the DOM and attaching listeners.
2850
- * @param htmlString - The HTML string to apply
2851
- * @returns {Promise<string | null>} - Returns error message if failed, otherwise null
2852
- */
2853
- public async applyModifiedComponents(htmlString: string): Promise<string | null> {
2854
- // Trim HTML string
2855
- const trimmedData = htmlString.trim()
2856
-
2857
- const openingSectionMatches = htmlString.match(/<section\b[^>]*>/gi) || []
2858
-
2859
- if (openingSectionMatches.length === 0) {
2860
- const error = 'No <section> tags found. Each component must be wrapped in a <section> tag.'
2861
- if (error) {
2862
- return error
2863
- }
2864
- }
2865
-
2866
- const validationError = this.validateMountingHTML(trimmedData)
2867
- if (validationError) return validationError
2868
-
2869
- // also fixed to use `trimmedData`
2870
- await this.mountComponentsToDOM(trimmedData)
2871
- await this.addListenersToEditableElements()
2872
- await nextTick()
2873
- return null
2874
- }
2875
-
2876
- /**
2877
- * Mounts builder components to the DOM from an HTML string.
2878
- *
2879
- * Input format detection:
2880
- * - If the input starts with `[` or `{`: treated as JSON (array or object).
2881
- * - If the input starts with `<`: treated as HTML.
2882
- *
2883
- * This function should be used when:
2884
- * - Restoring the builder from a published HTML snapshot.
2885
- * - Importing a static HTML export.
2886
- * - Loading the builder from previously published or saved HTML (e.g., from `getSavedPageHtml()`).
2887
- *
2888
- * Typical use cases include restoring a published state, importing templates, or previewing published content.
2889
- */
2890
-
2891
- private async mountComponentsToDOM(
2892
- htmlString: string,
2893
- usePassedPageSettings?: boolean,
2894
- pageSettingsFromHistory?: PageSettings,
2895
- ): Promise<void> {
2896
- // Trim HTML string
2897
- const trimmedData = htmlString.trim()
2898
-
2899
- const validationError = this.validateMountingHTML(trimmedData, { logError: true })
2900
- if (validationError) return
2901
-
2902
- // HTML string
2903
- try {
2904
- const parser = new DOMParser()
2905
- const doc = parser.parseFromString(htmlString, 'text/html')
2906
-
2907
- const importedPageBuilder = doc.querySelector('#pagebuilder') as HTMLElement | null
2908
- const livePageBuilder = document.querySelector('#pagebuilder') as HTMLElement | null
2909
-
2910
- // Initialize configPageSettings to null
2911
- let configPageSettings = null
2912
-
2913
- // Use stored page settings if the flag is true
2914
- if (usePassedPageSettings) {
2915
- configPageSettings = this.pageBuilderStateStore.getPageBuilderConfig?.pageSettings || null
2916
- }
2917
-
2918
- // Use imported page builder settings if available and pageSettings is still null
2919
- if (!pageSettingsFromHistory && !configPageSettings && importedPageBuilder) {
2920
- configPageSettings = {
2921
- classes: importedPageBuilder.className || '',
2922
- style: importedPageBuilder.getAttribute('style') || '',
2923
- }
2924
- }
2925
-
2926
- // Fallback to stored page settings if pageSettings is still null
2927
- if (!configPageSettings) {
2928
- configPageSettings = this.pageBuilderStateStore.getPageBuilderConfig?.pageSettings || null
2929
- }
2930
-
2931
- // Apply the page settings to the live page builder
2932
- if (!pageSettingsFromHistory && configPageSettings && livePageBuilder) {
2933
- // Remove existing class and style attributes
2934
- livePageBuilder.removeAttribute('class')
2935
- livePageBuilder.removeAttribute('style')
2936
-
2937
- // Apply new classes and styles
2938
- livePageBuilder.className = configPageSettings.classes || ''
2939
- livePageBuilder.setAttribute(
2940
- 'style',
2941
- this.convertStyleObjectToString(configPageSettings.style),
2942
- )
2943
- }
2944
-
2945
- // Apply the page settings to the live page builder
2946
- if (pageSettingsFromHistory && livePageBuilder) {
2947
- // Remove existing class and style attributes
2948
- livePageBuilder.removeAttribute('class')
2949
- livePageBuilder.removeAttribute('style')
2950
-
2951
- // Apply new classes and styles
2952
- livePageBuilder.className = pageSettingsFromHistory.classes || ''
2953
- livePageBuilder.setAttribute(
2954
- 'style',
2955
- this.convertStyleObjectToString(pageSettingsFromHistory.style),
2956
- )
2957
- }
2958
-
2959
- // Select all <section> elements
2960
- const sectionElements = doc.querySelectorAll('section')
2961
-
2962
- const extractedSections: ComponentObject[] = []
2963
- sectionElements.forEach((section) => {
2964
- // Prefix all classes inside section
2965
- section.querySelectorAll('[class]').forEach((el) => {
2966
- el.setAttribute(
2967
- 'class',
2968
- this.addTailwindPrefixToClasses(el.getAttribute('class') || '', 'pbx-'),
2969
- )
2970
- })
2971
-
2972
- const htmlElement = section as HTMLElement
2973
-
2974
- // Ensure data-componentid exists
2975
- if (!htmlElement.hasAttribute('data-componentid')) {
2976
- htmlElement.setAttribute('data-componentid', uuidv4())
2977
- }
2978
- const componentId = htmlElement.getAttribute('data-componentid')!
2979
-
2980
- // Ensure data-component-title exists
2981
- const title = htmlElement.getAttribute('data-component-title') || 'Untitled Component'
2982
-
2983
- htmlElement.setAttribute('data-component-title', title)
2984
-
2985
- extractedSections.push({
2986
- html_code: htmlElement.outerHTML,
2987
- id: componentId,
2988
- title: title,
2989
- })
2990
- })
2991
-
2992
- this.pageBuilderStateStore.setComponents(extractedSections)
2993
-
2994
- // Clear selections and re-bind events
2995
- await this.clearHtmlSelection()
2996
- await nextTick()
2997
- await this.addListenersToEditableElements()
2998
- } catch (error) {
2999
- console.error('Error parsing HTML components:', error)
3000
- this.deleteAllComponentsFromDOM()
3001
- // Clear selections and re-bind events
3002
- await this.clearHtmlSelection()
3003
- await nextTick()
3004
- await this.addListenersToEditableElements()
3005
- }
3006
- }
3007
- private updateLocalStorageItemName(): void {
3008
- const formtype =
3009
- this.pageBuilderStateStore.getPageBuilderConfig &&
3010
- this.pageBuilderStateStore.getPageBuilderConfig.updateOrCreate &&
3011
- this.pageBuilderStateStore.getPageBuilderConfig.updateOrCreate.formType
3012
-
3013
- const formname =
3014
- this.pageBuilderStateStore.getPageBuilderConfig &&
3015
- this.pageBuilderStateStore.getPageBuilderConfig.updateOrCreate &&
3016
- this.pageBuilderStateStore.getPageBuilderConfig.updateOrCreate.formName
3017
-
3018
- const resourceData =
3019
- this.pageBuilderStateStore.getPageBuilderConfig &&
3020
- this.pageBuilderStateStore.getPageBuilderConfig.resourceData
3021
-
3022
- // Logic for create resource
3023
- if (formtype === 'create') {
3024
- if (formname && formname.length > 0) {
3025
- this.pageBuilderStateStore.setLocalStorageItemName(
3026
- `page-builder-create-resource-${this.sanitizeForLocalStorage(formname)}`,
3027
- )
3028
- return
3029
- }
3030
-
3031
- this.pageBuilderStateStore.setLocalStorageItemName(`page-builder-create-resource`)
3032
- return
3033
- }
3034
-
3035
- // Logic for create
3036
- // Logic for update and with resource form name
3037
- if (formtype === 'update') {
3038
- if (typeof formname === 'string' && formname.length > 0) {
3039
- //
3040
- //
3041
- if (resourceData && resourceData != null && !resourceData.title) {
3042
- // Check if id is missing, null, undefined, or an empty string (after trimming)
3043
- if (!resourceData.id || typeof resourceData.id === 'string') {
3044
- this.pageBuilderStateStore.setLocalStorageItemName(
3045
- `page-builder-update-resource-${this.sanitizeForLocalStorage(formname)}`,
3046
- )
3047
- return
3048
- }
3049
- }
3050
-
3051
- // Runs when resourceData has title but no ID
3052
- if (resourceData && resourceData != null) {
3053
- if (
3054
- resourceData.title &&
3055
- typeof resourceData.title === 'string' &&
3056
- resourceData.title.length > 0
3057
- ) {
3058
- if (!resourceData.id || typeof resourceData.id === 'string') {
3059
- this.pageBuilderStateStore.setLocalStorageItemName(
3060
- `page-builder-update-resource-${this.sanitizeForLocalStorage(formname)}-${this.sanitizeForLocalStorage(resourceData.title)}`,
3061
- )
3062
- return
3063
- }
3064
- }
3065
- }
3066
-
3067
- // Runs when resourceData has ID but no title
3068
- if (resourceData && resourceData != null) {
3069
- if (!resourceData.title && typeof resourceData.title !== 'string') {
3070
- if (resourceData.id || typeof resourceData.id === 'number') {
3071
- this.pageBuilderStateStore.setLocalStorageItemName(
3072
- `page-builder-update-resource-${this.sanitizeForLocalStorage(formname)}-${this.sanitizeForLocalStorage(String(resourceData.id))}`,
3073
- )
3074
- return
3075
- }
3076
- }
3077
- }
3078
-
3079
- // Runs when resourceData has both title and ID
3080
- if (resourceData && resourceData != null) {
3081
- if (
3082
- resourceData.title &&
3083
- typeof resourceData.title === 'string' &&
3084
- resourceData.title.length > 0
3085
- ) {
3086
- if (resourceData.id || typeof resourceData.id === 'number') {
3087
- this.pageBuilderStateStore.setLocalStorageItemName(
3088
- `page-builder-update-resource-${this.sanitizeForLocalStorage(formname)}-${this.sanitizeForLocalStorage(resourceData.title)}-${this.sanitizeForLocalStorage(String(resourceData.id))}`,
3089
- )
3090
- return
3091
- }
3092
- }
3093
- }
3094
- }
3095
-
3096
- // Logic for update without without formname
3097
- if (!formname || (typeof formname === 'string' && formname.length === 0)) {
3098
- //
3099
- //
3100
- if (resourceData && resourceData != null && !resourceData.title) {
3101
- // Check if id is missing, null, undefined, or an empty string (after trimming)
3102
- if (!resourceData.id || typeof resourceData.id === 'string') {
3103
- this.pageBuilderStateStore.setLocalStorageItemName(`page-builder-update-resource`)
3104
- return
3105
- }
3106
- }
3107
-
3108
- // Runs when resourceData has title but no ID
3109
- if (resourceData && resourceData != null) {
3110
- if (
3111
- resourceData.title &&
3112
- typeof resourceData.title === 'string' &&
3113
- resourceData.title.length > 0
3114
- ) {
3115
- if (!resourceData.id || typeof resourceData.id === 'string') {
3116
- this.pageBuilderStateStore.setLocalStorageItemName(
3117
- `page-builder-update-resource-${this.sanitizeForLocalStorage(resourceData.title)}`,
3118
- )
3119
- return
3120
- }
3121
- }
3122
- }
3123
-
3124
- // Runs when resourceData has ID but no title
3125
- if (resourceData && resourceData != null) {
3126
- if (!resourceData.title && typeof resourceData.title !== 'string') {
3127
- if (resourceData.id || typeof resourceData.id === 'number') {
3128
- this.pageBuilderStateStore.setLocalStorageItemName(
3129
- `page-builder-update-resource-${this.sanitizeForLocalStorage(String(resourceData.id))}`,
3130
- )
3131
- return
3132
- }
3133
- }
3134
- }
3135
-
3136
- // Runs when resourceData has both title and ID
3137
- if (resourceData && resourceData != null) {
3138
- if (
3139
- resourceData.title &&
3140
- typeof resourceData.title === 'string' &&
3141
- resourceData.title.length > 0
3142
- ) {
3143
- if (resourceData.id || typeof resourceData.id === 'number') {
3144
- this.pageBuilderStateStore.setLocalStorageItemName(
3145
- `page-builder-update-resource-${this.sanitizeForLocalStorage(resourceData.title)}-${this.sanitizeForLocalStorage(String(resourceData.id))}`,
3146
- )
3147
- return
3148
- }
3149
- }
3150
- }
3151
- }
3152
- }
3153
- }
3154
-
3155
- /**
3156
- * Initializes the styles for the currently selected element.
3157
- */
3158
- public async initializeElementStyles(): Promise<void> {
3159
- // Wait for Vue to finish DOM updates before attaching event listeners.
3160
- // This ensures elements exist in the DOM.
3161
- await nextTick()
3162
- this.setBasePrimaryImageFromSelectedElement()
3163
- this.handleHyperlink(undefined, null, false)
3164
- this.handleOpacity(undefined)
3165
- this.handleBackgroundOpacity(undefined)
3166
- this.handleBackgroundColor(undefined)
3167
- this.handleTextColor(undefined)
3168
- this.handleBorderStyle(undefined)
3169
- this.handleBorderWidth(undefined)
3170
- this.handleBorderColor(undefined)
3171
- this.handleBorderRadiusGlobal(undefined)
3172
- this.handleBorderRadiusTopLeft(undefined)
3173
- this.handleBorderRadiusTopRight(undefined)
3174
- this.handleBorderRadiusBottomleft(undefined)
3175
- this.handleBorderRadiusBottomRight(undefined)
3176
- this.handleFontSizeBase(undefined)
3177
- this.handleFontSizeDesktop(undefined)
3178
- this.handleFontSizeTablet(undefined)
3179
- this.handleFontSizeMobile(undefined)
3180
- this.handleFontWeight(undefined)
3181
- this.handleFontFamily(undefined)
3182
- this.handleFontStyle(undefined)
3183
- this.handleVerticalPadding(undefined)
3184
- this.handleHorizontalPadding(undefined)
3185
- this.handleVerticalMargin(undefined)
3186
- this.handleHorizontalMargin(undefined)
3187
-
3188
- await this.syncCurrentClasses()
3189
- await this.syncCurrentStyles()
3190
- }
3191
- }