写点什么

尤大:不会说 Rap 的前端不是好前端!写一个 v-rap 指令!

作者:泰罗凹凸曼
  • 2023-03-23
    上海
  • 本文字数:3439 字

    阅读完需:约 11 分钟

最近上网多了,倒是有趣的事情也变得多了起来,Vue 的作者尤雨溪也整了段花活,给我们 Rap 了一段,其实唱歌本身不是很惹眼的事情,但是本是一位非常厉害的开发人员,同时又有比较"时髦"的说唱爱好,确实是反差感很大,哈哈!


既然尤大都整了,那我们也来整一小段。让你的输入框也 Rap 起来,如何?


看看效果先:


方案

方案是这样子,我们实现跳动的 CSS 非常简单,无非是利用 keyframes 给输入框内部的元素套上动画,但如何控制每一个输入框内部的字符,才是比较棘手的,不过也不算复杂,尝试下以下两种方案:


  • 生成一个新的 div,替换原本属于 input 的位置,拷贝 input 的字符组成 div 的内部元素

  • 直接使用可编辑的 div,控制其内部元素的输入样式即可


先准备一个看起来不唠的网页,基本结构明白了,我们就进行接下来的的试验:


<input class="input" placeholder="请输入你的 freestyle">
<style> body { display: flex; justify-content: center; align-items: center; background: #7ec699; flex-direction: column; height: 100vh; }
.input { background: #fff; width: 500px; height: 50px; outline: none; border: 5px solid #81d59f; text-align: center; line-height: 50px; border-radius: 25px; font-size: 16px; }</style>
复制代码


鸠占鹊巢

我们在 input 输入完成之后,监听其失焦事件,创建的 div 完全符合 input 的所有样式,再次点击当前 div 的时候,重新替换回来并聚焦,我们把这一招可以称为 "鸠占鹊巢" 式!


如何复制当前元素的所有的样式非常重要,这是我们需要突破的点!


我们可以尝试使用 getComputedStyle 函数来获取 input 的所有样式,并将其提取到 div 之上


尝试写出这个函数:


/** * @param {HTMLInputElement} inputEl */function replaceInput(inputEl) {  const newDivEl = document.createElement('div')  const inputStyles = window.getComputedStyle(inputEl)  for (let styleProp of inputStyles) {    newDivEl.style[styleProp] = inputStyles.getPropertyValue(styleProp);  }  // 不要忘记 div 和盒模型设置  newDivEl.style.boxSizing = 'border-box'    // 将其插入到input之后  inputEl.insertAdjacentElement('afterend', newDivEl)  // 隐藏原有的 input  inputEl.style.display = 'none'    return newDivEl}
复制代码


有了这个函数就很简单啦,我们绑定 blur 事件,并且鸠占鹊巢


const inputEl = document.querySelector('.input')inputEl.addEventListener('blur', (e) => {  if (e.target.value) {    const newDivEl = replaceInput(inputEl)    newDivEl.innerHTML = e.target.value        // 在这里我们需要实现 div 点击后,input 自动聚焦的问题  }})
复制代码


到这里你就会发现一个问题,如果我们直接替换了 input,那么其设置的 line-height 属性会导致 div 的字体靠下,因为 input 同时设置了 border 属性,要解决这个问题,我们必须强制 div 垂直居中,并且不能影响 input 上设置好的 text-align 属性!



我们需要修改刚刚声明的替换方法:


function replaceInput(inputEl) {  ...  // 不要忘记 div 和盒模型设置  newDivEl.style.boxSizing = 'border-box'
// 由于 input 设置了 `line-height` 和 `broder`,所以我们需要垂直居中,采用 FlexBox 的方式来解决 // 同时 input 也设置了 `text-align`,所以我们需要将其转换为 `justify-content` newDivEl.style.display = 'inline-flex' newDivEl.style.alignItems = 'center' if (newDivEl.style.textAlign === 'center') { newDivEl.style.justifyContent = 'center' } else if (newDivEl.style.textAlign === 'right') { newDivEl.style.justifyContent = 'flex-end' }
// 将其插入到input之后 ...}
复制代码


OK,这样我们就相对完美的解决了上述问题,现在我们需要在 div 点击之后,自动切换回 input,并聚焦:


newDivEl.addEventListener('click', () => {  inputEl.style.display = 'inline-block'  inputEl.focus()  newDivEl.parentNode.removeChild(newDivEl)})
复制代码


现在就让我们的文字 Rap 起来吧,添加一个动画子元素:


@keyframes jumping {  25% {    transform: translateY(-10px);  }  75% {    transform: translateY(10px);  }}
复制代码


function appendJumpingLetters(value, parentNode) {  for (let i = 0; i < value.length; i++) {    const spanEl = document.createElement('span')    spanEl.innerText = value[i]    // 动画,给每一个动画加上延迟    const delay = -i * 0.3    spanEl.style.animation = 'jumping 0.8s linear infinite ' + delay + 's'    parentNode.appendChild(spanEl)  }}
// 替换掉刚刚的 newDivEl.innerHTML = e.target.valueappendJumpingLetters(e.target.value, newDivEl)
复制代码


可编辑的 div

如果要更方便的实现这个功能,我们需要使用可编辑的 div,这样子的话,不需要创建新的元素就可以直接控制输入框内部的子元素。


我们来看看怎么实现吧!先准备一个可编辑的 div !


<div contenteditable class="input"></div>
复制代码


由于我们可以很方便的控制 div 的样式,所以修改一下上面的样式,让 line-heightheight 实现的居中效果不要被 border 影响!


.input {  --border-width: 5px;
background: #fff; width: 500px; height: 50px; outline: none; border: var(--border-width) solid #81d59f; border-radius: 25px; font-size: 16px; box-sizing: border-box; text-align: center; line-height: calc(50px - var(--border-width) * 2);}
复制代码


接下来就很简单了,只需要监听失焦事件后,替换其子元素就妥啦,一步到位如何?


const inputEl = document.querySelector('.input')inputEl.addEventListener('blur', (e) => {  const text = inputEl.innerText.trim()  inputEl.innerHTML = ''  if (text) {    // 不加 setTimeout 的时候,div 无法顺利失焦    setTimeout(() => appendJumpingLetters(text, inputEl))  }})
inputEl.addEventListener('focus', (e) => { inputEl.innerHTML = inputEl.innerText.trim()})
复制代码


看看效果吧:


v-rap

既然是尤大提供的灵感,那么我们自然不能忘记 Vue,同时我们还需要支持两种不同的模式:


  • 如果指令用在了 input,就使用第一中方式去执行

  • 如果指令用在了 div, 就使用第二种方式


<div id="app">  <header>v-rap</header>
<input class="input" v-rap> <div contenteditable class="input" v-rap></div></div>
复制代码


OK,定义我们的指令:


<script type="module">  import { createApp } from 'vue'  const app = createApp({})    const vRap = {    mounted(el) {      console.log(el)    }  }  app.directive('rap', vRap)  app.mount('#app')</script>
复制代码


接下来,我们需要判断当前的指令的作用域是在 input 或者 div 上,然后使用相应的函数进行控制就 OK 啦!


function withElType(el) {  const type = el.tagName  switch (type) {    case 'DIV':      const text = el.innerText.trim()      el.innerHTML = ''      if (text) {        setTimeout(() => appendJumpingLetters(text, el))      }      break;    case 'INPUT':      if (el.value) {        const newDivEl = replaceInput(el)        appendJumpingLetters(el.value, newDivEl)
newDivEl.addEventListener('click', () => { el.style.display = 'inline-block' el.focus() newDivEl.parentNode.removeChild(newDivEl) }) } break; }}
const vRap = { mounted(el) { el.addEventListener('blur', () => { withElType(el) })
if (el.tagName === 'DIV') { el.addEventListener('focus', () => { el.innerHTML = el.innerText.trim() }) } }}
复制代码


自己动手点击看看?


jcode

结语

一些有趣的小实验,可以激发我们的学习乐趣与欲望,我一直奉行的一句话是:"兴趣是学习的唯一动力!",希望诸位在前进的路途上一帆风顺!


一道思考题:如果我想在 element-ui 的 el-input 上使用该指令,要怎么办呢?


我是泰罗凹凸曼,M78 星云最爱写代码的,我们下一篇再会!


去探索,不知道的东西还多着呢!


发布于: 15 小时前阅读数: 4
用户头像

我令代码如花般 2018-12-29 加入

https://www.github.com/taroxin

评论

发布
暂无评论
尤大:不会说 Rap 的前端不是好前端!写一个 v-rap 指令!_JavaScript_泰罗凹凸曼_InfoQ写作社区