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
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,15 @@ release steps in [CONTRIBUTING.md](CONTRIBUTING.md).

## [Unreleased]

### Fixed

- Partial style overrides no longer discard the defaults. Passing something like
`focusPoint: { style: { width: "40px" } }` previously replaced the whole style
object, leaving the marker with no border, shadow or background instead of
just a different width. The same applied to `cropZone.style` and
`cropZone.handleStyle`. The nested style objects are now merged a level
deeper, so an override layers onto the defaults.

## [0.2.6] - 2026-09-05

### Changed
Expand Down
57 changes: 41 additions & 16 deletions src/visual-image-tool.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,32 +34,57 @@ class VisualImageTool {
}

// Default options
const focusPointDefaults = {
enabled: true,
style: {
width: "30px",
height: "30px",
border: "3px solid white",
boxShadow: "0 0 0 2px black, 0 0 5px rgba(0,0,0,0.5)",
backgroundColor: "rgba(255, 0, 0, 0.5)",
},
};

const cropZoneDefaults = {
enabled: true,
style: {
border: "1px dashed #fff",
backgroundColor: "rgba(0, 0, 0, 0.4)",
},
handleStyle: {
width: "14px",
height: "14px",
backgroundColor: "white",
border: "2px solid black",
boxShadow: "0 0 3px rgba(0,0,0,0.5)",
},
};

// The style objects are merged a level deeper than the rest of the
// options: overriding one property should leave the others at their
// defaults, not drop them. A plain spread would replace the whole
// object, so `{ style: { width: "40px" } }` would leave the marker
// with no border, shadow or background at all.
this.options = {
focusPoint: {
enabled: true,
...focusPointDefaults,
...options.focusPoint,
style: {
width: "30px",
height: "30px",
border: "3px solid white",
boxShadow: "0 0 0 2px black, 0 0 5px rgba(0,0,0,0.5)",
backgroundColor: "rgba(255, 0, 0, 0.5)",
...focusPointDefaults.style,
...options.focusPoint?.style,
},
...options.focusPoint,
},
cropZone: {
enabled: true,
...cropZoneDefaults,
...options.cropZone,
style: {
border: "1px dashed #fff",
backgroundColor: "rgba(0, 0, 0, 0.4)",
...cropZoneDefaults.style,
...options.cropZone?.style,
},
handleStyle: {
width: "14px",
height: "14px",
backgroundColor: "white",
border: "2px solid black",
boxShadow: "0 0 3px rgba(0,0,0,0.5)",
...cropZoneDefaults.handleStyle,
...options.cropZone?.handleStyle,
},
...options.cropZone,
},
onChange: options.onChange || (() => {}),
debug: options.debug || false,
Expand Down
78 changes: 78 additions & 0 deletions src/visual-image-tool.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -199,4 +199,82 @@ describe("VisualImageTool", () => {
expect(instance.interaction.cropResizing).toBe(true);
expect(instance.interaction.activeHandle).toBe("br");
});

describe("partial style overrides", () => {
// Overriding one style property used to replace the whole style object,
// leaving every property the caller did not mention unset.
let tool;

afterEach(() => {
tool?.destroy();
tool = null;
});

it("keeps the focus marker's other defaults", () => {
tool = new VisualImageTool({
imageElement,
focusPoint: { style: { width: "40px" } },
});
tool.toggleFocusPoint(true);
const marker = tool.state.focusMarker;

expect(marker.style.width).toBe("40px");
expect(marker.style.height).toBe("30px");
expect(marker.style.border).toBe("3px solid white");
expect(marker.style.backgroundColor).toBe("rgba(255, 0, 0, 0.5)");
expect(marker.style.boxShadow).not.toBe("");
});

it("keeps the crop overlay's other defaults", () => {
tool = new VisualImageTool({
imageElement,
cropZone: { style: { border: "2px solid red" } },
});
tool.toggleCropZone(true);
const overlay = tool.state.cropOverlay;

expect(overlay.style.border).toBe("2px solid red");
expect(overlay.style.backgroundColor).toBe("rgba(0, 0, 0, 0.4)");
});

it("keeps a resize handle's other defaults", () => {
tool = new VisualImageTool({
imageElement,
cropZone: { handleStyle: { width: "20px" } },
});
tool.toggleCropZone(true);
const handle = tool.cropHandles.find((h) => h.dataset.handle === "br");

expect(handle.style.width).toBe("20px");
expect(handle.style.height).toBe("14px");
expect(handle.style.backgroundColor).toBe("white");
expect(handle.style.border).toBe("2px solid black");
expect(handle.style.boxShadow).not.toBe("");
});

it("still lets scalar options through untouched", () => {
tool = new VisualImageTool({
imageElement,
focusPoint: { enabled: false, style: { width: "40px" } },
});

expect(tool.options.focusPoint.enabled).toBe(false);
expect(tool.options.focusPoint.style.width).toBe("40px");
expect(tool.options.focusPoint.style.height).toBe("30px");

// A disabled feature stays disabled.
tool.toggleFocusPoint(true);
expect(tool.state.focusActive).toBe(false);
});

it("falls back to the defaults when no style is given at all", () => {
tool = new VisualImageTool({
imageElement,
cropZone: { enabled: true },
});

expect(tool.options.cropZone.style.border).toBe("1px dashed #fff");
expect(tool.options.cropZone.handleStyle.width).toBe("14px");
});
});
});