diff --git a/README.md b/README.md
index f717def9..eed5c609 100644
--- a/README.md
+++ b/README.md
@@ -102,11 +102,8 @@ To use the inspector mode, you need to tag fields by adding the live preview dat
You can do this in React via our helper function.
-The necessary styles for the live edit tags can be found in the `@contentful/live-preview/style.css` file.
-
```jsx
import { ContentfulLivePreview } from '@contentful/live-preview';
-import '@contentful/live-preview/style.css';
...
@@ -197,18 +194,13 @@ or
npm install @contentful/live-preview
```
-2. Once you've got the data from Contentful, then you can initialize the live preview. You can use the `ContentfulLivePreview` class' [init function](#init-configuration) and add the stylesheet for field tagging as a stylesheet link.
+2. Once you've got the data from Contentful, then you can initialize the live preview. You can use the `ContentfulLivePreview` class' [init function](#init-configuration).
```html
Live Preview Example
-
diff --git a/examples/vanilla-js/package.json b/examples/vanilla-js/package.json
index 6555e6db..450d8b3c 100644
--- a/examples/vanilla-js/package.json
+++ b/examples/vanilla-js/package.json
@@ -11,7 +11,7 @@
"author": "",
"license": "MIT",
"dependencies": {
- "@contentful/live-preview": "^2.9.1",
+ "@contentful/live-preview": "latest",
"contentful": "^10.5.0",
"dotenv": "^16.3.1"
},
diff --git a/packages/live-preview-sdk/src/__tests__/index.spec.ts b/packages/live-preview-sdk/src/__tests__/index.spec.ts
index 2a17925e..1df2e403 100644
--- a/packages/live-preview-sdk/src/__tests__/index.spec.ts
+++ b/packages/live-preview-sdk/src/__tests__/index.spec.ts
@@ -6,13 +6,23 @@ import { ContentfulLivePreview } from '../index';
import { InspectorMode } from '../inspectorMode';
import { LiveUpdates } from '../liveUpdates';
import { SaveEvent } from '../saveEvent';
-import { TagAttributes } from '../types';
+import { InspectorModeDataAttributes } from '../inspectorMode/types';
vi.mock('../inspectorMode');
vi.mock('../liveUpdates');
vi.mock('../saveEvent');
vi.mock('../helpers');
+const ObserverMock = vi.fn(() => ({
+ disconnect: vi.fn(),
+ observe: vi.fn(),
+ takeRecords: vi.fn(),
+ unobserve: vi.fn(),
+}));
+
+vi.stubGlobal('ResizeObserver', ObserverMock);
+vi.stubGlobal('MutationObserver', ObserverMock);
+
describe('ContentfulLivePreview', () => {
const receiveMessageInspectorMode = vi.fn();
const receiveMessageLiveUpdates = vi.fn();
@@ -125,9 +135,9 @@ describe('ContentfulLivePreview', () => {
});
expect(result).toStrictEqual({
- [TagAttributes.FIELD_ID]: fieldId,
- [TagAttributes.ENTRY_ID]: entryId,
- [TagAttributes.LOCALE]: locale,
+ [InspectorModeDataAttributes.FIELD_ID]: fieldId,
+ [InspectorModeDataAttributes.ENTRY_ID]: entryId,
+ [InspectorModeDataAttributes.LOCALE]: locale,
});
});
diff --git a/packages/live-preview-sdk/src/__tests__/init.spec.ts b/packages/live-preview-sdk/src/__tests__/init.spec.ts
index c2feab11..a06d8362 100644
--- a/packages/live-preview-sdk/src/__tests__/init.spec.ts
+++ b/packages/live-preview-sdk/src/__tests__/init.spec.ts
@@ -9,6 +9,16 @@ import { LiveUpdates } from '../liveUpdates';
vi.mock('../helpers');
+const ObserverMock = vi.fn(() => ({
+ disconnect: vi.fn(),
+ observe: vi.fn(),
+ takeRecords: vi.fn(),
+ unobserve: vi.fn(),
+}));
+
+vi.stubGlobal('ResizeObserver', ObserverMock);
+vi.stubGlobal('MutationObserver', ObserverMock);
+
describe('init', () => {
beforeEach(() => {
(isInsideIframe as Mock).mockReturnValue(true);
diff --git a/packages/live-preview-sdk/src/__tests__/inspectorMode.spec.ts b/packages/live-preview-sdk/src/__tests__/inspectorMode.spec.ts
index 5b708f7b..4b2e3fb3 100644
--- a/packages/live-preview-sdk/src/__tests__/inspectorMode.spec.ts
+++ b/packages/live-preview-sdk/src/__tests__/inspectorMode.spec.ts
@@ -3,10 +3,23 @@ import { describe, test, expect, beforeEach, afterEach, vi } from 'vitest';
import { LIVE_PREVIEW_EDITOR_SOURCE } from '../constants';
import { InspectorMode } from '../inspectorMode';
-import { LivePreviewPostMessageMethods } from '../messages';
+import { InspectorModeEventMethods, LivePreviewPostMessageMethods } from '../messages';
+import { sendMessageToEditor } from '../helpers';
+
+vi.mock('../helpers');
const locale = 'en-US';
+const ObserverMock = vi.fn(() => ({
+ disconnect: vi.fn(),
+ observe: vi.fn(),
+ takeRecords: vi.fn(),
+ unobserve: vi.fn(),
+}));
+
+vi.stubGlobal('ResizeObserver', ObserverMock);
+vi.stubGlobal('MutationObserver', ObserverMock);
+
describe('InspectorMode', () => {
let inspectorMode: InspectorMode;
const targetOrigin = ['https://app.contentful.com'];
@@ -30,16 +43,23 @@ describe('InspectorMode', () => {
expect(spy).not.toHaveBeenCalled();
});
- test('should toggle "contentful-inspector--active" class on document.body based on value of isInspectorActive', () => {
- const spy = vi.spyOn(document.body.classList, 'toggle');
+ test('should send the tagged elements back to the editor', () => {
inspectorMode.receiveMessage({
- action: LivePreviewPostMessageMethods.INSPECTOR_MODE_CHANGED,
+ action: InspectorModeEventMethods.INSPECTOR_MODE_CHANGED,
from: 'live-preview',
- method: LivePreviewPostMessageMethods.INSPECTOR_MODE_CHANGED,
+ method: InspectorModeEventMethods.INSPECTOR_MODE_CHANGED,
source: LIVE_PREVIEW_EDITOR_SOURCE,
isInspectorActive: true,
});
- expect(spy).toHaveBeenCalledWith('contentful-inspector--active', true);
+
+ expect(sendMessageToEditor).toHaveBeenCalledOnce();
+ expect(sendMessageToEditor).toHaveBeenCalledWith(
+ InspectorModeEventMethods.TAGGED_ELEMENTS,
+ {
+ elements: [],
+ },
+ ['https://app.contentful.com']
+ );
});
});
});
diff --git a/packages/live-preview-sdk/src/__tests__/liveUpdates.spec.ts b/packages/live-preview-sdk/src/__tests__/liveUpdates.spec.ts
index 0fb81039..9db47220 100644
--- a/packages/live-preview-sdk/src/__tests__/liveUpdates.spec.ts
+++ b/packages/live-preview-sdk/src/__tests__/liveUpdates.spec.ts
@@ -5,7 +5,7 @@ import { describe, it, vi, expect, beforeEach, afterEach } from 'vitest';
import { LIVE_PREVIEW_EDITOR_SOURCE } from '../constants';
import * as helpers from '../helpers';
import { LiveUpdates } from '../liveUpdates';
-import { LivePreviewPostMessageMethods } from '../messages';
+import { InspectorModeEventMethods, LivePreviewPostMessageMethods } from '../messages';
import { ContentType } from '../types';
import assetFromEntryEditor from './fixtures/assetFromEntryEditor.json';
import landingPageContentType from './fixtures/landingPageContentType.json';
@@ -152,8 +152,8 @@ describe('LiveUpdates', () => {
await liveUpdates.receiveMessage({
isInspectorActive: false,
- action: LivePreviewPostMessageMethods.INSPECTOR_MODE_CHANGED,
- method: LivePreviewPostMessageMethods.INSPECTOR_MODE_CHANGED,
+ action: InspectorModeEventMethods.INSPECTOR_MODE_CHANGED,
+ method: InspectorModeEventMethods.INSPECTOR_MODE_CHANGED,
from: 'live-preview',
source: LIVE_PREVIEW_EDITOR_SOURCE,
});
diff --git a/packages/live-preview-sdk/src/__tests__/saveEvent.spec.ts b/packages/live-preview-sdk/src/__tests__/saveEvent.spec.ts
index 266bcd47..b5eea4ff 100644
--- a/packages/live-preview-sdk/src/__tests__/saveEvent.spec.ts
+++ b/packages/live-preview-sdk/src/__tests__/saveEvent.spec.ts
@@ -1,12 +1,12 @@
import { Entry } from 'contentful';
import { describe, beforeEach, vi, it, afterEach, Mock, expect } from 'vitest';
-import { getAllTaggedEntries } from '../fieldTaggingUtils';
import { EntrySavedMessage, LivePreviewPostMessageMethods } from '../messages';
import { SaveEvent } from '../saveEvent';
import { ContentType } from '../types';
+import { getAllTaggedEntries } from '../inspectorMode/utils';
-vi.mock('../fieldTaggingUtils');
+vi.mock('../inspectorMode/utils');
describe('SaveEvent', () => {
const locale = 'en-US';
diff --git a/packages/live-preview-sdk/src/constants.ts b/packages/live-preview-sdk/src/constants.ts
index 70657b26..30cd2508 100644
--- a/packages/live-preview-sdk/src/constants.ts
+++ b/packages/live-preview-sdk/src/constants.ts
@@ -1,13 +1,3 @@
-import { TagAttributes } from './types';
-
-export const DATA_CURR_FIELD_ID = `current-${TagAttributes.FIELD_ID}`;
-export const DATA_CURR_ENTRY_ID = `current-${TagAttributes.ENTRY_ID}`;
-export const DATA_CURR_LOCALE = `current-${TagAttributes.LOCALE}`;
-export const TOOLTIP_CLASS = 'contentful-tooltip';
-
-export const TOOLTIP_HEIGHT = 32;
-export const TOOLTIP_PADDING_LEFT = 5;
-
export const MAX_DEPTH = 10;
export const LIVE_PREVIEW_EDITOR_SOURCE = 'live-preview-editor' as const;
diff --git a/packages/live-preview-sdk/src/fieldTaggingUtils.ts b/packages/live-preview-sdk/src/fieldTaggingUtils.ts
deleted file mode 100644
index f3a4e6cf..00000000
--- a/packages/live-preview-sdk/src/fieldTaggingUtils.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import { TagAttributes } from './types';
-
-/**
- * Returns a list of tagged entries on the page
- */
-export function getAllTaggedEntries(): string[] {
- return [
- ...new Set(
- [...document.querySelectorAll(`[${TagAttributes.ENTRY_ID}]`)]
- .map((element) => element.getAttribute(TagAttributes.ENTRY_ID))
- .filter(Boolean) as string[]
- ),
- ];
-}
diff --git a/packages/live-preview-sdk/src/helpers/pollUrlChanges.ts b/packages/live-preview-sdk/src/helpers/pollUrlChanges.ts
index 65845615..d3778e18 100644
--- a/packages/live-preview-sdk/src/helpers/pollUrlChanges.ts
+++ b/packages/live-preview-sdk/src/helpers/pollUrlChanges.ts
@@ -1,3 +1,4 @@
+// TODO: instead of polling could we use MutationObserver to check for dom changes and then compare URL's
export function pollUrlChanges(callback: (newUrl: string) => void, interval = 500): () => void {
let initialUrl = window.location.href;
diff --git a/packages/live-preview-sdk/src/helpers/utils.ts b/packages/live-preview-sdk/src/helpers/utils.ts
index 26e5872a..6f118f7d 100644
--- a/packages/live-preview-sdk/src/helpers/utils.ts
+++ b/packages/live-preview-sdk/src/helpers/utils.ts
@@ -13,14 +13,14 @@ export function sendMessageToEditor(
data: EditorMessage,
targetOrigin: string[]
): void {
- const message: MessageFromSDK = {
+ const message = {
...data,
method,
from: 'live-preview',
source: LIVE_PREVIEW_SDK_SOURCE,
location: window.location.href,
version,
- };
+ } as MessageFromSDK;
debug.log('Send message', message);
diff --git a/packages/live-preview-sdk/src/index.ts b/packages/live-preview-sdk/src/index.ts
index fd1c7f31..6394aabd 100644
--- a/packages/live-preview-sdk/src/index.ts
+++ b/packages/live-preview-sdk/src/index.ts
@@ -3,7 +3,6 @@ import './styles.css';
import { type DocumentNode } from 'graphql';
import { version } from '../package.json';
-import { getAllTaggedEntries } from './fieldTaggingUtils';
import {
sendMessageToEditor,
pollUrlChanges,
@@ -13,24 +12,20 @@ import {
} from './helpers';
import { isValidMessage } from './helpers/validateMessage';
import { InspectorMode } from './inspectorMode';
+import { type InspectorModeTags, InspectorModeDataAttributes } from './inspectorMode/types';
+import { getAllTaggedEntries } from './inspectorMode/utils';
import { LiveUpdates } from './liveUpdates';
import {
- ConnectedMessage,
- EditorMessage,
+ type ConnectedMessage,
+ type EditorMessage,
+ type MessageFromEditor,
+ type PostMessageMethods,
+ type UrlChangedMessage,
LivePreviewPostMessageMethods,
- MessageFromEditor,
- PostMessageMethods,
- UrlChangedMessage,
openEntryInEditorUtility,
} from './messages';
import { SaveEvent } from './saveEvent';
-import {
- Argument,
- InspectorModeTags,
- LivePreviewProps,
- SubscribeCallback,
- TagAttributes,
-} from './types';
+import type { Argument, LivePreviewProps, SubscribeCallback } from './types';
export const VERSION = version;
@@ -156,14 +151,18 @@ export class ContentfulLivePreview {
LivePreviewPostMessageMethods.URL_CHANGED,
{
action: LivePreviewPostMessageMethods.URL_CHANGED,
- taggedElementCount: document.querySelectorAll(`[${TagAttributes.ENTRY_ID}]`).length,
+ taggedElementCount: document.querySelectorAll(
+ `[${InspectorModeDataAttributes.ENTRY_ID}]`
+ ).length,
} as UrlChangedMessage,
this.targetOrigin
);
});
// tell the editor that there's a SDK
- const taggedElementCount = document.querySelectorAll(`[${TagAttributes.ENTRY_ID}]`).length;
+ const taggedElementCount = document.querySelectorAll(
+ `[${InspectorModeDataAttributes.ENTRY_ID}]`
+ ).length;
sendMessageToEditor(
LivePreviewPostMessageMethods.CONNECTED,
{
@@ -234,9 +233,9 @@ export class ContentfulLivePreview {
}
return {
- [TagAttributes.FIELD_ID]: fieldId,
- [TagAttributes.ENTRY_ID]: entryId,
- [TagAttributes.LOCALE]: locale,
+ [InspectorModeDataAttributes.FIELD_ID]: fieldId,
+ [InspectorModeDataAttributes.ENTRY_ID]: entryId,
+ [InspectorModeDataAttributes.LOCALE]: locale,
};
}
diff --git a/packages/live-preview-sdk/src/inspectorMode.ts b/packages/live-preview-sdk/src/inspectorMode.ts
deleted file mode 100644
index 87b161bf..00000000
--- a/packages/live-preview-sdk/src/inspectorMode.ts
+++ /dev/null
@@ -1,128 +0,0 @@
-import {
- DATA_CURR_ENTRY_ID,
- DATA_CURR_FIELD_ID,
- DATA_CURR_LOCALE,
- TOOLTIP_CLASS,
- TOOLTIP_HEIGHT,
- TOOLTIP_PADDING_LEFT,
-} from './constants';
-import {
- InspectorModeChangedMessage,
- LivePreviewPostMessageMethods,
- MessageFromEditor,
- openEntryInEditorUtility,
-} from './messages';
-import { TagAttributes } from './types';
-
-export class InspectorMode {
- private tooltip: HTMLButtonElement | null = null; // this tooltip scrolls to the correct field in the entry editor
- private currentElementBesideTooltip: HTMLElement | null = null; // this element helps to position the tooltip
- private defaultLocale: string;
- private targetOrigin: string[];
-
- constructor({ locale, targetOrigin }: { locale: string; targetOrigin: string[] }) {
- this.tooltip = null;
- this.currentElementBesideTooltip = null;
- this.defaultLocale = locale;
- this.targetOrigin = targetOrigin;
-
- this.updateTooltipPosition = this.updateTooltipPosition.bind(this);
- this.addTooltipOnHover = this.addTooltipOnHover.bind(this);
- this.createTooltip = this.createTooltip.bind(this);
- this.clickHandler = this.clickHandler.bind(this);
-
- this.createTooltip();
- window.addEventListener('scroll', this.updateTooltipPosition);
- window.addEventListener('mouseover', this.addTooltipOnHover);
- }
-
- // Handles incoming messages from Contentful
- public receiveMessage(data: MessageFromEditor): void {
- if (
- ('action' in data && data.action === 'INSPECTOR_MODE_CHANGED') ||
- data.method === LivePreviewPostMessageMethods.INSPECTOR_MODE_CHANGED
- ) {
- // Toggle the contentful-inspector--active class on the body element based on the isInspectorActive boolean
- document.body.classList.toggle(
- 'contentful-inspector--active',
- (data as InspectorModeChangedMessage).isInspectorActive
- );
- }
- }
-
- // Updates the position of the tooltip
- private updateTooltipPosition() {
- if (!this.currentElementBesideTooltip || !this.tooltip) return false;
-
- const currentRectOfElement = this.currentElementBesideTooltip.getBoundingClientRect();
- const currentRectOfParentOfElement = this.tooltip.parentElement?.getBoundingClientRect();
-
- if (currentRectOfElement && currentRectOfParentOfElement) {
- let upperBoundOfTooltip = currentRectOfElement.top - TOOLTIP_HEIGHT;
- const left = currentRectOfElement.left - TOOLTIP_PADDING_LEFT;
-
- if (upperBoundOfTooltip < 0) {
- if (currentRectOfElement.top < 0) upperBoundOfTooltip = currentRectOfElement.top;
- else upperBoundOfTooltip = 0;
- }
-
- this.tooltip.style.top = upperBoundOfTooltip + 'px';
- this.tooltip.style.left = left + 'px';
-
- return true;
- }
-
- return false;
- }
-
- private addTooltipOnHover(e: MouseEvent) {
- const eventTargets = e.composedPath();
-
- for (const eventTarget of eventTargets) {
- const element = eventTarget as HTMLElement;
- if (element.nodeName === 'BODY') break;
- if (typeof element?.getAttribute !== 'function') continue;
-
- const currFieldId = element.getAttribute(TagAttributes.FIELD_ID);
- const currEntryId = element.getAttribute(TagAttributes.ENTRY_ID);
- const currLocale = element.getAttribute(TagAttributes.LOCALE) ?? this.defaultLocale;
-
- if (currFieldId && currEntryId && currLocale) {
- this.currentElementBesideTooltip = element;
-
- if (this.updateTooltipPosition()) {
- this.tooltip?.setAttribute(DATA_CURR_FIELD_ID, currFieldId);
- this.tooltip?.setAttribute(DATA_CURR_ENTRY_ID, currEntryId);
- this.tooltip?.setAttribute(DATA_CURR_LOCALE, currLocale);
- }
-
- break;
- }
- }
- }
-
- private createTooltip() {
- if (!document.querySelector(`.${TOOLTIP_CLASS}`)) {
- const tooltip = document.createElement('button');
- tooltip.classList.add(TOOLTIP_CLASS);
- tooltip.innerHTML = `Edit`;
- window.document.body.insertAdjacentElement('beforeend', tooltip);
- tooltip.addEventListener('click', this.clickHandler);
- this.tooltip = tooltip;
- }
- this.updateTooltipPosition();
- }
-
- // responsible for handling the event when the user clicks on the edit button in the tooltip
- private clickHandler() {
- const fieldId = this.tooltip?.getAttribute(DATA_CURR_FIELD_ID);
- const entryId = this.tooltip?.getAttribute(DATA_CURR_ENTRY_ID);
- const locale = this.tooltip?.getAttribute(DATA_CURR_LOCALE) || this.defaultLocale;
-
- if (fieldId && entryId && locale) {
- openEntryInEditorUtility(fieldId, entryId, locale, this.targetOrigin);
- }
- }
-}
diff --git a/packages/live-preview-sdk/src/inspectorMode/index.ts b/packages/live-preview-sdk/src/inspectorMode/index.ts
new file mode 100644
index 00000000..6db96a0a
--- /dev/null
+++ b/packages/live-preview-sdk/src/inspectorMode/index.ts
@@ -0,0 +1,212 @@
+import { sendMessageToEditor } from '../helpers';
+import type { MessageFromEditor } from '../messages';
+import {
+ InspectorModeDataAttributes,
+ type InspectorModeChangedMessage,
+ InspectorModeEventMethods,
+} from './types';
+import { getAllTaggedElements, getInspectorModeAttributes } from './utils';
+
+export class InspectorMode {
+ private defaultLocale: string;
+ private targetOrigin: string[];
+
+ private isScrolling = false;
+ private scrollTimeout?: NodeJS.Timeout;
+
+ private isResizing = false;
+ private resizeTimeout?: NodeJS.Timeout;
+
+ private hoveredElement?: HTMLElement;
+ private taggedElements: Element[] = [];
+
+ constructor({ locale, targetOrigin }: { locale: string; targetOrigin: string[] }) {
+ this.defaultLocale = locale;
+ this.targetOrigin = targetOrigin;
+
+ this.bindHoverListener = this.bindHoverListener.bind(this);
+ this.bindScrollListener = this.bindScrollListener.bind(this);
+ this.bindMutationListener = this.bindMutationListener.bind(this);
+ this.bindResizeListener = this.bindResizeListener.bind(this);
+
+ this.handleTaggedElement = this.handleTaggedElement.bind(this);
+ this.sendAllElements = this.sendAllElements.bind(this);
+
+ // Attach interaction listeners
+ this.bindHoverListener();
+ this.bindScrollListener();
+ this.bindMutationListener();
+ this.bindResizeListener();
+ }
+
+ // Handles incoming messages from Contentful
+ public receiveMessage(data: MessageFromEditor): void {
+ if (data.method === InspectorModeEventMethods.INSPECTOR_MODE_CHANGED) {
+ const { isInspectorActive } = data as InspectorModeChangedMessage;
+ if (isInspectorActive) {
+ this.sendAllElements();
+ }
+ }
+ }
+
+ /** Checks if the hovered element is an tagged entry and then sends it to the editor */
+ private bindHoverListener() {
+ const onMouseOver = (e: MouseEvent) => {
+ const eventTargets = e.composedPath();
+
+ for (const eventTarget of eventTargets) {
+ const element = eventTarget as HTMLElement;
+ if (element.nodeName === 'BODY') break;
+ if (typeof element?.getAttribute !== 'function') continue;
+
+ if (this.handleTaggedElement(element)) {
+ return;
+ }
+ }
+
+ // Clear if no tagged element is hovered
+ if (this.hoveredElement) {
+ this.hoveredElement = undefined;
+ sendMessageToEditor(
+ InspectorModeEventMethods.MOUSE_MOVE,
+ { element: null },
+ this.targetOrigin
+ );
+ }
+ };
+
+ window.addEventListener('mouseover', onMouseOver);
+
+ return () => window.removeEventListener('mouseover', onMouseOver);
+ }
+
+ /** Sends scroll start and end event to the editor, on end it also sends the tagged elements again */
+ private bindScrollListener() {
+ const onScroll = () => {
+ if (!this.isScrolling) {
+ this.isScrolling = true;
+ sendMessageToEditor(InspectorModeEventMethods.SCROLL_START, {}, this.targetOrigin);
+ }
+
+ if (this.scrollTimeout) {
+ clearTimeout(this.scrollTimeout);
+ }
+
+ this.scrollTimeout = setTimeout(() => {
+ // No longer scrolling, let's update everything
+ this.isScrolling = false;
+ sendMessageToEditor(InspectorModeEventMethods.SCROLL_END, {}, this.targetOrigin);
+ this.sendAllElements();
+ if (this.hoveredElement) {
+ this.handleTaggedElement(this.hoveredElement);
+ }
+ }, 150);
+ };
+
+ window.addEventListener('scroll', onScroll);
+
+ return () => window.removeEventListener('scroll', onScroll);
+ }
+
+ /** Detects DOM changes and sends the tagged elements to the editor */
+ private bindMutationListener() {
+ const mutationObserver = new MutationObserver(() => {
+ const taggedElements = getAllTaggedElements().filter(
+ (el) => !!getInspectorModeAttributes(el)
+ );
+
+ if (this.taggedElements?.length !== taggedElements.length) {
+ this.sendAllElements();
+ }
+ });
+
+ mutationObserver.observe(document.body, {
+ attributes: true,
+ attributeFilter: [
+ InspectorModeDataAttributes.ENTRY_ID,
+ InspectorModeDataAttributes.FIELD_ID,
+ InspectorModeDataAttributes.LOCALE,
+ ],
+ childList: true,
+ subtree: true,
+ });
+
+ return () => mutationObserver.disconnect();
+ }
+
+ /** Sends resize start and end event to the editor, on end it also sends the tagged elements again */
+ private bindResizeListener() {
+ const resizeObserver = new ResizeObserver(() => {
+ if (!this.isResizing) {
+ this.isScrolling = true;
+ sendMessageToEditor(InspectorModeEventMethods.RESIZE_START, {}, this.targetOrigin);
+ }
+
+ if (this.resizeTimeout) {
+ clearTimeout(this.resizeTimeout);
+ }
+
+ this.resizeTimeout = setTimeout(() => {
+ // No longer resizing, let's update everything
+ this.isScrolling = false;
+ sendMessageToEditor(InspectorModeEventMethods.RESIZE_END, {}, this.targetOrigin);
+ this.sendAllElements();
+ if (this.hoveredElement) {
+ this.handleTaggedElement(this.hoveredElement);
+ }
+ }, 150);
+ });
+
+ resizeObserver.observe(document.body);
+
+ return () => resizeObserver.disconnect();
+ }
+
+ /**
+ * Validates if the element has the inspector mode attributes
+ * and sends it then to the editor
+ */
+ private handleTaggedElement(element: HTMLElement): boolean {
+ const taggedInformation = getInspectorModeAttributes(element, this.defaultLocale);
+
+ if (!taggedInformation) {
+ return false;
+ }
+
+ this.hoveredElement = element;
+ sendMessageToEditor(
+ InspectorModeEventMethods.MOUSE_MOVE,
+ {
+ element: {
+ attributes: taggedInformation,
+ coordinates: element.getBoundingClientRect(),
+ },
+ },
+ this.targetOrigin
+ );
+
+ return true;
+ }
+
+ /**
+ * Finds all elements that have all inspector mode attributes
+ * and sends them to the editor
+ */
+ private sendAllElements() {
+ const entries = getAllTaggedElements().filter(
+ (element) => !!getInspectorModeAttributes(element, this.defaultLocale)
+ );
+
+ this.taggedElements = entries;
+
+ sendMessageToEditor(
+ InspectorModeEventMethods.TAGGED_ELEMENTS,
+ {
+ elements: entries.map((e) => ({
+ coordinates: e.getBoundingClientRect(),
+ })),
+ },
+ this.targetOrigin
+ );
+ }
+}
diff --git a/packages/live-preview-sdk/src/inspectorMode/types.ts b/packages/live-preview-sdk/src/inspectorMode/types.ts
new file mode 100644
index 00000000..0b7ae01e
--- /dev/null
+++ b/packages/live-preview-sdk/src/inspectorMode/types.ts
@@ -0,0 +1,49 @@
+import type { SetRequired } from 'type-fest';
+
+export type InspectorModeTags = {
+ [InspectorModeDataAttributes.ENTRY_ID]: string;
+ [InspectorModeDataAttributes.FIELD_ID]: string;
+ [InspectorModeDataAttributes.LOCALE]?: string;
+} | null;
+
+export const enum InspectorModeDataAttributes {
+ FIELD_ID = 'data-contentful-field-id',
+ ENTRY_ID = 'data-contentful-entry-id',
+ LOCALE = 'data-contentful-locale',
+}
+
+export enum InspectorModeEventMethods {
+ MOUSE_MOVE = 'MOUSE_MOVE',
+ SCROLL_START = 'SCROLL_START',
+ SCROLL_END = 'SCROLL_END',
+ RESIZE_START = 'RESIZE_START',
+ RESIZE_END = 'RESIZE_END',
+ TAGGED_ELEMENTS = 'TAGGED_ELEMENTS',
+ INSPECTOR_MODE_CHANGED = 'INSPECTOR_MODE_CHANGED',
+}
+
+export type InspectorModeAttributes = {
+ entryId: string;
+ fieldId: string;
+ locale: string;
+};
+
+export type InspectorModeElement = {
+ attributes?: InspectorModeAttributes | null;
+ coordinates: DOMRect;
+};
+
+export type InspectorModeScrollMessage = Record;
+export type InspectorModeResizeMessage = Record;
+export type InspectorModeMouseMoveMessage = {
+ element: SetRequired | null;
+};
+export type InspectorModeTaggedElementsMessage = {
+ elements: Array;
+};
+
+export type InspectorModeChangedMessage = {
+ /** @deprecated use method instead */
+ action: InspectorModeEventMethods.INSPECTOR_MODE_CHANGED;
+ isInspectorActive: boolean;
+};
diff --git a/packages/live-preview-sdk/src/inspectorMode/utils.ts b/packages/live-preview-sdk/src/inspectorMode/utils.ts
new file mode 100644
index 00000000..7eaf2472
--- /dev/null
+++ b/packages/live-preview-sdk/src/inspectorMode/utils.ts
@@ -0,0 +1,42 @@
+import { InspectorModeAttributes, InspectorModeDataAttributes } from './types';
+
+/**
+ * Parses the necessary information from the element and returns them.
+ * If **one** of the information is missing it returns null
+ */
+export function getInspectorModeAttributes(
+ element: Element,
+ fallbackLocale?: string
+): InspectorModeAttributes | null {
+ const fieldId = element.getAttribute(InspectorModeDataAttributes.FIELD_ID);
+ const entryId = element.getAttribute(InspectorModeDataAttributes.ENTRY_ID);
+ const locale = element.getAttribute(InspectorModeDataAttributes.LOCALE) ?? fallbackLocale;
+
+ if (!fieldId || !entryId || !locale) {
+ return null;
+ }
+
+ return { fieldId, entryId, locale };
+}
+
+/**
+ * Query the document for all tagged elements
+ * **Attention:** Can include elements that have not all attributes,
+ * if you want to have only valid ones check for `getTaggedInformation`
+ */
+export function getAllTaggedElements(): Element[] {
+ return [...document.querySelectorAll(`[${InspectorModeDataAttributes.ENTRY_ID}]`)];
+}
+
+/**
+ * Returns a list of tagged entries on the page
+ */
+export function getAllTaggedEntries(): string[] {
+ return [
+ ...new Set(
+ getAllTaggedElements()
+ .map((element) => element.getAttribute(InspectorModeDataAttributes.ENTRY_ID))
+ .filter(Boolean) as string[]
+ ),
+ ];
+}
diff --git a/packages/live-preview-sdk/src/messages.ts b/packages/live-preview-sdk/src/messages.ts
index 8bad2f64..3cfaa7b2 100644
--- a/packages/live-preview-sdk/src/messages.ts
+++ b/packages/live-preview-sdk/src/messages.ts
@@ -5,6 +5,15 @@ import type { SysLink } from 'contentful-management';
import type { LIVE_PREVIEW_EDITOR_SOURCE, LIVE_PREVIEW_SDK_SOURCE } from './constants';
import { sendMessageToEditor } from './helpers';
+import {
+ InspectorModeAttributes,
+ InspectorModeChangedMessage,
+ InspectorModeEventMethods,
+ InspectorModeMouseMoveMessage,
+ InspectorModeResizeMessage,
+ InspectorModeScrollMessage,
+ InspectorModeTaggedElementsMessage,
+} from './inspectorMode/types';
import type { ContentType, EntityReferenceMap } from './types';
enum LivePreviewPostMessageMethods {
@@ -21,7 +30,6 @@ enum LivePreviewPostMessageMethods {
ENTRY_UPDATED = 'ENTRY_UPDATED',
ENTRY_SAVED = 'ENTRY_SAVED',
- INSPECTOR_MODE_CHANGED = 'INSPECTOR_MODE_CHANGED',
DEBUG_MODE_ENABLED = 'DEBUG_MODE_ENABLED',
/**
@@ -39,8 +47,13 @@ export {
LivePreviewPostMessageMethods,
RequestEntitiesMessage,
RequestedEntitiesMessage,
+ InspectorModeEventMethods,
};
-export type PostMessageMethods = LivePreviewPostMessageMethods | StorePostMessageMethods;
+
+export type PostMessageMethods =
+ | LivePreviewPostMessageMethods
+ | StorePostMessageMethods
+ | InspectorModeEventMethods;
export type ConnectedMessage = {
/** @deprecated use method instead */
@@ -57,10 +70,7 @@ export type ConnectedMessage = {
export type TaggedFieldClickMessage = {
/** @deprecated use method instead */
action: LivePreviewPostMessageMethods.TAGGED_FIELD_CLICKED;
- fieldId: string;
- entryId: string;
- locale: string;
-};
+} & InspectorModeAttributes;
/** @deprecated use RequestEntitiesMessage instead */
export type UnknownEntityMessage = {
@@ -101,7 +111,11 @@ export type EditorMessage =
| UrlChangedMessage
| SubscribedMessage
| RequestEntitiesMessage
- | ErrorMessage;
+ | ErrorMessage
+ | InspectorModeMouseMoveMessage
+ | InspectorModeScrollMessage
+ | InspectorModeResizeMessage
+ | InspectorModeTaggedElementsMessage;
export type MessageFromSDK = EditorMessage & {
method: PostMessageMethods;
@@ -137,12 +151,6 @@ export type UnknownReferenceLoaded = {
entityReferenceMap: EntityReferenceMap;
};
-export type InspectorModeChangedMessage = {
- /** @deprecated use method instead */
- action: LivePreviewPostMessageMethods.INSPECTOR_MODE_CHANGED;
- isInspectorActive: boolean;
-};
-
export type DebugModeEnabledMessage = {
/** @deprecated use method instead */
action: LivePreviewPostMessageMethods.DEBUG_MODE_ENABLED;
diff --git a/packages/live-preview-sdk/src/react.tsx b/packages/live-preview-sdk/src/react.tsx
index e9cf7622..8eaede20 100644
--- a/packages/live-preview-sdk/src/react.tsx
+++ b/packages/live-preview-sdk/src/react.tsx
@@ -17,7 +17,8 @@ import isEqual from 'lodash.isequal';
import { debounce } from './helpers';
import { ContentfulLivePreview, ContentfulLivePreviewInitConfig } from './index';
-import { Argument, InspectorModeTags, LivePreviewProps } from './types';
+import type { InspectorModeTags } from './inspectorMode/types';
+import { Argument, LivePreviewProps } from './types';
type UseEffectParams = Parameters;
type EffectCallback = UseEffectParams[0];
diff --git a/packages/live-preview-sdk/src/saveEvent.ts b/packages/live-preview-sdk/src/saveEvent.ts
index b1413df9..b225caf7 100644
--- a/packages/live-preview-sdk/src/saveEvent.ts
+++ b/packages/live-preview-sdk/src/saveEvent.ts
@@ -1,5 +1,5 @@
-import { getAllTaggedEntries } from './fieldTaggingUtils';
import { debug } from './helpers';
+import { getAllTaggedEntries } from './inspectorMode/utils';
import { EntrySavedMessage, LivePreviewPostMessageMethods, MessageFromEditor } from './messages';
import { SubscribeCallback } from './types';
diff --git a/packages/live-preview-sdk/src/styles.css b/packages/live-preview-sdk/src/styles.css
index 2e9da646..ec8f8ee2 100644
--- a/packages/live-preview-sdk/src/styles.css
+++ b/packages/live-preview-sdk/src/styles.css
@@ -1,50 +1 @@
-[data-contentful-field-id][data-contentful-entry-id] {
- outline: 1px dashed rgba(64, 160, 255, 0) !important;
- transition: outline-color 0.3s ease-in-out;
-}
-
-.contentful-inspector--active [data-contentful-field-id][data-contentful-entry-id] {
- outline: 1px dashed rgba(64, 160, 255, 1) !important;
-}
-
-.contentful-inspector--active [data-contentful-field-id][data-contentful-entry-id]:hover {
- outline: 2px solid rgba(64, 160, 255, 1) !important;
-}
-
-button.contentful-tooltip {
- padding: 0;
- display: none;
- outline: none;
- border: none;
- z-index: 999999 !important;
- position: fixed;
- margin: 0;
- height: 32px;
- width: 72px;
- background: rgb(3, 111, 227);
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
- font-weight: 500 !important;
- font-size: 14px !important;
- color: #ffffff !important;
- transition: background 0.2s;
- text-align: center !important;
- border-radius: 6px !important;
- justify-content: center;
- align-items: center;
- box-shadow: 0px 1px 0px rgba(17, 27, 43, 0.05);
- box-sizing: border-box;
- cursor: pointer;
- gap: 6px;
-}
-
-button.contentful-tooltip:hover {
- background: rgb(0, 89, 200);
-}
-
-button.contentful-tooltip:active:hover {
- background: rgb(0, 65, 171);
-}
-
-.contentful-inspector--active button.contentful-tooltip {
- display: flex;
-}
+/* TODO: remove with next breaking version */
diff --git a/packages/live-preview-sdk/src/types.ts b/packages/live-preview-sdk/src/types.ts
index 8da34e43..61d17e0b 100644
--- a/packages/live-preview-sdk/src/types.ts
+++ b/packages/live-preview-sdk/src/types.ts
@@ -12,18 +12,6 @@ export type LivePreviewProps = {
locale?: string;
};
-export const enum TagAttributes {
- FIELD_ID = 'data-contentful-field-id',
- ENTRY_ID = 'data-contentful-entry-id',
- LOCALE = 'data-contentful-locale',
-}
-
-export type InspectorModeTags = {
- [TagAttributes.ENTRY_ID]: string;
- [TagAttributes.FIELD_ID]: string;
- [TagAttributes.LOCALE]?: string;
-} | null;
-
export interface SysProps {
id: string;
[key: string]: unknown;