@dotcms/uve 0.0.1-beta.2 → 0.0.1-beta.21

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 (40) hide show
  1. package/README.md +321 -4
  2. package/index.cjs.d.ts +1 -1
  3. package/index.cjs.js +10 -54
  4. package/index.cjs2.js +1151 -0
  5. package/index.esm.d.ts +1 -1
  6. package/index.esm.js +2 -56
  7. package/index.esm2.js +1119 -0
  8. package/internal.cjs.d.ts +1 -0
  9. package/internal.cjs.default.js +1 -0
  10. package/internal.cjs.js +39 -0
  11. package/internal.cjs.mjs +2 -0
  12. package/internal.esm.d.ts +1 -0
  13. package/internal.esm.js +2 -0
  14. package/package.json +26 -7
  15. package/src/index.d.ts +2 -0
  16. package/src/internal/constants.d.ts +76 -0
  17. package/src/internal/events.d.ts +66 -0
  18. package/src/internal/index.d.ts +1 -0
  19. package/src/internal.d.ts +6 -0
  20. package/src/lib/{utils.d.ts → core/core.utils.d.ts} +20 -1
  21. package/src/lib/dom/dom.utils.d.ts +206 -0
  22. package/src/lib/editor/internal.d.ts +23 -0
  23. package/src/lib/editor/public.d.ts +62 -0
  24. package/src/lib/types/block-editor-renderer/internal.d.ts +46 -0
  25. package/src/lib/types/block-editor-renderer/public.d.ts +38 -0
  26. package/src/lib/types/editor/internal.d.ts +119 -0
  27. package/src/lib/types/editor/public.d.ts +271 -0
  28. package/src/lib/types/events/internal.d.ts +34 -0
  29. package/src/lib/types/events/public.d.ts +18 -0
  30. package/src/lib/types/page/public.d.ts +485 -0
  31. package/src/script/sdk-editor.d.ts +6 -0
  32. package/src/script/utils.d.ts +53 -0
  33. package/src/types.d.ts +4 -0
  34. package/types.cjs.d.ts +1 -1
  35. package/types.cjs.js +97 -4
  36. package/types.esm.d.ts +1 -1
  37. package/types.esm.js +98 -5
  38. package/src/lib/types.d.ts +0 -33
  39. package/src/public/index.d.ts +0 -2
  40. package/src/public/types.d.ts +0 -2
package/index.esm2.js ADDED
@@ -0,0 +1,1119 @@
1
+ import { UVEEventType, DotCMSUVEAction, UVE_MODE } from './types.esm.js';
2
+
3
+ /**
4
+ * Actions received from the dotcms editor
5
+ *
6
+ * @export
7
+ * @enum {number}
8
+ */
9
+ var __DOTCMS_UVE_EVENT__;
10
+ (function (__DOTCMS_UVE_EVENT__) {
11
+ /**
12
+ * Request to page to reload
13
+ */
14
+ __DOTCMS_UVE_EVENT__["UVE_RELOAD_PAGE"] = "uve-reload-page";
15
+ /**
16
+ * Request the bounds for the elements
17
+ */
18
+ __DOTCMS_UVE_EVENT__["UVE_REQUEST_BOUNDS"] = "uve-request-bounds";
19
+ /**
20
+ * Received pong from the editor
21
+ */
22
+ __DOTCMS_UVE_EVENT__["UVE_EDITOR_PONG"] = "uve-editor-pong";
23
+ /**
24
+ * Received scroll event trigger from the editor
25
+ */
26
+ __DOTCMS_UVE_EVENT__["UVE_SCROLL_INSIDE_IFRAME"] = "uve-scroll-inside-iframe";
27
+ /**
28
+ * TODO:
29
+ * Set the page data - This is used to catch the "changes" event.
30
+ * We must to re-check the name late.
31
+ */
32
+ __DOTCMS_UVE_EVENT__["UVE_SET_PAGE_DATA"] = "uve-set-page-data";
33
+ /**
34
+ * Copy contentlet inline editing success
35
+ */
36
+ __DOTCMS_UVE_EVENT__["UVE_COPY_CONTENTLET_INLINE_EDITING_SUCCESS"] = "uve-copy-contentlet-inline-editing-success";
37
+ })(__DOTCMS_UVE_EVENT__ || (__DOTCMS_UVE_EVENT__ = {}));
38
+
39
+ /**
40
+ * Calculates the bounding information for each page element within the given containers.
41
+ *
42
+ * @export
43
+ * @param {HTMLDivElement[]} containers - An array of HTMLDivElement representing the containers.
44
+ * @return {DotCMSContainerBound[]} An array of objects containing the bounding information for each page element.
45
+ * @example
46
+ * ```ts
47
+ * const containers = document.querySelectorAll('.container');
48
+ * const bounds = getDotCMSPageBounds(containers);
49
+ * console.log(bounds);
50
+ * ```
51
+ */
52
+ function getDotCMSPageBounds(containers) {
53
+ return containers.map(container => {
54
+ const containerRect = container.getBoundingClientRect();
55
+ const contentlets = Array.from(container.querySelectorAll('[data-dot-object="contentlet"]'));
56
+ return {
57
+ x: containerRect.x,
58
+ y: containerRect.y,
59
+ width: containerRect.width,
60
+ height: containerRect.height,
61
+ payload: JSON.stringify({
62
+ container: getDotCMSContainerData(container)
63
+ }),
64
+ contentlets: getDotCMSContentletsBound(containerRect, contentlets)
65
+ };
66
+ });
67
+ }
68
+ /**
69
+ * Calculates the bounding information for each contentlet inside a container.
70
+ *
71
+ * @export
72
+ * @param {DOMRect} containerRect - The bounding rectangle of the container.
73
+ * @param {HTMLDivElement[]} contentlets - An array of HTMLDivElement representing the contentlets.
74
+ * @return {DotCMSContentletBound[]} An array of objects containing the bounding information for each contentlet.
75
+ * @example
76
+ * ```ts
77
+ * const containerRect = container.getBoundingClientRect();
78
+ * const contentlets = container.querySelectorAll('.contentlet');
79
+ * const bounds = getDotCMSContentletsBound(containerRect, contentlets);
80
+ * console.log(bounds); // Element bounds within the container
81
+ * ```
82
+ */
83
+ function getDotCMSContentletsBound(containerRect, contentlets) {
84
+ return contentlets.map(contentlet => {
85
+ const contentletRect = contentlet.getBoundingClientRect();
86
+ return {
87
+ x: 0,
88
+ y: contentletRect.y - containerRect.y,
89
+ width: contentletRect.width,
90
+ height: contentletRect.height,
91
+ payload: JSON.stringify({
92
+ container: contentlet.dataset?.['dotContainer'] ? JSON.parse(contentlet.dataset?.['dotContainer']) : getClosestDotCMSContainerData(contentlet),
93
+ contentlet: {
94
+ identifier: contentlet.dataset?.['dotIdentifier'],
95
+ title: contentlet.dataset?.['dotTitle'],
96
+ inode: contentlet.dataset?.['dotInode'],
97
+ contentType: contentlet.dataset?.['dotType']
98
+ }
99
+ })
100
+ };
101
+ });
102
+ }
103
+ /**
104
+ * Get container data from VTLS.
105
+ *
106
+ * @export
107
+ * @param {HTMLElement} container - The container element.
108
+ * @return {object} An object containing the container data.
109
+ * @example
110
+ * ```ts
111
+ * const container = document.querySelector('.container');
112
+ * const data = getContainerData(container);
113
+ * console.log(data);
114
+ * ```
115
+ */
116
+ function getDotCMSContainerData(container) {
117
+ return {
118
+ acceptTypes: container.dataset?.['dotAcceptTypes'] || '',
119
+ identifier: container.dataset?.['dotIdentifier'] || '',
120
+ maxContentlets: container.dataset?.['maxContentlets'] || '',
121
+ uuid: container.dataset?.['dotUuid'] || ''
122
+ };
123
+ }
124
+ /**
125
+ * Get the closest container data from the contentlet.
126
+ *
127
+ * @export
128
+ * @param {Element} element - The contentlet element.
129
+ * @return {object | null} An object containing the closest container data or null if no container is found.
130
+ * @example
131
+ * ```ts
132
+ * const contentlet = document.querySelector('.contentlet');
133
+ * const data = getClosestDotCMSContainerData(contentlet);
134
+ * console.log(data);
135
+ * ```
136
+ */
137
+ function getClosestDotCMSContainerData(element) {
138
+ // Find the closest ancestor element with data-dot-object="container" attribute
139
+ const container = element.closest('[data-dot-object="container"]');
140
+ // If a container element is found
141
+ if (container) {
142
+ // Return the dataset of the container element
143
+ return getDotCMSContainerData(container);
144
+ } else {
145
+ // If no container element is found, return null
146
+ console.warn('No container found for the contentlet');
147
+ return null;
148
+ }
149
+ }
150
+ /**
151
+ * Find the closest contentlet element based on HTMLElement.
152
+ *
153
+ * @export
154
+ * @param {HTMLElement | null} element - The starting element.
155
+ * @return {HTMLElement | null} The closest contentlet element or null if not found.
156
+ * @example
157
+ * const element = document.querySelector('.some-element');
158
+ * const contentlet = findDotCMSElement(element);
159
+ * console.log(contentlet);
160
+ */
161
+ function findDotCMSElement(element) {
162
+ if (!element) return null;
163
+ if (element?.dataset?.['dotObject'] === 'contentlet' || element?.dataset?.['dotObject'] === 'container' && element.children.length === 0) {
164
+ return element;
165
+ }
166
+ return findDotCMSElement(element?.['parentElement']);
167
+ }
168
+ /**
169
+ * Find VTL data within a target element.
170
+ *
171
+ * @export
172
+ * @param {HTMLElement} target - The target element to search within.
173
+ * @return {Array<{ inode: string, name: string }> | null} An array of objects containing VTL data or null if none found.
174
+ * @example
175
+ * ```ts
176
+ * const target = document.querySelector('.target-element');
177
+ * const vtlData = findDotCMSVTLData(target);
178
+ * console.log(vtlData);
179
+ * ```
180
+ */
181
+ function findDotCMSVTLData(target) {
182
+ const vltElements = target.querySelectorAll('[data-dot-object="vtl-file"]');
183
+ if (!vltElements.length) {
184
+ return null;
185
+ }
186
+ return Array.from(vltElements).map(vltElement => {
187
+ return {
188
+ inode: vltElement.dataset?.['dotInode'],
189
+ name: vltElement.dataset?.['dotUrl']
190
+ };
191
+ });
192
+ }
193
+ /**
194
+ * Check if the scroll position is at the bottom of the page.
195
+ *
196
+ * @export
197
+ * @return {boolean} True if the scroll position is at the bottom, otherwise false.
198
+ * @example
199
+ * ```ts
200
+ * if (dotCMSScrollIsInBottom()) {
201
+ * console.log('Scrolled to the bottom');
202
+ * }
203
+ * ```
204
+ */
205
+ function computeScrollIsInBottom() {
206
+ const documentHeight = document.documentElement.scrollHeight;
207
+ const viewportHeight = window.innerHeight;
208
+ const scrollY = window.scrollY;
209
+ return scrollY + viewportHeight >= documentHeight;
210
+ }
211
+ /**
212
+ *
213
+ *
214
+ * Combine classes into a single string.
215
+ *
216
+ * @param {string[]} classes
217
+ * @returns {string} Combined classes
218
+ */
219
+ const combineClasses = classes => classes.filter(Boolean).join(' ');
220
+ /**
221
+ *
222
+ *
223
+ * Calculates and returns the CSS Grid positioning classes for a column based on its configuration.
224
+ * Uses a 12-column grid system where columns are positioned using grid-column-start and grid-column-end.
225
+ *
226
+ * @example
227
+ * ```typescript
228
+ * const classes = getColumnPositionClasses({
229
+ * leftOffset: 1, // Starts at the first column
230
+ * width: 6 // Spans 6 columns
231
+ * });
232
+ * // Returns: { startClass: 'col-start-1', endClass: 'col-end-7' }
233
+ * ```
234
+ *
235
+ * @param {DotPageAssetLayoutColumn} column - Column configuration object
236
+ * @param {number} column.leftOffset - Starting position (0-based) in the grid
237
+ * @param {number} column.width - Number of columns to span
238
+ * @returns {{ startClass: string, endClass: string }} Object containing CSS class names for grid positioning
239
+ */
240
+ const getColumnPositionClasses = column => {
241
+ const {
242
+ leftOffset,
243
+ width
244
+ } = column;
245
+ const startClass = `${START_CLASS}${leftOffset}`;
246
+ const endClass = `${END_CLASS}${leftOffset + width}`;
247
+ return {
248
+ startClass,
249
+ endClass
250
+ };
251
+ };
252
+ /**
253
+ *
254
+ *
255
+ * Helper function that returns an object containing the dotCMS data attributes.
256
+ * @param {DotCMSContentlet} contentlet - The contentlet to get the attributes for
257
+ * @param {string} container - The container to get the attributes for
258
+ * @returns {DotContentletAttributes} The dotCMS data attributes
259
+ */
260
+ function getDotContentletAttributes(contentlet, container) {
261
+ return {
262
+ 'data-dot-identifier': contentlet?.identifier,
263
+ 'data-dot-basetype': contentlet?.baseType,
264
+ 'data-dot-title': contentlet?.['widgetTitle'] || contentlet?.title,
265
+ 'data-dot-inode': contentlet?.inode,
266
+ 'data-dot-type': contentlet?.contentType,
267
+ 'data-dot-container': container,
268
+ 'data-dot-on-number-of-pages': contentlet?.['onNumberOfPages']
269
+ };
270
+ }
271
+ /**
272
+ *
273
+ *
274
+ * Retrieves container data from a DotCMS page asset using the container reference.
275
+ * This function processes the container information and returns a standardized format
276
+ * for container editing.
277
+ *
278
+ * @param {DotCMSPageAsset} dotCMSPageAsset - The page asset containing all containers data
279
+ * @param {DotCMSColumnContainer} columContainer - The container reference from the layout
280
+ * @throws {Error} When page asset is invalid or container is not found
281
+ * @returns {EditableContainerData} Formatted container data for editing
282
+ *
283
+ * @example
284
+ * const containerData = getContainersData(pageAsset, containerRef);
285
+ * // Returns: { uuid: '123', identifier: 'cont1', acceptTypes: 'type1,type2', maxContentlets: 5 }
286
+ */
287
+ const getContainersData = (dotCMSPageAsset, columContainer) => {
288
+ const {
289
+ identifier,
290
+ uuid
291
+ } = columContainer;
292
+ const dotContainer = dotCMSPageAsset.containers[identifier];
293
+ if (!dotContainer) {
294
+ return null;
295
+ }
296
+ const {
297
+ containerStructures,
298
+ container
299
+ } = dotContainer;
300
+ const acceptTypes = containerStructures?.map(structure => structure.contentTypeVar).join(',') ?? '';
301
+ const variantId = container?.parentPermissionable?.variantId;
302
+ const maxContentlets = container?.maxContentlets ?? 0;
303
+ const path = container?.path;
304
+ return {
305
+ uuid,
306
+ variantId,
307
+ acceptTypes,
308
+ maxContentlets,
309
+ identifier: path ?? identifier
310
+ };
311
+ };
312
+ /**
313
+ *
314
+ *
315
+ * Retrieves the contentlets (content items) associated with a specific container.
316
+ * Handles different UUID formats and provides warning for missing contentlets.
317
+ *
318
+ * @param {DotCMSPageAsset} dotCMSPageAsset - The page asset containing all containers data
319
+ * @param {DotCMSColumnContainer} columContainer - The container reference from the layout
320
+ * @returns {DotCMSContentlet[]} Array of contentlets in the container
321
+ *
322
+ * @example
323
+ * const contentlets = getContentletsInContainer(pageAsset, containerRef);
324
+ * // Returns: [{ identifier: 'cont1', ... }, { identifier: 'cont2', ... }]
325
+ */
326
+ const getContentletsInContainer = (dotCMSPageAsset, columContainer) => {
327
+ const {
328
+ identifier,
329
+ uuid
330
+ } = columContainer;
331
+ const {
332
+ contentlets
333
+ } = dotCMSPageAsset.containers[identifier];
334
+ const contentletsInContainer = contentlets[`uuid-${uuid}`] || contentlets[`uuid-dotParser_${uuid}`] || [];
335
+ if (!contentletsInContainer) {
336
+ console.warn(`We couldn't find the contentlets for the container with the identifier ${identifier} and the uuid ${uuid} becareful by adding content to this container.\nWe recommend to change the container in the layout and add the content again.`);
337
+ }
338
+ return contentletsInContainer;
339
+ };
340
+ /**
341
+ *
342
+ *
343
+ * Generates the required DotCMS data attributes for a container element.
344
+ * These attributes are used by DotCMS for container identification and functionality.
345
+ *
346
+ * @param {EditableContainerData} params - Container data including uuid, identifier, acceptTypes, and maxContentlets
347
+ * @returns {DotContainerAttributes} Object containing all necessary data attributes
348
+ *
349
+ * @example
350
+ * const attributes = getDotContainerAttributes({
351
+ * uuid: '123',
352
+ * identifier: 'cont1',
353
+ * acceptTypes: 'type1,type2',
354
+ * maxContentlets: 5
355
+ * });
356
+ * // Returns: { 'data-dot-object': 'container', 'data-dot-identifier': 'cont1', ... }
357
+ */
358
+ function getDotContainerAttributes({
359
+ uuid,
360
+ identifier,
361
+ acceptTypes,
362
+ maxContentlets
363
+ }) {
364
+ return {
365
+ 'data-dot-object': 'container',
366
+ 'data-dot-accept-types': acceptTypes,
367
+ 'data-dot-identifier': identifier,
368
+ 'data-max-contentlets': maxContentlets.toString(),
369
+ 'data-dot-uuid': uuid
370
+ };
371
+ }
372
+
373
+ /* eslint-disable @typescript-eslint/no-explicit-any */
374
+ /**
375
+ * Sets up scroll event handlers for the window to notify the editor about scroll events.
376
+ * Adds listeners for both 'scroll' and 'scrollend' events, sending appropriate messages
377
+ * to the editor when these events occur.
378
+ */
379
+ function scrollHandler() {
380
+ const scrollCallback = () => {
381
+ sendMessageToUVE({
382
+ action: DotCMSUVEAction.IFRAME_SCROLL
383
+ });
384
+ };
385
+ const scrollEndCallback = () => {
386
+ sendMessageToUVE({
387
+ action: DotCMSUVEAction.IFRAME_SCROLL_END
388
+ });
389
+ };
390
+ window.addEventListener('scroll', scrollCallback);
391
+ window.addEventListener('scrollend', scrollEndCallback);
392
+ return {
393
+ destroyScrollHandler: () => {
394
+ window.removeEventListener('scroll', scrollCallback);
395
+ window.removeEventListener('scrollend', scrollEndCallback);
396
+ }
397
+ };
398
+ }
399
+ /**
400
+ * Adds 'empty-contentlet' class to contentlet elements that have no height.
401
+ * This helps identify and style empty contentlets in the editor view.
402
+ *
403
+ * @remarks
404
+ * The function queries all elements with data-dot-object="contentlet" attribute
405
+ * and checks their clientHeight. If an element has no height (clientHeight = 0),
406
+ * it adds the 'empty-contentlet' class to that element.
407
+ */
408
+ function addClassToEmptyContentlets() {
409
+ const contentlets = document.querySelectorAll('[data-dot-object="contentlet"]');
410
+ contentlets.forEach(contentlet => {
411
+ if (contentlet.clientHeight) {
412
+ return;
413
+ }
414
+ contentlet.classList.add('empty-contentlet');
415
+ });
416
+ }
417
+ /**
418
+ * Registers event handlers for various UVE (Universal Visual Editor) events.
419
+ *
420
+ * This function sets up subscriptions for:
421
+ * - Page reload events that refresh the window
422
+ * - Bounds request events to update editor boundaries
423
+ * - Iframe scroll events to handle smooth scrolling within bounds
424
+ * - Contentlet hover events to notify the editor
425
+ *
426
+ * @remarks
427
+ * For scroll events, the function includes logic to prevent scrolling beyond
428
+ * the top or bottom boundaries of the iframe, which helps maintain proper
429
+ * scroll event handling.
430
+ */
431
+ function registerUVEEvents() {
432
+ const pageReloadSubscription = createUVESubscription(UVEEventType.PAGE_RELOAD, () => {
433
+ window.location.reload();
434
+ });
435
+ const requestBoundsSubscription = createUVESubscription(UVEEventType.REQUEST_BOUNDS, bounds => {
436
+ setBounds(bounds);
437
+ });
438
+ const iframeScrollSubscription = createUVESubscription(UVEEventType.IFRAME_SCROLL, direction => {
439
+ if (window.scrollY === 0 && direction === 'up' || computeScrollIsInBottom() && direction === 'down') {
440
+ // If the iframe scroll is at the top or bottom, do not send anything.
441
+ // This avoids losing the scrollend event.
442
+ return;
443
+ }
444
+ const scrollY = direction === 'up' ? -120 : 120;
445
+ window.scrollBy({
446
+ left: 0,
447
+ top: scrollY,
448
+ behavior: 'smooth'
449
+ });
450
+ });
451
+ const contentletHoveredSubscription = createUVESubscription(UVEEventType.CONTENTLET_HOVERED, contentletHovered => {
452
+ sendMessageToUVE({
453
+ action: DotCMSUVEAction.SET_CONTENTLET,
454
+ payload: contentletHovered
455
+ });
456
+ });
457
+ return {
458
+ subscriptions: [pageReloadSubscription, requestBoundsSubscription, iframeScrollSubscription, contentletHoveredSubscription]
459
+ };
460
+ }
461
+ /**
462
+ * Notifies the editor that the UVE client is ready to receive messages.
463
+ *
464
+ * This function sends a message to the editor indicating that the client-side
465
+ * initialization is complete and it's ready to handle editor interactions.
466
+ *
467
+ * @remarks
468
+ * This is typically called after all UVE event handlers and DOM listeners
469
+ * have been set up successfully.
470
+ */
471
+ function setClientIsReady(config) {
472
+ sendMessageToUVE({
473
+ action: DotCMSUVEAction.CLIENT_READY,
474
+ payload: config
475
+ });
476
+ }
477
+ /**
478
+ * Listen for block editor inline event.
479
+ */
480
+ function listenBlockEditorInlineEvent() {
481
+ if (document.readyState === 'complete') {
482
+ // The page is fully loaded or interactive
483
+ listenBlockEditorClick();
484
+ return {
485
+ destroyListenBlockEditorInlineEvent: () => {
486
+ window.removeEventListener('load', () => listenBlockEditorClick());
487
+ }
488
+ };
489
+ }
490
+ window.addEventListener('load', () => listenBlockEditorClick());
491
+ return {
492
+ destroyListenBlockEditorInlineEvent: () => {
493
+ window.removeEventListener('load', () => listenBlockEditorClick());
494
+ }
495
+ };
496
+ }
497
+ const listenBlockEditorClick = () => {
498
+ const editBlockEditorNodes = document.querySelectorAll('[data-block-editor-content]');
499
+ if (!editBlockEditorNodes.length) {
500
+ return;
501
+ }
502
+ editBlockEditorNodes.forEach(node => {
503
+ const {
504
+ inode,
505
+ language = '1',
506
+ contentType,
507
+ fieldName,
508
+ blockEditorContent
509
+ } = node.dataset;
510
+ const content = JSON.parse(blockEditorContent || '');
511
+ if (!inode || !language || !contentType || !fieldName) {
512
+ console.error('Missing data attributes for block editor inline editing.');
513
+ console.warn('inode, language, contentType and fieldName are required.');
514
+ return;
515
+ }
516
+ node.classList.add('dotcms__inline-edit-field');
517
+ node.addEventListener('click', () => {
518
+ initInlineEditing('BLOCK_EDITOR', {
519
+ inode,
520
+ content,
521
+ language: parseInt(language),
522
+ fieldName,
523
+ contentType
524
+ });
525
+ });
526
+ });
527
+ };
528
+
529
+ /**
530
+ * Post message to dotcms page editor
531
+ *
532
+ * @export
533
+ * @template T
534
+ * @param {DotCMSUVEMessage<T>} message
535
+ */
536
+ function sendMessageToUVE(message) {
537
+ window.parent.postMessage(message, '*');
538
+ }
539
+ /**
540
+ * You can use this function to edit a contentlet in the editor.
541
+ *
542
+ * Calling this function inside the editor, will prompt the UVE to open a dialog to edit the contentlet.
543
+ *
544
+ * @export
545
+ * @template T
546
+ * @param {Contentlet<T>} contentlet - The contentlet to edit.
547
+ */
548
+ function editContentlet(contentlet) {
549
+ sendMessageToUVE({
550
+ action: DotCMSUVEAction.EDIT_CONTENTLET,
551
+ payload: contentlet
552
+ });
553
+ }
554
+ /*
555
+ * Reorders the menu based on the provided configuration.
556
+ *
557
+ * @param {ReorderMenuConfig} [config] - Optional configuration for reordering the menu.
558
+ * @param {number} [config.startLevel=1] - The starting level of the menu to reorder.
559
+ * @param {number} [config.depth=2] - The depth of the menu to reorder.
560
+ *
561
+ * This function constructs a URL for the reorder menu page with the specified
562
+ * start level and depth, and sends a message to the editor to perform the reorder action.
563
+ */
564
+ function reorderMenu(config) {
565
+ const {
566
+ startLevel = 1,
567
+ depth = 2
568
+ } = config || {};
569
+ sendMessageToUVE({
570
+ action: DotCMSUVEAction.REORDER_MENU,
571
+ payload: {
572
+ startLevel,
573
+ depth
574
+ }
575
+ });
576
+ }
577
+ /**
578
+ * Initializes the inline editing in the editor.
579
+ *
580
+ * @export
581
+ * @param {INLINE_EDITING_EVENT_KEY} type
582
+ * @param {InlineEditEventData} eventData
583
+ * @return {*}
584
+ *
585
+ * * @example
586
+ * ```html
587
+ * <div onclick="initInlineEditing('BLOCK_EDITOR', { inode, languageId, contentType, fieldName, content })">
588
+ * ${My Content}
589
+ * </div>
590
+ * ```
591
+ */
592
+ function initInlineEditing(type, data) {
593
+ sendMessageToUVE({
594
+ action: DotCMSUVEAction.INIT_INLINE_EDITING,
595
+ payload: {
596
+ type,
597
+ data
598
+ }
599
+ });
600
+ }
601
+ /**
602
+ * Initializes the Universal Visual Editor (UVE) with required handlers and event listeners.
603
+ *
604
+ * This function sets up:
605
+ * - Scroll handling
606
+ * - Empty contentlet styling
607
+ * - Block editor inline event listening
608
+ * - Client ready state
609
+ * - UVE event subscriptions
610
+ *
611
+ * @returns {Object} An object containing the cleanup function
612
+ * @returns {Function} destroyUVESubscriptions - Function to clean up all UVE event subscriptions
613
+ *
614
+ * @example
615
+ * ```typescript
616
+ * const { destroyUVESubscriptions } = initUVE();
617
+ *
618
+ * // When done with UVE
619
+ * destroyUVESubscriptions();
620
+ * ```
621
+ */
622
+ function initUVE(config = {}) {
623
+ addClassToEmptyContentlets();
624
+ setClientIsReady(config);
625
+ const {
626
+ subscriptions
627
+ } = registerUVEEvents();
628
+ const {
629
+ destroyScrollHandler
630
+ } = scrollHandler();
631
+ const {
632
+ destroyListenBlockEditorInlineEvent
633
+ } = listenBlockEditorInlineEvent();
634
+ return {
635
+ destroyUVESubscriptions: () => {
636
+ subscriptions.forEach(subscription => subscription.unsubscribe());
637
+ destroyScrollHandler();
638
+ destroyListenBlockEditorInlineEvent();
639
+ }
640
+ };
641
+ }
642
+
643
+ /**
644
+ * Sets the bounds of the containers in the editor.
645
+ * Retrieves the containers from the DOM and sends their position data to the editor.
646
+ * @private
647
+ * @memberof DotCMSPageEditor
648
+ */
649
+ function setBounds(bounds) {
650
+ sendMessageToUVE({
651
+ action: DotCMSUVEAction.SET_BOUNDS,
652
+ payload: bounds
653
+ });
654
+ }
655
+ /**
656
+ * Validates the structure of a Block Editor block.
657
+ *
658
+ * This function checks that:
659
+ * 1. The blocks parameter is a valid object
660
+ * 2. The block has a 'doc' type
661
+ * 3. The block has a valid content array that is not empty
662
+ *
663
+ * @param {Block} blocks - The blocks structure to validate
664
+ * @returns {BlockEditorState} Object containing validation state and any error message
665
+ * @property {boolean} BlockEditorState.isValid - Whether the blocks structure is valid
666
+ * @property {string | null} BlockEditorState.error - Error message if invalid, null if valid
667
+ */
668
+ const isValidBlocks = blocks => {
669
+ if (!blocks) {
670
+ return {
671
+ error: `Error: Blocks object is not defined`
672
+ };
673
+ }
674
+ if (typeof blocks !== 'object') {
675
+ return {
676
+ error: `Error: Blocks must be an object, but received: ${typeof blocks}`
677
+ };
678
+ }
679
+ if (blocks.type !== 'doc') {
680
+ return {
681
+ error: `Error: Invalid block type. Expected 'doc' but received: '${blocks.type}'`
682
+ };
683
+ }
684
+ if (!blocks.content) {
685
+ return {
686
+ error: 'Error: Blocks content is missing'
687
+ };
688
+ }
689
+ if (!Array.isArray(blocks.content)) {
690
+ return {
691
+ error: `Error: Blocks content must be an array, but received: ${typeof blocks.content}`
692
+ };
693
+ }
694
+ if (blocks.content.length === 0) {
695
+ return {
696
+ error: 'Error: Blocks content is empty. At least one block is required.'
697
+ };
698
+ }
699
+ // Validate each block in the content array
700
+ for (let i = 0; i < blocks.content.length; i++) {
701
+ const block = blocks.content[i];
702
+ if (!block.type) {
703
+ return {
704
+ error: `Error: Block at index ${i} is missing required 'type' property`
705
+ };
706
+ }
707
+ if (typeof block.type !== 'string') {
708
+ return {
709
+ error: `Error: Block type at index ${i} must be a string, but received: ${typeof block.type}`
710
+ };
711
+ }
712
+ // Validate block attributes if present
713
+ if (block.attrs && typeof block.attrs !== 'object') {
714
+ return {
715
+ error: `Error: Block attributes at index ${i} must be an object, but received: ${typeof block.attrs}`
716
+ };
717
+ }
718
+ // Validate nested content if present
719
+ if (block.content) {
720
+ if (!Array.isArray(block.content)) {
721
+ return {
722
+ error: `Error: Block content at index ${i} must be an array, but received: ${typeof block.content}`
723
+ };
724
+ }
725
+ // Recursively validate nested blocks
726
+ const nestedValidation = isValidBlocks({
727
+ type: 'doc',
728
+ content: block.content
729
+ });
730
+ if (nestedValidation.error) {
731
+ return {
732
+ error: `Error in nested block at index ${i}: ${nestedValidation.error}`
733
+ };
734
+ }
735
+ }
736
+ }
737
+ return {
738
+ error: null
739
+ };
740
+ };
741
+
742
+ /**
743
+ * Enum representing the different types of blocks available in the Block Editor
744
+ *
745
+ * @export
746
+ * @enum {string}
747
+ */
748
+ var Blocks;
749
+ (function (Blocks) {
750
+ /** Represents a paragraph block */
751
+ Blocks["PARAGRAPH"] = "paragraph";
752
+ /** Represents a heading block */
753
+ Blocks["HEADING"] = "heading";
754
+ /** Represents a text block */
755
+ Blocks["TEXT"] = "text";
756
+ /** Represents a bullet/unordered list block */
757
+ Blocks["BULLET_LIST"] = "bulletList";
758
+ /** Represents an ordered/numbered list block */
759
+ Blocks["ORDERED_LIST"] = "orderedList";
760
+ /** Represents a list item within a list block */
761
+ Blocks["LIST_ITEM"] = "listItem";
762
+ /** Represents a blockquote block */
763
+ Blocks["BLOCK_QUOTE"] = "blockquote";
764
+ /** Represents a code block */
765
+ Blocks["CODE_BLOCK"] = "codeBlock";
766
+ /** Represents a hard break (line break) */
767
+ Blocks["HARDBREAK"] = "hardBreak";
768
+ /** Represents a horizontal rule/divider */
769
+ Blocks["HORIZONTAL_RULE"] = "horizontalRule";
770
+ /** Represents a DotCMS image block */
771
+ Blocks["DOT_IMAGE"] = "dotImage";
772
+ /** Represents a DotCMS video block */
773
+ Blocks["DOT_VIDEO"] = "dotVideo";
774
+ /** Represents a table block */
775
+ Blocks["TABLE"] = "table";
776
+ /** Represents a DotCMS content block */
777
+ Blocks["DOT_CONTENT"] = "dotContent";
778
+ })(Blocks || (Blocks = {}));
779
+
780
+ /**
781
+ * Subscribes to content changes in the UVE editor
782
+ *
783
+ * @param {UVEEventHandler} callback - Function to be called when content changes are detected
784
+ * @returns {Object} Object containing unsubscribe function and event type
785
+ * @returns {Function} .unsubscribe - Function to remove the event listener
786
+ * @returns {UVEEventType} .event - The event type being subscribed to
787
+ * @internal
788
+ */
789
+ function onContentChanges(callback) {
790
+ const messageCallback = event => {
791
+ if (event.data.name === __DOTCMS_UVE_EVENT__.UVE_SET_PAGE_DATA) {
792
+ callback(event.data.payload);
793
+ }
794
+ };
795
+ window.addEventListener('message', messageCallback);
796
+ return {
797
+ unsubscribe: () => {
798
+ window.removeEventListener('message', messageCallback);
799
+ },
800
+ event: UVEEventType.CONTENT_CHANGES
801
+ };
802
+ }
803
+ /**
804
+ * Subscribes to page reload events in the UVE editor
805
+ *
806
+ * @param {UVEEventHandler} callback - Function to be called when page reload is triggered
807
+ * @returns {Object} Object containing unsubscribe function and event type
808
+ * @returns {Function} .unsubscribe - Function to remove the event listener
809
+ * @returns {UVEEventType} .event - The event type being subscribed to
810
+ * @internal
811
+ */
812
+ function onPageReload(callback) {
813
+ const messageCallback = event => {
814
+ if (event.data.name === __DOTCMS_UVE_EVENT__.UVE_RELOAD_PAGE) {
815
+ callback();
816
+ }
817
+ };
818
+ window.addEventListener('message', messageCallback);
819
+ return {
820
+ unsubscribe: () => {
821
+ window.removeEventListener('message', messageCallback);
822
+ },
823
+ event: UVEEventType.PAGE_RELOAD
824
+ };
825
+ }
826
+ /**
827
+ * Subscribes to request bounds events in the UVE editor
828
+ *
829
+ * @param {UVEEventHandler} callback - Function to be called when bounds are requested
830
+ * @returns {Object} Object containing unsubscribe function and event type
831
+ * @returns {Function} .unsubscribe - Function to remove the event listener
832
+ * @returns {UVEEventType} .event - The event type being subscribed to
833
+ * @internal
834
+ */
835
+ function onRequestBounds(callback) {
836
+ const messageCallback = event => {
837
+ if (event.data.name === __DOTCMS_UVE_EVENT__.UVE_REQUEST_BOUNDS) {
838
+ const containers = Array.from(document.querySelectorAll('[data-dot-object="container"]'));
839
+ const positionData = getDotCMSPageBounds(containers);
840
+ callback(positionData);
841
+ }
842
+ };
843
+ window.addEventListener('message', messageCallback);
844
+ return {
845
+ unsubscribe: () => {
846
+ window.removeEventListener('message', messageCallback);
847
+ },
848
+ event: UVEEventType.REQUEST_BOUNDS
849
+ };
850
+ }
851
+ /**
852
+ * Subscribes to iframe scroll events in the UVE editor
853
+ *
854
+ * @param {UVEEventHandler} callback - Function to be called when iframe scroll occurs
855
+ * @returns {Object} Object containing unsubscribe function and event type
856
+ * @returns {Function} .unsubscribe - Function to remove the event listener
857
+ * @returns {UVEEventType} .event - The event type being subscribed to
858
+ * @internal
859
+ */
860
+ function onIframeScroll(callback) {
861
+ const messageCallback = event => {
862
+ if (event.data.name === __DOTCMS_UVE_EVENT__.UVE_SCROLL_INSIDE_IFRAME) {
863
+ const direction = event.data.direction;
864
+ callback(direction);
865
+ }
866
+ };
867
+ window.addEventListener('message', messageCallback);
868
+ return {
869
+ unsubscribe: () => {
870
+ window.removeEventListener('message', messageCallback);
871
+ },
872
+ event: UVEEventType.IFRAME_SCROLL
873
+ };
874
+ }
875
+ /**
876
+ * Subscribes to contentlet hover events in the UVE editor
877
+ *
878
+ * @param {UVEEventHandler} callback - Function to be called when a contentlet is hovered
879
+ * @returns {Object} Object containing unsubscribe function and event type
880
+ * @returns {Function} .unsubscribe - Function to remove the event listener
881
+ * @returns {UVEEventType} .event - The event type being subscribed to
882
+ * @internal
883
+ */
884
+ function onContentletHovered(callback) {
885
+ const pointerMoveCallback = event => {
886
+ const foundElement = findDotCMSElement(event.target);
887
+ if (!foundElement) return;
888
+ const {
889
+ x,
890
+ y,
891
+ width,
892
+ height
893
+ } = foundElement.getBoundingClientRect();
894
+ const isContainer = foundElement.dataset?.['dotObject'] === 'container';
895
+ const contentletForEmptyContainer = {
896
+ identifier: 'TEMP_EMPTY_CONTENTLET',
897
+ title: 'TEMP_EMPTY_CONTENTLET',
898
+ contentType: 'TEMP_EMPTY_CONTENTLET_TYPE',
899
+ inode: 'TEMPY_EMPTY_CONTENTLET_INODE',
900
+ widgetTitle: 'TEMP_EMPTY_CONTENTLET',
901
+ baseType: 'TEMP_EMPTY_CONTENTLET',
902
+ onNumberOfPages: 1
903
+ };
904
+ const contentlet = {
905
+ identifier: foundElement.dataset?.['dotIdentifier'],
906
+ title: foundElement.dataset?.['dotTitle'],
907
+ inode: foundElement.dataset?.['dotInode'],
908
+ contentType: foundElement.dataset?.['dotType'],
909
+ baseType: foundElement.dataset?.['dotBasetype'],
910
+ widgetTitle: foundElement.dataset?.['dotWidgetTitle'],
911
+ onNumberOfPages: foundElement.dataset?.['dotOnNumberOfPages']
912
+ };
913
+ const vtlFiles = findDotCMSVTLData(foundElement);
914
+ const contentletPayload = {
915
+ container:
916
+ // Here extract dot-container from contentlet if it is Headless
917
+ // or search in parent container if it is VTL
918
+ foundElement.dataset?.['dotContainer'] ? JSON.parse(foundElement.dataset?.['dotContainer']) : getClosestDotCMSContainerData(foundElement),
919
+ contentlet: isContainer ? contentletForEmptyContainer : contentlet,
920
+ vtlFiles
921
+ };
922
+ const contentletHoveredPayload = {
923
+ x,
924
+ y,
925
+ width,
926
+ height,
927
+ payload: contentletPayload
928
+ };
929
+ callback(contentletHoveredPayload);
930
+ };
931
+ document.addEventListener('pointermove', pointerMoveCallback);
932
+ return {
933
+ unsubscribe: () => {
934
+ document.removeEventListener('pointermove', pointerMoveCallback);
935
+ },
936
+ event: UVEEventType.CONTENTLET_HOVERED
937
+ };
938
+ }
939
+
940
+ /**
941
+ * Events that can be subscribed to in the UVE
942
+ *
943
+ * @internal
944
+ * @type {Record<UVEEventType, UVEEventSubscriber>}
945
+ */
946
+ const __UVE_EVENTS__ = {
947
+ [UVEEventType.CONTENT_CHANGES]: callback => {
948
+ return onContentChanges(callback);
949
+ },
950
+ [UVEEventType.PAGE_RELOAD]: callback => {
951
+ return onPageReload(callback);
952
+ },
953
+ [UVEEventType.REQUEST_BOUNDS]: callback => {
954
+ return onRequestBounds(callback);
955
+ },
956
+ [UVEEventType.IFRAME_SCROLL]: callback => {
957
+ return onIframeScroll(callback);
958
+ },
959
+ [UVEEventType.CONTENTLET_HOVERED]: callback => {
960
+ return onContentletHovered(callback);
961
+ }
962
+ };
963
+ /**
964
+ * Default UVE event
965
+ *
966
+ * @param {string} event - The event to subscribe to.
967
+ * @internal
968
+ */
969
+ const __UVE_EVENT_ERROR_FALLBACK__ = event => {
970
+ return {
971
+ unsubscribe: () => {
972
+ /* do nothing */
973
+ },
974
+ event
975
+ };
976
+ };
977
+ /**
978
+ * Development mode
979
+ *
980
+ * @internal
981
+ */
982
+ const DEVELOPMENT_MODE = 'development';
983
+ /**
984
+ * Production mode
985
+ *
986
+ * @internal
987
+ */
988
+ const PRODUCTION_MODE = 'production';
989
+ /**
990
+ * End class
991
+ *
992
+ * @internal
993
+ */
994
+ const END_CLASS = 'col-end-';
995
+ /**
996
+ * Start class
997
+ *
998
+ * @internal
999
+ */
1000
+ const START_CLASS = 'col-start-';
1001
+ /**
1002
+ * Empty container style for React
1003
+ *
1004
+ * @internal
1005
+ */
1006
+ const EMPTY_CONTAINER_STYLE_REACT = {
1007
+ width: '100%',
1008
+ backgroundColor: '#ECF0FD',
1009
+ display: 'flex',
1010
+ justifyContent: 'center',
1011
+ alignItems: 'center',
1012
+ color: '#030E32',
1013
+ height: '10rem'
1014
+ };
1015
+ /**
1016
+ * Empty container style for Angular
1017
+ *
1018
+ * @internal
1019
+ */
1020
+ const EMPTY_CONTAINER_STYLE_ANGULAR = {
1021
+ width: '100%',
1022
+ 'background-color': '#ECF0FD',
1023
+ display: 'flex',
1024
+ 'justify-content': 'center',
1025
+ 'align-items': 'center',
1026
+ color: '#030E32',
1027
+ height: '10rem'
1028
+ };
1029
+ /**
1030
+ * Custom no component
1031
+ *
1032
+ * @internal
1033
+ */
1034
+ const CUSTOM_NO_COMPONENT = 'CustomNoComponent';
1035
+
1036
+ /**
1037
+ * Gets the current state of the Universal Visual Editor (UVE).
1038
+ *
1039
+ * This function checks if the code is running inside the DotCMS Universal Visual Editor
1040
+ * and returns information about its current state, including the editor mode.
1041
+ *
1042
+ * @export
1043
+ * @return {UVEState | undefined} Returns the UVE state object if running inside the editor,
1044
+ * undefined otherwise.
1045
+ *
1046
+ * The state includes:
1047
+ * - mode: The current editor mode (preview, edit, live)
1048
+ * - languageId: The language ID of the current page setted on the UVE
1049
+ * - persona: The persona of the current page setted on the UVE
1050
+ * - variantName: The name of the current variant
1051
+ * - experimentId: The ID of the current experiment
1052
+ * - publishDate: The publish date of the current page setted on the UVE
1053
+ *
1054
+ * @note The absence of any of these properties means that the value is the default one.
1055
+ *
1056
+ * @example
1057
+ * ```ts
1058
+ * const editorState = getUVEState();
1059
+ * if (editorState?.mode === 'edit') {
1060
+ * // Enable editing features
1061
+ * }
1062
+ * ```
1063
+ */
1064
+ function getUVEState() {
1065
+ if (typeof window === 'undefined' || window.parent === window || !window.location) {
1066
+ return undefined;
1067
+ }
1068
+ const url = new URL(window.location.href);
1069
+ const possibleModes = Object.values(UVE_MODE);
1070
+ let mode = url.searchParams.get('mode') ?? UVE_MODE.EDIT;
1071
+ const languageId = url.searchParams.get('language_id');
1072
+ const persona = url.searchParams.get('personaId');
1073
+ const variantName = url.searchParams.get('variantName');
1074
+ const experimentId = url.searchParams.get('experimentId');
1075
+ const publishDate = url.searchParams.get('publishDate');
1076
+ if (!possibleModes.includes(mode)) {
1077
+ mode = UVE_MODE.EDIT;
1078
+ }
1079
+ return {
1080
+ mode,
1081
+ languageId,
1082
+ persona,
1083
+ variantName,
1084
+ experimentId,
1085
+ publishDate
1086
+ };
1087
+ }
1088
+ /**
1089
+ * Creates a subscription to a UVE event.
1090
+ *
1091
+ * @param eventType - The type of event to subscribe to
1092
+ * @param callback - The callback function that will be called when the event occurs
1093
+ * @returns An event subscription that can be used to unsubscribe
1094
+ *
1095
+ * @example
1096
+ * ```ts
1097
+ * // Subscribe to page changes
1098
+ * const subscription = createUVESubscription(UVEEventType.CONTENT_CHANGES, (changes) => {
1099
+ * console.log('Content changes:', changes);
1100
+ * });
1101
+ *
1102
+ * // Later, unsubscribe when no longer needed
1103
+ * subscription.unsubscribe();
1104
+ * ```
1105
+ */
1106
+ function createUVESubscription(eventType, callback) {
1107
+ if (!getUVEState()) {
1108
+ console.warn('UVE Subscription: Not running inside UVE');
1109
+ return __UVE_EVENT_ERROR_FALLBACK__(eventType);
1110
+ }
1111
+ const eventCallback = __UVE_EVENTS__[eventType];
1112
+ if (!eventCallback) {
1113
+ console.error(`UVE Subscription: Event ${eventType} not found`);
1114
+ return __UVE_EVENT_ERROR_FALLBACK__(eventType);
1115
+ }
1116
+ return eventCallback(callback);
1117
+ }
1118
+
1119
+ export { Blocks as B, CUSTOM_NO_COMPONENT as C, DEVELOPMENT_MODE as D, END_CLASS as E, PRODUCTION_MODE as P, START_CLASS as S, __UVE_EVENTS__ as _, __UVE_EVENT_ERROR_FALLBACK__ as a, EMPTY_CONTAINER_STYLE_REACT as b, EMPTY_CONTAINER_STYLE_ANGULAR as c, __DOTCMS_UVE_EVENT__ as d, getDotCMSContentletsBound as e, getDotCMSContainerData as f, getDotCMSPageBounds as g, getClosestDotCMSContainerData as h, findDotCMSElement as i, findDotCMSVTLData as j, computeScrollIsInBottom as k, combineClasses as l, getColumnPositionClasses as m, getDotContentletAttributes as n, getContainersData as o, getContentletsInContainer as p, getDotContainerAttributes as q, isValidBlocks as r, setBounds as s, getUVEState as t, createUVESubscription as u, sendMessageToUVE as v, editContentlet as w, reorderMenu as x, initInlineEditing as y, initUVE as z };