diff --git a/packages/live-preview-sdk/src/inspectorMode/__tests__/addCalculatedAttributesToTaggedElements.test.ts b/packages/live-preview-sdk/src/inspectorMode/__tests__/addCalculatedAttributesToTaggedElements.test.ts
new file mode 100644
index 00000000..55c20591
--- /dev/null
+++ b/packages/live-preview-sdk/src/inspectorMode/__tests__/addCalculatedAttributesToTaggedElements.test.ts
@@ -0,0 +1,144 @@
+import { describe, expect, it } from 'vitest';
+
+import { InspectorModeDataAttributes } from '../types.js';
+import { addCalculatedAttributesToTaggedElements, getAllTaggedElements } from '../utils.js';
+
+describe('addCalculatedAttributesToTaggedElements', () => {
+ const dataEntry = InspectorModeDataAttributes.ENTRY_ID;
+ const dataField = InspectorModeDataAttributes.FIELD_ID;
+ const dataLocale = InspectorModeDataAttributes.LOCALE;
+
+ const html = (text: string) => {
+ return new DOMParser().parseFromString(text, 'text/html');
+ };
+
+ describe('visibility attributes', () => {
+ it('returns isCoveredByOtherElement as false if no element is covering tagged element', () => {
+ const dom = html(`
+
+ `);
+
+ dom.getElementById('entry-1')!.checkVisibility = () => true;
+ const entry1Coordinates = {
+ bottom: 100,
+ height: 100,
+ left: 10,
+ right: 100,
+ top: 10,
+ width: 100,
+ x: 10,
+ y: 10,
+ };
+ const entry1BoundingClientRect = {
+ ...entry1Coordinates,
+ toJSON: () => entry1Coordinates,
+ };
+ dom.getElementById('entry-1')!.getBoundingClientRect = () => entry1BoundingClientRect;
+
+ const { taggedElements: elements } = getAllTaggedElements({
+ root: dom,
+ options: { locale: 'locale-1' },
+ });
+
+ const entry1 = dom.getElementById('entry-1');
+
+ dom.elementFromPoint = (_x: number, _y: number) => {
+ return entry1;
+ };
+
+ const elementsWithCalculatedAttributes = addCalculatedAttributesToTaggedElements(
+ elements,
+ dom,
+ );
+ expect(elementsWithCalculatedAttributes).toEqual([
+ {
+ attributes: {
+ entryId: 'entry-1',
+ environment: undefined,
+ fieldId: 'field-1',
+ locale: 'locale-1',
+ space: undefined,
+ manuallyTagged: true,
+ },
+ element: dom.getElementById('entry-1'),
+ isVisible: true,
+ isCoveredByOtherElement: false,
+ coordinates: entry1BoundingClientRect,
+ },
+ ]);
+ });
+
+ it('returns isCoveredByOtherElement as true if an element is covering tagged element', () => {
+ const dom = html(`
+
+ `);
+
+ dom.getElementById('entry-1')!.checkVisibility = () => true;
+ const entry1Coordinates = {
+ bottom: 100,
+ height: 100,
+ left: 10,
+ right: 100,
+ top: 10,
+ width: 100,
+ x: 10,
+ y: 10,
+ };
+ const entry1BoundingClientRect = {
+ ...entry1Coordinates,
+ toJSON: () => entry1Coordinates,
+ };
+ dom.getElementById('entry-1')!.getBoundingClientRect = () => entry1BoundingClientRect;
+
+ const { taggedElements: elements } = getAllTaggedElements({
+ root: dom,
+ options: { locale: 'locale-1' },
+ });
+
+ const coveringElement = dom.getElementById('covering-element');
+
+ dom.elementFromPoint = (_x: number, _y: number) => {
+ return coveringElement;
+ };
+
+ const elementsWithCalculatedAttributes = addCalculatedAttributesToTaggedElements(
+ elements,
+ dom,
+ );
+ expect(elementsWithCalculatedAttributes).toEqual([
+ {
+ attributes: {
+ entryId: 'entry-1',
+ environment: undefined,
+ fieldId: 'field-1',
+ locale: 'locale-1',
+ space: undefined,
+ manuallyTagged: true,
+ },
+ element: dom.getElementById('entry-1'),
+ isVisible: true,
+ isCoveredByOtherElement: true,
+ coordinates: entry1BoundingClientRect,
+ },
+ ]);
+ });
+ });
+});
diff --git a/packages/live-preview-sdk/src/inspectorMode/index.ts b/packages/live-preview-sdk/src/inspectorMode/index.ts
index 049ae968..70d3e8dc 100644
--- a/packages/live-preview-sdk/src/inspectorMode/index.ts
+++ b/packages/live-preview-sdk/src/inspectorMode/index.ts
@@ -5,10 +5,13 @@ import { type MessageFromEditor } from '../messages.js';
import {
InspectorModeDataAttributes,
InspectorModeEventMethods,
- type InspectorModeAttributes,
type InspectorModeChangedMessage,
} from './types.js';
-import { getAllTaggedElements } from './utils.js';
+import {
+ addCalculatedAttributesToTaggedElements,
+ getAllTaggedElements,
+ TaggedElement,
+} from './utils.js';
export type InspectorModeOptions = {
locale: string;
@@ -18,13 +21,6 @@ export type InspectorModeOptions = {
ignoreManuallyTaggedElements?: boolean;
};
-type TaggedElement = {
- attributes: InspectorModeAttributes | null;
- coordinates: DOMRect;
- element: Element;
- isVisible: boolean;
-};
-
export class InspectorMode {
private delay = 300;
@@ -239,10 +235,11 @@ export class InspectorMode {
{
elements: this.taggedElements.map((taggedElement) => ({
// Important: do not add `element` as it can't be cloned by sendMessage
- coordinates: taggedElement.coordinates,
- isVisible: taggedElement.isVisible,
+ coordinates: taggedElement.coordinates!,
+ isVisible: !!taggedElement.isVisible,
attributes: taggedElement.attributes,
isHovered: this.hoveredElement === taggedElement.element,
+ isCoveredByOtherElement: !!taggedElement.isCoveredByOtherElement,
})),
automaticallyTaggedCount: this.automaticallyTaggedCount,
manuallyTaggedCount: this.manuallyTaggedCount,
@@ -260,15 +257,7 @@ export class InspectorMode {
options: this.options,
});
- const nextElements = taggedElements.map(({ element, attributes }) => ({
- element,
- coordinates: element.getBoundingClientRect(),
- attributes,
- isVisible: element.checkVisibility({
- checkOpacity: true,
- checkVisibilityCSS: true,
- }),
- }));
+ const nextElements = addCalculatedAttributesToTaggedElements(taggedElements);
if (isEqual(nextElements, this.taggedElements)) {
return;
@@ -279,7 +268,7 @@ export class InspectorMode {
this.observersCB = [];
// update elements and watch them
- this.taggedElements = nextElements;
+ this.taggedElements = nextElements as TaggedElement[];
taggedElements.forEach(({ element }) => this.observe(element));
// update the counters for telemetry
diff --git a/packages/live-preview-sdk/src/inspectorMode/utils.ts b/packages/live-preview-sdk/src/inspectorMode/utils.ts
index 45adf37d..7f7fc949 100644
--- a/packages/live-preview-sdk/src/inspectorMode/utils.ts
+++ b/packages/live-preview-sdk/src/inspectorMode/utils.ts
@@ -16,11 +16,18 @@ export type AutoTaggedElement = {
sourceMap: SourceMapMetadata;
};
-interface TaggedElement {
+interface PrecaulculatedTaggedElement {
element: Element;
attributes: InspectorModeAttributes | null;
}
+export interface TaggedElement extends PrecaulculatedTaggedElement {
+ isHovered: boolean;
+ isVisible: boolean;
+ coordinates: DOMRect;
+ isCoveredByOtherElement: boolean;
+}
+
const isTaggedElement = (node?: Node | null): boolean => {
if (!node) {
return false;
@@ -132,7 +139,10 @@ function getNodeText(node: HTMLElement): string {
.join('');
}
-function hasTaggedParent(node: HTMLElement, taggedElements: TaggedElement[]): boolean {
+function hasTaggedParent(
+ node: HTMLElement,
+ taggedElements: PrecaulculatedTaggedElement[],
+): boolean {
for (const tagged of taggedElements) {
if (tagged.element === node || tagged.element.contains(node)) {
return true;
@@ -154,7 +164,7 @@ export function getAllTaggedElements({
options: Omit;
ignoreManual?: boolean;
}): {
- taggedElements: TaggedElement[];
+ taggedElements: PrecaulculatedTaggedElement[];
manuallyTaggedCount: number;
automaticallyTaggedCount: number;
autoTaggedElements: AutoTaggedElement[];
@@ -166,7 +176,7 @@ export function getAllTaggedElements({
);
//Spread operator is necessary to convert the NodeList to an array
- const taggedElements: TaggedElement[] = [...alreadyTagged]
+ const taggedElements: PrecaulculatedTaggedElement[] = [...alreadyTagged]
.map((element: Element) => ({
element,
attributes: getManualInspectorModeAttributes(element, options),
@@ -261,7 +271,7 @@ export function getAllTaggedEntries({
return [
...new Set(
getAllTaggedElements({ options })
- .taggedElements.map((element: TaggedElement) => {
+ .taggedElements.map((element: PrecaulculatedTaggedElement) => {
if (element.attributes && 'entryId' in element.attributes) {
return element.attributes.entryId;
}
@@ -271,3 +281,53 @@ export function getAllTaggedEntries({
),
];
}
+
+const isElementOverlapped = (element: Element, coordinates: DOMRect, root = window.document) => {
+ const { top, right, bottom, left } = coordinates;
+ const topLeft = root.elementFromPoint(left, top);
+ const topRight = root.elementFromPoint(right, top);
+ const bottomLeft = root.elementFromPoint(left, bottom);
+ const bottomRight = root.elementFromPoint(right, bottom);
+
+ return (
+ topLeft === element || topRight === element || bottomLeft === element || bottomRight === element
+ );
+};
+
+const addVisibilityToTaggedElements = (
+ taggedElements: Partial[],
+ root = window.document,
+) =>
+ taggedElements.map((taggedElement) => ({
+ ...taggedElement,
+ isVisible: taggedElement.element!.checkVisibility({
+ checkOpacity: true,
+ checkVisibilityCSS: true,
+ }),
+ isCoveredByOtherElement: !isElementOverlapped(
+ taggedElement.element!,
+ taggedElement.coordinates!,
+ root,
+ ),
+ }));
+
+const addCoordinatesToTaggedElements = (
+ taggedElements: Partial[],
+): Partial[] =>
+ taggedElements.map(({ element, attributes }) => ({
+ element,
+ coordinates: element!.getBoundingClientRect(),
+ attributes,
+ }));
+
+/**
+ * Applies the attributes that we cannot simply get from the tagged elements itself
+ * but need to calculate based on the current state of the document
+ */
+export const addCalculatedAttributesToTaggedElements = (
+ taggedElements: PrecaulculatedTaggedElement[],
+ root = window.document,
+): TaggedElement[] => {
+ const taggedElementWithCoordinates = addCoordinatesToTaggedElements(taggedElements);
+ return addVisibilityToTaggedElements(taggedElementWithCoordinates, root) as TaggedElement[];
+};