@@ -36,11 +36,16 @@ import { onMounted, onUpdated, ref } from "vue";
3636type 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
4145const props = withDefaults (defineProps <Props >(), {
4246 text: " " ,
4347 isSvg: false ,
48+ truncateWidth: undefined ,
4449});
4550
4651const container = ref <HTMLSpanElement | SVGTSpanElement | null >(null );
@@ -147,7 +152,7 @@ const replacementTags = new Map([
147152]);
148153
149154function 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
225293onMounted (() => {
0 commit comments