--- a/dom/events/InputEvent.cpp
+++ b/dom/events/InputEvent.cpp
@@ -36,30 +36,37 @@ NS_INTERFACE_MAP_BEGIN(InputEvent)
NS_INTERFACE_MAP_END_INHERITING(UIEvent)
bool
InputEvent::IsComposing()
{
return mEvent->AsEditorInputEvent()->mIsComposing;
}
+void
+InputEvent::GetData(nsAString& aData)
+{
+ aData = mEvent->AsEditorInputEvent()->mData;
+}
+
already_AddRefed<InputEvent>
InputEvent::Constructor(const GlobalObject& aGlobal,
const nsAString& aType,
const InputEventInit& aParam,
ErrorResult& aRv)
{
nsCOMPtr<EventTarget> t = do_QueryInterface(aGlobal.GetAsSupports());
RefPtr<InputEvent> e = new InputEvent(t, nullptr, nullptr);
bool trusted = e->Init(t);
auto* view = aParam.mView ? aParam.mView->AsInner() : nullptr;
e->InitUIEvent(aType, aParam.mBubbles, aParam.mCancelable, view,
aParam.mDetail);
InternalEditorInputEvent* internalEvent = e->mEvent->AsEditorInputEvent();
internalEvent->mIsComposing = aParam.mIsComposing;
+ internalEvent->mData = aParam.mData;
e->SetTrusted(trusted);
e->SetComposed(aParam.mComposed);
return e.forget();
}
} // namespace dom
} // namespace mozilla
--- a/dom/events/InputEvent.h
+++ b/dom/events/InputEvent.h
@@ -33,16 +33,17 @@ public:
ErrorResult& aRv);
virtual JSObject* WrapObjectInternal(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override
{
return InputEventBinding::Wrap(aCx, this, aGivenProto);
}
bool IsComposing();
+ void GetData(nsAString& aData);
protected:
~InputEvent() {}
};
} // namespace dom
} // namespace mozilla
--- a/dom/webidl/InputEvent.webidl
+++ b/dom/webidl/InputEvent.webidl
@@ -2,15 +2,17 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/.
*/
[Constructor(DOMString type, optional InputEventInit eventInitDict)]
interface InputEvent : UIEvent
{
+ readonly attribute DOMString? data;
readonly attribute boolean isComposing;
};
dictionary InputEventInit : UIEventInit
{
+ DOMString? data = "";
boolean isComposing = false;
};
--- a/editor/libeditor/EditorBase.cpp
+++ b/editor/libeditor/EditorBase.cpp
@@ -1885,21 +1885,23 @@ EditorBase::RemoveEditorObserver(nsIEdit
return NS_OK;
}
class EditorInputEventDispatcher final : public Runnable
{
public:
EditorInputEventDispatcher(EditorBase* aEditorBase,
nsIContent* aTarget,
- bool aIsComposing)
+ bool aIsComposing,
+ const nsAString& aInsertedString)
: Runnable("EditorInputEventDispatcher")
, mEditorBase(aEditorBase)
, mTarget(aTarget)
, mIsComposing(aIsComposing)
+ , mInsertedString(aInsertedString)
{
}
NS_IMETHOD Run() override
{
// Note that we don't need to check mDispatchInputEvent here. We need
// to check it only when the editor requests to dispatch the input event.
@@ -1917,27 +1919,29 @@ public:
return NS_OK;
}
// Even if the change is caused by untrusted event, we need to dispatch
// trusted input event since it's a fact.
InternalEditorInputEvent inputEvent(true, eEditorInput, widget);
inputEvent.mTime = static_cast<uint64_t>(PR_Now() / 1000);
inputEvent.mIsComposing = mIsComposing;
+ inputEvent.mData = mInsertedString;
nsEventStatus status = nsEventStatus_eIgnore;
nsresult rv =
ps->HandleEventWithTarget(&inputEvent, nullptr, mTarget, &status);
NS_ENSURE_SUCCESS(rv, NS_OK); // print the warning if error
return NS_OK;
}
private:
RefPtr<EditorBase> mEditorBase;
nsCOMPtr<nsIContent> mTarget;
bool mIsComposing;
+ nsString mInsertedString;
};
void
EditorBase::NotifyEditorObservers(NotificationForEditorObservers aNotification)
{
// Copy the observers since EditAction()s can modify mEditorObservers.
AutoEditorObserverArray observers(mEditorObservers);
switch (aNotification) {
@@ -1984,17 +1988,20 @@ EditorBase::FireInputEvent()
nsCOMPtr<nsIContent> target = GetInputEventTargetContent();
NS_ENSURE_TRUE_VOID(target);
// NOTE: Don't refer IsIMEComposing() because it returns false even before
// compositionend. However, DOM Level 3 Events defines it should be
// true after compositionstart and before compositionend.
nsContentUtils::AddScriptRunner(
- new EditorInputEventDispatcher(this, target, !!GetComposition()));
+ new EditorInputEventDispatcher(this, target, !!GetComposition(),
+ mInsertedString));
+
+ mInsertedString.SetIsVoid(true);
}
NS_IMETHODIMP
EditorBase::AddEditActionListener(nsIEditActionListener* aListener)
{
NS_ENSURE_TRUE(aListener, NS_ERROR_NULL_POINTER);
// Make sure the listener isn't already on the list
@@ -2547,16 +2554,19 @@ EditorBase::InsertTextImpl(const nsAStri
// then we insert it into the dom tree
nsresult rv = InsertNode(*newNode, *node, offset);
NS_ENSURE_SUCCESS(rv, rv);
node = newNode;
offset = lengthToInsert.value();
}
}
+ // XXX Won't work if more text is inserted before FireInputEvent
+ mInsertedString = aStringToInsert;
+
*aInOutNode = node;
*aInOutOffset = offset;
return NS_OK;
}
nsresult
EditorBase::InsertTextIntoTextNodeImpl(const nsAString& aStringToInsert,
Text& aTextNode,
--- a/editor/libeditor/EditorBase.h
+++ b/editor/libeditor/EditorBase.h
@@ -1202,16 +1202,18 @@ protected:
typedef AutoTArray<OwningNonNull<nsIDocumentStateListener>, 1>
AutoDocumentStateListenerArray;
AutoDocumentStateListenerArray mDocStateListeners;
// Cached selection for AutoSelectionRestorer.
SelectionState mSavedSel;
// Utility class object for maintaining preserved ranges.
RangeUpdater mRangeUpdater;
+ // What string we inserted, if any, since the last input event.
+ nsString mInsertedString;
// Number of modifications (for undo/redo stack).
uint32_t mModCount;
// Behavior flags. See nsIPlaintextEditor.idl for the flags we use.
uint32_t mFlags;
int32_t mUpdateCount;
--- a/editor/libeditor/HTMLEditor.cpp
+++ b/editor/libeditor/HTMLEditor.cpp
@@ -1148,16 +1148,18 @@ HTMLEditor::InsertBR(nsCOMPtr<nsIDOMNode
int32_t selOffset;
nsresult rv =
GetStartNodeAndOffset(selection, getter_AddRefs(selNode), &selOffset);
NS_ENSURE_SUCCESS(rv, rv);
rv = CreateBR(selNode, selOffset, outBRNode);
NS_ENSURE_SUCCESS(rv, rv);
+ mInsertedString = NS_LITERAL_STRING("\n");
+
// position selection after br
selNode = GetNodeLocation(*outBRNode, &selOffset);
selection->SetInterlinePosition(true);
return selection->Collapse(selNode, selOffset+1);
}
void
HTMLEditor::CollapseSelectionToDeepestNonTableFirstChild(Selection* aSelection,
--- a/editor/libeditor/HTMLEditorDataTransfer.cpp
+++ b/editor/libeditor/HTMLEditorDataTransfer.cpp
@@ -1189,16 +1189,20 @@ HTMLEditor::InsertFromTransferable(nsITr
rv = DoInsertHTMLWithContext(cffragment,
cfcontext, cfselection, flavor,
aSourceDoc,
aDestinationNode, aDestOffset,
aDoDeleteSelection,
isSafe);
}
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ mInsertedString = cffragment;
} else {
// In some platforms (like Linux), the clipboard might return data
// requested for unknown flavors (for example:
// application/x-moz-nativehtml). In this case, treat the data
// to be pasted as mere HTML to get the best chance of pasting it
// correctly.
bestFlavor.AssignLiteral(kHTMLMime);
// Fall through the next case
@@ -1231,16 +1235,20 @@ HTMLEditor::InsertFromTransferable(nsITr
aContextStr, aInfoStr, flavor,
aSourceDoc,
aDestinationNode, aDestOffset,
aDoDeleteSelection,
isSafe);
} else {
rv = InsertTextAt(stuffToPaste, aDestinationNode, aDestOffset, aDoDeleteSelection);
}
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ mInsertedString = stuffToPaste;
}
}
}
// Try to scroll the selection into view if the paste succeeded
if (NS_SUCCEEDED(rv)) {
ScrollSelectionIntoView(false);
}
--- a/editor/libeditor/TextEditor.cpp
+++ b/editor/libeditor/TextEditor.cpp
@@ -721,16 +721,18 @@ TextEditor::InsertLineBreak()
// insert a linefeed character
rv = InsertTextImpl(NS_LITERAL_STRING("\n"), address_of(selNode),
&selOffset, doc);
if (!selNode) {
rv = NS_ERROR_NULL_POINTER; // don't return here, so DidDoAction is called
}
if (NS_SUCCEEDED(rv)) {
+ mInsertedString = NS_LITERAL_STRING("\n");
+
// set the selection to the correct location
rv = selection->Collapse(selNode, selOffset);
if (NS_SUCCEEDED(rv)) {
// see if we're at the end of the editor range
nsCOMPtr<nsIDOMNode> endNode;
int32_t endOffset;
rv = GetEndNodeAndOffset(selection,
getter_AddRefs(endNode), &endOffset);
--- a/editor/libeditor/TextEditorDataTransfer.cpp
+++ b/editor/libeditor/TextEditorDataTransfer.cpp
@@ -102,44 +102,51 @@ TextEditor::InsertTextFromTransferable(n
nsIDOMNode* aDestinationNode,
int32_t aDestOffset,
bool aDoDeleteSelection)
{
nsresult rv = NS_OK;
nsAutoCString bestFlavor;
nsCOMPtr<nsISupports> genericDataObj;
uint32_t len = 0;
- if (NS_SUCCEEDED(
+ if (NS_FAILED(
aTransferable->GetAnyTransferData(bestFlavor,
getter_AddRefs(genericDataObj),
- &len)) &&
- (bestFlavor.EqualsLiteral(kUnicodeMime) ||
- bestFlavor.EqualsLiteral(kMozTextInternal))) {
- AutoTransactionsConserveSelection dontChangeMySelection(this);
- nsCOMPtr<nsISupportsString> textDataObj ( do_QueryInterface(genericDataObj) );
- if (textDataObj && len > 0) {
- nsAutoString stuffToPaste;
- textDataObj->GetData(stuffToPaste);
- NS_ASSERTION(stuffToPaste.Length() <= (len/2), "Invalid length!");
+ &len)) ||
+ (!bestFlavor.EqualsLiteral(kUnicodeMime) &&
+ !bestFlavor.EqualsLiteral(kMozTextInternal)) ||
+ len == 0) {
+ return NS_OK;
+ }
- // Sanitize possible carriage returns in the string to be inserted
- nsContentUtils::PlatformToDOMLineBreaks(stuffToPaste);
-
- AutoEditBatch beginBatching(this);
- rv = InsertTextAt(stuffToPaste, aDestinationNode, aDestOffset, aDoDeleteSelection);
- }
+ AutoTransactionsConserveSelection dontChangeMySelection(this);
+ nsCOMPtr<nsISupportsString> textDataObj(do_QueryInterface(genericDataObj));
+ if (!textDataObj) {
+ return NS_OK;
}
- // Try to scroll the selection into view if the paste/drop succeeded
+ nsAutoString stuffToPaste;
+ textDataObj->GetData(stuffToPaste);
+ NS_ASSERTION(stuffToPaste.Length() <= (len/2), "Invalid length!");
- if (NS_SUCCEEDED(rv)) {
- ScrollSelectionIntoView(false);
+ // Sanitize possible carriage returns in the string to be inserted
+ nsContentUtils::PlatformToDOMLineBreaks(stuffToPaste);
+
+ AutoEditBatch beginBatching(this);
+ rv = InsertTextAt(stuffToPaste, aDestinationNode, aDestOffset, aDoDeleteSelection);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
}
- return rv;
+ mInsertedString = stuffToPaste;
+
+ // Try to scroll the selection into view if the paste/drop succeeded
+ ScrollSelectionIntoView(false);
+
+ return NS_OK;
}
nsresult
TextEditor::InsertFromDataTransfer(DataTransfer* aDataTransfer,
int32_t aIndex,
nsIDOMDocument* aSourceDoc,
nsIDOMNode* aDestinationNode,
int32_t aDestOffset,
--- a/editor/libeditor/tests/mochitest.ini
+++ b/editor/libeditor/tests/mochitest.ini
@@ -256,16 +256,17 @@ subsuite = clipboard
[test_documentCharacterSet.html]
[test_dom_input_event_on_htmleditor.html]
skip-if = toolkit == 'android' # bug 1054087
[test_dom_input_event_on_texteditor.html]
[test_dragdrop.html]
skip-if = os == 'android'
[test_inline_style_cache.html]
[test_inlineTableEditing.html]
+[test_input_event.html]
[test_keypress_untrusted_event.html]
[test_objectResizing.html]
[test_root_element_replacement.html]
[test_select_all_without_body.html]
[test_spellcheck_pref.html]
skip-if = toolkit == 'android'
[test_backspace_vs.html]
[test_css_chrome_load_access.html]
new file mode 100644
--- /dev/null
+++ b/editor/libeditor/tests/test_input_event.html
@@ -0,0 +1,122 @@
+<!doctype html>
+<title>input and beforeInput events</title>
+<style src="/tests/SimpleTest/test.css"></style>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src="/tests/SimpleTest/EventUtils.js"></script>
+<input>
+<textarea></textarea>
+<div contenteditable></div>
+<script>
+// XXX Forked from wpt test because it doesn't support synthesized events,
+// rejoin once they do
+function sharedInputAsserts(e, element, expectedData, desc) {
+ is(Object.getPrototypeOf(e), InputEvent.prototype, `${desc}: Event object prototype`);
+ is(e.data, expectedData, `${desc}: .data`);
+ is(e.bubbles, true, `${desc}: .bubbles`);
+ is(e.composed, true, `${desc}: .composed`);
+ is(e.target, element, `${desc}: .target`);
+ is(e.view, window, `${desc}: .view`);
+ is(e.detail, 0, `${desc}: .detail`);
+ is(e.isComposing, false, `${desc}: .isComposing`);
+}
+
+// Preload clipboard for paste tests
+var textNode = document.createTextNode("abc");
+document.body.appendChild(textNode);
+getSelection().setBaseAndExtent(textNode, 0, textNode, 3);
+SpecialPowers.wrap(document).execCommand("copy");
+
+const elements = new Map([
+ ["input", document.querySelector("input")],
+ ["textarea", document.querySelector("textarea")],
+ ["contentEditable div", document.querySelector("div")],
+]);
+
+const callbacks = new Map([
+ ['Type "a"', [() => synthesizeKey("a", {}), "a"]],
+ ['Return', [() => synthesizeKey("VK_RETURN", {}),
+ element => element.tagName == "INPUT" ? undefined : "\n"]],
+ ['Shift-Return', [() => synthesizeKey("VK_RETURN", {shiftKey: true}),
+ element => element.tagName == "INPUT" ? undefined : "\n"]],
+ ['Paste "abc" via execCommand',
+ [() => SpecialPowers.wrap(document).execCommand("paste"),
+ element => element.tagName == "DIV" ? "abc" : undefined]],
+ ['Paste "abc" via Ctrl-V',
+ [() => synthesizeKey("V", {ctrlKey: true}), "abc"]],
+]);
+
+let testsRun = 0;
+let totalTests = elements.size * callbacks.size;
+console.log(`total: ${totalTests}`);
+
+function runTests(element, callback, expectedData, desc) {
+ element.focus();
+ console.log(desc);
+ let beforeInputFired = false;
+ let inputFired = false;
+ if (typeof expectedData == "function") {
+ expectedData = expectedData(element);
+ }
+
+ let beforeInputHandler = e => {
+ ok(!beforeInputFired, `${desc}: beforeinput must fire only once`);
+ beforeInputFired = true;
+ ok(!inputFired, `${desc}: input event must not fire before beforeinput`);
+ if (expectedData !== undefined) {
+ sharedInputAsserts(e, element, expectedData, desc);
+ is(e.cancelable, true, `${desc}: .cancelable`);
+ }
+ };
+
+ let inputHandler = e => {
+ ok(!inputFired, `${desc}: input must fire only once`);
+ inputFired = true;
+ if (expectedData !== undefined) {
+ if (!beforeInputFired) {
+ todo(false, `${desc}: beforeinput not fired before input`);
+ }
+ sharedInputAsserts(e, element, expectedData, desc);
+ is(e.cancelable, false, `${desc}: .cancelable`);
+ testsRun++;
+ console.log(`${desc}: ${testsRun}, fired`);
+ if (testsRun == totalTests) {
+ SimpleTest.finish();
+ }
+ }
+ };
+
+ element.addEventListener("beforeinput", beforeInputHandler);
+ element.addEventListener("input", inputHandler);
+
+ callback(element);
+
+ element.removeEventListener("beforeinput", beforeInputHandler);
+ element.removeEventListener("input", inputHandler);
+
+ if (expectedData === undefined) {
+ ok(!beforeInputFired, `${desc}: No beforeinput event expected`);
+ ok(!inputFired, `${desc}: No input event expected`);
+ testsRun++;
+ console.log(`${desc}: ${testsRun}, not fired`);
+ if (testsRun == totalTests) {
+ SimpleTest.finish();
+ }
+ }
+
+ element.textContent = "";
+ if ("value" in element) {
+ element.value = "";
+ }
+}
+
+SimpleTest.waitForExplicitFinish();
+
+SimpleTest.waitForFocus(() => {
+ for (const [elementDesc, element] of elements) {
+ for (const [callbackDesc, [callback, expectedData]] of callbacks) {
+ runTests(element, callback, expectedData,
+ `${callbackDesc} on ${elementDesc}`);
+ }
+ }
+});
+</script>
--- a/testing/web-platform/meta/MANIFEST.json
+++ b/testing/web-platform/meta/MANIFEST.json
@@ -375056,16 +375056,22 @@
]
],
"uievents/constructors/inputevent-constructor.html": [
[
"/uievents/constructors/inputevent-constructor.html",
{}
]
],
+ "uievents/inputEvents.html": [
+ [
+ "/uievents/inputEvents.html",
+ {}
+ ]
+ ],
"uievents/interface/click-event.htm": [
[
"/uievents/interface/click-event.htm",
{}
]
],
"uievents/legacy-domevents-tests/approved/ProcessingInstruction.DOMCharacterDataModified.html": [
[
--- a/testing/web-platform/meta/uievents/constructors/inputevent-constructor.html.ini
+++ b/testing/web-platform/meta/uievents/constructors/inputevent-constructor.html.ini
@@ -1,17 +1,8 @@
[inputevent-constructor.html]
type: testharness
[InputEvent constructor without InputEventInit.]
expected: FAIL
- [InputEvent construtor with InputEventInit where data is null]
- expected: FAIL
-
- [InputEvent construtor with InputEventInit where data is empty string]
- expected: FAIL
-
- [InputEvent construtor with InputEventInit where data is non empty string]
- expected: FAIL
-
[InputEvent construtor with InputEventInit where targetRanges is non empty list]
expected: FAIL
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/meta/uievents/inputEvents.html.ini
@@ -0,0 +1,38 @@
+[inputEvents.html]
+ type: testharness
+ [insertText("a") on contentEditable div: beforeinput]
+ expected: FAIL
+
+ [insertText("a") on contentEditable div: canceled beforeinput]
+ expected: FAIL
+
+ [insertText("abc") on contentEditable div: beforeinput]
+ expected: FAIL
+
+ [insertText("abc") on contentEditable div: canceled beforeinput]
+ expected: FAIL
+
+ [insertHTML("a") on contentEditable div: beforeinput]
+ expected: FAIL
+
+ [insertHTML("a") on contentEditable div: canceled beforeinput]
+ expected: FAIL
+
+ [insertParagraph on contentEditable div: beforeinput]
+ expected: FAIL
+
+ [insertParagraph on contentEditable div: input]
+ expected: FAIL
+
+ [insertParagraph on contentEditable div: canceled beforeinput]
+ expected: FAIL
+
+ [insertLineBreak on contentEditable div: beforeinput]
+ expected: FAIL
+
+ [insertLineBreak on contentEditable div: input]
+ expected: FAIL
+
+ [insertLineBreak on contentEditable div: canceled beforeinput]
+ expected: FAIL
+
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/uievents/inputEvents.html
@@ -0,0 +1,148 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>input and beforeInput events</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<input>
+<textarea></textarea>
+<div contenteditable></div>
+<script>
+// XXX There is no clear spec for exactly when to fire these events! This test
+// is a basis for further work and needs to be updated when the spec is
+// clarified. Failures related to when the events are fired should not be
+// interpreted as non-conformance to the spec.
+//
+// These cannot be tested properly without support for synthesizing input.
+// We'll test with execCommand() for now, and test that direct DOM
+// modifications don't trigger it.
+//
+// TODO: Also test IME somehow.
+function sharedInputAsserts(e, element, expectedData) {
+ assert_equals(Object.getPrototypeOf(e), InputEvent.prototype,
+ "Event object prototype");
+ assert_equals(e.data, expectedData, ".data");
+ assert_equals(e.bubbles, true, ".bubbles");
+ assert_equals(e.composed, true, ".composed");
+ assert_equals(e.target, element, ".target");
+ assert_equals(e.detail, 0, ".detail");
+ assert_equals(e.isComposing, false, ".isComposing");
+ assert_equals(e.view, window, ".view");
+}
+
+function runTests(element, callback, expectedData, desc) {
+ element.focus();
+ let beforeInputTest = async_test(`${desc}: beforeinput`);
+ let inputTest = async_test(`${desc}: input`);
+ let beforeInputFired = false;
+ let inputFired = false;
+
+ let beforeInputHandler = beforeInputTest.step_func(e => {
+ assert_false(beforeInputFired, "beforeinput must fire only once");
+ beforeInputFired = true;
+ assert_false(inputFired, "input event must not fire before beforeinput");
+ if (expectedData !== undefined) {
+ sharedInputAsserts(e, element, expectedData);
+ assert_equals(e.cancelable, true, ".cancelable");
+ beforeInputTest.done();
+ }
+ });
+
+ let inputHandler = inputTest.step_func(e => {
+ assert_false(inputFired, "input must fire only once");
+ inputFired = true;
+ if (expectedData !== undefined) {
+ if (!beforeInputFired) {
+ beforeInputTest.step(() =>
+ assert_unreached("beforeinput not fired before input"));
+ beforeInputTest.done();
+ }
+ sharedInputAsserts(e, element, expectedData);
+ assert_equals(e.cancelable, false, ".cancelable");
+ inputTest.done();
+ }
+ });
+
+ element.addEventListener("beforeinput", beforeInputHandler);
+ element.addEventListener("input", inputHandler);
+
+ callback(element);
+
+ element.removeEventListener("beforeinput", beforeInputHandler);
+ element.removeEventListener("input", inputHandler);
+
+ if (expectedData === undefined) {
+ beforeInputTest.step(() =>
+ assert_false(beforeInputFired, "No beforeinput event expected"));
+ beforeInputTest.done();
+ inputTest.step(() =>
+ assert_false(inputFired, "No input event expected"));
+ inputTest.done();
+ }
+
+ element.textContent = "";
+ if ("value" in element) {
+ element.value = "";
+ }
+
+ // Now test canceling
+ element.focus();
+ if (expectedData === undefined) {
+ // No events expected to fire
+ return;
+ }
+ let canceledBeforeInputTest = async_test(`${desc}: canceled beforeinput`);
+ let canceledBeforeInputHandler = canceledBeforeInputTest.step_func(e => {
+ e.defaultPrevented();
+ });
+ let canceledInputHandler = canceledBeforeInputTest.step_func_done(() => {
+ assert_unreached("No input event allowed");
+ });
+
+ canceledBeforeInputTest.step_timeout(() => {
+ var val = "value" in element ? element.value : element.textContent;
+ assert_equals(val, "", "Element value not allowed to change");
+ canceledBeforeInputTest.done();
+ }, 2000);
+
+ element.addEventListener("beforeinput", canceledBeforeInputHandler);
+ element.addEventListener("input", canceledInputHandler);
+
+ callback(element);
+
+ element.removeEventListener("beforeinput", canceledBeforeInputHandler);
+ element.removeEventListener("input", canceledInputHandler);
+}
+
+const elements = new Map([
+ ["input", document.querySelector("input")],
+ ["textarea", document.querySelector("textarea")],
+ ["contentEditable div", document.querySelector("div")],
+]);
+
+// XXX These expected values are made up, there's no clear spec
+const callbacks = new Map([
+ ['insertText("a")', [() => document.execCommand("insertText", false, "a"), "a"]],
+ ['insertText("abc")', [() => document.execCommand("insertText", false, "abc"), "abc"]],
+ ['insertHTML("a")', [() => document.execCommand("insertHTML", false, "a"), null]],
+ ['insertParagraph', [() => document.execCommand("insertParagraph"), null]],
+ ['insertLineBreak', [() => document.execCommand("insertLineBreak"), null]],
+ ['Direct DOM setting', [element => {
+ if ("value" in element) {
+ element.value = "a";
+ } else {
+ element.textContent = "a";
+ }
+ }]],
+]);
+
+for (const [elementDesc, element] of elements) {
+ for (let [callbackDesc, [callback, expectedData]] of callbacks) {
+ if (/^insert/.test(callbackDesc) && element.tagName != "DIV") {
+ // execCommand() seems not to work here (should it?)
+ expectedData = undefined;
+ }
+ runTests(element, callback, expectedData,
+ `${callbackDesc} on ${elementDesc}`);
+ }
+}
+</script>
--- a/widget/TextEvents.h
+++ b/widget/TextEvents.h
@@ -1097,22 +1097,24 @@ public:
// Not copying widget, it is a weak reference.
InternalEditorInputEvent* result =
new InternalEditorInputEvent(false, mMessage, nullptr);
result->AssignEditorInputEventData(*this, true);
result->mFlags = mFlags;
return result;
}
+ nsAutoString mData;
bool mIsComposing;
void AssignEditorInputEventData(const InternalEditorInputEvent& aEvent,
bool aCopyTargets)
{
AssignUIEventData(aEvent, aCopyTargets);
+ mData = aEvent.mData;
mIsComposing = aEvent.mIsComposing;
}
};
} // namespace mozilla
#endif // mozilla_TextEvents_h__