@strapi/content-manager 5.52.3 → 5.54.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/dist/admin/components/ActionsDrawer.js +17 -6
- package/dist/admin/components/ActionsDrawer.js.map +1 -1
- package/dist/admin/components/ActionsDrawer.mjs +19 -8
- package/dist/admin/components/ActionsDrawer.mjs.map +1 -1
- package/dist/admin/history/components/VersionsList.js +37 -43
- package/dist/admin/history/components/VersionsList.js.map +1 -1
- package/dist/admin/history/components/VersionsList.mjs +37 -43
- package/dist/admin/history/components/VersionsList.mjs.map +1 -1
- package/dist/admin/pages/EditView/EditViewPage.js +15 -20
- package/dist/admin/pages/EditView/EditViewPage.js.map +1 -1
- package/dist/admin/pages/EditView/EditViewPage.mjs +15 -20
- package/dist/admin/pages/EditView/EditViewPage.mjs.map +1 -1
- package/dist/admin/pages/EditView/components/DocumentActions.js +2 -0
- package/dist/admin/pages/EditView/components/DocumentActions.js.map +1 -1
- package/dist/admin/pages/EditView/components/DocumentActions.mjs +2 -0
- package/dist/admin/pages/EditView/components/DocumentActions.mjs.map +1 -1
- package/dist/admin/pages/EditView/components/EditorToolbarObserver.js +74 -7
- package/dist/admin/pages/EditView/components/EditorToolbarObserver.js.map +1 -1
- package/dist/admin/pages/EditView/components/EditorToolbarObserver.mjs +74 -7
- package/dist/admin/pages/EditView/components/EditorToolbarObserver.mjs.map +1 -1
- package/dist/admin/pages/EditView/components/FormInputs/BlocksInput/Blocks/Link.js +25 -16
- package/dist/admin/pages/EditView/components/FormInputs/BlocksInput/Blocks/Link.js.map +1 -1
- package/dist/admin/pages/EditView/components/FormInputs/BlocksInput/Blocks/Link.mjs +25 -16
- package/dist/admin/pages/EditView/components/FormInputs/BlocksInput/Blocks/Link.mjs.map +1 -1
- package/dist/admin/pages/EditView/components/FormInputs/BlocksInput/BlocksContent.js +7 -2
- package/dist/admin/pages/EditView/components/FormInputs/BlocksInput/BlocksContent.js.map +1 -1
- package/dist/admin/pages/EditView/components/FormInputs/BlocksInput/BlocksContent.mjs +7 -2
- package/dist/admin/pages/EditView/components/FormInputs/BlocksInput/BlocksContent.mjs.map +1 -1
- package/dist/admin/pages/EditView/components/FormInputs/BlocksInput/BlocksEditor.js +59 -2
- package/dist/admin/pages/EditView/components/FormInputs/BlocksInput/BlocksEditor.js.map +1 -1
- package/dist/admin/pages/EditView/components/FormInputs/BlocksInput/BlocksEditor.mjs +60 -3
- package/dist/admin/pages/EditView/components/FormInputs/BlocksInput/BlocksEditor.mjs.map +1 -1
- package/dist/admin/pages/EditView/components/FormInputs/BlocksInput/BlocksInput.js +4 -1
- package/dist/admin/pages/EditView/components/FormInputs/BlocksInput/BlocksInput.js.map +1 -1
- package/dist/admin/pages/EditView/components/FormInputs/BlocksInput/BlocksInput.mjs +4 -1
- package/dist/admin/pages/EditView/components/FormInputs/BlocksInput/BlocksInput.mjs.map +1 -1
- package/dist/admin/pages/EditView/components/FormInputs/BlocksInput/BlocksToolbar.js +4 -1
- package/dist/admin/pages/EditView/components/FormInputs/BlocksInput/BlocksToolbar.js.map +1 -1
- package/dist/admin/pages/EditView/components/FormInputs/BlocksInput/BlocksToolbar.mjs +4 -1
- package/dist/admin/pages/EditView/components/FormInputs/BlocksInput/BlocksToolbar.mjs.map +1 -1
- package/dist/admin/pages/EditView/components/FormInputs/BlocksInput/EditorLayout.js +2 -1
- package/dist/admin/pages/EditView/components/FormInputs/BlocksInput/EditorLayout.js.map +1 -1
- package/dist/admin/pages/EditView/components/FormInputs/BlocksInput/EditorLayout.mjs +2 -1
- package/dist/admin/pages/EditView/components/FormInputs/BlocksInput/EditorLayout.mjs.map +1 -1
- package/dist/admin/pages/EditView/components/FormInputs/Wysiwyg/WysiwygNav.js +25 -2
- package/dist/admin/pages/EditView/components/FormInputs/Wysiwyg/WysiwygNav.js.map +1 -1
- package/dist/admin/pages/EditView/components/FormInputs/Wysiwyg/WysiwygNav.mjs +5 -2
- package/dist/admin/pages/EditView/components/FormInputs/Wysiwyg/WysiwygNav.mjs.map +1 -1
- package/dist/admin/pages/EditView/components/InputRenderer.js +5 -1
- package/dist/admin/pages/EditView/components/InputRenderer.js.map +1 -1
- package/dist/admin/pages/EditView/components/InputRenderer.mjs +6 -2
- package/dist/admin/pages/EditView/components/InputRenderer.mjs.map +1 -1
- package/dist/admin/pages/ListView/ListViewPage.js +1 -0
- package/dist/admin/pages/ListView/ListViewPage.js.map +1 -1
- package/dist/admin/pages/ListView/ListViewPage.mjs +1 -0
- package/dist/admin/pages/ListView/ListViewPage.mjs.map +1 -1
- package/dist/admin/preview/components/InputPopover.js +64 -46
- package/dist/admin/preview/components/InputPopover.js.map +1 -1
- package/dist/admin/preview/components/InputPopover.mjs +66 -49
- package/dist/admin/preview/components/InputPopover.mjs.map +1 -1
- package/dist/admin/preview/hooks/usePreviewInputManager.js +23 -7
- package/dist/admin/preview/hooks/usePreviewInputManager.js.map +1 -1
- package/dist/admin/preview/hooks/usePreviewInputManager.mjs +25 -9
- package/dist/admin/preview/hooks/usePreviewInputManager.mjs.map +1 -1
- package/dist/admin/preview/pages/Preview.js +19 -0
- package/dist/admin/preview/pages/Preview.js.map +1 -1
- package/dist/admin/preview/pages/Preview.mjs +20 -1
- package/dist/admin/preview/pages/Preview.mjs.map +1 -1
- package/dist/admin/preview/utils/constants.js +9 -3
- package/dist/admin/preview/utils/constants.js.map +1 -1
- package/dist/admin/preview/utils/constants.mjs +9 -3
- package/dist/admin/preview/utils/constants.mjs.map +1 -1
- package/dist/admin/src/components/ActionsDrawer.d.ts +1 -0
- package/dist/admin/src/pages/EditView/components/EditorToolbarObserver.d.ts +2 -1
- package/dist/admin/src/pages/EditView/components/FormInputs/BlocksInput/BlocksContent.d.ts +5 -1
- package/dist/admin/src/pages/EditView/components/FormInputs/BlocksInput/BlocksEditor.d.ts +5 -0
- package/dist/admin/src/pages/EditView/components/FormInputs/BlocksInput/BlocksInput.d.ts +5 -0
- package/dist/admin/src/pages/EditView/components/FormInputs/BlocksInput/EditorLayout.d.ts +2 -1
- package/dist/admin/src/preview/components/InputPopover.d.ts +2 -1
- package/dist/admin/src/preview/pages/Preview.d.ts +1 -0
- package/dist/admin/src/preview/utils/constants.d.ts +8 -1
- package/dist/server/preview/controllers/previewScript.js +548 -22
- package/package.json +5 -5
|
@@ -82,6 +82,62 @@ function previewScript(config) {
|
|
|
82
82
|
return document.querySelectorAll(`[${SOURCE_ATTRIBUTE}*="path=${path}"]`);
|
|
83
83
|
};
|
|
84
84
|
|
|
85
|
+
/**
|
|
86
|
+
* Group key for the highlight manager. For most fields this is the raw
|
|
87
|
+
* source attribute (so identical sources share one highlight, which is what
|
|
88
|
+
* gives multi-media galleries their single bounding box). For blocks fields,
|
|
89
|
+
* every encoded marker shares the same `path` and `fieldPath` (the blocks
|
|
90
|
+
* field path). We drop `path` from the key so all marked elements cluster
|
|
91
|
+
* into a single highlight whose rect spans the entire rendered field.
|
|
92
|
+
* @param {string} sourceAttr
|
|
93
|
+
* @returns {string}
|
|
94
|
+
*/
|
|
95
|
+
const deriveGroupKey = (sourceAttr) => {
|
|
96
|
+
const params = new URLSearchParams(sourceAttr);
|
|
97
|
+
const fieldPath = params.get('fieldPath');
|
|
98
|
+
if (!fieldPath) return sourceAttr;
|
|
99
|
+
params.delete('path');
|
|
100
|
+
return params.toString();
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Returns true when value looks like a Strapi blocks AST. Inlined here because
|
|
105
|
+
* this script is serialised and injected into iframes where imports are unavailable.
|
|
106
|
+
* @param {unknown} value
|
|
107
|
+
* @returns {boolean}
|
|
108
|
+
*/
|
|
109
|
+
const isBlocksValue = (value) => {
|
|
110
|
+
if (!Array.isArray(value) || value.length === 0) return false;
|
|
111
|
+
return value.every(
|
|
112
|
+
(n) =>
|
|
113
|
+
n !== null &&
|
|
114
|
+
typeof n === 'object' &&
|
|
115
|
+
'type' in n &&
|
|
116
|
+
'children' in n &&
|
|
117
|
+
Array.isArray(/** @type {{ children: unknown }} */ (n).children)
|
|
118
|
+
);
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Blocks live updates are rendered by the host frontend (e.g. via BlocksRenderer).
|
|
123
|
+
* Re-dispatch the change on the iframe window so integrators can subscribe with
|
|
124
|
+
* `window.addEventListener('strapiFieldChange', …)` without the preview script
|
|
125
|
+
* attempting to patch the DOM. The admin panel also posts the same payload via
|
|
126
|
+
* `postMessage`, which hosts can listen for on `window` 'message' events.
|
|
127
|
+
*
|
|
128
|
+
* 'strapiFieldChange' is a public event — host apps depend on this name, so it
|
|
129
|
+
* is intentionally hardcoded rather than read from INTERNAL_EVENTS config.
|
|
130
|
+
* @param {string} field
|
|
131
|
+
* @param {unknown} value
|
|
132
|
+
*/
|
|
133
|
+
const forwardBlocksFieldChange = (field, value) => {
|
|
134
|
+
window.dispatchEvent(
|
|
135
|
+
new CustomEvent('strapiFieldChange', {
|
|
136
|
+
detail: { field, value },
|
|
137
|
+
})
|
|
138
|
+
);
|
|
139
|
+
};
|
|
140
|
+
|
|
85
141
|
/**
|
|
86
142
|
* @param {Element} element
|
|
87
143
|
* @returns {boolean}
|
|
@@ -287,6 +343,25 @@ function previewScript(config) {
|
|
|
287
343
|
return sourceAttr;
|
|
288
344
|
};
|
|
289
345
|
|
|
346
|
+
/**
|
|
347
|
+
* Resolve the source attribute to use when opening the popover for a clicked
|
|
348
|
+
* element. Blocks markers carry a `fieldPath`; redirect the popover to that
|
|
349
|
+
* field. Falls back to the media-specific normalization for everything else.
|
|
350
|
+
* @param {string} sourceAttr
|
|
351
|
+
* @param {Element} element
|
|
352
|
+
* @returns {string}
|
|
353
|
+
*/
|
|
354
|
+
const getFocusPath = (sourceAttr, element) => {
|
|
355
|
+
const params = new URLSearchParams(sourceAttr);
|
|
356
|
+
const fieldPath = params.get('fieldPath');
|
|
357
|
+
if (fieldPath) {
|
|
358
|
+
params.set('path', fieldPath);
|
|
359
|
+
params.delete('fieldPath');
|
|
360
|
+
return params.toString();
|
|
361
|
+
}
|
|
362
|
+
return getFieldPathForMedia(sourceAttr, element);
|
|
363
|
+
};
|
|
364
|
+
|
|
290
365
|
/* -----------------------------------------------------------------------------------------------
|
|
291
366
|
* Functionality pieces
|
|
292
367
|
* ---------------------------------------------------------------------------------------------*/
|
|
@@ -314,7 +389,9 @@ function previewScript(config) {
|
|
|
314
389
|
* @param {Element} element
|
|
315
390
|
*/
|
|
316
391
|
const applyStegaToElement = (element) => {
|
|
317
|
-
// Handle img and video tags - check src
|
|
392
|
+
// Handle img and video tags - check src and alt attributes for stega encoding.
|
|
393
|
+
// The src path handles media fields; the alt path handles blocks image blocks
|
|
394
|
+
// (the url is intentionally not encoded to avoid corrupting the src attribute).
|
|
318
395
|
if (isMediaElement(element)) {
|
|
319
396
|
const src = element.getAttribute('src');
|
|
320
397
|
if (src) {
|
|
@@ -339,6 +416,26 @@ function previewScript(config) {
|
|
|
339
416
|
}
|
|
340
417
|
} catch (error) {}
|
|
341
418
|
}
|
|
419
|
+
|
|
420
|
+
// Blocks image markers are encoded into the alt attribute (not the src).
|
|
421
|
+
// If the alt carries a marker that is not yet superseded by a src-derived one,
|
|
422
|
+
// apply it and clean the visible alt text.
|
|
423
|
+
if (!element.hasAttribute(SOURCE_ATTRIBUTE)) {
|
|
424
|
+
const alt = element.getAttribute('alt');
|
|
425
|
+
if (alt) {
|
|
426
|
+
try {
|
|
427
|
+
const result = stegaDecode(alt);
|
|
428
|
+
if (result && 'strapiSource' in result) {
|
|
429
|
+
element.setAttribute(SOURCE_ATTRIBUTE, result.strapiSource);
|
|
430
|
+
}
|
|
431
|
+
const cleanedAlt = stegaClean(alt);
|
|
432
|
+
if (cleanedAlt !== alt) {
|
|
433
|
+
element.setAttribute('alt', cleanedAlt);
|
|
434
|
+
}
|
|
435
|
+
} catch (error) {}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
342
439
|
return;
|
|
343
440
|
}
|
|
344
441
|
|
|
@@ -505,10 +602,265 @@ function previewScript(config) {
|
|
|
505
602
|
/** @type {string | null} */
|
|
506
603
|
let focusedField = null;
|
|
507
604
|
|
|
605
|
+
/**
|
|
606
|
+
* Blocks fields fully delegate live-typing renders to the host frontend
|
|
607
|
+
* (see forwardBlocksFieldChange) — new or resized block content never gets
|
|
608
|
+
* a stega tag of its own, so the per-element ResizeObserver used for every
|
|
609
|
+
* other field type can't see it. Observing the field's own container
|
|
610
|
+
* instead picks up any size change inside it (new blocks, growing or
|
|
611
|
+
* shrinking text, removed blocks) without requiring each child to be
|
|
612
|
+
* individually tagged. computeGroupRect already reads the container's
|
|
613
|
+
* rect fresh on every call — this only makes sure something re-triggers
|
|
614
|
+
* that read while the user is typing, instead of waiting for the next
|
|
615
|
+
* explicit rescan.
|
|
616
|
+
* @type {Map<string, HTMLElement>}
|
|
617
|
+
*/
|
|
618
|
+
const observedContainers = new Map();
|
|
619
|
+
const containerResizeObserver = new ResizeObserver(() => {
|
|
620
|
+
updateAllHighlights();
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
/**
|
|
624
|
+
* Block-level HTML tags produced by standard Strapi blocks renderers.
|
|
625
|
+
* Each corresponds to exactly one top-level Slate block in the editor.
|
|
626
|
+
*/
|
|
627
|
+
const BLOCK_LEVEL_TAGS = [
|
|
628
|
+
'P',
|
|
629
|
+
'H1',
|
|
630
|
+
'H2',
|
|
631
|
+
'H3',
|
|
632
|
+
'H4',
|
|
633
|
+
'H5',
|
|
634
|
+
'H6',
|
|
635
|
+
'UL',
|
|
636
|
+
'OL',
|
|
637
|
+
'BLOCKQUOTE',
|
|
638
|
+
'PRE',
|
|
639
|
+
];
|
|
640
|
+
|
|
641
|
+
/**
|
|
642
|
+
* Maximum area a candidate blocks container may have, relative to the union
|
|
643
|
+
* of the group's own marked elements. A real field container is only
|
|
644
|
+
* slightly larger than its content (padding, empty trailing blocks). A
|
|
645
|
+
* candidate many times larger is a page-level layout element that merely
|
|
646
|
+
* happens to contain block-level children, and using it would stretch the
|
|
647
|
+
* highlight across unrelated content.
|
|
648
|
+
*/
|
|
649
|
+
const MAX_CONTAINER_AREA_RATIO = 6;
|
|
650
|
+
|
|
651
|
+
/**
|
|
652
|
+
* Extra height added below a blocks field's marked content so empty trailing
|
|
653
|
+
* blocks (which produce no stega span) stay hoverable and clickable. Always
|
|
654
|
+
* clamped to the field's own bounds — see computeGroupRect.
|
|
655
|
+
*/
|
|
656
|
+
const BLOCKS_TRAILING_BUFFER = 80;
|
|
657
|
+
|
|
658
|
+
/**
|
|
659
|
+
* Union rect of a group's marked elements, ignoring zero-sized ones.
|
|
660
|
+
* @param {HighlightGroup} group
|
|
661
|
+
* @returns {{ left: number, top: number, width: number, height: number } | null}
|
|
662
|
+
*/
|
|
663
|
+
const getGroupUnionRect = (group) => {
|
|
664
|
+
let minLeft = Infinity;
|
|
665
|
+
let minTop = Infinity;
|
|
666
|
+
let maxRight = -Infinity;
|
|
667
|
+
let maxBottom = -Infinity;
|
|
668
|
+
let any = false;
|
|
669
|
+
group.elements.forEach((el) => {
|
|
670
|
+
const r = el.getBoundingClientRect();
|
|
671
|
+
if (r.width === 0 && r.height === 0) return;
|
|
672
|
+
any = true;
|
|
673
|
+
if (r.left < minLeft) minLeft = r.left;
|
|
674
|
+
if (r.top < minTop) minTop = r.top;
|
|
675
|
+
if (r.right > maxRight) maxRight = r.right;
|
|
676
|
+
if (r.bottom > maxBottom) maxBottom = r.bottom;
|
|
677
|
+
});
|
|
678
|
+
if (!any) return null;
|
|
679
|
+
return { left: minLeft, top: minTop, width: maxRight - minLeft, height: maxBottom - minTop };
|
|
680
|
+
};
|
|
681
|
+
|
|
682
|
+
/**
|
|
683
|
+
* Find the DOM element that wraps the entire rendered output of a blocks
|
|
684
|
+
* field — the direct parent of all top-level block elements (`<p>`, `<h1>`,
|
|
685
|
+
* etc.). Used to derive a tight, always-current bounding rect for the
|
|
686
|
+
* highlight, including empty blocks and container padding that stega spans
|
|
687
|
+
* cannot cover.
|
|
688
|
+
*
|
|
689
|
+
* Strategy: walk up from the first stega span until we find an element
|
|
690
|
+
* whose direct children include at least one block-level element. This
|
|
691
|
+
* mirrors the fallback in findBlockIndex and handles all DOM shapes
|
|
692
|
+
* correctly — including lists where NCA would land on <li> (not the
|
|
693
|
+
* container) and single-span groups where there is no useful NCA.
|
|
694
|
+
*
|
|
695
|
+
* The block-level-tag test alone is not sufficient: a blocks field whose
|
|
696
|
+
* blocks produce no block-level tags (e.g. a field containing only an
|
|
697
|
+
* image, which renders as <img>) has no matching ancestor inside the field,
|
|
698
|
+
* so the walk escapes into page layout and matches an unrelated container.
|
|
699
|
+
* That stretched the highlight over the whole page and — because highlights
|
|
700
|
+
* sit on top and swallow clicks — made the rest of the preview unclickable.
|
|
701
|
+
* We therefore reject candidates whose area dwarfs the group's own content.
|
|
702
|
+
*
|
|
703
|
+
* @param {HighlightGroup} group
|
|
704
|
+
* @returns {HTMLElement | null}
|
|
705
|
+
*/
|
|
706
|
+
const findBlocksContainer = (group) => {
|
|
707
|
+
const firstEl = group.elements.values().next().value;
|
|
708
|
+
if (!firstEl) return null;
|
|
709
|
+
|
|
710
|
+
const union = getGroupUnionRect(group);
|
|
711
|
+
const unionArea = union ? union.width * union.height : 0;
|
|
712
|
+
|
|
713
|
+
let el = firstEl.parentElement;
|
|
714
|
+
while (el && el !== document.body && el !== document.documentElement) {
|
|
715
|
+
// Skip list elements — <li> can have a nested <ul>/<ol> as a direct child, and
|
|
716
|
+
// @strapi/blocks-react-renderer places nested lists directly inside <ul>/<ol>
|
|
717
|
+
// (not wrapped in a <li>), so list containers also satisfy the block-level check
|
|
718
|
+
// while being blocks themselves, not the field container.
|
|
719
|
+
if (
|
|
720
|
+
el.tagName !== 'LI' &&
|
|
721
|
+
el.tagName !== 'UL' &&
|
|
722
|
+
el.tagName !== 'OL' &&
|
|
723
|
+
Array.from(el.children).some((c) => BLOCK_LEVEL_TAGS.includes(c.tagName))
|
|
724
|
+
) {
|
|
725
|
+
const r = el.getBoundingClientRect();
|
|
726
|
+
const area = r.width * r.height;
|
|
727
|
+
// Accept only a container that stays proportionate to the field's own
|
|
728
|
+
// content. Bail out entirely rather than climbing further: everything
|
|
729
|
+
// above an over-large candidate is even larger.
|
|
730
|
+
if (unionArea > 0 && area > unionArea * MAX_CONTAINER_AREA_RATIO) {
|
|
731
|
+
return null;
|
|
732
|
+
}
|
|
733
|
+
return el;
|
|
734
|
+
}
|
|
735
|
+
el = el.parentElement;
|
|
736
|
+
}
|
|
737
|
+
return null;
|
|
738
|
+
};
|
|
739
|
+
|
|
740
|
+
/**
|
|
741
|
+
* Resolves the trusted container for a blocks field's group, preferring a
|
|
742
|
+
* still-attached, already-observed container over re-running the
|
|
743
|
+
* ratio-guarded discovery walk.
|
|
744
|
+
*
|
|
745
|
+
* findBlocksContainer's area guard only needs to protect *first*
|
|
746
|
+
* discovery (telling a real field container apart from an unrelated
|
|
747
|
+
* page-level ancestor we escaped into, e.g. an image-only field with no
|
|
748
|
+
* block-level tags of its own). Once a container has been established for
|
|
749
|
+
* a groupKey, live edits inside it (e.g. typing a new, unmarked image
|
|
750
|
+
* block into a field whose only marked content is a short paragraph) can
|
|
751
|
+
* legitimately dwarf the originally-marked content — re-running the guard
|
|
752
|
+
* on every redraw would then reject the same, still-correct container and
|
|
753
|
+
* collapse the highlight down to a too-small fallback box. The container
|
|
754
|
+
* element itself doesn't change across those edits (the host re-renders
|
|
755
|
+
* its children, not the field's own wrapper), so trusting it is safe as
|
|
756
|
+
* long as it's still in the document.
|
|
757
|
+
* @param {string} groupKey
|
|
758
|
+
* @param {HighlightGroup} group
|
|
759
|
+
* @returns {HTMLElement | null}
|
|
760
|
+
*/
|
|
761
|
+
const getBlocksContainer = (groupKey, group) => {
|
|
762
|
+
const cached = observedContainers.get(groupKey);
|
|
763
|
+
if (cached && document.contains(cached)) {
|
|
764
|
+
return cached;
|
|
765
|
+
}
|
|
766
|
+
return findBlocksContainer(group);
|
|
767
|
+
};
|
|
768
|
+
|
|
769
|
+
/**
|
|
770
|
+
* Keeps a blocks group's container under observation as its membership
|
|
771
|
+
* changes. Safe to call every time an element joins the group — re-finding
|
|
772
|
+
* and re-observing the same container is a no-op; it only does real work
|
|
773
|
+
* when the container genuinely changes or disappears (e.g. an image-only
|
|
774
|
+
* field where findBlocksContainer's area guard rejects every candidate).
|
|
775
|
+
* @param {string} groupKey
|
|
776
|
+
* @param {HighlightGroup} group
|
|
777
|
+
*/
|
|
778
|
+
const syncContainerObservation = (groupKey, group) => {
|
|
779
|
+
const firstEl = group.elements.values().next().value;
|
|
780
|
+
const sourceAttr = firstEl?.getAttribute(SOURCE_ATTRIBUTE);
|
|
781
|
+
const isBlocksField = !!sourceAttr && new URLSearchParams(sourceAttr).has('fieldPath');
|
|
782
|
+
if (!isBlocksField) return;
|
|
783
|
+
|
|
784
|
+
const container = getBlocksContainer(groupKey, group);
|
|
785
|
+
const previousContainer = observedContainers.get(groupKey);
|
|
786
|
+
if (container === previousContainer) return;
|
|
787
|
+
|
|
788
|
+
if (previousContainer) {
|
|
789
|
+
containerResizeObserver.unobserve(previousContainer);
|
|
790
|
+
}
|
|
791
|
+
if (container) {
|
|
792
|
+
containerResizeObserver.observe(container);
|
|
793
|
+
observedContainers.set(groupKey, container);
|
|
794
|
+
} else {
|
|
795
|
+
observedContainers.delete(groupKey);
|
|
796
|
+
}
|
|
797
|
+
};
|
|
798
|
+
|
|
799
|
+
/**
|
|
800
|
+
* Find the 0-based index of the Slate block that was clicked in a blocks
|
|
801
|
+
* field. We cannot use `group.elements.indexOf(anchor)` because
|
|
802
|
+
* `group.elements` has one entry per stega text-span, not one per block —
|
|
803
|
+
* formatted text (bold, italic…) produces multiple spans per block, so an
|
|
804
|
+
* element-list index is not the same as the Slate block index.
|
|
805
|
+
*
|
|
806
|
+
* Uses the same trusted-container resolution as computeGroupRect
|
|
807
|
+
* (getBlocksContainer) rather than its own walk-up-with-area-guard, so
|
|
808
|
+
* that double-clicking a legitimately marked block next to a large
|
|
809
|
+
* unmarked one (e.g. an image typed in next to a short paragraph) doesn't
|
|
810
|
+
* spuriously fail the guard and lose the block index.
|
|
811
|
+
* @param {HTMLElement} anchor - stega span that was clicked
|
|
812
|
+
* @param {string} groupKey
|
|
813
|
+
* @param {HighlightGroup} group
|
|
814
|
+
* @returns {number} 0-based block index, or -1 if it cannot be determined
|
|
815
|
+
*/
|
|
816
|
+
const findBlockIndex = (anchor, groupKey, group) => {
|
|
817
|
+
const fieldContainer = getBlocksContainer(groupKey, group);
|
|
818
|
+
if (!fieldContainer) return -1;
|
|
819
|
+
|
|
820
|
+
// Walk up from anchor to its direct-child-of-container ancestor
|
|
821
|
+
let blockEl = anchor;
|
|
822
|
+
while (blockEl.parentElement && blockEl.parentElement !== fieldContainer) {
|
|
823
|
+
blockEl = /** @type {HTMLElement} */ (blockEl.parentElement);
|
|
824
|
+
}
|
|
825
|
+
if (blockEl.parentElement !== fieldContainer) return -1;
|
|
826
|
+
|
|
827
|
+
// Use the full children list (not just block-level tags) so that every
|
|
828
|
+
// Slate block maps to its correct DOM position. Empty paragraphs render
|
|
829
|
+
// as <br> and images may render as <img> or custom elements — filtering
|
|
830
|
+
// by tag name would make the index drift whenever one of these appears
|
|
831
|
+
// before the clicked block.
|
|
832
|
+
return Array.from(fieldContainer.children).indexOf(blockEl);
|
|
833
|
+
};
|
|
834
|
+
|
|
508
835
|
/**
|
|
509
836
|
* @param {HighlightGroup} group
|
|
837
|
+
* @param {string} groupKey
|
|
510
838
|
*/
|
|
511
|
-
const computeGroupRect = (group) => {
|
|
839
|
+
const computeGroupRect = (group, groupKey) => {
|
|
840
|
+
if (group.elements.size === 0) return null;
|
|
841
|
+
|
|
842
|
+
// For blocks fields (identified by fieldPath in the source attribute),
|
|
843
|
+
// derive the highlight from the field container's bounding rect so that
|
|
844
|
+
// empty blocks and container padding are always included — a fixed pixel
|
|
845
|
+
// buffer on the span union is too brittle when content grows or shrinks.
|
|
846
|
+
const firstEl = group.elements.values().next().value;
|
|
847
|
+
const firstSourceAttr = firstEl?.getAttribute(SOURCE_ATTRIBUTE);
|
|
848
|
+
const isBlocksField =
|
|
849
|
+
!!firstSourceAttr && new URLSearchParams(firstSourceAttr).has('fieldPath');
|
|
850
|
+
|
|
851
|
+
if (isBlocksField) {
|
|
852
|
+
const container = getBlocksContainer(groupKey, group);
|
|
853
|
+
if (container) {
|
|
854
|
+
const r = container.getBoundingClientRect();
|
|
855
|
+
if (r.width > 0 || r.height > 0) {
|
|
856
|
+
return { left: r.left, top: r.top, width: r.width, height: r.height };
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
// Non-blocks fields (and blocks fallback): union of all non-zero span rects.
|
|
862
|
+
// For blocks fields add a buffer so that empty trailing blocks are still
|
|
863
|
+
// clickable even when they produce no stega spans.
|
|
512
864
|
let minLeft = Infinity;
|
|
513
865
|
let minTop = Infinity;
|
|
514
866
|
let maxRight = -Infinity;
|
|
@@ -524,19 +876,45 @@ function previewScript(config) {
|
|
|
524
876
|
if (r.bottom > maxBottom) maxBottom = r.bottom;
|
|
525
877
|
});
|
|
526
878
|
if (!any) return null;
|
|
879
|
+
|
|
880
|
+
let bottom = maxBottom;
|
|
881
|
+
if (isBlocksField) {
|
|
882
|
+
// The buffer keeps empty trailing blocks clickable, but it must not spill
|
|
883
|
+
// past the field itself — otherwise it covers whatever the host renders
|
|
884
|
+
// below (e.g. the next field's label) and swallows its clicks.
|
|
885
|
+
//
|
|
886
|
+
// Clamp it to the field's own extent: walk up while each ancestor still
|
|
887
|
+
// starts at the marked content's top edge. Such an ancestor wraps only
|
|
888
|
+
// this field, so its bottom includes trailing blocks (which carry no
|
|
889
|
+
// stega span) while stopping short of sibling content. The first
|
|
890
|
+
// ancestor that starts higher up belongs to the surrounding layout.
|
|
891
|
+
const limit = maxBottom + BLOCKS_TRAILING_BUFFER;
|
|
892
|
+
let fieldBottom = maxBottom;
|
|
893
|
+
const firstElement = /** @type {HTMLElement} */ (firstEl);
|
|
894
|
+
let el = firstElement?.parentElement;
|
|
895
|
+
while (el && el !== document.body && el !== document.documentElement) {
|
|
896
|
+
const r = el.getBoundingClientRect();
|
|
897
|
+
if (r.height === 0 || r.top < minTop - 1) break;
|
|
898
|
+
if (r.bottom > fieldBottom) fieldBottom = r.bottom;
|
|
899
|
+
el = el.parentElement;
|
|
900
|
+
}
|
|
901
|
+
bottom = Math.min(limit, Math.max(maxBottom, fieldBottom));
|
|
902
|
+
}
|
|
903
|
+
|
|
527
904
|
return {
|
|
528
905
|
left: minLeft,
|
|
529
906
|
top: minTop,
|
|
530
907
|
width: maxRight - minLeft,
|
|
531
|
-
height:
|
|
908
|
+
height: bottom - minTop,
|
|
532
909
|
};
|
|
533
910
|
};
|
|
534
911
|
|
|
535
912
|
/**
|
|
536
913
|
* @param {HighlightGroup} group
|
|
914
|
+
* @param {string} groupKey
|
|
537
915
|
*/
|
|
538
|
-
const drawGroup = (group) => {
|
|
539
|
-
const rect = computeGroupRect(group);
|
|
916
|
+
const drawGroup = (group, groupKey) => {
|
|
917
|
+
const rect = computeGroupRect(group, groupKey);
|
|
540
918
|
if (!rect) {
|
|
541
919
|
group.highlight.style.display = 'none';
|
|
542
920
|
return;
|
|
@@ -548,28 +926,49 @@ function previewScript(config) {
|
|
|
548
926
|
};
|
|
549
927
|
|
|
550
928
|
const updateAllHighlights = () => {
|
|
551
|
-
groups.forEach(drawGroup);
|
|
929
|
+
groups.forEach((group, groupKey) => drawGroup(group, groupKey));
|
|
552
930
|
};
|
|
553
931
|
|
|
554
932
|
/**
|
|
555
933
|
* Pick the underlying source element under the pointer so single-click
|
|
556
934
|
* redispatch hits the specific item the user clicked, even when the group
|
|
557
935
|
* highlight covers several elements (multi-media gallery).
|
|
936
|
+
* When no element rect contains the point (e.g. click in empty space within
|
|
937
|
+
* a blocks field, or on an unmarked node like a code block or an image with
|
|
938
|
+
* no alt text), falls back to the nearest element by distance to rect.
|
|
939
|
+
*
|
|
940
|
+
* `exact` tells callers whether the returned element actually contains the
|
|
941
|
+
* point or is just the nearest fallback — the blocks double-click handler
|
|
942
|
+
* needs this to avoid deriving a Slate block index from an element the user
|
|
943
|
+
* didn't actually click on.
|
|
558
944
|
*
|
|
559
945
|
* @param {HighlightGroup} group
|
|
560
946
|
* @param {number} x
|
|
561
947
|
* @param {number} y
|
|
562
|
-
* @returns {HTMLElement | null}
|
|
948
|
+
* @returns {{ element: HTMLElement | null, exact: boolean }}
|
|
563
949
|
*/
|
|
564
950
|
const pickElementAtPoint = (group, x, y) => {
|
|
565
951
|
for (const el of group.elements) {
|
|
566
952
|
const r = el.getBoundingClientRect();
|
|
567
953
|
if (x >= r.left && x <= r.right && y >= r.top && y <= r.bottom) {
|
|
568
|
-
return el;
|
|
954
|
+
return { element: el, exact: true };
|
|
569
955
|
}
|
|
570
956
|
}
|
|
571
|
-
|
|
572
|
-
|
|
957
|
+
// No exact hit — find the nearest element by squared distance to its rect.
|
|
958
|
+
let nearest = null;
|
|
959
|
+
let minDist = Infinity;
|
|
960
|
+
for (const el of group.elements) {
|
|
961
|
+
const r = el.getBoundingClientRect();
|
|
962
|
+
if (r.width === 0 && r.height === 0) continue;
|
|
963
|
+
const dx = Math.max(r.left - x, 0, x - r.right);
|
|
964
|
+
const dy = Math.max(r.top - y, 0, y - r.bottom);
|
|
965
|
+
const dist = dx * dx + dy * dy;
|
|
966
|
+
if (dist < minDist) {
|
|
967
|
+
minDist = dist;
|
|
968
|
+
nearest = el;
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
return { element: nearest ?? group.elements.values().next().value ?? null, exact: false };
|
|
573
972
|
};
|
|
574
973
|
|
|
575
974
|
/**
|
|
@@ -593,6 +992,10 @@ function previewScript(config) {
|
|
|
593
992
|
event.preventDefault();
|
|
594
993
|
event.stopPropagation();
|
|
595
994
|
|
|
995
|
+
// Notify admin immediately so it can close any open popover.
|
|
996
|
+
// (highlights call stopPropagation so the document-level listener below won't fire)
|
|
997
|
+
sendMessage(INTERNAL_EVENTS.STRAPI_IFRAME_CLICK, null);
|
|
998
|
+
|
|
596
999
|
const existingTimeout = pendingClicks.get(group);
|
|
597
1000
|
if (existingTimeout) {
|
|
598
1001
|
window.clearTimeout(existingTimeout);
|
|
@@ -606,7 +1009,7 @@ function previewScript(config) {
|
|
|
606
1009
|
|
|
607
1010
|
// Pick the specific underlying element under the pointer so the
|
|
608
1011
|
// redispatched click targets the right item in a multi-element group
|
|
609
|
-
const underlying = pickElementAtPoint(group, event.clientX, event.clientY);
|
|
1012
|
+
const { element: underlying } = pickElementAtPoint(group, event.clientX, event.clientY);
|
|
610
1013
|
if (!underlying) return;
|
|
611
1014
|
|
|
612
1015
|
/** @type {HTMLElement} */
|
|
@@ -662,13 +1065,65 @@ function previewScript(config) {
|
|
|
662
1065
|
pendingClicks.delete(group);
|
|
663
1066
|
}
|
|
664
1067
|
|
|
665
|
-
const anchor = pickElementAtPoint(group, event.clientX, event.clientY);
|
|
1068
|
+
const { element: anchor, exact } = pickElementAtPoint(group, event.clientX, event.clientY);
|
|
666
1069
|
if (!anchor) return;
|
|
667
1070
|
const sourceAttribute = anchor.getAttribute(SOURCE_ATTRIBUTE);
|
|
668
1071
|
if (!sourceAttribute) return;
|
|
669
|
-
const path =
|
|
670
|
-
|
|
1072
|
+
const path = getFocusPath(sourceAttribute, anchor);
|
|
1073
|
+
// For blocks fields, use the specific clicked element's rect so the
|
|
1074
|
+
// popover opens adjacent to the clicked block, not the entire field.
|
|
1075
|
+
// If the stega element is zero-width (empty block with only invisible
|
|
1076
|
+
// chars), walk up to the nearest block-level ancestor that has a real
|
|
1077
|
+
// width so the popover isn't anchored to a point at the far edge of a line.
|
|
1078
|
+
const isBlocksField = new URLSearchParams(sourceAttribute).has('fieldPath');
|
|
1079
|
+
let rect;
|
|
1080
|
+
if (isBlocksField) {
|
|
1081
|
+
let anchorRect = anchor.getBoundingClientRect();
|
|
1082
|
+
if (anchorRect.width <= 5) {
|
|
1083
|
+
let el = anchor.parentElement;
|
|
1084
|
+
while (el && el !== document.documentElement) {
|
|
1085
|
+
const r = el.getBoundingClientRect();
|
|
1086
|
+
if (r.width > 5) {
|
|
1087
|
+
anchorRect = r;
|
|
1088
|
+
break;
|
|
1089
|
+
}
|
|
1090
|
+
el = el.parentElement;
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
rect = anchorRect;
|
|
1094
|
+
} else {
|
|
1095
|
+
rect = computeGroupRect(group, groupKey);
|
|
1096
|
+
}
|
|
671
1097
|
if (!rect) return;
|
|
1098
|
+
let blockIndex = -1;
|
|
1099
|
+
if (isBlocksField) {
|
|
1100
|
+
if (exact) {
|
|
1101
|
+
blockIndex = findBlockIndex(anchor, groupKey, group);
|
|
1102
|
+
} else {
|
|
1103
|
+
// No exact hit on a marked element — resolve the real clicked
|
|
1104
|
+
// element's own position instead of guessing from the fallback
|
|
1105
|
+
// anchor above (nearest marked sibling), which is only reliable
|
|
1106
|
+
// for "which field", not "which block". findBlockIndex only
|
|
1107
|
+
// needs a genuine DOM descendant of the field container, not a
|
|
1108
|
+
// marked one, so this works whether the element is unmarked
|
|
1109
|
+
// because it's not yet saved (live-typed content) or unmarked by
|
|
1110
|
+
// design (code blocks, alt-less images) — no stega marker needed
|
|
1111
|
+
// either way.
|
|
1112
|
+
// elementsFromPoint (not elementFromPoint) skips past the
|
|
1113
|
+
// highlight overlay itself, which is what actually received this
|
|
1114
|
+
// dblclick and would otherwise be the only hit at this point.
|
|
1115
|
+
const clickedElement = document
|
|
1116
|
+
.elementsFromPoint(event.clientX, event.clientY)
|
|
1117
|
+
.find((el) => !overlay.contains(el));
|
|
1118
|
+
if (clickedElement) {
|
|
1119
|
+
blockIndex = findBlockIndex(
|
|
1120
|
+
/** @type {HTMLElement} */ (clickedElement),
|
|
1121
|
+
groupKey,
|
|
1122
|
+
group
|
|
1123
|
+
);
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
672
1127
|
sendMessage(INTERNAL_EVENTS.STRAPI_FIELD_FOCUS_INTENT, {
|
|
673
1128
|
path,
|
|
674
1129
|
position: {
|
|
@@ -679,6 +1134,7 @@ function previewScript(config) {
|
|
|
679
1134
|
width: rect.width,
|
|
680
1135
|
height: rect.height,
|
|
681
1136
|
},
|
|
1137
|
+
blockIndex: blockIndex >= 0 ? blockIndex : null,
|
|
682
1138
|
});
|
|
683
1139
|
};
|
|
684
1140
|
|
|
@@ -719,6 +1175,12 @@ function previewScript(config) {
|
|
|
719
1175
|
* @param {HighlightGroup} group
|
|
720
1176
|
*/
|
|
721
1177
|
const destroyGroup = (groupKey, group) => {
|
|
1178
|
+
const observedContainer = observedContainers.get(groupKey);
|
|
1179
|
+
if (observedContainer) {
|
|
1180
|
+
containerResizeObserver.unobserve(observedContainer);
|
|
1181
|
+
observedContainers.delete(groupKey);
|
|
1182
|
+
}
|
|
1183
|
+
|
|
722
1184
|
const pendingTimeout = pendingClicks.get(group);
|
|
723
1185
|
if (pendingTimeout) {
|
|
724
1186
|
window.clearTimeout(pendingTimeout);
|
|
@@ -754,8 +1216,9 @@ function previewScript(config) {
|
|
|
754
1216
|
if (elementToGroupKey.has(element)) {
|
|
755
1217
|
return;
|
|
756
1218
|
}
|
|
757
|
-
const
|
|
758
|
-
if (!
|
|
1219
|
+
const sourceAttr = element.getAttribute(SOURCE_ATTRIBUTE);
|
|
1220
|
+
if (!sourceAttr) return;
|
|
1221
|
+
const groupKey = deriveGroupKey(sourceAttr);
|
|
759
1222
|
|
|
760
1223
|
let group = groups.get(groupKey);
|
|
761
1224
|
|
|
@@ -828,7 +1291,8 @@ function previewScript(config) {
|
|
|
828
1291
|
}
|
|
829
1292
|
group.elements.add(element);
|
|
830
1293
|
elementToGroupKey.set(element, groupKey);
|
|
831
|
-
drawGroup(group);
|
|
1294
|
+
drawGroup(group, groupKey);
|
|
1295
|
+
syncContainerObservation(groupKey, group);
|
|
832
1296
|
};
|
|
833
1297
|
|
|
834
1298
|
/**
|
|
@@ -846,7 +1310,7 @@ function previewScript(config) {
|
|
|
846
1310
|
if (group.elements.size === 0) {
|
|
847
1311
|
destroyGroup(groupKey, group);
|
|
848
1312
|
} else {
|
|
849
|
-
drawGroup(group);
|
|
1313
|
+
drawGroup(group, groupKey);
|
|
850
1314
|
}
|
|
851
1315
|
};
|
|
852
1316
|
|
|
@@ -879,6 +1343,28 @@ function previewScript(config) {
|
|
|
879
1343
|
}
|
|
880
1344
|
});
|
|
881
1345
|
|
|
1346
|
+
/**
|
|
1347
|
+
* Reconcile tracked elements with the current DOM. Removes any elements
|
|
1348
|
+
* that are no longer mounted, registers any new ones, and redraws all
|
|
1349
|
+
* highlights. Called after the editor popover closes to correct staleness
|
|
1350
|
+
* from live-preview updates that changed the field's rendered height.
|
|
1351
|
+
*/
|
|
1352
|
+
const rescan = () => {
|
|
1353
|
+
// Prune elements that were removed from the DOM while the popover was open
|
|
1354
|
+
for (const element of [...elementToGroupKey.keys()]) {
|
|
1355
|
+
if (!document.contains(element)) {
|
|
1356
|
+
removeHighlightForElement(element);
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
// Register any elements that appeared during editing
|
|
1360
|
+
document.querySelectorAll(`[${SOURCE_ATTRIBUTE}]`).forEach((element) => {
|
|
1361
|
+
if (element instanceof HTMLElement) {
|
|
1362
|
+
createHighlightForElement(element); // no-op for already-tracked elements
|
|
1363
|
+
}
|
|
1364
|
+
});
|
|
1365
|
+
updateAllHighlights();
|
|
1366
|
+
};
|
|
1367
|
+
|
|
882
1368
|
return {
|
|
883
1369
|
get elements() {
|
|
884
1370
|
return Array.from(elementToGroupKey.keys());
|
|
@@ -887,6 +1373,7 @@ function previewScript(config) {
|
|
|
887
1373
|
return Array.from(groups.values());
|
|
888
1374
|
},
|
|
889
1375
|
updateAllHighlights,
|
|
1376
|
+
rescan,
|
|
890
1377
|
eventListeners,
|
|
891
1378
|
focusedHighlights,
|
|
892
1379
|
createHighlightForElement,
|
|
@@ -901,6 +1388,9 @@ function previewScript(config) {
|
|
|
901
1388
|
pendingClicks.forEach((timeout) => clearTimeout(timeout));
|
|
902
1389
|
pendingClicks.clear();
|
|
903
1390
|
},
|
|
1391
|
+
disconnectContainerObserver: () => {
|
|
1392
|
+
containerResizeObserver.disconnect();
|
|
1393
|
+
},
|
|
904
1394
|
};
|
|
905
1395
|
};
|
|
906
1396
|
|
|
@@ -1214,11 +1704,22 @@ function previewScript(config) {
|
|
|
1214
1704
|
if (!event.data?.type) return;
|
|
1215
1705
|
if (event.source !== window.parent || event.origin !== parentOrigin) return;
|
|
1216
1706
|
|
|
1217
|
-
// The user typed in an input, reflect the change in the preview
|
|
1218
|
-
|
|
1219
|
-
|
|
1707
|
+
// The user typed in an input, reflect the change in the preview.
|
|
1708
|
+
// 'strapiFieldChange' is a public event name — host apps also depend on it.
|
|
1709
|
+
if (event.data.type === 'strapiFieldChange') {
|
|
1710
|
+
const { field, value, type } = event.data.payload;
|
|
1220
1711
|
if (!field) return;
|
|
1221
1712
|
|
|
1713
|
+
// Blocks fields are identified by the `type` key in the payload (set by
|
|
1714
|
+
// usePreviewInputManager). isBlocksValue is a fallback for any value that
|
|
1715
|
+
// looks like a blocks AST without an explicit type. Both paths delegate to
|
|
1716
|
+
// the host frontend so that null/[] clears are forwarded correctly instead
|
|
1717
|
+
// of falling through to DOM text patching.
|
|
1718
|
+
if (type === 'blocks' || isBlocksValue(value)) {
|
|
1719
|
+
forwardBlocksFieldChange(field, value);
|
|
1720
|
+
return;
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1222
1723
|
const matchedElements = /** @type {HTMLElement[]} */ (
|
|
1223
1724
|
Array.from(getElementsByPath(field)).filter((el) => el instanceof HTMLElement)
|
|
1224
1725
|
);
|
|
@@ -1367,6 +1868,15 @@ function previewScript(config) {
|
|
|
1367
1868
|
return;
|
|
1368
1869
|
}
|
|
1369
1870
|
|
|
1871
|
+
// The editor popover just closed; rescan groups after the next frame so
|
|
1872
|
+
// any in-flight React renders in the iframe have committed first.
|
|
1873
|
+
if (event.data.type === INTERNAL_EVENTS.STRAPI_RESCAN_HIGHLIGHTS) {
|
|
1874
|
+
requestAnimationFrame(() => {
|
|
1875
|
+
highlightManager.rescan();
|
|
1876
|
+
});
|
|
1877
|
+
return;
|
|
1878
|
+
}
|
|
1879
|
+
|
|
1370
1880
|
// The user is no longer focusing an input, remove the highlights
|
|
1371
1881
|
if (event.data.type === INTERNAL_EVENTS.STRAPI_FIELD_BLUR) {
|
|
1372
1882
|
const { field } = event.data.payload;
|
|
@@ -1382,6 +1892,13 @@ function previewScript(config) {
|
|
|
1382
1892
|
|
|
1383
1893
|
window.addEventListener('message', handleMessage);
|
|
1384
1894
|
|
|
1895
|
+
// Notify admin of any click on non-highlighted page areas so it can close an open popover.
|
|
1896
|
+
// Highlight click handlers call stopPropagation, so this only fires for background clicks.
|
|
1897
|
+
const documentClickHandler = () => {
|
|
1898
|
+
sendMessage(INTERNAL_EVENTS.STRAPI_IFRAME_CLICK, null);
|
|
1899
|
+
};
|
|
1900
|
+
document.addEventListener('click', documentClickHandler);
|
|
1901
|
+
|
|
1385
1902
|
// Add the message handler to the cleanup list
|
|
1386
1903
|
const messageEventListener = {
|
|
1387
1904
|
element: window,
|
|
@@ -1389,7 +1906,13 @@ function previewScript(config) {
|
|
|
1389
1906
|
handler: /** @type {EventListener} */ (handleMessage),
|
|
1390
1907
|
};
|
|
1391
1908
|
|
|
1392
|
-
|
|
1909
|
+
const documentClickEventListener = {
|
|
1910
|
+
element: /** @type {any} */ (document),
|
|
1911
|
+
type: /** @type {keyof HTMLElementEventMap} */ ('click'),
|
|
1912
|
+
handler: /** @type {EventListener} */ (documentClickHandler),
|
|
1913
|
+
};
|
|
1914
|
+
|
|
1915
|
+
return [...highlightManager.eventListeners, messageEventListener, documentClickEventListener];
|
|
1393
1916
|
};
|
|
1394
1917
|
|
|
1395
1918
|
/**
|
|
@@ -1417,6 +1940,9 @@ function previewScript(config) {
|
|
|
1417
1940
|
// Clear all pending click timeouts
|
|
1418
1941
|
highlightManager.clearAllPendingClicks();
|
|
1419
1942
|
|
|
1943
|
+
// Stop watching blocks-field containers for live resize
|
|
1944
|
+
highlightManager.disconnectContainerObserver();
|
|
1945
|
+
|
|
1420
1946
|
// Remove highlight event listeners
|
|
1421
1947
|
eventHandlers.forEach(({ element, type, handler }) => {
|
|
1422
1948
|
element.removeEventListener(type, handler);
|