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/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
index f3a4e6cf..74f3ca74 100644
--- a/packages/live-preview-sdk/src/fieldTaggingUtils.ts
+++ b/packages/live-preview-sdk/src/fieldTaggingUtils.ts
@@ -1,12 +1,40 @@
import { TagAttributes } from './types';
+/**
+ * Parses the necessary information from the element and returns them.
+ * If **one** of the information is missing it returns null
+ */
+export function getTaggedInformation(
+ element: Element,
+ fallbackLocale?: string
+): { fieldId: string; entryId: string; locale: string } | null {
+ const fieldId = element.getAttribute(TagAttributes.FIELD_ID);
+ const entryId = element.getAttribute(TagAttributes.ENTRY_ID);
+ const locale = element.getAttribute(TagAttributes.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(`[${TagAttributes.ENTRY_ID}]`)];
+}
+
/**
* Returns a list of tagged entries on the page
*/
export function getAllTaggedEntries(): string[] {
return [
...new Set(
- [...document.querySelectorAll(`[${TagAttributes.ENTRY_ID}]`)]
+ getAllTaggedElements()
.map((element) => element.getAttribute(TagAttributes.ENTRY_ID))
.filter(Boolean) as string[]
),
diff --git a/packages/live-preview-sdk/src/index.ts b/packages/live-preview-sdk/src/index.ts
index fd1c7f31..c000019f 100644
--- a/packages/live-preview-sdk/src/index.ts
+++ b/packages/live-preview-sdk/src/index.ts
@@ -1,5 +1,3 @@
-import './styles.css';
-
import { type DocumentNode } from 'graphql';
import { version } from '../package.json';
diff --git a/packages/live-preview-sdk/src/inspectorMode.ts b/packages/live-preview-sdk/src/inspectorMode.ts
index 87b161bf..67d0aa4b 100644
--- a/packages/live-preview-sdk/src/inspectorMode.ts
+++ b/packages/live-preview-sdk/src/inspectorMode.ts
@@ -1,81 +1,95 @@
-import {
- DATA_CURR_ENTRY_ID,
- DATA_CURR_FIELD_ID,
- DATA_CURR_LOCALE,
- TOOLTIP_CLASS,
- TOOLTIP_HEIGHT,
- TOOLTIP_PADDING_LEFT,
-} from './constants';
+import { getAllTaggedElements, getTaggedInformation } from './fieldTaggingUtils';
+import { sendMessageToEditor } from './helpers';
import {
InspectorModeChangedMessage,
LivePreviewPostMessageMethods,
MessageFromEditor,
- openEntryInEditorUtility,
+ InteractionEventMethods,
} 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[];
+ private isScrolling: boolean = false;
+ private isResizing: boolean = false;
+ private scrollTimeout?: NodeJS.Timeout;
+ private resizeTimeout?: NodeJS.Timeout;
+ private hoveredElement?: HTMLElement;
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);
+ // TODO: we we need this?
+ this.onMouseOver = this.onMouseOver.bind(this);
+ this.onScroll = this.onScroll.bind(this);
+ this.onResize = this.onResize.bind(this);
+ this.handleElementInteraction = this.handleElementInteraction.bind(this);
+ this.sendAllElements = this.sendAllElements.bind(this);
+
+ // TODO: on resize do the something similar as onScroll
+ window.addEventListener('scroll', this.onScroll);
+ window.addEventListener('resize', this.onResize);
+ window.addEventListener('mouseover', this.onMouseOver);
}
// 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
- ) {
+ if (data.method === LivePreviewPostMessageMethods.INSPECTOR_MODE_CHANGED) {
+ const { isInspectorActive } = data as InspectorModeChangedMessage;
// 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
- );
+ document.body.classList.toggle('contentful-inspector--active', isInspectorActive);
+
+ if (isInspectorActive) {
+ this.sendAllElements();
+ }
}
}
- // 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();
+ // TODO: onResize and onScroll are quite similar, can we use a factory to set it up
+ private onResize() {
+ if (!this.isResizing) {
+ this.isScrolling = true;
+ sendMessageToEditor(InteractionEventMethods.RESIZE_START, {} as any, this.targetOrigin);
+ }
- if (currentRectOfElement && currentRectOfParentOfElement) {
- let upperBoundOfTooltip = currentRectOfElement.top - TOOLTIP_HEIGHT;
- const left = currentRectOfElement.left - TOOLTIP_PADDING_LEFT;
+ if (this.resizeTimeout) {
+ clearTimeout(this.resizeTimeout);
+ }
- if (upperBoundOfTooltip < 0) {
- if (currentRectOfElement.top < 0) upperBoundOfTooltip = currentRectOfElement.top;
- else upperBoundOfTooltip = 0;
+ this.resizeTimeout = setTimeout(() => {
+ // No longer resizing, let's update everything
+ this.isScrolling = false;
+ sendMessageToEditor(InteractionEventMethods.RESIZE_END, {} as any, this.targetOrigin);
+ this.sendAllElements();
+ if (this.hoveredElement) {
+ this.handleElementInteraction(this.hoveredElement);
}
+ }, 150);
+ }
- this.tooltip.style.top = upperBoundOfTooltip + 'px';
- this.tooltip.style.left = left + 'px';
+ private onScroll() {
+ if (!this.isScrolling) {
+ this.isScrolling = true;
+ sendMessageToEditor(InteractionEventMethods.SCROLL_START, {} as any, this.targetOrigin);
+ }
- return true;
+ if (this.scrollTimeout) {
+ clearTimeout(this.scrollTimeout);
}
- return false;
+ this.scrollTimeout = setTimeout(() => {
+ // No longer scrolling, let's update everything
+ this.isScrolling = false;
+ sendMessageToEditor(InteractionEventMethods.SCROLL_END, {} as any, this.targetOrigin);
+ this.sendAllElements();
+ if (this.hoveredElement) {
+ this.handleElementInteraction(this.hoveredElement);
+ }
+ }, 150);
}
- private addTooltipOnHover(e: MouseEvent) {
+ private onMouseOver(e: MouseEvent) {
const eventTargets = e.composedPath();
for (const eventTarget of eventTargets) {
@@ -83,46 +97,57 @@ export class InspectorMode {
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;
+ if (this.handleElementInteraction(element)) {
+ return;
}
}
- }
- 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();
+ // Clear if no tagged element is hovered
+ this.hoveredElement = undefined;
+ sendMessageToEditor(
+ InteractionEventMethods.MOUSE_MOVE,
+ {
+ hoveredElement: null,
+ coordinates: null,
+ } as any,
+ this.targetOrigin
+ );
}
- // 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;
+ private handleElementInteraction(element: HTMLElement): boolean {
+ const taggedInformation = getTaggedInformation(element, this.defaultLocale);
- if (fieldId && entryId && locale) {
- openEntryInEditorUtility(fieldId, entryId, locale, this.targetOrigin);
+ if (!taggedInformation) {
+ return false;
}
+
+ this.hoveredElement = element;
+ sendMessageToEditor(
+ InteractionEventMethods.MOUSE_MOVE,
+ {
+ hoveredElement: taggedInformation,
+ coordinates: element.getBoundingClientRect(),
+ } as any,
+ this.targetOrigin
+ );
+
+ return true;
+ }
+
+ private sendAllElements() {
+ const entries = getAllTaggedElements().filter(
+ (element) => !!getTaggedInformation(element, this.defaultLocale)
+ );
+
+ // FIXME: typing
+ sendMessageToEditor(
+ 'TAGGED_ELEMENTS' as any,
+ {
+ elements: entries.map((e) => ({
+ coordinates: e.getBoundingClientRect(),
+ })),
+ } as any,
+ this.targetOrigin
+ );
}
}
diff --git a/packages/live-preview-sdk/src/messages.ts b/packages/live-preview-sdk/src/messages.ts
index 8bad2f64..bffabc15 100644
--- a/packages/live-preview-sdk/src/messages.ts
+++ b/packages/live-preview-sdk/src/messages.ts
@@ -7,6 +7,14 @@ import type { LIVE_PREVIEW_EDITOR_SOURCE, LIVE_PREVIEW_SDK_SOURCE } from './cons
import { sendMessageToEditor } from './helpers';
import type { ContentType, EntityReferenceMap } from './types';
+enum InteractionEventMethods {
+ MOUSE_MOVE = 'MOUSE_MOVE',
+ SCROLL_START = 'SCROLL_START',
+ SCROLL_END = 'SCROLL_END',
+ RESIZE_START = 'RESIZE_START',
+ RESIZE_END = 'RESIZE_END',
+}
+
enum LivePreviewPostMessageMethods {
CONNECTED = 'CONNECTED',
DISCONNECTED = 'DISCONNECTED',
@@ -39,8 +47,12 @@ export {
LivePreviewPostMessageMethods,
RequestEntitiesMessage,
RequestedEntitiesMessage,
+ InteractionEventMethods,
};
-export type PostMessageMethods = LivePreviewPostMessageMethods | StorePostMessageMethods;
+export type PostMessageMethods =
+ | LivePreviewPostMessageMethods
+ | StorePostMessageMethods
+ | InteractionEventMethods;
export type ConnectedMessage = {
/** @deprecated use method instead */
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 */