如何将剪贴板事件限制在只有在没有其他影响时才触发?

huangapple go评论70阅读模式
英文:

How to limit clipboard events to fire only when it would have no other effect?

问题

我有一个React应用程序,我正在尝试添加剪贴板功能。应用程序的很多部分都是由用户选择和重新排列的div组成。有时它们会有文本区域和输入框供用户输入,或者文本可以选择和复制。我有两个事件监听器:

  // 复制
  document.addEventListener('copy', (event) => {
    if (!selectedItem) {
      return; // 没有选择任何项目
    }

    event.clipboardData?.setData('text/plain', selectedItem.id);
    event.clipboardData?.setData(
      'application/x-my-app-item-json',
      JSON.stringify(selectedItem),
    );

    event.preventDefault();
  });
  // 粘贴
  document.addEventListener('paste', (event) => {
    const itemJson = event.clipboardData?.getData(
      'application/x-my-app-item-json'
    );
    if (!itemJson) {
      return; // 剪贴板上没有内容
    }

    let item;
    try {
      item = JSON.parse(itemJson);
    } catch {
      return; // 格式不正确的json,忽略
    }

    insertItem(item);
    event.preventDefault();
  });

我对输入和文本的期望是剪贴板应该表现出您所期望的行为。您可以复制和粘贴到/从输入框中。您可以复制所选文本。但是,如果没有发生任何情况,它应该回退到作为document.addEventListener('copy/paste')的一部分设置的复制粘贴事件,这些事件复制和粘贴与当前页面的状态和布局相关的数据。

我目前遇到的问题是,无论选择了什么,都会覆盖剪贴板事件,所以如果我选择文本并尝试复制,实际上复制的是页面数据。同样,如果我尝试粘贴到文本框中,它将粘贴复制数据的文本版本,但也会触发页面事件。

到目前为止,我设法想出的最好的解决方法是防止事件目标是body

if (event.target !== document.getElementsByTagName('body').item(0)) {
  // 早期中断,因为我们选择了其他内容
  return;
}

然而,这并不总是起作用,因为有时目标将是不相关的内容(例如,最近聚焦的按钮,或在粘贴操作期间选择的文本),不应中断事件处理程序。

总结一下问题。是否有一种方法可以限制剪贴板事件监听器,以便当选择了应该触发复制/粘贴事件的其他内容时,它不会运行,但当事件不会导致现有行为时,它将始终运行?

英文:

I've got a react app I'm trying to add clipboard functionality to. A lot of the app is made of divs that the user selects and rearranges. Occasionally they will have text areas and inputs to enter things into, or text they could select and copy. I've got my two event listeners:

  // Copy
  document.addEventListener('copy', (event) => {
    if (!selectedItem) {
      return; // No item has been selected
    }

    event.clipboardData?.setData('text/plain', selectedItem.id);
    event.clipboardData?.setData(
      'application/x-my-app-item-json',
      JSON.stringify(selectedItem),
    );

    event.preventDefault();
  });
  // Paste
  document.addEventListener('paste', (event) => {
    const itemJson = event.clipboardData?.getData(
      'application/x-my-app-item-json'
    );
    if (!itemJson) {
      return; // Nothing on the clipboard
    }

    let item;
    try {
      item = JSON.parse(itemJson);
    } catch {
      return; // malformed or incorrect json, ignore
    }

    insertItem(item);
    event.preventDefault();
  });

My expectation for inputs and text is that clipboard would behave as you'd expect. You can copy and paste from/into inputs. You can copy selected text. However if none of that occurs, it should fallback to the copy paste events set as part of document.addEventListener('copy/paste'), which copies and pastes data relating to the state and layout of the current page.

The problem I'm having right now is that regardless of what's selected, I'll override the clipboard events with my listener. So if I select text and try and copy, it's the page data that is copied instead. Similarly, if I am trying to paste into a text box, it will paste the text version of the copied data, but it will also trigger the page event.

The best workaround I've managed to come up with so far is to guard whether the target is the body:

if (event.target !== document.getElementsByTagName('body').item(0)) {
  // Early break as we've selected something else
  return;
}

However that doesn't always work, as sometimes the target will be something unrelated, (e.g. a recently focused button, or selected text during a paste action), that shouldn't interrupt the event handler.

To summarise the question. Is there a way to limit the clipboard event listener, so that it doesn't run when something else that should trigger a copy/paste event is selected, but will always run when the event would result in no existing behaviour?

答案1

得分: 0

我找到的最佳解决方案(虽然可能不完美)是依赖于 window.getSelection()document.activeElement 两者。

我们可以测试是否已选择任何文本:

const isPageTextSelected = () => {
  const selection = window.getSelection();
  return selection && !selection.isCollapsed;
};

我们还可以测试当前的 activeElement 是否是 <Input /><TextArea />,这两者都应该保留默认行为:

const isInputActive = () => {
  const { activeElement } = document;
  return activeElement instanceof HTMLInputElement
    || activeElement instanceof HTMLTextAreaElement
    || activeElement?.isContentEditable;
};

(注意,我包括 isContentEditable,因为我们有一些依赖于带有 contenteditable 属性的 div 标签的自定义文本字段,如果只查找输入和文本区域元素,这些字段将被忽略)

然后,在复制和粘贴期间只需检查这些内容,并在不阻止默认行为的情况下提前退出:

  // 复制
  document.addEventListener('copy', (event) => {
    if (!selectedItem) {
      return; // 没有选定任何项目
    }
    // === 在这里进行更改 ===
    if (isPageTextSelected() || isInputActive()) {
      return; // 我们要复制的内容不是选定的项目
    }

    event.clipboardData?.setData('text/plain', selectedItem.id);
    event.clipboardData?.setData(
      'application/x-my-app-item-json',
      JSON.stringify(selectedItem),
    );

    event.preventDefault();
  });
  // 粘贴
  document.addEventListener('paste', (event) => {
    // === 在这里进行更改 ===
    if (isInputActive()) {
      return; // 我们要粘贴到特定输入框中
    }

    const itemJson = event.clipboardData?.getData(
      'application/x-my-app-item-json'
    );
    if (!itemJson) {
      return; // 剪贴板上没有任何内容
    }

    let item;
    try {
      item = JSON.parse(itemJson);
    } catch {
      return; // 格式错误或不正确的 JSON,忽略
    }

    insertItem(item);
    event.preventDefault();
  });

这当然假定我们没有其他类型的项目需要复制或粘贴,除了文本输入之外。它实际上并不检查是否存在默认的复制/粘贴事件,只是检查可能具有默认复制/粘贴行为的特定元素。不过,就目前而言,它似乎适用于我的用例。

英文:

The best solution I've found, (though probably not perfect) is to rely on both window.getSelection() and document.activeElement.

We can test to see if any text has been selected with:

const isPageTextSelected = () => {
  const selection = window.getSelection();
  return selection && !selection.isCollapsed;
};

We can also test to check if the activeElement right now is an <Input /> or <TextArea /> which we'd want the default behaviour for as well:

const isInputActive = () => {
  const { activeElement } = document;
  return activeElement instanceof HTMLInputElement
    || activeElement instanceof HTMLTextAreaElement
    || activeElement?.isContentEditable;
};

(Note, I include isContentEditable because we have some custom text fields which rely on div tags with contenteditable, which would be missed if just looking for inputs and text area elements)

Then it's just a matter of checking these during copy and paste, and exiting early without preventing the default behaviour:

  // Copy
  document.addEventListener('copy', (event) => {
    if (!selectedItem) {
      return; // No item has been selected
    }
    // === CHANGE HERE ===
    if (isPageTextSelected() || isInputActive()) {
      return; // We want to copy something other than the selected item
    }

    event.clipboardData?.setData('text/plain', selectedItem.id);
    event.clipboardData?.setData(
      'application/x-my-app-item-json',
      JSON.stringify(selectedItem),
    );

    event.preventDefault();
  });
  // Paste
  document.addEventListener('paste', (event) => {
    // === CHANGE HERE ===
    if (isInputActive()) {
      return; // We want to paste into a specific input
    }

    const itemJson = event.clipboardData?.getData(
      'application/x-my-app-item-json'
    );
    if (!itemJson) {
      return; // Nothing on the clipboard
    }

    let item;
    try {
      item = JSON.parse(itemJson);
    } catch {
      return; // malformed or incorrect json, ignore
    }

    insertItem(item);
    event.preventDefault();
  });

This of course assumes that we have no other types of items we want to be copying or pasting beyond text inputs. It doesn't actually check whether or not there is some default copy/paste event, only for specific elements which may have default copy/paste behaviour. For now though, it seems to be working for my use case.

huangapple
  • 本文由 发表于 2023年7月17日 15:28:38
  • 转载请务必保留本文链接:https://go.coder-hub.com/76702312.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定