jekyll-svg-viewer 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE.txt +22 -0
- data/README.md +110 -0
- data/assets/svg-viewer/css/svg-viewer.css +439 -0
- data/assets/svg-viewer/i18n/locales.json +78 -0
- data/assets/svg-viewer/js/svg-viewer.js +1604 -0
- data/assets/svg-viewer/preview/index.html +178 -0
- data/assets/svg-viewer/preview/preset-builder.css +338 -0
- data/assets/svg-viewer/preview/preset-builder.js +793 -0
- data/lib/jekyll/svg_viewer/asset_manager.rb +129 -0
- data/lib/jekyll/svg_viewer/config.rb +40 -0
- data/lib/jekyll/svg_viewer/preview_page.rb +54 -0
- data/lib/jekyll/svg_viewer/tag.rb +528 -0
- data/lib/jekyll/svg_viewer/version.rb +6 -0
- data/lib/jekyll/svg_viewer.rb +6 -0
- data/lib/jekyll-svg-viewer.rb +11 -0
- metadata +93 -0
|
@@ -0,0 +1,793 @@
|
|
|
1
|
+
(() => {
|
|
2
|
+
const defaults = window.__SVG_VIEWER_DEFAULTS__ || {};
|
|
3
|
+
const form = document.getElementById("preset-form");
|
|
4
|
+
const previewContainer = document.getElementById("viewer-preview");
|
|
5
|
+
const statusEl = document.getElementById("builder-status");
|
|
6
|
+
const liquidOutput = document.getElementById("liquid-tag-output");
|
|
7
|
+
const copyLiquidBtn = document.getElementById("copy-liquid");
|
|
8
|
+
const loadPreviewBtn = document.getElementById("load-preview");
|
|
9
|
+
const captureCenterBtn = document.getElementById("capture-center");
|
|
10
|
+
const showYamlBtn = document.getElementById("show-yaml");
|
|
11
|
+
const yamlModal = document.getElementById("yaml-modal");
|
|
12
|
+
const yamlOutput = document.getElementById("yaml-output");
|
|
13
|
+
const copyYamlBtn = document.getElementById("copy-yaml");
|
|
14
|
+
|
|
15
|
+
if (!form || !previewContainer) return;
|
|
16
|
+
|
|
17
|
+
const state = {
|
|
18
|
+
src: "",
|
|
19
|
+
height: defaults["height"] || "600px",
|
|
20
|
+
zoom: defaults["zoom"] || "100",
|
|
21
|
+
min_zoom: defaults["min_zoom"] || "25",
|
|
22
|
+
max_zoom: defaults["max_zoom"] || "800",
|
|
23
|
+
zoom_step: defaults["zoom_step"] || "10",
|
|
24
|
+
center_x: "",
|
|
25
|
+
center_y: "",
|
|
26
|
+
show_coords: Boolean(defaults["show_coords"]),
|
|
27
|
+
title: defaults["title"] || "",
|
|
28
|
+
caption: defaults["caption"] || "",
|
|
29
|
+
controls_position: defaults["controls_position"] || "top",
|
|
30
|
+
controls_buttons: defaults["controls_buttons"] || "both",
|
|
31
|
+
button_fill: defaults["button_fill"] || "",
|
|
32
|
+
button_border: defaults["button_border"] || "",
|
|
33
|
+
button_foreground: defaults["button_foreground"] || "",
|
|
34
|
+
pan_mode: defaults["pan_mode"] || "",
|
|
35
|
+
zoom_mode: defaults["zoom_mode"] || "",
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const baseUrl = (window.__SVG_VIEWER_BASEURL__ || "").replace(/\/+$/, "");
|
|
39
|
+
fetch(`${baseUrl}/assets/svg-viewer/i18n/locales.json`)
|
|
40
|
+
.then((response) => (response.ok ? response.json() : {}))
|
|
41
|
+
.then((data) => {
|
|
42
|
+
window.__SVG_VIEWER_I18N__ = data;
|
|
43
|
+
})
|
|
44
|
+
.catch(() => {
|
|
45
|
+
window.__SVG_VIEWER_I18N__ = {};
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const fieldElements = Array.from(form.querySelectorAll("[data-setting]"));
|
|
49
|
+
fieldElements.forEach((field) => {
|
|
50
|
+
const key = field.dataset.setting;
|
|
51
|
+
if (!(key in state)) return;
|
|
52
|
+
setFieldValue(field, state[key]);
|
|
53
|
+
field.dataset.originalValue = field.value;
|
|
54
|
+
field.addEventListener(
|
|
55
|
+
field.type === "checkbox" ? "change" : "input",
|
|
56
|
+
() => {
|
|
57
|
+
state[key] = readFieldValue(field);
|
|
58
|
+
updateOutputs();
|
|
59
|
+
}
|
|
60
|
+
);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
let currentViewerId = null;
|
|
64
|
+
let currentViewer = null;
|
|
65
|
+
|
|
66
|
+
loadPreviewBtn.addEventListener("click", (event) => {
|
|
67
|
+
event.preventDefault();
|
|
68
|
+
renderPreview();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
captureCenterBtn.addEventListener("click", (event) => {
|
|
72
|
+
event.preventDefault();
|
|
73
|
+
if (!currentViewer) {
|
|
74
|
+
flashStatus(
|
|
75
|
+
"Load the preview before capturing the current view.",
|
|
76
|
+
"warn"
|
|
77
|
+
);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const center = currentViewer.getVisibleCenterPoint
|
|
81
|
+
? currentViewer.getVisibleCenterPoint()
|
|
82
|
+
: null;
|
|
83
|
+
if (!center) {
|
|
84
|
+
flashStatus("Unable to read the center point from the preview.", "error");
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
state.center_x = center.x.toFixed(2);
|
|
88
|
+
state.center_y = center.y.toFixed(2);
|
|
89
|
+
state.zoom = (currentViewer.currentZoom * 100).toFixed(0);
|
|
90
|
+
syncFields();
|
|
91
|
+
updateOutputs();
|
|
92
|
+
flashStatus("Captured the current view. Fields updated.", "success");
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
copyLiquidBtn.addEventListener("click", async (event) => {
|
|
96
|
+
event.preventDefault();
|
|
97
|
+
if (!liquidOutput.value.trim()) {
|
|
98
|
+
flashStatus(
|
|
99
|
+
"Nothing to copy yet. Add a source and load the preview first.",
|
|
100
|
+
"warn"
|
|
101
|
+
);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
await copyText(liquidOutput.value);
|
|
105
|
+
flashStatus("Liquid tag copied to clipboard.", "success");
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
showYamlBtn.addEventListener("click", (event) => {
|
|
109
|
+
event.preventDefault();
|
|
110
|
+
const yaml = buildYamlSnippet();
|
|
111
|
+
yamlOutput.value = yaml;
|
|
112
|
+
openDialog(yamlModal);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
copyYamlBtn.addEventListener("click", async (event) => {
|
|
116
|
+
event.preventDefault();
|
|
117
|
+
await copyText(yamlOutput.value);
|
|
118
|
+
flashStatus("YAML snippet copied to clipboard.", "success");
|
|
119
|
+
closeDialog(yamlModal);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
if (yamlModal) {
|
|
123
|
+
yamlModal.addEventListener("cancel", (event) => {
|
|
124
|
+
event.preventDefault();
|
|
125
|
+
closeDialog(yamlModal);
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
updateOutputs();
|
|
130
|
+
|
|
131
|
+
function renderPreview() {
|
|
132
|
+
if (!state.src.trim()) {
|
|
133
|
+
flashStatus(
|
|
134
|
+
"Provide an SVG source URL before loading the preview.",
|
|
135
|
+
"error"
|
|
136
|
+
);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
destroyCurrentViewer();
|
|
141
|
+
|
|
142
|
+
const viewerId = `svg-viewer-preview-${Date.now()}`;
|
|
143
|
+
currentViewerId = viewerId;
|
|
144
|
+
|
|
145
|
+
const wrapper = document.createElement("div");
|
|
146
|
+
wrapper.id = viewerId;
|
|
147
|
+
wrapper.className = buildWrapperClasses();
|
|
148
|
+
const wrapperStyle = buildWrapperStyle();
|
|
149
|
+
if (wrapperStyle) {
|
|
150
|
+
wrapper.style.cssText = wrapperStyle;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const controlsConfig = parseControlsConfig(Boolean(state.show_coords));
|
|
154
|
+
const mainClasses = buildMainClasses(controlsConfig);
|
|
155
|
+
const controlsMarkup = buildControlsMarkup(viewerId, controlsConfig);
|
|
156
|
+
const containerMarkup = `
|
|
157
|
+
<div class="${mainClasses}">
|
|
158
|
+
${controlsMarkup}
|
|
159
|
+
<div class="svg-container" style="height: ${escapeHtml(
|
|
160
|
+
state.height
|
|
161
|
+
)}; width: 100%; max-width: 100%; min-width: 0;" data-viewer="${viewerId}">
|
|
162
|
+
<div class="svg-viewport" data-viewer="${viewerId}"></div>
|
|
163
|
+
</div>
|
|
164
|
+
</div>
|
|
165
|
+
`;
|
|
166
|
+
|
|
167
|
+
wrapper.innerHTML = `
|
|
168
|
+
${
|
|
169
|
+
state.title
|
|
170
|
+
? `<div class="svg-viewer-title">${escapeHtml(state.title)}</div>`
|
|
171
|
+
: ""
|
|
172
|
+
}
|
|
173
|
+
${containerMarkup}
|
|
174
|
+
${
|
|
175
|
+
state.caption
|
|
176
|
+
? `<div class="svg-viewer-caption">${escapeHtml(state.caption)}</div>`
|
|
177
|
+
: ""
|
|
178
|
+
}
|
|
179
|
+
`;
|
|
180
|
+
|
|
181
|
+
previewContainer.innerHTML = "";
|
|
182
|
+
previewContainer.appendChild(wrapper);
|
|
183
|
+
|
|
184
|
+
ensureViewerScript(() => {
|
|
185
|
+
const options = viewerOptions(viewerId);
|
|
186
|
+
try {
|
|
187
|
+
window.svgViewerInstances ||= {};
|
|
188
|
+
currentViewer = new window.SVGViewer(options);
|
|
189
|
+
window.svgViewerInstances[viewerId] = currentViewer;
|
|
190
|
+
flashStatus("Preview loaded successfully.", "success");
|
|
191
|
+
} catch (error) {
|
|
192
|
+
flashStatus(
|
|
193
|
+
"Failed to initialize the viewer. Check the console for details.",
|
|
194
|
+
"error"
|
|
195
|
+
);
|
|
196
|
+
console.error("[SVG Viewer Preview]", error);
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
updateOutputs();
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function destroyCurrentViewer() {
|
|
204
|
+
if (currentViewer && typeof currentViewer.destroy === "function") {
|
|
205
|
+
try {
|
|
206
|
+
currentViewer.destroy();
|
|
207
|
+
} catch (error) {
|
|
208
|
+
console.warn("[SVG Viewer Preview] destroy failed", error);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
if (currentViewerId && window.svgViewerInstances) {
|
|
212
|
+
delete window.svgViewerInstances[currentViewerId];
|
|
213
|
+
}
|
|
214
|
+
currentViewer = null;
|
|
215
|
+
currentViewerId = null;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function viewerOptions(viewerId) {
|
|
219
|
+
return {
|
|
220
|
+
viewerId,
|
|
221
|
+
svgUrl: state.src.trim(),
|
|
222
|
+
initialZoom: clampZoom(state.zoom) / 100,
|
|
223
|
+
minZoom: clampZoom(state.min_zoom) / 100,
|
|
224
|
+
maxZoom: Math.max(
|
|
225
|
+
clampZoom(state.max_zoom) / 100,
|
|
226
|
+
clampZoom(state.zoom) / 100
|
|
227
|
+
),
|
|
228
|
+
zoomStep: Math.max(clampZoom(state.zoom_step) / 100, 0.01),
|
|
229
|
+
centerX: parseMaybeFloat(state.center_x),
|
|
230
|
+
centerY: parseMaybeFloat(state.center_y),
|
|
231
|
+
showCoordinates: Boolean(state.show_coords),
|
|
232
|
+
panMode: normalizePan(state.pan_mode),
|
|
233
|
+
zoomMode: normalizeZoom(state.zoom_mode),
|
|
234
|
+
controlsConfig: parseControlsConfig(Boolean(state.show_coords)),
|
|
235
|
+
buttonFill: state.button_fill || undefined,
|
|
236
|
+
buttonBorder: state.button_border || undefined,
|
|
237
|
+
buttonForeground: state.button_foreground || undefined,
|
|
238
|
+
locale: "en",
|
|
239
|
+
i18n: window.__SVG_VIEWER_I18N__ || {},
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function parseControlsConfig(showCoords) {
|
|
244
|
+
return parseControls(
|
|
245
|
+
state.controls_position,
|
|
246
|
+
state.controls_buttons,
|
|
247
|
+
showCoords
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function updateOutputs() {
|
|
252
|
+
liquidOutput.value = buildLiquidTag();
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function buildLiquidTag() {
|
|
256
|
+
if (!state.src.trim()) return "";
|
|
257
|
+
|
|
258
|
+
const attributes = [];
|
|
259
|
+
attributes.push(`src="${escapeQuotes(state.src.trim())}"`);
|
|
260
|
+
|
|
261
|
+
const keys = [
|
|
262
|
+
"height",
|
|
263
|
+
"zoom",
|
|
264
|
+
"min_zoom",
|
|
265
|
+
"max_zoom",
|
|
266
|
+
"zoom_step",
|
|
267
|
+
"center_x",
|
|
268
|
+
"center_y",
|
|
269
|
+
"show_coords",
|
|
270
|
+
"title",
|
|
271
|
+
"caption",
|
|
272
|
+
"controls_position",
|
|
273
|
+
"controls_buttons",
|
|
274
|
+
"button_fill",
|
|
275
|
+
"button_border",
|
|
276
|
+
"button_foreground",
|
|
277
|
+
"pan_mode",
|
|
278
|
+
"zoom_mode",
|
|
279
|
+
];
|
|
280
|
+
|
|
281
|
+
keys.forEach((key) => {
|
|
282
|
+
const value = state[key];
|
|
283
|
+
const defaultValue = defaults[key];
|
|
284
|
+
|
|
285
|
+
if (value === undefined || value === null || value === "") {
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (key === "show_coords") {
|
|
290
|
+
if (Boolean(value) === Boolean(defaultValue)) return;
|
|
291
|
+
attributes.push(`${key}="true"`);
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (String(value) === String(defaultValue)) {
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
attributes.push(`${key}="${escapeQuotes(value)}"`);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
return `{% svg_viewer ${attributes.join(" ")} %}`;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function buildYamlSnippet() {
|
|
306
|
+
const yamlLines = ["svg_viewer:", " defaults:"];
|
|
307
|
+
const entries = {
|
|
308
|
+
height: state.height,
|
|
309
|
+
zoom: state.zoom,
|
|
310
|
+
min_zoom: state.min_zoom,
|
|
311
|
+
max_zoom: state.max_zoom,
|
|
312
|
+
zoom_step: state.zoom_step,
|
|
313
|
+
show_coords: Boolean(state.show_coords),
|
|
314
|
+
title: state.title,
|
|
315
|
+
caption: state.caption,
|
|
316
|
+
controls_position: state.controls_position,
|
|
317
|
+
controls_buttons: state.controls_buttons,
|
|
318
|
+
button_fill: state.button_fill,
|
|
319
|
+
button_border: state.button_border,
|
|
320
|
+
button_foreground: state.button_foreground,
|
|
321
|
+
pan_mode: state.pan_mode,
|
|
322
|
+
zoom_mode: state.zoom_mode,
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
Object.entries(entries).forEach(([key, value]) => {
|
|
326
|
+
if (value === undefined || value === null || value === "") {
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (typeof value === "boolean") {
|
|
331
|
+
yamlLines.push(` ${key}: ${value}`);
|
|
332
|
+
} else {
|
|
333
|
+
yamlLines.push(` ${key}: "${escapeQuotes(String(value))}"`);
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
return yamlLines.join("\n");
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function parseControls(position, buttonsSetting, showCoords) {
|
|
341
|
+
const pos = ["top", "bottom", "left", "right"].includes(position)
|
|
342
|
+
? position
|
|
343
|
+
: "top";
|
|
344
|
+
const raw = buttonsSetting || "both";
|
|
345
|
+
const tokens = raw
|
|
346
|
+
.toLowerCase()
|
|
347
|
+
.replace(/:/g, ",")
|
|
348
|
+
.split(",")
|
|
349
|
+
.map((t) => t.trim())
|
|
350
|
+
.filter(Boolean);
|
|
351
|
+
|
|
352
|
+
let hasSlider = tokens.includes("slider");
|
|
353
|
+
let mode = "both";
|
|
354
|
+
let alignment = "alignleft";
|
|
355
|
+
const styles = [];
|
|
356
|
+
let buttons = showCoords
|
|
357
|
+
? ["zoom_in", "zoom_out", "reset", "center", "coords"]
|
|
358
|
+
: ["zoom_in", "zoom_out", "reset", "center"];
|
|
359
|
+
let isCustom = false;
|
|
360
|
+
|
|
361
|
+
const hiddenOptions = ["hidden", "none"];
|
|
362
|
+
const styleOptions = ["compact", "labels_on_hover", "labels-on-hover"];
|
|
363
|
+
const modeOptions = ["icon", "text", "both"];
|
|
364
|
+
const alignmentOptions = ["alignleft", "aligncenter", "alignright"];
|
|
365
|
+
|
|
366
|
+
const normalized = raw.toLowerCase();
|
|
367
|
+
if (hiddenOptions.includes(normalized)) {
|
|
368
|
+
mode = "hidden";
|
|
369
|
+
buttons = [];
|
|
370
|
+
} else if (normalized === "minimal") {
|
|
371
|
+
buttons = showCoords
|
|
372
|
+
? ["zoom_in", "zoom_out", "center", "coords"]
|
|
373
|
+
: ["zoom_in", "zoom_out", "center"];
|
|
374
|
+
} else if (normalized === "slider") {
|
|
375
|
+
hasSlider = true;
|
|
376
|
+
buttons = showCoords
|
|
377
|
+
? ["reset", "center", "coords"]
|
|
378
|
+
: ["reset", "center"];
|
|
379
|
+
} else if (styleOptions.includes(normalized)) {
|
|
380
|
+
styles.push(normalized.replace("_", "-"));
|
|
381
|
+
} else if (alignmentOptions.includes(normalized)) {
|
|
382
|
+
alignment = normalized;
|
|
383
|
+
} else if (modeOptions.includes(normalized)) {
|
|
384
|
+
mode = normalized;
|
|
385
|
+
} else if (normalized === "custom" || normalized.includes(",")) {
|
|
386
|
+
isCustom = true;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
if (isCustom) {
|
|
390
|
+
let parts = raw
|
|
391
|
+
.replace(/:/g, ",")
|
|
392
|
+
.split(",")
|
|
393
|
+
.map((t) => t.trim())
|
|
394
|
+
.filter(Boolean);
|
|
395
|
+
if (parts[0] && parts[0].toLowerCase() === "custom") {
|
|
396
|
+
parts = parts.slice(1);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
if (parts[0] && parts[0].toLowerCase() === "slider") {
|
|
400
|
+
hasSlider = true;
|
|
401
|
+
parts = parts.slice(1);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
if (parts[0] && modeOptions.includes(parts[0].toLowerCase())) {
|
|
405
|
+
mode = parts[0].toLowerCase();
|
|
406
|
+
parts = parts.slice(1);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
if (parts[0] && alignmentOptions.includes(parts[0].toLowerCase())) {
|
|
410
|
+
alignment = parts[0].toLowerCase();
|
|
411
|
+
parts = parts.slice(1);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const styleTokens = parts.filter((token) =>
|
|
415
|
+
styleOptions.includes(token.toLowerCase())
|
|
416
|
+
);
|
|
417
|
+
styleTokens.forEach((token) => {
|
|
418
|
+
styles.push(token.replace("_", "-").toLowerCase());
|
|
419
|
+
});
|
|
420
|
+
parts = parts.filter(
|
|
421
|
+
(token) => !styleOptions.includes(token.toLowerCase())
|
|
422
|
+
);
|
|
423
|
+
|
|
424
|
+
const customButtons = [];
|
|
425
|
+
parts.forEach((token) => {
|
|
426
|
+
const key = token.toLowerCase();
|
|
427
|
+
if (key === "slider") {
|
|
428
|
+
hasSlider = true;
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
if (key === "coords" && !showCoords) {
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
if (
|
|
435
|
+
["zoom_in", "zoom_out", "reset", "center", "coords"].includes(key) &&
|
|
436
|
+
!customButtons.includes(key)
|
|
437
|
+
) {
|
|
438
|
+
customButtons.push(key);
|
|
439
|
+
}
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
if (mode !== "hidden") {
|
|
443
|
+
if (customButtons.length > 0) {
|
|
444
|
+
buttons = customButtons;
|
|
445
|
+
} else if (hasSlider) {
|
|
446
|
+
buttons = showCoords
|
|
447
|
+
? ["reset", "center", "coords"]
|
|
448
|
+
: ["reset", "center"];
|
|
449
|
+
}
|
|
450
|
+
} else {
|
|
451
|
+
buttons = [];
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
if (mode !== "hidden") {
|
|
456
|
+
if (showCoords && !buttons.includes("coords")) {
|
|
457
|
+
buttons.push("coords");
|
|
458
|
+
}
|
|
459
|
+
if (!showCoords) {
|
|
460
|
+
buttons = buttons.filter((key) => key !== "coords");
|
|
461
|
+
}
|
|
462
|
+
if (buttons.length === 0) {
|
|
463
|
+
buttons = showCoords
|
|
464
|
+
? ["zoom_in", "zoom_out", "reset", "center", "coords"]
|
|
465
|
+
: ["zoom_in", "zoom_out", "reset", "center"];
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
if (hasSlider) {
|
|
470
|
+
if (!raw.includes("zoom_in")) {
|
|
471
|
+
buttons = buttons.filter((key) => key !== "zoom_in");
|
|
472
|
+
}
|
|
473
|
+
if (!raw.includes("zoom_out")) {
|
|
474
|
+
buttons = buttons.filter((key) => key !== "zoom_out");
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
return {
|
|
479
|
+
position: pos,
|
|
480
|
+
mode,
|
|
481
|
+
styles: [...new Set(styles)],
|
|
482
|
+
alignment,
|
|
483
|
+
buttons,
|
|
484
|
+
has_slider: hasSlider,
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function buildControlsMarkup(viewerId, config) {
|
|
489
|
+
const resolvedConfig =
|
|
490
|
+
config || parseControlsConfig(Boolean(state.show_coords));
|
|
491
|
+
if (resolvedConfig.mode === "hidden") {
|
|
492
|
+
return "";
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
const classes = [
|
|
496
|
+
"svg-controls",
|
|
497
|
+
`controls-mode-${resolvedConfig.mode}`,
|
|
498
|
+
`controls-align-${resolvedConfig.alignment}`,
|
|
499
|
+
];
|
|
500
|
+
resolvedConfig.styles.forEach((style) => {
|
|
501
|
+
classes.push(`controls-style-${style}`);
|
|
502
|
+
});
|
|
503
|
+
if (["left", "right"].includes(resolvedConfig.position)) {
|
|
504
|
+
classes.push("controls-vertical");
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
const sliderMarkup = resolvedConfig.has_slider
|
|
508
|
+
? `
|
|
509
|
+
<div class="zoom-slider-wrapper">
|
|
510
|
+
<input type="range"
|
|
511
|
+
class="zoom-slider"
|
|
512
|
+
data-viewer="${viewerId}"
|
|
513
|
+
min="${clampZoom(state.min_zoom)}"
|
|
514
|
+
max="${clampZoom(state.max_zoom)}"
|
|
515
|
+
step="${Math.max(1, Number(state.zoom_step) || 10)}"
|
|
516
|
+
value="${clampZoom(state.zoom)}"
|
|
517
|
+
aria-label="Zoom level" />
|
|
518
|
+
</div>
|
|
519
|
+
`
|
|
520
|
+
: "";
|
|
521
|
+
|
|
522
|
+
const buttonMarkup = resolvedConfig.buttons
|
|
523
|
+
.map((key) => buttonDefinition(key))
|
|
524
|
+
.filter(Boolean)
|
|
525
|
+
.map(
|
|
526
|
+
(definition) => `
|
|
527
|
+
<button type="button"
|
|
528
|
+
class="svg-viewer-btn ${definition.class}"
|
|
529
|
+
data-viewer="${viewerId}"
|
|
530
|
+
title="${escapeHtml(definition.title)}"
|
|
531
|
+
aria-label="${escapeHtml(definition.text)}">
|
|
532
|
+
<span class="btn-icon" aria-hidden="true">${definition.icon}</span>
|
|
533
|
+
<span class="btn-text">${escapeHtml(definition.text)}</span>
|
|
534
|
+
</button>
|
|
535
|
+
`
|
|
536
|
+
)
|
|
537
|
+
.join("");
|
|
538
|
+
|
|
539
|
+
const coordOutput = resolvedConfig.buttons.includes("coords")
|
|
540
|
+
? `<span class="coord-output" data-viewer="${viewerId}" aria-live="polite"></span>`
|
|
541
|
+
: "";
|
|
542
|
+
|
|
543
|
+
return `
|
|
544
|
+
<div class="${classes.join(" ")}" data-viewer="${viewerId}">
|
|
545
|
+
${sliderMarkup}
|
|
546
|
+
${buttonMarkup}
|
|
547
|
+
${coordOutput}
|
|
548
|
+
<div class="divider"></div>
|
|
549
|
+
<span class="zoom-display">
|
|
550
|
+
<span class="zoom-percentage" data-viewer="${viewerId}">${clampZoom(
|
|
551
|
+
state.zoom
|
|
552
|
+
)}</span>%
|
|
553
|
+
</span>
|
|
554
|
+
</div>
|
|
555
|
+
`;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
function buttonDefinition(key) {
|
|
559
|
+
const definitions = {
|
|
560
|
+
zoom_in: {
|
|
561
|
+
class: "zoom-in-btn",
|
|
562
|
+
icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640" aria-hidden="true" focusable="false"><path fill="currentColor" d="M480 272C480 317.9 465.1 360.3 440 394.7L566.6 521.4C579.1 533.9 579.1 554.2 566.6 566.7C554.1 579.2 533.8 579.2 521.3 566.7L394.7 440C360.3 465.1 317.9 480 272 480C157.1 480 64 386.9 64 272C64 157.1 157.1 64 272 64C386.9 64 480 157.1 480 272zM272 176C258.7 176 248 186.7 248 200L248 248L200 248C186.7 248 176 258.7 176 272C176 285.3 186.7 296 200 296L248 296L248 344C248 357.3 258.7 368 272 368C285.3 368 296 357.3 296 344L296 296L344 296C357.3 296 368 285.3 368 272C368 258.7 357.3 248 344 248L296 248L296 200C296 186.7 285.3 176 272 176z"/></svg>',
|
|
563
|
+
text: "Zoom In",
|
|
564
|
+
title: "Zoom In (Ctrl +)",
|
|
565
|
+
},
|
|
566
|
+
zoom_out: {
|
|
567
|
+
class: "zoom-out-btn",
|
|
568
|
+
icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640" aria-hidden="true" focusable="false"><path fill="currentColor" d="M480 272C480 317.9 465.1 360.3 440 394.7L566.6 521.4C579.1 533.9 579.1 554.2 566.6 566.7C554.1 579.2 533.8 579.2 521.3 566.7L394.7 440C360.3 465.1 317.9 480 272 480C157.1 480 64 386.9 64 272C64 157.1 157.1 64 272 64C386.9 64 480 157.1 480 272zM200 248C186.7 248 176 258.7 176 272C176 285.3 186.7 296 200 296L344 296C357.3 296 368 285.3 368 272C368 258.7 357.3 248 344 248L200 248z"/></svg>',
|
|
569
|
+
text: "Zoom Out",
|
|
570
|
+
title: "Zoom Out (Ctrl -)",
|
|
571
|
+
},
|
|
572
|
+
reset: {
|
|
573
|
+
class: "reset-zoom-btn",
|
|
574
|
+
icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640" aria-hidden="true" focusable="false"><path fill="currentColor" d="M480 272C480 317.9 465.1 360.3 440 394.7L566.6 521.4C579.1 533.9 579.1 554.2 566.6 566.7C554.1 579.2 533.8 579.2 521.3 566.7L394.7 440C360.3 465.1 317.9 480 272 480C157.1 480 64 386.9 64 272C64 157.1 157.1 64 272 64C386.9 64 480 157.1 480 272zM272 416C351.5 416 416 351.5 416 272C416 192.5 351.5 128 272 128C192.5 128 128 192.5 128 272C128 351.5 192.5 416 272 416z"/></svg>',
|
|
575
|
+
text: "Reset Zoom",
|
|
576
|
+
title: "Reset Zoom",
|
|
577
|
+
},
|
|
578
|
+
center: {
|
|
579
|
+
class: "center-view-btn",
|
|
580
|
+
icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640" aria-hidden="true" focusable="false"><path fill="currentColor" d="M320 48C337.7 48 352 62.3 352 80L352 98.3C450.1 112.3 527.7 189.9 541.7 288L560 288C577.7 288 592 302.3 592 320C592 337.7 577.7 352 560 352L541.7 352C527.7 450.1 450.1 527.7 352 541.7L352 560C352 577.7 337.7 592 320 592C302.3 592 288 577.7 288 560L288 541.7C189.9 527.7 112.3 450.1 98.3 352L80 352C62.3 352 48 337.7 48 320C48 302.3 62.3 288 80 288L98.3 288C112.3 189.9 189.9 112.3 288 98.3L288 80C288 62.3 302.3 48 320 48zM163.2 352C175.9 414.7 225.3 464.1 288 476.8L288 464C288 446.3 302.3 432 320 432C337.7 432 352 446.3 352 464L352 476.8C414.7 464.1 464.1 414.7 476.8 352L464 352C446.3 352 432 337.7 432 320C432 302.3 446.3 288 464 288L476.8 288C464.1 225.3 414.7 175.9 352 163.2L352 176C352 193.7 337.7 208 320 208C302.3 208 288 193.7 288 176L288 163.2C225.3 175.9 175.9 225.3 163.2 288L176 288C193.7 288 208 302.3 208 320C208 337.7 193.7 352 176 352L163.2 352zM320 272C346.5 272 368 293.5 368 320C368 346.5 346.5 368 320 368C293.5 368 272 346.5 272 320C272 293.5 293.5 272 320 272z"/></svg>',
|
|
581
|
+
text: "Center View",
|
|
582
|
+
title: "Center View",
|
|
583
|
+
},
|
|
584
|
+
coords: {
|
|
585
|
+
class: "coord-copy-btn",
|
|
586
|
+
icon: "📍",
|
|
587
|
+
text: "Copy Center",
|
|
588
|
+
title: "Copy current center coordinates",
|
|
589
|
+
},
|
|
590
|
+
};
|
|
591
|
+
return definitions[key];
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
function setFieldValue(field, value) {
|
|
595
|
+
if (field.type === "checkbox") {
|
|
596
|
+
field.checked = Boolean(value);
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
if (field.type === "color") {
|
|
600
|
+
field.value = value || "#000000";
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
field.value = value ?? "";
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
function readFieldValue(field) {
|
|
607
|
+
if (field.type === "checkbox") {
|
|
608
|
+
return field.checked;
|
|
609
|
+
}
|
|
610
|
+
if (field.type === "color") {
|
|
611
|
+
return field.value === field.dataset.originalValue ? "" : field.value;
|
|
612
|
+
}
|
|
613
|
+
return field.value;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
function syncFields() {
|
|
617
|
+
fieldElements.forEach((field) => {
|
|
618
|
+
const key = field.dataset.setting;
|
|
619
|
+
if (key in state) {
|
|
620
|
+
setFieldValue(field, state[key]);
|
|
621
|
+
}
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
function buildWrapperClasses() {
|
|
626
|
+
const controlsConfig = parseControlsConfig(Boolean(state.show_coords));
|
|
627
|
+
const classes = [
|
|
628
|
+
"svg-viewer-wrapper",
|
|
629
|
+
`controls-position-${controlsConfig.position}`,
|
|
630
|
+
`controls-mode-${controlsConfig.mode}`,
|
|
631
|
+
`pan-mode-${normalizePan(state.pan_mode)}`,
|
|
632
|
+
`zoom-mode-${normalizeZoom(state.zoom_mode)}`,
|
|
633
|
+
];
|
|
634
|
+
controlsConfig.styles.forEach((style) =>
|
|
635
|
+
classes.push(`controls-style-${style}`)
|
|
636
|
+
);
|
|
637
|
+
if (state.className) classes.push(state.className);
|
|
638
|
+
return classes.join(" ");
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
function buildMainClasses(controlsConfig) {
|
|
642
|
+
const classes = [
|
|
643
|
+
"svg-viewer-main",
|
|
644
|
+
`controls-position-${controlsConfig.position}`,
|
|
645
|
+
`controls-align-${controlsConfig.alignment}`,
|
|
646
|
+
];
|
|
647
|
+
controlsConfig.styles.forEach((style) =>
|
|
648
|
+
classes.push(`controls-style-${style}`)
|
|
649
|
+
);
|
|
650
|
+
return classes.join(" ");
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
function buildWrapperStyle() {
|
|
654
|
+
const declarations = ["width: 100%", "max-width: 100%", "min-width: 0"];
|
|
655
|
+
if (isHexColor(state.button_fill)) {
|
|
656
|
+
declarations.push(`--svg-viewer-button-fill: ${state.button_fill}`);
|
|
657
|
+
const hover = adjustBrightness(state.button_fill, -12);
|
|
658
|
+
if (hover) declarations.push(`--svg-viewer-button-hover: ${hover}`);
|
|
659
|
+
}
|
|
660
|
+
const border = state.button_border || state.button_fill;
|
|
661
|
+
if (isHexColor(border)) {
|
|
662
|
+
declarations.push(`--svg-viewer-button-border: ${border}`);
|
|
663
|
+
}
|
|
664
|
+
if (isHexColor(state.button_foreground)) {
|
|
665
|
+
declarations.push(`--svg-viewer-button-text: ${state.button_foreground}`);
|
|
666
|
+
}
|
|
667
|
+
return declarations.join("; ");
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
function ensureViewerScript(callback) {
|
|
671
|
+
if (window.SVGViewer) {
|
|
672
|
+
callback();
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
const interval = setInterval(() => {
|
|
676
|
+
if (window.SVGViewer) {
|
|
677
|
+
clearInterval(interval);
|
|
678
|
+
callback();
|
|
679
|
+
}
|
|
680
|
+
}, 50);
|
|
681
|
+
setTimeout(() => clearInterval(interval), 3000);
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
function clampZoom(value) {
|
|
685
|
+
const numeric = Number(value);
|
|
686
|
+
if (Number.isFinite(numeric)) {
|
|
687
|
+
return Math.max(1, Math.min(3200, Math.round(numeric)));
|
|
688
|
+
}
|
|
689
|
+
return 100;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
function parseMaybeFloat(value) {
|
|
693
|
+
const numeric = Number(value);
|
|
694
|
+
return Number.isFinite(numeric) ? numeric : null;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
function normalizePan(value) {
|
|
698
|
+
return value && value.toString().toLowerCase() === "drag"
|
|
699
|
+
? "drag"
|
|
700
|
+
: "scroll";
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
function normalizeZoom(value) {
|
|
704
|
+
const normalized = (value || "")
|
|
705
|
+
.toString()
|
|
706
|
+
.toLowerCase()
|
|
707
|
+
.replace(/[\s-]+/g, "_");
|
|
708
|
+
if (normalized === "click" || normalized === "scroll") return normalized;
|
|
709
|
+
return "super_scroll";
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
function escapeQuotes(value) {
|
|
713
|
+
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
function escapeHtml(value) {
|
|
717
|
+
return value
|
|
718
|
+
.replace(/&/g, "&")
|
|
719
|
+
.replace(/</g, "<")
|
|
720
|
+
.replace(/>/g, ">")
|
|
721
|
+
.replace(/"/g, """)
|
|
722
|
+
.replace(/'/g, "'");
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
function isHexColor(value) {
|
|
726
|
+
return /^#([0-9a-f]{3}){1,2}$/i.test(value || "");
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
function adjustBrightness(hexColor, percentage) {
|
|
730
|
+
if (!isHexColor(hexColor)) return null;
|
|
731
|
+
let hex = hexColor.replace("#", "");
|
|
732
|
+
if (hex.length === 3)
|
|
733
|
+
hex = hex
|
|
734
|
+
.split("")
|
|
735
|
+
.map((c) => c + c)
|
|
736
|
+
.join("");
|
|
737
|
+
const amount = Math.round(2.55 * percentage);
|
|
738
|
+
const [r, g, b] = hex.match(/.{2}/g).map((component) => {
|
|
739
|
+
return Math.max(0, Math.min(255, parseInt(component, 16) + amount));
|
|
740
|
+
});
|
|
741
|
+
return `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
async function copyText(text) {
|
|
745
|
+
try {
|
|
746
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
747
|
+
await navigator.clipboard.writeText(text);
|
|
748
|
+
} else {
|
|
749
|
+
const textarea = document.createElement("textarea");
|
|
750
|
+
textarea.value = text;
|
|
751
|
+
textarea.style.position = "fixed";
|
|
752
|
+
textarea.style.top = "-9999px";
|
|
753
|
+
document.body.appendChild(textarea);
|
|
754
|
+
textarea.focus();
|
|
755
|
+
textarea.select();
|
|
756
|
+
document.execCommand("copy");
|
|
757
|
+
document.body.removeChild(textarea);
|
|
758
|
+
}
|
|
759
|
+
} catch (error) {
|
|
760
|
+
console.error("[SVG Viewer Preview] copy failed", error);
|
|
761
|
+
flashStatus("Copy failed. Select and copy the text manually.", "error");
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
function openDialog(dialog) {
|
|
766
|
+
if (!dialog) return;
|
|
767
|
+
if (typeof dialog.showModal === "function") {
|
|
768
|
+
dialog.showModal();
|
|
769
|
+
} else {
|
|
770
|
+
dialog.setAttribute("open", "");
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
function closeDialog(dialog) {
|
|
775
|
+
if (!dialog) return;
|
|
776
|
+
if (typeof dialog.close === "function" && dialog.open) {
|
|
777
|
+
dialog.close();
|
|
778
|
+
} else {
|
|
779
|
+
dialog.removeAttribute("open");
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
let statusTimeout = null;
|
|
784
|
+
function flashStatus(message, tone = "info") {
|
|
785
|
+
if (!statusEl) return;
|
|
786
|
+
statusEl.textContent = message;
|
|
787
|
+
statusEl.dataset.tone = tone;
|
|
788
|
+
clearTimeout(statusTimeout);
|
|
789
|
+
statusTimeout = setTimeout(() => {
|
|
790
|
+
statusEl.textContent = "";
|
|
791
|
+
}, 3500);
|
|
792
|
+
}
|
|
793
|
+
})();
|