diff --git a/README.md b/README.md index c6c35e01..0a95348b 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ ContentstackLivePreview.init({ MIT License -Copyright © 2021-2025 [Contentstack](https://www.contentstack.com/). All Rights Reserved +Copyright © 2021-2026 [Contentstack](https://www.contentstack.com/). All Rights Reserved Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: diff --git a/src/livePreview/editButton/__test__/editButtonAction.test.ts b/src/livePreview/editButton/__test__/editButtonAction.test.ts index 67b388cd..db2acca8 100644 --- a/src/livePreview/editButton/__test__/editButtonAction.test.ts +++ b/src/livePreview/editButton/__test__/editButtonAction.test.ts @@ -598,7 +598,7 @@ describe("cslp tooltip", () => { }); new LivePreview(); - let tooltip = document.querySelector( + const tooltip = document.querySelector( "[data-test-id='cs-cslp-tooltip']" ); const tooltipParent = tooltip?.parentNode; diff --git a/src/preview/__test__/contentstack-live-preview-HOC.test.ts b/src/preview/__test__/contentstack-live-preview-HOC.test.ts index 5c765332..fdb314e2 100644 --- a/src/preview/__test__/contentstack-live-preview-HOC.test.ts +++ b/src/preview/__test__/contentstack-live-preview-HOC.test.ts @@ -105,7 +105,8 @@ describe("Live Preview HOC init", () => { expect(livePreviewPostMessageSpy).toHaveBeenCalledTimes(1); expect(visualBuilderPostMessageSpy).toHaveBeenCalledWith('init', { isSSR: true, href: 'http://localhost:3000/' }); expect(visualBuilderPostMessageSpy).toHaveBeenCalledWith('send-variant-and-locale'); - expect(visualBuilderPostMessageSpy).toHaveBeenCalledTimes(2); + expect(visualBuilderPostMessageSpy).toHaveBeenCalledWith('get-highlight-variant-fields-status'); + expect(visualBuilderPostMessageSpy).toHaveBeenCalledTimes(3); }); test("should return the existing live preview instance if it is already initialized", async () => { diff --git a/src/visualBuilder/eventManager/__test__/useVariantsPostMessageEvent.spec.ts b/src/visualBuilder/eventManager/__test__/useVariantsPostMessageEvent.spec.ts index 412d1a40..f2528c65 100644 --- a/src/visualBuilder/eventManager/__test__/useVariantsPostMessageEvent.spec.ts +++ b/src/visualBuilder/eventManager/__test__/useVariantsPostMessageEvent.spec.ts @@ -7,6 +7,20 @@ import { afterEach, MockedObject, } from "vitest"; + +const { debounce } = vi.hoisted(() => { + return { + debounce: vi.fn((fn: any, _delay: number, _options?: any) => { + return fn; + }), + }; +}); +vi.mock("lodash-es", () => { + return { + ...vi.importActual("lodash-es"), + debounce: debounce, + }; +}); import { useVariantFieldsPostMessageEvent, addVariantFieldClass, @@ -14,6 +28,8 @@ import { setAudienceMode, setVariant, setLocale, + setHighlightVariantFields, + getHighlightVariantFieldsStatus, } from "../../../visualBuilder/eventManager/useVariantsPostMessageEvent"; import { VisualBuilderPostMessageEvents } from "../../../visualBuilder/utils/types/postMessage.types"; import { VisualBuilder } from "../../../visualBuilder"; @@ -21,6 +37,7 @@ import { FieldSchemaMap } from "../../../visualBuilder/utils/fieldSchemaMap"; import { visualBuilderStyles } from "../../../visualBuilder/visualBuilder.style"; import visualBuilderPostMessage from "../../../visualBuilder/utils/visualBuilderPostMessage"; import { EventManager } from "@contentstack/advanced-post-message"; +import { updateVariantClasses } from "../../../visualBuilder/eventManager/useRecalculateVariantDataCSLPValues"; import * as cslpdata from "../../../cslp/cslpdata"; const mockVisualBuilderPostMessage = @@ -44,6 +61,12 @@ vi.mock("../../../visualBuilder/utils/fieldSchemaMap", () => { }; }); +vi.mock("../../../visualBuilder/eventManager/useRecalculateVariantDataCSLPValues", () => { + return { + updateVariantClasses: vi.fn(), + }; +}); + vi.mock("../../../visualBuilder", () => { return { VisualBuilder: { @@ -52,6 +75,7 @@ vi.mock("../../../visualBuilder", () => { audienceMode: false, variant: null, locale: "en-us", + highlightVariantFields: false, }, }, }, @@ -60,11 +84,13 @@ vi.mock("../../../visualBuilder", () => { // Create a more realistic mock of the CSS modules const cssClassMock = "go109692693"; // Match the actual generated class name +const cssOutlineClassMock = "go109692694"; -vi.mock("../../../visualBuilder.style", () => { +vi.mock("../../visualBuilder.style", () => { return { visualBuilderStyles: () => ({ "visual-builder__variant-field": cssClassMock, + "visual-builder__variant-field-outline": cssOutlineClassMock, }), }; }); @@ -98,7 +124,7 @@ const mockQuerySelectorAll = vi.fn().mockImplementation((selector) => { // Return different mocks based on selector if (selector === "[data-cslp]") { return mockElements; - } else if (selector === `.${cssClassMock}`) { + } else if (selector === `.${cssOutlineClassMock}`) { return mockElements; // For onlyHighlighted=true case } else if ( selector === @@ -111,6 +137,12 @@ const mockQuerySelectorAll = vi.fn().mockImplementation((selector) => { return []; }); +describe("debounceAddVariantFieldClass", () => { + // Moved to the top of the file to ensure it is mocks are not cleared before the test is run + it("should debounce addVariantFieldClass calls", () => { + expect(debounce).toHaveBeenCalledWith(addVariantFieldClass, 1000, { trailing: true }); + }); +}); describe("useVariantFieldsPostMessageEvent", () => { // Store original document.querySelectorAll const originalQuerySelectorAll = document.querySelectorAll; @@ -130,7 +162,7 @@ describe("useVariantFieldsPostMessageEvent", () => { it("should register all event listeners", () => { // Call the function - useVariantFieldsPostMessageEvent(); + useVariantFieldsPostMessageEvent({ isSSR: false }); // Verify event listeners are registered expect(mockVisualBuilderPostMessage.on).toHaveBeenCalledWith( @@ -161,7 +193,7 @@ describe("useVariantFieldsPostMessageEvent", () => { it("should handle GET_VARIANT_ID event", () => { // Register event handlers - useVariantFieldsPostMessageEvent(); + useVariantFieldsPostMessageEvent({ isSSR: false }); // Extract the event handler function const call = mockVisualBuilderPostMessage.on.mock.calls.find( @@ -185,7 +217,7 @@ describe("useVariantFieldsPostMessageEvent", () => { it("should handle GET_LOCALE event", () => { // Register event handlers - useVariantFieldsPostMessageEvent(); + useVariantFieldsPostMessageEvent({ isSSR: false }); // Extract the event handler function const call = mockVisualBuilderPostMessage.on.mock.calls.find( @@ -206,7 +238,7 @@ describe("useVariantFieldsPostMessageEvent", () => { it("should handle SET_AUDIENCE_MODE event", () => { // Register event handlers - useVariantFieldsPostMessageEvent(); + useVariantFieldsPostMessageEvent({ isSSR: false }); // Extract the event handler function const call = mockVisualBuilderPostMessage.on.mock.calls.find( @@ -227,7 +259,7 @@ describe("useVariantFieldsPostMessageEvent", () => { it("should handle SHOW_VARIANT_FIELDS event", () => { // Register event handlers - useVariantFieldsPostMessageEvent(); + useVariantFieldsPostMessageEvent({ isSSR: false }); // Extract the event handler function const call = mockVisualBuilderPostMessage.on.mock.calls.find( @@ -252,16 +284,16 @@ describe("useVariantFieldsPostMessageEvent", () => { // Verify that classes were added to elements correctly expect(mockElements[0].classList.add).toHaveBeenCalledWith( - visualBuilderStyles()["visual-builder__variant-field"] + "visual-builder__variant-field" ); expect(mockElements[0].classList.add).toHaveBeenCalledWith( - "visual-builder__variant-field" + visualBuilderStyles()["visual-builder__variant-field-outline"] ); }); it("should handle REMOVE_VARIANT_FIELDS event with onlyHighlighted=true", () => { // Register event handlers - useVariantFieldsPostMessageEvent(); + useVariantFieldsPostMessageEvent({ isSSR: false }); // Extract the event handler function const call = mockVisualBuilderPostMessage.on.mock.calls.find( @@ -276,20 +308,20 @@ describe("useVariantFieldsPostMessageEvent", () => { // Verify querySelectorAll was called with the correct selector expect(mockQuerySelectorAll).toHaveBeenCalledWith( - `.${visualBuilderStyles()["visual-builder__variant-field"]}` + `.${visualBuilderStyles()["visual-builder__variant-field-outline"]}` ); // Verify that classes were removed from elements correctly mockElements.forEach((element) => { expect(element.classList.remove).toHaveBeenCalledWith( - visualBuilderStyles()["visual-builder__variant-field"] + visualBuilderStyles()["visual-builder__variant-field-outline"] ); }); }); it("should handle REMOVE_VARIANT_FIELDS event with onlyHighlighted=false", () => { // Register event handlers - useVariantFieldsPostMessageEvent(); + useVariantFieldsPostMessageEvent({ isSSR: false }); // Extract the event handler function const call = mockVisualBuilderPostMessage.on.mock.calls.find( @@ -312,7 +344,7 @@ describe("useVariantFieldsPostMessageEvent", () => { expect(element.classList.remove).toHaveBeenCalledWith( "visual-builder__disabled-variant-field", "visual-builder__variant-field", - visualBuilderStyles()["visual-builder__variant-field"], + visualBuilderStyles()["visual-builder__variant-field-outline"], "visual-builder__base-field", "visual-builder__lower-order-variant-field" ); @@ -339,9 +371,9 @@ describe("addVariantFieldClass", () => { it("should add classes to elements correctly based on data-cslp attribute", () => { const variantUid = "variant-123"; - const highlightVariantFields = true; + VisualBuilder.VisualBuilderGlobalState.value.highlightVariantFields = true; - addVariantFieldClass(variantUid, highlightVariantFields, []); + addVariantFieldClass(variantUid); // Verify querySelectorAll was called with the correct selector expect(mockQuerySelectorAll).toHaveBeenCalledWith("[data-cslp]"); @@ -349,10 +381,10 @@ describe("addVariantFieldClass", () => { // First element has the variant ID expect(mockElements[0].getAttribute).toHaveBeenCalledWith("data-cslp"); expect(mockElements[0].classList.add).toHaveBeenCalledWith( - visualBuilderStyles()["visual-builder__variant-field"] + "visual-builder__variant-field" ); expect(mockElements[0].classList.add).toHaveBeenCalledWith( - "visual-builder__variant-field" + visualBuilderStyles()["visual-builder__variant-field-outline"] ); // Second element does not start with 'v2:' @@ -370,18 +402,18 @@ describe("addVariantFieldClass", () => { it("should not add highlight class when highlightVariantFields is false", () => { const variantUid = "variant-123"; - const highlightVariantFields = false; + VisualBuilder.VisualBuilderGlobalState.value.highlightVariantFields = false; - addVariantFieldClass(variantUid, highlightVariantFields, []); + addVariantFieldClass(variantUid); // First element has the variant ID but should not get highlight class expect(mockElements[0].getAttribute).toHaveBeenCalledWith("data-cslp"); - expect(mockElements[0].classList.add).not.toHaveBeenCalledWith( - visualBuilderStyles()["visual-builder__variant-field"] - ); expect(mockElements[0].classList.add).toHaveBeenCalledWith( "visual-builder__variant-field" ); + expect(mockElements[0].classList.add).not.toHaveBeenCalledWith( + visualBuilderStyles()["visual-builder__variant-field-outline"] + ); }); it("should handle lower order variant fields correctly", () => { @@ -392,10 +424,9 @@ describe("addVariantFieldClass", () => { } }); const variantUid = "variant-456"; - const highlightVariantFields = false; const variantOrder = ["variant-123", "variant-456"]; - - addVariantFieldClass(variantUid, highlightVariantFields, variantOrder); + VisualBuilder.VisualBuilderGlobalState.value.variantOrder = variantOrder; + addVariantFieldClass(variantUid); // Verify that classes were added to elements correctly expect(mockElements[0].classList.add).toHaveBeenCalledWith("visual-builder__variant-field", "visual-builder__lower-order-variant-field"); @@ -424,13 +455,13 @@ describe("removeVariantFieldClass", () => { // Verify querySelectorAll was called with the correct selector expect(mockQuerySelectorAll).toHaveBeenCalledWith( - `.${visualBuilderStyles()["visual-builder__variant-field"]}` + `.${visualBuilderStyles()["visual-builder__variant-field-outline"]}` ); // Verify classes were removed mockElements.forEach((element) => { expect(element.classList.remove).toHaveBeenCalledWith( - visualBuilderStyles()["visual-builder__variant-field"] + visualBuilderStyles()["visual-builder__variant-field-outline"] ); }); }); @@ -448,7 +479,7 @@ describe("removeVariantFieldClass", () => { expect(element.classList.remove).toHaveBeenCalledWith( "visual-builder__disabled-variant-field", "visual-builder__variant-field", - visualBuilderStyles()["visual-builder__variant-field"], + visualBuilderStyles()["visual-builder__variant-field-outline"], "visual-builder__base-field", "visual-builder__lower-order-variant-field" ); @@ -474,6 +505,7 @@ describe("State Management Functions", () => { VisualBuilder.VisualBuilderGlobalState.value.audienceMode = false; VisualBuilder.VisualBuilderGlobalState.value.variant = null; VisualBuilder.VisualBuilderGlobalState.value.locale = "en-us"; + VisualBuilder.VisualBuilderGlobalState.value.highlightVariantFields = false; }); it("setAudienceMode should update global state", () => { @@ -509,4 +541,125 @@ describe("State Management Functions", () => { "en-us" ); }); + + it("setHighlightVariantFields should update global state", () => { + setHighlightVariantFields(true); + expect(VisualBuilder.VisualBuilderGlobalState.value.highlightVariantFields).toBe( + true + ); + + setHighlightVariantFields(false); + expect(VisualBuilder.VisualBuilderGlobalState.value.highlightVariantFields).toBe( + false + ); + }); +}); + +describe("getHighlightVariantFieldsStatus", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("should return highlight status when successful", async () => { + const mockResponse = { highlightVariantFields: true }; + (mockVisualBuilderPostMessage.send as any).mockResolvedValue(mockResponse); + + const result = await getHighlightVariantFieldsStatus(); + + expect(mockVisualBuilderPostMessage.send).toHaveBeenCalledWith( + VisualBuilderPostMessageEvents.GET_HIGHLIGHT_VARIANT_FIELDS_STATUS + ); + expect(result).toEqual(mockResponse); + }); + + it("should return default false when response is null", async () => { + (mockVisualBuilderPostMessage.send as any).mockResolvedValue(null); + + const result = await getHighlightVariantFieldsStatus(); + + expect(result).toEqual({ highlightVariantFields: false }); + }); + + it("should return default false when request fails", async () => { + (mockVisualBuilderPostMessage.send as any).mockRejectedValue( + new Error("Network error") + ); + + const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); + + const result = await getHighlightVariantFieldsStatus(); + + expect(result).toEqual({ highlightVariantFields: false }); + expect(consoleErrorSpy).toHaveBeenCalledWith( + "Failed to get highlight variant fields status:", + expect.any(Error) + ); + + consoleErrorSpy.mockRestore(); + }); +}); + + +describe("useVariantFieldsPostMessageEvent SSR handling", () => { + const originalQuerySelectorAll = document.querySelectorAll; + + beforeEach(() => { + document.querySelectorAll = mockQuerySelectorAll; + vi.clearAllMocks(); + }); + + afterEach(() => { + document.querySelectorAll = originalQuerySelectorAll; + }); + + it("should call addVariantFieldClass directly when isSSR is true and variant is provided", () => { + useVariantFieldsPostMessageEvent({ isSSR: true }); + + const call = mockVisualBuilderPostMessage.on.mock.calls.find( + (call: any[]) => + call[0] === VisualBuilderPostMessageEvents.GET_VARIANT_ID + ); + const handler = call ? call[1] : null; + + vi.clearAllMocks(); + handler!({ data: { variant: "variant-123" } }); + + // Should call addVariantFieldClass directly (not updateVariantClasses) + expect(mockQuerySelectorAll).toHaveBeenCalledWith("[data-cslp]"); + expect(updateVariantClasses).not.toHaveBeenCalled(); + }); + + it("should call updateVariantClasses when isSSR is false", () => { + useVariantFieldsPostMessageEvent({ isSSR: false }); + + const call = mockVisualBuilderPostMessage.on.mock.calls.find( + (call: any[]) => + call[0] === VisualBuilderPostMessageEvents.GET_VARIANT_ID + ); + const handler = call ? call[1] : null; + + vi.clearAllMocks(); + handler!({ data: { variant: "variant-123" } }); + + // Should call updateVariantClasses (not addVariantFieldClass directly) + expect(updateVariantClasses).toHaveBeenCalled(); + expect(mockQuerySelectorAll).not.toHaveBeenCalled(); + }); + + it("should not call addVariantFieldClass when isSSR is true but variant is null", () => { + useVariantFieldsPostMessageEvent({ isSSR: true }); + + const call = mockVisualBuilderPostMessage.on.mock.calls.find( + (call: any[]) => + call[0] === VisualBuilderPostMessageEvents.GET_VARIANT_ID + ); + const handler = call ? call[1] : null; + + vi.clearAllMocks(); + handler!({ data: { variant: null } }); + + // Should not call addVariantFieldClass when variant is null + expect(mockQuerySelectorAll).not.toHaveBeenCalled(); + expect(updateVariantClasses).not.toHaveBeenCalled(); + }); }); diff --git a/src/visualBuilder/eventManager/useRecalculateVariantDataCSLPValues.ts b/src/visualBuilder/eventManager/useRecalculateVariantDataCSLPValues.ts index a577da4a..54142d82 100644 --- a/src/visualBuilder/eventManager/useRecalculateVariantDataCSLPValues.ts +++ b/src/visualBuilder/eventManager/useRecalculateVariantDataCSLPValues.ts @@ -3,6 +3,7 @@ import livePreviewPostMessage from "../../livePreview/eventManager/livePreviewEv import { LIVE_PREVIEW_POST_MESSAGE_EVENTS } from "../../livePreview/eventManager/livePreviewEventManager.constant"; import { DATA_CSLP_ATTR_SELECTOR } from "../utils/constants"; import { visualBuilderStyles } from "../visualBuilder.style"; +import { setHighlightVariantFields } from "./useVariantsPostMessageEvent"; const VARIANT_UPDATE_DELAY_MS: Readonly = 8000; @@ -19,15 +20,14 @@ export function useRecalculateVariantDataCSLPValues(): void { LIVE_PREVIEW_POST_MESSAGE_EVENTS.VARIANT_PATCH, (event) => { if (VisualBuilder.VisualBuilderGlobalState.value.audienceMode) { - updateVariantClasses(event.data); + setHighlightVariantFields(event.data.highlightVariantFields); + updateVariantClasses(); } } ); } -function updateVariantClasses({ - highlightVariantFields, - expectedCSLPValues, -}: OnAudienceModeVariantPatchUpdate): void { +export function updateVariantClasses(): void { + const highlightVariantFields = VisualBuilder.VisualBuilderGlobalState.value.highlightVariantFields; const variant = VisualBuilder.VisualBuilderGlobalState.value.variant; const observers: MutationObserver[] = []; @@ -46,20 +46,17 @@ function updateVariantClasses({ if (element.classList.contains("visual-builder__base-field")) { element.classList.remove("visual-builder__base-field"); } + const variantFieldClasses = ["visual-builder__variant-field"]; if (highlightVariantFields) { - element.classList.add( - visualBuilderStyles()["visual-builder__variant-field"], - "visual-builder__variant-field" - ); - } else { - element.classList.add("visual-builder__variant-field"); + variantFieldClasses.push(visualBuilderStyles()["visual-builder__variant-field-outline"]); } + element.classList.add(...variantFieldClasses); } else if ( !dataCslp.startsWith("v2:") && element.classList.contains("visual-builder__variant-field") ) { element.classList.remove( - visualBuilderStyles()["visual-builder__variant-field"], + visualBuilderStyles()["visual-builder__variant-field-outline"], "visual-builder__variant-field" ); element.classList.add("visual-builder__base-field"); @@ -70,7 +67,7 @@ function updateVariantClasses({ element.classList.contains("visual-builder__variant-field") ) { element.classList.remove( - visualBuilderStyles()["visual-builder__variant-field"], + visualBuilderStyles()["visual-builder__variant-field-outline"], "visual-builder__variant-field" ); element.classList.add("visual-builder__disabled-variant-field"); @@ -111,18 +108,15 @@ function updateVariantClasses({ if (element.classList.contains("visual-builder__base-field")) { element.classList.remove("visual-builder__base-field"); } + const variantFieldClasses = ["visual-builder__variant-field"]; if (highlightVariantFields) { - element.classList.add( - visualBuilderStyles()["visual-builder__variant-field"], - "visual-builder__variant-field" - ); - } else { - element.classList.add("visual-builder__variant-field"); + variantFieldClasses.push(visualBuilderStyles()["visual-builder__variant-field-outline"]); } + element.classList.add(...variantFieldClasses); } else if (!dataCslp.startsWith("v2:")) { if (element.classList.contains("visual-builder__variant-field")) { element.classList.remove( - visualBuilderStyles()["visual-builder__variant-field"], + visualBuilderStyles()["visual-builder__variant-field-outline"], "visual-builder__variant-field" ); } @@ -166,6 +160,7 @@ function updateVariantClasses({ }); observers.push(observer); + // TODO: Check if we could add attributeFilter to the observer to only observe the attribute changes for the data-cslp attribute. observer.observe(element, { attributes: true, childList: true, // Observe direct children diff --git a/src/visualBuilder/eventManager/useVariantsPostMessageEvent.ts b/src/visualBuilder/eventManager/useVariantsPostMessageEvent.ts index b1d8a229..382a6976 100644 --- a/src/visualBuilder/eventManager/useVariantsPostMessageEvent.ts +++ b/src/visualBuilder/eventManager/useVariantsPostMessageEvent.ts @@ -3,6 +3,8 @@ import { visualBuilderStyles } from "../visualBuilder.style"; import visualBuilderPostMessage from "../utils/visualBuilderPostMessage"; import { VisualBuilderPostMessageEvents } from "../utils/types/postMessage.types"; import { FieldSchemaMap } from "../utils/fieldSchemaMap"; +import { updateVariantClasses } from "./useRecalculateVariantDataCSLPValues"; +import { debounce } from "lodash-es"; import { extractDetailsFromCslp } from "../../cslp/cslpdata"; interface VariantFieldsEvent { @@ -50,22 +52,24 @@ function isLowerOrderVariant(variant_uid: string, dataCslp: string, variantOrder return indexOfCslpVariant < indexOfCmsVariant; } + export function addVariantFieldClass( - variant_uid: string, - highlightVariantFields: boolean, - variantOrder: string[] + variant_uid: string ): void { + const variantOrder = VisualBuilder.VisualBuilderGlobalState.value.variantOrder; + const highlightVariantFields = VisualBuilder.VisualBuilderGlobalState.value.highlightVariantFields; const elements = document.querySelectorAll(`[data-cslp]`); elements.forEach((element) => { const dataCslp = element.getAttribute("data-cslp"); if (!dataCslp) return; if (dataCslp?.includes(variant_uid)) { - highlightVariantFields && + element.classList.add("visual-builder__variant-field"); + if (highlightVariantFields) { element.classList.add( - visualBuilderStyles()["visual-builder__variant-field"] + visualBuilderStyles()["visual-builder__variant-field-outline"] ); - element.classList.add("visual-builder__variant-field"); + } } else if (!dataCslp.startsWith("v2:")) { element.classList.add("visual-builder__base-field"); } @@ -78,16 +82,22 @@ export function addVariantFieldClass( }); } +export const debounceAddVariantFieldClass = debounce( + addVariantFieldClass, + 1000, + { trailing: true } +) as (variant_uid: string) => void; + export function removeVariantFieldClass( onlyHighlighted: boolean = false ): void { if (onlyHighlighted) { const variantElements = document.querySelectorAll( - `.${visualBuilderStyles()["visual-builder__variant-field"]}` + `.${visualBuilderStyles()["visual-builder__variant-field-outline"]}` ); variantElements.forEach((element) => { element.classList.remove( - visualBuilderStyles()["visual-builder__variant-field"] + visualBuilderStyles()["visual-builder__variant-field-outline"] ); }); } else { @@ -98,7 +108,7 @@ export function removeVariantFieldClass( element.classList.remove( "visual-builder__disabled-variant-field", "visual-builder__variant-field", - visualBuilderStyles()["visual-builder__variant-field"], + visualBuilderStyles()["visual-builder__variant-field-outline"], "visual-builder__base-field", "visual-builder__lower-order-variant-field" ); @@ -115,18 +125,52 @@ export function setVariant(uid: string | null): void { export function setLocale(locale: string): void { VisualBuilder.VisualBuilderGlobalState.value.locale = locale; } +export function setHighlightVariantFields(highlight: boolean): void { + VisualBuilder.VisualBuilderGlobalState.value.highlightVariantFields = highlight; +} +export function setVariantOrder(variantOrder: string[]): void { + VisualBuilder.VisualBuilderGlobalState.value.variantOrder = variantOrder; +} + +interface GetHighlightVariantFieldsStatusResponse { + highlightVariantFields: boolean; +} +export async function getHighlightVariantFieldsStatus(): Promise { + try { + const result = await visualBuilderPostMessage?.send( + VisualBuilderPostMessageEvents.GET_HIGHLIGHT_VARIANT_FIELDS_STATUS + ); + return result ?? { + highlightVariantFields: false, + }; + } catch (error) { + console.error("Failed to get highlight variant fields status:", error); + return { + highlightVariantFields: false, + }; + } +} -export function useVariantFieldsPostMessageEvent(): void { +export function useVariantFieldsPostMessageEvent({ isSSR }: { isSSR: boolean }): void { visualBuilderPostMessage?.on( VisualBuilderPostMessageEvents.GET_VARIANT_ID, (event: VariantEvent) => { - setVariant(event.data.variant); + const selectedVariant = event.data.variant; + setVariant(selectedVariant); // clear field schema when variant is changed. // this is required as we cache field schema // which contain a key isUnlinkedVariant. // This key can change when variant is changed, // so clear the field schema cache FieldSchemaMap.clear(); + if (isSSR) { + if (selectedVariant) { + addVariantFieldClass(selectedVariant); + } + } else { + // recalculate and apply classes + updateVariantClasses(); + } } ); visualBuilderPostMessage?.on( @@ -144,17 +188,18 @@ export function useVariantFieldsPostMessageEvent(): void { visualBuilderPostMessage?.on( VisualBuilderPostMessageEvents.SHOW_VARIANT_FIELDS, (event: VariantFieldsEvent) => { + setHighlightVariantFields(event.data.variant_data.highlightVariantFields); + setVariantOrder(event.data.variant_data.variantOrder || []); removeVariantFieldClass(); addVariantFieldClass( - event.data.variant_data.variant, - event.data.variant_data.highlightVariantFields, - event.data.variant_data.variantOrder + event.data.variant_data.variant ); } ); visualBuilderPostMessage?.on( VisualBuilderPostMessageEvents.REMOVE_VARIANT_FIELDS, (event: RemoveVariantFieldsEvent) => { + setHighlightVariantFields(false); removeVariantFieldClass(event?.data?.onlyHighlighted); } ); diff --git a/src/visualBuilder/index.ts b/src/visualBuilder/index.ts index da5992f3..8e24ba8c 100644 --- a/src/visualBuilder/index.ts +++ b/src/visualBuilder/index.ts @@ -26,7 +26,7 @@ import initUI from "./components"; import { useDraftFieldsPostMessageEvent } from "./eventManager/useDraftFieldsPostMessageEvent"; import { useHideFocusOverlayPostMessageEvent } from "./eventManager/useHideFocusOverlayPostMessageEvent"; import { useScrollToField } from "./eventManager/useScrollToField"; -import { useVariantFieldsPostMessageEvent } from "./eventManager/useVariantsPostMessageEvent"; +import { debounceAddVariantFieldClass, getHighlightVariantFieldsStatus, setHighlightVariantFields, useVariantFieldsPostMessageEvent } from "./eventManager/useVariantsPostMessageEvent"; import { generateEmptyBlocks, removeEmptyBlocks, @@ -66,6 +66,8 @@ interface VisualBuilderGlobalStateImpl { audienceMode: boolean; locale: string; variant: string | null; + highlightVariantFields: boolean; + variantOrder: string[]; focusElementObserver: MutationObserver | null; referenceParentMap: Record; isFocussed: boolean; @@ -89,6 +91,8 @@ export class VisualBuilder { audienceMode: false, locale: Config.get().stackDetails.masterLocale || "en-us", variant: null, + highlightVariantFields: false, + variantOrder: [], focusElementObserver: null, referenceParentMap: {}, isFocussed: false, @@ -238,6 +242,9 @@ export class VisualBuilder { previousEmptyBlockParents: emptyBlockParents, }; } + if (VisualBuilder.VisualBuilderGlobalState.value.variant && VisualBuilder.VisualBuilderGlobalState.value.highlightVariantFields) { + debounceAddVariantFieldClass(VisualBuilder.VisualBuilderGlobalState.value.variant); + } }, 100, { trailing: true } @@ -363,6 +370,9 @@ export class VisualBuilder { subtree: true, }); + getHighlightVariantFieldsStatus().then((result) => { + setHighlightVariantFields(result.highlightVariantFields); + }); visualBuilderPostMessage?.on( VisualBuilderPostMessageEvents.GET_ALL_ENTRIES_IN_CURRENT_PAGE, getEntryIdentifiersInCurrentPage @@ -398,7 +408,7 @@ export class VisualBuilder { useOnEntryUpdatePostMessageEvent(); useRecalculateVariantDataCSLPValues(); useDraftFieldsPostMessageEvent(); - useVariantFieldsPostMessageEvent(); + useVariantFieldsPostMessageEvent({ isSSR: config.ssr ?? false }); } }) .catch(() => { @@ -441,6 +451,8 @@ export class VisualBuilder { audienceMode: false, locale: "en-us", variant: null, + highlightVariantFields: false, + variantOrder: [], focusElementObserver: null, referenceParentMap: {}, isFocussed: false, diff --git a/src/visualBuilder/utils/types/postMessage.types.ts b/src/visualBuilder/utils/types/postMessage.types.ts index 06ea3f91..54bda7d0 100644 --- a/src/visualBuilder/utils/types/postMessage.types.ts +++ b/src/visualBuilder/utils/types/postMessage.types.ts @@ -47,6 +47,7 @@ export enum VisualBuilderPostMessageEvents { REMOVE_HIGHLIGHTED_COMMENTS = "remove-highlighted-comments", GET_VARIANT_ID = "get-variant-id", GET_LOCALE = "get-locale", + GET_HIGHLIGHT_VARIANT_FIELDS_STATUS = "get-highlight-variant-fields-status", SEND_VARIANT_AND_LOCALE = "send-variant-and-locale", GET_CONTENT_TYPE_NAME = "get-content-type-name", REFERENCE_MAP = "get-reference-map", diff --git a/src/visualBuilder/visualBuilder.style.ts b/src/visualBuilder/visualBuilder.style.ts index 87a669d9..bd6c9e11 100644 --- a/src/visualBuilder/visualBuilder.style.ts +++ b/src/visualBuilder/visualBuilder.style.ts @@ -673,7 +673,8 @@ export function visualBuilderStyles() { "visual-builder__draft-field": css` outline: 2px dashed #eb5646; `, - "visual-builder__variant-field": css` + "visual-builder__variant-field": css``, + "visual-builder__variant-field-outline": css` outline: 2px solid #bd59fa; outline-offset: -2px; `,