зеркало из https://github.com/mozilla/gecko-dev.git
Bug 903746 - part 2: Add `textInput` event and make `EditorBase` dispatch it as a default action of `beforeinput` r=smaug
Differential Revision: https://phabricator.services.mozilla.com/D200121
This commit is contained in:
Родитель
421f87708c
Коммит
c1fc3d961a
|
@ -225,6 +225,8 @@ EVENT(gotpointercapture, ePointerGotCapture, EventNameType_All,
|
|||
EVENT(lostpointercapture, ePointerLostCapture, EventNameType_All,
|
||||
ePointerEventClass)
|
||||
EVENT(selectstart, eSelectStart, EventNameType_HTMLXUL, eBasicEventClass)
|
||||
NON_IDL_EVENT(textInput, eLegacyTextInput, EventNameType_None,
|
||||
eLegacyTextEventClass)
|
||||
|
||||
EVENT(contextlost, eContextLost, EventNameType_HTML, eBasicEventClass)
|
||||
EVENT(contextrestored, eContextRestored, EventNameType_HTML, eBasicEventClass)
|
||||
|
|
|
@ -4,9 +4,12 @@
|
|||
* 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/. */
|
||||
|
||||
#include "mozilla/EventForwards.h"
|
||||
#include "mozilla/TextEvents.h"
|
||||
#include "mozilla/dom/DataTransfer.h"
|
||||
#include "mozilla/dom/TextEvent.h"
|
||||
#include "nsGlobalWindowInner.h"
|
||||
#include "nsIPrincipal.h"
|
||||
#include "nsPresContext.h"
|
||||
|
||||
namespace mozilla::dom {
|
||||
|
@ -33,9 +36,22 @@ void TextEvent::InitTextEvent(const nsAString& typeArg, bool canBubbleArg,
|
|||
static_cast<InternalLegacyTextEvent*>(mEvent)->mData = dataArg;
|
||||
}
|
||||
|
||||
void TextEvent::GetData(nsAString& aData) const {
|
||||
// TODO: Add a security check like InputEvent::GetData().
|
||||
aData = static_cast<InternalLegacyTextEvent*>(mEvent)->mData;
|
||||
void TextEvent::GetData(nsAString& aData,
|
||||
nsIPrincipal& aSubjectPrincipal) const {
|
||||
InternalLegacyTextEvent* textEvent = mEvent->AsLegacyTextEvent();
|
||||
MOZ_ASSERT(textEvent);
|
||||
if (mEvent->IsTrusted() && !aSubjectPrincipal.IsSystemPrincipal() &&
|
||||
!StaticPrefs::dom_event_clipboardevents_enabled() &&
|
||||
ExposesClipboardDataOrDataTransfer(textEvent->mInputType)) {
|
||||
aData.Truncate();
|
||||
return;
|
||||
}
|
||||
if (!textEvent->mDataTransfer) {
|
||||
aData = textEvent->mData;
|
||||
return;
|
||||
}
|
||||
textEvent->mDataTransfer->GetData(u"text/plain"_ns, aData, aSubjectPrincipal,
|
||||
IgnoreErrors());
|
||||
}
|
||||
|
||||
} // namespace mozilla::dom
|
||||
|
|
|
@ -12,6 +12,8 @@
|
|||
#include "mozilla/dom/TextEventBinding.h"
|
||||
#include "mozilla/EventForwards.h"
|
||||
|
||||
class nsIPrincipal;
|
||||
|
||||
namespace mozilla::dom {
|
||||
|
||||
class TextEvent : public UIEvent {
|
||||
|
@ -26,7 +28,7 @@ class TextEvent : public UIEvent {
|
|||
return TextEvent_Binding::Wrap(aCx, this, aGivenProto);
|
||||
}
|
||||
|
||||
void GetData(nsAString& aData) const;
|
||||
void GetData(nsAString& aData, nsIPrincipal& aSubjectPrincipal) const;
|
||||
void InitTextEvent(const nsAString& typeArg, bool canBubbleArg,
|
||||
bool cancelableArg, nsGlobalWindowInner* viewArg,
|
||||
const nsAString& dataArg);
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
[Pref="dom.events.textevent.enabled", Exposed=Window]
|
||||
interface TextEvent : UIEvent
|
||||
{
|
||||
[NeedsSubjectPrincipal]
|
||||
readonly attribute DOMString data;
|
||||
|
||||
undefined initTextEvent(DOMString type,
|
||||
|
|
|
@ -81,16 +81,19 @@ enum class EditAction {
|
|||
// new non-empty composition string and IME selections.
|
||||
eUpdateComposition,
|
||||
|
||||
// eCommitComposition indicates that user commits composition.
|
||||
// eUpdateCompositionToCommit indicates that user commits composition with
|
||||
// the new data. That means that there will be no IME selections, but the
|
||||
// composition continues until the following eCompositionEnd event.
|
||||
eUpdateCompositionToCommit,
|
||||
|
||||
// eCommitComposition indicates that user commits composition and ends the
|
||||
// composition.
|
||||
eCommitComposition,
|
||||
|
||||
// eCancelComposition indicates that user cancels composition.
|
||||
// eCancelComposition indicates that user cancels composition and ends the
|
||||
// composition with empty string.
|
||||
eCancelComposition,
|
||||
|
||||
// eDeleteByComposition indicates that user starts composition with
|
||||
// empty string and there was selected content.
|
||||
eDeleteByComposition,
|
||||
|
||||
// eUndo/eRedo indicate to undo/redo a transaction.
|
||||
eUndo,
|
||||
eRedo,
|
||||
|
@ -547,6 +550,7 @@ inline EditorInputType ToInputType(EditAction aEditAction) {
|
|||
case EditAction::ePasteAsQuotation:
|
||||
return EditorInputType::eInsertFromPasteAsQuotation;
|
||||
case EditAction::eUpdateComposition:
|
||||
case EditAction::eUpdateCompositionToCommit:
|
||||
return EditorInputType::eInsertCompositionText;
|
||||
case EditAction::eCommitComposition:
|
||||
if (StaticPrefs::dom_input_events_conform_to_level_1()) {
|
||||
|
@ -558,13 +562,6 @@ inline EditorInputType ToInputType(EditAction aEditAction) {
|
|||
return EditorInputType::eInsertCompositionText;
|
||||
}
|
||||
return EditorInputType::eDeleteCompositionText;
|
||||
case EditAction::eDeleteByComposition:
|
||||
if (StaticPrefs::dom_input_events_conform_to_level_1()) {
|
||||
// XXX Or EditorInputType::eDeleteContent? I don't know which IME may
|
||||
// causes this situation.
|
||||
return EditorInputType::eInsertCompositionText;
|
||||
}
|
||||
return EditorInputType::eDeleteByComposition;
|
||||
case EditAction::eInsertLinkElement:
|
||||
return EditorInputType::eInsertLink;
|
||||
case EditAction::eDeleteWordBackward:
|
||||
|
@ -713,9 +710,9 @@ inline bool MayEditActionDeleteSelection(const EditAction aEditAction) {
|
|||
return false;
|
||||
|
||||
case EditAction::eUpdateComposition:
|
||||
case EditAction::eUpdateCompositionToCommit:
|
||||
case EditAction::eCommitComposition:
|
||||
case EditAction::eCancelComposition:
|
||||
case EditAction::eDeleteByComposition:
|
||||
return true;
|
||||
|
||||
case EditAction::eUndo:
|
||||
|
|
|
@ -34,6 +34,7 @@
|
|||
#include "ErrorList.h"
|
||||
#include "gfxFontUtils.h" // for gfxFontUtils
|
||||
#include "mozilla/Assertions.h"
|
||||
#include "mozilla/AsyncEventDispatcher.h"
|
||||
#include "mozilla/intl/BidiEmbeddingLevel.h"
|
||||
#include "mozilla/BasePrincipal.h" // for BasePrincipal
|
||||
#include "mozilla/CheckedInt.h" // for CheckedInt
|
||||
|
@ -3714,32 +3715,20 @@ nsresult EditorBase::OnCompositionChange(
|
|||
return NS_ERROR_FAILURE;
|
||||
}
|
||||
|
||||
AutoEditActionDataSetter editActionData(*this,
|
||||
EditAction::eUpdateComposition);
|
||||
AutoEditActionDataSetter editActionData(
|
||||
*this,
|
||||
// We need to distinguish whether the composition change is followed by
|
||||
// compositionend or not (i.e., wether IME has already ended the
|
||||
// composition or still has the composition) because we need to dispatch
|
||||
// `textInput` event only for the last composition change.
|
||||
aCompositionChangeEvent.IsFollowedByCompositionEnd()
|
||||
? EditAction::eUpdateCompositionToCommit
|
||||
: EditAction::eUpdateComposition);
|
||||
if (NS_WARN_IF(!editActionData.CanHandle())) {
|
||||
return NS_ERROR_NOT_INITIALIZED;
|
||||
}
|
||||
|
||||
// If:
|
||||
// - new composition string is not empty,
|
||||
// - there is no composition string in the DOM tree,
|
||||
// - and there is non-collapsed Selection,
|
||||
// the selected content will be removed by this composition.
|
||||
if (aCompositionChangeEvent.mData.IsEmpty() &&
|
||||
mComposition->String().IsEmpty() && !SelectionRef().IsCollapsed()) {
|
||||
editActionData.UpdateEditAction(EditAction::eDeleteByComposition);
|
||||
}
|
||||
|
||||
// If Input Events Level 2 is enabled, EditAction::eDeleteByComposition is
|
||||
// mapped to EditorInputType::eDeleteByComposition and it requires null
|
||||
// for InputEvent.data. Therefore, only otherwise, we should set data.
|
||||
if (ToInputType(editActionData.GetEditAction()) !=
|
||||
EditorInputType::eDeleteByComposition) {
|
||||
MOZ_ASSERT(ToInputType(editActionData.GetEditAction()) ==
|
||||
EditorInputType::eInsertCompositionText);
|
||||
MOZ_ASSERT(!aCompositionChangeEvent.mData.IsVoid());
|
||||
editActionData.SetData(aCompositionChangeEvent.mData);
|
||||
}
|
||||
MOZ_ASSERT(!aCompositionChangeEvent.mData.IsVoid());
|
||||
editActionData.SetData(aCompositionChangeEvent.mData);
|
||||
|
||||
// If we're an `HTMLEditor` and this is second or later composition change,
|
||||
// we should set target range to the range of composition string.
|
||||
|
@ -6513,6 +6502,13 @@ nsresult EditorBase::AutoEditActionDataSetter::MaybeFlushPendingNotifications()
|
|||
return NS_OK;
|
||||
}
|
||||
|
||||
void EditorBase::AutoEditActionDataSetter::MarkEditActionCanceled() {
|
||||
mBeforeInputEventCanceled = true;
|
||||
if (mEditorBase.IsHTMLEditor()) {
|
||||
mEditorBase.AsHTMLEditor()->mHasBeforeInputBeenCanceled = true;
|
||||
}
|
||||
}
|
||||
|
||||
nsresult EditorBase::AutoEditActionDataSetter::MaybeDispatchBeforeInputEvent(
|
||||
nsIEditor::EDirection aDeleteDirectionAndAmount /* = nsIEditor::eNone */) {
|
||||
MOZ_ASSERT(!HasTriedToDispatchBeforeInputEvent(),
|
||||
|
@ -6637,11 +6633,79 @@ nsresult EditorBase::AutoEditActionDataSetter::MaybeDispatchBeforeInputEvent(
|
|||
NS_WARNING("nsContentUtils::DispatchInputEvent() failed");
|
||||
return rv;
|
||||
}
|
||||
mBeforeInputEventCanceled = status == nsEventStatus_eConsumeNoDefault;
|
||||
if (mBeforeInputEventCanceled && mEditorBase.IsHTMLEditor()) {
|
||||
mEditorBase.AsHTMLEditor()->mHasBeforeInputBeenCanceled = true;
|
||||
if (status == nsEventStatus_eConsumeNoDefault) {
|
||||
MarkEditActionCanceled();
|
||||
return NS_ERROR_EDITOR_ACTION_CANCELED;
|
||||
}
|
||||
return mBeforeInputEventCanceled ? NS_ERROR_EDITOR_ACTION_CANCELED : NS_OK;
|
||||
|
||||
nsCOMPtr<nsIWidget> widget = editorBase->GetWidget();
|
||||
if (!StaticPrefs::dom_events_textevent_enabled() ||
|
||||
!targetElement->IsInComposedDoc() || !widget) {
|
||||
return NS_OK;
|
||||
}
|
||||
nsString textInputData;
|
||||
RefPtr<DataTransfer> textInputDataTransfer;
|
||||
switch (inputType) {
|
||||
case EditorInputType::eInsertCompositionText:
|
||||
// If the composition is still being composed, we should not dispatch
|
||||
// textInput event, but we need to dispatch it for the last composition
|
||||
// change because web apps should know the inserting commit string as
|
||||
// same as input from keyboard.
|
||||
if (mEditAction == EditAction::eUpdateComposition) {
|
||||
return NS_OK;
|
||||
}
|
||||
[[fallthrough]];
|
||||
case EditorInputType::eInsertText:
|
||||
textInputData = mData;
|
||||
break;
|
||||
case EditorInputType::eInsertFromDrop:
|
||||
case EditorInputType::eInsertFromPaste:
|
||||
case EditorInputType::eInsertFromPasteAsQuotation:
|
||||
if (mDataTransfer) {
|
||||
textInputDataTransfer = mDataTransfer;
|
||||
} else {
|
||||
textInputData = mData;
|
||||
}
|
||||
break;
|
||||
case EditorInputType::eInsertLineBreak:
|
||||
case EditorInputType::eInsertParagraph:
|
||||
// Don't dispatch `textInput` on <input> because Chrome does not do it.
|
||||
// On the other hand, we need to dispatch it on <textarea> and
|
||||
// contenteditable.
|
||||
if (mEditorBase.IsTextEditor() && mEditorBase.IsSingleLineEditor()) {
|
||||
return NS_OK;
|
||||
}
|
||||
textInputData.Assign(u'\n');
|
||||
break;
|
||||
default:
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
InternalLegacyTextEvent textEvent(true, eLegacyTextInput, widget);
|
||||
textEvent.mData = std::move(textInputData);
|
||||
textEvent.mDataTransfer = std::move(textInputDataTransfer);
|
||||
textEvent.mInputType = inputType;
|
||||
// Make it always cancelable even though we ignore it when inserting or
|
||||
// deleting composition. This is compatible with Chrome.
|
||||
// However, if and only if it's unsafe, let's set it not cancelable because of
|
||||
// asynchronous dispatching.
|
||||
textEvent.mFlags.mCancelable = nsContentUtils::IsSafeToRunScript();
|
||||
|
||||
status = nsEventStatus_eIgnore;
|
||||
rv = AsyncEventDispatcher::RunDOMEventWhenSafe(*targetElement, textEvent,
|
||||
&status);
|
||||
if (NS_WARN_IF(mEditorBase.Destroyed())) {
|
||||
return NS_ERROR_EDITOR_DESTROYED;
|
||||
}
|
||||
if (NS_FAILED(rv)) {
|
||||
NS_WARNING("AsyncEventDispatcher::RunDOMEventWhenSafe() failed");
|
||||
return rv;
|
||||
}
|
||||
if (status == nsEventStatus_eConsumeNoDefault) {
|
||||
MarkEditActionCanceled();
|
||||
return NS_ERROR_EDITOR_ACTION_CANCELED;
|
||||
}
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
/*****************************************************************************
|
||||
|
|
|
@ -1361,6 +1361,8 @@ class EditorBase : public nsIEditor,
|
|||
}
|
||||
}
|
||||
|
||||
void MarkEditActionCanceled();
|
||||
|
||||
EditorBase& mEditorBase;
|
||||
RefPtr<Selection> mSelection;
|
||||
nsTArray<OwningNonNull<Selection>> mRetiredSelections;
|
||||
|
|
|
@ -270,9 +270,6 @@ inline bool IsCancelableBeforeInputEvent(EditorInputType aInputType) {
|
|||
return true;
|
||||
case EditorInputType::eInsertLink:
|
||||
return true;
|
||||
case EditorInputType::eDeleteByComposition:
|
||||
MOZ_ASSERT(!StaticPrefs::dom_input_events_conform_to_level_1());
|
||||
return true;
|
||||
case EditorInputType::eDeleteCompositionText:
|
||||
MOZ_ASSERT(!StaticPrefs::dom_input_events_conform_to_level_1());
|
||||
return false;
|
||||
|
|
|
@ -1527,12 +1527,16 @@ class InternalLegacyTextEvent : public InternalUIEvent {
|
|||
}
|
||||
|
||||
nsString mData;
|
||||
RefPtr<dom::DataTransfer> mDataTransfer;
|
||||
EditorInputType mInputType = EditorInputType::eUnknown;
|
||||
|
||||
void AssignLegacyTextEventData(const InternalLegacyTextEvent& aEvent,
|
||||
bool aCopyTargets) {
|
||||
AssignUIEventData(aEvent, aCopyTargets);
|
||||
|
||||
mData = aEvent.mData;
|
||||
mDataTransfer = aEvent.mDataTransfer;
|
||||
mInputType = aEvent.mInputType;
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -59,6 +59,7 @@
|
|||
<body>
|
||||
<div id="display">
|
||||
<input id="input-text">
|
||||
<div contenteditable id="contenteditable"><br></div>
|
||||
<button id="button">button</button>
|
||||
<a id="a" href="about:blank">hyper link</a>
|
||||
<span id="pointer-target">span</span>
|
||||
|
@ -352,6 +353,44 @@ const kTests = [
|
|||
},
|
||||
todoMismatch: [ ],
|
||||
},
|
||||
{ description: "InternalLegacyTextEvent (input at key input)",
|
||||
targetID: "input-text", eventType: "textInput",
|
||||
dispatchEvent() {
|
||||
const input = document.getElementById(this.targetID);
|
||||
input.value = "";
|
||||
input.focus();
|
||||
synthesizeNativeKey(KEYBOARD_LAYOUT_EN_US, kIsWin ? WIN_VK_B : MAC_VK_ANSI_B,
|
||||
{ shiftKey: true }, "B", "B");
|
||||
observeKeyUpOnContent(KeyboardEvent.DOM_VK_B, runNextTest);
|
||||
return true;
|
||||
},
|
||||
canRun() {
|
||||
return (kIsMac || kIsWin);
|
||||
},
|
||||
todoMismatch: [],
|
||||
},
|
||||
{ description: "InternalLegacyTextEvent (paste)",
|
||||
targetID: "contenteditable", eventType: "textInput",
|
||||
async dispatchEvent() {
|
||||
const editingHost = document.getElementById(this.targetID);
|
||||
editingHost.innerHTML = "abc";
|
||||
editingHost.focus();
|
||||
getSelection().selectAllChildren(editingHost);
|
||||
synthesizeNativeKey(KEYBOARD_LAYOUT_EN_US, kIsWin ? WIN_VK_C : MAC_VK_ANSI_C,
|
||||
{ accelKey: true }, "", "c");
|
||||
const waitForInput = new Promise(resolve => {
|
||||
editingHost.addEventListener("input", resolve, {once: true});
|
||||
});
|
||||
// In this case, TextEvent.data is stored with a dataTransfer.
|
||||
synthesizeNativeKey(KEYBOARD_LAYOUT_EN_US, kIsWin ? WIN_VK_V : MAC_VK_ANSI_V,
|
||||
{ accelKey: true }, "", "v");
|
||||
await waitForInput;
|
||||
},
|
||||
canRun() {
|
||||
return (kIsMac || kIsWin);
|
||||
},
|
||||
todoMismatch: [],
|
||||
},
|
||||
{ description: "WidgetMouseScrollEvent (DOMMouseScroll, vertical)",
|
||||
targetID: "input-text", eventType: "DOMMouseScroll",
|
||||
dispatchEvent() {
|
||||
|
@ -615,7 +654,7 @@ const kTests = [
|
|||
},
|
||||
];
|
||||
|
||||
function doTest(aTest) {
|
||||
async function doTest(aTest) {
|
||||
if (!aTest.canRun()) {
|
||||
SimpleTest.executeSoon(runNextTest);
|
||||
return;
|
||||
|
@ -650,7 +689,7 @@ function doTest(aTest) {
|
|||
runNextTest();
|
||||
}
|
||||
};
|
||||
var testWillCallRunNextTest = aTest.dispatchEvent();
|
||||
var testWillCallRunNextTest = await aTest.dispatchEvent();
|
||||
}
|
||||
|
||||
var gIndex = -1;
|
||||
|
@ -695,7 +734,8 @@ function init() {
|
|||
["mousewheel.with_alt.action", 0],
|
||||
["mousewheel.with_alt.action.override_x", -1],
|
||||
["mousewheel.with_meta.action", 0],
|
||||
["mousewheel.with_meta.action.override_x", -1]]}, runNextTest);
|
||||
["mousewheel.with_meta.action.override_x", -1],
|
||||
["dom.events.textevent.enabled", true]]}, runNextTest);
|
||||
}
|
||||
|
||||
function finish() {
|
||||
|
|
|
@ -806,6 +806,16 @@ function checkInputEvent(aEvent, aIsComposing, aInputType, aData, aTargetRanges,
|
|||
}
|
||||
}
|
||||
|
||||
function checkTextInputEvent(aEvent, aData, aDescription) {
|
||||
if (aEvent.type !== "textInput") {
|
||||
throw new Error(`${aDescription}: "${aEvent.type}" is not TextEvent`);
|
||||
}
|
||||
ok(TextEvent.isInstance(aEvent), `"${aEvent.type}" event should be dispatched with TextEvent interface: ${aDescription}`);
|
||||
is(aEvent.cancelable, true, `"${aEvent.type}" event should be cancelable: ${aDescription}`);
|
||||
is(aEvent.bubbles, true, `"${aEvent.type}" event should always bubble: ${aDescription}`);
|
||||
is(aEvent.data, aData, `data of "${aEvent.type}" event should be ${aData}: ${aDescription}`);
|
||||
}
|
||||
|
||||
function runCompositionCommitAsIsTest()
|
||||
{
|
||||
textarea.focus();
|
||||
|
@ -824,6 +834,7 @@ function runCompositionCommitAsIsTest()
|
|||
textarea.addEventListener("compositionupdate", handler, true);
|
||||
textarea.addEventListener("compositionend", handler, true);
|
||||
textarea.addEventListener("beforeinput", handler, true);
|
||||
textarea.addEventListener("textInput", handler, true);
|
||||
textarea.addEventListener("input", handler, true);
|
||||
textarea.addEventListener("text", handler, true);
|
||||
|
||||
|
@ -842,22 +853,29 @@ function runCompositionCommitAsIsTest()
|
|||
});
|
||||
is(textarea.value, "\u3042", "runCompositionCommitAsIsTest: textarea doesn't have composition string #1");
|
||||
|
||||
is(result.findIndex(value => value.type == "textInput"), -1,
|
||||
"runCompositionCommitAsIsTest: no textInput event should be fired before commit #1");
|
||||
|
||||
clearResult();
|
||||
synthesizeComposition({ type: "compositioncommitasis", key: { key: "Enter" } });
|
||||
|
||||
is(result.length, 4,
|
||||
"runCompositionCommitAsIsTest: 4 events should be fired after dispatching compositioncommitasis #1");
|
||||
is(result.length, 5,
|
||||
"runCompositionCommitAsIsTest: 5 events should be fired after dispatching compositioncommitasis #1");
|
||||
is(result[0].type, "text",
|
||||
"runCompositionCommitAsIsTest: text should be fired after dispatching compositioncommitasis because it's dispatched when there is composing string #1");
|
||||
is(result[1].type, "beforeinput",
|
||||
"runCompositionCommitAsIsTest: beforeinput should be fired after dispatching compositioncommitasis because it's dispatched when there is composing string #1");
|
||||
checkInputEvent(result[1], true, "insertCompositionText", "\u3042", [],
|
||||
"runCompositionCommitAsIsTest: after dispatching compositioncommitasis #1");
|
||||
is(result[2].type, "compositionend",
|
||||
is(result[2].type, "textInput",
|
||||
"runCompositionCommitAsIsText: textInput should be fired after dispatching compositioncommitasis because it's after the last beforeinput for the composition #1");
|
||||
checkTextInputEvent(result[2], "\u3042",
|
||||
"runCompositionCommitAsIsText: after dispatching compositioncommitasis #1");
|
||||
is(result[3].type, "compositionend",
|
||||
"runCompositionCommitAsIsTest: compositionend should be fired after dispatching compositioncommitasis #1");
|
||||
is(result[3].type, "input",
|
||||
is(result[4].type, "input",
|
||||
"runCompositionCommitAsIsTest: input should be fired after dispatching compositioncommitasis #1");
|
||||
checkInputEvent(result[3], false, "insertCompositionText", "\u3042", [],
|
||||
checkInputEvent(result[4], false, "insertCompositionText", "\u3042", [],
|
||||
"runCompositionCommitAsIsTest: after dispatching compositioncommitasis #1");
|
||||
is(textarea.value, "\u3042", "runCompositionCommitAsIsTest: textarea doesn't have committed string #1");
|
||||
|
||||
|
@ -887,6 +905,8 @@ function runCompositionCommitAsIsTest()
|
|||
"key": { key: "KEY_Enter", type: "keydown" },
|
||||
});
|
||||
is(textarea.value, "\u3042", "runCompositionCommitAsIsTest: textarea doesn't have committed string #2");
|
||||
isnot(result.findIndex(value => value.type == "textInput"), -1,
|
||||
"runCompositionCommitAsIsTest: a textInput event should be fired before commit #2");
|
||||
|
||||
clearResult();
|
||||
synthesizeComposition({ type: "compositioncommitasis", key: { key: "KEY_Enter", type: "keyup" } });
|
||||
|
@ -928,6 +948,8 @@ function runCompositionCommitAsIsTest()
|
|||
"key": { key: "KEY_Escape", type: "keydown" },
|
||||
});
|
||||
is(textarea.value, "", "runCompositionCommitAsIsTest: textarea has non-empty composition string #3");
|
||||
todo_isnot(result.findIndex(value => value.type == "textInput"), -1,
|
||||
"runCompositionCommitAsIsTest: a textInput event should be fired immediately before commit #3");
|
||||
|
||||
clearResult();
|
||||
synthesizeComposition({ type: "compositioncommitasis", key: { key: "KEY_Escape", type: "keyup" } });
|
||||
|
@ -946,6 +968,7 @@ function runCompositionCommitAsIsTest()
|
|||
textarea.removeEventListener("compositionupdate", handler, true);
|
||||
textarea.removeEventListener("compositionend", handler, true);
|
||||
textarea.removeEventListener("beforeinput", handler, true);
|
||||
textarea.removeEventListener("textInput", handler, true);
|
||||
textarea.removeEventListener("input", handler, true);
|
||||
textarea.removeEventListener("text", handler, true);
|
||||
}
|
||||
|
@ -968,6 +991,7 @@ function runCompositionCommitTest()
|
|||
textarea.addEventListener("compositionupdate", handler, true);
|
||||
textarea.addEventListener("compositionend", handler, true);
|
||||
textarea.addEventListener("beforeinput", handler, true);
|
||||
textarea.addEventListener("textInput", handler, true);
|
||||
textarea.addEventListener("input", handler, true);
|
||||
textarea.addEventListener("text", handler, true);
|
||||
|
||||
|
@ -985,12 +1009,14 @@ function runCompositionCommitTest()
|
|||
"key": { key: "a", type: "keydown" },
|
||||
});
|
||||
is(textarea.value, "\u3042", "runCompositionCommitTest: textarea doesn't have composition string #1");
|
||||
is(result.findIndex(value => value.type == "textInput"), -1,
|
||||
"runCompositionCommitTest: no textInput event should be fired before commit #1");
|
||||
|
||||
clearResult();
|
||||
synthesizeComposition({ type: "compositioncommit", data: "\u3043", key: { key: "a", type: "keyup" } });
|
||||
|
||||
is(result.length, 5,
|
||||
"runCompositionCommitTest: 5 events should be fired after dispatching compositioncommit #1");
|
||||
is(result.length, 6,
|
||||
"runCompositionCommitTest: 6 events should be fired after dispatching compositioncommit #1");
|
||||
is(result[0].type, "compositionupdate",
|
||||
"runCompositionCommitTest: compositionupdate should be fired after dispatching compositioncommit because it's dispatched when there is composing string #1");
|
||||
is(result[1].type, "text",
|
||||
|
@ -999,16 +1025,21 @@ function runCompositionCommitTest()
|
|||
"runCompositionCommitTest: beforeinput should be fired after dispatching compositioncommit because it's dispatched when there is composing string #1");
|
||||
checkInputEvent(result[2], true, "insertCompositionText", "\u3043", [],
|
||||
"runCompositionCommitTest: after dispatching compositioncommit #1");
|
||||
is(result[3].type, "compositionend",
|
||||
is(result[3].type, "textInput",
|
||||
"runCompositionCommitTest: textInput should be fired after dispatching compositioncommit because the preceding beforeinput is last one for the composition #1");
|
||||
checkTextInputEvent(result[3], "\u3043",
|
||||
"runCompositionCommitTest: after dispatching compositioncommit #1");
|
||||
is(result[4].type, "compositionend",
|
||||
"runCompositionCommitTest: compositionend should be fired after dispatching compositioncommit #1");
|
||||
is(result[4].type, "input",
|
||||
is(result[5].type, "input",
|
||||
"runCompositionCommitTest: input should be fired after dispatching compositioncommit #1");
|
||||
checkInputEvent(result[4], false, "insertCompositionText", "\u3043", [],
|
||||
checkInputEvent(result[5], false, "insertCompositionText", "\u3043", [],
|
||||
"runCompositionCommitTest: after dispatching compositioncommit #1");
|
||||
is(textarea.value, "\u3043", "runCompositionCommitTest: textarea doesn't have committed string #1");
|
||||
|
||||
// compositioncommit with different committed string when there is already committed string
|
||||
textarea.value = "";
|
||||
clearResult();
|
||||
synthesizeCompositionChange(
|
||||
{ "composition":
|
||||
{ "string": "\u3042",
|
||||
|
@ -1033,12 +1064,14 @@ function runCompositionCommitTest()
|
|||
"key": { key: "KEY_Enter", type: "keydown" },
|
||||
});
|
||||
is(textarea.value, "\u3042", "runCompositionCommitTest: textarea doesn't have committed string #2");
|
||||
is(result.findIndex(value => value.type == "textInput"), -1,
|
||||
"runCompositionCommitTest: no textInput event should be fired before commit #2");
|
||||
|
||||
clearResult();
|
||||
synthesizeComposition({ type: "compositioncommit", data: "\u3043", key: { key: "KEY_Enter", type: "keyup" } });
|
||||
|
||||
is(result.length, 5,
|
||||
"runCompositionCommitTest: 5 events should be fired after dispatching compositioncommit #2");
|
||||
is(result.length, 6,
|
||||
"runCompositionCommitTest: 6 events should be fired after dispatching compositioncommit #2");
|
||||
is(result[0].type, "compositionupdate",
|
||||
"runCompositionCommitTest: compositionupdate should be fired after dispatching compositioncommit #2");
|
||||
is(result[1].type, "text",
|
||||
|
@ -1047,16 +1080,21 @@ function runCompositionCommitTest()
|
|||
"runCompositionCommitTest: beforeinput should be fired after dispatching compositioncommit #2");
|
||||
checkInputEvent(result[2], true, "insertCompositionText", "\u3043", [],
|
||||
"runCompositionCommitTest: after dispatching compositioncommit #2");
|
||||
is(result[3].type, "compositionend",
|
||||
is(result[3].type, "textInput",
|
||||
"runCompositionCommitTest: textInput should be fired after dispatching compositioncommit #2");
|
||||
checkTextInputEvent(result[3], "\u3043",
|
||||
"runCompositionCommitTest: after dispatching compositioncommit #2");
|
||||
is(result[4].type, "compositionend",
|
||||
"runCompositionCommitTest: compositionend should be fired after dispatching compositioncommit #2");
|
||||
is(result[4].type, "input",
|
||||
is(result[5].type, "input",
|
||||
"runCompositionCommitTest: input should be fired after dispatching compositioncommit #2");
|
||||
checkInputEvent(result[4], false, "insertCompositionText", "\u3043", [],
|
||||
checkInputEvent(result[5], false, "insertCompositionText", "\u3043", [],
|
||||
"runCompositionCommitTest: after dispatching compositioncommit #2");
|
||||
is(textarea.value, "\u3043", "runCompositionCommitTest: textarea doesn't have committed string #2");
|
||||
|
||||
// compositioncommit with empty composition string.
|
||||
textarea.value = "";
|
||||
clearResult();
|
||||
synthesizeCompositionChange(
|
||||
{ "composition":
|
||||
{ "string": "\u3042",
|
||||
|
@ -1081,12 +1119,14 @@ function runCompositionCommitTest()
|
|||
"key": { key: "KEY_Enter", type: "keydown" },
|
||||
});
|
||||
is(textarea.value, "", "runCompositionCommitTest: textarea has non-empty composition string #3");
|
||||
is(result.findIndex(value => value.type == "textInput"), -1,
|
||||
"runCompositionCommitTest: no textInput event should be fired before commit #3");
|
||||
|
||||
clearResult();
|
||||
synthesizeComposition({ type: "compositioncommit", data: "\u3043", key: { key: "KEY_Enter", type: "keyup" } });
|
||||
|
||||
is(result.length, 5,
|
||||
"runCompositionCommitTest: 5 events should be fired after dispatching compositioncommit #3");
|
||||
is(result.length, 6,
|
||||
"runCompositionCommitTest: 6 events should be fired after dispatching compositioncommit #3");
|
||||
is(result[0].type, "compositionupdate",
|
||||
"runCompositionCommitTest: compositionupdate should be fired after dispatching compositioncommit #3");
|
||||
is(result[1].type, "text",
|
||||
|
@ -1095,11 +1135,15 @@ function runCompositionCommitTest()
|
|||
"runCompositionCommitTest: beforeinput should be fired after dispatching compositioncommit #3");
|
||||
checkInputEvent(result[2], true, "insertCompositionText", "\u3043", [],
|
||||
"runCompositionCommitTest: after dispatching compositioncommit #3");
|
||||
is(result[3].type, "compositionend",
|
||||
is(result[3].type, "textInput",
|
||||
"runCompositionCommitTest: textInput should be fired after dispatching compositioncommit #3");
|
||||
checkTextInputEvent(result[3], "\u3043",
|
||||
"runCompositionCommitTest: after dispatching compositioncommit #3");
|
||||
is(result[4].type, "compositionend",
|
||||
"runCompositionCommitTest: compositionend should be fired after dispatching compositioncommit #3");
|
||||
is(result[4].type, "input",
|
||||
is(result[5].type, "input",
|
||||
"runCompositionCommitTest: input should be fired after dispatching compositioncommit #3");
|
||||
checkInputEvent(result[4], false, "insertCompositionText", "\u3043", [],
|
||||
checkInputEvent(result[5], false, "insertCompositionText", "\u3043", [],
|
||||
"runCompositionCommitTest: after dispatching compositioncommit #3");
|
||||
is(textarea.value, "\u3043", "runCompositionCommitTest: textarea doesn't have committed string #3");
|
||||
|
||||
|
@ -1111,19 +1155,23 @@ function runCompositionCommitTest()
|
|||
clearResult();
|
||||
synthesizeComposition({ type: "compositioncommit", data: "" });
|
||||
|
||||
is(result.length, 4,
|
||||
"runCompositionCommitTest: 4 events should be fired when inserting empty string with composition");
|
||||
is(result.length, 5,
|
||||
"runCompositionCommitTest: 5 events should be fired when inserting empty string with composition");
|
||||
is(result[0].type, "text",
|
||||
"runCompositionCommitTest: text should be fired when inserting empty string with composition");
|
||||
is(result[1].type, "beforeinput",
|
||||
"runCompositionCommitTest: beforeinput should be fired when inserting empty string with composition");
|
||||
checkInputEvent(result[1], true, "insertCompositionText", "", [],
|
||||
"runCompositionCommitTest: when inserting empty string with composition");
|
||||
is(result[2].type, "compositionend",
|
||||
is(result[2].type, "textInput",
|
||||
"runCompositionCommitTest: textInput should be fired when inserting empty string with composition");
|
||||
checkTextInputEvent(result[2], "",
|
||||
"runCompositionCommitTest: when inserting empty string with composition");
|
||||
is(result[3].type, "compositionend",
|
||||
"runCompositionCommitTest: compositionend should be fired when inserting empty string with composition");
|
||||
is(result[3].type, "input",
|
||||
is(result[4].type, "input",
|
||||
"runCompositionCommitTest: input should be fired when inserting empty string with composition");
|
||||
checkInputEvent(result[3], false, "insertCompositionText", "", [],
|
||||
checkInputEvent(result[4], false, "insertCompositionText", "", [],
|
||||
"runCompositionCommitTest: when inserting empty string with composition");
|
||||
is(textarea.value, "abc",
|
||||
"runCompositionCommitTest: textarea should keep original value when inserting empty string with composition");
|
||||
|
@ -1136,19 +1184,23 @@ function runCompositionCommitTest()
|
|||
clearResult();
|
||||
synthesizeComposition({ type: "compositioncommit", data: "" });
|
||||
|
||||
is(result.length, 4,
|
||||
"runCompositionCommitTest: 4 events should be fired when replacing with empty string with composition");
|
||||
is(result.length, 5,
|
||||
"runCompositionCommitTest: 5 events should be fired when replacing with empty string with composition");
|
||||
is(result[0].type, "text",
|
||||
"runCompositionCommitTest: text should be fired when replacing with empty string with composition");
|
||||
is(result[1].type, "beforeinput",
|
||||
"runCompositionCommitTest: beforeinput should be fired when replacing with empty string with composition");
|
||||
checkInputEvent(result[1], true, "insertCompositionText", "", [],
|
||||
"runCompositionCommitTest: when replacing with empty string with composition");
|
||||
is(result[2].type, "compositionend",
|
||||
is(result[2].type, "textInput",
|
||||
"runCompositionCommitTest: textInput should be fired when replacing with empty string with composition");
|
||||
checkTextInputEvent(result[2], "",
|
||||
"runCompositionCommitTest: when replacing with empty string with composition");
|
||||
is(result[3].type, "compositionend",
|
||||
"runCompositionCommitTest: compositionend should be fired when replacing with empty string with composition");
|
||||
is(result[3].type, "input",
|
||||
is(result[4].type, "input",
|
||||
"runCompositionCommitTest: input should be fired when replacing with empty string with composition");
|
||||
checkInputEvent(result[3], false, "insertCompositionText", "", [],
|
||||
checkInputEvent(result[4], false, "insertCompositionText", "", [],
|
||||
"runCompositionCommitTest: when replacing with empty string with composition");
|
||||
is(textarea.value, "",
|
||||
"runCompositionCommitTest: textarea should become empty when replacing selection with empty string with composition");
|
||||
|
@ -1161,8 +1213,8 @@ function runCompositionCommitTest()
|
|||
clearResult();
|
||||
synthesizeComposition({ type: "compositioncommit", data: "abc" });
|
||||
|
||||
is(result.length, 5,
|
||||
"runCompositionCommitTest: 5 events should be fired when replacing selection with same string with composition");
|
||||
is(result.length, 6,
|
||||
"runCompositionCommitTest: 6 events should be fired when replacing selection with same string with composition");
|
||||
is(result[0].type, "compositionupdate",
|
||||
"runCompositionCommitTest: compositionupdate should be fired when replacing selection with same string with composition");
|
||||
is(result[1].type, "text",
|
||||
|
@ -1171,11 +1223,15 @@ function runCompositionCommitTest()
|
|||
"runCompositionCommitTest: beforeinput should be fired when replacing selection with same string with composition");
|
||||
checkInputEvent(result[2], true, "insertCompositionText", "abc", [],
|
||||
"runCompositionCommitTest: when replacing selection with same string with composition");
|
||||
is(result[3].type, "compositionend",
|
||||
is(result[3].type, "textInput",
|
||||
"runCompositionCommitTest: textInput should be fired when replacing selection with same string with composition");
|
||||
checkTextInputEvent(result[3], "abc",
|
||||
"runCompositionCommitTest: when replacing selection with same string with composition");
|
||||
is(result[4].type, "compositionend",
|
||||
"runCompositionCommitTest: compositionend should be fired when replacing selection with same string with composition");
|
||||
is(result[4].type, "input",
|
||||
is(result[5].type, "input",
|
||||
"runCompositionCommitTest: input should be fired when replacing selection with same string with composition");
|
||||
checkInputEvent(result[4], false, "insertCompositionText", "abc", [],
|
||||
checkInputEvent(result[5], false, "insertCompositionText", "abc", [],
|
||||
"runCompositionCommitTest: when replacing selection with same string with composition");
|
||||
is(textarea.value, "abc",
|
||||
"runCompositionCommitTest: textarea should keep same value when replacing selection with same string with composition");
|
||||
|
@ -1198,8 +1254,8 @@ function runCompositionCommitTest()
|
|||
clearResult();
|
||||
synthesizeComposition({ type: "compositioncommit", data: "", key: { key: "KEY_Enter" } });
|
||||
|
||||
is(result.length, 5,
|
||||
"runCompositionCommitTest: 5 events should be fired after dispatching compositioncommit #4");
|
||||
is(result.length, 6,
|
||||
"runCompositionCommitTest: 6 events should be fired after dispatching compositioncommit #4");
|
||||
is(result[0].type, "compositionupdate",
|
||||
"runCompositionCommitTest: compositionupdate should be fired after dispatching compositioncommit #4");
|
||||
is(result[1].type, "text",
|
||||
|
@ -1208,11 +1264,15 @@ function runCompositionCommitTest()
|
|||
"runCompositionCommitTest: beforeinput should be fired after dispatching compositioncommit #4");
|
||||
checkInputEvent(result[2], true, "insertCompositionText", "", [],
|
||||
"runCompositionCommitTest: after dispatching compositioncommit #4");
|
||||
is(result[3].type, "compositionend",
|
||||
is(result[3].type, "textInput",
|
||||
"runCompositionCommitTest: textInput should be fired after dispatching compositioncommit #4");
|
||||
checkTextInputEvent(result[3], "",
|
||||
"runCompositionCommitTest: after dispatching compositioncommit #4");
|
||||
is(result[4].type, "compositionend",
|
||||
"runCompositionCommitTest: compositionend should be fired after dispatching compositioncommit #4");
|
||||
is(result[4].type, "input",
|
||||
is(result[5].type, "input",
|
||||
"runCompositionCommitTest: input should be fired after dispatching compositioncommit #4");
|
||||
checkInputEvent(result[4], false, "insertCompositionText", "", [],
|
||||
checkInputEvent(result[5], false, "insertCompositionText", "", [],
|
||||
"runCompositionCommitTest: after dispatching compositioncommit #4");
|
||||
is(textarea.value, "", "runCompositionCommitTest: textarea should be empty #4");
|
||||
|
||||
|
@ -1222,8 +1282,8 @@ function runCompositionCommitTest()
|
|||
clearResult();
|
||||
synthesizeComposition({ type: "compositioncommit", data: "\u3042", key: { key: "a" } });
|
||||
|
||||
is(result.length, 5,
|
||||
"runCompositionCommitTest: 5 events should be fired after dispatching compositioncommit #5");
|
||||
is(result.length, 6,
|
||||
"runCompositionCommitTest: 6 events should be fired after dispatching compositioncommit #5");
|
||||
is(result[0].type, "compositionupdate",
|
||||
"runCompositionCommitTest: compositionupdate should be fired after dispatching compositioncommit #5");
|
||||
is(result[1].type, "text",
|
||||
|
@ -1232,11 +1292,15 @@ function runCompositionCommitTest()
|
|||
"runCompositionCommitTest: beforeinput should be fired after dispatching compositioncommit #5");
|
||||
checkInputEvent(result[2], true, "insertCompositionText", "\u3042", [],
|
||||
"runCompositionCommitTest: after dispatching compositioncommit #5");
|
||||
is(result[3].type, "compositionend",
|
||||
is(result[3].type, "textInput",
|
||||
"runCompositionCommitTest: textInput should be fired after dispatching compositioncommit #5");
|
||||
checkTextInputEvent(result[3], "\u3042",
|
||||
"runCompositionCommitTest: after dispatching compositioncommit #5");
|
||||
is(result[4].type, "compositionend",
|
||||
"runCompositionCommitTest: compositionend should be fired after dispatching compositioncommit #5");
|
||||
is(result[4].type, "input",
|
||||
is(result[5].type, "input",
|
||||
"runCompositionCommitTest: input should be fired after dispatching compositioncommit #5");
|
||||
checkInputEvent(result[4], false, "insertCompositionText", "\u3042", [],
|
||||
checkInputEvent(result[5], false, "insertCompositionText", "\u3042", [],
|
||||
"runCompositionCommitTest: after dispatching compositioncommit #5");
|
||||
is(textarea.value, "\u3042", "runCompositionCommitTest: textarea should be empty #5");
|
||||
|
||||
|
@ -1258,24 +1322,29 @@ function runCompositionCommitTest()
|
|||
clearResult();
|
||||
synthesizeComposition({ type: "compositioncommit", data: "\u3042", key: { key: "KEY_Enter" } });
|
||||
|
||||
is(result.length, 4,
|
||||
"runCompositionCommitTest: 4 events should be fired after dispatching compositioncommit #6");
|
||||
is(result.length, 5,
|
||||
"runCompositionCommitTest: 5 events should be fired after dispatching compositioncommit #6");
|
||||
is(result[0].type, "text",
|
||||
"runCompositionCommitTest: text should be fired after dispatching compositioncommit #6");
|
||||
is(result[1].type, "beforeinput",
|
||||
"runCompositionCommitTest: beforeinput should be fired after dispatching compositioncommit #6");
|
||||
checkInputEvent(result[1], true, "insertCompositionText", "\u3042", [],
|
||||
"runCompositionCommitTest: after dispatching compositioncommit #6");
|
||||
is(result[2].type, "compositionend",
|
||||
is(result[2].type, "textInput",
|
||||
"runCompositionCommitTest: textInput should be fired after dispatching compositioncommit #6");
|
||||
checkTextInputEvent(result[2], "\u3042",
|
||||
"runCompositionCommitTest: after dispatching compositioncommit #6");
|
||||
is(result[3].type, "compositionend",
|
||||
"runCompositionCommitTest: compositionend should be fired after dispatching compositioncommit #6");
|
||||
is(result[3].type, "input",
|
||||
is(result[4].type, "input",
|
||||
"runCompositionCommitTest: input should be fired after dispatching compositioncommit #6");
|
||||
checkInputEvent(result[3], false, "insertCompositionText", "\u3042", [],
|
||||
checkInputEvent(result[4], false, "insertCompositionText", "\u3042", [],
|
||||
"runCompositionCommitTest: after dispatching compositioncommit #6");
|
||||
is(textarea.value, "\u3042", "runCompositionCommitTest: textarea should have committed string #6");
|
||||
|
||||
// compositioncommit with same composition string when there is committed string
|
||||
textarea.value = "";
|
||||
clearResult();
|
||||
synthesizeCompositionChange(
|
||||
{ "composition":
|
||||
{ "string": "\u3042",
|
||||
|
@ -1301,6 +1370,8 @@ function runCompositionCommitTest()
|
|||
"key": { key: "KEY_Enter", type: "keydown" },
|
||||
});
|
||||
is(textarea.value, "\u3042", "runCompositionCommitTest: textarea doesn't have composition string #6");
|
||||
todo_isnot(result.findIndex(value => value.type == "textInput"), -1,
|
||||
"runCompositionCommitTest: a textInput event should be fired before immediately commit #6");
|
||||
|
||||
clearResult();
|
||||
synthesizeComposition({ type: "compositioncommit", data: "\u3042", key: { key: "KEY_Enter", type: "keyup" } });
|
||||
|
@ -1319,6 +1390,7 @@ function runCompositionCommitTest()
|
|||
textarea.removeEventListener("compositionupdate", handler, true);
|
||||
textarea.removeEventListener("compositionend", handler, true);
|
||||
textarea.removeEventListener("beforeinput", handler, true);
|
||||
textarea.removeEventListener("textInput", handler, true);
|
||||
textarea.removeEventListener("input", handler, true);
|
||||
textarea.removeEventListener("text", handler, true);
|
||||
}
|
||||
|
@ -10860,6 +10932,10 @@ async function runInputModeTest()
|
|||
|
||||
async function runTest()
|
||||
{
|
||||
await SpecialPowers.pushPrefEnv({
|
||||
set: [["dom.events.textevent.enabled", true]],
|
||||
});
|
||||
|
||||
window.addEventListener("unload", window.arguments[0].SimpleTest.finish, {once: true, capture: true});
|
||||
|
||||
contenteditable = document.getElementById("iframe4").contentDocument.getElementById("contenteditable");
|
||||
|
|
|
@ -927,6 +927,7 @@ STATIC_ATOMS = [
|
|||
Atom("ontypechange", "ontypechange"),
|
||||
Atom("onterminate", "onterminate"),
|
||||
Atom("ontext", "ontext"),
|
||||
Atom("ontextInput", "ontextInput"),
|
||||
Atom("ontoggle", "ontoggle"),
|
||||
Atom("ontonechange", "ontonechange"),
|
||||
Atom("ontouchstart", "ontouchstart"),
|
||||
|
|
Загрузка…
Ссылка в новой задаче