Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 35 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,23 +1,57 @@
# Changelog

## [v4.5.0](https://github.com/contentstack/live-preview-sdk/compare/v4.4.5...v4.5.0)

> 5 August 2026

### New Features

- feat(VB-2055): add isVisualEditorEditing helper (Karan Bhavesh Gandhi - [#629](https://github.com/contentstack/live-preview-sdk/pull/629))

### Fixes

- fix: isolate goober pragma from host app to prevent react-hot-toast crash (Hitesh Shetty - [#630](https://github.com/contentstack/live-preview-sdk/pull/630))
- fix(VB-1797): match caret direction to text when inline editing (Karan Bhavesh Gandhi - [#620](https://github.com/contentstack/live-preview-sdk/pull/620))

### General Changes

- develop to stage 6 aug release (Karan Bhavesh Gandhi - [#631](https://github.com/contentstack/live-preview-sdk/pull/631))

### Fixes

- fix(VB-1797): keep dir=auto working on the pseudo editable element (Karan Gandhi - [a06ad34](https://github.com/contentstack/live-preview-sdk/commit/a06ad3403106cad06d899034f76d2d9a180b7460))

### General Changes

- sca-scan.yml (Aravind Kumar - [9852a61](https://github.com/contentstack/live-preview-sdk/commit/9852a61b443e8fd75cef3fedf95f1a72b648b874))
- sca-scan.yml (Aravind Kumar - [bf89762](https://github.com/contentstack/live-preview-sdk/commit/bf89762514dd27ac41f331e6f02aa3b3d5067ea2))
- sca-scan.yml (Aravind Kumar - [0285857](https://github.com/contentstack/live-preview-sdk/commit/0285857239cadd075d42d09b2975d4e2e8f88f55))

## [v4.4.5](https://github.com/contentstack/live-preview-sdk/compare/v4.4.4...v4.4.5)

> 13 July 2026
> 21 July 2026

### Fixes

- fix(security): bump dompurify to patch XSS and prototype pollution issues (Hitesh Shetty - [#622](https://github.com/contentstack/live-preview-sdk/pull/622))
- fix(VB-1820): navigate on alt+click of in-iframe links instead of swallowing the click (Shivam Mishra - [#618](https://github.com/contentstack/live-preview-sdk/pull/618))
- fix(visual-builder): keep overlayPropagation from piercing the SDK's own toolbar (Hitesh Shetty - [#617](https://github.com/contentstack/live-preview-sdk/pull/617))
- fix(VP-2254): keep cursor visible over RTE links in Visual Builder (Shivam Mishra - [#616](https://github.com/contentstack/live-preview-sdk/pull/616))

### Chores And Housekeeping

- chore: update CODEOWNERS to visual-preview-developers (Hitesh Shetty - [#623](https://github.com/contentstack/live-preview-sdk/pull/623))

### General Changes

- Release v4.4.5 (Hitesh Shetty - [#624](https://github.com/contentstack/live-preview-sdk/pull/624))
- release: promote develop_v4 to stage_v4 (Hitesh Shetty - [#619](https://github.com/contentstack/live-preview-sdk/pull/619))

### Fixes

- fix(VP-2254): keep cursor visible over RTE links in Visual Builder and patch npm audit vulnerabilities (shivamfl - [5650527](https://github.com/contentstack/live-preview-sdk/commit/5650527aa43edd6c4b9fcd00a3ea76329934b62a))
- fix(VB-1820): restrict alt+click navigation to safe url schemes, fix leaked test spy (shivamfl - [f856b0a](https://github.com/contentstack/live-preview-sdk/commit/f856b0a52c5e6d7ee42bee04fe61e5f22abe0058))
- fix: update CDN version to 4.4.5 in README (hitesh-shetty-cstk - [9494423](https://github.com/contentstack/live-preview-sdk/commit/9494423085222a2c6b69346631912129819d84ec))

### Refactoring and Updates

Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,11 @@ npm install @contentstack/live-preview-utils

### Load from a CDN (advanced)

Pin the version to match your app (update `4.4.5` when you upgrade):
Pin the version to match your app (update `4.5.0` when you upgrade):

```html
<script type="module" crossorigin="anonymous">
import ContentstackLivePreview from "https://esm.sh/@contentstack/live-preview-utils@4.4.5";
import ContentstackLivePreview from "https://esm.sh/@contentstack/live-preview-utils@4.5.0";

ContentstackLivePreview.init({
stackDetails: {
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@contentstack/live-preview-utils",
"version": "4.4.5",
"version": "4.5.0",
"description": "Contentstack provides the Live Preview SDK to establish a communication channel between the various Contentstack SDKs and your website, transmitting live changes to the preview pane.",
"type": "module",
"types": "dist/legacy/index.d.ts",
Expand Down
4 changes: 3 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import LightLivePreviewHoC from "./light-sdk";

export type IStackSdk = ExternalStackSdkType;


const ContentstackLivePreview =
typeof process !== "undefined" &&
(process?.env?.PURGE_PREVIEW_SDK === "true" ||
Expand All @@ -14,4 +13,7 @@ const ContentstackLivePreview =
: ContentstackLivePreviewHOC;

export const VB_EmptyBlockParentClass = "visual-builder__empty-block-parent";

export { isVisualEditorEditing } from "./visualBuilder/utils/editingState";

export default ContentstackLivePreview;
5 changes: 0 additions & 5 deletions src/visualBuilder/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,7 @@ import { resolvePageContext } from "./utils/resolvePageContext";
import visualBuilderPostMessage from "./utils/visualBuilderPostMessage";
import { VisualBuilderPostMessageEvents } from "./utils/types/postMessage.types";

import { setup } from "goober";
import { debounce, isEqual } from "lodash-es";
import { h } from "preact";
import { extractDetailsFromCslp, isValidCslp } from "../cslp";
import initUI from "./components";
import { useDraftFieldsPostMessageEvent } from "./eventManager/useDraftFieldsPostMessageEvent";
Expand Down Expand Up @@ -289,9 +287,6 @@ export class VisualBuilder {
resizeObserver: this.resizeObserver,
});

// Initializing goober for css-in-js
setup(h);

this.visualBuilderContainer = document.querySelector(
".visual-builder__container"
);
Expand Down
60 changes: 60 additions & 0 deletions src/visualBuilder/utils/__test__/editingState.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import { isVisualEditorEditing } from "../editingState";

const ATTR = "data-cslp-field-type";

describe("isVisualEditorEditing", () => {
let el: HTMLElement;

beforeEach(() => {
document.body.innerHTML = "";
el = document.createElement("div");
document.body.appendChild(el);
});

it("returns false when nothing is being edited", () => {
expect(isVisualEditorEditing()).toBe(false);
expect(isVisualEditorEditing(el)).toBe(false);
});

it("returns true when a descendant of the element is being edited", () => {
const child = document.createElement("span");
el.appendChild(child);
child.setAttribute(ATTR, "singleline");

expect(isVisualEditorEditing(el)).toBe(true);
});

it("returns true when the element itself carries the attribute", () => {
el.setAttribute(ATTR, "singleline");
expect(isVisualEditorEditing(el)).toBe(true);
});

it("scopes the check to the given element", () => {
const other = document.createElement("div");
document.body.appendChild(other);
const child = document.createElement("span");
other.appendChild(child);
child.setAttribute(ATTR, "singleline");

expect(isVisualEditorEditing(el)).toBe(false);
expect(isVisualEditorEditing(other)).toBe(true);
});

it("checks the whole document when no element is passed", () => {
expect(isVisualEditorEditing()).toBe(false);
el.setAttribute(ATTR, "singleline");
expect(isVisualEditorEditing()).toBe(true);
});

describe("without a DOM (SSR)", () => {
afterEach(() => {
vi.unstubAllGlobals();
});

it("returns false", () => {
vi.stubGlobal("document", undefined);
expect(isVisualEditorEditing()).toBe(false);
});
});
});
33 changes: 33 additions & 0 deletions src/visualBuilder/utils/__test__/enableInlineEditing.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,39 @@ describe("enableInlineEditing", () => {
expect(editableElement.focus).toHaveBeenCalled();
});

it("should set dir=auto so the caret follows the text direction", () => {
enableInlineEditing({
expectedFieldData: "Test content",
editableElement,
fieldType: FieldDataType.SINGLELINE,
elements: {
visualBuilderContainer,
resizeObserver,
lastEditedField: null,
},
});

expect(editableElement.getAttribute("dir")).toBe("auto");
});

it("should set dir=auto on the pseudo element when one is created", () => {
// Content differs from expected, so a pseudo editable element is used.
enableInlineEditing({
expectedFieldData: "Different content",
editableElement,
fieldType: FieldDataType.SINGLELINE,
elements: {
visualBuilderContainer,
resizeObserver,
lastEditedField: null,
},
});

const pseudoElement =
generatePseudoEditableElement.mock.results[0].value;
expect(pseudoElement.getAttribute("dir")).toBe("auto");
});

it("should handle multiline fields correctly", () => {
enableInlineEditing({
expectedFieldData: "Test content",
Expand Down
10 changes: 10 additions & 0 deletions src/visualBuilder/utils/__test__/getStyleOfAnElement.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,14 @@ describe("getStyleOfAnElement", () => {
width: "100px",
});
});

test("it should not copy direction/unicode-bidi so dir=auto can govern", () => {
const elem = document.createElement("div");
elem.style.direction = "rtl";
elem.style.unicodeBidi = "isolate";

const style = getStyleOfAnElement(elem);
expect(style).not.toHaveProperty("direction");
expect(style).not.toHaveProperty("unicode-bidi");
});
});
22 changes: 22 additions & 0 deletions src/visualBuilder/utils/editingState.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { VISUAL_BUILDER_FIELD_TYPE_ATTRIBUTE_KEY } from "./constants";

const EDITING_SELECTOR = `[${VISUAL_BUILDER_FIELD_TYPE_ATTRIBUTE_KEY}]`;

/**
* Returns whether a field inside `element` is being edited in Visual Editor.
* Pass the element wrapping your self-updating content to pause it while true.
* Omit `element` to check the whole document. Returns `false` during SSR.
*/
export function isVisualEditorEditing(element?: HTMLElement | null): boolean {
if (typeof document === "undefined") return false;

if (element) {
return (
(typeof element.matches === "function" &&
element.matches(EDITING_SELECTOR)) ||
element.querySelector(EDITING_SELECTOR) !== null
);
}

return document.querySelector(EDITING_SELECTOR) !== null;
}
2 changes: 2 additions & 0 deletions src/visualBuilder/utils/enableInlineEditing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ export function enableInlineEditing({
}

actualEditableField.setAttribute("contenteditable", "true");
// caret follows the text direction (fixes LTR caret on RTL content)
actualEditableField.setAttribute("dir", "auto");
actualEditableField.addEventListener("input", handleFieldInput);
actualEditableField.addEventListener("keydown", handleFieldKeyDown);

Expand Down
3 changes: 3 additions & 0 deletions src/visualBuilder/utils/getStyleOfAnElement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ export default function getStyleOfAnElement(element: HTMLElement): {
"margin-bottom",
"-webkit-user-modify",
"cursor",
// let the pseudo element's dir="auto" decide direction, not inline copies
"direction",
"unicode-bidi",
];

const styles: { [key: string]: string } = {};
Expand Down
1 change: 1 addition & 0 deletions src/visualBuilder/utils/handleIndividualFields.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ export function cleanIndividualFieldResidual(elements: {
VISUAL_BUILDER_FIELD_TYPE_ATTRIBUTE_KEY
);
previousSelectedEditableDOM.removeAttribute("contenteditable");
previousSelectedEditableDOM.removeAttribute("dir");
previousSelectedEditableDOM.removeEventListener(
"input",
handleFieldInput
Expand Down
Loading