Bug 1528289 - part 1: Move selection at middle button down rather than middle button up r=edgar

Chrome and Safari move selection at middle button down and does not modify the
range at middle button up.  However, they handle middle button down with
`Shift` key is "continue selection".  So, we should handle selection in
nsIFrame when `mousedown` event for middle mouse button is fired, but ignore
multiple selection, drag and drop and maintaining selection for aligning the
behavior to the other browsers.

This patch splits `nsIFrame::HandlePress()` and calls new method which
moves selection from `nsIFrame::HandleEvent()` when middle button is pressed.
(Note that this patch does not check whether middle click paste is enabled
because Chrome moves selection even on Windows which Chrome always disable
middle click paste on.)

With this change, "paste" event target is changed.  Previously, we used target
of the preceding `mouseup` event, but we start to use the target of the
preceding `mousedown` event.

Note that even with this patch, we still behave differently from Chrome even
in the following cases:
- middle mouse button down in selected range, we collapse it, but Chrome keeps
  the selection.
- middle mouse button click in selected range, we dispatch "paste" event, but
  Chrome collapse selection and not dispatch "paste" event.
- middle mouse button down in selected range and up in different element,
  Chrome does not modify the range nor dispatch "paste" event.
- Shift + middle mouse button in editable `<table>` works as non-editable
  in Chrome, but our editor handles it with special path.  Therefore, we
  don't modify the range but dispatch "paste" event in the selected range.

Changing them requires bigger change and probably requires some other features'
behavior changes.  Therefore, we shouldn't touch these issues until they are
actually reported as web-compat issues.

Differential Revision: https://phabricator.services.mozilla.com/D103997
This commit is contained in:
Masayuki Nakano 2021-03-01 22:57:06 +00:00
parent babc9888a5
commit 246163150b
9 changed files with 762 additions and 106 deletions

View File

@ -5198,20 +5198,8 @@ nsresult EventStateManager::HandleMiddleClickPaste(
}
}
// Move selection to the clicked point.
nsCOMPtr<nsIContent> container;
int32_t offset;
nsLayoutUtils::GetContainerAndOffsetAtEvent(
aPresShell, aMouseEvent, getter_AddRefs(container), &offset);
if (container) {
// XXX If readonly or disabled <input> or <textarea> in contenteditable
// designMode editor is clicked, the point is in the editor.
// However, outer HTMLEditor and Selection should handle it.
// So, in such case, Selection::Collapse() will fail.
DebugOnly<nsresult> rv = selection->CollapseInLimiter(container, offset);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
"Failed to collapse Selection at middle clicked");
}
// Don't modify selection here because we've already set caret to the point
// at "mousedown" event.
int32_t clipboardType = nsIClipboard::kGlobalClipboard;
nsresult rv = NS_OK;

View File

@ -4344,7 +4344,23 @@ nsresult nsIFrame::HandleEvent(nsPresContext* aPresContext,
} else if (aEvent->mMessage == eMouseUp || aEvent->mMessage == eTouchEnd) {
HandleRelease(aPresContext, aEvent, aEventStatus);
}
return NS_OK;
}
// When middle button is down, we need to just move selection and focus at
// the clicked point. Note that even if middle click paste is not enabled,
// Chrome moves selection at middle mouse button down. So, we should follow
// the behavior for the compatibility.
if (aEvent->mMessage == eMouseDown) {
WidgetMouseEvent* mouseEvent = aEvent->AsMouseEvent();
if (mouseEvent && mouseEvent->mButton == MouseButton::eMiddle) {
if (*aEventStatus == nsEventStatus_eConsumeNoDefault) {
return NS_OK;
}
return MoveCaretToEventPoint(aPresContext, mouseEvent, aEventStatus);
}
}
return NS_OK;
}
@ -4549,14 +4565,6 @@ nsIFrame::HandlePress(nsPresContext* aPresContext, WidgetGUIEvent* aEvent,
return NS_ERROR_FAILURE;
}
// if we are in Navigator and the click is in a draggable node, we don't want
// to start selection because we don't want to interfere with a potential
// drag of said node and steal all its glory.
int16_t isEditor = presShell->GetSelectionFlags();
// weaaak. only the editor can display frame selection not just text and
// images
isEditor = isEditor == nsISelectionDisplay::DISPLAY_ALL;
WidgetMouseEvent* mouseEvent = aEvent->AsMouseEvent();
if (!mouseEvent->IsAlt()) {
@ -4574,6 +4582,35 @@ nsIFrame::HandlePress(nsPresContext* aPresContext, WidgetGUIEvent* aEvent,
}
}
return MoveCaretToEventPoint(aPresContext, mouseEvent, aEventStatus);
}
nsresult nsIFrame::MoveCaretToEventPoint(nsPresContext* aPresContext,
WidgetMouseEvent* aMouseEvent,
nsEventStatus* aEventStatus) {
MOZ_ASSERT(aPresContext);
MOZ_ASSERT(aMouseEvent);
MOZ_ASSERT(aMouseEvent->mMessage == eMouseDown);
MOZ_ASSERT(aMouseEvent->mButton == MouseButton::ePrimary ||
aMouseEvent->mButton == MouseButton::eMiddle);
MOZ_ASSERT(aEventStatus);
mozilla::PresShell* presShell = aPresContext->GetPresShell();
if (!presShell) {
return NS_ERROR_FAILURE;
}
// if we are in Navigator and the click is in a draggable node, we don't want
// to start selection because we don't want to interfere with a potential
// drag of said node and steal all its glory.
int16_t isEditor = presShell->GetSelectionFlags();
// weaaak. only the editor can display frame selection not just text and
// images
isEditor = isEditor == nsISelectionDisplay::DISPLAY_ALL;
// Don't do something if it's moddle button down event.
bool isPrimaryButtonDown = aMouseEvent->mButton == MouseButton::ePrimary;
// check whether style allows selection
// if not, don't tell selection the mouse event even occurred.
StyleUserSelect selectStyle;
@ -4582,141 +4619,155 @@ nsIFrame::HandlePress(nsPresContext* aPresContext, WidgetGUIEvent* aEvent,
return NS_OK;
}
bool useFrameSelection = (selectStyle == StyleUserSelect::Text);
// If the mouse is dragged outside the nearest enclosing scrollable area
// while making a selection, the area will be scrolled. To do this, capture
// the mouse on the nearest scrollable frame. If there isn't a scrollable
// frame, or something else is already capturing the mouse, there's no
// reason to capture.
if (!PresShell::GetCapturingContent()) {
nsIScrollableFrame* scrollFrame = nsLayoutUtils::GetNearestScrollableFrame(
this, nsLayoutUtils::SCROLLABLE_SAME_DOC |
nsLayoutUtils::SCROLLABLE_INCLUDE_HIDDEN);
if (scrollFrame) {
nsIFrame* capturingFrame = do_QueryFrame(scrollFrame);
PresShell::SetCapturingContent(capturingFrame->GetContent(),
CaptureFlags::IgnoreAllowedState);
if (isPrimaryButtonDown) {
// If the mouse is dragged outside the nearest enclosing scrollable area
// while making a selection, the area will be scrolled. To do this, capture
// the mouse on the nearest scrollable frame. If there isn't a scrollable
// frame, or something else is already capturing the mouse, there's no
// reason to capture.
if (!PresShell::GetCapturingContent()) {
nsIScrollableFrame* scrollFrame =
nsLayoutUtils::GetNearestScrollableFrame(
this, nsLayoutUtils::SCROLLABLE_SAME_DOC |
nsLayoutUtils::SCROLLABLE_INCLUDE_HIDDEN);
if (scrollFrame) {
nsIFrame* capturingFrame = do_QueryFrame(scrollFrame);
PresShell::SetCapturingContent(capturingFrame->GetContent(),
CaptureFlags::IgnoreAllowedState);
}
}
}
// XXX This is screwy; it really should use the selection frame, not the
// event frame
const nsFrameSelection* frameselection = nullptr;
if (useFrameSelection)
frameselection = GetConstFrameSelection();
else
frameselection = presShell->ConstFrameSelection();
const nsFrameSelection* frameselection =
selectStyle == StyleUserSelect::Text ? GetConstFrameSelection()
: presShell->ConstFrameSelection();
if (!frameselection || frameselection->GetDisplaySelection() ==
nsISelectionController::SELECTION_OFF)
nsISelectionController::SELECTION_OFF) {
return NS_OK; // nothing to do we cannot affect selection from here
}
#ifdef XP_MACOSX
if (mouseEvent->IsControl())
return NS_OK; // short circuit. hard coded for mac due to time restraints.
bool control = mouseEvent->IsMeta();
// If Control key is pressed on macOS, it should be treated as right click.
// So, don't change selection.
if (aMouseEvent->IsControl()) {
return NS_OK;
}
bool control = aMouseEvent->IsMeta();
#else
bool control = mouseEvent->IsControl();
bool control = aMouseEvent->IsControl();
#endif
RefPtr<nsFrameSelection> fc = const_cast<nsFrameSelection*>(frameselection);
if (mouseEvent->mClickCount > 1) {
if (isPrimaryButtonDown && aMouseEvent->mClickCount > 1) {
// These methods aren't const but can't actually delete anything,
// so no need for AutoWeakFrame.
fc->SetDragState(true);
return HandleMultiplePress(aPresContext, mouseEvent, aEventStatus, control);
return HandleMultiplePress(aPresContext, aMouseEvent, aEventStatus,
control);
}
nsPoint pt = nsLayoutUtils::GetEventCoordinatesRelativeTo(mouseEvent,
nsPoint pt = nsLayoutUtils::GetEventCoordinatesRelativeTo(aMouseEvent,
RelativeTo{this});
ContentOffsets offsets = GetContentOffsetsFromPoint(pt, SKIP_HIDDEN);
if (!offsets.content) return NS_ERROR_FAILURE;
if (!offsets.content) {
return NS_ERROR_FAILURE;
}
// Let Ctrl/Cmd+mouse down do table selection instead of drag initiation
nsCOMPtr<nsIContent> parentContent;
int32_t contentOffset;
TableSelectionMode target;
nsresult rv;
rv = GetDataForTableSelection(frameselection, presShell, mouseEvent,
getter_AddRefs(parentContent), &contentOffset,
&target);
if (NS_SUCCEEDED(rv) && parentContent) {
fc->SetDragState(true);
return fc->HandleTableSelection(parentContent, contentOffset, target,
mouseEvent);
if (isPrimaryButtonDown) {
// Let Ctrl/Cmd + left mouse down do table selection instead of drag
// initiation.
nsCOMPtr<nsIContent> parentContent;
int32_t contentOffset;
TableSelectionMode target;
nsresult rv = GetDataForTableSelection(
frameselection, presShell, aMouseEvent, getter_AddRefs(parentContent),
&contentOffset, &target);
if (NS_SUCCEEDED(rv) && parentContent) {
fc->SetDragState(true);
return fc->HandleTableSelection(parentContent, contentOffset, target,
aMouseEvent);
}
}
fc->SetDelayedCaretData(0);
// Check if any part of this frame is selected, and if the
// user clicked inside the selected region. If so, we delay
// starting a new selection since the user may be trying to
// drag the selected region to some other app.
if (isPrimaryButtonDown) {
// Check if any part of this frame is selected, and if the user clicked
// inside the selected region, and if it's the left button. If so, we delay
// starting a new selection since the user may be trying to drag the
// selected region to some other app.
if (GetContent() && GetContent()->IsMaybeSelected()) {
bool inSelection = false;
UniquePtr<SelectionDetails> details = frameselection->LookUpSelection(
offsets.content, 0, offsets.EndOffset(), false);
if (GetContent() && GetContent()->IsMaybeSelected()) {
bool inSelection = false;
UniquePtr<SelectionDetails> details = frameselection->LookUpSelection(
offsets.content, 0, offsets.EndOffset(), false);
//
// If there are any details, check to see if the user clicked
// within any selected region of the frame.
//
for (SelectionDetails* curDetail = details.get(); curDetail;
curDetail = curDetail->mNext.get()) {
//
// If the user clicked inside a selection, then just
// return without doing anything. We will handle placing
// the caret later on when the mouse is released. We ignore
// the spellcheck, find and url formatting selections.
// If there are any details, check to see if the user clicked
// within any selected region of the frame.
//
if (curDetail->mSelectionType != SelectionType::eSpellCheck &&
curDetail->mSelectionType != SelectionType::eFind &&
curDetail->mSelectionType != SelectionType::eURLSecondary &&
curDetail->mSelectionType != SelectionType::eURLStrikeout &&
curDetail->mStart <= offsets.StartOffset() &&
offsets.EndOffset() <= curDetail->mEnd) {
inSelection = true;
for (SelectionDetails* curDetail = details.get(); curDetail;
curDetail = curDetail->mNext.get()) {
//
// If the user clicked inside a selection, then just
// return without doing anything. We will handle placing
// the caret later on when the mouse is released. We ignore
// the spellcheck, find and url formatting selections.
//
if (curDetail->mSelectionType != SelectionType::eSpellCheck &&
curDetail->mSelectionType != SelectionType::eFind &&
curDetail->mSelectionType != SelectionType::eURLSecondary &&
curDetail->mSelectionType != SelectionType::eURLStrikeout &&
curDetail->mStart <= offsets.StartOffset() &&
offsets.EndOffset() <= curDetail->mEnd) {
inSelection = true;
}
}
if (inSelection) {
fc->SetDragState(false);
fc->SetDelayedCaretData(aMouseEvent);
return NS_OK;
}
}
if (inSelection) {
fc->SetDragState(false);
fc->SetDelayedCaretData(mouseEvent);
return NS_OK;
}
fc->SetDragState(true);
}
fc->SetDragState(true);
// Do not touch any nsFrame members after this point without adding
// weakFrame checks.
const nsFrameSelection::FocusMode focusMode = [&]() {
// If "Shift" and "Ctrl" are both pressed, "Shift" is given precedence. This
// mimics the old behaviour.
if (mouseEvent->IsShift()) {
if (aMouseEvent->IsShift()) {
return nsFrameSelection::FocusMode::kExtendSelection;
}
if (control) {
if (isPrimaryButtonDown && control) {
return nsFrameSelection::FocusMode::kMultiRangeSelection;
}
return nsFrameSelection::FocusMode::kCollapseToNewPoint;
}();
rv = fc->HandleClick(MOZ_KnownLive(offsets.content) /* bug 1636889 */,
offsets.StartOffset(), offsets.EndOffset(), focusMode,
offsets.associate);
nsresult rv = fc->HandleClick(
MOZ_KnownLive(offsets.content) /* bug 1636889 */, offsets.StartOffset(),
offsets.EndOffset(), focusMode, offsets.associate);
if (NS_FAILED(rv)) {
return rv;
}
if (NS_FAILED(rv)) return rv;
// We don't handle mouse button up if it's middle button.
if (isPrimaryButtonDown && offsets.offset != offsets.secondaryOffset) {
fc->MaintainSelection();
}
if (offsets.offset != offsets.secondaryOffset) fc->MaintainSelection();
if (isEditor && !mouseEvent->IsShift() &&
if (isPrimaryButtonDown && isEditor && !aMouseEvent->IsShift() &&
(offsets.EndOffset() - offsets.StartOffset()) == 1) {
// A single node is selected and we aren't extending an existing
// selection, which means the user clicked directly on an object (either
@ -4726,7 +4777,7 @@ nsIFrame::HandlePress(nsPresContext* aPresContext, WidgetGUIEvent* aEvent,
fc->SetDragState(false);
}
return rv;
return NS_OK;
}
nsresult nsIFrame::SelectByTypeAtPoint(nsPresContext* aPresContext,

View File

@ -2176,6 +2176,20 @@ class nsIFrame : public nsQueryFrame {
HandlePress(nsPresContext* aPresContext, mozilla::WidgetGUIEvent* aEvent,
nsEventStatus* aEventStatus);
/**
* MoveCaretToEventPoint() moves caret at the point of aMouseEvent.
*
* @param aPresContext Must not be nullptr.
* @param aMouseEvent Must not be nullptr, the message must be
* eMouseDown and its button must be primary or
* middle button.
* @param aEventStatus [out] Must not be nullptr. This method ignores
* its initial value, but callees may refer it.
*/
MOZ_CAN_RUN_SCRIPT nsresult MoveCaretToEventPoint(
nsPresContext* aPresContext, mozilla::WidgetMouseEvent* aMouseEvent,
nsEventStatus* aEventStatus);
MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHOD HandleMultiplePress(
nsPresContext* aPresContext, mozilla::WidgetGUIEvent* aEvent,
nsEventStatus* aEventStatus, bool aControlHeld);

View File

@ -119,6 +119,7 @@ skip-if = (toolkit == "gtk") || (os == "win")
support-files = page_scroll_with_fixed_pos_window.html
[test_scroll_behavior.html]
[test_scrollframe_abspos_interrupt.html]
[test_selection_changes_with_middle_mouse_button.html]
[test_selection_doubleclick.html]
[test_selection_expanding.html]
[test_selection_preventDefault.html]

View File

@ -0,0 +1,211 @@
<!DOCTYPE html>
<html>
<head>
<title>Test for selection changes with middle mouse button</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" />
<style>
span, td {
white-space: nowrap;
}
</style>
</head>
<body>
<p id="display"></p>
<div id="content" style="display: none;">
</div>
<textarea id="textarea" cols="1" rows="1">copied to clipboard</textarea>
<div id="container">
<span id="span1">first span.</span>
<span id="span2">second span.</span>
<table>
<tr><td id="td1">first td.</td></tr>
<tr><td id="td2">second td.</td></tr>
</table>
</div>
<pre id="test">
<script class="testbody" type="application/javascript">
SimpleTest.waitForExplicitFinish();
const kIsMac = navigator.platform.includes("Mac");
const selection = getSelection();
async function doTests(aEnableMiddlePaste, aEditable, aDescription) {
await SpecialPowers.pushPrefEnv({"set": [["middlemouse.paste", aEnableMiddlePaste],
["middlemouse.contentLoadURL", false],
["general.autoScroll", false]]});
if (aEditable) {
document.getElementById("container").setAttribute("contenteditable", "true");
} else {
document.getElementById("container").removeAttribute("contenteditable");
}
let pasteEvents = [];
function pasteEventHandler(event) {
pasteEvents.push(event);
event.preventDefault();
}
document.getElementById("container").addEventListener("paste", pasteEventHandler, true);
// We need to behave as same as Chromium as far as possible.
// - middle mouse button down should be collapse selection at the point.
// - middle mouse button down can expand only with mouse button down with Shift key.
// - middle mouse button shouldn't select table cells.
function doTest(aMouseDown, aMouseUp,
aExpectedSelectionAnchor, aExpectedSelectionFocus, aExpectedPastEventTarget,
aAdditionalDescription) {
pasteEvents = [];
synthesizeMouseAtCenter(aMouseDown.target,
{
button: 1,
type: "mousedown",
shiftKey: aMouseDown.shiftKey,
ctrlKey: aMouseDown.ctrlKey && !kIsMac,
metaKey: aMouseDown.ctrlKey && kIsMac,
});
if (aExpectedSelectionAnchor === aExpectedSelectionFocus) {
ok(selection.isCollapsed,
aDescription + aAdditionalDescription + "Selection should be collapsed at mousedown");
is(selection.focusNode, aExpectedSelectionFocus,
aDescription + aAdditionalDescription + "Selection should be collapsed in the node at mousedown");
} else {
is(selection.anchorNode, aExpectedSelectionAnchor,
aDescription + aAdditionalDescription + "Anchor node of Selection should be previous anchor node");
is(selection.focusNode, aExpectedSelectionFocus,
aDescription + aAdditionalDescription + "Focus node of Selection should be the node at mousedown");
}
is(pasteEvents.length, 0,
aDescription + aAdditionalDescription + "paste event shouldn't be fired when middle mouse button down");
if (aMouseDown.target != aMouseUp.target) {
synthesizeMouseAtCenter(aMouseUp.target, {type: "mousemove"});
}
synthesizeMouseAtCenter(aMouseUp.target,
{
button: 1,
type: "mouseup",
shiftKey: aMouseUp.shiftKey,
ctrlKey: aMouseUp.ctrlKey && !kIsMac,
metaKey: aMouseUp.ctrlKey && kIsMac,
});
is(selection.anchorNode, aExpectedSelectionAnchor,
aDescription + aAdditionalDescription + "Anchor node of Selection shouldn't be modified at mouseup");
is(selection.focusNode, aExpectedSelectionFocus,
aDescription + aAdditionalDescription + "Focus node of Selection shouldn't be modified at mouseup");
if (aEnableMiddlePaste) {
if (aExpectedPastEventTarget === null) {
is(pasteEvents.length, 0,
aDescription + aAdditionalDescription + "paste event shouldn't be fired even when middle mouse button up");
} else {
is(pasteEvents.length, 1,
aDescription + aAdditionalDescription + "paste event should be fired only once at mouse up");
is(pasteEvents[0].target, aExpectedPastEventTarget,
aDescription + aAdditionalDescription + "paste event should be fired on start of selection");
}
} else {
is(pasteEvents.length, 0,
aDescription + aAdditionalDescription + "paste event shouldn't be fired when middle mouse button up when middle mouse paste is disabled");
}
}
let span1 = document.getElementById("span1");
let span2 = document.getElementById("span2");
selection.removeAllRanges();
doTest({target: span1}, {target: span1},
span1.firstChild, span1.firstChild, span1,
"Clicking span1 when there is no selection: ");
doTest({target: span2}, {target: span2},
span2.firstChild, span2.firstChild, span2,
"Clicking span2 when selection is collapsed in span1: ");
doTest({target: span1}, {target: span2},
span1.firstChild, span1.firstChild, span1,
"Dragging from span1 to span2: ");
doTest({target: span2}, {target: span1},
span2.firstChild, span2.firstChild, span2,
"Dragging from span2 to span1: ");
doTest({target: span1, shiftKey: true}, {target: span1, shiftKey: true},
span2.firstChild, span1.firstChild, span1,
"Expanding selection with Shift key from span2 to span1: ");
// "paste" event should be fired in the "start" of selection.
selection.collapse(span1.firstChild, 3);
doTest({target: span2, shiftKey: true}, {target: span2, shiftKey: true},
span1.firstChild, span2.firstChild, span1,
"Expanding selection with Shift key from span1 to span2: ");
// XXX This case is different from Chrome for Linux.
// In this case, Chrome does not collapse Selection at mousedown,
// but collapse at click. So, if mouseup occurs different element,
// Selection isn't modified.
selection.selectAllChildren(span1);
doTest({target: span1}, {target: span1},
span1.firstChild, span1.firstChild, span1,
"Clicking span1 when span1 is selected: ");
let td1 = document.getElementById("td1");
let td2 = document.getElementById("td2");
selection.removeAllRanges();
doTest({target: td1}, {target: td1},
td1.firstChild, td1.firstChild, td1,
"Clicking td1 when there is no selection: ");
if (aEditable) {
// XXX In this case, we don't allow to expand selection with Shift key
// click across table cell boundary.
doTest({target: td2, shiftKey: true}, {target: td2, shiftKey: true},
td1.firstChild, td1.firstChild, td1,
"Expanding selection with Shift key from td1 to td2: ");
} else {
doTest({target: td2, shiftKey: true}, {target: td2, shiftKey: true},
td1.firstChild, td2.firstChild, td1,
"Expanding selection with Shift key from td1 to td2: ");
}
// Shouldn't select per table cell when the button is middle mouse button.
doTest({target: td1, ctrlKey: true}, {target: td1, ctrlKey: true},
td1.firstChild, td1.firstChild, td1,
"Click td1 with Control key: ");
document.getElementById("container").removeEventListener("paste", pasteEventHandler, true);
}
async function runAllTests() {
let textarea = document.getElementById("textarea");
textarea.focus();
await new Promise((resolve, reject) => {
SimpleTest.waitForClipboard(textarea.value,
() => {
synthesizeKey("a", {accelKey: true});
synthesizeKey("c", {accelKey: true});
},
() => {
ok(true, `Succeeded to copy "${textarea.value}" to clipboard`);
textarea.style.display = "none";
resolve();
},
() => {
ok(false, `Failed to copy "${textarea.value}" to clipboard`);
reject();
});
});
await doTests(true, false, "Testing with the middle paste enabled: ");
await doTests(false, false, "Testing with the middle paste disabled: ");
await doTests(true, true, "Testing in editable content with the middle paste enabled: ");
await doTests(false, true, "Testing in editable content with the middle paste disabled: ");
SimpleTest.finish();
}
SimpleTest.waitForFocus(runAllTests);
</script>
</pre>
</body>
</html>

View File

@ -0,0 +1,4 @@
[modifying-selection-with-middle-mouse-button.tentative.html]
[Middle click shouldn't move caret in an editable element if the default of pointerdown event is prevented]
expected: FAIL

View File

@ -0,0 +1,4 @@
[modifying-selection-with-primary-mouse-button.tentative.html]
[Primary click should move caret in an editable element]
expected: FAIL

View File

@ -0,0 +1,192 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Testing default action of `mousedown` of middle button and `mouseup` of middle button</title>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="/resources/testdriver.js"></script>
<script src="/resources/testdriver-vendor.js"></script>
<script src="/resources/testdriver-actions.js"></script>
<style>
span {
white-space: nowrap;
}
</style>
</head>
<body>
<div contenteditable></div>
<script>
"use strict";
var editor = document.querySelector("div[contenteditable]");
var span1, span2;
var selection = getSelection();
function preventDefault(event) {
event.preventDefault();
}
editor.addEventListener("paste", preventDefault, {capture: true});
function resetEditor() {
editor.innerHTML =
'<span id="span1">first span.</span><br><span id="span2">second span.</span>';
span1 = document.getElementById("span1");
span2 = document.getElementById("span2");
}
promise_test(async () => {
resetEditor();
editor.blur();
let actions = new test_driver.Actions();
await actions
.pointerMove(0, 0)
.pointerMove(0, 0, {origin: span1})
.pointerDown({button: actions.ButtonType.MIDDLE})
.pointerUp({button: actions.ButtonType.MIDDLE})
.send();
assert_equals(document.activeElement, editor,
"The clicked editor should get focus");
assert_true(selection.isCollapsed,
"Selection should be collapsed after middle button click");
assert_equals(selection.focusNode, span1.firstChild,
"Selection should be collapsed in the first <span> element which was clicked by middle button");
}, "Middle click should set focus to clicked editable element and collapse selection around the clicked point");
promise_test(async () => {
resetEditor();
editor.focus();
selection.collapse(span1.firstChild, 2);
let actions = new test_driver.Actions();
await actions
.pointerMove(0, 0)
.pointerMove(0, 0, {origin: span2})
.pointerDown({button: actions.ButtonType.MIDDLE})
.pointerUp({button: actions.ButtonType.MIDDLE})
.send();
assert_equals(document.activeElement, editor,
"The clicked editor should keep having focus");
assert_true(selection.isCollapsed,
"Selection should be collapsed after middle button click");
assert_equals(selection.focusNode, span2.firstChild,
"Selection should be collapsed in the second <span> element which was clicked by middle button");
}, "Middle click should move caret in an editable element");
promise_test(async () => {
resetEditor();
editor.focus();
selection.collapse(span1.firstChild, 2);
addEventListener("mousedown", preventDefault);
let actions = new test_driver.Actions();
await actions
.pointerMove(0, 0)
.pointerMove(0, 0, {origin: span2})
.pointerDown({button: actions.ButtonType.MIDDLE})
.pointerUp({button: actions.ButtonType.MIDDLE})
.send();
removeEventListener("mousedown", preventDefault);
assert_equals(selection.focusNode, span1.firstChild,
"Selection should keep collapsed selection in the first <span> element");
assert_equals(selection.focusOffset, 2,
"Selection should keep collapsed selection at 2 of the first <span> element");
}, "Middle click shouldn't move caret in an editable element if the default of mousedown event is prevented");
promise_test(async () => {
resetEditor();
editor.focus();
selection.collapse(span1.firstChild, 2);
addEventListener("pointerdown", preventDefault);
let actions = new test_driver.Actions();
await actions
.pointerMove(0, 0)
.pointerMove(0, 0, {origin: span2})
.pointerDown({button: actions.ButtonType.MIDDLE})
.pointerUp({button: actions.ButtonType.MIDDLE})
.send();
removeEventListener("pointerdown", preventDefault);
assert_equals(selection.focusNode, span1.firstChild,
"Selection should keep collapsed selection in the first <span> element");
assert_equals(selection.focusOffset, 2,
"Selection should keep collapsed selection at 2 of the first <span> element");
}, "Middle click shouldn't move caret in an editable element if the default of pointerdown event is prevented");
promise_test(async () => {
resetEditor();
editor.focus();
selection.collapse(span1.firstChild, 2);
let actions = new test_driver.Actions();
await actions
.pointerMove(0, 0)
.pointerMove(0, 0, {origin: span2})
.keyDown("\uE008")
.pointerDown({button: actions.ButtonType.MIDDLE})
.pointerUp({button: actions.ButtonType.MIDDLE})
.keyUp("\uE008")
.send();
assert_equals(selection.anchorNode, span1.firstChild,
"Selection#anchorNode should keep in the first <span> element");
assert_equals(selection.anchorOffset, 2,
"Selection#anchorNode should keep at 2 of the first <span> element");
assert_equals(selection.focusNode, span2.firstChild,
"Selection#focusNode should be in the second <span> element which was clicked by middle button");
}, "Shift + Middle click should extend the selection");
promise_test(async () => {
resetEditor();
editor.focus();
selection.collapse(span1.firstChild, 2);
editor.addEventListener("pointerdown", () => {
assert_true(selection.isCollapsed,
"Selection shouldn't be modified before pointerdown event");
assert_equals(selection.focusNode, span1.firstChild,
"Selection should stay in the first <span> element when pointerdown event is fired (focusNode)");
assert_equals(selection.focusOffset, 2,
"Selection should stay in the first <span> element when pointerdown event is fired (focusOffset)");
}, {once: true});
editor.addEventListener("mousedown", () => {
assert_true(selection.isCollapsed,
"Selection shouldn't be modified before mousedown event");
assert_equals(selection.focusNode, span1.firstChild,
"Selection should stay in the first <span> element when mousedown event is fired (focusNode)");
assert_equals(selection.focusOffset, 2,
"Selection should stay in the first <span> element when mousedown event is fired (focusOffset)");
}, {once: true});
editor.addEventListener("pointerup", () => {
assert_true(selection.isCollapsed,
"Selection should be collapsed before pointerup event");
assert_equals(selection.focusNode, span2.firstChild,
"Selection should be collapsed in the second <span> element which was clicked by middle button before pointerup event ");
}, {once: true});
let focusOffsetAtMouseUp;
editor.addEventListener("mouseup", () => {
assert_true(selection.isCollapsed,
"Selection should be collapsed before mouseup event");
assert_equals(selection.focusNode, span2.firstChild,
"Selection should be collapsed in the second <span> element which was clicked by middle button before mouseup event ");
focusOffsetAtMouseUp = selection.focusOffset;
}, {once: true});
let actions = new test_driver.Actions();
await actions
.pointerMove(0, 0)
.pointerMove(0, 0, {origin: span2})
.pointerDown({button: actions.ButtonType.MIDDLE})
.pointerMove(0, 0, {origin: span1})
.pointerUp({button: actions.ButtonType.MIDDLE})
.send();
assert_true(selection.isCollapsed,
"Selection shouldn't be extended by pointer moves during pressing middle button");
assert_equals(selection.focusNode, span2.firstChild,
"Selection#focusNode should stay in the second <span> element");
assert_equals(selection.focusOffset, focusOffsetAtMouseUp,
"Selection#focusOffset should stay in the second <span> element");
}, "Middle mouse button down should move caret, but middle mouse button up shouldn't move caret");
</script>
</body>
</html>

View File

@ -0,0 +1,191 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Testing default action of `mousedown` of primary button and `mouseup` of primary button</title>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="/resources/testdriver.js"></script>
<script src="/resources/testdriver-vendor.js"></script>
<script src="/resources/testdriver-actions.js"></script>
<style>
span {
white-space: nowrap;
}
</style>
</head>
<body>
<div contenteditable></div>
<script>
"use strict";
var editor = document.querySelector("div[contenteditable]");
var span1, span2;
var selection = getSelection();
function preventDefault(event) {
event.preventDefault();
}
editor.addEventListener("paste", preventDefault, {capture: true});
function resetEditor() {
editor.innerHTML =
'<span id="span1">first span.</span><br><span id="span2">second span.</span>';
span1 = document.getElementById("span1");
span2 = document.getElementById("span2");
}
promise_test(async () => {
resetEditor();
editor.blur();
let actions = new test_driver.Actions();
await actions
.pointerMove(0, 0)
.pointerMove(0, 0, {origin: span1})
.pointerDown({button: actions.ButtonType.LEFT})
.pointerUp({button: actions.ButtonType.LEFT})
.send();
assert_equals(document.activeElement, editor,
"The clicked editor should get focus");
assert_true(selection.isCollapsed,
"Selection should be collapsed after primary button click");
assert_equals(selection.focusNode, span1.firstChild,
"Selection should be collapsed in the first <span> element which was clicked by primary button");
}, "Primary click should set focus to clicked editable element and collapse selection around the clicked point");
promise_test(async () => {
resetEditor();
editor.focus();
selection.collapse(span1.firstChild, 2);
let actions = new test_driver.Actions();
await actions
.pointerMove(0, 0)
.pointerMove(0, 0, {origin: span2})
.pointerDown({button: actions.ButtonType.LEFT})
.pointerUp({button: actions.ButtonType.LEFT})
.send();
assert_equals(document.activeElement, editor,
"The clicked editor should keep having focus");
assert_true(selection.isCollapsed,
"Selection should be collapsed after primary button click");
assert_equals(selection.focusNode, span2.firstChild,
"Selection should be collapsed in the second <span> element which was clicked by primary button");
}, "Primary click should move caret in an editable element");
promise_test(async () => {
resetEditor();
editor.focus();
selection.collapse(span1.firstChild, 2);
addEventListener("mousedown", preventDefault);
let actions = new test_driver.Actions();
await actions
.pointerMove(0, 0)
.pointerMove(0, 0, {origin: span2})
.pointerDown({button: actions.ButtonType.LEFT})
.pointerUp({button: actions.ButtonType.LEFT})
.send();
removeEventListener("mousedown", preventDefault);
assert_equals(selection.focusNode, span1.firstChild,
"Selection should keep collapsed selection in the first <span> element");
assert_equals(selection.focusOffset, 2,
"Selection should keep collapsed selection at 2 of the first <span> element");
}, "Primary click shouldn't move caret in an editable element if the default of mousedown event is prevented");
promise_test(async () => {
resetEditor();
editor.focus();
selection.collapse(span1.firstChild, 2);
addEventListener("pointerdown", preventDefault);
let actions = new test_driver.Actions();
await actions
.pointerMove(0, 0)
.pointerMove(0, 0, {origin: span2})
.pointerDown({button: actions.ButtonType.LEFT})
.pointerUp({button: actions.ButtonType.LEFT})
.send();
removeEventListener("pointerdown", preventDefault);
assert_equals(selection.focusNode, span1.firstChild,
"Selection should keep collapsed selection in the first <span> element");
assert_equals(selection.focusOffset, 2,
"Selection should keep collapsed selection at 2 of the first <span> element");
}, "Primary click shouldn't move caret in an editable element if the default of pointerdown event is prevented");
promise_test(async () => {
resetEditor();
editor.focus();
selection.collapse(span1.firstChild, 2);
let actions = new test_driver.Actions();
await actions
.pointerMove(0, 0)
.pointerMove(0, 0, {origin: span2})
.keyDown("\uE008")
.pointerDown({button: actions.ButtonType.LEFT})
.pointerUp({button: actions.ButtonType.LEFT})
.keyUp("\uE008")
.send();
assert_equals(selection.anchorNode, span1.firstChild,
"Selection#anchorNode should keep in the first <span> element");
assert_equals(selection.anchorOffset, 2,
"Selection#anchorNode should keep at 2 of the first <span> element");
assert_equals(selection.focusNode, span2.firstChild,
"Selection#focusNode should be in the second <span> element which was clicked by primary button");
}, "Shift + Primary click should extend the selection");
promise_test(async () => {
resetEditor();
editor.focus();
selection.collapse(span1.firstChild, 2);
editor.addEventListener("pointerdown", () => {
assert_true(selection.isCollapsed,
"Selection shouldn't be modified before pointerdown event");
assert_equals(selection.focusNode, span1.firstChild,
"Selection should stay in the first <span> element when pointerdown event is fired (focusNode)");
assert_equals(selection.focusOffset, 2,
"Selection should stay in the first <span> element when pointerdown event is fired (focusOffset)");
}, {once: true});
editor.addEventListener("mousedown", () => {
assert_true(selection.isCollapsed,
"Selection shouldn't be modified before mousedown event");
assert_equals(selection.focusNode, span1.firstChild,
"Selection should stay in the first <span> element when mousedown event is fired (focusNode)");
assert_equals(selection.focusOffset, 2,
"Selection should stay in the first <span> element when mousedown event is fired (focusOffset)");
}, {once: true});
editor.addEventListener("pointerup", () => {
assert_true(!selection.isCollapsed,
"Selection should be modified before pointerup event");
assert_equals(selection.focusNode, span1.firstChild,
"Selection should be modified to extend the range before pointerup event ");
}, {once: true});
let anchorOffsetAtMouseUp;
editor.addEventListener("mouseup", () => {
assert_true(!selection.isCollapsed,
"Selection should be modified before mouseup event");
assert_equals(selection.focusNode, span1.firstChild,
"Selection should be modified to extend the range before mouseup event ");
anchorOffsetAtMouseUp = selection.anchorOffset;
}, {once: true});
let actions = new test_driver.Actions();
await actions
.pointerMove(0, 0)
.pointerMove(0, 0, {origin: span2})
.pointerDown({button: actions.ButtonType.LEFT})
.pointerMove(0, 0, {origin: span1})
.pointerUp({button: actions.ButtonType.LEFT})
.send();
assert_equals(selection.anchorNode, span2.firstChild,
"Selection#anchorNode should stay in the second <span> element which mousedown occurred on");
assert_equals(selection.anchorOffset, anchorOffsetAtMouseUp,
"Selection#anchorOffset should stay in the second <span> element which mousedown occurred on");
assert_equals(selection.focusNode, span1.firstChild,
"Selection#focusNode should be in the first <span> element which mouseup occurred on");
}, "Primary mouse button down should move caret, and primary mouse button up should extend the selection");
</script>
</body>
</html>