From e7d5702847205e5bc02b7ffd9c74cdc8a9e0609d Mon Sep 17 00:00:00 2001 From: Karan Gandhi Date: Tue, 14 Jul 2026 17:52:59 +0530 Subject: [PATCH 1/6] fix(VB-1797): match caret direction to text when inline editing Inline-editable fields kept an LTR caret even when the content was right-to-left, so the caret ignored where RTL text (Arabic, Hebrew) was being inserted. Set dir="auto" on the editable element so the browser derives writing direction from the content, and clear it on teardown. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../utils/__test__/enableInlineEditing.test.ts | 15 +++++++++++++++ src/visualBuilder/utils/enableInlineEditing.ts | 3 +++ src/visualBuilder/utils/handleIndividualFields.ts | 1 + 3 files changed, 19 insertions(+) diff --git a/src/visualBuilder/utils/__test__/enableInlineEditing.test.ts b/src/visualBuilder/utils/__test__/enableInlineEditing.test.ts index c8565a53..b2b7ef6d 100644 --- a/src/visualBuilder/utils/__test__/enableInlineEditing.test.ts +++ b/src/visualBuilder/utils/__test__/enableInlineEditing.test.ts @@ -127,6 +127,21 @@ 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 handle multiline fields correctly", () => { enableInlineEditing({ expectedFieldData: "Test content", diff --git a/src/visualBuilder/utils/enableInlineEditing.ts b/src/visualBuilder/utils/enableInlineEditing.ts index ea020a92..b9f396cd 100644 --- a/src/visualBuilder/utils/enableInlineEditing.ts +++ b/src/visualBuilder/utils/enableInlineEditing.ts @@ -92,6 +92,9 @@ export function enableInlineEditing({ } actualEditableField.setAttribute("contenteditable", "true"); + // Let the caret follow the text's own direction so RTL content (e.g. + // Arabic, Hebrew) reads naturally instead of keeping an LTR caret. + actualEditableField.setAttribute("dir", "auto"); actualEditableField.addEventListener("input", handleFieldInput); actualEditableField.addEventListener("keydown", handleFieldKeyDown); diff --git a/src/visualBuilder/utils/handleIndividualFields.ts b/src/visualBuilder/utils/handleIndividualFields.ts index 3ddb6763..52f27869 100644 --- a/src/visualBuilder/utils/handleIndividualFields.ts +++ b/src/visualBuilder/utils/handleIndividualFields.ts @@ -133,6 +133,7 @@ export function cleanIndividualFieldResidual(elements: { VISUAL_BUILDER_FIELD_TYPE_ATTRIBUTE_KEY ); previousSelectedEditableDOM.removeAttribute("contenteditable"); + previousSelectedEditableDOM.removeAttribute("dir"); previousSelectedEditableDOM.removeEventListener( "input", handleFieldInput From a06ad3403106cad06d899034f76d2d9a180b7460 Mon Sep 17 00:00:00 2001 From: Karan Gandhi Date: Thu, 16 Jul 2026 17:28:21 +0530 Subject: [PATCH 2/6] fix(VB-1797): keep dir=auto working on the pseudo editable element The pseudo element used for truncated/mismatched fields copies the source element's computed direction/unicode-bidi inline, which overrode dir="auto" and left an LTR caret on RTL content. Exclude both from the copied styles so the attribute governs direction on that path too. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../utils/__test__/enableInlineEditing.test.ts | 18 ++++++++++++++++++ .../utils/__test__/getStyleOfAnElement.test.ts | 10 ++++++++++ src/visualBuilder/utils/enableInlineEditing.ts | 3 +-- src/visualBuilder/utils/getStyleOfAnElement.ts | 3 +++ 4 files changed, 32 insertions(+), 2 deletions(-) diff --git a/src/visualBuilder/utils/__test__/enableInlineEditing.test.ts b/src/visualBuilder/utils/__test__/enableInlineEditing.test.ts index b2b7ef6d..2bee0a0e 100644 --- a/src/visualBuilder/utils/__test__/enableInlineEditing.test.ts +++ b/src/visualBuilder/utils/__test__/enableInlineEditing.test.ts @@ -142,6 +142,24 @@ describe("enableInlineEditing", () => { 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", diff --git a/src/visualBuilder/utils/__test__/getStyleOfAnElement.test.ts b/src/visualBuilder/utils/__test__/getStyleOfAnElement.test.ts index bc449ba8..22decf8f 100644 --- a/src/visualBuilder/utils/__test__/getStyleOfAnElement.test.ts +++ b/src/visualBuilder/utils/__test__/getStyleOfAnElement.test.ts @@ -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"); + }); }); diff --git a/src/visualBuilder/utils/enableInlineEditing.ts b/src/visualBuilder/utils/enableInlineEditing.ts index b9f396cd..92563775 100644 --- a/src/visualBuilder/utils/enableInlineEditing.ts +++ b/src/visualBuilder/utils/enableInlineEditing.ts @@ -92,8 +92,7 @@ export function enableInlineEditing({ } actualEditableField.setAttribute("contenteditable", "true"); - // Let the caret follow the text's own direction so RTL content (e.g. - // Arabic, Hebrew) reads naturally instead of keeping an LTR caret. + // caret follows the text direction (fixes LTR caret on RTL content) actualEditableField.setAttribute("dir", "auto"); actualEditableField.addEventListener("input", handleFieldInput); actualEditableField.addEventListener("keydown", handleFieldKeyDown); diff --git a/src/visualBuilder/utils/getStyleOfAnElement.ts b/src/visualBuilder/utils/getStyleOfAnElement.ts index d70a78b0..4451e249 100644 --- a/src/visualBuilder/utils/getStyleOfAnElement.ts +++ b/src/visualBuilder/utils/getStyleOfAnElement.ts @@ -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 } = {}; From 23dcbe4e7e84497cb67f9b940f516a856d877e91 Mon Sep 17 00:00:00 2001 From: Karan Gandhi Date: Tue, 28 Jul 2026 16:18:53 +0530 Subject: [PATCH 3/6] feat(VB-2055): add isVisualEditorEditing helper Expose isVisualEditorEditing(element) so a site can pause self-updating content (CSS animations, carousels, polled/streamed data) while a field is being edited in Visual Editor. It reads the existing data-cslp-field-type marker the SDK already sets on the focused element, is side-effect-free, and returns false during SSR. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/index.ts | 4 +- .../utils/__test__/editingState.test.ts | 60 +++++++++++++++++++ src/visualBuilder/utils/editingState.ts | 22 +++++++ 3 files changed, 85 insertions(+), 1 deletion(-) create mode 100644 src/visualBuilder/utils/__test__/editingState.test.ts create mode 100644 src/visualBuilder/utils/editingState.ts diff --git a/src/index.ts b/src/index.ts index 45074abc..88c9be14 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,7 +5,6 @@ import LightLivePreviewHoC from "./light-sdk"; export type IStackSdk = ExternalStackSdkType; - const ContentstackLivePreview = typeof process !== "undefined" && (process?.env?.PURGE_PREVIEW_SDK === "true" || @@ -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; diff --git a/src/visualBuilder/utils/__test__/editingState.test.ts b/src/visualBuilder/utils/__test__/editingState.test.ts new file mode 100644 index 00000000..2f53699a --- /dev/null +++ b/src/visualBuilder/utils/__test__/editingState.test.ts @@ -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); + }); + }); +}); diff --git a/src/visualBuilder/utils/editingState.ts b/src/visualBuilder/utils/editingState.ts new file mode 100644 index 00000000..675f2ba7 --- /dev/null +++ b/src/visualBuilder/utils/editingState.ts @@ -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; +} From e44b9e5d6715f3ab4f7ee12bc4941acb8e20c04e Mon Sep 17 00:00:00 2001 From: hitesh-shetty-cstk Date: Wed, 29 Jul 2026 15:10:47 +0530 Subject: [PATCH 4/6] fix: isolate goober pragma from host app to prevent react-hot-toast crash VisualBuilder called goober's setup(h) with Preact's h at init, which mutates goober's shared global pragma. A host app that also uses a goober-backed library such as react-hot-toast then renders through that pragma, emitting Preact elements into a React 19 tree and crashing with "A React Element from an older version of React was rendered." The SDK only uses goober's css/glob/keyframes, none of which read the pragma, and never uses styled (the sole pragma consumer). setup(h) was therefore inert for the SDK and only leaked into the host. Removing it restores the pragma-free behavior the SDK shipped with before the call was introduced. Co-Authored-By: Claude --- src/visualBuilder/index.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/visualBuilder/index.ts b/src/visualBuilder/index.ts index f484f1a2..3fb234dc 100644 --- a/src/visualBuilder/index.ts +++ b/src/visualBuilder/index.ts @@ -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"; @@ -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" ); From 1822f43232bab919bd8ea0a2395554200675e681 Mon Sep 17 00:00:00 2001 From: Karan Gandhi Date: Wed, 5 Aug 2026 16:40:09 +0530 Subject: [PATCH 5/6] 4.5.0 Co-Authored-By: Claude --- CHANGELOG.md | 36 +++++++++++++++++++++++++++++++++++- package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 38 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6379eec0..18eae937 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/package-lock.json b/package-lock.json index 569f841e..d5a874a6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@contentstack/live-preview-utils", - "version": "4.4.5", + "version": "4.5.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@contentstack/live-preview-utils", - "version": "4.4.5", + "version": "4.5.0", "license": "MIT", "dependencies": { "@floating-ui/dom": "^1.7.2", diff --git a/package.json b/package.json index 45434265..6db223ab 100644 --- a/package.json +++ b/package.json @@ -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", From 76f5ca73387965c74b54bf032ea45a8d2ca1bf82 Mon Sep 17 00:00:00 2001 From: Karan Gandhi Date: Wed, 5 Aug 2026 17:14:44 +0530 Subject: [PATCH 6/6] docs: bump README CDN URL to 4.5.0 Co-Authored-By: Claude --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 489cc89f..0acab9b5 100644 --- a/README.md +++ b/README.md @@ -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