Commit d822e1f2 authored by Masayuki Nakano's avatar Masayuki Nakano
Browse files

Bug 1815969 - part 5: Make editors handle pasting something in new focused...

Bug 1815969 - part 5: Make editors handle pasting something in new focused editor if a `paste` event listener moves focus r=m_kato

If a `paste` event listener moves focus, Chrome makes new editor keep handling
the pasting.  For the compatibility, we should follow it unless the new focused
element is in different document because user should allow to paste it
explicitly.

On the other hand, this just stops handling "cut" in same situation because
handling it requires to update clipboard without user's activation.  Therefore,
the clipboard content and/or the new editor content may be lost from the users
point of view.

Note that `nsContentUtils::GetActiveEditor` may return `HTMLEditor` instance
when focused element does not have `TextEditor` even when non-editable element
has focus.  Therefore, if it returns an `HTMLEditor`, we need to check whether
it's active in the DOM window with a call of `HTMEditor::IsActiveInDOMWindow`.

Differential Revision: https://phabricator.services.mozilla.com/D176741
parent 04661561
Loading
Loading
Loading
Loading
+128 −5
Original line number Diff line number Diff line
@@ -1652,6 +1652,12 @@ nsresult EditorBase::CutAsAction(nsIPrincipal* aPrincipal) {
  }

  {
    RefPtr<nsFocusManager> focusManager = nsFocusManager::GetFocusManager();
    if (NS_WARN_IF(!focusManager)) {
      return NS_ERROR_UNEXPECTED;
    }
    const RefPtr<Element> focusedElement = focusManager->GetFocusedElement();

    Result<ClipboardEventResult, nsresult> ret =
        DispatchClipboardEventAndUpdateClipboard(
            eCut, nsIClipboard::kGlobalClipboard);
@@ -1671,6 +1677,24 @@ nsresult EditorBase::CutAsAction(nsIPrincipal* aPrincipal) {
      case ClipboardEventResult::DefaultPreventedOfPaste:
        MOZ_ASSERT_UNREACHABLE("Invalid result for eCut");
    }

    // If focus is changed by a "cut" event listener, we should stop handling
    // the cut.
    const RefPtr<Element> newFocusedElement = focusManager->GetFocusedElement();
    if (MOZ_UNLIKELY(focusedElement != newFocusedElement)) {
      if (focusManager->GetFocusedWindow() != GetWindow()) {
        return NS_OK;
      }
      RefPtr<EditorBase> editorBase =
          nsContentUtils::GetActiveEditor(GetPresContext());
      if (!editorBase || (editorBase->IsHTMLEditor() &&
                          !editorBase->AsHTMLEditor()->IsActiveInDOMWindow())) {
        return NS_OK;
      }
      if (editorBase != this) {
        return NS_OK;
      }
    }
  }

  // Dispatch "beforeinput" event after dispatching "cut" event.
@@ -1788,6 +1812,12 @@ nsresult EditorBase::PasteAsAction(int32_t aClipboardType,
  }

  if (aDispatchPasteEvent == DispatchPasteEvent::Yes) {
    RefPtr<nsFocusManager> focusManager = nsFocusManager::GetFocusManager();
    if (NS_WARN_IF(!focusManager)) {
      return NS_ERROR_UNEXPECTED;
    }
    const RefPtr<Element> focusedElement = focusManager->GetFocusedElement();

    Result<ClipboardEventResult, nsresult> ret =
        DispatchClipboardEventAndUpdateClipboard(ePaste, aClipboardType);
    if (MOZ_UNLIKELY(ret.isErr())) {
@@ -1805,6 +1835,31 @@ nsresult EditorBase::PasteAsAction(int32_t aClipboardType,
      case ClipboardEventResult::CopyOrCutHandled:
        MOZ_ASSERT_UNREACHABLE("Invalid result for ePaste");
    }

    // If focus is changed by a "paste" event listener, we should keep handling
    // the "pasting" in new focused editor because Chrome works as so.
    const RefPtr<Element> newFocusedElement = focusManager->GetFocusedElement();
    if (MOZ_UNLIKELY(focusedElement != newFocusedElement)) {
      // For the privacy reason, let's top handling it if new focused element is
      // in different document.
      if (focusManager->GetFocusedWindow() != GetWindow()) {
        return EditorBase::ToGenericNSResult(NS_ERROR_EDITOR_ACTION_CANCELED);
      }
      RefPtr<EditorBase> editorBase =
          nsContentUtils::GetActiveEditor(GetPresContext());
      if (!editorBase || (editorBase->IsHTMLEditor() &&
                          !editorBase->AsHTMLEditor()->IsActiveInDOMWindow())) {
        return EditorBase::ToGenericNSResult(NS_ERROR_EDITOR_ACTION_CANCELED);
      }
      if (editorBase != this) {
        nsresult rv = editorBase->PasteAsAction(
            aClipboardType, DispatchPasteEvent::No, aPrincipal);
        NS_WARNING_ASSERTION(
            NS_SUCCEEDED(rv),
            "EditorBase::PasteAsAction(DispatchPasteEvent::No) failed");
        return EditorBase::ToGenericNSResult(rv);
      }
    }
  } else {
    // The caller must already have dispatched a "paste" event.
    editActionData.NotifyOfDispatchingClipboardEvent();
@@ -1832,6 +1887,12 @@ nsresult EditorBase::PasteAsQuotationAsAction(
  }

  if (aDispatchPasteEvent == DispatchPasteEvent::Yes) {
    RefPtr<nsFocusManager> focusManager = nsFocusManager::GetFocusManager();
    if (NS_WARN_IF(!focusManager)) {
      return NS_ERROR_UNEXPECTED;
    }
    const RefPtr<Element> focusedElement = focusManager->GetFocusedElement();

    Result<ClipboardEventResult, nsresult> ret =
        DispatchClipboardEventAndUpdateClipboard(ePaste, aClipboardType);
    if (MOZ_UNLIKELY(ret.isErr())) {
@@ -1849,6 +1910,31 @@ nsresult EditorBase::PasteAsQuotationAsAction(
      case ClipboardEventResult::CopyOrCutHandled:
        MOZ_ASSERT_UNREACHABLE("Invalid result for ePaste");
    }

    // If focus is changed by a "paste" event listener, we should keep handling
    // the "pasting" in new focused editor because Chrome works as so.
    const RefPtr<Element> newFocusedElement = focusManager->GetFocusedElement();
    if (MOZ_UNLIKELY(focusedElement != newFocusedElement)) {
      // For the privacy reason, let's top handling it if new focused element is
      // in different document.
      if (focusManager->GetFocusedWindow() != GetWindow()) {
        return EditorBase::ToGenericNSResult(NS_ERROR_EDITOR_ACTION_CANCELED);
      }
      RefPtr<EditorBase> editorBase =
          nsContentUtils::GetActiveEditor(GetPresContext());
      if (!editorBase || (editorBase->IsHTMLEditor() &&
                          !editorBase->AsHTMLEditor()->IsActiveInDOMWindow())) {
        return EditorBase::ToGenericNSResult(NS_ERROR_EDITOR_ACTION_CANCELED);
      }
      if (editorBase != this) {
        nsresult rv = editorBase->PasteAsQuotationAsAction(
            aClipboardType, DispatchPasteEvent::No, aPrincipal);
        NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
                             "EditorBase::PasteAsQuotationAsAction("
                             "DispatchPasteEvent::No) failed");
        return EditorBase::ToGenericNSResult(rv);
      }
    }
  } else {
    // The caller must already have dispatched a "paste" event.
    editActionData.NotifyOfDispatchingClipboardEvent();
@@ -1861,7 +1947,8 @@ nsresult EditorBase::PasteAsQuotationAsAction(
}

nsresult EditorBase::PasteTransferableAsAction(
    nsITransferable* aTransferable, nsIPrincipal* aPrincipal /* = nullptr */) {
    nsITransferable* aTransferable, DispatchPasteEvent aDispatchPasteEvent,
    nsIPrincipal* aPrincipal /* = nullptr */) {
  // FIXME: This may be called as a call of nsIEditor::PasteTransferable.
  // In this case, we should keep handling the paste even in the readonly mode.
  if (IsHTMLEditor() && IsReadonly()) {
@@ -1874,7 +1961,13 @@ nsresult EditorBase::PasteTransferableAsAction(
    return NS_ERROR_NOT_INITIALIZED;
  }

  {
  if (aDispatchPasteEvent == DispatchPasteEvent::Yes) {
    RefPtr<nsFocusManager> focusManager = nsFocusManager::GetFocusManager();
    if (NS_WARN_IF(!focusManager)) {
      return NS_ERROR_UNEXPECTED;
    }
    const RefPtr<Element> focusedElement = focusManager->GetFocusedElement();

    // Use an invalid value for the clipboard type as data comes from
    // aTransferable and we don't currently implement a way to put that in the
    // data transfer in TextEditor yet.
@@ -1896,6 +1989,34 @@ nsresult EditorBase::PasteTransferableAsAction(
      case ClipboardEventResult::CopyOrCutHandled:
        MOZ_ASSERT_UNREACHABLE("Invalid result for ePaste");
    }

    // If focus is changed by a "paste" event listener, we should keep handling
    // the "pasting" in new focused editor because Chrome works as so.
    const RefPtr<Element> newFocusedElement = focusManager->GetFocusedElement();
    if (MOZ_UNLIKELY(focusedElement != newFocusedElement)) {
      // For the privacy reason, let's top handling it if new focused element is
      // in different document.
      if (focusManager->GetFocusedWindow() != GetWindow()) {
        return EditorBase::ToGenericNSResult(NS_ERROR_EDITOR_ACTION_CANCELED);
      }
      RefPtr<EditorBase> editorBase =
          nsContentUtils::GetActiveEditor(GetPresContext());
      if (!editorBase || (editorBase->IsHTMLEditor() &&
                          !editorBase->AsHTMLEditor()->IsActiveInDOMWindow())) {
        return EditorBase::ToGenericNSResult(NS_ERROR_EDITOR_ACTION_CANCELED);
      }
      if (editorBase != this) {
        nsresult rv = editorBase->PasteTransferableAsAction(
            aTransferable, DispatchPasteEvent::No, aPrincipal);
        NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
                             "EditorBase::PasteTransferableAsAction("
                             "DispatchPasteEvent::No) failed");
        return EditorBase::ToGenericNSResult(rv);
      }
    }
  } else {
    // The caller must already have dispatched a "paste" event.
    editActionData.NotifyOfDispatchingClipboardEvent();
  }

  if (NS_WARN_IF(!aTransferable)) {
@@ -1982,9 +2103,11 @@ EditorBase::SafeToInsertData EditorBase::IsSafeToInsertData(
}

NS_IMETHODIMP EditorBase::PasteTransferable(nsITransferable* aTransferable) {
  nsresult rv = PasteTransferableAsAction(aTransferable);
  NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
                       "EditorBase::PasteTransferableAsAction() failed");
  nsresult rv =
      PasteTransferableAsAction(aTransferable, DispatchPasteEvent::Yes);
  NS_WARNING_ASSERTION(
      NS_SUCCEEDED(rv),
      "EditorBase::PasteTransferableAsAction(DispatchPasteEvent::Yes) failed");
  return rv;
}

+4 −1
Original line number Diff line number Diff line
@@ -762,12 +762,15 @@ class EditorBase : public nsIEditor,
   * Paste aTransferable at Selection.
   *
   * @param aTransferable       Must not be nullptr.
   * @param aDispatchPasteEvent Yes if this should dispatch ePaste event
   *                            before pasting.  Otherwise, No.
   * @param aPrincipal          Set subject principal if it may be called by
   *                            JS.  If set to nullptr, will be treated as
   *                            called by system.
   */
  MOZ_CAN_RUN_SCRIPT nsresult PasteTransferableAsAction(
      nsITransferable* aTransferable, nsIPrincipal* aPrincipal = nullptr);
      nsITransferable* aTransferable, DispatchPasteEvent aDispatchPasteEvent,
      nsIPrincipal* aPrincipal = nullptr);

  /**
   * PasteAsQuotationAsAction() pastes content in clipboard as quotation.
+5 −4
Original line number Diff line number Diff line
@@ -507,10 +507,11 @@ nsresult PasteTransferableCommand::DoCommandParam(
  if (NS_WARN_IF(!aTransferableParam)) {
    return NS_ERROR_INVALID_ARG;
  }
  nsresult rv =
      aEditorBase.PasteTransferableAsAction(aTransferableParam, aPrincipal);
  NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
                       "EditorBase::PasteTransferableAsAction() failed");
  nsresult rv = aEditorBase.PasteTransferableAsAction(
      aTransferableParam, EditorBase::DispatchPasteEvent::Yes, aPrincipal);
  NS_WARNING_ASSERTION(
      NS_SUCCEEDED(rv),
      "EditorBase::PasteTransferableAsAction(DispatchPasteEvent::Yes) failed");
  return rv;
}

+5 −2
Original line number Diff line number Diff line
@@ -234,14 +234,17 @@ class HTMLEditor final : public EditorBase,
   * PasteNoFormattingAsAction() pastes content in clipboard without any style
   * information.
   *
   * @param aSelectionType      nsIClipboard::kGlobalClipboard or
   * @param aClipboardType      nsIClipboard::kGlobalClipboard or
   *                            nsIClipboard::kSelectionClipboard.
   * @param aDispatchPasteEvent Yes if this should dispatch ePaste event
   *                            before pasting.  Otherwise, No.
   * @param aPrincipal          Set subject principal if it may be called by
   *                            JS.  If set to nullptr, will be treated as
   *                            called by system.
   */
  MOZ_CAN_RUN_SCRIPT nsresult PasteNoFormattingAsAction(
      int32_t aSelectionType, nsIPrincipal* aPrincipal = nullptr);
      int32_t aClipboardType, DispatchPasteEvent aDispatchPasteEvent,
      nsIPrincipal* aPrincipal = nullptr);

  bool CanPasteTransferable(nsITransferable* aTransferable) final;

+6 −4
Original line number Diff line number Diff line
@@ -128,10 +128,12 @@ nsresult PasteNoFormattingCommand::DoCommand(Command aCommand,
  }
  // Known live because we hold a ref above in "editor"
  nsresult rv = MOZ_KnownLive(htmlEditor)
                    ->PasteNoFormattingAsAction(nsIClipboard::kGlobalClipboard,
                                                aPrincipal);
  NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
                       "HTMLEditor::PasteNoFormattingAsAction() failed");
                    ->PasteNoFormattingAsAction(
                        nsIClipboard::kGlobalClipboard,
                        EditorBase::DispatchPasteEvent::Yes, aPrincipal);
  NS_WARNING_ASSERTION(
      NS_SUCCEEDED(rv),
      "HTMLEditor::PasteNoFormattingAsAction(DispatchPasteEvent::Yes) failed");
  return rv;
}

Loading