Bug 1196479 - Fire selectstart and selectionchange events on the input node when the selection in that editor changes. r=ehsan

This commit is contained in:
Michael Layzell 2015-09-02 13:39:39 -04:00
parent d5934c389f
commit 931d0d32a7
5 changed files with 235 additions and 17 deletions

View File

@ -93,6 +93,9 @@ public:
return NS_OK;
}
AutoHideSelectionChanges hideSelectionChanges
(mFrame->GetConstFrameSelection());
if (mFrame) {
// SetSelectionRange leads to Selection::AddRange which flushes Layout -
// need to block script to avoid nested PrepareEditor calls (bug 642800).
@ -1248,6 +1251,8 @@ nsTextEditorState::PrepareEditor(const nsAString *aValue)
return NS_OK;
}
AutoHideSelectionChanges hideSelectionChanges(GetConstFrameSelection());
// Don't attempt to initialize recursively!
InitializationGuard guard(*this);
if (guard.IsInitializingRecursively()) {

View File

@ -15,6 +15,10 @@
This is a random block of text
</div>
<input type="text" id="input" value="XXXXXXXXXXXXXXXXXXX" width="200"> <br>
<textarea id="textarea" width="200">XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX</textarea>
<script>
// Call the testing methods from the parent window
var is = parent.is;
@ -36,6 +40,9 @@
var selectstart = 0;
var selectionchange = 0;
var inputSelectionchange = 0;
var textareaSelectionchange = 0;
var cancel = false;
var selectstartTarget = null;
@ -60,34 +67,69 @@
});
function elt(aId) { return document.getElementById(aId); }
function reset() { selectstart = 0; selectionchange = 0; cancel = false; }
function reset() {
selectstart = 0;
selectionchange = 0;
inputSelectionchange = 0;
textareaSelectionchange = 0;
cancel = false;
}
function* mouseAction(aElement, aOffset, aType, aSelStart, aSelChng) {
elt("input").addEventListener('selectionchange', function(aEvent) {
is (aEvent.originalTarget, elt("input"),
"The original target of selectionchange should be the input");
console.log(inputSelectionchange);
inputSelectionchange++;
});
elt("textarea").addEventListener('selectionchange', function(aEvent) {
is (aEvent.originalTarget, elt("textarea"),
"The original target of selectionchange should be the textarea");
console.log(textareaSelectionchange);
textareaSelectionchange++;
});
function* mouseAction(aElement, aOffset, aType,
aSelStart, aSelChng, aISelChng, aTSelChng)
{
if (aType == "click") { // You can simulate a click event by sending undefined
aType = undefined;
}
synthesizeMouse(aElement, aOffset, 10, { type: aType });
yield spin();
is(selectstart, aSelStart,
"SelStart Mouse Action (" + aOffset + " - " + aType + ")");
is(selectionchange, aSelChng,
"SelChng Mouse Action (" + aOffset + " - " + aType + ")");
is(inputSelectionchange, aISelChng || 0,
"ISelChng Mouse Action (" + aOffset + " - " + aType + ")");
is(textareaSelectionchange, aTSelChng || 0,
"TSelChng Mouse Action (" + aOffset + " - " + aType + ")");
reset();
}
function* keyAction(aKey, aShift, aAccel, aSelStart, aSelChng) {
function* keyAction(aKey, aShift, aAccel,
aSelStart, aSelChng, aISelChng, aTSelChng)
{
synthesizeKey(aKey, { shiftKey: aShift, accelKey: aAccel });
yield spin();
is(selectstart, aSelStart,
"SelStart Key Action (" + aKey + " - " + aShift + " - " + aAccel + ")");
is(selectionchange, aSelChng,
"SelChng Key Action (" + aKey + " - " + aShift + " - " + aAccel + ")");
is(inputSelectionchange, aISelChng || 0,
"ISelChng Key Action (" + aKey + " - " + aShift + " - " + aAccel + ")");
is(textareaSelectionchange, aTSelChng || 0,
"TSelChng Key Action (" + aKey + " - " + aShift + " - " + aAccel + ")");
reset();
}
var selection = document.getSelection();
function isCollapsed() { is(selection.isCollapsed, true, "Selection is collapsed"); }
function isNotCollapsed() { is(selection.isCollapsed, false, "Selection is not collapsed"); }
function isCollapsed() {
is(selection.isCollapsed, true, "Selection is collapsed");
}
function isNotCollapsed() {
is(selection.isCollapsed, false, "Selection is not collapsed");
}
// Focus the contenteditable text
yield* mouseAction(elt("ce"), 100, "click", 0, 1);
@ -229,6 +271,139 @@
is(selectionchange, 1, "Synthesized range mutations should change selectionchange");
reset();
isNotCollapsed();
// Remove the range
s.removeAllRanges();
yield spin();
is(selectstart, 0, "Synthesized range removal");
is(selectionchange, 1, "Synthesized range removal");
reset();
isCollapsed();
/*
Selection events mouse move on input type=text
*/
// Select a region
yield* mouseAction(elt("input"), 50, "mousedown", 0, 1, 1, 0);
selectstartTarget = elt("input");
yield* mouseAction(elt("input"), 100, "mousemove", 1, 0, 1, 0);
// Moving it more shouldn't trigger a start (move back to empty)
yield* mouseAction(elt("input"), 75, "mousemove", 0, 0, 1, 0);
yield* mouseAction(elt("input"), 50, "mousemove", 0, 0, 1, 0);
// Wiggling the mouse a little such that it doesn't select any
// characters shouldn't trigger a selection
yield* mouseAction(elt("input"), 49, "mousemove", 0, 0, 0, 0);
// Moving the mouse again from an empty selection should trigger a
// selectstart
selectstartTarget = elt("input");
yield* mouseAction(elt("input"), 25, "mousemove", 1, 0, 1, 0);
// Releasing the mouse shouldn't do anything
yield* mouseAction(elt("input"), 25, "mouseup", 0, 0, 0, 0);
// And neither should moving your mouse around when the mouse
// button isn't pressed
yield* mouseAction(elt("input"), 50, "mousemove", 0, 0, 0, 0);
// Clicking in an random location should move the selection, but
// not perform a selectstart
yield* mouseAction(elt("input"), 50, "click", 0, 0, 1, 0);
// Clicking there again should do nothing
yield* mouseAction(elt("input"), 50, "click", 0, 0, 0, 0);
// Selecting a region, and canceling the selectstart should mean that the
// selection remains collapsed
yield* mouseAction(elt("input"), 75, "mousedown", 0, 0, 1, 0);
cancel = true;
selectstartTarget = elt("input");
yield* mouseAction(elt("input"), 100, "mousemove", 1, 0, 1, 0);
yield* mouseAction(elt("input"), 100, "mouseup", 0, 0, 0, 0);
// Select a region
// XXX For some reason we fire 2 selectchange events on the body
// when switching from the input to the text area.
yield* mouseAction(elt("textarea"), 50, "mousedown", 0, 2, 0, 1);
selectstartTarget = elt("textarea");
yield* mouseAction(elt("textarea"), 100, "mousemove", 1, 0, 0, 1);
// Moving it more shouldn't trigger a start (move back to empty)
yield* mouseAction(elt("textarea"), 75, "mousemove", 0, 0, 0, 1);
yield* mouseAction(elt("textarea"), 50, "mousemove", 0, 0, 0, 1);
// Wiggling the mouse a little such that it doesn't select any
// characters shouldn't trigger a selection
yield* mouseAction(elt("textarea"), 49, "mousemove", 0, 0, 0, 0);
// Moving the mouse again from an empty selection should trigger a
// selectstart
selectstartTarget = elt("textarea");
yield* mouseAction(elt("textarea"), 25, "mousemove", 1, 0, 0, 1);
// Releasing the mouse shouldn't do anything
yield* mouseAction(elt("textarea"), 25, "mouseup", 0, 0, 0, 0);
// And neither should moving your mouse around when the mouse
// button isn't pressed
yield* mouseAction(elt("textarea"), 50, "mousemove", 0, 0, 0, 0);
// Clicking in an random location should move the selection, but not perform a
// selectstart
yield* mouseAction(elt("textarea"), 50, "click", 0, 0, 0, 1);
// Clicking there again should do nothing
yield* mouseAction(elt("textarea"), 50, "click", 0, 0, 0, 0);
// Selecting a region, and canceling the selectstart should mean that the
// selection remains collapsed
yield* mouseAction(elt("textarea"), 75, "mousedown", 0, 0, 0, 1);
cancel = true;
selectstartTarget = elt("textarea");
yield* mouseAction(elt("textarea"), 100, "mousemove", 1, 0, 0, 1);
yield* mouseAction(elt("textarea"), 100, "mouseup", 0, 0, 0, 0);
// Marking the input and textarea as display: none and then as visible again
// shouldn't trigger any changes, although the nodes will be re-framed
elt("input").setAttribute("style", "display: none;");
yield spin();
is(selectstart, 0, "idn - ss 1");
is(selectionchange, 0, "idn - sc 1");
is(inputSelectionchange, 0, "idn - isc 1");
is(textareaSelectionchange, 0, "idn - tsc 1");
reset();
elt("input").setAttribute("style", "");
yield spin();
is(selectstart, 0, "idn - ss 2");
is(selectionchange, 0, "idn - sc 2");
is(inputSelectionchange, 0, "idn - isc 2");
is(textareaSelectionchange, 0, "idn - tsc 2");
reset();
elt("textarea").setAttribute("style", "display: none;");
yield spin();
is(selectstart, 0, "tdn - ss 1");
is(selectionchange, 0, "tdn - sc 1");
is(inputSelectionchange, 0, "tdn - isc 1");
is(textareaSelectionchange, 0, "tdn - tsc 1");
reset();
elt("textarea").setAttribute("style", "");
yield spin();
is(selectstart, 0, "tdn - ss 2");
is(selectionchange, 0, "tdn - sc 2");
is(inputSelectionchange, 0, "tdn - isc 2");
is(textareaSelectionchange, 0, "tdn - tsc 2");
reset();
});
</script>
</body>

View File

@ -152,8 +152,6 @@ partial interface Document {
attribute EventHandler onbeforescriptexecute;
attribute EventHandler onafterscriptexecute;
[Pref="dom.select_events.enabled"]
attribute EventHandler onselectionchange;
/**
* True if this document is synthetic : stand alone image, video, audio file,
* etc.

View File

@ -46,6 +46,7 @@
#include "nsStyleSet.h"
#include "mozilla/dom/ScriptSettings.h"
#include "mozilla/MathAlgorithms.h"
#include "nsFrameSelection.h"
#define DEFAULT_COLUMN_WIDTH 20
@ -259,6 +260,13 @@ nsTextControlFrame::EnsureEditorInitialized()
// Make sure that editor init doesn't do things that would kill us off
// (especially off the script blockers it'll create for its DOM mutations).
{
nsCOMPtr<nsITextControlElement> txtCtrl = do_QueryInterface(GetContent());
MOZ_ASSERT(txtCtrl, "Content not a text control element");
// Hide selection changes during the initialization, as webpages should not
// be aware of these initializations
AutoHideSelectionChanges hideSelectionChanges(txtCtrl->GetConstFrameSelection());
nsAutoScriptBlocker scriptBlocker;
// Time to mess with our security context... See comments in GetValue()
@ -288,8 +296,6 @@ nsTextControlFrame::EnsureEditorInitialized()
#endif
// Create an editor for the frame, if one doesn't already exist
nsCOMPtr<nsITextControlElement> txtCtrl = do_QueryInterface(GetContent());
NS_ASSERTION(txtCtrl, "Content not a text control element");
nsresult rv = txtCtrl->CreateEditor();
NS_ENSURE_SUCCESS(rv, rv);
NS_ENSURE_STATE(weakFrame.IsAlive());

View File

@ -3725,8 +3725,14 @@ Selection::AddItem(nsRange* aItem, int32_t* aOutIndex, bool aNoStartSelect)
// and the selection is becoming uncollapsed, and this is caused by a user
// initiated event.
bool defaultAction = true;
nsContentUtils::DispatchTrustedEvent(GetParentObject(),
aItem->GetStartParent(),
// Get the first element which isn't in a native anonymous subtree
nsCOMPtr<nsINode> target = aItem->GetStartParent();
while (target && target->IsInNativeAnonymousSubtree()) {
target = target->GetParent();
}
nsContentUtils::DispatchTrustedEvent(GetParentObject(), target,
NS_LITERAL_STRING("selectstart"),
true, true, &defaultAction);
@ -6377,7 +6383,6 @@ NS_INTERFACE_MAP_END
NS_IMPL_CYCLE_COLLECTING_ADDREF(SelectionChangeListener)
NS_IMPL_CYCLE_COLLECTING_RELEASE(SelectionChangeListener)
NS_IMETHODIMP
SelectionChangeListener::NotifySelectionChanged(nsIDOMDocument* aDoc,
nsISelection* aSel, int16_t aReason)
@ -6387,7 +6392,8 @@ SelectionChangeListener::NotifySelectionChanged(nsIDOMDocument* aDoc,
nsRefPtr<Selection> sel = static_cast<Selection*>(aSel);
// Check if the ranges have actually changed
if (mOldRanges.Length() == sel->RangeCount()) {
// Don't bother checking this if we are hiding changes.
if (mOldRanges.Length() == sel->RangeCount() && !sel->IsBlockingSelectionChangeEvents()) {
bool changed = false;
for (size_t i = 0; i < mOldRanges.Length(); i++) {
@ -6408,11 +6414,39 @@ SelectionChangeListener::NotifySelectionChanged(nsIDOMDocument* aDoc,
mOldRanges.AppendElement(RawRangeData(sel->GetRangeAt(i)));
}
// Actually fire off the event
nsCOMPtr<nsIDocument> doc = do_QueryInterface(aDoc);
if (doc) {
// If we are hiding changes, then don't do anything else. We do this after we
// update mOldRanges so that changes after the changes stop being hidden don't
// incorrectly trigger a change, even though they didn't change anything
if (sel->IsBlockingSelectionChangeEvents()) {
return NS_OK;
}
nsCOMPtr<nsINode> target;
// Check if we should be firing this event to a different node than the
// document. The limiter of the nsFrameSelection will be within the native
// anonymous subtree of the node we want to fire the event on. We need to
// climb up the parent chain to escape the native anonymous subtree, and then
// fire the event.
if (nsFrameSelection* fs = sel->GetFrameSelection()) {
if (nsCOMPtr<nsIContent> root = fs->GetLimiter()) {
while (root && root->IsInNativeAnonymousSubtree()) {
root = root->GetParent();
}
target = root.forget();
}
}
// If we didn't get a target before, we can instead fire the event at the document.
if (!target) {
nsCOMPtr<nsIDocument> doc = do_QueryInterface(aDoc);
target = doc.forget();
}
if (target) {
nsRefPtr<AsyncEventDispatcher> asyncDispatcher =
new AsyncEventDispatcher(doc, NS_LITERAL_STRING("selectionchange"), false);
new AsyncEventDispatcher(target, NS_LITERAL_STRING("selectionchange"), false);
asyncDispatcher->PostDOMEvent();
}