javascript - 如何在修改 DOM 后使自定义文本编辑器使用撤消和重做快捷方式?

标签 javascript events

我正在使用 contenteditable 构建一个小型自定义编辑器div 上的属性。

默认情况下,当用户粘贴 HTML 内容时,HTML 会插入到 contenteditable 中div,我不想要。

为了解决这个问题,我使用了一个像这个问题建议的自定义粘贴处理程序:

Stop pasting html style in a contenteditable div only paste the plain text

editor.addEventListener("paste", (e) => {
  e.preventDefault();

  const text = e.clipboardData.getData('text/plain');

  document.execCommand("insertText", false, text.replaceAll("\n", "<div><br></div>"));
})

到目前为止一切顺利。因为 execCommand 已弃用,我使用了替代方法,基本上是使用范围手动插入文本(去除 HTML)。

execCommand() is now obsolete, what's the alternative?

replace selected text in contenteditable div

editor.addEventListener("paste", (e) => {
  e.preventDefault();

  const text = (e.originalEvent || e).clipboardData.getData("text/plain");

  const selection = window.getSelection();
  selection.deleteFromDocument();

  const range = selection.getRangeAt(0);
  range.insertNode(document.createTextNode(text));
  range.collapse();
});

直到我注意到撤消和重做命令在粘贴某些内容后不再起作用之前,它一直运行良好。这是因为在自定义粘贴事件中完成了 DOM 修改,这会破坏交易。


在寻找解决方案时,我发现了以下问题:

Allowing contenteditable to undo after dom modification

那里的第一个答案建议使用 execCommand ,已弃用,因此这不是一个好的解决方案

第二个答案建议构建自定义撤消重做堆栈来手动处理所有这些事件。所以我选择了第二种解决方案。


我使用一个简单的数组来构建自定义撤消重做处理程序来存储版本。为此,我使用了 beforeinput 监听撤消和重做事件的事件。

editor.addEventListener("beforeinput", (e) => {
  if (e.inputType === "historyUndo") {
    e.preventDefault();
    console.log("undo");
  };

  if (e.inputType === "historyRedo") {
    e.preventDefault();
    console.log("redo");
  };
});

这对于撤消非常有效,但是,由于我阻止了默认设置,浏览器撤消/重做堆栈永远不会处于重做状态,所以 beforeinputcmd + z 时永远不会触发监听器或 ctrl + z在 window 上。

我也尝试过使用 input结果相同的事件。

我已经搜索了其他解决方案来使用 JavaScript 处理撤消/重做事件,但是使用 keydown 构建自定义处理程序在这里没有选择,因为每个操作系统都使用不同的快捷方式,尤其是移动设备!

问题

现在,有多种可能的解决方案可以解决我的问题,因此非常欢迎任何可行的解决方案。

有没有办法手动处理粘贴事件并将粘贴的明文插入文档,同时保留撤消/重做功能或手动实现对此类功能的替换?

例子

尝试粘贴一些东西然后撤消,这是行不通的。

const editor = document.getElementById("editor");

editor.addEventListener("paste", (e) => {
  e.preventDefault();

  const text = (e.originalEvent || e).clipboardData.getData("text/plain").replaceAll("\n", "<div><br></div>");
        
  const selection = window.getSelection();
  selection.deleteFromDocument();

  const range = selection.getRangeAt(0);
  range.insertNode(document.createTextNode(text));
  range.collapse();
});
#editor {
  border: 1px solid black;
  height: 100px;
}
<div id="editor" contenteditable="true"></div>

编辑

修改 ClipboardEvent不起作用,因为它是只读的。调度新事件也不起作用,因为该事件不受信任,因此浏览器不会粘贴内容。

const event = new ClipboardEvent("paste", {
  clipboardData: new DataTransfer(),
});

event.clipboardData.setData("text/plain", "blah");

editor.dispatchEvent(event);

最佳答案

我面临同样的问题并使用 this writeup 中讨论的方法解决了它, 用他的原始代码 here .这是一个非常聪明的技巧。最后,他的方法仍然使用 execCommand,但这是对 execCommand 的一种非常狭窄和隐藏的使用,只是为了将整数索引压入 native 撤消堆栈,以便您的“撤消数据”可以在单独的堆栈上进行跟踪。它还具有能够将正常的撤消操作(如恢复键入的输入)与您推送到单独的撤消堆栈的撤消操作穿插的好处。

我不得不修改他的撤消构造函数以使用“输入”元素而不是 contentEditable“div”。因为我已经在我自己的 contentEditable 'div' 中监听 'input',所以我需要对隐藏的输入 stopImmediatePropagation

constructor(callback, zero=null) {
  this._duringUpdate = false;
  this._stack = [zero];
  
  // Using an input element rather than contentEditable div because parent is already a
  // contentEditable div
  this._ctrl = document.createElement('input');
  this._ctrl.setAttribute('aria-hidden', 'true');
  this._ctrl.setAttribute('id', 'hiddenInput');
  this._ctrl.style.opacity = 0;
  this._ctrl.style.position = 'fixed';
  this._ctrl.style.top = '-1000px';
  this._ctrl.style.pointerEvents = 'none';
  this._ctrl.tabIndex = -1;
  
  this._ctrl.textContent = '0';
  this._ctrl.style.visibility = 'hidden';  // hide element while not used
  
  this._ctrl.addEventListener('focus', (ev) => {
    window.setTimeout(() => void this._ctrl.blur(), 1);
  });
  this._ctrl.addEventListener('input', (ev) => {
    ev.stopImmediatePropagation();  // We don't want this event to be seen by the parent
    if (!this._duringUpdate) {
        callback(this.data);
        this._ctrl.textContent = this._depth - 1;
    } else {
        this._ctrl.textContent = ev.data;
    }
    const s = window.getSelection();
    if (s.containsNode(this._ctrl, true)) {
        s.removeAllRanges();
    }
  });
}

我创建了撤消程序和一种捕获 undoerData 的方法,我正在使用它进行粘贴和其他需要在操作发生时访问该范围的操作。为了将其用于粘贴,我在我自己的 contentEditable div(下面称之为 editor)中监听粘贴事件。

const undoer = new Undoer(_undoOperation, null);

const _undoerData = function(operation, data) {
  const undoData = {
    operation: operation,
    range: _rangeProxy(), // <- Capture document.currentSelection() as a range
    data: data
  }
  return undoData;
};

editor.addEventListener('paste', function(e) {
  e.preventDefault();
  var pastedText = undefined;
  if (e.clipboardData && e.clipboardData.getData) {
      pastedText = e.clipboardData.getData('text/plain');
  }
  const undoerData = _undoerData('pasteText', pastedText);
  undoer.push(undoerData, editor);
  _doOperation(undoerData);
});

然后 _doOperation_undoOperation 开启操作并在 undoerData 中获取信息以采取适当的行动:

const _doOperation = function(undoerData) {
  switch (undoerData.operation) {
    case 'pasteText':
      let pastedText = undoerData.data;
      let range = undoerData.range;
      // Do whatever is needed to paste the pastedText at the location
      // identified in range
      break;
    default:
      // Throw an error or do something reasonable
  };
};

const _undoOperation = function(undoerData) {
    switch (undoerData.operation) {
      case 'pasteText':
        let pastedText = undoerData.data;
        let range = undoerData.range;
        // Do whatever is needed to remove the pastedText at the location
        // identified in range
        break;
      default:
        // Throw an error or do something reasonable
    };
};

通过此方案和实现的 pasteText 操作,您现在可以复制文本、粘贴 - 输入 - 删除文本 - 输入 - 重新定位光标 - 粘贴,然后撤消 6 次,在第 1 次和第 6 次撤消时调用 _undoOperation其他人只是按照他们应该的方式工作。

关于javascript - 如何在修改 DOM 后使自定义文本编辑器使用撤消和重做快捷方式?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/66854679/

相关文章:

javascript - 从没有名称和 ID 值的元素分离 CK 编辑器

javascript - 在 Typescript Vue 项目中使用 Javascript 模块

google-chrome - Fabric.js Canvas 无法在 Google Chrome 上捕捉鼠标事件

jquery - 如何停止事件传播?

javascript - 使多个选择菜单跳转到第一个选项

javascript - 未捕获( promise 中)提供的元素不在文档内

javascript - 使用 jQuery 将多个元素彼此相邻放置后,第一个元素的(额外)填充/边距是多少

java - 为什么 mouselistener 不工作?

events - 脚本 block 的新关闭

c# - 如何确定 Validation.ErrorEvent 中是否不再有错误?