最近上网多了,倒是有趣的事情也变得多了起来,Vue
的作者尤雨溪也整了段花活,给我们 Rap 了一段,其实唱歌本身不是很惹眼的事情,但是本是一位非常厉害的开发人员,同时又有比较"时髦"的说唱爱好,确实是反差感很大,哈哈!
既然尤大都整了,那我们也来整一小段。让你的输入框也 Rap 起来,如何?
看看效果先:
方案
方案是这样子,我们实现跳动的 CSS 非常简单,无非是利用 keyframes
给输入框内部的元素套上动画,但如何控制每一个输入框内部的字符,才是比较棘手的,不过也不算复杂,尝试下以下两种方案:
先准备一个看起来不唠的网页,基本结构明白了,我们就进行接下来的的试验:
<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.value
appendJumpingLetters(e.target.value, newDivEl)
复制代码
可编辑的 div
如果要更方便的实现这个功能,我们需要使用可编辑的 div,这样子的话,不需要创建新的元素就可以直接控制输入框内部的子元素。
我们来看看怎么实现吧!先准备一个可编辑的 div !
<div contenteditable class="input"></div>
复制代码
由于我们可以很方便的控制 div
的样式,所以修改一下上面的样式,让 line-height
和 height
实现的居中效果不要被 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
,同时我们还需要支持两种不同的模式:
<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 星云最爱写代码的,我们下一篇再会!
去探索,不知道的东西还多着呢!
评论