From 23dcbe4e7e84497cb67f9b940f516a856d877e91 Mon Sep 17 00:00:00 2001 From: Karan Gandhi Date: Tue, 28 Jul 2026 16:18:53 +0530 Subject: [PATCH] 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; +}