diff --git a/src/visualBuilder/index.ts b/src/visualBuilder/index.ts index 3fb234dc..8179d3b6 100644 --- a/src/visualBuilder/index.ts +++ b/src/visualBuilder/index.ts @@ -14,7 +14,10 @@ import { import { generateStartEditingButton } from "./generators/generateStartEditingButton"; import { addFocusOverlay } from "./generators/generateOverlay"; -import { getEntryIdentifiersInCurrentPage } from "./utils/getEntryIdentifiersInCurrentPage"; +import { + getEntryIdentifiersInCurrentPage, + getEntryIdentifiersSignature, +} from "./utils/getEntryIdentifiersInCurrentPage"; import { resolvePageContext } from "./utils/resolvePageContext"; import visualBuilderPostMessage from "./utils/visualBuilderPostMessage"; import { VisualBuilderPostMessageEvents } from "./utils/types/postMessage.types"; @@ -206,6 +209,22 @@ export class VisualBuilder { }); }); + private lastEntriesSignature = ""; + + /** Tell the editor which entries are on the page, only when the set changed. */ + private notifyEntriesInPageIfChanged = (): void => { + const entries = getEntryIdentifiersInCurrentPage(); + const signature = getEntryIdentifiersSignature( + entries.entriesInCurrentPage + ); + if (signature === this.lastEntriesSignature) return; + this.lastEntriesSignature = signature; + visualBuilderPostMessage?.send( + VisualBuilderPostMessageEvents.ENTRIES_IN_CURRENT_PAGE_CHANGED, + entries + ); + }; + private mutationObserver = new MutationObserver( debounce( async () => { @@ -215,6 +234,7 @@ export class VisualBuilder { this.visualBuilderContainer, this.resizeObserver ); + this.notifyEntriesInPageIfChanged(); const emptyBlockParents = Array.from( document.querySelectorAll(`.${VB_EmptyBlockParentClass}`) @@ -366,9 +386,13 @@ export class VisualBuilder { useScrollToField(); useHighlightCommentIcon(); + // Frameworks reuse nodes and rewrite data-cslp in place (variant + // switch, re-keyed lists), which childList alone never reports. this.mutationObserver.observe(document.body, { childList: true, subtree: true, + attributes: true, + attributeFilter: ["data-cslp"], }); getHighlightVariantFieldsStatus().then((result) => { @@ -378,6 +402,7 @@ export class VisualBuilder { VisualBuilderPostMessageEvents.GET_ALL_ENTRIES_IN_CURRENT_PAGE, getEntryIdentifiersInCurrentPage ); + this.notifyEntriesInPageIfChanged(); visualBuilderPostMessage?.send( VisualBuilderPostMessageEvents.SEND_VARIANT_AND_LOCALE ); diff --git a/src/visualBuilder/utils/__test__/getEntryIdentifiersInCurrentPage.test.ts b/src/visualBuilder/utils/__test__/getEntryIdentifiersInCurrentPage.test.ts index 7f50ce74..14d8b8a4 100644 --- a/src/visualBuilder/utils/__test__/getEntryIdentifiersInCurrentPage.test.ts +++ b/src/visualBuilder/utils/__test__/getEntryIdentifiersInCurrentPage.test.ts @@ -1,5 +1,8 @@ import { JSDOM } from "jsdom"; -import { getEntryIdentifiersInCurrentPage } from "../getEntryIdentifiersInCurrentPage"; +import { + getEntryIdentifiersInCurrentPage, + getEntryIdentifiersSignature, +} from "../getEntryIdentifiersInCurrentPage"; const dom = new JSDOM(`
@@ -23,8 +26,8 @@ const domWithNoCslp = new JSDOM(` `); describe("getEntryIdentifiersInCurrentPage", () => { - test('should return an empty array if no elements with data-cslp attribute are found', () => { - document.body.innerHTML = ''; + test("should return an empty array if no elements with data-cslp attribute are found", () => { + document.body.innerHTML = ""; const result = getEntryIdentifiersInCurrentPage(); expect(result.entriesInCurrentPage).toEqual([]); }); @@ -58,6 +61,24 @@ describe("getEntryIdentifiersInCurrentPage", () => { `; const { entriesInCurrentPage } = getEntryIdentifiersInCurrentPage(); expect(entriesInCurrentPage.length).toBe(1); - expect(entriesInCurrentPage[0].entryUid).toBe('bltf5bb5f8fb088a332'); + expect(entriesInCurrentPage[0].entryUid).toBe("bltf5bb5f8fb088a332"); + }); +}); + +describe("getEntryIdentifiersSignature", () => { + test("should be order independent and change with the set", () => { + const a = { entryUid: "blt1", contentTypeUid: "page", locale: "en-us" }; + const b = { + entryUid: "blt2", + contentTypeUid: "header", + locale: "en-us", + }; + expect(getEntryIdentifiersSignature([a, b])).toBe( + getEntryIdentifiersSignature([b, a]) + ); + expect(getEntryIdentifiersSignature([a])).not.toBe( + getEntryIdentifiersSignature([a, b]) + ); + expect(getEntryIdentifiersSignature([])).toBe(""); }); }); diff --git a/src/visualBuilder/utils/getEntryIdentifiersInCurrentPage.ts b/src/visualBuilder/utils/getEntryIdentifiersInCurrentPage.ts index d3e74685..5d73a025 100644 --- a/src/visualBuilder/utils/getEntryIdentifiersInCurrentPage.ts +++ b/src/visualBuilder/utils/getEntryIdentifiersInCurrentPage.ts @@ -1,34 +1,37 @@ import { extractDetailsFromCslp, isValidCslp } from "../../cslp/cslpdata"; -type EntryIdentifiers = { - entriesInCurrentPage: { - entryUid: string; - contentTypeUid: string; - locale: string; - }[]; -} +export type EntryIdentifier = { + entryUid: string; + contentTypeUid: string; + locale: string; +}; + +export type EntryIdentifiers = { + entriesInCurrentPage: EntryIdentifier[]; +}; +/** Distinct entries rendered on the page, one per entry uid, read from `data-cslp`. */ export function getEntryIdentifiersInCurrentPage(): EntryIdentifiers { - const elementsWithCslp = Array.from( - document.querySelectorAll("[data-cslp]") - ); - const uniqueEntriesMap = new Map(); - elementsWithCslp.forEach((element) => { + const uniqueEntriesMap = new Map(); + document.querySelectorAll("[data-cslp]").forEach((element) => { const cslpValue = element.getAttribute("data-cslp"); if (!isValidCslp(cslpValue)) return; const cslpData = extractDetailsFromCslp(cslpValue); - uniqueEntriesMap.set(cslpData.entry_uid, - { - entryUid: cslpData.entry_uid, - contentTypeUid: cslpData.content_type_uid, - locale: cslpData.locale - } - ); + uniqueEntriesMap.set(cslpData.entry_uid, { + entryUid: cslpData.entry_uid, + contentTypeUid: cslpData.content_type_uid, + locale: cslpData.locale, + }); }); - - const uniqueEntriesArray = Array.from(uniqueEntriesMap.values()); + return { entriesInCurrentPage: Array.from(uniqueEntriesMap.values()) }; +} - return { - entriesInCurrentPage: uniqueEntriesArray, - }; +/** Order-independent fingerprint of the entry set, used to notify only on change. */ +export function getEntryIdentifiersSignature( + entries: EntryIdentifier[] +): string { + return entries + .map((entry) => `${entry.entryUid}.${entry.locale}`) + .sort() + .join("|"); } diff --git a/src/visualBuilder/utils/types/postMessage.types.ts b/src/visualBuilder/utils/types/postMessage.types.ts index 54ce0f87..5b7b00ca 100644 --- a/src/visualBuilder/utils/types/postMessage.types.ts +++ b/src/visualBuilder/utils/types/postMessage.types.ts @@ -36,6 +36,7 @@ export enum VisualBuilderPostMessageEvents { // FROM visual builder GET_ALL_ENTRIES_IN_CURRENT_PAGE = "get-entries-in-current-page", + ENTRIES_IN_CURRENT_PAGE_CHANGED = "entries-in-current-page-changed", HIDE_FOCUS_OVERLAY = "hide-focus-overlay", SHOW_DRAFT_FIELDS = "show-draft-fields", REMOVE_DRAFT_FIELDS = "remove-draft-fields",