@dotcms/uve 0.0.1-beta.9 → 1.0.0

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