diff --git a/CHANGELOG.md b/CHANGELOG.md index 888b1f2..61e10b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/src/visual-image-tool.js b/src/visual-image-tool.js index e9520f2..acb779f 100644 --- a/src/visual-image-tool.js +++ b/src/visual-image-tool.js @@ -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, diff --git a/src/visual-image-tool.test.js b/src/visual-image-tool.test.js index 3a8e768..f2d1875 100644 --- a/src/visual-image-tool.test.js +++ b/src/visual-image-tool.test.js @@ -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"); + }); + }); });