目录
前端模拟终端(一):如果我的这款 IOTerm 不是你想要的
前端模拟终端(二):部分可输入而部分不可修改的多行文本域
前端模拟终端(三):文本显示与自动换行
前端模拟终端(四):显示、输入与光标
前端模拟终端(五):看谁用了 rm -rf / 之历史记录
前端模拟终端(六):快捷输入的好助手、终端的灵魂之补全和提示
文本显示与自动换行
前面都在说 IOTerm 中使用 div
显示文本,但其实一开始选用的是 pre
元素,因为它可以原模原样的显示,而不会少了空格。
pre
默认使用等宽字体,这也正好符合终端的需要。但是 pre
默认是不会自动换行的,文本会超出元素范围,也就是 white-space
为 pre
。可以将 white-space
改成 pre-wrap
,这样就可以既保留空格又能正常换行了。但是因为默认的 world-break
是在单词之间换行,而不会破开整个单词,这就造成在最后一列凹凸不平,不和终端一样。把 world-break
换成 break-all
,就可以在单词中断开了。但是效果依然不佳,在都是英文字母,且字体为等宽字体时,还是会出现最后一列不齐的情况。也就是当本应换行处的字母的后面是标签符号时,最后一个字符会被拉到下一行,即标签符号不会出现在一行开头(除了左括号、引号)。有什么好的办法呢?是的,最初,我选择自己写自动换行。不断地测量以确定插入换行符的位置。
首先需要一个辅助的元素,它的样式要和显示文本的 pre
或 div
一模一样,至少字体、字号等要一样。然后开始不断地尝试与测量吧。最容易想到的当然就是顺序取字符,测量字符串长度。取第一个字符赋值给辅助元素,获取辅助元素的宽度,短了就继续取,长了就插入换行符。取第二个字符与第一个字符组成字符串赋值为辅助元素,获取辅助元素的宽度,进行比较……这样就是一个 O(n)。慢,是肯定的。
所以,我用了二分法。一行的最大宽度是已知的,记为 maxWidth
。设当前测量的字符串为 subText
,其宽度为 subTextWidth
。当使用的是等宽字体时,就英文来说,每个字符的宽度是一样的,而终端中输入的最多的就是英文了。那么可以通过以下方式估算插入换行符的位置:
endIndex = Math.floor(subText.length * maxWidth / subTextWidth);
当字符串中有中文等其他文字时,这种估算也只能获取大概的位置,还需要评估其左右。若其宽度 subTextWidth
小于 maxWidth
,则往其右再取一个字符,比较宽度,若长了,则 endIndex
为精确的换行位置。若 subTextWidth
大于 maxWidth
,则往其左再取一个字符,比较长度,若短了,则 endIndex - 1
为精确的换行位置。
考虑到实际处理的字符串和显示出来的字符串是可能不一样的,比如存在高亮时,HTML 标签是占长度但不显示的,以及 <、>、& 等转义字符,在代码实现中,将确定插入换行符位置和处理 HTML 标签以及转义字符分为两个步骤。具体代码如下:
private getLineFeedsIndices(text: string) {
// Find the indices where to insert line feeds into the string `text`.
//
// This function returns a object containing `indices`, `numRows` and
// 'colOffset'. The `indices` is an array recording the indices where to
// insert line feeds in the string `text`. The `numRows` is the number
// of rows after the `text` is inserted with line feeds <br>.
// The 'colOffset' is the width in pixel of the last line after the
// `text` is inserted with line feeds <br>.
// The `startIndex` is the starting index of the string `subText` in the
// string `text`.
let startIndex = 0;
let maxWidth = this.main.panel.offsetWidth;
let indices: number[] = [];
let subText: string;