Skip to content

Commit a7982af

Browse files
author
Patrick Golden
committed
Allow truncating SVG in AppNodeText
Additionally, use AppNodeText for SVG labels
1 parent 3cafaf5 commit a7982af

File tree

2 files changed

+83
-15
lines changed

2 files changed

+83
-15
lines changed

frontend/src/components/AppNodeText.vue

Lines changed: 71 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,16 @@ import { onMounted, onUpdated, ref } from "vue";
3636
type Props = {
3737
text?: string;
3838
isSvg?: boolean;
39+
40+
// Truncating text in SVG is not possible using "text-overflow: ellipses",
41+
// so we use our own strategy.
42+
truncateWidth?: number;
3943
};
4044
4145
const props = withDefaults(defineProps<Props>(), {
4246
text: "",
4347
isSvg: false,
48+
truncateWidth: undefined,
4449
});
4550
4651
const container = ref<HTMLSpanElement | SVGTSpanElement | null>(null);
@@ -147,7 +152,7 @@ const replacementTags = new Map([
147152
]);
148153
149154
function buildDOM(containerEl: Element) {
150-
const text = props.text;
155+
const { text, truncateWidth, isSvg } = props;
151156
152157
const containsOnlyText =
153158
containerEl.childNodes.length === 1 &&
@@ -210,16 +215,79 @@ function buildDOM(containerEl: Element) {
210215
range.setEndBefore(endNode!);
211216
212217
// Surround that range with the appropriate DOM element.
213-
const el = createSurroundingEl(props.isSvg);
218+
const el = createSurroundingEl(isSvg);
214219
range.surroundContents(el);
215220
216221
// Run any code required after the container element is mounted.
217-
afterMount(props.isSvg, el);
222+
afterMount(isSvg, el);
218223
219224
// Remove the start and end tag text nodes
220225
startNode!.parentNode!.removeChild(startNode!);
221226
endNode!.parentNode!.removeChild(endNode!);
222227
});
228+
229+
// This is an SVG and we have been told to truncate it at a certain width.
230+
if (isSvg && truncateWidth) {
231+
const tspan = containerEl as SVGTSpanElement;
232+
233+
// Create a DOMPoint of the character where the text should be truncated.
234+
const pos = tspan.getStartPositionOfChar(0);
235+
pos.x += truncateWidth;
236+
237+
// Find the character (if any) where the string should be truncated.
238+
const endChar = tspan.getCharNumAtPosition(pos);
239+
240+
if (endChar > 0) {
241+
const textNodes = getAllTextNodes(containerEl);
242+
let firstNodeOut: Text | null = null;
243+
let curChar = 0;
244+
245+
textNodes.forEach((textNode) => {
246+
if (firstNodeOut) {
247+
// We've already truncated a node, so we can remove the content of
248+
// any following node.
249+
textNode.textContent = "";
250+
} else if (curChar + textNode.length > endChar) {
251+
// The character where we need to truncate is in this node. Get the
252+
// offset where we need to truncate, then split the text node into
253+
// two, and clear the contents of the second node.
254+
const splitAt = endChar - curChar;
255+
firstNodeOut = textNode.splitText(splitAt - 1);
256+
if (firstNodeOut) {
257+
firstNodeOut.textContent = "";
258+
}
259+
if (textNode.textContent) {
260+
// If the text node that has been truncated ends in whitespace,
261+
// remove that whitespace.
262+
textNode.textContent = textNode.textContent.trimEnd();
263+
}
264+
} else {
265+
// We haven't reached the text node where we need to truncate yet--
266+
// just add the length to the counter and move on.
267+
curChar += textNode.length;
268+
}
269+
});
270+
271+
const dotsText = document.createElementNS(
272+
"http://www.w3.org/2000/svg",
273+
"tspan",
274+
);
275+
dotsText.textContent = "...";
276+
containerEl.appendChild(dotsText);
277+
}
278+
}
279+
}
280+
281+
function getAllTextNodes(container: Node) {
282+
const textNodes: Text[] = [];
283+
const walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT);
284+
while (walker.nextNode()) {
285+
const node = walker.currentNode;
286+
if (node.nodeType === Node.TEXT_NODE) {
287+
textNodes.push(node as Text);
288+
}
289+
}
290+
return textNodes;
223291
}
224292
225293
onMounted(() => {

frontend/src/components/ThePhenogrid.vue

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,11 @@
7979
cellSize * 0.25
8080
}) rotate(-45)`"
8181
>
82-
{{ truncateLabels ? truncate(col.label) : col.label }}
82+
<AppNodeText
83+
is-svg
84+
:text="col.label"
85+
:truncate-width="marginLeft * 0.8"
86+
/>
8387
</text>
8488
<template #content>
8589
<AppNodeBadge :node="col" :absolute="true" :show-id="true" />
@@ -116,7 +120,11 @@
116120
(0.5 + rowIndex) * cellSize
117121
})`"
118122
>
119-
{{ truncateLabels ? truncate(row.label) : row.label }}
123+
<AppNodeText
124+
is-svg
125+
:text="row.label"
126+
:truncate-width="marginLeft * 0.8"
127+
/>
120128
</text>
121129
<template #content>
122130
<AppNodeBadge :node="row" :absolute="true" :show-id="true" />
@@ -364,10 +372,11 @@ import { useResizeObserver, useScroll } from "@vueuse/core";
364372
import type { TermInfo, TermPairwiseSimilarity } from "@/api/model";
365373
import AppCheckbox from "@/components/AppCheckbox.vue";
366374
import AppNodeBadge from "@/components/AppNodeBadge.vue";
375+
import AppNodeText from "@/components/AppNodeText.vue";
367376
import AppSelectSingle, { type Option } from "@/components/AppSelectSingle.vue";
368377
import { appendToBody } from "@/global/tooltip";
369378
import { frame } from "@/util/debug";
370-
import { screenToSvgCoords, truncateBySize } from "@/util/dom";
379+
import { screenToSvgCoords } from "@/util/dom";
371380
import { downloadSvg } from "@/util/download";
372381
import { copyToClipboard } from "@/util/string";
373382
import type AppFlex from "./AppFlex.vue";
@@ -491,15 +500,6 @@ function copy() {
491500
);
492501
}
493502
494-
/** truncate labels */
495-
function truncate(text?: string) {
496-
return truncateBySize(
497-
text || "",
498-
marginLeft * 0.9,
499-
cellSize * 0.5 + "px Poppins",
500-
);
501-
}
502-
503503
/** track grid scroll */
504504
const scrollInfo = useScroll(scroll);
505505
const scrollCoords = ref({ x: 0, y: 0, w: 0, h: 0 });

0 commit comments

Comments
 (0)