31 | `;
32 | };
33 |
34 | const createViewer = (overrides = {}) =>
35 | new SVGViewer({
36 | viewerId: "viewer-1",
37 | svgUrl: "https://example.com/test.svg",
38 | initialZoom: 1,
39 | minZoom: 0.5,
40 | maxZoom: 4,
41 | zoomStep: 0.25,
42 | showCoordinates: true,
43 | panMode: "drag",
44 | zoomMode: "scroll",
45 | ...overrides,
46 | });
47 |
48 | beforeEach(() => {
49 | setupDom();
50 | vi.spyOn(window, "requestAnimationFrame").mockImplementation((cb) => {
51 | return setTimeout(() => cb(Date.now()), 0);
52 | });
53 | vi.spyOn(window, "cancelAnimationFrame").mockImplementation((id) => {
54 | clearTimeout(id);
55 | });
56 | });
57 |
58 | afterEach(() => {
59 | vi.restoreAllMocks();
60 | document.body.innerHTML = "";
61 | });
62 |
63 | it("normalizes interaction modes", () => {
64 | expect(SVGViewer.normalizePanMode("Drag")).toBe("drag");
65 | expect(SVGViewer.normalizePanMode("scroll")).toBe("scroll");
66 | expect(SVGViewer.normalizeZoomMode("CLICK")).toBe("click");
67 | expect(SVGViewer.normalizeZoomMode("super scroll")).toBe("super_scroll");
68 | });
69 |
70 | it("initializes with provided options and loads SVG", async () => {
71 | const viewer = createViewer({ showCoordinates: false });
72 |
73 | await flush();
74 |
75 | expect(global.fetch).toHaveBeenCalledWith("https://example.com/test.svg");
76 | expect(viewer.currentZoom).toBe(1);
77 | expect(
78 | document
79 | .querySelector('[data-viewer="viewer-1"].zoom-percentage')
80 | .textContent.trim()
81 | ).toContain("100");
82 | });
83 |
84 | it("clamps zoom values when setZoom is called directly", () => {
85 | const viewer = createViewer({
86 | maxZoom: 1.5,
87 | minZoom: 0.5,
88 | zoomStep: 0.5,
89 | showCoordinates: false,
90 | });
91 |
92 | viewer.setZoom(1.5, { animate: false });
93 | expect(viewer.currentZoom).toBeCloseTo(1.5, 5);
94 |
95 | viewer.setZoom(2.0, { animate: false });
96 | expect(viewer.currentZoom).toBeCloseTo(1.5, 5);
97 |
98 | viewer.setZoom(0.5, { animate: false });
99 | expect(viewer.currentZoom).toBeCloseTo(0.5, 5);
100 |
101 | viewer.setZoom(0.2, { animate: false });
102 | expect(viewer.currentZoom).toBeCloseTo(0.5, 5);
103 | });
104 |
105 | it("propagates slider input to setZoom", async () => {
106 | setupDom({ includeSlider: true });
107 |
108 | const viewer = createViewer({
109 | minZoom: 1,
110 | maxZoom: 2,
111 | zoomStep: 0.1,
112 | showCoordinates: false,
113 | panMode: "scroll",
114 | zoomMode: "super_scroll",
115 | });
116 |
117 | const slider = viewer.zoomSliderEls[0];
118 | expect(slider).toBeDefined();
119 |
120 | slider.value = "150";
121 | const setZoomSpy = vi.spyOn(viewer, "setZoom");
122 | slider.dispatchEvent(new Event("input"));
123 |
124 | await flush();
125 |
126 | expect(setZoomSpy).toHaveBeenLastCalledWith(1.5);
127 | });
128 |
129 | it("copies center coordinates to clipboard when available", async () => {
130 | const viewer = createViewer({
131 | showCoordinates: true,
132 | panMode: "scroll",
133 | zoomMode: "super_scroll",
134 | });
135 |
136 | const copyButton = document.querySelector(
137 | '[data-viewer="viewer-1"].coord-copy-btn'
138 | );
139 | viewer.getVisibleCenterPoint = vi
140 | .fn()
141 | .mockReturnValue({ x: 12.345, y: 67.89 });
142 |
143 | copyButton.click();
144 | await flush();
145 |
146 | expect(navigator.clipboard.writeText).toHaveBeenCalledWith("12.35, 67.89");
147 | });
148 |
149 | it("falls back to prompt when clipboard API is unavailable", async () => {
150 | const viewer = createViewer({
151 | showCoordinates: true,
152 | panMode: "scroll",
153 | zoomMode: "super_scroll",
154 | });
155 |
156 | const copyButton = document.querySelector(
157 | '[data-viewer="viewer-1"].coord-copy-btn'
158 | );
159 | viewer.getVisibleCenterPoint = vi.fn().mockReturnValue({ x: 1.2, y: 3.4 });
160 |
161 | const originalWriteText = navigator.clipboard.writeText;
162 | navigator.clipboard.writeText = undefined;
163 | window.prompt.mockClear();
164 |
165 | copyButton.click();
166 | await flush();
167 |
168 | expect(window.prompt).toHaveBeenCalledWith(
169 | "Copy coordinates",
170 | "1.20, 3.40"
171 | );
172 |
173 | navigator.clipboard.writeText = originalWriteText;
174 | });
175 |
176 | it("invokes performWheelZoom for eligible wheel events", async () => {
177 | const viewer = createViewer({
178 | maxZoom: 3,
179 | panMode: "scroll",
180 | zoomMode: "scroll",
181 | showCoordinates: false,
182 | });
183 |
184 | const container = document.querySelector(
185 | '[data-viewer="viewer-1"].svg-container'
186 | );
187 | viewer.baseDimensions = { width: 2000, height: 1000 };
188 | viewer.baseOrigin = { x: 0, y: 0 };
189 | viewer.unitsPerCss = { x: 1, y: 1 };
190 | Object.defineProperty(container, "clientWidth", { value: 500 });
191 | Object.defineProperty(container, "clientHeight", { value: 400 });
192 | Object.defineProperty(container, "scrollWidth", { value: 600 });
193 | Object.defineProperty(container, "scrollHeight", { value: 800 });
194 |
195 | const performWheelZoomSpy = vi.spyOn(viewer, "performWheelZoom");
196 |
197 | const wheelEvent = new WheelEvent("wheel", {
198 | deltaY: -160,
199 | clientX: 250,
200 | clientY: 200,
201 | bubbles: true,
202 | cancelable: true,
203 | });
204 |
205 | viewer.handleMouseWheel(wheelEvent);
206 | await flush();
207 |
208 | expect(performWheelZoomSpy).toHaveBeenCalled();
209 | });
210 |
211 | it("adjusts container scroll when zooming around a focus point", () => {
212 | const viewer = createViewer({
213 | maxZoom: 3,
214 | panMode: "scroll",
215 | zoomMode: "scroll",
216 | showCoordinates: false,
217 | });
218 |
219 | const container = document.querySelector(
220 | '[data-viewer="viewer-1"].svg-container'
221 | );
222 | viewer.baseDimensions = { width: 2000, height: 1000 };
223 | viewer.baseOrigin = { x: 0, y: 0 };
224 | viewer.unitsPerCss = { x: 1, y: 1 };
225 | Object.defineProperty(container, "clientWidth", { value: 500 });
226 | Object.defineProperty(container, "clientHeight", { value: 400 });
227 | Object.defineProperty(container, "scrollWidth", {
228 | get() {
229 | return 800;
230 | },
231 | });
232 | Object.defineProperty(container, "scrollHeight", {
233 | get() {
234 | return 900;
235 | },
236 | });
237 |
238 | viewer.setZoom(1.5, {
239 | animate: false,
240 | focusX: 400,
241 | focusY: 300,
242 | focusOffsetX: 200,
243 | focusOffsetY: 150,
244 | });
245 |
246 | expect(container.scrollLeft).toBeGreaterThanOrEqual(0);
247 | expect(container.scrollTop).toBeGreaterThanOrEqual(0);
248 | });
249 |
250 | it("disables zoom buttons when reaching bounds", async () => {
251 | const viewer = createViewer({
252 | maxZoom: 1.5,
253 | minZoom: 0.5,
254 | zoomStep: 0.5,
255 | showCoordinates: false,
256 | });
257 |
258 | const zoomInButton = document.querySelector(
259 | '[data-viewer="viewer-1"].zoom-in-btn'
260 | );
261 | const zoomOutButton = document.querySelector(
262 | '[data-viewer="viewer-1"].zoom-out-btn'
263 | );
264 |
265 | viewer.setZoom(1.5, { animate: false });
266 | await flush();
267 |
268 | expect(zoomInButton.disabled).toBe(true);
269 | expect(zoomInButton.getAttribute("aria-disabled")).toBe("true");
270 | expect(zoomOutButton.disabled).toBe(false);
271 |
272 | viewer.setZoom(0.5, { animate: false });
273 | await flush();
274 |
275 | expect(zoomOutButton.disabled).toBe(true);
276 | expect(zoomOutButton.getAttribute("aria-disabled")).toBe("true");
277 | expect(zoomInButton.disabled).toBe(false);
278 | });
279 |
280 | it("enforces zoom bounds through zoom buttons", async () => {
281 | const viewer = createViewer({
282 | maxZoom: 1.5,
283 | minZoom: 0.5,
284 | zoomStep: 0.5,
285 | showCoordinates: false,
286 | });
287 |
288 | viewer.setZoom(viewer.computeZoomTarget("in"), { animate: false });
289 | expect(viewer.currentZoom).toBeCloseTo(1.5, 5);
290 |
291 | viewer.setZoom(viewer.computeZoomTarget("out"), { animate: false });
292 | expect(viewer.currentZoom).toBeCloseTo(1.0, 5);
293 |
294 | viewer.setZoom(viewer.computeZoomTarget("out"), { animate: false });
295 | expect(viewer.currentZoom).toBeCloseTo(0.5, 5);
296 | });
297 | });
298 |
--------------------------------------------------------------------------------
/tests/test-plugin.php:
--------------------------------------------------------------------------------
1 | register_preset_post_type();
27 |
28 | if (!function_exists('sanitize_hex_color')) {
29 | require_once ABSPATH . WPINC . '/formatting.php';
30 |
31 | if (!function_exists('sanitize_hex_color')) {
32 | function sanitize_hex_color($color)
33 | {
34 | $color = is_string($color) ? trim($color) : '';
35 |
36 | if ($color === '') {
37 | return false;
38 | }
39 |
40 | if ($color[0] !== '#') {
41 | $color = '#' . $color;
42 | }
43 |
44 | if (preg_match('/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/', $color)) {
45 | return strtolower($color);
46 | }
47 |
48 | return false;
49 | }
50 | }
51 | }
52 | }
53 |
54 | /**
55 | * Inline shortcode attributes should override preset values.
56 | */
57 | public function test_shortcode_inline_overrides_preset_values()
58 | {
59 | $uploads_url = 'https://example.com/uploads';
60 |
61 | $uploads_filter = static function ($dirs) use ($uploads_url) {
62 | $dirs['baseurl'] = $uploads_url;
63 | return $dirs;
64 | };
65 | add_filter('upload_dir', $uploads_filter);
66 |
67 | $preset_id = self::factory()->post->create(
68 | array(
69 | 'post_type' => 'btsvviewer_preset',
70 | 'post_status' => 'publish',
71 | 'post_title' => 'Override Test Preset',
72 | )
73 | );
74 |
75 | update_post_meta($preset_id, '_btsvviewer_src', 'preset-image.svg');
76 | update_post_meta($preset_id, '_btsvviewer_button_fill', '#123456');
77 |
78 | $output = self::$plugin->render_shortcode(
79 | array(
80 | 'id' => $preset_id,
81 | 'button_fill' => '#abcdef',
82 | 'height' => '720px',
83 | )
84 | );
85 |
86 | $this->assertStringContainsString('--bt-svg-viewer-button-fill: #abcdef', $output);
87 | $this->assertStringContainsString('style="height: 720px"', $output);
88 |
89 | remove_filter('upload_dir', $uploads_filter);
90 | }
91 |
92 | /**
93 | * Missing preset IDs should return an error message.
94 | */
95 | public function test_render_shortcode_returns_error_when_preset_missing()
96 | {
97 | $output = self::$plugin->render_shortcode(array('id' => 999999));
98 |
99 | $this->assertStringContainsString(
100 | 'Error: SVG preset not found',
101 | wp_strip_all_tags($output)
102 | );
103 | }
104 |
105 | /**
106 | * Helper should sanitize color declarations and infer defaults.
107 | */
108 | public function test_get_button_color_style_declarations_sanitizes_values()
109 | {
110 | $method = new ReflectionMethod(BT_SVG_Viewer::class, 'get_button_color_style_declarations');
111 | $method->setAccessible(true);
112 |
113 | $declarations = $method->invoke(self::$plugin, '#AaCcDd', '', ' #F0F0F0 ');
114 |
115 | $this->assertContains('--bt-svg-viewer-button-fill: #aaccdd', $declarations);
116 | $this->assertContains('--bt-svg-viewer-button-border: #aaccdd', $declarations);
117 | $this->assertContains('--bt-svg-viewer-button-text: #f0f0f0', $declarations);
118 | }
119 |
120 | /**
121 | * adjust_color_brightness should clamp values inside 0-255 and handle extremes.
122 | */
123 | public function test_adjust_color_brightness_handles_extremes()
124 | {
125 | $method = new ReflectionMethod(BT_SVG_Viewer::class, 'adjust_color_brightness');
126 | $method->setAccessible(true);
127 |
128 | $this->assertSame('#ffffff', $method->invoke(self::$plugin, '#ffffff', 15.0));
129 | $this->assertSame('#000000', $method->invoke(self::$plugin, '#000000', -20.0));
130 | $this->assertSame('#0d0d0d', $method->invoke(self::$plugin, '#000000', 5.0));
131 | }
132 |
133 | /**
134 | * build_style_attribute should trim duplicates and semicolons.
135 | */
136 | public function test_build_style_attribute_trims_duplicates()
137 | {
138 | $method = new ReflectionMethod(BT_SVG_Viewer::class, 'build_style_attribute');
139 | $method->setAccessible(true);
140 |
141 | $result = $method->invoke(
142 | self::$plugin,
143 | array(
144 | ' color: red; ',
145 | 'color: red',
146 | 'background: #fff;;',
147 | '',
148 | )
149 | );
150 |
151 | $this->assertSame('color: red; background: #fff', $result);
152 | }
153 |
154 | /**
155 | * Ensure the plugin adds SVG support to the mime type list.
156 | */
157 | public function test_btsvviewer_mime_type_is_added()
158 | {
159 | $existing = array(
160 | 'jpg' => 'image/jpeg',
161 | );
162 |
163 | $result = self::$plugin->svg_use_mimetypes($existing);
164 |
165 | $this->assertArrayHasKey('svg', $result);
166 | $this->assertSame('image/svg+xml', $result['svg']);
167 | }
168 |
169 | /**
170 | * Rendering without a source should return a descriptive error message.
171 | */
172 | public function test_render_shortcode_requires_source()
173 | {
174 | $output = self::$plugin->render_shortcode(array());
175 |
176 | $this->assertStringContainsString(
177 | 'Error: SVG source not specified.',
178 | wp_strip_all_tags($output)
179 | );
180 | }
181 |
182 | /**
183 | * Rendering with button color aliases should normalize and sanitize styles.
184 | */
185 | public function test_render_shortcode_generates_button_styles_from_aliases()
186 | {
187 | $output = self::$plugin->render_shortcode(
188 | array(
189 | 'src' => 'https://example.com/test.svg',
190 | 'button_bg' => '#336699',
191 | 'button_border' => 'not-a-color',
192 | 'button_foreground' => '#ffffff',
193 | )
194 | );
195 |
196 | $this->assertStringContainsString('--bt-svg-viewer-button-fill: #336699', $output);
197 | $this->assertStringContainsString('--bt-svg-viewer-button-border: #336699', $output);
198 | $this->assertStringContainsString('--bt-svg-viewer-button-text: #ffffff', $output);
199 | }
200 |
201 | /**
202 | * Interaction configuration should resolve conflicting pan/zoom modes and expose helper messages.
203 | */
204 | public function test_resolve_interaction_config_adjusts_pan_mode_and_messages()
205 | {
206 | $method = new ReflectionMethod(BT_SVG_Viewer::class, 'resolve_interaction_config');
207 | $method->setAccessible(true);
208 |
209 | $result = $method->invoke(self::$plugin, 'scroll', 'scroll');
210 |
211 | $this->assertSame('drag', $result['pan_mode']);
212 | $this->assertSame('scroll', $result['zoom_mode']);
213 | $this->assertContains(
214 | 'Scroll up to zoom in, scroll down to zoom out.',
215 | $result['messages']
216 | );
217 | $this->assertContains(
218 | 'Drag to pan around the image while scrolling zooms.',
219 | $result['messages']
220 | );
221 | }
222 |
223 | /**
224 | * Shortcode rendering should pick up preset data when provided via ID.
225 | */
226 | public function test_render_shortcode_uses_preset_values()
227 | {
228 | $uploads_url = 'https://example.com/uploads';
229 |
230 | $uploads_filter = static function ($dirs) use ($uploads_url) {
231 | $dirs['baseurl'] = $uploads_url;
232 | return $dirs;
233 | };
234 | add_filter('upload_dir', $uploads_filter);
235 |
236 | $preset_id = self::factory()->post->create(
237 | array(
238 | 'post_type' => 'btsvviewer_preset',
239 | 'post_status' => 'publish',
240 | 'post_title' => 'Preset',
241 | )
242 | );
243 |
244 | update_post_meta($preset_id, '_btsvviewer_src', 'preset-path.svg');
245 | update_post_meta($preset_id, '_btsvviewer_initial_zoom', 200);
246 | update_post_meta($preset_id, '_btsvviewer_pan_mode', 'drag');
247 | update_post_meta($preset_id, '_btsvviewer_zoom_mode', 'click');
248 |
249 | $output = self::$plugin->render_shortcode(
250 | array(
251 | 'id' => $preset_id,
252 | )
253 | );
254 |
255 | $this->assertStringContainsString('bt-svg-viewer-wrapper', $output);
256 | $this->assertStringContainsString('bt-svg-viewer-main', $output);
257 | $this->assertStringContainsString($uploads_url . '/preset-path.svg', $output);
258 | $this->assertStringContainsString('controls-mode', $output);
259 |
260 | remove_filter('upload_dir', $uploads_filter);
261 | }
262 |
263 | /**
264 | * Rendering through do_shortcode should enqueue frontend assets.
265 | */
266 | public function test_shortcode_enqueue_assets()
267 | {
268 | if (wp_script_is('bt-svg-viewer-script')) {
269 | wp_dequeue_script('bt-svg-viewer-script');
270 | }
271 | if (wp_style_is('bt-svg-viewer-style')) {
272 | wp_dequeue_style('bt-svg-viewer-style');
273 | }
274 |
275 | $this->assertFalse(wp_style_is('bt-svg-viewer-style', 'enqueued'));
276 | $this->assertFalse(wp_script_is('bt-svg-viewer-script', 'enqueued'));
277 |
278 | do_action('wp_enqueue_scripts');
279 |
280 | $this->assertTrue(wp_style_is('bt-svg-viewer-style', 'enqueued'));
281 | }
282 |
283 | }
--------------------------------------------------------------------------------
/bt-svg-viewer/css/bt-svg-viewer.css:
--------------------------------------------------------------------------------
1 | /* BT SVG Viewer Plugin Styles */
2 |
3 | .bt-svg-viewer-wrapper {
4 | display: flex;
5 | flex-direction: column;
6 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen,
7 | Ubuntu, Cantarell, sans-serif;
8 | border: 1px solid #ddd;
9 | border-radius: 4px;
10 | overflow: hidden;
11 | background-color: #fff;
12 | --bt-svg-viewer-button-fill: #0073aa;
13 | --bt-svg-viewer-button-hover: #005a87;
14 | --bt-svg-viewer-button-border: #0073aa;
15 | --bt-svg-viewer-button-text: #fff;
16 | }
17 |
18 | .bt-svg-viewer-main {
19 | display: flex;
20 | flex-direction: column;
21 | gap: 0;
22 | }
23 |
24 | .bt-svg-viewer-main.controls-position-bottom {
25 | flex-direction: column-reverse;
26 | }
27 |
28 | .bt-svg-viewer-main.controls-position-left {
29 | flex-direction: row;
30 | }
31 |
32 | .bt-svg-viewer-main.controls-position-right {
33 | flex-direction: row-reverse;
34 | }
35 |
36 | .bt-svg-viewer-main.controls-position-left,
37 | .bt-svg-viewer-main.controls-position-right {
38 | align-items: stretch;
39 | }
40 |
41 | .bt-svg-viewer-main.controls-position-left .svg-controls,
42 | .bt-svg-viewer-main.controls-position-right .svg-controls {
43 | flex: 0 0 auto;
44 | }
45 |
46 | .bt-svg-viewer-main > .svg-container {
47 | flex: 1 1 auto;
48 | }
49 |
50 | .bt-svg-viewer-title,
51 | .bt-svg-viewer-caption {
52 | text-align: center;
53 | font-weight: 600;
54 | margin: 0;
55 | padding: 12px 16px;
56 | }
57 |
58 | .bt-svg-viewer-interaction-caption {
59 | font-weight: 400;
60 | font-size: 0.9em;
61 | color: #555;
62 | background-color: #fafafa;
63 | }
64 |
65 | .bt-svg-viewer-title {
66 | border-bottom: 1px solid #ddd;
67 | }
68 |
69 | .svg-controls {
70 | display: flex;
71 | gap: 10px;
72 | padding: 15px;
73 | background-color: #f5f5f5;
74 | border-bottom: 1px solid #ddd;
75 | flex-wrap: wrap;
76 | align-items: center;
77 | justify-content: flex-start;
78 | }
79 |
80 | .bt-svg-viewer-main.controls-position-bottom .svg-controls {
81 | border-top: 1px solid #ddd;
82 | border-bottom: none;
83 | }
84 |
85 | .bt-svg-viewer-main.controls-position-left .svg-controls,
86 | .bt-svg-viewer-main.controls-position-right .svg-controls {
87 | border-bottom: none;
88 | align-items: flex-start;
89 | justify-content: center;
90 | }
91 |
92 | .bt-svg-viewer-main.controls-position-left.controls-align-alignleft .svg-controls,
93 | .bt-svg-viewer-main.controls-position-right.controls-align-alignleft
94 | .svg-controls {
95 | align-items: flex-start;
96 | }
97 |
98 | .bt-svg-viewer-main.controls-position-left.controls-align-alignright .svg-controls,
99 | .bt-svg-viewer-main.controls-position-right.controls-align-alignright
100 | .svg-controls {
101 | align-items: flex-end;
102 | }
103 |
104 | .bt-svg-viewer-main.controls-position-left.controls-align-aligncenter
105 | .svg-controls,
106 | .bt-svg-viewer-main.controls-position-right.controls-align-aligncenter
107 | .svg-controls {
108 | align-items: center;
109 | }
110 |
111 | .bt-svg-viewer-main.controls-position-left .svg-controls {
112 | border-right: 1px solid #ddd;
113 | }
114 |
115 | .bt-svg-viewer-main.controls-position-right .svg-controls {
116 | border-left: 1px solid #ddd;
117 | }
118 |
119 | .svg-controls.controls-vertical {
120 | flex-direction: column;
121 | gap: 12px;
122 | align-items: stretch;
123 | }
124 |
125 | .svg-controls .zoom-slider-wrapper {
126 | display: inline-flex;
127 | align-items: center;
128 | width: var(--bt-svg-viewer-slider-width, 240px);
129 | padding: 0 4px;
130 | }
131 |
132 | .svg-controls.controls-mode-icon .zoom-slider-wrapper {
133 | width: var(--bt-svg-viewer-slider-width-icon, 200px);
134 | }
135 |
136 | .svg-controls.controls-vertical .zoom-slider-wrapper {
137 | width: 100%;
138 | }
139 |
140 | .svg-controls .zoom-slider {
141 | width: 100%;
142 | height: 6px;
143 | appearance: none;
144 | -webkit-appearance: none;
145 | background: var(--bt-svg-viewer-slider-track, rgba(0, 0, 0, 0.15));
146 | border-radius: 999px;
147 | outline: none;
148 | }
149 |
150 | .svg-controls .zoom-slider::-webkit-slider-thumb {
151 | -webkit-appearance: none;
152 | appearance: none;
153 | width: 18px;
154 | height: 18px;
155 | border-radius: 50%;
156 | background: var(--bt-svg-viewer-button-fill, #0073aa);
157 | border: 2px solid var(--bt-svg-viewer-button-border, #0073aa);
158 | cursor: pointer;
159 | box-shadow: 0 0 0 2px #fff;
160 | }
161 |
162 | .svg-controls .zoom-slider::-moz-range-thumb {
163 | width: 18px;
164 | height: 18px;
165 | border-radius: 50%;
166 | background: var(--bt-svg-viewer-button-fill, #0073aa);
167 | border: 2px solid var(--bt-svg-viewer-button-border, #0073aa);
168 | cursor: pointer;
169 | }
170 |
171 | .svg-controls .zoom-slider:focus-visible {
172 | outline: 2px solid var(--bt-svg-viewer-button-fill, #0073aa);
173 | outline-offset: 2px;
174 | }
175 |
176 | .bt-svg-viewer-btn {
177 | display: inline-flex;
178 | align-items: center;
179 | gap: 6px;
180 | padding: 8px 16px;
181 | background-color: var(--bt-svg-viewer-button-fill, #0073aa);
182 | color: var(--bt-svg-viewer-button-text, #fff);
183 | border: 1px solid var(--bt-svg-viewer-button-border, #0073aa);
184 | border-radius: 4px;
185 | cursor: pointer;
186 | font-size: 14px;
187 | font-weight: 500;
188 | transition: background-color 0.2s ease, border-color 0.2s ease,
189 | transform 0.1s ease;
190 | white-space: nowrap;
191 | }
192 | .bt-svg-viewer-btn.is-disabled,
193 | .bt-svg-viewer-btn:disabled {
194 | background-color: #c9ccd1;
195 | border-color: #b2b5ba;
196 | color: #f1f1f1;
197 | cursor: not-allowed;
198 | opacity: 0.65;
199 | }
200 | .bt-svg-viewer-btn.is-disabled:hover,
201 | .bt-svg-viewer-btn:disabled:hover {
202 | background-color: #c9ccd1;
203 | }
204 |
205 | .bt-svg-viewer-btn .btn-icon {
206 | font-size: 16px;
207 | line-height: 1;
208 | }
209 |
210 | .bt-svg-viewer-btn .btn-text {
211 | font-size: 14px;
212 | line-height: 1.3;
213 | }
214 |
215 | .bt-svg-viewer-btn .btn-icon svg {
216 | display: block;
217 | width: 16px;
218 | height: 16px;
219 | }
220 |
221 | .bt-svg-viewer-btn:hover {
222 | background-color: var(
223 | --bt-svg-viewer-button-hover,
224 | var(--bt-svg-viewer-button-fill, #0073aa)
225 | );
226 | }
227 |
228 | .bt-svg-viewer-btn:active {
229 | transform: scale(0.98);
230 | }
231 |
232 | .svg-controls .divider {
233 | width: 1px;
234 | height: 24px;
235 | background-color: #ddd;
236 | margin: 0 8px;
237 | }
238 |
239 | .svg-controls.controls-vertical .divider {
240 | width: 100%;
241 | height: 1px;
242 | margin: 8px 0;
243 | }
244 |
245 | .zoom-display {
246 | font-size: 14px;
247 | color: #666;
248 | min-width: 60px;
249 | text-align: center;
250 | padding: 4px 8px;
251 | }
252 |
253 | .controls-mode-icon .btn-text {
254 | display: none;
255 | }
256 |
257 | .controls-mode-icon .bt-svg-viewer-btn {
258 | padding: 8px;
259 | justify-content: center;
260 | }
261 |
262 | .controls-mode-icon .bt-svg-viewer-btn .btn-icon svg {
263 | width: 20px;
264 | height: 20px;
265 | }
266 |
267 | .controls-mode-text .btn-icon {
268 | display: none;
269 | }
270 |
271 | .controls-style-compact .bt-svg-viewer-btn {
272 | padding: 6px 12px;
273 | font-size: 13px;
274 | }
275 |
276 | .controls-style-compact .svg-controls {
277 | gap: 8px;
278 | padding: 12px;
279 | }
280 |
281 | .controls-style-labels-on-hover .btn-text {
282 | opacity: 0;
283 | max-width: 0;
284 | overflow: hidden;
285 | transition: opacity 0.2s ease, max-width 0.2s ease;
286 | }
287 |
288 | .controls-style-labels-on-hover .bt-svg-viewer-btn:hover .btn-text,
289 | .controls-style-labels-on-hover .bt-svg-viewer-btn:focus .btn-text,
290 | .controls-style-labels-on-hover .bt-svg-viewer-btn:focus-visible .btn-text {
291 | opacity: 1;
292 | max-width: 200px;
293 | }
294 |
295 | .svg-controls.controls-vertical .bt-svg-viewer-btn {
296 | width: 100%;
297 | justify-content: center;
298 | }
299 |
300 | .svg-controls.controls-align-aligncenter {
301 | justify-content: center;
302 | }
303 |
304 | .svg-controls.controls-align-alignright {
305 | justify-content: flex-end;
306 | }
307 |
308 | .svg-controls.controls-align-alignleft {
309 | justify-content: flex-start;
310 | }
311 |
312 | .svg-controls.controls-vertical.controls-align-alignleft {
313 | align-items: flex-start;
314 | }
315 |
316 | .svg-controls.controls-vertical.controls-align-alignright {
317 | align-items: flex-end;
318 | }
319 |
320 | .svg-controls.controls-vertical.controls-align-aligncenter {
321 | align-items: center;
322 | }
323 |
324 | .bt-svg-viewer-main.controls-align-aligncenter .svg-controls {
325 | justify-content: center;
326 | }
327 |
328 | .bt-svg-viewer-main.controls-align-alignright .svg-controls {
329 | justify-content: flex-end;
330 | }
331 |
332 | .bt-svg-viewer-main.controls-align-alignleft .svg-controls {
333 | justify-content: flex-start;
334 | }
335 |
336 | .svg-controls.controls-vertical .coord-output {
337 | margin-left: 0;
338 | margin-top: 4px;
339 | }
340 |
341 | .svg-controls.controls-vertical .zoom-display {
342 | align-self: center;
343 | }
344 |
345 | .svg-container {
346 | flex: 0 0 auto;
347 | width: 100%;
348 | overflow: auto;
349 | background-color: #fff;
350 | position: relative;
351 | display: block;
352 | scroll-behavior: smooth;
353 | }
354 |
355 | .bt-svg-viewer-wrapper.pan-mode-drag .svg-container {
356 | cursor: grab;
357 | touch-action: pan-x pan-y;
358 | }
359 |
360 | .bt-svg-viewer-wrapper.pan-mode-drag .svg-container.is-dragging {
361 | cursor: grabbing;
362 | }
363 |
364 | .bt-svg-viewer-wrapper.pan-mode-drag .svg-container.is-dragging,
365 | .bt-svg-viewer-wrapper.pan-mode-drag .svg-container.is-dragging * {
366 | user-select: none;
367 | }
368 |
369 | .bt-svg-viewer-caption {
370 | border-top: 1px solid #ddd;
371 | }
372 |
373 | .svg-viewport {
374 | display: inline-block;
375 | transform-origin: top left;
376 | transition: none;
377 | }
378 |
379 | .svg-container svg {
380 | display: block;
381 | background-color: #f3f3f3;
382 | }
383 |
384 | /* Responsive adjustments */
385 | @media (max-width: 768px) {
386 | .bt-svg-viewer-btn {
387 | padding: 6px 12px;
388 | font-size: 13px;
389 | }
390 |
391 | .bt-svg-viewer-btn .btn-icon {
392 | font-size: 14px;
393 | }
394 |
395 | .svg-controls {
396 | gap: 8px;
397 | padding: 12px;
398 | }
399 |
400 | .zoom-display {
401 | font-size: 12px;
402 | }
403 | }
404 |
405 | /* Fix for WordPress admin/editor environments */
406 | .wp-block-html .bt-svg-viewer-wrapper {
407 | max-width: 100%;
408 | }
409 |
410 | .bt-svg-viewer-wrapper .svg-controls .zoom-display {
411 | display: inline-flex;
412 | align-items: center;
413 | gap: 0.25rem;
414 | justify-content: center;
415 | }
416 |
417 | .bt-svg-viewer-wrapper .svg-controls .coord-output {
418 | margin-left: 0.75rem;
419 | font-family: Menlo, Monaco, Consolas, "Liberation Mono", "Courier New",
420 | monospace;
421 | font-size: 0.85rem;
422 | color: #444;
423 | }
424 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | # BT SVG Viewer
3 |
4 | Embed large SVG diagrams in WordPress with zoom, pan, center, and authoring tools. Recent releases add a visual preset editor, icon-based controls, deeper shortcode options, and configurable button colors.
5 |
6 | ---
7 |
8 | ## Contents
9 |
10 | - [Contents](#contents)
11 | - [Installation](#installation)
12 | - [Quick Start](#quick-start)
13 | - [Shortcode Reference](#shortcode-reference)
14 | - [`controls_buttons` Cheat Sheet](#controls_buttons-cheat-sheet)
15 | - [Admin Preset Editor](#admin-preset-editor)
16 | - [Location](#location)
17 | - [Fields](#fields)
18 | - [Preview Pane](#preview-pane)
19 | - [Preset Shortcodes](#preset-shortcodes)
20 | - [Defaults Tab](#defaults-tab)
21 | - [Using Presets in Posts/Pages](#using-presets-in-postspages)
22 | - [Preview Workflow](#preview-workflow)
23 | - [Examples](#examples)
24 | - [Styling (CSS Hooks)](#styling-css-hooks)
25 | - [Tips \& Troubleshooting](#tips--troubleshooting)
26 | - [SVG Preparation](#svg-preparation)
27 | - [Common Issues](#common-issues)
28 | - [Debugging](#debugging)
29 | - [Changelog Highlights (1.1.0)](#changelog-highlights-110)
30 | - [License \& Credits](#license--credits)
31 |
32 | ---
33 |
34 | ## Installation
35 |
36 | 1. **Unzip the plugin archive**
37 | - Download [bt-svg-viewer.zip](github.com/ttscoff/bt-svg-viewer/releases/latest/download/bt-svg-viewer.zip).
38 | - Unzip it locally; you will get a folder named `bt-svg-viewer`.
39 | 2. **Upload the plugin**
40 | - Copy the entire `bt-svg-viewer` folder into your WordPress installation at `/wp-content/plugins/`.
41 | 3. **Activate**
42 | - In the WordPress admin, navigate to **Plugins** and click **Activate** on ???BT SVG Viewer???.
43 |
44 | ---
45 |
46 | ## Quick Start
47 |
48 | ```text
49 | [btsvviewer src="/wp-content/uploads/diagrams/system-map.svg"]
50 | ```
51 |
52 | - Place the shortcode in a classic editor, Gutenberg shortcode block, or template.
53 | - The SVG renders with default height (600px), zoom controls, pan/scroll behaviour, keyboard shortcuts, and responsive layout. Zoom buttons now gray out at the minimum/maximum zoom to make the limits obvious to visitors.
54 | - Existing shortcodes created before the rename continue to render, so archived posts don???t need updates.
55 |
56 | ---
57 |
58 | ## Shortcode Reference
59 |
60 | | Attribute | Type | Default | Description |
61 | | -------------------------------------------------- | ----------------------------------- | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
62 | | `src` | string (required) | ??? | SVG URL. Supports absolute URLs, `/absolute/path.svg`, or relative to the uploads directory. |
63 | | `height` | string | `600px` | CSS height of the viewer. Accepts px, vh, %, etc. |
64 | | `class` | string | ??? | Additional class appended to the wrapper. |
65 | | `zoom` | number | `100` | Initial zoom percentage. |
66 | | `min_zoom` | number | `25` | Minimum zoom percentage allowed. |
67 | | `max_zoom` | number | `800` | Maximum zoom percentage allowed. |
68 | | `zoom_step` | number | `10` | Increment used by buttons/keyboard shortcuts. |
69 | | `initial_zoom` | number | ??? | Alias captured when presets save the preview state. Overrides `zoom` if present. |
70 | | `pan` / `pan_mode` | `scroll` or `drag` | `scroll` | Toggle between scroll-wheel panning and click-drag panning. Drag is enforced when zoom modes require it. |
71 | | `zoom_mode` / `zoom_behavior` / `zoom_interaction` | `super_scroll` / `scroll` / `click` | `super_scroll` | Choose how wheel and modifier gestures zoom: Cmd/Ctrl + wheel (`super_scroll`), every wheel (`scroll`), or Cmd/Ctrl-click & Alt-click (`click`). |
72 | | `center_x` / `center_y` | number | ??? | Manual center point in SVG units. Defaults to viewBox center. |
73 | | `show_coords` | boolean | `false` | Appends ???Copy Center??? button for debugging coordinate values. |
74 | | `controls_position` | `top`/`bottom`/`left`/`right` | `top` | Placement of the entire control group. |
75 | | `controls_buttons` | string | `both` | Comma-delimited mode/align/button list. See table below (supports `slider`). |
76 | | `title` | string | ??? | Optional heading above the viewer. HTML allowed. |
77 | | `caption` | string | ??? | Optional caption below the viewer. HTML allowed. |
78 | | `button_fill` / `button_background` / `button_bg` | color string | theme default (`#0073aa`) | Button background color. Aliases exist for backwards compatibility (all map to `button_fill`). |
79 | | `button_border` | color string | matches fill | Outline color for buttons. Blank inherits the fill color. |
80 | | `button_foreground` / `button_fg` | color string | `#ffffff` | Text and icon color for buttons. Blank uses the default. |
81 | | `id` | number | ??? | Reference a saved preset (admin). Inline attributes override preset values. |
82 |
83 | > Changing the interaction defaults automatically inserts a helper caption (e.g. ???Cmd/Ctrl-click to zoom in??????) above your custom caption so visitors know the gesture.
84 |
85 | ### `controls_buttons` Cheat Sheet
86 |
87 | ```text
88 | controls_buttons="MODE,ALIGNMENT,BUTTON_1,BUTTON_2,..."
89 | ```
90 |
91 | Mode keywords (optional):
92 |
93 | - `both` (default)
94 | - `icon`
95 | - `text`
96 | - `compact`
97 | - `labels_on_hover`
98 | - `minimal`
99 | - `hidden` / `none`
100 |
101 | Alignment keywords (optional, anywhere in list):
102 |
103 | - `alignleft`
104 | - `aligncenter`
105 | - `alignright`
106 |
107 | Additional keywords:
108 |
109 | - `slider` ??? replaces zoom buttons with a live-updating range input. Combine with `icon`/`text`/`both` for layout. Use `custom:slider,zoom_in,zoom_out` to show both slider and buttons.
110 |
111 | Button names (pick any order):
112 |
113 | - `zoom_in`, `zoom_out`, `reset`, `center`, `coords` *(coords button only renders if `show_coords="true"`)*
114 |
115 | Example:
116 |
117 | ```text
118 | [btsvviewer
119 | src="/uploads/system-map.svg"
120 | controls_position="right"
121 | controls_buttons="slider,icon,aligncenter,reset,center"
122 | ]
123 | ```
124 |
125 | ---
126 |
127 | ## Admin Preset Editor
128 |
129 | ### Location
130 |
131 | `WordPress Dashboard ??? BT SVG Viewer ??? Presets`
132 |
133 | ### Fields
134 |
135 | | Field | Description |
136 | | --------------------------- | ------------------------------------------------------------------------------ |
137 | | **SVG Source URL** | Choose or upload an SVG from the media library. Required before preview loads. |
138 | | **Viewer Height** | Same as shortcode `height`. |
139 | | **Zoom Settings** | Minimum, maximum, step, and initial zoom percentages. |
140 | | **Center Coordinates** | Override auto centering with explicit `center_x`/`center_y`. |
141 | | **Controls Position** | Dropdown for `top`, `bottom`, `left`, `right`. |
142 | | **Controls Buttons/Layout** | Text field following the `MODE,ALIGN,buttons???` pattern described above. |
143 | | **Button Colors** | Three color pickers for fill, border, and foreground (text/icon) colors. |
144 | | **Title & Caption** | Optional, displayed above/below the viewer wrapper. |
145 |
146 | ### Preview Pane
147 |
148 | - **Load / Refresh Preview**: Injects the SVG using the current field values.
149 | - **Use Current View for Initial State**: Captures the visible zoom level and center point from the preview and writes them back into the form (ideal for fine-tuning coordinates visually).
150 | - **Copy Center**: When `show_coords` is enabled, the button copies coordinates to the clipboard.
151 | - Zoom controls in the preview (and front end) now snap to their minimum/maximum, disable the corresponding buttons, and keep your pointer-focused zoom locked to the selected point???what you see in the preview is exactly what site visitors experience.
152 |
153 | ### Preset Shortcodes
154 |
155 | - At the top of the preset editor and in the presets list table you???ll find a copy-ready snippet in the form of `[btsvviewer id="123"]`.
156 | - Click **Copy** to put the shortcode on the clipboard without selecting manually.
157 |
158 | ### Defaults Tab
159 |
160 | - Visit **BT SVG Viewer ??? Presets ??? Defaults** to seed the fields used when creating a new preset.
161 | - The panel now includes **Enable asset cache busting for debugging**, which appends a time-based suffix to scripts and styles. It is automatically active on hosts that start with `dev.` or `wptest.` and can be toggled manually when you need to defeat browser caching.
162 |
163 | ---
164 |
165 | ## Using Presets in Posts/Pages
166 |
167 | 1. Create or edit a preset as described above.
168 | 2. Copy the generated shortcode (`[btsvviewer id="123"]`).
169 | 3. Paste into:
170 | - Classic editor (Visual tab: use Shortcode block or paste directly).
171 | - Block editor (Shortcode block or HTML block).
172 | - Template files via `do_shortcode()`.
173 | 4. Override on a per-use basis if needed:
174 |
175 | ```text
176 | [btsvviewer id="123" controls_buttons="icon,alignright,zoom_in,zoom_out"]
177 | ```
178 |
179 | ---
180 |
181 | ## Preview Workflow
182 |
183 | 1. **Load / Refresh Preview** once the SVG path is entered.
184 | 2. Drag or scroll the SVG to the desired default view.
185 | 3. **Use Current View for Initial State** to stash those coordinates/zoom.
186 | 4. Optionally toggle `show_coords` to display the current x/y values of the viewport center.
187 | 5. Save/publish the preset and test the shortcode on the front end.
188 |
189 | ---
190 |
191 | ## Examples
192 |
193 | ```text
194 | [btsvviewer src="/uploads/floorplan.svg"]
195 | ```
196 |
197 | ```text
198 | [btsvviewer
199 | src="/uploads/system-map.svg"
200 | height="100vh"
201 | controls_position="bottom"
202 | controls_buttons="compact,aligncenter,zoom_in,zoom_out,reset,center"
203 | zoom="175"
204 | min_zoom="50"
205 | max_zoom="400"
206 | ]
207 | ```
208 |
209 | ```text
210 | [btsvviewer
211 | id="42"
212 | controls_buttons="custom:slider,zoom_in,zoom_out,reset"
213 | ]
214 | ```
215 |
216 | ```text
217 | [btsvviewer
218 | src="/uploads/mind-map.svg"
219 | show_coords="true"
220 | controls_buttons="icon,alignleft,zoom_in,zoom_out,reset,center,coords"
221 | ]
222 | ```
223 |
224 | ```text
225 | [btsvviewer
226 | src="/uploads/campus-map.svg"
227 | button_bg="#2f855a"
228 | button_border="#22543d"
229 | button_fg="#ffffff"
230 | controls_buttons="both,aligncenter,zoom_in,zoom_out,reset,center"
231 | ]
232 | ```
233 |
234 | ---
235 |
236 | ## Styling (CSS Hooks)
237 |
238 | Wrapper classes added by the plugin:
239 |
240 | - `.bt-svg-viewer-wrapper` ??? outer container
241 | - `.bt-svg-viewer-main` ??? wraps controls and SVG container
242 | - `.svg-controls` ??? control bar
243 | - `.controls-position-{top|bottom|left|right}`
244 | - `.controls-mode-{icon|text|both|compact|labels-on-hover|minimal}`
245 | - `.controls-align-{alignleft|aligncenter|alignright}`
246 | - `.bt-svg-viewer-btn`, `.btn-icon`, `.btn-text`
247 | - `.svg-container`, `.svg-viewport`, `.coord-output`, `.zoom-display`
248 | - `.zoom-slider-wrapper`, `.zoom-slider`
249 |
250 | Button colors are powered by CSS custom properties on the wrapper. Shortcode attributes and preset color pickers set these values, but you can override them manually:
251 |
252 | ```css
253 | .bt-svg-viewer-wrapper {
254 | --bt-svg-viewer-button-fill: #1d4ed8;
255 | --bt-svg-viewer-button-border: #1d4ed8;
256 | --bt-svg-viewer-button-hover: #1e40af;
257 | --bt-svg-viewer-button-text: #ffffff;
258 | --bt-svg-viewer-slider-width: 240px; /* optional: tweak slider width */
259 | }
260 | ```
261 |
262 | Example overrides:
263 |
264 | ```css
265 | .bt-svg-viewer-wrapper.custom-map {
266 | border-radius: 12px;
267 | overflow: hidden;
268 | }
269 |
270 | .bt-svg-viewer-wrapper.custom-map .svg-controls {
271 | background: #111;
272 | color: #fff;
273 | gap: 6px;
274 | }
275 |
276 | .bt-svg-viewer-wrapper.custom-map .bt-svg-viewer-btn {
277 | background: rgba(255,255,255,0.1);
278 | border: 1px solid rgba(255,255,255,0.3);
279 | }
280 |
281 | .bt-svg-viewer-wrapper.custom-map .bt-svg-viewer-btn:hover {
282 | background: rgba(255,255,255,0.25);
283 | }
284 | ```
285 |
286 | ---
287 |
288 | ## Tips & Troubleshooting
289 |
290 | ### SVG Preparation
291 |
292 | - Remove hard-coded width/height if possible; rely on `viewBox`.
293 | - Ensure the SVG renders correctly when opened directly in a browser.
294 | - Compress large SVGs to keep loading snappy.
295 |
296 | ### Common Issues
297 |
298 | - **Blank viewer**: check for 404 on the SVG path, confirm the file is accessible without authentication.
299 | - **Scaling looks wrong**: verify the SVG has an accurate `viewBox`.
300 | - **Controls disappear**: check `controls_buttons` for `hidden` or empty button list.
301 | - **Clipboard errors**: Some browsers require user interaction for `Copy Center`; the plugin falls back to a prompt.
302 |
303 | ### Debugging
304 |
305 | - Toggle `show_coords="true"` or inspect `window.btsvviewerInstances['viewer-id']` to troubleshoot zoom, center, or scroll behaviour.
306 | - Use the Defaults tab???s **Enable asset cache busting for debugging** switch if your browser clings to stale copies of the viewer script or styles.
307 |
308 | ---
309 |
310 | ## Changelog Highlights (1.1.0)
311 |
312 | - **Pan/Zoom Interaction Modes**: Shortcode and presets can now request `pan_mode="drag"` or `zoom_mode="scroll"` / `click"`, with the front end auto-explaining gesture hints to visitors.
313 | - **Smooth Cursor-Focused Zoom**: Wheel, slider, and modifier-click zoom animate between stops and keep the point under the pointer locked in place.
314 | - **Responsive Drag Panning**: Dragging now tracks 1:1 with the pointer and ignores stray wheel events so diagonal swipes stay fluid.
315 | - **Dev-Friendly Cache Busting**: The Defaults tab adds an ???Enable asset cache busting??? switch (also auto-enabled for `dev.*` and `wptest.*` hosts) to force fresh JS/CSS while testing.
316 |
317 | Full changelog lives in the repository???s `CHANGELOG.md`.
318 |
319 | ---
320 |
321 | ## License & Credits
322 |
323 | - License: GPL-2.0 (same as WordPress).
324 | - Built to make large interactive diagrams pleasant to navigate inside WordPress.
325 |
--------------------------------------------------------------------------------
/src/_README.md:
--------------------------------------------------------------------------------
1 |
2 | # BT SVG Viewer
3 |
4 | Embed large SVG diagrams in WordPress with zoom, pan, center, and authoring tools. Recent releases add a visual preset editor, icon-based controls, deeper shortcode options, and configurable button colors.
5 |
6 | ---
7 |
8 | ## Contents
9 |
10 | - [Contents](#contents)
11 | - [Installation](#installation)
12 | - [Quick Start](#quick-start)
13 | - [Shortcode Reference](#shortcode-reference)
14 | - [`controls_buttons` Cheat Sheet](#controls_buttons-cheat-sheet)
15 | - [Admin Preset Editor](#admin-preset-editor)
16 | - [Location](#location)
17 | - [Fields](#fields)
18 | - [Preview Pane](#preview-pane)
19 | - [Preset Shortcodes](#preset-shortcodes)
20 | - [Defaults Tab](#defaults-tab)
21 | - [Using Presets in Posts/Pages](#using-presets-in-postspages)
22 | - [Preview Workflow](#preview-workflow)
23 | - [Examples](#examples)
24 | - [Styling (CSS Hooks)](#styling-css-hooks)
25 | - [Tips \& Troubleshooting](#tips--troubleshooting)
26 | - [SVG Preparation](#svg-preparation)
27 | - [Common Issues](#common-issues)
28 | - [Debugging](#debugging)
29 | - [Changelog Highlights (1.1.0)](#changelog-highlights-110)
30 | - [License \& Credits](#license--credits)
31 |
32 | ---
33 |
34 | ## Installation
35 |
36 | 1. **Unzip the plugin archive**
37 | - Download [bt-svg-viewer.zip](github.com/ttscoff/bt-svg-viewer/releases/latest/download/bt-svg-viewer.zip).
38 | - Unzip it locally; you will get a folder named `bt-svg-viewer`.
39 | 2. **Upload the plugin**
40 | - Copy the entire `bt-svg-viewer` folder into your WordPress installation at `/wp-content/plugins/`.
41 | 3. **Activate**
42 | - In the WordPress admin, navigate to **Plugins** and click **Activate** on “BT SVG Viewer”.
43 |
44 | ---
45 |
46 | ## Quick Start
47 |
48 | ```text
49 | [btsvviewer src="/wp-content/uploads/diagrams/system-map.svg"]
50 | ```
51 |
52 | - Place the shortcode in a classic editor, Gutenberg shortcode block, or template.
53 | - The SVG renders with default height (600px), zoom controls, pan/scroll behaviour, keyboard shortcuts, and responsive layout. Zoom buttons now gray out at the minimum/maximum zoom to make the limits obvious to visitors.
54 | - Existing shortcodes created before the rename continue to render, so archived posts don’t need updates.
55 |
56 | ---
57 |
58 | ## Shortcode Reference
59 |
60 | | Attribute | Type | Default | Description |
61 | | -------------------------------------------------- | ----------------------------------- | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
62 | | `src` | string (required) | – | SVG URL. Supports absolute URLs, `/absolute/path.svg`, or relative to the uploads directory. |
63 | | `height` | string | `600px` | CSS height of the viewer. Accepts px, vh, %, etc. |
64 | | `class` | string | – | Additional class appended to the wrapper. |
65 | | `zoom` | number | `100` | Initial zoom percentage. |
66 | | `min_zoom` | number | `25` | Minimum zoom percentage allowed. |
67 | | `max_zoom` | number | `800` | Maximum zoom percentage allowed. |
68 | | `zoom_step` | number | `10` | Increment used by buttons/keyboard shortcuts. |
69 | | `initial_zoom` | number | – | Alias captured when presets save the preview state. Overrides `zoom` if present. |
70 | | `pan` / `pan_mode` | `scroll` or `drag` | `scroll` | Toggle between scroll-wheel panning and click-drag panning. Drag is enforced when zoom modes require it. |
71 | | `zoom_mode` / `zoom_behavior` / `zoom_interaction` | `super_scroll` / `scroll` / `click` | `super_scroll` | Choose how wheel and modifier gestures zoom: Cmd/Ctrl + wheel (`super_scroll`), every wheel (`scroll`), or Cmd/Ctrl-click & Alt-click (`click`). |
72 | | `center_x` / `center_y` | number | – | Manual center point in SVG units. Defaults to viewBox center. |
73 | | `show_coords` | boolean | `false` | Appends “Copy Center” button for debugging coordinate values. |
74 | | `controls_position` | `top`/`bottom`/`left`/`right` | `top` | Placement of the entire control group. |
75 | | `controls_buttons` | string | `both` | Comma-delimited mode/align/button list. See table below (supports `slider`). |
76 | | `title` | string | – | Optional heading above the viewer. HTML allowed. |
77 | | `caption` | string | – | Optional caption below the viewer. HTML allowed. |
78 | | `button_fill` / `button_background` / `button_bg` | color string | theme default (`#0073aa`) | Button background color. Aliases exist for backwards compatibility (all map to `button_fill`). |
79 | | `button_border` | color string | matches fill | Outline color for buttons. Blank inherits the fill color. |
80 | | `button_foreground` / `button_fg` | color string | `#ffffff` | Text and icon color for buttons. Blank uses the default. |
81 | | `id` | number | – | Reference a saved preset (admin). Inline attributes override preset values. |
82 |
83 | > Changing the interaction defaults automatically inserts a helper caption (e.g. “Cmd/Ctrl-click to zoom in…”) above your custom caption so visitors know the gesture.
84 |
85 | ### `controls_buttons` Cheat Sheet
86 |
87 | ```text
88 | controls_buttons="MODE,ALIGNMENT,BUTTON_1,BUTTON_2,..."
89 | ```
90 |
91 | Mode keywords (optional):
92 |
93 | - `both` (default)
94 | - `icon`
95 | - `text`
96 | - `compact`
97 | - `labels_on_hover`
98 | - `minimal`
99 | - `hidden` / `none`
100 |
101 | Alignment keywords (optional, anywhere in list):
102 |
103 | - `alignleft`
104 | - `aligncenter`
105 | - `alignright`
106 |
107 | Additional keywords:
108 |
109 | - `slider` – replaces zoom buttons with a live-updating range input. Combine with `icon`/`text`/`both` for layout. Use `custom:slider,zoom_in,zoom_out` to show both slider and buttons.
110 |
111 | Button names (pick any order):
112 |
113 | - `zoom_in`, `zoom_out`, `reset`, `center`, `coords` *(coords button only renders if `show_coords="true"`)*
114 |
115 | Example:
116 |
117 | ```text
118 | [btsvviewer
119 | src="/uploads/system-map.svg"
120 | controls_position="right"
121 | controls_buttons="slider,icon,aligncenter,reset,center"
122 | ]
123 | ```
124 |
125 | ---
126 |
127 | ## Admin Preset Editor
128 |
129 | ### Location
130 |
131 | `WordPress Dashboard → BT SVG Viewer → Presets`
132 |
133 | ### Fields
134 |
135 | | Field | Description |
136 | | --------------------------- | ------------------------------------------------------------------------------ |
137 | | **SVG Source URL** | Choose or upload an SVG from the media library. Required before preview loads. |
138 | | **Viewer Height** | Same as shortcode `height`. |
139 | | **Zoom Settings** | Minimum, maximum, step, and initial zoom percentages. |
140 | | **Center Coordinates** | Override auto centering with explicit `center_x`/`center_y`. |
141 | | **Controls Position** | Dropdown for `top`, `bottom`, `left`, `right`. |
142 | | **Controls Buttons/Layout** | Text field following the `MODE,ALIGN,buttons…` pattern described above. |
143 | | **Button Colors** | Three color pickers for fill, border, and foreground (text/icon) colors. |
144 | | **Title & Caption** | Optional, displayed above/below the viewer wrapper. |
145 |
146 | ### Preview Pane
147 |
148 | - **Load / Refresh Preview**: Injects the SVG using the current field values.
149 | - **Use Current View for Initial State**: Captures the visible zoom level and center point from the preview and writes them back into the form (ideal for fine-tuning coordinates visually).
150 | - **Copy Center**: When `show_coords` is enabled, the button copies coordinates to the clipboard.
151 | - Zoom controls in the preview (and front end) now snap to their minimum/maximum, disable the corresponding buttons, and keep your pointer-focused zoom locked to the selected point—what you see in the preview is exactly what site visitors experience.
152 |
153 | ### Preset Shortcodes
154 |
155 | - At the top of the preset editor and in the presets list table you’ll find a copy-ready snippet in the form of `[btsvviewer id="123"]`.
156 | - Click **Copy** to put the shortcode on the clipboard without selecting manually.
157 |
158 | ### Defaults Tab
159 |
160 | - Visit **BT SVG Viewer → Presets → Defaults** to seed the fields used when creating a new preset.
161 | - The panel now includes **Enable asset cache busting for debugging**, which appends a time-based suffix to scripts and styles. It is automatically active on hosts that start with `dev.` or `wptest.` and can be toggled manually when you need to defeat browser caching.
162 |
163 | ---
164 |
165 | ## Using Presets in Posts/Pages
166 |
167 | 1. Create or edit a preset as described above.
168 | 2. Copy the generated shortcode (`[btsvviewer id="123"]`).
169 | 3. Paste into:
170 | - Classic editor (Visual tab: use Shortcode block or paste directly).
171 | - Block editor (Shortcode block or HTML block).
172 | - Template files via `do_shortcode()`.
173 | 4. Override on a per-use basis if needed:
174 |
175 | ```text
176 | [btsvviewer id="123" controls_buttons="icon,alignright,zoom_in,zoom_out"]
177 | ```
178 |
179 | ---
180 |
181 | ## Preview Workflow
182 |
183 | 1. **Load / Refresh Preview** once the SVG path is entered.
184 | 2. Drag or scroll the SVG to the desired default view.
185 | 3. **Use Current View for Initial State** to stash those coordinates/zoom.
186 | 4. Optionally toggle `show_coords` to display the current x/y values of the viewport center.
187 | 5. Save/publish the preset and test the shortcode on the front end.
188 |
189 | ---
190 |
191 | ## Examples
192 |
193 | ```text
194 | [btsvviewer src="/uploads/floorplan.svg"]
195 | ```
196 |
197 | ```text
198 | [btsvviewer
199 | src="/uploads/system-map.svg"
200 | height="100vh"
201 | controls_position="bottom"
202 | controls_buttons="compact,aligncenter,zoom_in,zoom_out,reset,center"
203 | zoom="175"
204 | min_zoom="50"
205 | max_zoom="400"
206 | ]
207 | ```
208 |
209 | ```text
210 | [btsvviewer
211 | id="42"
212 | controls_buttons="custom:slider,zoom_in,zoom_out,reset"
213 | ]
214 | ```
215 |
216 | ```text
217 | [btsvviewer
218 | src="/uploads/mind-map.svg"
219 | show_coords="true"
220 | controls_buttons="icon,alignleft,zoom_in,zoom_out,reset,center,coords"
221 | ]
222 | ```
223 |
224 | ```text
225 | [btsvviewer
226 | src="/uploads/campus-map.svg"
227 | button_bg="#2f855a"
228 | button_border="#22543d"
229 | button_fg="#ffffff"
230 | controls_buttons="both,aligncenter,zoom_in,zoom_out,reset,center"
231 | ]
232 | ```
233 |
234 | ---
235 |
236 | ## Styling (CSS Hooks)
237 |
238 | Wrapper classes added by the plugin:
239 |
240 | - `.bt-svg-viewer-wrapper` – outer container
241 | - `.bt-svg-viewer-main` – wraps controls and SVG container
242 | - `.svg-controls` – control bar
243 | - `.controls-position-{top|bottom|left|right}`
244 | - `.controls-mode-{icon|text|both|compact|labels-on-hover|minimal}`
245 | - `.controls-align-{alignleft|aligncenter|alignright}`
246 | - `.bt-svg-viewer-btn`, `.btn-icon`, `.btn-text`
247 | - `.svg-container`, `.svg-viewport`, `.coord-output`, `.zoom-display`
248 | - `.zoom-slider-wrapper`, `.zoom-slider`
249 |
250 | Button colors are powered by CSS custom properties on the wrapper. Shortcode attributes and preset color pickers set these values, but you can override them manually:
251 |
252 | ```css
253 | .bt-svg-viewer-wrapper {
254 | --bt-svg-viewer-button-fill: #1d4ed8;
255 | --bt-svg-viewer-button-border: #1d4ed8;
256 | --bt-svg-viewer-button-hover: #1e40af;
257 | --bt-svg-viewer-button-text: #ffffff;
258 | --bt-svg-viewer-slider-width: 240px; /* optional: tweak slider width */
259 | }
260 | ```
261 |
262 | Example overrides:
263 |
264 | ```css
265 | .bt-svg-viewer-wrapper.custom-map {
266 | border-radius: 12px;
267 | overflow: hidden;
268 | }
269 |
270 | .bt-svg-viewer-wrapper.custom-map .svg-controls {
271 | background: #111;
272 | color: #fff;
273 | gap: 6px;
274 | }
275 |
276 | .bt-svg-viewer-wrapper.custom-map .bt-svg-viewer-btn {
277 | background: rgba(255,255,255,0.1);
278 | border: 1px solid rgba(255,255,255,0.3);
279 | }
280 |
281 | .bt-svg-viewer-wrapper.custom-map .bt-svg-viewer-btn:hover {
282 | background: rgba(255,255,255,0.25);
283 | }
284 | ```
285 |
286 | ---
287 |
288 | ## Tips & Troubleshooting
289 |
290 | ### SVG Preparation
291 |
292 | - Remove hard-coded width/height if possible; rely on `viewBox`.
293 | - Ensure the SVG renders correctly when opened directly in a browser.
294 | - Compress large SVGs to keep loading snappy.
295 |
296 | ### Common Issues
297 |
298 | - **Blank viewer**: check for 404 on the SVG path, confirm the file is accessible without authentication.
299 | - **Scaling looks wrong**: verify the SVG has an accurate `viewBox`.
300 | - **Controls disappear**: check `controls_buttons` for `hidden` or empty button list.
301 | - **Clipboard errors**: Some browsers require user interaction for `Copy Center`; the plugin falls back to a prompt.
302 |
303 | ### Debugging
304 |
305 | - Toggle `show_coords="true"` or inspect `window.btsvviewerInstances['viewer-id']` to troubleshoot zoom, center, or scroll behaviour.
306 | - Use the Defaults tab’s **Enable asset cache busting for debugging** switch if your browser clings to stale copies of the viewer script or styles.
307 |
308 | ---
309 |
310 | ## Changelog Highlights (1.1.0)
311 |
312 | - **Pan/Zoom Interaction Modes**: Shortcode and presets can now request `pan_mode="drag"` or `zoom_mode="scroll"` / `click"`, with the front end auto-explaining gesture hints to visitors.
313 | - **Smooth Cursor-Focused Zoom**: Wheel, slider, and modifier-click zoom animate between stops and keep the point under the pointer locked in place.
314 | - **Responsive Drag Panning**: Dragging now tracks 1:1 with the pointer and ignores stray wheel events so diagonal swipes stay fluid.
315 | - **Dev-Friendly Cache Busting**: The Defaults tab adds an “Enable asset cache busting” switch (also auto-enabled for `dev.*` and `wptest.*` hosts) to force fresh JS/CSS while testing.
316 |
317 | Full changelog lives in the repository’s `CHANGELOG.md`.
318 |
319 | ---
320 |
321 | ## License & Credits
322 |
323 | - License: GPL-2.0 (same as WordPress).
324 | - Built to make large interactive diagrams pleasant to navigate inside WordPress.
325 |
326 |
--------------------------------------------------------------------------------
/bt-svg-viewer/admin/help.html:
--------------------------------------------------------------------------------
1 |
BT SVG Viewer
2 |
3 |
Embed large SVG diagrams in WordPress with zoom, pan, center, and authoring tools. Recent releases add a visual preset editor, icon-based controls, deeper shortcode options, and configurable button colors.
Place the shortcode in a classic editor, Gutenberg shortcode block, or template.
71 |
The SVG renders with default height (600px), zoom controls, pan/scroll behaviour, keyboard shortcuts, and responsive layout. Zoom buttons now gray out at the minimum/maximum zoom to make the limits obvious to visitors.
72 |
Existing shortcodes created before the rename continue to render, so archived posts don???t need updates.
73 |
74 |
75 |
76 |
77 |
Shortcode Reference
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
Attribute
90 |
Type
91 |
Default
92 |
Description
93 |
94 |
95 |
96 |
97 |
98 |
src
99 |
string (required)
100 |
???
101 |
SVG URL. Supports absolute URLs, /absolute/path.svg, or relative to the uploads directory.
102 |
103 |
104 |
height
105 |
string
106 |
600px
107 |
CSS height of the viewer. Accepts px, vh, %, etc.
108 |
109 |
110 |
class
111 |
string
112 |
???
113 |
Additional class appended to the wrapper.
114 |
115 |
116 |
zoom
117 |
number
118 |
100
119 |
Initial zoom percentage.
120 |
121 |
122 |
min_zoom
123 |
number
124 |
25
125 |
Minimum zoom percentage allowed.
126 |
127 |
128 |
max_zoom
129 |
number
130 |
800
131 |
Maximum zoom percentage allowed.
132 |
133 |
134 |
zoom_step
135 |
number
136 |
10
137 |
Increment used by buttons/keyboard shortcuts.
138 |
139 |
140 |
initial_zoom
141 |
number
142 |
???
143 |
Alias captured when presets save the preview state. Overrides zoom if present.
144 |
145 |
146 |
pan / pan_mode
147 |
scroll or drag
148 |
scroll
149 |
Toggle between scroll-wheel panning and click-drag panning. Drag is enforced when zoom modes require it.
150 |
151 |
152 |
zoom_mode / zoom_behavior / zoom_interaction
153 |
super_scroll / scroll / click
154 |
super_scroll
155 |
Choose how wheel and modifier gestures zoom: Cmd/Ctrl + wheel (super_scroll), every wheel (scroll), or Cmd/Ctrl-click & Alt-click (click).
156 |
157 |
158 |
center_x / center_y
159 |
number
160 |
???
161 |
Manual center point in SVG units. Defaults to viewBox center.
162 |
163 |
164 |
show_coords
165 |
boolean
166 |
false
167 |
Appends ???Copy Center??? button for debugging coordinate values.
168 |
169 |
170 |
controls_position
171 |
top/bottom/left/right
172 |
top
173 |
Placement of the entire control group.
174 |
175 |
176 |
controls_buttons
177 |
string
178 |
both
179 |
Comma-delimited mode/align/button list. See table below (supports slider).
180 |
181 |
182 |
title
183 |
string
184 |
???
185 |
Optional heading above the viewer. HTML allowed.
186 |
187 |
188 |
caption
189 |
string
190 |
???
191 |
Optional caption below the viewer. HTML allowed.
192 |
193 |
194 |
button_fill / button_background / button_bg
195 |
color string
196 |
theme default (#0073aa)
197 |
Button background color. Aliases exist for backwards compatibility (all map to button_fill).
198 |
199 |
200 |
button_border
201 |
color string
202 |
matches fill
203 |
Outline color for buttons. Blank inherits the fill color.
204 |
205 |
206 |
button_foreground / button_fg
207 |
color string
208 |
#ffffff
209 |
Text and icon color for buttons. Blank uses the default.
210 |
211 |
212 |
id
213 |
number
214 |
???
215 |
Reference a saved preset (admin). Inline attributes override preset values.
216 |
217 |
218 |
219 |
220 |
221 |
Changing the interaction defaults automatically inserts a helper caption (e.g. ???Cmd/Ctrl-click to zoom in??????) above your custom caption so visitors know the gesture.
slider ??? replaces zoom buttons with a live-updating range input. Combine with icon/text/both for layout. Use custom:slider,zoom_in,zoom_out to show both slider and buttons.
253 |
254 |
255 |
Button names (pick any order):
256 |
257 |
258 |
zoom_in, zoom_out, reset, center, coords(coords button only renders if show_coords="true")
Choose or upload an SVG from the media library. Required before preview loads.
297 |
298 |
299 |
Viewer Height
300 |
Same as shortcode height.
301 |
302 |
303 |
Zoom Settings
304 |
Minimum, maximum, step, and initial zoom percentages.
305 |
306 |
307 |
Center Coordinates
308 |
Override auto centering with explicit center_x/center_y.
309 |
310 |
311 |
Controls Position
312 |
Dropdown for top, bottom, left, right.
313 |
314 |
315 |
Controls Buttons/Layout
316 |
Text field following the MODE,ALIGN,buttons??? pattern described above.
317 |
318 |
319 |
Button Colors
320 |
Three color pickers for fill, border, and foreground (text/icon) colors.
321 |
322 |
323 |
Title & Caption
324 |
Optional, displayed above/below the viewer wrapper.
325 |
326 |
327 |
328 |
329 |
Preview Pane
330 |
331 |
332 |
Load / Refresh Preview: Injects the SVG using the current field values.
333 |
Use Current View for Initial State: Captures the visible zoom level and center point from the preview and writes them back into the form (ideal for fine-tuning coordinates visually).
334 |
Copy Center: When show_coords is enabled, the button copies coordinates to the clipboard.
335 |
Zoom controls in the preview (and front end) now snap to their minimum/maximum, disable the corresponding buttons, and keep your pointer-focused zoom locked to the selected point???what you see in the preview is exactly what site visitors experience.
336 |
337 |
338 |
Preset Shortcodes
339 |
340 |
341 |
At the top of the preset editor and in the presets list table you???ll find a copy-ready snippet in the form of [btsvviewer id="123"].
342 |
Click Copy to put the shortcode on the clipboard without selecting manually.
343 |
344 |
345 |
Defaults Tab
346 |
347 |
348 |
Visit BT SVG Viewer ??? Presets ??? Defaults to seed the fields used when creating a new preset.
349 |
The panel now includes Enable asset cache busting for debugging, which appends a time-based suffix to scripts and styles. It is automatically active on hosts that start with dev. or wptest. and can be toggled manually when you need to defeat browser caching.
350 |
351 |
352 |
353 |
354 |
Using Presets in Posts/Pages
355 |
356 |
357 |
Create or edit a preset as described above.
358 |
Copy the generated shortcode ([btsvviewer id="123"]).
359 |
Paste into:
360 |
361 |
362 |
363 |
Classic editor (Visual tab: use Shortcode block or paste directly).
Button colors are powered by CSS custom properties on the wrapper. Shortcode attributes and preset color pickers set these values, but you can override them manually:
Remove hard-coded width/height if possible; rely on viewBox.
488 |
Ensure the SVG renders correctly when opened directly in a browser.
489 |
Compress large SVGs to keep loading snappy.
490 |
491 |
492 |
Common Issues
493 |
494 |
495 |
Blank viewer: check for 404 on the SVG path, confirm the file is accessible without authentication.
496 |
Scaling looks wrong: verify the SVG has an accurate viewBox.
497 |
Controls disappear: check controls_buttons for hidden or empty button list.
498 |
Clipboard errors: Some browsers require user interaction for Copy Center; the plugin falls back to a prompt.
499 |
500 |
501 |
Debugging
502 |
503 |
504 |
Toggle show_coords="true" or inspect window.btsvviewerInstances['viewer-id'] to troubleshoot zoom, center, or scroll behaviour.
505 |
Use the Defaults tab???s Enable asset cache busting for debugging switch if your browser clings to stale copies of the viewer script or styles.
506 |
507 |
508 |
509 |
510 |
Changelog Highlights (1.1.0)
511 |
512 |
513 |
Pan/Zoom Interaction Modes: Shortcode and presets can now request pan_mode="drag" or zoom_mode="scroll" / click", with the front end auto-explaining gesture hints to visitors.
514 |
Smooth Cursor-Focused Zoom: Wheel, slider, and modifier-click zoom animate between stops and keep the point under the pointer locked in place.
515 |
Responsive Drag Panning: Dragging now tracks 1:1 with the pointer and ignores stray wheel events so diagonal swipes stay fluid.
516 |
Dev-Friendly Cache Busting: The Defaults tab adds an ???Enable asset cache busting??? switch (also auto-enabled for dev.* and wptest.* hosts) to force fresh JS/CSS while testing.
517 |
518 |
519 |
Full changelog lives in the repository???s CHANGELOG.md.
520 |
521 |
522 |
523 |
License & Credits
524 |
525 |
526 |
License: GPL-2.0 (same as WordPress).
527 |
Built to make large interactive diagrams pleasant to navigate inside WordPress.