Commit 9b40433e authored by Masayuki Nakano's avatar Masayuki Nakano
Browse files

Bug 1461708 - part 7: Make EventStateManager::HandleMiddleClickPaste()...

Bug 1461708 - part 7: Make EventStateManager::HandleMiddleClickPaste() dispatch ePaste event by itself r=smaug

This is preparation of the last patch.  Even if no editor is clicked with
middle button, we need to do:
- collapse Selection at the clicked point.
- dispatch "paste" event.

Therefore, HandleMiddleClickPaste() should dispatch ePaste event by itself
and each editor methods should have a bool argument which the caller wants
ePaste event automatically.

Note that Chromium dispatches "paste" event and pastes clipboard content
into clicked editor even if preceding "auxclick" event is consumed.
However, our traditional behavior is not dispatching "paste" event nor
pasting clipboard content.  Unless Chromium developer keeps their odd
behavior, we should keep our traditional behavior since our behavior is
conforming to DOM event model.

Differential Revision: https://phabricator.services.mozilla.com/D7854

--HG--
extra : moz-landing-system : lando
parent d581c696
......@@ -11,7 +11,6 @@
#include "nsAccUtils.h"
#include "nsIClipboard.h"
#include "nsIEditor.h"
#include "nsIPersistentProperties2.h"
#include "nsFrameSelection.h"
......@@ -122,7 +121,7 @@ HyperTextAccessible::PasteText(int32_t aPosition)
RefPtr<TextEditor> textEditor = GetEditor();
if (textEditor) {
SetSelectionRange(aPosition, aPosition);
textEditor->PasteAsAction(nsIClipboard::kGlobalClipboard);
textEditor->PasteAsAction(nsIClipboard::kGlobalClipboard, true);
}
}
......
......@@ -33,6 +33,7 @@
#include "nsCommandParams.h"
#include "nsCOMPtr.h"
#include "nsCopySupport.h"
#include "nsFocusManager.h"
#include "nsGenericHTMLElement.h"
#include "nsIClipboard.h"
......@@ -5132,13 +5133,9 @@ EventStateManager::HandleMiddleClickPaste(nsIPresShell* aPresShell,
MOZ_ASSERT(aMouseEvent->mMessage == eMouseClick &&
aMouseEvent->button == WidgetMouseEventBase::eMiddleButton);
MOZ_ASSERT(aStatus);
MOZ_ASSERT(*aStatus != nsEventStatus_eConsumeNoDefault);
MOZ_ASSERT(aTextEditor);
if (*aStatus == nsEventStatus_eConsumeNoDefault) {
// Already consumed. Do nothing.
return NS_OK;
}
RefPtr<Selection> selection = aTextEditor->GetSelection();
if (NS_WARN_IF(!selection)) {
return NS_ERROR_FAILURE;
......@@ -5172,6 +5169,15 @@ EventStateManager::HandleMiddleClickPaste(nsIPresShell* aPresShell,
}
}
// Fire ePaste event by ourselves since we need to dispatch "paste" event
// even if the middle click event was consumed for compatibility with
// Chromium.
if (!nsCopySupport::FireClipboardEvent(ePaste, clipboardType,
aPresShell, selection)) {
*aStatus = nsEventStatus_eConsumeNoDefault;
return NS_OK;
}
// Check if the editor is still the good target to paste.
if (aTextEditor->Destroyed() ||
aTextEditor->IsReadonly() ||
......@@ -5198,11 +5204,11 @@ EventStateManager::HandleMiddleClickPaste(nsIPresShell* aPresShell,
// quotation. Otherwise, paste it as is.
if (aMouseEvent->IsControl()) {
DebugOnly<nsresult> rv =
aTextEditor->PasteAsQuotationAsAction(clipboardType);
aTextEditor->PasteAsQuotationAsAction(clipboardType, false);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Failed to paste as quotation");
} else {
DebugOnly<nsresult> rv =
aTextEditor->PasteAsAction(clipboardType);
aTextEditor->PasteAsAction(clipboardType, false);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Failed to paste");
}
*aStatus = nsEventStatus_eConsumeNoDefault;
......
......@@ -800,12 +800,18 @@ add_task(async function test_event_target() {
selection.setBaseAndExtent(p1.firstChild, 1, p2.firstChild, 1);
let pasteTarget = null;
document.addEventListener("paste", (event) => { pasteTarget = event.target; }, {once: true});
let pasteEventCount = 0;
function pasteEventLogger(event) {
pasteTarget = event.target;
pasteEventCount++;
}
document.addEventListener("paste", pasteEventLogger);
synthesizeKey("v", {accelKey: 1});
is(pasteTarget.getAttribute("id"), "p1",
"'paste' event's target should be always an element which includes start container of the first Selection range");
is(pasteEventCount, 1,
"'paste' event should be fired only once when Accel+'v' is pressed");
document.removeEventListener("paste", pasteEventLogger);
contenteditableContainer.innerHTML = "";
});
......
......@@ -1201,7 +1201,7 @@ EditorBase::CanDelete(bool* aCanDelete)
NS_IMETHODIMP
EditorBase::Paste(int32_t aClipboardType)
{
nsresult rv = AsTextEditor()->PasteAsAction(aClipboardType);
nsresult rv = AsTextEditor()->PasteAsAction(aClipboardType, true);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
......
......@@ -559,7 +559,7 @@ PasteCommand::DoCommand(const char* aCommandName,
}
TextEditor* textEditor = editor->AsTextEditor();
MOZ_ASSERT(textEditor);
return textEditor->PasteAsAction(nsIClipboard::kGlobalClipboard);
return textEditor->PasteAsAction(nsIClipboard::kGlobalClipboard, true);
}
NS_IMETHODIMP
......@@ -1308,7 +1308,8 @@ PasteQuotationCommand::DoCommand(const char* aCommandName,
}
TextEditor* textEditor = editor->AsTextEditor();
MOZ_ASSERT(textEditor);
return textEditor->PasteAsQuotationAsAction(nsIClipboard::kGlobalClipboard);
return textEditor->PasteAsQuotationAsAction(nsIClipboard::kGlobalClipboard,
true);
}
NS_IMETHODIMP
......@@ -1322,7 +1323,8 @@ PasteQuotationCommand::DoCommandParams(const char* aCommandName,
}
TextEditor* textEditor = editor->AsTextEditor();
MOZ_ASSERT(textEditor);
return textEditor->PasteAsQuotationAsAction(nsIClipboard::kGlobalClipboard);
return textEditor->PasteAsQuotationAsAction(nsIClipboard::kGlobalClipboard,
true);
}
NS_IMETHODIMP
......
......@@ -673,7 +673,7 @@ EditorEventListener::MouseClick(WidgetMouseEvent* aMouseClickEvent)
}
// If we got a mouse down inside the editing area, we should force the
// IME to commit before we change the cursor position
// IME to commit before we change the cursor position.
if (!EnsureCommitCompoisition()) {
return NS_OK;
}
......
......@@ -159,8 +159,11 @@ public:
*
* @param aClipboardType nsIClipboard::kGlobalClipboard or
* nsIClipboard::kSelectionClipboard.
* @param aDispatchPasteEvent true if this should dispatch ePaste event
* before pasting. Otherwise, false.
*/
virtual nsresult PasteAsQuotationAsAction(int32_t aClipboardType) override;
virtual nsresult PasteAsQuotationAsAction(int32_t aClipboardType,
bool aDispatchPasteEvent) override;
/**
* Can we paste |aTransferable| or, if |aTransferable| is null, will a call
......@@ -1310,10 +1313,13 @@ protected: // Shouldn't be used by friend classes
* This tries to dispatch ePaste event first. If its defaultPrevent() is
* called, this does nothing but returns NS_OK.
*
* @param aClipboardType nsIClipboard::kGlobalClipboard or
* nsIClipboard::kSelectionClipboard.
* @param aClipboardType nsIClipboard::kGlobalClipboard or
* nsIClipboard::kSelectionClipboard.
* @param aDispatchPasteEvent true if this should dispatch ePaste event
* before pasting. Otherwise, false.
*/
nsresult PasteInternal(int32_t aClipboardType);
nsresult PasteInternal(int32_t aClipboardType,
bool aDispatchPasteEvent);
/**
* InsertNodeIntoProperAncestorWithTransaction() attempts to insert aNode
......
......@@ -1459,9 +1459,10 @@ HTMLEditor::HavePrivateHTMLFlavor(nsIClipboard* aClipboard)
}
nsresult
HTMLEditor::PasteInternal(int32_t aClipboardType)
HTMLEditor::PasteInternal(int32_t aClipboardType,
bool aDispatchPasteEvent)
{
if (!FireClipboardEvent(ePaste, aClipboardType)) {
if (aDispatchPasteEvent && !FireClipboardEvent(ePaste, aClipboardType)) {
return NS_OK;
}
......@@ -1690,12 +1691,14 @@ HTMLEditor::CanPasteTransferable(nsITransferable* aTransferable)
}
nsresult
HTMLEditor::PasteAsQuotationAsAction(int32_t aClipboardType)
HTMLEditor::PasteAsQuotationAsAction(int32_t aClipboardType,
bool aDispatchPasteEvent)
{
MOZ_ASSERT(aClipboardType == nsIClipboard::kGlobalClipboard ||
aClipboardType == nsIClipboard::kSelectionClipboard);
if (IsPlaintextEditor()) {
// XXX In this case, we don't dispatch ePaste event. Why?
return PasteAsPlaintextQuotation(aClipboardType);
}
......@@ -1743,7 +1746,11 @@ HTMLEditor::PasteAsQuotationAsAction(int32_t aClipboardType)
}
// XXX Why don't we call HTMLEditRules::DidDoAction() after Paste()?
rv = PasteInternal(aClipboardType);
// XXX If ePaste event has not been dispatched yet but selected content
// has already been removed and created a <blockquote> element.
// So, web apps cannot prevent the default of ePaste event which
// will be dispatched by PasteInternal().
rv = PasteInternal(aClipboardType, aDispatchPasteEvent);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
......
......@@ -1936,7 +1936,8 @@ TextEditor::ComputeValueInternal(const nsAString& aFormatType,
}
nsresult
TextEditor::PasteAsQuotationAsAction(int32_t aClipboardType)
TextEditor::PasteAsQuotationAsAction(int32_t aClipboardType,
bool aDispatchPasteEvent)
{
MOZ_ASSERT(aClipboardType == nsIClipboard::kGlobalClipboard ||
aClipboardType == nsIClipboard::kSelectionClipboard);
......@@ -1949,6 +1950,8 @@ TextEditor::PasteAsQuotationAsAction(int32_t aClipboardType)
return rv;
}
// XXX Why don't we dispatch ePaste event here?
// Get the nsITransferable interface for getting the data from the clipboard
nsCOMPtr<nsITransferable> trans;
rv = PrepareTransferable(getter_AddRefs(trans));
......
......@@ -118,8 +118,11 @@ public:
*
* @param aClipboardType nsIClipboard::kGlobalClipboard or
* nsIClipboard::kSelectionClipboard.
* @param aDispatchPasteEvent true if this should dispatch ePaste event
* before pasting. Otherwise, false.
*/
nsresult PasteAsAction(int32_t aClipboardType);
nsresult PasteAsAction(int32_t aClipboardType,
bool aDispatchPasteEvent);
/**
* InsertTextAsAction() inserts aStringToInsert at selection.
......@@ -138,8 +141,11 @@ public:
*
* @param aClipboardType nsIClipboard::kGlobalClipboard or
* nsIClipboard::kSelectionClipboard.
* @param aDispatchPasteEvent true if this should dispatch ePaste event
* before pasting. Otherwise, false.
*/
virtual nsresult PasteAsQuotationAsAction(int32_t aClipboardType);
virtual nsresult PasteAsQuotationAsAction(int32_t aClipboardType,
bool aDispatchPasteEvent);
/**
* DeleteSelectionAsAction() removes selection content or content around
......
......@@ -295,17 +295,19 @@ TextEditor::OnDrop(DragEvent* aDropEvent)
}
nsresult
TextEditor::PasteAsAction(int32_t aClipboardType)
TextEditor::PasteAsAction(int32_t aClipboardType,
bool aDispatchPasteEvent)
{
if (AsHTMLEditor()) {
nsresult rv = AsHTMLEditor()->PasteInternal(aClipboardType);
nsresult rv =
AsHTMLEditor()->PasteInternal(aClipboardType, aDispatchPasteEvent);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
return NS_OK;
}
if (!FireClipboardEvent(ePaste, aClipboardType)) {
if (aDispatchPasteEvent && !FireClipboardEvent(ePaste, aClipboardType)) {
return NS_OK;
}
......
<!DOCTYPE html>
<html>
<head>
<title>Test for paste as quotation with middle button click</title>
<title>Test for paste with middle button click</title>
<script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
<script type="application/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
......@@ -108,6 +108,57 @@ async function doTextareaTests(aTextarea) {
"> abc\n> def\n> \n",
"If pasted text ends with \"\\n\", only the last line should not started with \">\"");
aTextarea.value = "";
let pasteEventCount = 0;
function pasteEventLogger(event) {
pasteEventCount++;
}
aTextarea.addEventListener("paste", pasteEventLogger);
await copyPlaintext("abc");
aTextarea.focus();
document.body.addEventListener("click", (event) => { event.preventDefault(); }, {capture: true, once: true});
synthesizeMouseAtCenter(aTextarea, {button: 1});
is(aTextarea.value, "",
"If 'click' event is consumed at capturing phase of the <body>, paste should be canceled");
is(pasteEventCount, 0,
"If 'click' event is consumed at capturing phase of the <body>, 'paste' event should not be fired");
aTextarea.value = "";
await copyPlaintext("abc");
aTextarea.focus();
aTextarea.addEventListener("mouseup", (event) => { event.preventDefault(); }, {once: true});
pasteEventCount = 0;
synthesizeMouseAtCenter(aTextarea, {button: 1});
is(aTextarea.value, "abc",
"Even if 'mouseup' event is consumed, paste should be done");
is(pasteEventCount, 1,
"Even if 'mouseup' event is consumed, 'paste' event should be fired once");
aTextarea.value = "";
await copyPlaintext("abc");
aTextarea.focus();
aTextarea.addEventListener("click", (event) => { event.preventDefault(); }, {once: true});
pasteEventCount = 0;
synthesizeMouseAtCenter(aTextarea, {button: 1});
is(aTextarea.value, "abc",
"Even if 'click' event handler is added to the <textarea>, paste should not be canceled");
is(pasteEventCount, 1,
"Even if 'click' event handler is added to the <textarea>, 'paste' event should be fired once");
aTextarea.value = "";
await copyPlaintext("abc");
aTextarea.focus();
aTextarea.addEventListener("auxclick", (event) => { event.preventDefault(); }, {once: true});
pasteEventCount = 0;
synthesizeMouseAtCenter(aTextarea, {button: 1});
todo_is(aTextarea.value, "",
"If 'auxclick' event is consumed, paste should be canceled");
todo_is(pasteEventCount, 0,
"If 'auxclick' event is consumed, 'paste' event should not be fired once");
aTextarea.value = "";
aTextarea.removeEventListener("paste", pasteEventLogger);
}
async function doContenteditableTests(aEditableDiv) {
......@@ -119,6 +170,57 @@ async function doContenteditableTests(aEditableDiv) {
"Pasted plaintext should be in <blockquote> element and each linebreaker should be <br> element");
aEditableDiv.innerHTML = "";
let pasteEventCount = 0;
function pasteEventLogger(event) {
pasteEventCount++;
}
aEditableDiv.addEventListener("paste", pasteEventLogger);
await copyPlaintext("abc");
aEditableDiv.focus();
window.addEventListener("click", (event) => { event.preventDefault(); }, {capture: true, once: true});
synthesizeMouseAtCenter(aEditableDiv, {button: 1});
is(aEditableDiv.innerHTML, "",
"If 'click' event is consumed at capturing phase of the window, paste should be canceled");
is(pasteEventCount, 0,
"If 'click' event is consumed at capturing phase of the window, 'paste' event should be fired once");
aEditableDiv.innerHTML = "";
await copyPlaintext("abc");
aEditableDiv.focus();
aEditableDiv.addEventListener("mouseup", (event) => { event.preventDefault(); }, {once: true});
pasteEventCount = 0;
synthesizeMouseAtCenter(aEditableDiv, {button: 1});
is(aEditableDiv.innerHTML, "abc",
"Even if 'mouseup' event is consumed, paste should be done");
is(pasteEventCount, 1,
"Even if 'mouseup' event is consumed, 'paste' event should be fired once");
aEditableDiv.innerHTML = "";
await copyPlaintext("abc");
aEditableDiv.focus();
aEditableDiv.addEventListener("click", (event) => { event.preventDefault(); }, {once: true});
pasteEventCount = 0;
synthesizeMouseAtCenter(aEditableDiv, {button: 1});
is(aEditableDiv.innerHTML, "abc",
"Even if 'click' event handler is added to the editing host, paste should not be canceled");
is(pasteEventCount, 1,
"Even if 'click' event handler is added to the editing host, 'paste' event should be fired");
aEditableDiv.innerHTML = "";
await copyPlaintext("abc");
aEditableDiv.focus();
aEditableDiv.addEventListener("auxclick", (event) => { event.preventDefault(); }, {once: true});
pasteEventCount = 0;
synthesizeMouseAtCenter(aEditableDiv, {button: 1});
todo_is(aEditableDiv.innerHTML, "",
"If 'auxclick' event is consumed, paste should be canceled");
todo_is(pasteEventCount, 0,
"If 'auxclick' event is consumed, 'paste' event should not be fired");
aEditableDiv.innerHTML = "";
aEditableDiv.removeEventListener("paste", pasteEventLogger);
// Oddly, copyHTMLContent fails randomly only on Linux. Let's skip this.
if (navigator.platform.startsWith("Linux")) {
return;
......@@ -140,6 +242,80 @@ async function doContenteditableTests(aEditableDiv) {
aEditableDiv.innerHTML = "";
}
async function doNestedEditorTests(aEditableDiv) {
await copyPlaintext("CLIPBOARD TEXT");
aEditableDiv.innerHTML = '<p id="p">foo</p><textarea id="textarea"></textarea>';
aEditableDiv.focus();
let textarea = document.getElementById("textarea");
let pasteTarget = null;
function onPaste(aEvent) {
pasteTarget = aEvent.target;
}
document.addEventListener("paste", onPaste);
synthesizeMouseAtCenter(textarea, {button: 1});
is(pasteTarget.getAttribute("id"), "textarea",
"Target of 'paste' event should be the clicked <textarea>");
is(textarea.value, "CLIPBOARD TEXT",
"Clicking in <textarea> in an editable <div> should paste the clipboard text into the <textarea>");
is(aEditableDiv.innerHTML, '<p id="p">foo</p><textarea id="textarea"></textarea>',
"Pasting in the <textarea> shouldn't be handled by the HTMLEditor");
textarea.value = "";
textarea.readOnly = true;
pasteTarget = null;
synthesizeMouseAtCenter(textarea, {button: 1});
todo_is(pasteTarget, textarea,
"Target of 'paste' event should be the clicked <textarea> even if it's read-only");
is(textarea.value, "",
"Clicking in read-only <textarea> in an editable <div> should not paste the clipboard text into the read-only <textarea>");
// HTMLEditor thinks that read-only <textarea> is not modifiable.
// Therefore, HTMLEditor does not paste the text.
is(aEditableDiv.innerHTML, '<p id="p">foo</p><textarea id="textarea" readonly=""></textarea>',
"Clicking in read-only <textarea> shouldn't cause pasting the clipboard text into its parent HTMLEditor");
textarea.value = "";
textarea.readOnly = false;
textarea.disabled = true;
pasteTarget = null;
synthesizeMouseAtCenter(textarea, {button: 1});
// Although, this compares with <textarea>, I'm not sure it's proper event
// target because of disabled <textarea>.
todo_is(pasteTarget, textarea,
"Target of 'paste' event should be the clicked <textarea> even if it's disabled");
is(textarea.value, "",
"Clicking in disabled <textarea> in an editable <div> should not paste the clipboard text into the disabled <textarea>");
// HTMLEditor thinks that disabled <textarea> is not modifiable.
// Therefore, HTMLEditor does not paste the text.
is(aEditableDiv.innerHTML, '<p id="p">foo</p><textarea id="textarea" disabled=""></textarea>',
"Clicking in disabled <textarea> shouldn't cause pasting the clipboard text into its parent HTMLEditor");
document.removeEventListener("paste", onPaste);
aEditableDiv.innerHTML = "";
}
async function doAfterRemoveOfClickedElementTest(aEditableDiv) {
await copyPlaintext("CLIPBOARD TEXT");
aEditableDiv.innerHTML = '<p id="p">foo<span id="span">bar</span></p>';
aEditableDiv.focus();
let span = document.getElementById("span");
let pasteTarget = null;
document.addEventListener("paste", (aEvent) => { pasteTarget = aEvent.target; }, {once: true});
document.addEventListener("auxclick", (aEvent) => {
is(aEvent.target.getAttribute("id"), "span",
"Target of auxclick event should be the <span> element");
span.parentElement.removeChild(span);
}, {once: true});
synthesizeMouseAtCenter(span, {button: 1});
todo_is(pasteTarget.getAttribute("id"), "p",
"Target of 'paste' event should be the <p> element since <span> has gone");
// XXX Currently, pasted to start of the <p> because EventStateManager
// do not recompute event target frame.
todo_is(aEditableDiv.innerHTML, '<p id="p">fooCLIPBOARD TEXT</p>',
"Clipbpard text should looks like replacing the <span> element");
aEditableDiv.innerHTML = "";
}
async function doTests() {
await SpecialPowers.pushPrefEnv({"set": [["middlemouse.paste", true],
["middlemouse.contentLoadURL", false]]});
......@@ -148,6 +324,8 @@ async function doTests() {
await doTextareaTests(document.getElementById("editor"));
container.innerHTML = "<div id=\"editor\" contenteditable style=\"min-height: 1em;\"></div>";
await doContenteditableTests(document.getElementById("editor"));
await doNestedEditorTests(document.getElementById("editor"));
await doAfterRemoveOfClickedElementTest(document.getElementById("editor"));
SimpleTest.finish();
}
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment