@opendata-ai/openchart-vanilla 6.3.0 → 6.4.1
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.
- package/dist/index.d.ts +44 -3
- package/dist/index.js +580 -4
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/selection-events.test.ts +869 -0
- package/src/index.ts +3 -1
- package/src/mount.ts +607 -3
- package/src/svg-renderer.ts +3 -0
- package/src/text-edit-overlay.ts +255 -0
package/dist/index.js
CHANGED
|
@@ -3255,7 +3255,7 @@ function escapeHtml(str) {
|
|
|
3255
3255
|
}
|
|
3256
3256
|
|
|
3257
3257
|
// src/mount.ts
|
|
3258
|
-
import { isLayerSpec } from "@opendata-ai/openchart-core";
|
|
3258
|
+
import { elementRef, isLayerSpec } from "@opendata-ai/openchart-core";
|
|
3259
3259
|
import { compileChart, compileLayer } from "@opendata-ai/openchart-engine";
|
|
3260
3260
|
|
|
3261
3261
|
// src/svg-renderer.ts
|
|
@@ -3847,6 +3847,9 @@ function renderAnnotation(parent, annotation, index2) {
|
|
|
3847
3847
|
const g = createSVGElement("g");
|
|
3848
3848
|
g.setAttribute("class", `viz-annotation viz-annotation-${annotation.type}`);
|
|
3849
3849
|
g.setAttribute("data-annotation-index", String(index2));
|
|
3850
|
+
if (annotation.id) {
|
|
3851
|
+
g.setAttribute("data-annotation-id", annotation.id);
|
|
3852
|
+
}
|
|
3850
3853
|
if (annotation.rect) {
|
|
3851
3854
|
const rect = createSVGElement("rect");
|
|
3852
3855
|
rect.setAttribute("class", "viz-annotation-range");
|
|
@@ -4181,6 +4184,159 @@ function renderChartSVG(layout, container) {
|
|
|
4181
4184
|
return svg;
|
|
4182
4185
|
}
|
|
4183
4186
|
|
|
4187
|
+
// src/text-edit-overlay.ts
|
|
4188
|
+
function getScale(svg) {
|
|
4189
|
+
const viewBox = svg.viewBox?.baseVal;
|
|
4190
|
+
const svgRect = svg.getBoundingClientRect();
|
|
4191
|
+
return {
|
|
4192
|
+
scaleX: viewBox?.width && svgRect.width ? svgRect.width / viewBox.width : 1,
|
|
4193
|
+
scaleY: viewBox?.height && svgRect.height ? svgRect.height / viewBox.height : 1
|
|
4194
|
+
};
|
|
4195
|
+
}
|
|
4196
|
+
function getTextStyles(targetElement, scale) {
|
|
4197
|
+
const computed = window.getComputedStyle(targetElement);
|
|
4198
|
+
const svgFontSize = parseFloat(
|
|
4199
|
+
targetElement.getAttribute("font-size") ?? computed.fontSize ?? "14"
|
|
4200
|
+
);
|
|
4201
|
+
const pixelFontSize = svgFontSize * scale.scaleY;
|
|
4202
|
+
const fontFamily = targetElement.getAttribute("font-family") ?? computed.fontFamily ?? "inherit";
|
|
4203
|
+
const fontWeight = targetElement.getAttribute("font-weight") ?? computed.fontWeight ?? "400";
|
|
4204
|
+
const fill = targetElement.style.getPropertyValue("fill") || targetElement.getAttribute("fill") || computed.color || "#000";
|
|
4205
|
+
const textAnchor = targetElement.getAttribute("text-anchor") ?? "start";
|
|
4206
|
+
let textAlign = "left";
|
|
4207
|
+
if (textAnchor === "middle") textAlign = "center";
|
|
4208
|
+
else if (textAnchor === "end") textAlign = "right";
|
|
4209
|
+
return {
|
|
4210
|
+
fontFamily,
|
|
4211
|
+
fontSize: `${pixelFontSize}px`,
|
|
4212
|
+
fontWeight,
|
|
4213
|
+
color: fill,
|
|
4214
|
+
textAlign,
|
|
4215
|
+
lineHeight: "1.3"
|
|
4216
|
+
};
|
|
4217
|
+
}
|
|
4218
|
+
function computePosition2(targetElement, svg, container) {
|
|
4219
|
+
const bbox = targetElement.getBBox();
|
|
4220
|
+
const scale = getScale(svg);
|
|
4221
|
+
const svgRect = svg.getBoundingClientRect();
|
|
4222
|
+
const containerRect = container.getBoundingClientRect();
|
|
4223
|
+
const padding = 4;
|
|
4224
|
+
const left = bbox.x * scale.scaleX + (svgRect.left - containerRect.left) - padding;
|
|
4225
|
+
const top = bbox.y * scale.scaleY + (svgRect.top - containerRect.top) - padding;
|
|
4226
|
+
const width = bbox.width * scale.scaleX + padding * 2;
|
|
4227
|
+
const height = bbox.height * scale.scaleY + padding * 2;
|
|
4228
|
+
return {
|
|
4229
|
+
top: Math.max(0, top),
|
|
4230
|
+
left: Math.max(0, left),
|
|
4231
|
+
width: Math.max(width, 60),
|
|
4232
|
+
height: Math.max(height, 24)
|
|
4233
|
+
};
|
|
4234
|
+
}
|
|
4235
|
+
function createTextEditOverlay(config) {
|
|
4236
|
+
const { container, svg, targetElement, currentText, onCommit, onCancel } = config;
|
|
4237
|
+
let destroyed = false;
|
|
4238
|
+
const originalOpacity = targetElement.getAttribute("opacity");
|
|
4239
|
+
targetElement.setAttribute("opacity", "0");
|
|
4240
|
+
const scale = getScale(svg);
|
|
4241
|
+
const styles = getTextStyles(targetElement, scale);
|
|
4242
|
+
const position = computePosition2(targetElement, svg, container);
|
|
4243
|
+
const textarea = document.createElement("textarea");
|
|
4244
|
+
textarea.value = currentText;
|
|
4245
|
+
const containerPosition = window.getComputedStyle(container).position;
|
|
4246
|
+
const containerWasStatic = containerPosition === "static";
|
|
4247
|
+
if (containerWasStatic) {
|
|
4248
|
+
container.style.position = "relative";
|
|
4249
|
+
}
|
|
4250
|
+
Object.assign(textarea.style, {
|
|
4251
|
+
position: "absolute",
|
|
4252
|
+
top: `${position.top}px`,
|
|
4253
|
+
left: `${position.left}px`,
|
|
4254
|
+
width: `${position.width}px`,
|
|
4255
|
+
minHeight: `${position.height}px`,
|
|
4256
|
+
fontFamily: styles.fontFamily,
|
|
4257
|
+
fontSize: styles.fontSize,
|
|
4258
|
+
fontWeight: styles.fontWeight,
|
|
4259
|
+
color: styles.color,
|
|
4260
|
+
textAlign: styles.textAlign,
|
|
4261
|
+
lineHeight: styles.lineHeight,
|
|
4262
|
+
padding: "2px 4px",
|
|
4263
|
+
margin: "0",
|
|
4264
|
+
border: "1px solid rgba(79, 70, 229, 0.4)",
|
|
4265
|
+
borderRadius: "3px",
|
|
4266
|
+
background: "rgba(255, 255, 255, 0.95)",
|
|
4267
|
+
outline: "none",
|
|
4268
|
+
resize: "none",
|
|
4269
|
+
overflow: "hidden",
|
|
4270
|
+
boxSizing: "border-box",
|
|
4271
|
+
zIndex: "10000",
|
|
4272
|
+
// Remove default textarea appearance
|
|
4273
|
+
WebkitAppearance: "none",
|
|
4274
|
+
appearance: "none"
|
|
4275
|
+
});
|
|
4276
|
+
container.appendChild(textarea);
|
|
4277
|
+
textarea.focus();
|
|
4278
|
+
textarea.select();
|
|
4279
|
+
function destroy() {
|
|
4280
|
+
if (destroyed) return;
|
|
4281
|
+
destroyed = true;
|
|
4282
|
+
if (originalOpacity !== null) {
|
|
4283
|
+
targetElement.setAttribute("opacity", originalOpacity);
|
|
4284
|
+
} else {
|
|
4285
|
+
targetElement.removeAttribute("opacity");
|
|
4286
|
+
}
|
|
4287
|
+
textarea.removeEventListener("keydown", handleKeyDown);
|
|
4288
|
+
document.removeEventListener("mousedown", handleClickOutside);
|
|
4289
|
+
resizeObserver.disconnect();
|
|
4290
|
+
if (containerWasStatic) {
|
|
4291
|
+
container.style.position = "";
|
|
4292
|
+
}
|
|
4293
|
+
if (textarea.parentNode) {
|
|
4294
|
+
textarea.parentNode.removeChild(textarea);
|
|
4295
|
+
}
|
|
4296
|
+
}
|
|
4297
|
+
function commit() {
|
|
4298
|
+
if (destroyed) return;
|
|
4299
|
+
const newText = textarea.value;
|
|
4300
|
+
destroy();
|
|
4301
|
+
onCommit(newText);
|
|
4302
|
+
}
|
|
4303
|
+
function cancel() {
|
|
4304
|
+
if (destroyed) return;
|
|
4305
|
+
destroy();
|
|
4306
|
+
onCancel();
|
|
4307
|
+
}
|
|
4308
|
+
const handleKeyDown = (e) => {
|
|
4309
|
+
if (e.key === "Enter" && !e.shiftKey) {
|
|
4310
|
+
e.preventDefault();
|
|
4311
|
+
commit();
|
|
4312
|
+
} else if (e.key === "Escape") {
|
|
4313
|
+
e.preventDefault();
|
|
4314
|
+
cancel();
|
|
4315
|
+
}
|
|
4316
|
+
};
|
|
4317
|
+
textarea.addEventListener("keydown", handleKeyDown);
|
|
4318
|
+
const handleClickOutside = (e) => {
|
|
4319
|
+
if (!textarea.contains(e.target)) {
|
|
4320
|
+
commit();
|
|
4321
|
+
}
|
|
4322
|
+
};
|
|
4323
|
+
requestAnimationFrame(() => {
|
|
4324
|
+
if (!destroyed) {
|
|
4325
|
+
document.addEventListener("mousedown", handleClickOutside);
|
|
4326
|
+
}
|
|
4327
|
+
});
|
|
4328
|
+
const resizeObserver = new ResizeObserver(() => {
|
|
4329
|
+
if (destroyed) return;
|
|
4330
|
+
const newPosition = computePosition2(targetElement, svg, container);
|
|
4331
|
+
textarea.style.top = `${newPosition.top}px`;
|
|
4332
|
+
textarea.style.left = `${newPosition.left}px`;
|
|
4333
|
+
textarea.style.width = `${newPosition.width}px`;
|
|
4334
|
+
textarea.style.minHeight = `${newPosition.height}px`;
|
|
4335
|
+
});
|
|
4336
|
+
resizeObserver.observe(container);
|
|
4337
|
+
return { destroy };
|
|
4338
|
+
}
|
|
4339
|
+
|
|
4184
4340
|
// src/mount.ts
|
|
4185
4341
|
function resolveDarkMode2(mode) {
|
|
4186
4342
|
if (mode === "force") return true;
|
|
@@ -4458,7 +4614,7 @@ function createDragHandler(config) {
|
|
|
4458
4614
|
let activeDocTouchMove = null;
|
|
4459
4615
|
let activeDocTouchEnd = null;
|
|
4460
4616
|
let activeDocTouchCancel = null;
|
|
4461
|
-
function
|
|
4617
|
+
function getScale2() {
|
|
4462
4618
|
const viewBox = svg.viewBox?.baseVal;
|
|
4463
4619
|
const svgRect = svg.getBoundingClientRect();
|
|
4464
4620
|
return {
|
|
@@ -4468,7 +4624,7 @@ function createDragHandler(config) {
|
|
|
4468
4624
|
}
|
|
4469
4625
|
function startDrag(startX, startY) {
|
|
4470
4626
|
setDragging(true);
|
|
4471
|
-
const { scaleX, scaleY } =
|
|
4627
|
+
const { scaleX, scaleY } = getScale2();
|
|
4472
4628
|
element.style.cursor = "grabbing";
|
|
4473
4629
|
svg.style.userSelect = "none";
|
|
4474
4630
|
const handleMove = (clientX, clientY) => {
|
|
@@ -5125,6 +5281,167 @@ function createScreenReaderTable(layout, container) {
|
|
|
5125
5281
|
container.appendChild(table);
|
|
5126
5282
|
return table;
|
|
5127
5283
|
}
|
|
5284
|
+
var EDITABLE_HOVER_CSS = `
|
|
5285
|
+
.viz-editable-hover {
|
|
5286
|
+
outline: 1.5px solid rgba(79, 70, 229, 0.35);
|
|
5287
|
+
outline-offset: 2px;
|
|
5288
|
+
border-radius: 2px;
|
|
5289
|
+
}
|
|
5290
|
+
`;
|
|
5291
|
+
function makeEditable(svg) {
|
|
5292
|
+
svg.setAttribute("tabindex", "0");
|
|
5293
|
+
svg.style.outline = "none";
|
|
5294
|
+
const style = document.createElementNS("http://www.w3.org/2000/svg", "style");
|
|
5295
|
+
style.textContent = EDITABLE_HOVER_CSS;
|
|
5296
|
+
svg.insertBefore(style, svg.firstChild);
|
|
5297
|
+
}
|
|
5298
|
+
function hasEditingCallbacks(opts) {
|
|
5299
|
+
return !!(opts?.onEdit || opts?.onSelect || opts?.onDeselect || opts?.onTextEdit);
|
|
5300
|
+
}
|
|
5301
|
+
function findElementByRef(svg, ref) {
|
|
5302
|
+
switch (ref.type) {
|
|
5303
|
+
case "annotation": {
|
|
5304
|
+
if (ref.id) {
|
|
5305
|
+
const byId = svg.querySelector(`[data-annotation-id="${ref.id}"]`);
|
|
5306
|
+
if (byId) return byId;
|
|
5307
|
+
}
|
|
5308
|
+
return svg.querySelector(`[data-annotation-index="${ref.index}"]`);
|
|
5309
|
+
}
|
|
5310
|
+
case "chrome":
|
|
5311
|
+
return svg.querySelector(`[data-chrome-key="${ref.key}"]`);
|
|
5312
|
+
case "series-label":
|
|
5313
|
+
return svg.querySelector(`.viz-mark-label[data-series="${ref.series}"]`);
|
|
5314
|
+
case "legend":
|
|
5315
|
+
return svg.querySelector(".viz-legend");
|
|
5316
|
+
case "legend-entry":
|
|
5317
|
+
return svg.querySelector(`[data-legend-index="${ref.index}"]`);
|
|
5318
|
+
}
|
|
5319
|
+
}
|
|
5320
|
+
function buildElementRef(element, _specAnnotations) {
|
|
5321
|
+
const annotationEl = element.closest("[data-annotation-index]");
|
|
5322
|
+
if (annotationEl) {
|
|
5323
|
+
const index2 = Number(annotationEl.getAttribute("data-annotation-index"));
|
|
5324
|
+
const id = annotationEl.getAttribute("data-annotation-id") ?? void 0;
|
|
5325
|
+
return elementRef.annotation(index2, id);
|
|
5326
|
+
}
|
|
5327
|
+
const chromeEl = element.closest("[data-chrome-key]");
|
|
5328
|
+
if (chromeEl) {
|
|
5329
|
+
const key = chromeEl.getAttribute("data-chrome-key");
|
|
5330
|
+
if (key) return elementRef.chrome(key);
|
|
5331
|
+
}
|
|
5332
|
+
const seriesLabelEl = element.closest(".viz-mark-label[data-series]");
|
|
5333
|
+
if (seriesLabelEl) {
|
|
5334
|
+
const series = seriesLabelEl.getAttribute("data-series");
|
|
5335
|
+
if (series) return elementRef.seriesLabel(series);
|
|
5336
|
+
}
|
|
5337
|
+
const legendEntryEl = element.closest("[data-legend-index]");
|
|
5338
|
+
if (legendEntryEl) {
|
|
5339
|
+
const index2 = Number(legendEntryEl.getAttribute("data-legend-index"));
|
|
5340
|
+
const series = legendEntryEl.getAttribute("data-legend-label") ?? "";
|
|
5341
|
+
return elementRef.legendEntry(series, index2);
|
|
5342
|
+
}
|
|
5343
|
+
const legendEl = element.closest(".viz-legend");
|
|
5344
|
+
if (legendEl) return elementRef.legend();
|
|
5345
|
+
return null;
|
|
5346
|
+
}
|
|
5347
|
+
function getEditableElements(spec, layout) {
|
|
5348
|
+
const refs = [];
|
|
5349
|
+
const chromeKeys = ["title", "subtitle", "source", "byline", "footer"];
|
|
5350
|
+
for (const key of chromeKeys) {
|
|
5351
|
+
if (layout.chrome[key]) {
|
|
5352
|
+
refs.push(elementRef.chrome(key));
|
|
5353
|
+
}
|
|
5354
|
+
}
|
|
5355
|
+
const annotations = "annotations" in spec && Array.isArray(spec.annotations) ? spec.annotations : [];
|
|
5356
|
+
for (let i = 0; i < annotations.length; i++) {
|
|
5357
|
+
refs.push(elementRef.annotation(i, annotations[i].id));
|
|
5358
|
+
}
|
|
5359
|
+
const seriesLabels = [];
|
|
5360
|
+
for (const mark of layout.marks) {
|
|
5361
|
+
if (mark.type === "line" && mark.label?.visible && mark.seriesKey) {
|
|
5362
|
+
seriesLabels.push(mark.seriesKey);
|
|
5363
|
+
}
|
|
5364
|
+
}
|
|
5365
|
+
seriesLabels.sort();
|
|
5366
|
+
for (const series of seriesLabels) {
|
|
5367
|
+
refs.push(elementRef.seriesLabel(series));
|
|
5368
|
+
}
|
|
5369
|
+
if (layout.legend.entries.length > 0) {
|
|
5370
|
+
refs.push(elementRef.legend());
|
|
5371
|
+
}
|
|
5372
|
+
return refs;
|
|
5373
|
+
}
|
|
5374
|
+
function isTextEditable(ref, specAnnotations) {
|
|
5375
|
+
if (ref.type === "chrome") return true;
|
|
5376
|
+
if (ref.type === "annotation") {
|
|
5377
|
+
const annotation = specAnnotations[ref.index];
|
|
5378
|
+
return annotation?.type === "text";
|
|
5379
|
+
}
|
|
5380
|
+
return false;
|
|
5381
|
+
}
|
|
5382
|
+
function getElementText(ref, spec) {
|
|
5383
|
+
if (ref.type === "chrome") {
|
|
5384
|
+
const chromeConfig = "chrome" in spec ? spec.chrome : void 0;
|
|
5385
|
+
if (!chromeConfig) return null;
|
|
5386
|
+
const entry = chromeConfig[ref.key];
|
|
5387
|
+
if (typeof entry === "string") return entry;
|
|
5388
|
+
if (typeof entry === "object" && entry !== null && "text" in entry) {
|
|
5389
|
+
return entry.text;
|
|
5390
|
+
}
|
|
5391
|
+
return null;
|
|
5392
|
+
}
|
|
5393
|
+
if (ref.type === "annotation") {
|
|
5394
|
+
const annotations = "annotations" in spec && Array.isArray(spec.annotations) ? spec.annotations : [];
|
|
5395
|
+
const annotation = annotations[ref.index];
|
|
5396
|
+
if (annotation?.type === "text") return annotation.text ?? null;
|
|
5397
|
+
if (annotation?.label) return annotation.label;
|
|
5398
|
+
return null;
|
|
5399
|
+
}
|
|
5400
|
+
return null;
|
|
5401
|
+
}
|
|
5402
|
+
function refsEqual(a2, b) {
|
|
5403
|
+
if (a2 === null || b === null) return a2 === b;
|
|
5404
|
+
if (a2.type !== b.type) return false;
|
|
5405
|
+
switch (a2.type) {
|
|
5406
|
+
case "annotation": {
|
|
5407
|
+
const bAnno = b;
|
|
5408
|
+
if (a2.id && bAnno.id) return a2.id === bAnno.id;
|
|
5409
|
+
return a2.index === bAnno.index;
|
|
5410
|
+
}
|
|
5411
|
+
case "chrome":
|
|
5412
|
+
return a2.key === b.key;
|
|
5413
|
+
case "series-label":
|
|
5414
|
+
return a2.series === b.series;
|
|
5415
|
+
case "legend":
|
|
5416
|
+
return true;
|
|
5417
|
+
case "legend-entry": {
|
|
5418
|
+
const bEntry = b;
|
|
5419
|
+
return a2.index === bEntry.index && a2.series === bEntry.series;
|
|
5420
|
+
}
|
|
5421
|
+
}
|
|
5422
|
+
}
|
|
5423
|
+
function renderSelectionOverlay(svg, ref, layout) {
|
|
5424
|
+
const target = findElementByRef(svg, ref);
|
|
5425
|
+
if (!target) return null;
|
|
5426
|
+
const bbox = target.getBBox();
|
|
5427
|
+
const padding = 4;
|
|
5428
|
+
const accentColor = layout.theme.colors.categorical?.[0] ?? "#4f46e5";
|
|
5429
|
+
const g = document.createElementNS("http://www.w3.org/2000/svg", "g");
|
|
5430
|
+
g.setAttribute("class", "viz-selection-overlay");
|
|
5431
|
+
const rect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
|
|
5432
|
+
rect.setAttribute("x", String(bbox.x - padding));
|
|
5433
|
+
rect.setAttribute("y", String(bbox.y - padding));
|
|
5434
|
+
rect.setAttribute("width", String(bbox.width + padding * 2));
|
|
5435
|
+
rect.setAttribute("height", String(bbox.height + padding * 2));
|
|
5436
|
+
rect.setAttribute("rx", "3");
|
|
5437
|
+
rect.setAttribute("fill", "transparent");
|
|
5438
|
+
rect.setAttribute("stroke", accentColor);
|
|
5439
|
+
rect.setAttribute("stroke-width", "1.5");
|
|
5440
|
+
rect.setAttribute("pointer-events", "none");
|
|
5441
|
+
g.appendChild(rect);
|
|
5442
|
+
svg.appendChild(g);
|
|
5443
|
+
return g;
|
|
5444
|
+
}
|
|
5128
5445
|
function createChart(container, spec, options) {
|
|
5129
5446
|
let currentSpec = spec;
|
|
5130
5447
|
let currentLayout;
|
|
@@ -5138,11 +5455,17 @@ function createChart(container, spec, options) {
|
|
|
5138
5455
|
let cleanupChartEvents = null;
|
|
5139
5456
|
let cleanupAnnotationDrag = null;
|
|
5140
5457
|
let cleanupEditDrags = null;
|
|
5458
|
+
let cleanupSelection = null;
|
|
5459
|
+
let cleanupKeyboardEdit = null;
|
|
5141
5460
|
let srTable = null;
|
|
5142
5461
|
let destroyed = false;
|
|
5143
5462
|
let isDragging = false;
|
|
5144
5463
|
let pendingRender = false;
|
|
5145
5464
|
let resizeTimer = null;
|
|
5465
|
+
let selectedElement = options?.selectedElement ?? null;
|
|
5466
|
+
let overlayElement = null;
|
|
5467
|
+
let isTextEditingActive = false;
|
|
5468
|
+
let textEditCleanup = null;
|
|
5146
5469
|
const measureText = createMeasureText();
|
|
5147
5470
|
function compile() {
|
|
5148
5471
|
const { width, height } = getContainerDimensions();
|
|
@@ -5166,6 +5489,198 @@ function createChart(container, spec, options) {
|
|
|
5166
5489
|
height: Math.max(rect.height || 400, 100)
|
|
5167
5490
|
};
|
|
5168
5491
|
}
|
|
5492
|
+
function getSpecAnnotations() {
|
|
5493
|
+
return "annotations" in currentSpec && Array.isArray(currentSpec.annotations) ? currentSpec.annotations : [];
|
|
5494
|
+
}
|
|
5495
|
+
function selectElement(ref) {
|
|
5496
|
+
if (!svgElement) return;
|
|
5497
|
+
const target = findElementByRef(svgElement, ref);
|
|
5498
|
+
if (!target) return;
|
|
5499
|
+
if (selectedElement && !refsEqual(selectedElement, ref)) {
|
|
5500
|
+
deselectElement();
|
|
5501
|
+
}
|
|
5502
|
+
selectedElement = ref;
|
|
5503
|
+
overlayElement = renderSelectionOverlay(svgElement, ref, currentLayout);
|
|
5504
|
+
options?.onSelect?.(ref);
|
|
5505
|
+
svgElement.focus();
|
|
5506
|
+
}
|
|
5507
|
+
function deselectElement() {
|
|
5508
|
+
if (!selectedElement) return;
|
|
5509
|
+
if (isTextEditingActive && textEditCleanup) {
|
|
5510
|
+
textEditCleanup();
|
|
5511
|
+
textEditCleanup = null;
|
|
5512
|
+
isTextEditingActive = false;
|
|
5513
|
+
}
|
|
5514
|
+
const prev = selectedElement;
|
|
5515
|
+
selectedElement = null;
|
|
5516
|
+
if (overlayElement?.parentNode) {
|
|
5517
|
+
overlayElement.parentNode.removeChild(overlayElement);
|
|
5518
|
+
}
|
|
5519
|
+
overlayElement = null;
|
|
5520
|
+
options?.onDeselect?.(prev);
|
|
5521
|
+
}
|
|
5522
|
+
function enterTextEditing() {
|
|
5523
|
+
if (!svgElement || !selectedElement || isTextEditingActive) return;
|
|
5524
|
+
const specAnnotations = getSpecAnnotations();
|
|
5525
|
+
if (!isTextEditable(selectedElement, specAnnotations)) return;
|
|
5526
|
+
const currentText = getElementText(selectedElement, currentSpec);
|
|
5527
|
+
if (currentText === null) return;
|
|
5528
|
+
const target = findElementByRef(svgElement, selectedElement);
|
|
5529
|
+
if (!target) return;
|
|
5530
|
+
const textEl = target.tagName === "text" ? target : target.querySelector("text");
|
|
5531
|
+
if (!textEl) return;
|
|
5532
|
+
isTextEditingActive = true;
|
|
5533
|
+
const editRef = selectedElement;
|
|
5534
|
+
const overlay = createTextEditOverlay({
|
|
5535
|
+
container,
|
|
5536
|
+
svg: svgElement,
|
|
5537
|
+
targetElement: textEl,
|
|
5538
|
+
currentText,
|
|
5539
|
+
onCommit: (newText) => {
|
|
5540
|
+
isTextEditingActive = false;
|
|
5541
|
+
textEditCleanup = null;
|
|
5542
|
+
if (newText !== currentText) {
|
|
5543
|
+
options?.onTextEdit?.(editRef, currentText, newText);
|
|
5544
|
+
options?.onEdit?.({
|
|
5545
|
+
type: "text-edit",
|
|
5546
|
+
element: editRef,
|
|
5547
|
+
oldText: currentText,
|
|
5548
|
+
newText
|
|
5549
|
+
});
|
|
5550
|
+
}
|
|
5551
|
+
},
|
|
5552
|
+
onCancel: () => {
|
|
5553
|
+
isTextEditingActive = false;
|
|
5554
|
+
textEditCleanup = null;
|
|
5555
|
+
}
|
|
5556
|
+
});
|
|
5557
|
+
textEditCleanup = overlay.destroy;
|
|
5558
|
+
}
|
|
5559
|
+
function wireSelectionEvents() {
|
|
5560
|
+
if (!svgElement) return () => {
|
|
5561
|
+
};
|
|
5562
|
+
const svg = svgElement;
|
|
5563
|
+
const cleanups = [];
|
|
5564
|
+
const handleClick = (e) => {
|
|
5565
|
+
const mouseEvent = e;
|
|
5566
|
+
const target = mouseEvent.target;
|
|
5567
|
+
if (isTextEditingActive) return;
|
|
5568
|
+
const specAnnotations = getSpecAnnotations();
|
|
5569
|
+
const ref = buildElementRef(target, specAnnotations);
|
|
5570
|
+
if (ref) {
|
|
5571
|
+
selectElement(ref);
|
|
5572
|
+
} else {
|
|
5573
|
+
deselectElement();
|
|
5574
|
+
}
|
|
5575
|
+
};
|
|
5576
|
+
svg.addEventListener("click", handleClick);
|
|
5577
|
+
cleanups.push(() => svg.removeEventListener("click", handleClick));
|
|
5578
|
+
const handleMouseEnter = (e) => {
|
|
5579
|
+
const target = e.target.closest(
|
|
5580
|
+
"[data-annotation-index], [data-chrome-key], .viz-mark-label[data-series], .viz-legend, [data-legend-index]"
|
|
5581
|
+
);
|
|
5582
|
+
if (target) {
|
|
5583
|
+
target.classList.add("viz-editable-hover");
|
|
5584
|
+
}
|
|
5585
|
+
};
|
|
5586
|
+
const handleMouseLeave = (e) => {
|
|
5587
|
+
const target = e.target.closest(".viz-editable-hover");
|
|
5588
|
+
if (target) {
|
|
5589
|
+
target.classList.remove("viz-editable-hover");
|
|
5590
|
+
}
|
|
5591
|
+
};
|
|
5592
|
+
svg.addEventListener("mouseenter", handleMouseEnter, true);
|
|
5593
|
+
svg.addEventListener("mouseleave", handleMouseLeave, true);
|
|
5594
|
+
cleanups.push(() => {
|
|
5595
|
+
svg.removeEventListener("mouseenter", handleMouseEnter, true);
|
|
5596
|
+
svg.removeEventListener("mouseleave", handleMouseLeave, true);
|
|
5597
|
+
});
|
|
5598
|
+
const handleDblClick = (e) => {
|
|
5599
|
+
const mouseEvent = e;
|
|
5600
|
+
const target = mouseEvent.target;
|
|
5601
|
+
const specAnnotations = getSpecAnnotations();
|
|
5602
|
+
const ref = buildElementRef(target, specAnnotations);
|
|
5603
|
+
if (ref && isTextEditable(ref, specAnnotations)) {
|
|
5604
|
+
if (!refsEqual(selectedElement, ref)) {
|
|
5605
|
+
selectElement(ref);
|
|
5606
|
+
}
|
|
5607
|
+
enterTextEditing();
|
|
5608
|
+
}
|
|
5609
|
+
};
|
|
5610
|
+
svg.addEventListener("dblclick", handleDblClick);
|
|
5611
|
+
cleanups.push(() => svg.removeEventListener("dblclick", handleDblClick));
|
|
5612
|
+
return () => {
|
|
5613
|
+
for (const cleanup of cleanups) {
|
|
5614
|
+
cleanup();
|
|
5615
|
+
}
|
|
5616
|
+
};
|
|
5617
|
+
}
|
|
5618
|
+
function wireKeyboardEditEvents() {
|
|
5619
|
+
if (!svgElement) return () => {
|
|
5620
|
+
};
|
|
5621
|
+
const svg = svgElement;
|
|
5622
|
+
const handleKeyDown = (e) => {
|
|
5623
|
+
const specAnnotations = getSpecAnnotations();
|
|
5624
|
+
switch (e.key) {
|
|
5625
|
+
case "Delete":
|
|
5626
|
+
case "Backspace": {
|
|
5627
|
+
if (selectedElement && !isTextEditingActive) {
|
|
5628
|
+
e.preventDefault();
|
|
5629
|
+
options?.onEdit?.({ type: "delete", element: selectedElement });
|
|
5630
|
+
}
|
|
5631
|
+
break;
|
|
5632
|
+
}
|
|
5633
|
+
case "Escape": {
|
|
5634
|
+
e.preventDefault();
|
|
5635
|
+
if (isTextEditingActive && textEditCleanup) {
|
|
5636
|
+
textEditCleanup();
|
|
5637
|
+
textEditCleanup = null;
|
|
5638
|
+
isTextEditingActive = false;
|
|
5639
|
+
} else if (selectedElement) {
|
|
5640
|
+
deselectElement();
|
|
5641
|
+
}
|
|
5642
|
+
break;
|
|
5643
|
+
}
|
|
5644
|
+
case "ArrowDown":
|
|
5645
|
+
case "ArrowRight": {
|
|
5646
|
+
if (!isTextEditingActive && selectedElement) {
|
|
5647
|
+
e.preventDefault();
|
|
5648
|
+
const editables = getEditableElements(currentSpec, currentLayout);
|
|
5649
|
+
if (editables.length === 0) break;
|
|
5650
|
+
const currentIndex = editables.findIndex((r) => refsEqual(r, selectedElement));
|
|
5651
|
+
const nextIndex = currentIndex >= editables.length - 1 ? 0 : currentIndex + 1;
|
|
5652
|
+
selectElement(editables[nextIndex]);
|
|
5653
|
+
}
|
|
5654
|
+
break;
|
|
5655
|
+
}
|
|
5656
|
+
case "ArrowUp":
|
|
5657
|
+
case "ArrowLeft": {
|
|
5658
|
+
if (!isTextEditingActive && selectedElement) {
|
|
5659
|
+
e.preventDefault();
|
|
5660
|
+
const editables = getEditableElements(currentSpec, currentLayout);
|
|
5661
|
+
if (editables.length === 0) break;
|
|
5662
|
+
const currentIndex = editables.findIndex((r) => refsEqual(r, selectedElement));
|
|
5663
|
+
const nextIndex = currentIndex <= 0 ? editables.length - 1 : currentIndex - 1;
|
|
5664
|
+
selectElement(editables[nextIndex]);
|
|
5665
|
+
}
|
|
5666
|
+
break;
|
|
5667
|
+
}
|
|
5668
|
+
case "Enter": {
|
|
5669
|
+
if (selectedElement && !isTextEditingActive) {
|
|
5670
|
+
if (isTextEditable(selectedElement, specAnnotations)) {
|
|
5671
|
+
e.preventDefault();
|
|
5672
|
+
enterTextEditing();
|
|
5673
|
+
}
|
|
5674
|
+
}
|
|
5675
|
+
break;
|
|
5676
|
+
}
|
|
5677
|
+
}
|
|
5678
|
+
};
|
|
5679
|
+
svg.addEventListener("keydown", handleKeyDown);
|
|
5680
|
+
return () => {
|
|
5681
|
+
svg.removeEventListener("keydown", handleKeyDown);
|
|
5682
|
+
};
|
|
5683
|
+
}
|
|
5169
5684
|
function render() {
|
|
5170
5685
|
if (isDragging) {
|
|
5171
5686
|
pendingRender = true;
|
|
@@ -5199,6 +5714,20 @@ function createChart(container, spec, options) {
|
|
|
5199
5714
|
cleanupEditDrags();
|
|
5200
5715
|
cleanupEditDrags = null;
|
|
5201
5716
|
}
|
|
5717
|
+
if (cleanupSelection) {
|
|
5718
|
+
cleanupSelection();
|
|
5719
|
+
cleanupSelection = null;
|
|
5720
|
+
}
|
|
5721
|
+
if (cleanupKeyboardEdit) {
|
|
5722
|
+
cleanupKeyboardEdit();
|
|
5723
|
+
cleanupKeyboardEdit = null;
|
|
5724
|
+
}
|
|
5725
|
+
if (textEditCleanup) {
|
|
5726
|
+
textEditCleanup();
|
|
5727
|
+
textEditCleanup = null;
|
|
5728
|
+
isTextEditingActive = false;
|
|
5729
|
+
}
|
|
5730
|
+
overlayElement = null;
|
|
5202
5731
|
if (svgElement?.parentNode) {
|
|
5203
5732
|
svgElement.parentNode.removeChild(svgElement);
|
|
5204
5733
|
}
|
|
@@ -5270,6 +5799,20 @@ function createChart(container, spec, options) {
|
|
|
5270
5799
|
}
|
|
5271
5800
|
};
|
|
5272
5801
|
}
|
|
5802
|
+
if (hasEditingCallbacks(options)) {
|
|
5803
|
+
makeEditable(svgElement);
|
|
5804
|
+
cleanupSelection = wireSelectionEvents();
|
|
5805
|
+
cleanupKeyboardEdit = wireKeyboardEditEvents();
|
|
5806
|
+
if (selectedElement) {
|
|
5807
|
+
const target = findElementByRef(svgElement, selectedElement);
|
|
5808
|
+
if (target) {
|
|
5809
|
+
overlayElement = renderSelectionOverlay(svgElement, selectedElement, currentLayout);
|
|
5810
|
+
} else {
|
|
5811
|
+
selectedElement = null;
|
|
5812
|
+
overlayElement = null;
|
|
5813
|
+
}
|
|
5814
|
+
}
|
|
5815
|
+
}
|
|
5273
5816
|
srTable = createScreenReaderTable(currentLayout, container);
|
|
5274
5817
|
container.classList.add("viz-root");
|
|
5275
5818
|
const isDark = resolveDarkMode2(options?.darkMode);
|
|
@@ -5279,9 +5822,12 @@ function createChart(container, spec, options) {
|
|
|
5279
5822
|
container.classList.remove("viz-dark");
|
|
5280
5823
|
}
|
|
5281
5824
|
}
|
|
5282
|
-
function update(newSpec) {
|
|
5825
|
+
function update(newSpec, updateOpts) {
|
|
5283
5826
|
if (destroyed) return;
|
|
5284
5827
|
currentSpec = newSpec;
|
|
5828
|
+
if (updateOpts && "selectedElement" in updateOpts) {
|
|
5829
|
+
selectedElement = updateOpts.selectedElement ?? null;
|
|
5830
|
+
}
|
|
5285
5831
|
render();
|
|
5286
5832
|
}
|
|
5287
5833
|
function resize() {
|
|
@@ -5344,6 +5890,21 @@ function createChart(container, spec, options) {
|
|
|
5344
5890
|
cleanupEditDrags();
|
|
5345
5891
|
cleanupEditDrags = null;
|
|
5346
5892
|
}
|
|
5893
|
+
if (cleanupSelection) {
|
|
5894
|
+
cleanupSelection();
|
|
5895
|
+
cleanupSelection = null;
|
|
5896
|
+
}
|
|
5897
|
+
if (cleanupKeyboardEdit) {
|
|
5898
|
+
cleanupKeyboardEdit();
|
|
5899
|
+
cleanupKeyboardEdit = null;
|
|
5900
|
+
}
|
|
5901
|
+
if (textEditCleanup) {
|
|
5902
|
+
textEditCleanup();
|
|
5903
|
+
textEditCleanup = null;
|
|
5904
|
+
isTextEditingActive = false;
|
|
5905
|
+
}
|
|
5906
|
+
selectedElement = null;
|
|
5907
|
+
overlayElement = null;
|
|
5347
5908
|
if (disconnectResize) {
|
|
5348
5909
|
disconnectResize();
|
|
5349
5910
|
disconnectResize = null;
|
|
@@ -5380,6 +5941,20 @@ function createChart(container, spec, options) {
|
|
|
5380
5941
|
destroy,
|
|
5381
5942
|
get layout() {
|
|
5382
5943
|
return currentLayout;
|
|
5944
|
+
},
|
|
5945
|
+
getSelectedElement() {
|
|
5946
|
+
return selectedElement;
|
|
5947
|
+
},
|
|
5948
|
+
select(ref) {
|
|
5949
|
+
if (destroyed) return;
|
|
5950
|
+
selectElement(ref);
|
|
5951
|
+
},
|
|
5952
|
+
deselect() {
|
|
5953
|
+
if (destroyed) return;
|
|
5954
|
+
deselectElement();
|
|
5955
|
+
},
|
|
5956
|
+
get isEditing() {
|
|
5957
|
+
return isTextEditingActive;
|
|
5383
5958
|
}
|
|
5384
5959
|
};
|
|
5385
5960
|
}
|
|
@@ -6500,6 +7075,7 @@ export {
|
|
|
6500
7075
|
createGraph,
|
|
6501
7076
|
createSimulationWorker,
|
|
6502
7077
|
createTable,
|
|
7078
|
+
createTextEditOverlay,
|
|
6503
7079
|
createTooltipManager,
|
|
6504
7080
|
exportCSV,
|
|
6505
7081
|
exportJPG,
|