Input Replacer

批量替换输入框中文字。可实时预览替换效果,在预览中可以选择某些项不进行替换。

// ==UserScript==
// @name        Input Replacer
// @namespace   Input Replacer
// @match       *://*/*
// @icon        https://i.v2ex.co/8t6RUhEhs.png
// @grant       GM_addStyle
// @grant       GM_registerMenuCommand
// @grant       GM_openInTab
// @inject-into 
// @noframes
// @version     2.1.1
// @author      稻米鼠
// @created     2020/9/19 下午1:12:20
// @update      2022-02-15 15:58:59
// @description 批量替换输入框中文字。可实时预览替换效果,在预览中可以选择某些项不进行替换。
// ==/UserScript==

// 注入面板样式
GM_addStyle(`
#input-replacer-userscript-panel {
  position: fixed;
  top: -10px;
  left: -10px;
  box-sizing: border-box;
  width: calc(100vw + 20px);
  height: calc(100vh + 20px);
  padding: 30px 0;
  font-size: 18px;
  overflow-y: scroll;
}
#input-replacer-userscript-panel * {
  box-sizing: border-box;
  font-size: 18px;
  line-height: 1.6em;
}
#input-replacer-userscript-panel.input-replacer-mask {
  background: rgba(0, 0, 0, .6);
}
#input-replacer-userscript-panel.input-replacer-show {
  display: block;
}
#input-replacer-userscript-panel.input-replacer-hide {
  display: none;
}
#input-replacer-userscript-panel > .input-replacer-panel {
  position: relative;
  width: 92%;
  max-width: 800px;
  padding: 30px;
  margin: 0 auto;
  border-radius: 6px;
  box-shadow: 0 12px 36px rgba(0, 0, 0, .6);
  background: #FFF;
}
#input-replacer-userscript-panel > .input-replacer-panel > .input-replacer-panel-close {
  position: absolute;
  top: 10px;
  right: 10px;
  color: #666;
  text-align: right;
  cursor: pointer;
}
#input-replacer-userscript-panel > .input-replacer-panel > .input-replacer-panel-header {

}
#input-replacer-userscript-panel > .input-replacer-panel > .input-replacer-panel-header > h2 {
  text-align: center;
  font-size: 36px;
  color: black;
}
#input-replacer-userscript-panel > .input-replacer-panel > .input-replacer-panel-header > p {
  text-align: center;
  color: #666;
}
#input-replacer-userscript-panel > .input-replacer-panel > .input-replacer-input-group > label,
#input-replacer-userscript-panel > .input-replacer-panel > .input-replacer-input-group > input {
  display: block;
}
#input-replacer-userscript-panel > .input-replacer-panel > .input-replacer-input-group > input {
  color: #666;
  width: 100%;
  padding: 8px;
  margin-bottom: 10px;
}
#input-replacer-userscript-panel > .input-replacer-panel > div > button {
  display: block;
  width: 100%;
  color: white;
  background: #04ABFF;
  width: 100%;
  padding: 8px;
  margin-bottom: 10px;
  text-align: center;
  border: none;
  border-raduis: 3px;
}
#input-replacer-userscript-panel > .input-replacer-panel #input-replacer-preview-header {
  text-align: center;
  font-size: 24px;
  color: black;
}
#input-replacer-userscript-panel > .input-replacer-panel .input-replacer-preview-table-row {
  width: 100%;
  float: left;
}
#input-replacer-userscript-panel > .input-replacer-panel .input-replacer-preview-table-row.input-replacer-lock {
  background: rgba(255, 0, 0, .1);
}
#input-replacer-userscript-panel > .input-replacer-panel #input-replacer-preview::after {
  content: ' ';
  display: block;
  clear: both;
}
#input-replacer-userscript-panel > .input-replacer-panel .input-replacer-preview-table-row {
  border-top: 2px solid #CCC;
}
#input-replacer-userscript-panel > .input-replacer-panel .input-replacer-preview-table-row > .input-replacer-preview-table-td {
  float: left;
  width: 45%;
  white-space: pre-wrap;
  overflow-wrap: break-word;
  word-break: break-all;
  padding: 5px;
}
#input-replacer-userscript-panel > .input-replacer-panel .input-replacer-preview-table-row > .input-replacer-preview-table-td:first-child {
  width: 10%;
  cursor: pointer;
}
#input-replacer-userscript-panel > .input-replacer-panel .input-replacer-preview-table-row > .input-replacer-preview-table-td > .input-replacer-highlight {
  display: inline;
  background: rgba(255, 255, 0, .6)
}
`)
let panelObj  // 面板对象
const inputSelectors = 'input[type=text], input[type=email], input[type=number], input[type=url], textarea' // 文本框选择器
let lockAll = false  // 当前是否处于全选状态(不会很准确,简单实现批量选择)
let replacer  // 替换函数
const getInputs = ()=>{
  const allInputs = Array.from(document.body.querySelectorAll(inputSelectors))
  // 如果使用下面这种写法则暴力的尝试所有非隐藏表单项
  // const allInputs = Array.from(document.body.querySelectorAll('input, textarea')).filter(e=>e.type!=='hidden')
  return allInputs
}
let inputs = getInputs() // 文本框合集(每一次使用前会重新统计。虽然资源开销大点,但可以尽可能保证不出问题)
/**
 * 获取页面中最大的 z-index,并加 1
 *
 * @return {*} 最大 z-index
 */
const maxZIndex = ()=>{
  let zIndex = 0
  document.body.querySelectorAll('*').forEach(el=>{
    if(!isNaN(window.getComputedStyle(el).zIndex)){
      const newZ = +window.getComputedStyle(el).zIndex
      zIndex = zIndex > newZ ? zIndex : newZ
    }
  })
  return zIndex+1
}
/**
 * 创建一个新元素
 *
 * @param {string} tag 新元素的标签
 * @param {element} parentNode 新元素的父节点
 * @param {object} options 新元素的属性
 * @param {array} classNames 新元素的 class 
 * @param {object} events 元素绑定的事件
 * @return {*} 新元素
 */
const createElement = (tag, parentNode, options, classNames, events)=>{
  const el = document.createElement(tag)
  for(const property in options){
    el[property] = options[property]
  }
  if(classNames){
    for(const cla of classNames){
      el.classList.add(cla)
    }
  }
  if(events){
    for(const eventName in events){
      el.addEventListener(eventName, events[eventName])
    }
  }
  parentNode.appendChild(el)
  return el
}
/**
 * 生成预览区的行(无参数则为表头)
 *
 * @param {*} refreshPreview 预览区域刷新函数
 * @param {*} index 文本框合集中的索引
 * @param {*} replacer 文本替换函数
 */
const preRow = (refreshPreview, index, replacer)=>{
  const isHead = isNaN(index)
  const input = inputs[index]
  if(!isHead && input.value.length === 0) return
  const isLock = isHead ? lockAll : input.classList.contains('input-replacer-lock')
  const rowClass = ['input-replacer-preview-table-row']
  if(isLock) rowClass.push('input-replacer-lock')
  const row = createElement(
    'div',
    panelObj.previwArea,
    {},
    rowClass
  );
  createElement(
    'div',
    row,
    { innerText: isLock ? '❌' : '⭕'},
    ['input-replacer-preview-table-td'],
    { click: ()=>{
      if(isHead){
        inputs.forEach(el=>{
          if(el.value.length){
            if(lockAll){
              el.classList.remove('input-replacer-lock')
            }else{
              el.classList.add('input-replacer-lock')
            }
          }
        })
        lockAll = !lockAll
      }else{
        if(isLock){
          input.classList.remove('input-replacer-lock')
        }else{
          input.classList.add('input-replacer-lock')
        }
      }
      refreshPreview()
    } }
  );
  createElement(
    'div',
    row,
    { innerHTML: isHead ? '当前内容' : replacer( inputs[index].value, 'now' )},
    ['input-replacer-preview-table-td']
  );
  createElement(
    'div',
    row,
    { innerHTML: isHead ? '替换结果' : ( isLock ? inputs[index].value : replacer( inputs[index].value, 'preview') )},
    ['input-replacer-preview-table-td']
  );
}
/**
 * 文本替换函数的生成器
 *
 * @param {*} in0 第一个输入框的内容(要搜索的内容)
 * @param {*} in1 第二个输入框的内容(要替换为的内容)
 * @return {*} 文本替换函数
 */
const replacerMaker = (in0, in1)=>{
  if(/^\/.*\/[a-z]*$/.test(in0)){ // 判断是否正则
    const regContent = in0.replace(/^\//, '').replace(/\/[a-z]*/, '').replace(/\\/g, '\\')
    const regFlag = in0.replace(/^\/.*\/([a-z]*)$/, '$1')
    const reg = new RegExp(regContent, regFlag)
    return (text, type)=>{
      const newStr = type ? (m)=>'<span class="input-replacer-highlight">'+(type==='now' ? m : in1)+'</span>' : in1
      return text.replace(reg, newStr)
    }
  }else{
    return (text, type)=>{
      const newStr = type ? (m)=>'<span class="input-replacer-highlight">'+(type==='now' ? m : in1)+'</span>' : in1
      return text.replaceAll(in0, newStr)
    }
  }
}
/**
 * 刷新预览区域
 *
 */
const refreshPreview = ()=>{
  panelObj.previwArea.innerHTML = ''
  const input0 = panelObj.inputGroup[0].value
  const input1 = panelObj.inputGroup[1].value
  inputs = getInputs()
  replacer = replacerMaker(input0, input1)
  if(inputs.length && panelObj){
    createElement(
      'div',
      panelObj.previwArea,
      { innerText: '效果预览', id: 'input-replacer-preview-header' }
    );
    // 插入表头
    preRow(refreshPreview)
    for(let i=0; i<inputs.length; i++){ preRow(refreshPreview, i ,replacer) }
  }
}
const replaceAllInput = e=>{
  const changeEvent = new CustomEvent('change', {})
  inputs.forEach(el=>{
    if(!el.classList.contains('input-replacer-lock')){
      const resultVal = replacer( el.value )
      el.value = resultVal
      try {
        // 触发 change 事件
        el.dispatchEvent(changeEvent)
        // 对 CKeditor 的支持
        if(unsafeWindow.CKEDITOR && el.tagName === 'TEXTAREA'){
          unsafeWindow.CKEDITOR.instances[el.id].setData(resultVal)
        }
      } catch (error) {
          
      }
    }
  })
  panelObj.container.classList.add('input-replacer-hide')
}
const addPanel = ()=> {
  // 创建容器
  const container = createElement(
    'div',
    document.body,
    {
      id: 'input-replacer-userscript-panel',
      style: 'z-index: ' + maxZIndex() + ';'
    },
    ['input-replacer-mask']
  );
  // 添加面板
  const panel = createElement(
    'div',
    container,
    {},
    ['input-replacer-panel']
  );
  // 注入关闭按钮
  const closeButton = createElement(
    'div',
    panel,
    { innerText: 'Close' },
    ['input-replacer-panel-close'],
    {
      click: ()=>{
        if(confirm('确认关闭?')){
          container.classList.add('input-replacer-hide')
        }
      }
    }
  );
  // 注入标题和描述
  const panelHeader = createElement(
    'div',
    panel,
    {
      innerHTML: `
        <h2>输入框查找替换</h2>
        <p>对页面中所有输入框和文本域的内容进行批量替换。</p>
        `
    },
    ['input-replacer-panel-header']
  );
  // 设置输入框组
  const inputNameGroup = ['要查找的内容(支持正则表达式)', '要替换为的内容(支持 $1、$2……替代分组)']
  const inputGroup = []
  for(const name of inputNameGroup){
    const inputArea = createElement(
      'div',
      panel,
      {},
      ['input-replacer-input-group']
    );
    const label = createElement(
      'label',
      inputArea,
      { for: "input-replacer-input-group-"+ inputNameGroup.indexOf(name), innerText: name },
      []
    );
    const input = createElement(
      'input',
      inputArea,
      { id: "input-replacer-input-group-"+ inputNameGroup.indexOf(name), type: 'search' },
      [],
      { keyup: refreshPreview }
    );
    inputGroup.push(input)
  }
  // 注入替换按钮
  const buttonArea = createElement(
    'div',
    panel,
    {},
    ['input-replacer-button-area']
  );
  const mainButton = createElement(
    'button',
    buttonArea,
    { innerText: '替换全部' },
    ['input-replacer-button-area'],
    { click: replaceAllInput }
  );
  // 注入预览区
  const previwArea = createElement(
    'div',
    panel,
    { id: 'input-replacer-preview' },
    []
  );

  return {
    container,
    panel,
    closeButton,
    panelHeader,
    inputGroup,
    buttonArea,
    mainButton,
    previwArea
  }
}
GM_registerMenuCommand('1、输入框批量替换', ()=>{
  if(panelObj){
    panelObj.container.classList.remove('input-replacer-hide')
  }else{
    panelObj = addPanel()
  }
})
GM_registerMenuCommand('2、更多脚本', ()=>{
  GM_openInTab('https://script.izyx.xyz/?from=input-replacer')
})