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
186 changes: 127 additions & 59 deletions CHANGELOG.md

Large diffs are not rendered by default.

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.4` when you upgrade):
Pin the version to match your app (update `4.4.5` when you upgrade):

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

ContentstackLivePreview.init({
stackDetails: {
Expand Down
28 changes: 3 additions & 25 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.4",
"version": "4.4.5",
"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
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,8 @@ describe("StartEditingButtonComponent", () => {
test("should update href with current URL when mouse enters button", async () => {
Object.defineProperty(window, "location", {
value: new URL("http://localhost:3000"),
configurable: true,
writable: true,
});

const { getByTestId } = await asyncRender(
Expand All @@ -120,6 +122,7 @@ describe("StartEditingButtonComponent", () => {

Object.defineProperty(window, "location", {
value: new URL("http://localhost:3000/about"),
configurable: true,
writable: true,
});

Expand All @@ -135,6 +138,8 @@ describe("StartEditingButtonComponent", () => {
test("should update href with current URL when button is focused", async () => {
Object.defineProperty(window, "location", {
value: new URL("http://localhost:3000"),
configurable: true,
writable: true,
});

const { getByTestId } = await asyncRender(
Expand All @@ -145,10 +150,11 @@ describe("StartEditingButtonComponent", () => {

Object.defineProperty(window, "location", {
value: new URL("http://localhost:3000/contact"),
configurable: true,
writable: true,
});

fireEvent.focus(button);
button.focus();

const updatedHref = button.getAttribute("href");
expect(updatedHref).not.toBe(initialHref);
Expand Down
121 changes: 120 additions & 1 deletion src/visualBuilder/listeners/__test__/mouseClick.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import handleBuilderInteraction from "../mouseClick";
import { VisualBuilder } from "../../index";
import { FieldSchemaMap } from "../../utils/fieldSchemaMap";
Expand Down Expand Up @@ -250,3 +250,122 @@ describe("handleBuilderInteraction — pauseFeedback guard", () => {
expect(generateThread).toHaveBeenCalled();
});
});

describe("handleBuilderInteraction — alt+click on anchor", () => {
const originalLocation = window.location;

beforeEach(() => {
Object.defineProperty(window, "location", {
value: { href: "" },
writable: true,
});
});

afterEach(() => {
Object.defineProperty(window, "location", {
value: originalLocation,
writable: true,
});
});

it("prevents default/propagation and drives navigation itself instead of relying on the native click", async () => {
document.body.innerHTML = "";
const anchor = document.createElement("a");
anchor.href = "https://example.com/blog#anchor";
document.body.appendChild(anchor);

const params = makeParams(anchor);
Object.defineProperty(params.event, "altKey", { value: true });
const preventDefaultSpy = vi.spyOn(params.event, "preventDefault");
const stopPropagationSpy = vi.spyOn(params.event, "stopPropagation");

await handleBuilderInteraction(params);

expect(preventDefaultSpy).toHaveBeenCalled();
expect(stopPropagationSpy).toHaveBeenCalled();
expect(window.location.href).toBe("https://example.com/blog#anchor");
expect(getCsDataOfElement).not.toHaveBeenCalled();
});

it("does nothing on alt+click when the target isn't an anchor", async () => {
const editableElement = makeEditableElement();
const params = makeParams(editableElement);
Object.defineProperty(params.event, "altKey", { value: true });
const preventDefaultSpy = vi.spyOn(params.event, "preventDefault");

await handleBuilderInteraction(params);

expect(preventDefaultSpy).not.toHaveBeenCalled();
expect(window.location.href).toBe("");
expect(getCsDataOfElement).not.toHaveBeenCalled();
});

it("opens target=_blank links (RTE 'open in new tab') in a new tab instead of hijacking the iframe", async () => {
document.body.innerHTML = "";
const anchor = document.createElement("a");
anchor.href = "https://example.com/other-entry";
anchor.target = "_blank";
document.body.appendChild(anchor);

const openSpy = vi.spyOn(window, "open").mockImplementation(() => null);
const params = makeParams(anchor);
Object.defineProperty(params.event, "altKey", { value: true });

await handleBuilderInteraction(params);

expect(openSpy).toHaveBeenCalledWith(
"https://example.com/other-entry",
"_blank",
"noopener,noreferrer"
);
expect(window.location.href).toBe("");
openSpy.mockRestore();
});

it("resolves the anchor ancestor when the click lands on nested formatted text inside the link", async () => {
document.body.innerHTML = "";
const anchor = document.createElement("a");
anchor.href = "https://example.com/bold-link";
const bold = document.createElement("strong");
bold.textContent = "click me";
anchor.appendChild(bold);
document.body.appendChild(anchor);

const params = makeParams(bold);
Object.defineProperty(params.event, "altKey", { value: true });
const preventDefaultSpy = vi.spyOn(params.event, "preventDefault");

await handleBuilderInteraction(params);

expect(preventDefaultSpy).toHaveBeenCalled();
expect(window.location.href).toBe("https://example.com/bold-link");
});

it("blocks unsafe url schemes like javascript: on alt+click", async () => {
document.body.innerHTML = "";
const anchor = document.createElement("a");
anchor.href = "javascript:alert(1)";
document.body.appendChild(anchor);

const params = makeParams(anchor);
Object.defineProperty(params.event, "altKey", { value: true });

await handleBuilderInteraction(params);

expect(window.location.href).toBe("");
});

it("allows relative hrefs (resolve to the page's own http/https scheme)", async () => {
document.body.innerHTML = "";
const anchor = document.createElement("a");
anchor.href = "/other-entry";
document.body.appendChild(anchor);

const params = makeParams(anchor);
Object.defineProperty(params.event, "altKey", { value: true });

await handleBuilderInteraction(params);

expect(window.location.href).toBe(anchor.href);
});
});
Loading
Loading