乱序的DOM,正序的文字

492 阅读4分钟

乱序的DOM,正序的文字

故事是这样开始的,今天一早,我正在沸点闲逛,忽见一位同学提了个问题,类似下图

咦,怎么 DOM 里面的文字完全是乱序的,可是页面上的文字却能正常显示呢?

嘻嘻,这个“反复制”操作好像有点意思,出于害死了猫的那个理由,我决定自己尝试实现下。

简单版

俗话说,先生存,再生活。

所以,我首先意思意思搞了个简单的版本,文字只有一行,先试试水吧。

先来定义个容器 container:

<head>
  <style>
    #container {
      position: relative;
      height: 20px;
      border: 1px solid #bdbdbd;
      border-radius: 3px;
      background: ivory;
      color: #708090;
      font-family: 'microsoft yahei';
      user-select: none;
    }
    #container > span {
      position: absolute;
    }
  </style>
</head>
<body>
  <div id="container"></div>
</body>

我的实现思路是这样的:

  1. 把文字拆成单个,给每个文字套上 <span> 标签
  2. 根据文字的位置给 span 标签加上偏移量
  3. 设置文字都相对于其父元素绝对定位
var container = document.querySelector('#container')
var fontSize = 16
var text = '反者道之动'
var textArr = text.split('')

var html = textArr
.map((s, i) => {
  // 根据每个文字的位置下标,设置该文字相对于父元素该向左移多少像素
  var left = fontSize * i
  return `<span style="left: ${ left }px;">${s}</span>`
})
// 打乱文字的顺序
.sort(() => Math.random() - 0.5)
.join('')
// 插入文档中
container.innerHTML = html

哒哒!简单版到此就完成啦。是不是超简单呢?

但仔细look一look,这个 demo 好像太简陋了点,只能处理单行文字,这哪行啊!

所以我决定升级一下它,让它也能支持多行文字。毕竟,解决了温饱问题,就会开始觊觎滋润的小日子了嘛。

多行文字版

其实,要支持多行文字一点也不复杂。

首先,我们需要先计算出 container 一行能放下多少个文字。

这还不简单嘛,只要我们知道 container 的宽度和每个文字所占的宽度,剩下的不就是小学鸡数学吗,难不倒我的。

// 获取 container 的宽度
var width = container.getBoundingClientRect().width
// 设置文字大小
var fontSize = 16
// 算术题:一根香蕉长x,一颗李子直径为y,请问一根香蕉里面能塞下多少个李子呢
var charPerLine = Math.floor(width / fontSize)

解出了第一道小学鸡算术题,下一步,我们来算一算一段文字可以分多少行。

var textTotal = text.length
var lines = Math.ceil(textTotal / charPerLine)

接下来可是重点,我们要算出每个文字相对父元素的位置。

在简单版里,我们只有一行文字,所以只需要文字的 X 轴偏移量。但在多行文字版中,我们需要考虑二维坐标啦。其实,再多看一眼,也还是小学鸡数学嘛。

textArr.map((s, i) => {
  // 先计算当前文字在第几行
  var line = Math.ceil((i + 1) / charPerLine)
  // 再计算文字位于当前行的第几个
  var indent = (i + 1) % charPerLine || charPerLine
  // Y 轴偏移量
  var top = lineHeight * (line - 1)
  // X 轴偏移量
  var left = fontSize * (indent - 1)
})

咦?是算出来了吗?好像是真的呢,Yeah。好啦,接下来的步骤就和简单版的一样啦。

问题1:窗口 resize 的时候,文字不会跟着流动?

这个问题很好解决的啦,只要监听 resize 事件,动态获取 container 的宽度做计算就好啦。

问题2:子元素都绝对定位了,父元素高度不就坍塌了嘛?

那我们给父元素动态设置个高度吧

container.style.height = Math.ceil(text.length / charPerLine) * lineHeight + 'px'

逆向

最后,顺便来玩一下把乱序的 HTML 转换成正序的文字吧。

var htmlText = container.innerHTML.replace(/\s+/g, ' ')
// 获取每个 span 的 top, left 偏移和内容,其实...写到这里...我突然想起了更简单的方法-_-
// 直接操作 DOM 不比操作字符串简单直接得多吗,我和字符串较什么劲呀
var charArr = htmlText.match(/<span.*?<\/span>/gi).map(el => {
  return {
    top: +el.match(/top:\s*(\d+)/i)[1],
    left: +el.match(/left:\s*(\d+)/i)[1],
    char: el.match(/>\s*(.*)\s*<\/span>/)[1],
  }
})

// 根据 top 和 left 偏移量给文字排序
charArr.sort((a, b) => {
  return a.top - b.top || a.left - b.left
})
var originText = charArr.map(el => el.char).join('')
console.log(originText)

操作 DOM 的写法:

var charArr = []
container.querySelectorAll('span').forEach(el => {
  charArr.push({
    top: el.style.top.match(/\d+/)[0],
    left: el.style.left.match(/\d+/)[0],
    char: el.innerText
  })
})

完整代码

最后给个干巴巴的JSBin链接

以上です。