美文网首页
原生js实现HTML输入框撤销、重做功能

原生js实现HTML输入框撤销、重做功能

作者: 猪和白菜的关系 | 来源:发表于2020-11-09 15:57 被阅读0次

    HTML代码:

    <!DOCTYPE html>
    <html lang="en">
    
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>Document</title>
    </head>
    
    <body>
      <textarea id="textarea" cols="40" rows="10" placeholder="请输入内容"></textarea>
      <br>
      <button id="previous">上一步(撤销)</button>
      <button id="next">下一步(重做)</button>
    
      <script src="./textarea.js"></script>
    </body>
    
    </html>
    

    JS代码:

    /**
     * 记录输入框的操作记录,可以进行上一步(撤销)或下一步(重做)操作
     * 
     */
    const inputEl = document.querySelectorAll('#textarea')[0]
    const previousEl = document.querySelectorAll('#previous')[0]
    const nextEl = document.querySelectorAll('#next')[0]
    // 操作数组
    const operateList = []
    // 按键按下时记录的输入框的信息
    let beforeInputInfo = null
    // 当前操作的下标,范围:[-1, operateList.length - 1]。-1代表一开始的空状态
    let operateIndex = -1
    // 最大操作数,超过时,最前面的操作会被丢弃
    const MAX_OPERATE_NUMBER = 200
    
    let isComposing = false
    
    inputEl.oninput = function(e) {
      console.log('oninput', this.value, this.value && this.value.length, e.isComposing)
      if(isComposing) {
        return
      }
    
      recordOperate(e)
    }
    
    // 按键按下时保存输入框信息
    inputEl.onmousedown = function(e) {
      beforeInputInfo = getCurrentOperateInfo(e)
    }
    
    // 按键按下时保存输入框信息
    inputEl.onkeydown = function(e) {
      beforeInputInfo = getCurrentOperateInfo(e)
    }
    
    // compositionstart和compositionend通过onxxx的方式无法绑定事件
    inputEl.addEventListener('compositionstart',function(e) {
      console.log('oncompositionstart')
      isComposing = true
    })
    
    inputEl.addEventListener('compositionend',function(e) {
      console.log('oncompositionend')
      recordOperate(e)
      isComposing = false
    })
    
    // 上一步(撤销)
    previousEl.onclick = function() {
      recoveryOperate(true)
    }
    
    // 下一步(重做)
    nextEl.onclick = function() {
      recoveryOperate(false)
    }
    
    /**
     * 记录操作
     * @param {Event} e 事件
     */
    function recordOperate(e) {
      // 操作下标不是最后的下标,证明之前有进行撤销或重做操作,那么重新输入后需要删除多余的操作
      if(operateIndex !== operateList.length - 1) {
        // 操作下标不是最后的操作,则要删除操作下标后面的所有操作
        operateList.splice(operateIndex + 1, operateList.length - 1 - operateIndex)
      }
    
      if(operateList.length) {
        // 之前有操作
        // 判断和前面的操作是否是连续的输入或删除,是的话则算一个操作,更新信息就好
        const previousOperate = operateList[operateList.length - 1]
        
        // e.data 返回当前输入的字符串
        if(
          previousOperate.direction === inputEl.selectionDirection &&
          previousOperate.start === previousOperate.end &&
          inputEl.selectionStart === inputEl.selectionEnd &&
          (
            (// 连续输入,无论是否是拼写状态
              (// 当前的输入类型
                (
                  e.type === 'input' &&
                  e.inputType === 'insertText'
                ) ||
                (
                  e.type === 'compositionend'
                )
              ) &&
              // 之前的输入类型
              ['insertText', 'compositionend'].includes(previousOperate.inputType) &&
              // 光标位置
              inputEl.selectionStart === previousOperate.start + (e.data ? e.data.length : 0)
            ) ||
            (// 连续删除,包括back删除和del删除
              // 当前的输入类型
              e.type === 'input' &&
              ['deleteContentBackward', 'deleteContentForward'].includes(e.inputType) &&
              // 之前的输入类型
              ['deleteContentBackward', 'deleteContentForward'].includes(previousOperate.inputType) &&
              // 光标位置
              inputEl.selectionStart === previousOperate.start - (e.inputType === 'deleteContentBackward' ? 1 : 0)
            )
          )
        ) {
          updateOperateInfo(previousOperate)
        } else {
          addOperate(e)
        }
      } else {
        // 之前没有操作
        addOperate(e)
      }
    
      beforeInputInfo = null
    }
    
    /**
     * 新增一个操作
     * @param {Event} e 事件
     * @param {Object} operate 操作对象
     */
    function addOperate(e, operate = null) {
      const resultOperate = operate ? operate : getCurrentOperateInfo(e)
      
      // 旧光标方向
      resultOperate.oldDirection = beforeInputInfo ? beforeInputInfo.direction : resultOperate.direction
      // 旧光标开始位置
      resultOperate.oldStart = beforeInputInfo ? beforeInputInfo.start : resultOperate.start
      // 旧光标结束位置
      resultOperate.oldEnd = beforeInputInfo ? beforeInputInfo.end : resultOperate.end
    
      if(operateList.length > MAX_OPERATE_NUMBER) {
        // 超过最大操作数,最前面的操作会被丢弃
        operateList.shift()
      }
      operateList.push(resultOperate)
      // 更新操作下标
      operateIndex = operateList.length - 1
    }
    
    /**
     * 获取当前操作的信息
     * @param {Event} e 事件
     * @returns {Object} 操作对象信息,包括光标位置、内容、事件的输入类型
     */
    function getCurrentOperateInfo(e) {
      return {
        start: inputEl.selectionStart, // 光标开始位置
        end: inputEl.selectionEnd, // 光标结束位置
        direction: inputEl.selectionDirection, // 光标方向
        value: inputEl.value, // 输入框的内容
        inputType: e.inputType ? e.inputType : e.type // 输入类型,只有input事件有inputType
      }
    }
    
    /**
     * 更新操作信息
     * @param {Object} operate 更新的操作对象
     */
    function updateOperateInfo(operate) {
      operate.start = inputEl.selectionStart, // 光标开始位置
      operate.end = inputEl.selectionEnd, // 光标结束位置
      operate.direction = inputEl.selectionDirection, // 光标方向
      operate.value = inputEl.value // 输入框的内容
    }
    
    /**
     * 恢复操作
     * @param {boolean} isPrevious 是否是撤销
     */
    function recoveryOperate(isPrevious) {
      if(isPrevious) {
        // 撤销
        if(operateIndex < 0) {
          alert('没有操作可以撤销!')
          return
        } else {
          const currentOperate = operateList[operateIndex]
          let previousOperate = null
    
          if(operateIndex > 0) {
            previousOperate = operateList[operateIndex - 1]
          }
    
          // 先回复内容,再恢复光标位置
          inputEl.focus()
          inputEl.value = previousOperate ? previousOperate.value : ''
          inputEl.setSelectionRange(currentOperate.oldStart, currentOperate.oldEnd, currentOperate.oldDirection);
          // 更新操作下标
          if(operateIndex > -1) {
            --operateIndex
          }
        }
      } else {
        // 重做
        if(operateIndex >= operateList.length - 1) {
          alert('没有操作可以重做!')
          return
        } else {
          let nextOperate = null
    
          if(operateIndex < operateList.length - 1) {
            nextOperate = operateList[operateIndex + 1]
          }
    
          // 先回复内容,再恢复光标位置
          inputEl.focus()
          inputEl.value = nextOperate ? nextOperate.value : ''
          inputEl.setSelectionRange(nextOperate.start, nextOperate.end, nextOperate.direction);
          // 更新操作下标
          if(operateIndex < operateList.length - 1) {
            ++operateIndex
          }
        }
      }
    }
    

    相关文章

      网友评论

          本文标题:原生js实现HTML输入框撤销、重做功能

          本文链接:https://www.haomeiwen.com/subject/ionlbktx.html