Commit 5c47cf2c authored by Masayuki Nakano's avatar Masayuki Nakano
Browse files

Bug 1461708 - part 8: Make EventStateManager handle middle click paste as a...

Bug 1461708 - part 8: Make EventStateManager handle middle click paste as a default action of mouseup event r=smaug

This patch makes EventStateManager handle middle click paste as a default
action.

Unfortunately, we cannot remove the call of HandleMiddleClickPaste() in
EditorEventListener because it's important to consume middle click event
before any elements in the editor.  For example, if clicked HTMLEditor has
non-editable <a href> element, middle click event needs to be handled by the
editor rather than contentAreaUtils which handles click events of <a href>
elements.  The cause of this kind of issues is, any click event handlers
which handle non-primary button events still listen to "click" events.
Therefore, this patch makes HandleMiddleClickPaste() do nothing if the mouseup
event is fired on an editor.

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

--HG--
extra : moz-landing-system : lando
parent 9b40433e
......@@ -8,6 +8,7 @@
#include "mozilla/EventDispatcher.h"
#include "mozilla/EventStateManager.h"
#include "mozilla/EventStates.h"
#include "mozilla/HTMLEditor.h"
#include "mozilla/IMEStateManager.h"
#include "mozilla/MiscEvents.h"
#include "mozilla/MathAlgorithms.h"
......@@ -41,6 +42,7 @@
#include "nsIContentInlines.h"
#include "nsIDocument.h"
#include "nsIFrame.h"
#include "nsITextControlElement.h"
#include "nsIWidget.h"
#include "nsPresContext.h"
#include "nsIPresShell.h"
......@@ -5016,6 +5018,12 @@ EventStateManager::InitAndDispatchClickEvent(WidgetMouseEvent* aMouseUpEvent,
nsEventStatus status = nsEventStatus_eIgnore;
nsresult rv = aPresShell->HandleEventWithTarget(&event, targetFrame,
target, &status);
// Copy mMultipleActionsPrevented flag from a click event to the mouseup
// event only when it's set to true. It may be set to true if an editor has
// already handled it. This is important to avoid two or more default
// actions handled here.
aMouseUpEvent->mFlags.mMultipleActionsPrevented |=
event.mFlags.mMultipleActionsPrevented;
// If current status is nsEventStatus_eConsumeNoDefault, we don't need to
// overwrite it.
if (*aStatus == nsEventStatus_eConsumeNoDefault) {
......@@ -5060,11 +5068,46 @@ EventStateManager::PostHandleMouseUp(WidgetMouseEvent* aMouseUpEvent,
}
// Fire click events if the event target is still available.
nsresult rv = DispatchClickEvents(presShell, aMouseUpEvent, aStatus,
// Note that do not include the eMouseUp event's status since we ignore it
// for compatibility with the other browsers.
nsEventStatus status = nsEventStatus_eIgnore;
nsresult rv = DispatchClickEvents(presShell, aMouseUpEvent, &status,
mouseUpContent, aOverrideClickTarget);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
// Do not do anything if preceding click events are consumed.
// Note that Chromium dispatches "paste" event and actually pates clipboard
// text into focused editor even if the preceding click events are consumed.
// However, this is different from our traditional behavior and does not
// conform to DOM events. If we need to keep compatibility with Chromium,
// we should change it later.
if (status == nsEventStatus_eConsumeNoDefault) {
*aStatus = nsEventStatus_eConsumeNoDefault;
return NS_OK;
}
// Handle middle click paste if it's enabled and the mouse button is middle.
if (aMouseUpEvent->button != WidgetMouseEventBase::eMiddleButton ||
!WidgetMouseEvent::IsMiddleClickPasteEnabled()) {
return NS_OK;
}
DebugOnly<nsresult> rvIgnored =
HandleMiddleClickPaste(presShell, aMouseUpEvent, &status, nullptr);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
"Failed to paste for a middle click");
// If new status is nsEventStatus_eConsumeNoDefault or
// nsEventStatus_eConsumeDoDefault, use it.
if (*aStatus != nsEventStatus_eConsumeNoDefault &&
(status == nsEventStatus_eConsumeNoDefault ||
status == nsEventStatus_eConsumeDoDefault)) {
*aStatus = status;
}
// Don't return error even if middle mouse paste fails since we haven't
// handled it here.
return NS_OK;
}
......@@ -5130,15 +5173,35 @@ EventStateManager::HandleMiddleClickPaste(nsIPresShell* aPresShell,
{
MOZ_ASSERT(aPresShell);
MOZ_ASSERT(aMouseEvent);
MOZ_ASSERT(aMouseEvent->mMessage == eMouseClick &&
aMouseEvent->button == WidgetMouseEventBase::eMiddleButton);
MOZ_ASSERT((aMouseEvent->mMessage == eMouseClick &&
aMouseEvent->button == WidgetMouseEventBase::eMiddleButton) ||
EventCausesClickEvents(*aMouseEvent));
MOZ_ASSERT(aStatus);
MOZ_ASSERT(*aStatus != nsEventStatus_eConsumeNoDefault);
MOZ_ASSERT(aTextEditor);
RefPtr<Selection> selection = aTextEditor->GetSelection();
if (NS_WARN_IF(!selection)) {
return NS_ERROR_FAILURE;
// Even if we're called twice or more for a mouse operation, we should
// handle only once. Although mMultipleActionsPrevented may be set to
// true by different event handler in the future, we can use it for now.
if (aMouseEvent->mFlags.mMultipleActionsPrevented) {
return NS_OK;
}
aMouseEvent->mFlags.mMultipleActionsPrevented = true;
RefPtr<Selection> selection;
if (aTextEditor) {
selection = aTextEditor->GetSelection();
if (NS_WARN_IF(!selection)) {
return NS_ERROR_FAILURE;
}
} else {
nsIDocument* document = aPresShell->GetDocument();
if (NS_WARN_IF(!document)) {
return NS_ERROR_FAILURE;
}
nsCopySupport::GetSelectionForCopy(document, getter_AddRefs(selection));
if (NS_WARN_IF(!selection)) {
return NS_ERROR_FAILURE;
}
}
// Move selection to the clicked point.
......@@ -5178,6 +5241,12 @@ EventStateManager::HandleMiddleClickPaste(nsIPresShell* aPresShell,
return NS_OK;
}
// Although we've fired "paste" event, there is no editor to accept the
// clipboard content.
if (!aTextEditor) {
return NS_OK;
}
// Check if the editor is still the good target to paste.
if (aTextEditor->Destroyed() ||
aTextEditor->IsReadonly() ||
......
......@@ -365,7 +365,9 @@ public:
/**
* HandleMiddleClickPaste() handles middle mouse button event as pasting
* clipboard text.
* clipboard text. Note that if aTextEditor is nullptr, this only
* dispatches ePaste event because it's necessary for some web apps which
* want to implement their own editor and supports middle click paste.
*
* @param aPresShell The PresShell for the ESM. This lifetime
* should be guaranteed by the caller.
......@@ -374,6 +376,8 @@ public:
* @param aStatus The event status of aMouseEvent.
* @param aTextEditor TextEditor which may be pasted the
* clipboard text by the middle click.
* If there is no editor for aMouseEvent,
* set nullptr.
*/
MOZ_CAN_RUN_SCRIPT
nsresult HandleMiddleClickPaste(nsIPresShell* aPresShell,
......
......@@ -67,6 +67,18 @@ function getClipboardText() {
return SpecialPowers.getClipboardData("text/unicode");
}
function getHTMLEditor() {
let editingSession = SpecialPowers.wrap(window).docShell.editingSession;
if (!editingSession) {
return null;
}
let editor = editingSession.getEditorForWindow(window);
if (!editor) {
return null;
}
return editor.QueryInterface(SpecialPowers.Ci.nsIHTMLEditor);
}
async function putOnClipboard(expected, operationFn, desc, type) {
await SimpleTest.promiseClipboardChange(expected, operationFn, type);
ok(true, desc);
......@@ -815,6 +827,76 @@ add_task(async function test_event_target() {
contenteditableContainer.innerHTML = "";
});
add_task(async function test_paste_event_for_middle_click_without_HTMLEditor() {
await SpecialPowers.pushPrefEnv({"set": [["middlemouse.paste", true],
["middlemouse.contentLoadURL", false]]});
await reset();
contenteditableContainer.innerHTML = '<div id="non-editable-target">non-editable</div>';
let noneditableDiv = document.getElementById("non-editable-target");
ok(!getHTMLEditor(), "There should not be HTMLEditor");
let selection = document.getSelection();
selection.setBaseAndExtent(content.firstChild, 0,
content.firstChild, "CONTENT".length);
await putOnClipboard("CONTENT", () => {
synthesizeKey("c", {accelKey: 1});
}, "copy text from non-editable element");
let auxclickFired = false;
function onAuxClick(event) {
auxclickFired = true;
}
document.addEventListener("auxclick", onAuxClick);
let pasteEventCount = 0;
function onPaste(event) {
pasteEventCount++;
ok(auxclickFired, "'auxclick' event should be fired before 'paste' event");
is(event.target, noneditableDiv,
"'paste' event should be fired on the clicked element");
}
document.addEventListener("paste", onPaste);
synthesizeMouseAtCenter(noneditableDiv, {button: 1});
is(pasteEventCount, 1, "'paste' event should be fired just once");
pasteEventCount = 0;
auxclickFired = false;
document.addEventListener("mouseup", (event) => { event.preventDefault(); }, {once: true});
synthesizeMouseAtCenter(noneditableDiv, {button: 1});
is(pasteEventCount, 1,
"Even if 'mouseup' event is consumed, 'paste' event should be fired");
pasteEventCount = 0;
auxclickFired = false;
document.addEventListener("click", (event) => { event.preventDefault(); }, {once: true, capture: true});
synthesizeMouseAtCenter(noneditableDiv, {button: 1});
is(pasteEventCount, 0,
"If 'click' event is consumed at capturing phase at the document node, 'paste' event should be not be fired");
pasteEventCount = 0;
auxclickFired = false;
noneditableDiv.addEventListener("click", (event) => { event.preventDefault(); }, {once: true});
synthesizeMouseAtCenter(noneditableDiv, {button: 1});
is(pasteEventCount, 1,
"Even if 'click' event listener is added to the click event target, 'paste' event should be fired");
pasteEventCount = 0;
auxclickFired = false;
document.addEventListener("auxclick", (event) => { event.preventDefault(); }, {once: true});
synthesizeMouseAtCenter(noneditableDiv, {button: 1});
is(pasteEventCount, 0,
"If 'auxclick' event is consumed, 'paste' event should be not be fired");
document.removeEventListener("auxclick", onAuxClick);
document.removeEventListener("paste", onPaste);
contenteditableContainer.innerHTML = "";
});
</script>
</pre>
</body>
......
......@@ -678,6 +678,16 @@ EditorEventListener::MouseClick(WidgetMouseEvent* aMouseClickEvent)
return NS_OK;
}
// XXX The following code is hack for our buggy "click" and "auxclick"
// implementation. "auxclick" event was added recently, however,
// any non-primary button click event handlers in our UI still keep
// listening to "click" events. Additionally, "auxclick" event is
// fired after "click" events and even if we do this in the system event
// group, middle click opens new tab before us. Therefore, we need to
// handle middle click at capturing phase of the default group even
// though this makes web apps cannot prevent middle click paste with
// calling preventDefault() of "click" nor "auxclick".
if (aMouseClickEvent->button != WidgetMouseEventBase::eMiddleButton ||
!WidgetMouseEvent::IsMiddleClickPasteEnabled()) {
return NS_OK;
......
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