fix: 🐛 break text with chinese characters(double byte character)

fix text overflow with ellipsis

re #596
This commit is contained in:
bubkoo
2021-02-01 17:04:32 +08:00
parent b1d0dae7ca
commit 14199bc852
2 changed files with 119 additions and 78 deletions

View File

@ -11,9 +11,9 @@ Graph.registerNode(
attrs: {
label: {
textAnchor: 'left',
refX: 20,
refX: 0,
textWrap: {
width: 120,
width: 160,
height: 16,
ellipsis: true,
},
@ -32,7 +32,7 @@ const data = {
y: 40,
attrs: {
label: {
text: 'hello 测试 test 测试 ffg123ASDFGHJ',
text: 'hell o测试tes t测试f fg123ASDFGHJ',
},
},
},

View File

@ -357,43 +357,82 @@ export function text(
elem.appendChild(containerNode)
}
function splitText(text: string, separator?: string) {
if (separator != null) {
const words = text.split(separator)
const seps = words.map((w) => separator)
seps.pop()
return { words, seps }
}
const words: string[] = []
const seps: string[] = []
let word = ''
for (let i = 0, l = text.length; i < l; i += 1) {
const char = text[i]
if (char === ' ') {
words.push(word)
seps.push(' ')
word = ''
} else if (char.match(/[^\x00-\xff]/)) {
if (word.length) {
words.push(word)
seps.push('')
}
words.push(char)
seps.push('')
word = ''
} else {
word += char
}
}
if (word.length) {
words.push(word)
}
return { words, seps }
}
export function breakText(
text: string,
size: Size,
styles: any = {},
options: {
svgDocument?: SVGSVGElement
ellipsis?: string
separator?: string
eol?: string
hyphen?: string
ellipsis?: string
svgDocument?: SVGSVGElement
} = {},
) {
const width = size.width
const height = size.height
const svgDocument = options.svgDocument || createSvgElement('svg')
const textSpan = createSvgElement('tspan') as SVGTSpanElement
const textElement = createSvgElement('text') as SVGTextElement
const telem = createSvgElement('text') as SVGTextElement
const tspan = createSvgElement('tspan') as SVGTSpanElement
const tnode = document.createTextNode('')
attr(textElement, styles)
textElement.appendChild(textSpan)
const textNode = document.createTextNode('')
attr(telem, styles)
telem.appendChild(tspan)
// Prevent flickering
textElement.style.opacity = '0'
telem.style.opacity = '0'
// Prevent FF from throwing an uncaught exception when `getBBox()`
// called on element that is not in the render tree (is not measurable).
// <tspan>.getComputedTextLength() returns always 0 in this case.
// Note that the `textElement` resp. `textSpan` can become hidden
// when it's appended to the DOM and a `display: none` CSS stylesheet
// rule gets applied.
textElement.style.display = 'block'
textSpan.style.display = 'block'
telem.style.display = 'block'
tspan.style.display = 'block'
textSpan.appendChild(textNode)
svgDocument.appendChild(textElement)
tspan.appendChild(tnode)
svgDocument.appendChild(telem)
const shouldAppend = svgDocument.parentNode == null
if (shouldAppend) {
@ -403,31 +442,36 @@ export function breakText(
const eol = options.eol || '\n'
const separator = options.separator || ' '
const hyphen = options.hyphen ? new RegExp(options.hyphen) : /[^\w\d]/
const words = text.split(separator)
const full = []
let lines = []
let p
let h
let partIndex
let hyphenIndex
let lineHeight
for (let i = 0, lineIndex = 0, len = words.length; i < len; i += 1) {
const word = words[i]
const { words, seps } = splitText(text, options.separator)
for (
let wordIndex = 0, lineIndex = 0, wordCount = words.length;
wordIndex < wordCount;
wordIndex += 1
) {
const word = words[wordIndex]
if (!word) {
continue
}
// word contains end-of-line character
if (eol && word.indexOf(eol) >= 0) {
// word contains end-of-line character
if (word.length > 1) {
// separate word and continue cycle
const eolWords = word.split(eol)
for (let j = 0, jl = eolWords.length - 1; j < jl; j += 1) {
eolWords.splice(2 * j + 1, 0, eol)
const parts = word.split(eol)
for (let j = 0, m = parts.length - 1; j < m; j += 1) {
parts.splice(2 * j + 1, 0, eol)
}
words.splice(i, 1, ...eolWords.filter((word) => word !== ''))
i -= 1
len = words.length
words.splice(wordIndex, 1, ...parts.filter((word) => word !== ''))
wordIndex -= 1
wordCount = words.length
} else {
// creates a new line
lineIndex += 1
@ -436,34 +480,36 @@ export function breakText(
continue
}
textNode.data = lines[lineIndex] ? `${lines[lineIndex]} ${word}` : word
if (lines[lineIndex] != null) {
tnode.data = `${lines[lineIndex]}${seps.pop() || ''}${word}`
} else {
tnode.data = word
}
if (textSpan.getComputedTextLength() <= width) {
// the current line fits
lines[lineIndex] = textNode.data
if (tspan.getComputedTextLength() <= width) {
lines[lineIndex] = tnode.data
if (p || h) {
// We were partitioning. Put rest of the word onto next line
// We were partitioning. Put rest of the word onto next line
if (partIndex || hyphenIndex) {
full[lineIndex] = true
lineIndex += 1
// cancel partitioning and splitting by hyphens
p = 0
h = 0
partIndex = 0
hyphenIndex = 0
}
} else {
if (!lines[lineIndex] || p) {
const partition = !!p
if (!lines[lineIndex] || partIndex) {
const partition = !!partIndex
p = word.length - 1
partIndex = word.length - 1
if (partition || !p) {
if (partition || !partIndex) {
// word has only one character.
if (!p) {
if (!partIndex) {
if (!lines[lineIndex]) {
// we won't fit this text within our rect
lines = []
break
}
@ -471,63 +517,58 @@ export function breakText(
// try again, but this time start with a new line
// cancel partitions created
words.splice(i, 2, word + words[i + 1])
words.splice(wordIndex, 2, word + words[wordIndex + 1])
// adjust word length
len -= 1
wordCount -= 1
full[lineIndex] = true
lineIndex += 1
i -= 1
wordIndex -= 1
continue
}
// move last letter to the beginning of the next word
words[i] = word.substring(0, p)
words[i + 1] = word.substring(p) + words[i + 1]
words[wordIndex] = word.substring(0, partIndex)
words[wordIndex + 1] =
word.substring(partIndex) + words[wordIndex + 1]
} else {
if (h) {
if (hyphenIndex) {
// cancel splitting and put the words together again
words.splice(i, 2, words[i] + words[i + 1])
h = 0
words.splice(wordIndex, 2, words[wordIndex] + words[wordIndex + 1])
hyphenIndex = 0
} else {
const hyphenIndex = word.search(hyphen)
if (
hyphenIndex > -1 &&
hyphenIndex !== word.length - 1 &&
hyphenIndex !== 0
) {
h = hyphenIndex + 1
p = 0
const hyphenIdx = word.search(hyphen)
if (hyphenIdx > 0 && hyphenIdx < word.length - 1) {
hyphenIndex = hyphenIdx + 1
partIndex = 0
}
// We initiate partitioning or splitting
// split the long word into two words
// partitioning or splitting the long word into two words
words.splice(
i,
wordIndex,
1,
word.substring(0, h || p),
word.substring(h || p),
word.substring(0, hyphenIndex || partIndex),
word.substring(hyphenIndex || partIndex),
)
// adjust words length
len += 1
wordCount += 1
}
// if the previous line is not full, try to fit max part of
// the current word there
if (lineIndex && !full[lineIndex - 1]) {
// if the previous line is not full, try to fit max part of
// the current word there
lineIndex -= 1
}
}
i -= 1
wordIndex -= 1
continue
}
lineIndex += 1
i -= 1
wordIndex -= 1
}
// if size.height is defined we have to check whether the height of the entire
@ -550,7 +591,7 @@ export function breakText(
lineHeight = heightValue.value
if (heightValue.unit === 'em') {
lineHeight *= textElement.getBBox().height
lineHeight *= telem.getBBox().height
}
}
@ -574,13 +615,13 @@ export function breakText(
break
}
let k = lastLine.length
let lastIndex = lastLine.length
let lastLineWithOmission
let lastChar
let separatorChar
do {
lastChar = lastLine[k]
lastLineWithOmission = lastLine.substring(0, k)
lastChar = lastLine[lastIndex]
lastLineWithOmission = lastLine.substring(0, lastIndex)
if (!lastChar) {
separatorChar = typeof separator === 'string' ? separator : ' '
lastLineWithOmission += separatorChar
@ -588,13 +629,13 @@ export function breakText(
lastLineWithOmission += lastChar
}
lastLineWithOmission += ellipsis
textNode.data = lastLineWithOmission
if (textSpan.getComputedTextLength() <= width) {
tnode.data = lastLineWithOmission
if (tspan.getComputedTextLength() <= width) {
lines[lastL] = lastLineWithOmission
break
}
k -= 1
} while (k >= 0)
lastIndex -= 1
} while (lastIndex >= 0)
break
}
@ -604,7 +645,7 @@ export function breakText(
if (shouldAppend) {
remove(svgDocument)
} else {
remove(textElement)
remove(telem)
}
return lines.join(eol)