diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
commit | 36d22d82aa202bb199967e9512281e9a53db42c9 (patch) | |
tree | 105e8c98ddea1c1e4784a60a5a6410fa416be2de /dom/events | |
parent | Initial commit. (diff) | |
download | firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.tar.xz firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.zip |
Adding upstream version 115.7.0esr.upstream/115.7.0esr
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'dom/events')
502 files changed, 88891 insertions, 0 deletions
diff --git a/dom/events/AnimationEvent.cpp b/dom/events/AnimationEvent.cpp new file mode 100644 index 0000000000..62a4c5196c --- /dev/null +++ b/dom/events/AnimationEvent.cpp @@ -0,0 +1,66 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#include "mozilla/dom/AnimationEvent.h" +#include "mozilla/ContentEvents.h" +#include "prtime.h" + +namespace mozilla::dom { + +AnimationEvent::AnimationEvent(EventTarget* aOwner, nsPresContext* aPresContext, + InternalAnimationEvent* aEvent) + : Event(aOwner, aPresContext, + aEvent ? aEvent : new InternalAnimationEvent(false, eVoidEvent)) { + if (aEvent) { + mEventIsInternal = false; + } else { + mEventIsInternal = true; + } +} + +// static +already_AddRefed<AnimationEvent> AnimationEvent::Constructor( + const GlobalObject& aGlobal, const nsAString& aType, + const AnimationEventInit& aParam) { + nsCOMPtr<EventTarget> t = do_QueryInterface(aGlobal.GetAsSupports()); + RefPtr<AnimationEvent> e = new AnimationEvent(t, nullptr, nullptr); + bool trusted = e->Init(t); + + e->InitEvent(aType, aParam.mBubbles, aParam.mCancelable); + + InternalAnimationEvent* internalEvent = e->mEvent->AsAnimationEvent(); + internalEvent->mAnimationName = aParam.mAnimationName; + internalEvent->mElapsedTime = aParam.mElapsedTime; + internalEvent->mPseudoElement = aParam.mPseudoElement; + + e->SetTrusted(trusted); + e->SetComposed(aParam.mComposed); + return e.forget(); +} + +void AnimationEvent::GetAnimationName(nsAString& aAnimationName) { + aAnimationName = mEvent->AsAnimationEvent()->mAnimationName; +} + +float AnimationEvent::ElapsedTime() { + return mEvent->AsAnimationEvent()->mElapsedTime; +} + +void AnimationEvent::GetPseudoElement(nsAString& aPseudoElement) { + aPseudoElement = mEvent->AsAnimationEvent()->mPseudoElement; +} + +} // namespace mozilla::dom + +using namespace mozilla; +using namespace mozilla::dom; + +already_AddRefed<AnimationEvent> NS_NewDOMAnimationEvent( + EventTarget* aOwner, nsPresContext* aPresContext, + InternalAnimationEvent* aEvent) { + RefPtr<AnimationEvent> it = new AnimationEvent(aOwner, aPresContext, aEvent); + return it.forget(); +} diff --git a/dom/events/AnimationEvent.h b/dom/events/AnimationEvent.h new file mode 100644 index 0000000000..eb40246069 --- /dev/null +++ b/dom/events/AnimationEvent.h @@ -0,0 +1,48 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ +#ifndef mozilla_dom_AnimationEvent_h_ +#define mozilla_dom_AnimationEvent_h_ + +#include "mozilla/EventForwards.h" +#include "mozilla/dom/Event.h" +#include "mozilla/dom/AnimationEventBinding.h" +#include "nsStringFwd.h" + +namespace mozilla::dom { + +class AnimationEvent : public Event { + public: + AnimationEvent(EventTarget* aOwner, nsPresContext* aPresContext, + InternalAnimationEvent* aEvent); + + NS_INLINE_DECL_REFCOUNTING_INHERITED(AnimationEvent, Event) + + static already_AddRefed<AnimationEvent> Constructor( + const GlobalObject& aGlobal, const nsAString& aType, + const AnimationEventInit& aParam); + + virtual JSObject* WrapObjectInternal( + JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override { + return AnimationEvent_Binding::Wrap(aCx, this, aGivenProto); + } + + void GetAnimationName(nsAString& aAnimationName); + + float ElapsedTime(); + + void GetPseudoElement(nsAString& aPseudoElement); + + protected: + ~AnimationEvent() = default; +}; + +} // namespace mozilla::dom + +already_AddRefed<mozilla::dom::AnimationEvent> NS_NewDOMAnimationEvent( + mozilla::dom::EventTarget* aOwner, nsPresContext* aPresContext, + mozilla::InternalAnimationEvent* aEvent); + +#endif // mozilla_dom_AnimationEvent_h_ diff --git a/dom/events/AsyncEventDispatcher.cpp b/dom/events/AsyncEventDispatcher.cpp new file mode 100644 index 0000000000..3c3a0a7aa3 --- /dev/null +++ b/dom/events/AsyncEventDispatcher.cpp @@ -0,0 +1,180 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#include "mozilla/AsyncEventDispatcher.h" +#include "mozilla/BasicEvents.h" +#include "mozilla/EventDispatcher.h" +#include "mozilla/dom/DocumentInlines.h" +#include "mozilla/dom/Event.h" +#include "mozilla/dom/EventTarget.h" +#include "nsContentUtils.h" + +namespace mozilla { + +using namespace dom; + +/****************************************************************************** + * mozilla::AsyncEventDispatcher + ******************************************************************************/ + +AsyncEventDispatcher::AsyncEventDispatcher(EventTarget* aTarget, + WidgetEvent& aEvent) + : CancelableRunnable("AsyncEventDispatcher"), + mTarget(aTarget), + mEventMessage(eUnidentifiedEvent) { + MOZ_ASSERT(mTarget); + RefPtr<Event> event = + EventDispatcher::CreateEvent(aTarget, nullptr, &aEvent, u""_ns); + mEvent = std::move(event); + mEventType.SetIsVoid(true); + NS_ASSERTION(mEvent, "Should never fail to create an event"); + mEvent->DuplicatePrivateData(); + mEvent->SetTrusted(aEvent.IsTrusted()); +} + +NS_IMETHODIMP +AsyncEventDispatcher::Run() { + if (mCanceled) { + return NS_OK; + } + nsINode* node = nsINode::FromEventTargetOrNull(mTarget); + if (mCheckStillInDoc) { + MOZ_ASSERT(node); + if (!node->IsInComposedDoc()) { + return NS_OK; + } + } + mTarget->AsyncEventRunning(this); + if (mEventMessage != eUnidentifiedEvent) { + MOZ_ASSERT(mComposed == Composed::eDefault); + return nsContentUtils::DispatchTrustedEvent<WidgetEvent>( + node->OwnerDoc(), mTarget, mEventMessage, mCanBubble, Cancelable::eNo, + nullptr /* aDefaultAction */, mOnlyChromeDispatch); + } + // MOZ_KnownLives because this instance shouldn't be touched while running. + if (mEvent) { + DispatchEventOnTarget(MOZ_KnownLive(mTarget), MOZ_KnownLive(mEvent), + mOnlyChromeDispatch, mComposed); + } else { + DispatchEventOnTarget(MOZ_KnownLive(mTarget), mEventType, mCanBubble, + mOnlyChromeDispatch, mComposed); + } + return NS_OK; +} + +// static +void AsyncEventDispatcher::DispatchEventOnTarget( + EventTarget* aTarget, const nsAString& aEventType, CanBubble aCanBubble, + ChromeOnlyDispatch aOnlyChromeDispatch, Composed aComposed) { + RefPtr<Event> event = NS_NewDOMEvent(aTarget, nullptr, nullptr); + event->InitEvent(aEventType, aCanBubble, Cancelable::eNo); + event->SetTrusted(true); + DispatchEventOnTarget(aTarget, event, aOnlyChromeDispatch, aComposed); +} + +// static +void AsyncEventDispatcher::DispatchEventOnTarget( + EventTarget* aTarget, Event* aEvent, ChromeOnlyDispatch aOnlyChromeDispatch, + Composed aComposed) { + if (aComposed != Composed::eDefault) { + aEvent->WidgetEventPtr()->mFlags.mComposed = aComposed == Composed::eYes; + } + if (aOnlyChromeDispatch == ChromeOnlyDispatch::eYes) { + MOZ_ASSERT(aEvent->IsTrusted()); + aEvent->WidgetEventPtr()->mFlags.mOnlyChromeDispatch = true; + } + aTarget->DispatchEvent(*aEvent); +} + +nsresult AsyncEventDispatcher::Cancel() { + mCanceled = true; + return NS_OK; +} + +nsresult AsyncEventDispatcher::PostDOMEvent() { + RefPtr<AsyncEventDispatcher> ensureDeletionWhenFailing = this; + if (NS_IsMainThread()) { + if (nsCOMPtr<nsIGlobalObject> global = mTarget->GetOwnerGlobal()) { + return global->Dispatch(TaskCategory::Other, + ensureDeletionWhenFailing.forget()); + } + + // Sometimes GetOwnerGlobal returns null because it uses + // GetScriptHandlingObject rather than GetScopeObject. + if (nsINode* node = nsINode::FromEventTargetOrNull(mTarget)) { + RefPtr<Document> doc = node->OwnerDoc(); + return doc->Dispatch(TaskCategory::Other, + ensureDeletionWhenFailing.forget()); + } + } + return NS_DispatchToCurrentThread(this); +} + +void AsyncEventDispatcher::RunDOMEventWhenSafe() { + RefPtr<AsyncEventDispatcher> ensureDeletionWhenFailing = this; + nsContentUtils::AddScriptRunner(this); +} + +// static +void AsyncEventDispatcher::RunDOMEventWhenSafe( + EventTarget& aTarget, const nsAString& aEventType, CanBubble aCanBubble, + ChromeOnlyDispatch aOnlyChromeDispatch /* = ChromeOnlyDispatch::eNo */, + Composed aComposed /* = Composed::eDefault */) { + if (nsContentUtils::IsSafeToRunScript()) { + OwningNonNull<EventTarget> target = aTarget; + DispatchEventOnTarget(target, aEventType, aCanBubble, aOnlyChromeDispatch, + aComposed); + return; + } + (new AsyncEventDispatcher(&aTarget, aEventType, aCanBubble, + aOnlyChromeDispatch, aComposed)) + ->RunDOMEventWhenSafe(); +} + +void AsyncEventDispatcher::RunDOMEventWhenSafe( + EventTarget& aTarget, Event& aEvent, + ChromeOnlyDispatch aOnlyChromeDispatch /* = ChromeOnlyDispatch::eNo */) { + if (nsContentUtils::IsSafeToRunScript()) { + DispatchEventOnTarget(&aTarget, &aEvent, aOnlyChromeDispatch, + Composed::eDefault); + return; + } + (new AsyncEventDispatcher(&aTarget, &aEvent, aOnlyChromeDispatch)) + ->RunDOMEventWhenSafe(); +} + +// static +nsresult AsyncEventDispatcher::RunDOMEventWhenSafe( + nsINode& aTarget, WidgetEvent& aEvent, + nsEventStatus* aEventStatus /* = nullptr */) { + if (nsContentUtils::IsSafeToRunScript()) { + // MOZ_KnownLive due to bug 1832202 + nsPresContext* presContext = aTarget.OwnerDoc()->GetPresContext(); + return EventDispatcher::Dispatch(MOZ_KnownLive(&aTarget), + MOZ_KnownLive(presContext), &aEvent, + nullptr, aEventStatus); + } + (new AsyncEventDispatcher(&aTarget, aEvent))->RunDOMEventWhenSafe(); + return NS_OK; +} + +void AsyncEventDispatcher::RequireNodeInDocument() { + MOZ_ASSERT(mTarget); + MOZ_ASSERT(mTarget->IsNode()); + mCheckStillInDoc = true; +} + +/****************************************************************************** + * mozilla::LoadBlockingAsyncEventDispatcher + ******************************************************************************/ + +LoadBlockingAsyncEventDispatcher::~LoadBlockingAsyncEventDispatcher() { + if (mBlockedDoc) { + mBlockedDoc->UnblockOnload(true); + } +} + +} // namespace mozilla diff --git a/dom/events/AsyncEventDispatcher.h b/dom/events/AsyncEventDispatcher.h new file mode 100644 index 0000000000..671a153b41 --- /dev/null +++ b/dom/events/AsyncEventDispatcher.h @@ -0,0 +1,209 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#ifndef mozilla_AsyncEventDispatcher_h_ +#define mozilla_AsyncEventDispatcher_h_ + +#include "mozilla/Attributes.h" +#include "mozilla/EventForwards.h" +#include "mozilla/RefPtr.h" +#include "mozilla/dom/Event.h" +#include "nsCOMPtr.h" +#include "mozilla/dom/Document.h" +#include "nsString.h" +#include "nsThreadUtils.h" + +class nsINode; + +namespace mozilla { + +/** + * Use AsyncEventDispatcher to fire a DOM event that requires safe a stable DOM. + * For example, you may need to fire an event from within layout, but + * want to ensure that the event handler doesn't mutate the DOM at + * the wrong time, in order to avoid resulting instability. + */ + +class AsyncEventDispatcher : public CancelableRunnable { + public: + /** + * If aOnlyChromeDispatch is true, the event is dispatched to only + * chrome node. In that case, if aTarget is already a chrome node, + * the event is dispatched to it, otherwise the dispatch path starts + * at the first chrome ancestor of that target. + */ + AsyncEventDispatcher( + dom::EventTarget* aTarget, const nsAString& aEventType, + CanBubble aCanBubble, + ChromeOnlyDispatch aOnlyChromeDispatch = ChromeOnlyDispatch::eNo, + Composed aComposed = Composed::eDefault) + : CancelableRunnable("AsyncEventDispatcher"), + mTarget(aTarget), + mEventType(aEventType), + mEventMessage(eUnidentifiedEvent), + mCanBubble(aCanBubble), + mOnlyChromeDispatch(aOnlyChromeDispatch), + mComposed(aComposed) {} + + /** + * If aOnlyChromeDispatch is true, the event is dispatched to only + * chrome node. In that case, if aTarget is already a chrome node, + * the event is dispatched to it, otherwise the dispatch path starts + * at the first chrome ancestor of that target. + */ + AsyncEventDispatcher(nsINode* aTarget, mozilla::EventMessage aEventMessage, + CanBubble aCanBubble, + ChromeOnlyDispatch aOnlyChromeDispatch) + : CancelableRunnable("AsyncEventDispatcher"), + mTarget(aTarget), + mEventMessage(aEventMessage), + mCanBubble(aCanBubble), + mOnlyChromeDispatch(aOnlyChromeDispatch) { + mEventType.SetIsVoid(true); + MOZ_ASSERT(mEventMessage != eUnidentifiedEvent); + } + + AsyncEventDispatcher(dom::EventTarget* aTarget, + mozilla::EventMessage aEventMessage, + CanBubble aCanBubble) + : CancelableRunnable("AsyncEventDispatcher"), + mTarget(aTarget), + mEventMessage(aEventMessage), + mCanBubble(aCanBubble) { + mEventType.SetIsVoid(true); + MOZ_ASSERT(mEventMessage != eUnidentifiedEvent); + } + + /** + * aEvent must have been created without Widget*Event and Internal*Event + * because this constructor assumes that it's safe to use aEvent + * asynchronously (i.e., after all objects allocated in the stack are + * destroyed). + */ + AsyncEventDispatcher( + dom::EventTarget* aTarget, dom::Event* aEvent, + ChromeOnlyDispatch aOnlyChromeDispatch = ChromeOnlyDispatch::eNo) + : CancelableRunnable("AsyncEventDispatcher"), + mTarget(aTarget), + mEvent(aEvent), + mEventMessage(eUnidentifiedEvent), + mOnlyChromeDispatch(aOnlyChromeDispatch) { + MOZ_ASSERT( + aEvent->IsSafeToBeDispatchedAsynchronously(), + "The DOM event should be created without Widget*Event and " + "Internal*Event " + "because if it needs to be safe to be dispatched asynchronously"); + } + + AsyncEventDispatcher(dom::EventTarget* aTarget, WidgetEvent& aEvent); + + MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHOD Run() override; + nsresult Cancel() override; + nsresult PostDOMEvent(); + + /** + * Dispatch event immediately if it's safe to dispatch the event. + * Otherwise, posting the event into the queue to dispatch it when it's safe. + * + * Note that this method allows callers to call itself with unsafe aTarget + * because its lifetime is guaranteed by this method (in the case of + * synchronous dispatching) or AsyncEventDispatcher (in the case of + * asynchronous dispatching). + */ + MOZ_CAN_RUN_SCRIPT_BOUNDARY static void RunDOMEventWhenSafe( + dom::EventTarget& aTarget, const nsAString& aEventType, + CanBubble aCanBubble, + ChromeOnlyDispatch aOnlyChromeDispatch = ChromeOnlyDispatch::eNo, + Composed aComposed = Composed::eDefault); + + /** + * Dispatch event immediately if it's safe to dispatch the event. + * Otherwise, posting the event into the queue to dispatch it when it's safe. + * + * NOTE: Only this is now marked as MOZ_CAN_RUN_SCRIPT because all callers + * have already had safe smart pointers for both aTarget and aEvent. + * If you need to use this in a hot path without smart pointers, you may need + * to create unsafe version of this method for avoiding the extra add/release + * refcount cost in the case of asynchronous dispatching. + */ + MOZ_CAN_RUN_SCRIPT static void RunDOMEventWhenSafe( + dom::EventTarget& aTarget, dom::Event& aEvent, + ChromeOnlyDispatch aOnlyChromeDispatch = ChromeOnlyDispatch::eNo); + + /** + * Dispatch event immediately if it's safe to dispatch the event. + * Otherwise, posting the event into the queue to dispatch it when it's safe. + * + * Note that this method allows callers to call itself with unsafe aTarget + * because its lifetime is guaranteed by EventDispatcher (in the case of + * synchronous dispatching) or AsyncEventDispatcher (in the case of + * asynchronous dispatching). + */ + MOZ_CAN_RUN_SCRIPT_BOUNDARY static nsresult RunDOMEventWhenSafe( + nsINode& aTarget, WidgetEvent& aEvent, + nsEventStatus* aEventStatus = nullptr); + + // Calling this causes the Run() method to check that + // mTarget->IsInComposedDoc(). mTarget must be an nsINode or else we'll + // assert. + void RequireNodeInDocument(); + + protected: + void RunDOMEventWhenSafe(); + MOZ_CAN_RUN_SCRIPT static void DispatchEventOnTarget( + dom::EventTarget* aTarget, dom::Event* aEvent, + ChromeOnlyDispatch aOnlyChromeDispatch, Composed aComposed); + MOZ_CAN_RUN_SCRIPT static void DispatchEventOnTarget( + dom::EventTarget* aTarget, const nsAString& aEventType, + CanBubble aCanBubble, ChromeOnlyDispatch aOnlyChromeDispatch, + Composed aComposed); + + public: + nsCOMPtr<dom::EventTarget> mTarget; + RefPtr<dom::Event> mEvent; + // If mEventType is set, mEventMessage will be eUnidentifiedEvent. + // If mEventMessage is set, mEventType will be void. + // They can never both be set at the same time. + nsString mEventType; + EventMessage mEventMessage; + CanBubble mCanBubble = CanBubble::eNo; + ChromeOnlyDispatch mOnlyChromeDispatch = ChromeOnlyDispatch::eNo; + Composed mComposed = Composed::eDefault; + bool mCanceled = false; + bool mCheckStillInDoc = false; +}; + +class LoadBlockingAsyncEventDispatcher final : public AsyncEventDispatcher { + public: + LoadBlockingAsyncEventDispatcher(nsINode* aEventNode, + const nsAString& aEventType, + CanBubble aBubbles, + ChromeOnlyDispatch aDispatchChromeOnly) + : AsyncEventDispatcher(aEventNode, aEventType, aBubbles, + aDispatchChromeOnly), + mBlockedDoc(aEventNode->OwnerDoc()) { + if (mBlockedDoc) { + mBlockedDoc->BlockOnload(); + } + } + + LoadBlockingAsyncEventDispatcher(nsINode* aEventNode, dom::Event* aEvent) + : AsyncEventDispatcher(aEventNode, aEvent), + mBlockedDoc(aEventNode->OwnerDoc()) { + if (mBlockedDoc) { + mBlockedDoc->BlockOnload(); + } + } + + ~LoadBlockingAsyncEventDispatcher(); + + private: + RefPtr<dom::Document> mBlockedDoc; +}; + +} // namespace mozilla + +#endif // mozilla_AsyncEventDispatcher_h_ diff --git a/dom/events/BeforeUnloadEvent.cpp b/dom/events/BeforeUnloadEvent.cpp new file mode 100644 index 0000000000..77f41d0010 --- /dev/null +++ b/dom/events/BeforeUnloadEvent.cpp @@ -0,0 +1,29 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#include "mozilla/dom/BeforeUnloadEvent.h" + +namespace mozilla::dom { + +void BeforeUnloadEvent::SetReturnValue(const nsAString& aReturnValue) { + mText = aReturnValue; +} + +void BeforeUnloadEvent::GetReturnValue(nsAString& aReturnValue) { + aReturnValue = mText; +} + +} // namespace mozilla::dom + +using namespace mozilla; +using namespace mozilla::dom; + +already_AddRefed<BeforeUnloadEvent> NS_NewDOMBeforeUnloadEvent( + EventTarget* aOwner, nsPresContext* aPresContext, WidgetEvent* aEvent) { + RefPtr<BeforeUnloadEvent> it = + new BeforeUnloadEvent(aOwner, aPresContext, aEvent); + return it.forget(); +} diff --git a/dom/events/BeforeUnloadEvent.h b/dom/events/BeforeUnloadEvent.h new file mode 100644 index 0000000000..2ffc34cfea --- /dev/null +++ b/dom/events/BeforeUnloadEvent.h @@ -0,0 +1,45 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#ifndef mozilla_dom_BeforeUnloadEvent_h_ +#define mozilla_dom_BeforeUnloadEvent_h_ + +#include "mozilla/dom/BeforeUnloadEventBinding.h" +#include "mozilla/dom/Event.h" + +namespace mozilla::dom { + +class BeforeUnloadEvent : public Event { + public: + BeforeUnloadEvent(EventTarget* aOwner, nsPresContext* aPresContext, + WidgetEvent* aEvent) + : Event(aOwner, aPresContext, aEvent) {} + + virtual BeforeUnloadEvent* AsBeforeUnloadEvent() override { return this; } + + virtual JSObject* WrapObjectInternal( + JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override { + return BeforeUnloadEvent_Binding::Wrap(aCx, this, aGivenProto); + } + + NS_INLINE_DECL_REFCOUNTING_INHERITED(BeforeUnloadEvent, Event) + + void GetReturnValue(nsAString& aReturnValue); + void SetReturnValue(const nsAString& aReturnValue); + + protected: + ~BeforeUnloadEvent() = default; + + nsString mText; +}; + +} // namespace mozilla::dom + +already_AddRefed<mozilla::dom::BeforeUnloadEvent> NS_NewDOMBeforeUnloadEvent( + mozilla::dom::EventTarget* aOwner, nsPresContext* aPresContext, + mozilla::WidgetEvent* aEvent); + +#endif // mozilla_dom_BeforeUnloadEvent_h_ diff --git a/dom/events/Clipboard.cpp b/dom/events/Clipboard.cpp new file mode 100644 index 0000000000..090f06834b --- /dev/null +++ b/dom/events/Clipboard.cpp @@ -0,0 +1,820 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#include "mozilla/dom/Clipboard.h" + +#include <algorithm> + +#include "mozilla/AbstractThread.h" +#include "mozilla/BasePrincipal.h" +#include "mozilla/RefPtr.h" +#include "mozilla/Result.h" +#include "mozilla/ResultVariant.h" +#include "mozilla/dom/BlobBinding.h" +#include "mozilla/dom/ClipboardItem.h" +#include "mozilla/dom/ClipboardBinding.h" +#include "mozilla/dom/ContentChild.h" +#include "mozilla/dom/Promise.h" +#include "mozilla/dom/PromiseNativeHandler.h" +#include "mozilla/dom/DataTransfer.h" +#include "mozilla/dom/DataTransferItemList.h" +#include "mozilla/dom/DataTransferItem.h" +#include "mozilla/dom/Document.h" +#include "mozilla/StaticPrefs_dom.h" +#include "imgIContainer.h" +#include "imgITools.h" +#include "nsArrayUtils.h" +#include "nsComponentManagerUtils.h" +#include "nsContentUtils.h" +#include "nsIClipboard.h" +#include "nsIInputStream.h" +#include "nsIParserUtils.h" +#include "nsISupportsPrimitives.h" +#include "nsITransferable.h" +#include "nsNetUtil.h" +#include "nsServiceManagerUtils.h" +#include "nsStringStream.h" +#include "nsTArray.h" +#include "nsThreadUtils.h" +#include "nsVariant.h" + +static mozilla::LazyLogModule gClipboardLog("Clipboard"); + +namespace mozilla::dom { + +Clipboard::Clipboard(nsPIDOMWindowInner* aWindow) + : DOMEventTargetHelper(aWindow) {} + +Clipboard::~Clipboard() = default; + +// static +bool Clipboard::IsTestingPrefEnabledOrHasReadPermission( + nsIPrincipal& aSubjectPrincipal) { + return IsTestingPrefEnabled() || + nsContentUtils::PrincipalHasPermission(aSubjectPrincipal, + nsGkAtoms::clipboardRead); +} + +// @return true iff the event was dispatched successfully. +static bool MaybeCreateAndDispatchMozClipboardReadPasteEvent( + nsPIDOMWindowInner& aOwner) { + RefPtr<Document> document = aOwner.GetDoc(); + + if (!document) { + // Presumably, this shouldn't happen but to be safe, this case is handled. + MOZ_LOG(Clipboard::GetClipboardLog(), LogLevel::Debug, + ("%s: no document.", __FUNCTION__)); + return false; + } + + // Conceptionally, `ClipboardReadPasteChild` is the target of the event. + // It ensures to receive the event by declaring the event in + // <BrowserGlue.sys.mjs>. + return !NS_WARN_IF(NS_FAILED(nsContentUtils::DispatchChromeEvent( + document, ToSupports(document), u"MozClipboardReadPaste"_ns, + CanBubble::eNo, Cancelable::eNo))); +} + +void Clipboard::ReadRequest::Answer() { + RefPtr<Promise> p(std::move(mPromise)); + RefPtr<nsPIDOMWindowInner> owner(std::move(mOwner)); + + nsresult rv; + nsCOMPtr<nsIClipboard> clipboardService( + do_GetService("@mozilla.org/widget/clipboard;1", &rv)); + if (NS_FAILED(rv)) { + p->MaybeReject(NS_ERROR_UNEXPECTED); + return; + } + + switch (mType) { + case ReadRequestType::eRead: { + clipboardService + ->AsyncHasDataMatchingFlavors( + // Mandatory data types defined in + // https://w3c.github.io/clipboard-apis/#mandatory-data-types-x + AutoTArray<nsCString, 3>{nsDependentCString(kHTMLMime), + nsDependentCString(kTextMime), + nsDependentCString(kPNGImageMime)}, + nsIClipboard::kGlobalClipboard) + ->Then( + GetMainThreadSerialEventTarget(), __func__, + /* resolve */ + [owner, p](nsTArray<nsCString> formats) { + nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(owner); + if (NS_WARN_IF(!global)) { + p->MaybeReject(NS_ERROR_UNEXPECTED); + return; + } + + AutoTArray<RefPtr<ClipboardItem::ItemEntry>, 3> entries; + for (const auto& format : formats) { + nsCOMPtr<nsITransferable> trans = + do_CreateInstance("@mozilla.org/widget/transferable;1"); + if (NS_WARN_IF(!trans)) { + continue; + } + + trans->Init(nullptr); + trans->AddDataFlavor(format.get()); + + RefPtr<ClipboardItem::ItemEntry> entry = + MakeRefPtr<ClipboardItem::ItemEntry>( + global, NS_ConvertUTF8toUTF16(format)); + entry->LoadDataFromSystemClipboard(*trans); + entries.AppendElement(std::move(entry)); + } + + // We currently only support one clipboard item. + AutoTArray<RefPtr<ClipboardItem>, 1> items; + items.AppendElement(MakeRefPtr<ClipboardItem>( + global, PresentationStyle::Unspecified, + std::move(entries))); + + p->MaybeResolve(std::move(items)); + }, + /* reject */ + [p](nsresult rv) { p->MaybeReject(rv); }); + break; + } + case ReadRequestType::eReadText: { + nsCOMPtr<nsITransferable> trans = + do_CreateInstance("@mozilla.org/widget/transferable;1"); + if (NS_WARN_IF(!trans)) { + p->MaybeReject(NS_ERROR_UNEXPECTED); + return; + } + + trans->Init(nullptr); + trans->AddDataFlavor(kTextMime); + clipboardService->AsyncGetData(trans, nsIClipboard::kGlobalClipboard) + ->Then( + GetMainThreadSerialEventTarget(), __func__, + /* resolve */ + [trans, p]() { + nsCOMPtr<nsISupports> data; + nsresult rv = + trans->GetTransferData(kTextMime, getter_AddRefs(data)); + + nsAutoString str; + if (!NS_WARN_IF(NS_FAILED(rv))) { + nsCOMPtr<nsISupportsString> supportsstr = + do_QueryInterface(data); + MOZ_ASSERT(supportsstr); + if (supportsstr) { + supportsstr->GetData(str); + } + } + + p->MaybeResolve(str); + }, + /* reject */ + [p](nsresult rv) { p->MaybeReject(rv); }); + break; + } + default: { + MOZ_ASSERT_UNREACHABLE("Unknown read type"); + break; + } + } +} + +static bool IsReadTextExposedToContent() { + return StaticPrefs::dom_events_asyncClipboard_readText_DoNotUseDirectly(); +} + +void Clipboard::CheckReadPermissionAndHandleRequest( + Promise& aPromise, nsIPrincipal& aSubjectPrincipal, ReadRequestType aType) { + if (IsTestingPrefEnabledOrHasReadPermission(aSubjectPrincipal)) { + MOZ_LOG(GetClipboardLog(), LogLevel::Debug, + ("%s: testing pref enabled or has read permission", __FUNCTION__)); + nsPIDOMWindowInner* owner = GetOwner(); + if (!owner) { + aPromise.MaybeRejectWithUndefined(); + return; + } + + ReadRequest{aPromise, aType, *owner}.Answer(); + return; + } + + if (aSubjectPrincipal.GetIsAddonOrExpandedAddonPrincipal()) { + // TODO: enable showing the "Paste" button in this case; see bug 1773681. + MOZ_LOG(GetClipboardLog(), LogLevel::Debug, + ("%s: Addon without read permssion.", __FUNCTION__)); + aPromise.MaybeRejectWithUndefined(); + return; + } + + HandleReadRequestWhichRequiresPasteButton(aPromise, aType); +} + +void Clipboard::HandleReadRequestWhichRequiresPasteButton( + Promise& aPromise, ReadRequestType aType) { + nsPIDOMWindowInner* owner = GetOwner(); + WindowContext* windowContext = owner ? owner->GetWindowContext() : nullptr; + if (!windowContext) { + MOZ_ASSERT_UNREACHABLE("There should be a WindowContext."); + aPromise.MaybeRejectWithUndefined(); + return; + } + + // If no transient user activation, reject the promise and return. + if (!windowContext->HasValidTransientUserGestureActivation()) { + aPromise.MaybeRejectWithNotAllowedError( + "Clipboard read request was blocked due to lack of " + "user activation."); + return; + } + + // TODO: when a user activation stems from a contextmenu event + // (https://developer.mozilla.org/en-US/docs/Web/API/Element/contextmenu_event), + // forbid pasting (bug 1767941). + + switch (mTransientUserPasteState.RefreshAndGet(*windowContext)) { + case TransientUserPasteState::Value::Initial: { + MOZ_ASSERT(mReadRequests.IsEmpty()); + + if (MaybeCreateAndDispatchMozClipboardReadPasteEvent(*owner)) { + mTransientUserPasteState.OnStartWaitingForUserReactionToPasteMenuPopup( + windowContext->GetUserGestureStart()); + mReadRequests.AppendElement( + MakeUnique<ReadRequest>(aPromise, aType, *owner)); + } else { + // This shouldn't happen but let's handle this case. + aPromise.MaybeRejectWithUndefined(); + } + break; + } + case TransientUserPasteState::Value:: + WaitingForUserReactionToPasteMenuPopup: { + MOZ_ASSERT(!mReadRequests.IsEmpty()); + + mReadRequests.AppendElement( + MakeUnique<ReadRequest>(aPromise, aType, *owner)); + break; + } + case TransientUserPasteState::Value::TransientlyForbiddenByUser: { + aPromise.MaybeRejectWithNotAllowedError( + "`Clipboard read request was blocked due to the user " + "dismissing the 'Paste' button."); + break; + } + case TransientUserPasteState::Value::TransientlyAllowedByUser: { + ReadRequest{aPromise, aType, *owner}.Answer(); + break; + } + } +} + +already_AddRefed<Promise> Clipboard::ReadHelper(nsIPrincipal& aSubjectPrincipal, + ReadRequestType aType, + ErrorResult& aRv) { + // Create a new promise + RefPtr<Promise> p = dom::Promise::Create(GetOwnerGlobal(), aRv); + if (aRv.Failed()) { + return nullptr; + } + + CheckReadPermissionAndHandleRequest(*p, aSubjectPrincipal, aType); + return p.forget(); +} + +auto Clipboard::TransientUserPasteState::RefreshAndGet( + WindowContext& aWindowContext) -> Value { + MOZ_ASSERT(aWindowContext.HasValidTransientUserGestureActivation()); + + switch (mValue) { + case Value::Initial: { + MOZ_ASSERT(mUserGestureStart.IsNull()); + break; + } + case Value::WaitingForUserReactionToPasteMenuPopup: { + MOZ_ASSERT(!mUserGestureStart.IsNull()); + MOZ_ASSERT( + mUserGestureStart == aWindowContext.GetUserGestureStart(), + "A new transient user gesture activation should be impossible while " + "there's no response to the 'Paste' button."); + // `OnUserReactedToPasteMenuPopup` will handle the reaction. + break; + } + case Value::TransientlyForbiddenByUser: { + [[fallthrough]]; + } + case Value::TransientlyAllowedByUser: { + MOZ_ASSERT(!mUserGestureStart.IsNull()); + + if (mUserGestureStart != aWindowContext.GetUserGestureStart()) { + *this = {}; + } + break; + } + } + + return mValue; +} + +void Clipboard::TransientUserPasteState:: + OnStartWaitingForUserReactionToPasteMenuPopup( + const TimeStamp& aUserGestureStart) { + MOZ_ASSERT(mValue == Value::Initial); + MOZ_ASSERT(!aUserGestureStart.IsNull()); + + mValue = Value::WaitingForUserReactionToPasteMenuPopup; + mUserGestureStart = aUserGestureStart; +} + +void Clipboard::TransientUserPasteState::OnUserReactedToPasteMenuPopup( + const bool aAllowed) { + mValue = aAllowed ? Value::TransientlyAllowedByUser + : Value::TransientlyForbiddenByUser; +} + +already_AddRefed<Promise> Clipboard::Read(nsIPrincipal& aSubjectPrincipal, + ErrorResult& aRv) { + return ReadHelper(aSubjectPrincipal, ReadRequestType::eRead, aRv); +} + +already_AddRefed<Promise> Clipboard::ReadText(nsIPrincipal& aSubjectPrincipal, + ErrorResult& aRv) { + return ReadHelper(aSubjectPrincipal, ReadRequestType::eReadText, aRv); +} + +namespace { + +struct NativeEntry { + nsString mType; + nsCOMPtr<nsIVariant> mData; + + NativeEntry(const nsAString& aType, nsIVariant* aData) + : mType(aType), mData(aData) {} +}; +using NativeEntryPromise = MozPromise<NativeEntry, CopyableErrorResult, false>; + +class BlobTextHandler final : public PromiseNativeHandler { + public: + NS_DECL_THREADSAFE_ISUPPORTS + + explicit BlobTextHandler(const nsAString& aType) : mType(aType) {} + + RefPtr<NativeEntryPromise> Promise() { return mHolder.Ensure(__func__); } + + void Reject() { + CopyableErrorResult rv; + rv.ThrowUnknownError("Unable to read blob for '"_ns + + NS_ConvertUTF16toUTF8(mType) + "' as text."_ns); + mHolder.Reject(rv, __func__); + } + + void ResolvedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue, + ErrorResult& aRv) override { + AssertIsOnMainThread(); + + nsString text; + if (!ConvertJSValueToUSVString(aCx, aValue, "ClipboardItem text", text)) { + Reject(); + return; + } + + RefPtr<nsVariantCC> variant = new nsVariantCC(); + variant->SetAsAString(text); + + NativeEntry native(mType, variant); + mHolder.Resolve(std::move(native), __func__); + } + + void RejectedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue, + ErrorResult& aRv) override { + Reject(); + } + + private: + ~BlobTextHandler() = default; + + nsString mType; + MozPromiseHolder<NativeEntryPromise> mHolder; +}; + +NS_IMPL_ISUPPORTS0(BlobTextHandler) + +static RefPtr<NativeEntryPromise> GetStringNativeEntry( + const nsAString& aType, const OwningStringOrBlob& aData) { + if (aData.IsString()) { + RefPtr<nsVariantCC> variant = new nsVariantCC(); + variant->SetAsAString(aData.GetAsString()); + NativeEntry native(aType, variant); + return NativeEntryPromise::CreateAndResolve(native, __func__); + } + + RefPtr<BlobTextHandler> handler = new BlobTextHandler(aType); + IgnoredErrorResult ignored; + RefPtr<Promise> promise = aData.GetAsBlob()->Text(ignored); + if (ignored.Failed()) { + CopyableErrorResult rv; + rv.ThrowUnknownError("Unable to read blob for '"_ns + + NS_ConvertUTF16toUTF8(aType) + "' as text."_ns); + return NativeEntryPromise::CreateAndReject(rv, __func__); + } + promise->AppendNativeHandler(handler); + return handler->Promise(); +} + +class ImageDecodeCallback final : public imgIContainerCallback { + public: + NS_DECL_ISUPPORTS + + explicit ImageDecodeCallback(const nsAString& aType) : mType(aType) {} + + RefPtr<NativeEntryPromise> Promise() { return mHolder.Ensure(__func__); } + + NS_IMETHOD OnImageReady(imgIContainer* aImage, nsresult aStatus) override { + // Request the image's width to force decoding the image header. + int32_t ignored; + if (NS_FAILED(aStatus) || NS_FAILED(aImage->GetWidth(&ignored))) { + CopyableErrorResult rv; + rv.ThrowDataError("Unable to decode blob for '"_ns + + NS_ConvertUTF16toUTF8(mType) + "' as image."_ns); + mHolder.Reject(rv, __func__); + return NS_OK; + } + + RefPtr<nsVariantCC> variant = new nsVariantCC(); + variant->SetAsISupports(aImage); + + // Note: We always put the image as "native" on the clipboard. + NativeEntry native(NS_LITERAL_STRING_FROM_CSTRING(kNativeImageMime), + variant); + mHolder.Resolve(std::move(native), __func__); + return NS_OK; + }; + + private: + ~ImageDecodeCallback() = default; + + nsString mType; + MozPromiseHolder<NativeEntryPromise> mHolder; +}; + +NS_IMPL_ISUPPORTS(ImageDecodeCallback, imgIContainerCallback) + +static RefPtr<NativeEntryPromise> GetImageNativeEntry( + const nsAString& aType, const OwningStringOrBlob& aData) { + if (aData.IsString()) { + CopyableErrorResult rv; + rv.ThrowTypeError("DOMString not supported for '"_ns + + NS_ConvertUTF16toUTF8(aType) + "' as image data."_ns); + return NativeEntryPromise::CreateAndReject(rv, __func__); + } + + IgnoredErrorResult ignored; + nsCOMPtr<nsIInputStream> stream; + aData.GetAsBlob()->CreateInputStream(getter_AddRefs(stream), ignored); + if (ignored.Failed()) { + CopyableErrorResult rv; + rv.ThrowUnknownError("Unable to read blob for '"_ns + + NS_ConvertUTF16toUTF8(aType) + "' as image."_ns); + return NativeEntryPromise::CreateAndReject(rv, __func__); + } + + RefPtr<ImageDecodeCallback> callback = new ImageDecodeCallback(aType); + nsCOMPtr<imgITools> imgtool = do_CreateInstance("@mozilla.org/image/tools;1"); + imgtool->DecodeImageAsync(stream, NS_ConvertUTF16toUTF8(aType), callback, + GetMainThreadSerialEventTarget()); + return callback->Promise(); +} + +static Result<NativeEntry, ErrorResult> SanitizeNativeEntry( + const NativeEntry& aEntry) { + MOZ_ASSERT(aEntry.mType.EqualsLiteral(kHTMLMime)); + + nsAutoString string; + aEntry.mData->GetAsAString(string); + + nsCOMPtr<nsIParserUtils> parserUtils = + do_GetService(NS_PARSERUTILS_CONTRACTID); + if (!parserUtils) { + ErrorResult rv; + rv.ThrowUnknownError("Error while processing '"_ns + + NS_ConvertUTF16toUTF8(aEntry.mType) + "'."_ns); + return Err(std::move(rv)); + } + + uint32_t flags = nsIParserUtils::SanitizerAllowStyle | + nsIParserUtils::SanitizerAllowComments; + nsAutoString sanitized; + if (NS_FAILED(parserUtils->Sanitize(string, flags, sanitized))) { + ErrorResult rv; + rv.ThrowUnknownError("Error while processing '"_ns + + NS_ConvertUTF16toUTF8(aEntry.mType) + "'."_ns); + return Err(std::move(rv)); + } + + RefPtr<nsVariantCC> variant = new nsVariantCC(); + variant->SetAsAString(sanitized); + return NativeEntry(aEntry.mType, variant); +} + +static RefPtr<NativeEntryPromise> GetNativeEntry( + const nsAString& aType, const OwningStringOrBlob& aData) { + if (aType.EqualsLiteral(kPNGImageMime)) { + return GetImageNativeEntry(aType, aData); + } + + RefPtr<NativeEntryPromise> promise = GetStringNativeEntry(aType, aData); + if (aType.EqualsLiteral(kHTMLMime)) { + promise = promise->Then( + GetMainThreadSerialEventTarget(), __func__, + [](const NativeEntryPromise::ResolveOrRejectValue& aValue) + -> RefPtr<NativeEntryPromise> { + if (aValue.IsReject()) { + return NativeEntryPromise::CreateAndReject(aValue.RejectValue(), + __func__); + } + + auto sanitized = SanitizeNativeEntry(aValue.ResolveValue()); + if (sanitized.isErr()) { + return NativeEntryPromise::CreateAndReject( + CopyableErrorResult(sanitized.unwrapErr()), __func__); + } + return NativeEntryPromise::CreateAndResolve(sanitized.unwrap(), + __func__); + }); + } + return promise; +} + +// Restrict to types allowed by Chrome +// SVG is still disabled by default in Chrome. +static bool IsValidType(const nsAString& aType) { + return aType.EqualsLiteral(kPNGImageMime) || aType.EqualsLiteral(kTextMime) || + aType.EqualsLiteral(kHTMLMime); +} + +using NativeItemPromise = NativeEntryPromise::AllPromiseType; +static RefPtr<NativeItemPromise> GetClipboardNativeItem( + const ClipboardItem& aItem) { + nsTArray<RefPtr<NativeEntryPromise>> promises; + for (const auto& entry : aItem.Entries()) { + const nsAString& type = entry->Type(); + if (!IsValidType(type)) { + CopyableErrorResult rv; + rv.ThrowNotAllowedError("Type '"_ns + NS_ConvertUTF16toUTF8(type) + + "' not supported for write"_ns); + return NativeItemPromise::CreateAndReject(rv, __func__); + } + + using GetDataPromise = ClipboardItem::ItemEntry::GetDataPromise; + promises.AppendElement(entry->GetData()->Then( + GetMainThreadSerialEventTarget(), __func__, + [t = nsString(type)](const GetDataPromise::ResolveOrRejectValue& aValue) + -> RefPtr<NativeEntryPromise> { + if (aValue.IsReject()) { + return NativeEntryPromise::CreateAndReject( + CopyableErrorResult(aValue.RejectValue()), __func__); + } + + return GetNativeEntry(t, aValue.ResolveValue()); + })); + } + return NativeEntryPromise::All(GetCurrentSerialEventTarget(), promises); +} + +class ClipboardWriteCallback final : public nsIAsyncSetClipboardDataCallback { + public: + // This object will never be held by a cycle-collected object, so it doesn't + // need to be cycle-collected despite holding alive cycle-collected objects. + NS_DECL_ISUPPORTS + + explicit ClipboardWriteCallback(Promise* aPromise, + ClipboardItem* aClipboardItem) + : mPromise(aPromise), mClipboardItem(aClipboardItem) {} + + // nsIAsyncSetClipboardDataCallback + NS_IMETHOD OnComplete(nsresult aResult) override { + MOZ_ASSERT(mPromise); + + RefPtr<Promise> promise = std::move(mPromise); + // XXX We need to check state here is because the promise might be rejected + // before the callback is called, we probably could wrap the promise into a + // structure to make it less confused. + if (promise->State() == Promise::PromiseState::Pending) { + if (NS_FAILED(aResult)) { + promise->MaybeRejectWithNotAllowedError( + "Clipboard write is not allowed."); + return NS_OK; + } + + promise->MaybeResolveWithUndefined(); + } + + return NS_OK; + } + + protected: + ~ClipboardWriteCallback() { + // Callback should be notified. + MOZ_ASSERT(!mPromise); + }; + + // It will be reset to nullptr once callback is notified. + RefPtr<Promise> mPromise; + // Keep ClipboardItem alive until clipboard write is done. + RefPtr<ClipboardItem> mClipboardItem; +}; + +NS_IMPL_ISUPPORTS(ClipboardWriteCallback, nsIAsyncSetClipboardDataCallback) + +} // namespace + +already_AddRefed<Promise> Clipboard::Write( + const Sequence<OwningNonNull<ClipboardItem>>& aData, + nsIPrincipal& aSubjectPrincipal, ErrorResult& aRv) { + // Create a promise + RefPtr<Promise> p = dom::Promise::Create(GetOwnerGlobal(), aRv); + if (aRv.Failed()) { + return nullptr; + } + + RefPtr<nsPIDOMWindowInner> owner = GetOwner(); + Document* doc = owner ? owner->GetDoc() : nullptr; + if (!doc) { + p->MaybeRejectWithUndefined(); + return p.forget(); + } + + // We want to disable security check for automated tests that have the pref + // dom.events.testing.asyncClipboard set to true + if (!IsTestingPrefEnabled() && + !nsContentUtils::IsCutCopyAllowed(doc, aSubjectPrincipal)) { + MOZ_LOG(GetClipboardLog(), LogLevel::Debug, + ("Clipboard, Write, Not allowed to write to clipboard\n")); + p->MaybeRejectWithNotAllowedError( + "Clipboard write was blocked due to lack of user activation."); + return p.forget(); + } + + // Get the clipboard service + nsCOMPtr<nsIClipboard> clipboard( + do_GetService("@mozilla.org/widget/clipboard;1")); + if (!clipboard) { + p->MaybeRejectWithUndefined(); + return p.forget(); + } + + nsCOMPtr<nsILoadContext> context = doc->GetLoadContext(); + if (!context) { + p->MaybeRejectWithUndefined(); + return p.forget(); + } + + if (aData.Length() > 1) { + p->MaybeRejectWithNotAllowedError( + "Clipboard write is only supported with one ClipboardItem at the " + "moment"); + return p.forget(); + } + + if (aData.Length() == 0) { + // Nothing needs to be written to the clipboard. + p->MaybeResolveWithUndefined(); + return p.forget(); + } + + nsCOMPtr<nsIAsyncSetClipboardData> request; + RefPtr<ClipboardWriteCallback> callback = + MakeRefPtr<ClipboardWriteCallback>(p, aData[0]); + nsresult rv = clipboard->AsyncSetData(nsIClipboard::kGlobalClipboard, + callback, getter_AddRefs(request)); + if (NS_FAILED(rv)) { + p->MaybeReject(rv); + return p.forget(); + } + + GetClipboardNativeItem(aData[0])->Then( + GetMainThreadSerialEventTarget(), __func__, + [owner, request, context, principal = RefPtr{&aSubjectPrincipal}]( + const nsTArray<NativeEntry>& aEntries) { + RefPtr<DataTransfer> dataTransfer = + new DataTransfer(owner, eCopy, + /* is external */ true, + /* clipboard type */ -1); + + for (const auto& entry : aEntries) { + nsresult rv = dataTransfer->SetDataWithPrincipal( + entry.mType, entry.mData, 0, principal); + + if (NS_FAILED(rv)) { + request->Abort(rv); + return; + } + } + + // Get the transferable + RefPtr<nsITransferable> transferable = + dataTransfer->GetTransferable(0, context); + if (!transferable) { + request->Abort(NS_ERROR_FAILURE); + return; + } + + // Finally write data to clipboard + request->SetData(transferable, /* clipboard owner */ nullptr); + }, + [p, request](const CopyableErrorResult& aErrorResult) { + p->MaybeReject(CopyableErrorResult(aErrorResult)); + request->Abort(NS_ERROR_ABORT); + }); + + return p.forget(); +} + +already_AddRefed<Promise> Clipboard::WriteText(const nsAString& aData, + nsIPrincipal& aSubjectPrincipal, + ErrorResult& aRv) { + nsCOMPtr<nsIGlobalObject> global = GetOwnerGlobal(); + if (!global) { + aRv.ThrowInvalidStateError("Unable to get global."); + return nullptr; + } + + // Create a single-element Sequence to reuse Clipboard::Write. + nsTArray<RefPtr<ClipboardItem::ItemEntry>> items; + items.AppendElement(MakeRefPtr<ClipboardItem::ItemEntry>( + global, NS_LITERAL_STRING_FROM_CSTRING(kTextMime), aData)); + + nsTArray<OwningNonNull<ClipboardItem>> sequence; + RefPtr<ClipboardItem> item = MakeRefPtr<ClipboardItem>( + GetOwner(), PresentationStyle::Unspecified, std::move(items)); + sequence.AppendElement(*item); + + return Write(std::move(sequence), aSubjectPrincipal, aRv); +} + +void Clipboard::ReadRequest::MaybeRejectWithNotAllowedError( + const nsACString& aMessage) { + mPromise->MaybeRejectWithNotAllowedError(aMessage); +} + +void Clipboard::OnUserReactedToPasteMenuPopup(const bool aAllowed) { + MOZ_LOG(GetClipboardLog(), LogLevel::Debug, ("%s", __FUNCTION__)); + + mTransientUserPasteState.OnUserReactedToPasteMenuPopup(aAllowed); + + MOZ_ASSERT(!mReadRequests.IsEmpty()); + + for (UniquePtr<ReadRequest>& request : mReadRequests) { + if (aAllowed) { + request->Answer(); + } else { + request->MaybeRejectWithNotAllowedError( + "The user dismissed the 'Paste' button."_ns); + } + } + + mReadRequests.Clear(); +} + +JSObject* Clipboard::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return Clipboard_Binding::Wrap(aCx, this, aGivenProto); +} + +/* static */ +LogModule* Clipboard::GetClipboardLog() { return gClipboardLog; } + +/* static */ +bool Clipboard::ReadTextEnabled(JSContext* aCx, JSObject* aGlobal) { + nsIPrincipal* prin = nsContentUtils::SubjectPrincipal(aCx); + return IsReadTextExposedToContent() || + prin->GetIsAddonOrExpandedAddonPrincipal() || + prin->IsSystemPrincipal(); +} + +/* static */ +bool Clipboard::IsTestingPrefEnabled() { + bool clipboardTestingEnabled = + StaticPrefs::dom_events_testing_asyncClipboard_DoNotUseDirectly(); + MOZ_LOG(GetClipboardLog(), LogLevel::Debug, + ("Clipboard, Is testing enabled? %d\n", clipboardTestingEnabled)); + return clipboardTestingEnabled; +} + +NS_IMPL_CYCLE_COLLECTION_CLASS(Clipboard) + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(Clipboard, + DOMEventTargetHelper) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(Clipboard, DOMEventTargetHelper) +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(Clipboard) +NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper) + +NS_IMPL_ADDREF_INHERITED(Clipboard, DOMEventTargetHelper) +NS_IMPL_RELEASE_INHERITED(Clipboard, DOMEventTargetHelper) + +} // namespace mozilla::dom diff --git a/dom/events/Clipboard.h b/dom/events/Clipboard.h new file mode 100644 index 0000000000..e4179c816f --- /dev/null +++ b/dom/events/Clipboard.h @@ -0,0 +1,133 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#ifndef mozilla_dom_Clipboard_h_ +#define mozilla_dom_Clipboard_h_ + +#include "nsString.h" +#include "nsStringFwd.h" +#include "mozilla/DOMEventTargetHelper.h" +#include "mozilla/Logging.h" +#include "mozilla/RefPtr.h" +#include "mozilla/UniquePtr.h" +#include "mozilla/dom/DataTransfer.h" + +namespace mozilla::dom { + +class Promise; +class ClipboardItem; + +// https://www.w3.org/TR/clipboard-apis/#clipboard-interface +class Clipboard : public DOMEventTargetHelper { + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(Clipboard, DOMEventTargetHelper) + + IMPL_EVENT_HANDLER(message) + IMPL_EVENT_HANDLER(messageerror) + + explicit Clipboard(nsPIDOMWindowInner* aWindow); + already_AddRefed<Promise> Read(nsIPrincipal& aSubjectPrincipal, + ErrorResult& aRv); + already_AddRefed<Promise> ReadText(nsIPrincipal& aSubjectPrincipal, + ErrorResult& aRv); + already_AddRefed<Promise> Write( + const Sequence<OwningNonNull<ClipboardItem>>& aData, + nsIPrincipal& aSubjectPrincipal, ErrorResult& aRv); + already_AddRefed<Promise> WriteText(const nsAString& aData, + nsIPrincipal& aSubjectPrincipal, + ErrorResult& aRv); + + // See documentation of the corresponding .webidl file. + void OnUserReactedToPasteMenuPopup(bool aAllowed); + + static LogModule* GetClipboardLog(); + + // Check if the Clipboard.readText API should be enabled for this context. + // This API is only enabled for Extension and System contexts, as there is no + // way to request the required permission for web content. If the clipboard + // API testing pref is enabled, ReadText is enabled for web content for + // testing purposes. + static bool ReadTextEnabled(JSContext* aCx, JSObject* aGlobal); + + virtual JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + private: + enum class ReadRequestType { + eRead, + eReadText, + }; + + // Checks if dom.events.testing.asyncClipboard pref is enabled. + // The aforementioned pref allows automated tests to bypass the security + // checks when writing to + // or reading from the clipboard. + static bool IsTestingPrefEnabled(); + + static bool IsTestingPrefEnabledOrHasReadPermission( + nsIPrincipal& aSubjectPrincipal); + + void CheckReadPermissionAndHandleRequest(Promise& aPromise, + nsIPrincipal& aSubjectPrincipal, + ReadRequestType aType); + + void HandleReadRequestWhichRequiresPasteButton(Promise& aPromise, + ReadRequestType aType); + + already_AddRefed<Promise> ReadHelper(nsIPrincipal& aSubjectPrincipal, + ReadRequestType aType, ErrorResult& aRv); + + ~Clipboard(); + + class ReadRequest final { + public: + ReadRequest(Promise& aPromise, ReadRequestType aType, + nsPIDOMWindowInner& aOwner) + : mType(aType), mPromise(&aPromise), mOwner(&aOwner) {} + + // Clears the request too. + void Answer(); + + void MaybeRejectWithNotAllowedError(const nsACString& aMessage); + + private: + ReadRequestType mType; + // Not cycle-collected, because it's nulled when the request is answered or + // destructed. + RefPtr<Promise> mPromise; + RefPtr<nsPIDOMWindowInner> mOwner; + }; + + AutoTArray<UniquePtr<ReadRequest>, 1> mReadRequests; + + class TransientUserPasteState final { + public: + enum class Value { + Initial, + WaitingForUserReactionToPasteMenuPopup, + TransientlyForbiddenByUser, + TransientlyAllowedByUser, + }; + + // @param aWindowContext requires valid transient user gesture activation. + Value RefreshAndGet(WindowContext& aWindowContext); + + void OnStartWaitingForUserReactionToPasteMenuPopup( + const TimeStamp& aUserGestureStart); + void OnUserReactedToPasteMenuPopup(bool aAllowed); + + private: + TimeStamp mUserGestureStart; + + Value mValue = Value::Initial; + }; + + TransientUserPasteState mTransientUserPasteState; +}; + +} // namespace mozilla::dom +#endif // mozilla_dom_Clipboard_h_ diff --git a/dom/events/ClipboardEvent.cpp b/dom/events/ClipboardEvent.cpp new file mode 100644 index 0000000000..e678f7c9fd --- /dev/null +++ b/dom/events/ClipboardEvent.cpp @@ -0,0 +1,90 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#include "mozilla/dom/ClipboardEvent.h" +#include "mozilla/ContentEvents.h" +#include "mozilla/ErrorResult.h" +#include "mozilla/dom/DataTransfer.h" +#include "nsIClipboard.h" + +namespace mozilla::dom { + +ClipboardEvent::ClipboardEvent(EventTarget* aOwner, nsPresContext* aPresContext, + InternalClipboardEvent* aEvent) + : Event(aOwner, aPresContext, + aEvent ? aEvent : new InternalClipboardEvent(false, eVoidEvent)) { + if (aEvent) { + mEventIsInternal = false; + } else { + mEventIsInternal = true; + } +} + +void ClipboardEvent::InitClipboardEvent(const nsAString& aType, bool aCanBubble, + bool aCancelable, + DataTransfer* aClipboardData) { + NS_ENSURE_TRUE_VOID(!mEvent->mFlags.mIsBeingDispatched); + + Event::InitEvent(aType, aCanBubble, aCancelable); + mEvent->AsClipboardEvent()->mClipboardData = aClipboardData; +} + +already_AddRefed<ClipboardEvent> ClipboardEvent::Constructor( + const GlobalObject& aGlobal, const nsAString& aType, + const ClipboardEventInit& aParam, ErrorResult& aRv) { + nsCOMPtr<EventTarget> t = do_QueryInterface(aGlobal.GetAsSupports()); + RefPtr<ClipboardEvent> e = new ClipboardEvent(t, nullptr, nullptr); + bool trusted = e->Init(t); + + RefPtr<DataTransfer> clipboardData; + if (e->mEventIsInternal) { + InternalClipboardEvent* event = e->mEvent->AsClipboardEvent(); + if (event) { + // Always create a clipboardData for the copy event. If this is changed to + // support other types of events, make sure that read/write privileges are + // checked properly within DataTransfer. + clipboardData = new DataTransfer(ToSupports(e), eCopy, false, -1); + clipboardData->SetData(aParam.mDataType, aParam.mData, + *aGlobal.GetSubjectPrincipal(), aRv); + NS_ENSURE_TRUE(!aRv.Failed(), nullptr); + } + } + + e->InitClipboardEvent(aType, aParam.mBubbles, aParam.mCancelable, + clipboardData); + e->SetTrusted(trusted); + e->SetComposed(aParam.mComposed); + return e.forget(); +} + +DataTransfer* ClipboardEvent::GetClipboardData() { + InternalClipboardEvent* event = mEvent->AsClipboardEvent(); + + if (!event->mClipboardData) { + if (mEventIsInternal) { + event->mClipboardData = + new DataTransfer(ToSupports(this), eCopy, false, -1); + } else { + event->mClipboardData = new DataTransfer( + ToSupports(this), event->mMessage, event->mMessage == ePaste, + nsIClipboard::kGlobalClipboard); + } + } + + return event->mClipboardData; +} + +} // namespace mozilla::dom + +using namespace mozilla; +using namespace mozilla::dom; + +already_AddRefed<ClipboardEvent> NS_NewDOMClipboardEvent( + EventTarget* aOwner, nsPresContext* aPresContext, + InternalClipboardEvent* aEvent) { + RefPtr<ClipboardEvent> it = new ClipboardEvent(aOwner, aPresContext, aEvent); + return it.forget(); +} diff --git a/dom/events/ClipboardEvent.h b/dom/events/ClipboardEvent.h new file mode 100644 index 0000000000..08d96b3772 --- /dev/null +++ b/dom/events/ClipboardEvent.h @@ -0,0 +1,48 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#ifndef mozilla_dom_ClipboardEvent_h_ +#define mozilla_dom_ClipboardEvent_h_ + +#include "mozilla/EventForwards.h" +#include "mozilla/dom/ClipboardEventBinding.h" +#include "mozilla/dom/Event.h" + +namespace mozilla::dom { +class DataTransfer; + +class ClipboardEvent : public Event { + public: + ClipboardEvent(EventTarget* aOwner, nsPresContext* aPresContext, + InternalClipboardEvent* aEvent); + + NS_INLINE_DECL_REFCOUNTING_INHERITED(ClipboardEvent, Event) + + virtual JSObject* WrapObjectInternal( + JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override { + return ClipboardEvent_Binding::Wrap(aCx, this, aGivenProto); + } + + static already_AddRefed<ClipboardEvent> Constructor( + const GlobalObject& aGlobal, const nsAString& aType, + const ClipboardEventInit& aParam, ErrorResult& aRv); + + DataTransfer* GetClipboardData(); + + void InitClipboardEvent(const nsAString& aType, bool aCanBubble, + bool aCancelable, DataTransfer* aClipboardData); + + protected: + ~ClipboardEvent() = default; +}; + +} // namespace mozilla::dom + +already_AddRefed<mozilla::dom::ClipboardEvent> NS_NewDOMClipboardEvent( + mozilla::dom::EventTarget* aOwner, nsPresContext* aPresContext, + mozilla::InternalClipboardEvent* aEvent); + +#endif // mozilla_dom_ClipboardEvent_h_ diff --git a/dom/events/ClipboardItem.cpp b/dom/events/ClipboardItem.cpp new file mode 100644 index 0000000000..3226f40824 --- /dev/null +++ b/dom/events/ClipboardItem.cpp @@ -0,0 +1,331 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#include "mozilla/dom/ClipboardItem.h" + +#include "mozilla/dom/Promise.h" +#include "mozilla/dom/Record.h" +#include "nsComponentManagerUtils.h" +#include "nsIClipboard.h" +#include "nsIInputStream.h" +#include "nsISupportsPrimitives.h" +#include "nsNetUtil.h" +#include "nsServiceManagerUtils.h" + +namespace mozilla::dom { + +NS_IMPL_CYCLE_COLLECTION(ClipboardItem::ItemEntry, mGlobal, mData, + mPendingGetTypeRequests) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ClipboardItem::ItemEntry) + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTING_ADDREF(ClipboardItem::ItemEntry) +NS_IMPL_CYCLE_COLLECTING_RELEASE(ClipboardItem::ItemEntry) + +void ClipboardItem::ItemEntry::ResolvedCallback(JSContext* aCx, + JS::Handle<JS::Value> aValue, + ErrorResult& aRv) { + MOZ_ASSERT(!mLoadingPromise.Exists()); + mIsLoadingData = false; + OwningStringOrBlob clipboardData; + if (!clipboardData.Init(aCx, aValue)) { + JS_ClearPendingException(aCx); + RejectPendingPromises(NS_ERROR_DOM_DATA_ERR); + return; + } + + MaybeResolvePendingPromises(std::move(clipboardData)); +} + +void ClipboardItem::ItemEntry::RejectedCallback(JSContext* aCx, + JS::Handle<JS::Value> aValue, + ErrorResult& aRv) { + MOZ_ASSERT(!mLoadingPromise.Exists()); + mIsLoadingData = false; + RejectPendingPromises(NS_ERROR_DOM_DATA_ERR); +} + +RefPtr<ClipboardItem::ItemEntry::GetDataPromise> +ClipboardItem::ItemEntry::GetData() { + // Data is still being loaded, either from the system clipboard or the data + // promise provided to the ClipboardItem constructor. + if (mIsLoadingData) { + MOZ_ASSERT(!mData.IsString() && !mData.IsBlob(), + "Data should be uninitialized"); + MOZ_ASSERT(mLoadResult.isNothing(), "Should have no load result"); + + MozPromiseHolder<GetDataPromise> holder; + RefPtr<GetDataPromise> promise = holder.Ensure(__func__); + mPendingGetDataRequests.AppendElement(std::move(holder)); + return promise.forget(); + } + + if (NS_FAILED(mLoadResult.value())) { + MOZ_ASSERT(!mData.IsString() && !mData.IsBlob(), + "Data should be uninitialized"); + // We are not able to load data, so reject the promise directly. + return GetDataPromise::CreateAndReject(mLoadResult.value(), __func__); + } + + MOZ_ASSERT(mData.IsString() || mData.IsBlob(), "Data should be initialized"); + OwningStringOrBlob data(mData); + return GetDataPromise::CreateAndResolve(std::move(data), __func__); +} + +void ClipboardItem::ItemEntry::LoadDataFromSystemClipboard( + nsITransferable& aTransferable) { + // XXX maybe we could consider adding a method to check whether the union + // object is uninitialized or initialized. + MOZ_DIAGNOSTIC_ASSERT(!mData.IsString() && !mData.IsBlob(), + "Data should be uninitialized."); + MOZ_DIAGNOSTIC_ASSERT(mLoadResult.isNothing(), "Should have no load result"); + MOZ_DIAGNOSTIC_ASSERT(!mIsLoadingData && !mLoadingPromise.Exists(), + "Should not be in the process of loading data"); + + nsresult rv; + nsCOMPtr<nsIClipboard> clipboard( + do_GetService("@mozilla.org/widget/clipboard;1", &rv)); + if (NS_FAILED(rv)) { + return; + } + + mIsLoadingData = true; + nsCOMPtr<nsITransferable> trans(&aTransferable); + clipboard->AsyncGetData(trans, nsIClipboard::kGlobalClipboard) + ->Then( + GetMainThreadSerialEventTarget(), __func__, + /* resolved */ + [self = RefPtr{this}, trans]() { + self->mIsLoadingData = false; + self->mLoadingPromise.Complete(); + + nsCOMPtr<nsISupports> data; + nsresult rv = trans->GetTransferData( + NS_ConvertUTF16toUTF8(self->Type()).get(), + getter_AddRefs(data)); + if (NS_WARN_IF(NS_FAILED(rv))) { + self->RejectPendingPromises(rv); + return; + } + + RefPtr<Blob> blob; + if (nsCOMPtr<nsISupportsString> supportsstr = + do_QueryInterface(data)) { + nsAutoString str; + supportsstr->GetData(str); + + blob = Blob::CreateStringBlob( + self->mGlobal, NS_ConvertUTF16toUTF8(str), self->Type()); + } else if (nsCOMPtr<nsIInputStream> istream = + do_QueryInterface(data)) { + uint64_t available; + void* data = nullptr; + nsresult rv = + NS_ReadInputStreamToBuffer(istream, &data, -1, &available); + if (NS_WARN_IF(NS_FAILED(rv))) { + self->RejectPendingPromises(rv); + return; + } + + blob = Blob::CreateMemoryBlob(self->mGlobal, data, available, + self->Type()); + } else if (nsCOMPtr<nsISupportsCString> supportscstr = + do_QueryInterface(data)) { + nsAutoCString str; + supportscstr->GetData(str); + + blob = Blob::CreateStringBlob(self->mGlobal, str, self->Type()); + } + + if (!blob) { + self->RejectPendingPromises(NS_ERROR_DOM_DATA_ERR); + return; + } + + OwningStringOrBlob clipboardData; + clipboardData.SetAsBlob() = std::move(blob); + self->MaybeResolvePendingPromises(std::move(clipboardData)); + }, + /* rejected */ + [self = RefPtr{this}](nsresult rv) { + self->mIsLoadingData = false; + self->mLoadingPromise.Complete(); + self->RejectPendingPromises(rv); + }) + ->Track(mLoadingPromise); +} + +void ClipboardItem::ItemEntry::LoadDataFromDataPromise(Promise& aDataPromise) { + MOZ_DIAGNOSTIC_ASSERT(!mData.IsString() && !mData.IsBlob(), + "Data should be uninitialized"); + MOZ_DIAGNOSTIC_ASSERT(mLoadResult.isNothing(), "Should have no load result"); + MOZ_DIAGNOSTIC_ASSERT(!mIsLoadingData && !mLoadingPromise.Exists(), + "Should not be in the process of loading data"); + + mIsLoadingData = true; + aDataPromise.AppendNativeHandler(this); +} + +void ClipboardItem::ItemEntry::ReactGetTypePromise(Promise& aPromise) { + // Data is still being loaded, either from the system clipboard or the data + // promise provided to the ClipboardItem constructor. + if (mIsLoadingData) { + MOZ_ASSERT(!mData.IsString() && !mData.IsBlob(), + "Data should be uninitialized"); + MOZ_ASSERT(mLoadResult.isNothing(), "Should have no load result."); + mPendingGetTypeRequests.AppendElement(&aPromise); + return; + } + + if (NS_FAILED(mLoadResult.value())) { + MOZ_ASSERT(!mData.IsString() && !mData.IsBlob(), + "Data should be uninitialized"); + aPromise.MaybeRejectWithDataError("The data for type '"_ns + + NS_ConvertUTF16toUTF8(mType) + + "' was not found"_ns); + return; + } + + MaybeResolveGetTypePromise(mData, aPromise); +} + +void ClipboardItem::ItemEntry::MaybeResolveGetTypePromise( + const OwningStringOrBlob& aData, Promise& aPromise) { + if (aData.IsBlob()) { + aPromise.MaybeResolve(aData); + return; + } + + // XXX This is for the case that data is from ClipboardItem constructor, + // maybe we should also load that into a Blob earlier. But Safari returns + // different `Blob` instances for each `getTypes` call if the string is from + // ClipboardItem constructor, which is more like our current setup. + if (RefPtr<Blob> blob = Blob::CreateStringBlob( + mGlobal, NS_ConvertUTF16toUTF8(aData.GetAsString()), mType)) { + aPromise.MaybeResolve(blob); + return; + } + + aPromise.MaybeRejectWithDataError("The data for type '"_ns + + NS_ConvertUTF16toUTF8(mType) + + "' was not found"_ns); +} + +void ClipboardItem::ItemEntry::RejectPendingPromises(nsresult aRv) { + MOZ_ASSERT(NS_FAILED(aRv), "Should have a failure code here"); + MOZ_DIAGNOSTIC_ASSERT(!mData.IsString() && !mData.IsBlob(), + "Data should be uninitialized"); + MOZ_DIAGNOSTIC_ASSERT(mLoadResult.isNothing(), "Should not have load result"); + MOZ_DIAGNOSTIC_ASSERT(!mIsLoadingData && !mLoadingPromise.Exists(), + "Should not be in the process of loading data"); + mLoadResult.emplace(aRv); + auto promiseHolders = std::move(mPendingGetDataRequests); + for (auto& promiseHolder : promiseHolders) { + promiseHolder.Reject(aRv, __func__); + } + auto getTypePromises = std::move(mPendingGetTypeRequests); + for (auto& promise : getTypePromises) { + promise->MaybeReject(aRv); + } +} + +void ClipboardItem::ItemEntry::MaybeResolvePendingPromises( + OwningStringOrBlob&& aData) { + MOZ_DIAGNOSTIC_ASSERT(!mData.IsString() && !mData.IsBlob(), + "Data should be uninitialized"); + MOZ_DIAGNOSTIC_ASSERT(mLoadResult.isNothing(), "Should not have load result"); + MOZ_DIAGNOSTIC_ASSERT(!mIsLoadingData && !mLoadingPromise.Exists(), + "Should not be in the process of loading data"); + mLoadResult.emplace(NS_OK); + mData = std::move(aData); + auto getDataPromiseHolders = std::move(mPendingGetDataRequests); + for (auto& promiseHolder : getDataPromiseHolders) { + OwningStringOrBlob data(mData); + promiseHolder.Resolve(std::move(data), __func__); + } + auto promises = std::move(mPendingGetTypeRequests); + for (auto& promise : promises) { + MaybeResolveGetTypePromise(mData, *promise); + } +} + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(ClipboardItem, mOwner, mItems) + +ClipboardItem::ClipboardItem(nsISupports* aOwner, + const dom::PresentationStyle aPresentationStyle, + nsTArray<RefPtr<ItemEntry>>&& aItems) + : mOwner(aOwner), + mPresentationStyle(aPresentationStyle), + mItems(std::move(aItems)) {} + +// static +already_AddRefed<ClipboardItem> ClipboardItem::Constructor( + const GlobalObject& aGlobal, + const Record<nsString, OwningNonNull<Promise>>& aItems, + const ClipboardItemOptions& aOptions, ErrorResult& aRv) { + if (aItems.Entries().IsEmpty()) { + aRv.ThrowTypeError("At least one entry required"); + return nullptr; + } + + nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports()); + MOZ_ASSERT(global); + + nsTArray<RefPtr<ItemEntry>> items; + for (const auto& entry : aItems.Entries()) { + RefPtr<ItemEntry> item = MakeRefPtr<ItemEntry>(global, entry.mKey); + item->LoadDataFromDataPromise(*entry.mValue); + items.AppendElement(std::move(item)); + } + + RefPtr<ClipboardItem> item = MakeRefPtr<ClipboardItem>( + global, aOptions.mPresentationStyle, std::move(items)); + return item.forget(); +} + +void ClipboardItem::GetTypes(nsTArray<nsString>& aTypes) const { + for (const auto& item : mItems) { + aTypes.AppendElement(item->Type()); + } +} + +already_AddRefed<Promise> ClipboardItem::GetType(const nsAString& aType, + ErrorResult& aRv) { + nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(GetParentObject()); + RefPtr<Promise> p = Promise::Create(global, aRv); + if (aRv.Failed()) { + return nullptr; + } + + for (auto& item : mItems) { + MOZ_ASSERT(item); + + const nsAString& type = item->Type(); + if (type == aType) { + nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(GetParentObject()); + if (NS_WARN_IF(!global)) { + p->MaybeReject(NS_ERROR_UNEXPECTED); + return p.forget(); + } + + item->ReactGetTypePromise(*p); + return p.forget(); + } + } + + p->MaybeRejectWithNotFoundError( + "The type '"_ns + NS_ConvertUTF16toUTF8(aType) + "' was not found"_ns); + return p.forget(); +} + +JSObject* ClipboardItem::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return mozilla::dom::ClipboardItem_Binding::Wrap(aCx, this, aGivenProto); +} + +} // namespace mozilla::dom diff --git a/dom/events/ClipboardItem.h b/dom/events/ClipboardItem.h new file mode 100644 index 0000000000..95fdcd6ea9 --- /dev/null +++ b/dom/events/ClipboardItem.h @@ -0,0 +1,132 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#ifndef mozilla_dom_ClipboardItem_h_ +#define mozilla_dom_ClipboardItem_h_ + +#include "mozilla/dom/Blob.h" +#include "mozilla/dom/ClipboardBinding.h" +#include "mozilla/dom/PromiseNativeHandler.h" +#include "mozilla/MozPromise.h" + +#include "nsWrapperCache.h" + +class nsITransferable; + +namespace mozilla::dom { + +struct ClipboardItemOptions; +template <typename KeyType, typename ValueType> +class Record; +class Promise; + +class ClipboardItem final : public nsWrapperCache { + public: + class ItemEntry final : public PromiseNativeHandler { + public: + using GetDataPromise = + MozPromise<OwningStringOrBlob, nsresult, /* IsExclusive = */ true>; + + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_CLASS(ItemEntry) + + explicit ItemEntry(nsIGlobalObject* aGlobal, const nsAString& aType) + : mGlobal(aGlobal), mType(aType) { + MOZ_ASSERT(mGlobal); + } + ItemEntry(nsIGlobalObject* aGlobal, const nsAString& aType, + const nsAString& aData) + : ItemEntry(aGlobal, aType) { + mLoadResult.emplace(NS_OK); + mData.SetAsString() = aData; + } + + // PromiseNativeHandler + void ResolvedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue, + ErrorResult& aRv) override; + void RejectedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue, + ErrorResult& aRv) override; + + const nsString& Type() const { return mType; } + RefPtr<GetDataPromise> GetData(); + + // Load data from system clipboard. + void LoadDataFromSystemClipboard(nsITransferable& aTransferable); + void LoadDataFromDataPromise(Promise& aDataPromise); + + // If clipboard data is in the process of loading from either system + // clipboard or data promise, add `aPromise` to the pending list which will + // be resolved/rejected later when process is finished. Otherwise, + // resolve/reject `aPromise` based on cached result and data. + void ReactGetTypePromise(Promise& aPromise); + + private: + ~ItemEntry() { + mLoadingPromise.DisconnectIfExists(); + if (!mPendingGetDataRequests.IsEmpty()) { + RejectPendingPromises(NS_ERROR_FAILURE); + } + }; + + void MaybeResolveGetTypePromise(const OwningStringOrBlob& aData, + Promise& aPromise); + void MaybeResolvePendingPromises(OwningStringOrBlob&& aData); + void RejectPendingPromises(nsresult rv); + + nsCOMPtr<nsIGlobalObject> mGlobal; + + // MIME type of this entry. + nsString mType; + + // Cache the loading result. + OwningStringOrBlob mData; + Maybe<nsresult> mLoadResult; + + // Indicates if the data is still being loaded. + bool mIsLoadingData = false; + MozPromiseRequestHolder<GenericPromise> mLoadingPromise; + + // Pending promises for data retrieval requests. + nsTArray<MozPromiseHolder<GetDataPromise>> mPendingGetDataRequests; + nsTArray<RefPtr<Promise>> mPendingGetTypeRequests; + }; + + NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(ClipboardItem) + NS_DECL_CYCLE_COLLECTION_NATIVE_WRAPPERCACHE_CLASS(ClipboardItem) + + ClipboardItem(nsISupports* aOwner, dom::PresentationStyle aPresentationStyle, + nsTArray<RefPtr<ItemEntry>>&& aItems); + + static already_AddRefed<ClipboardItem> Constructor( + const GlobalObject& aGlobal, + const Record<nsString, OwningNonNull<Promise>>& aItems, + const ClipboardItemOptions& aOptions, ErrorResult& aRv); + + dom::PresentationStyle PresentationStyle() const { + return mPresentationStyle; + }; + void GetTypes(nsTArray<nsString>& aTypes) const; + + already_AddRefed<Promise> GetType(const nsAString& aType, ErrorResult& aRv); + + nsISupports* GetParentObject() const { return mOwner; } + + JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + const nsTArray<RefPtr<ItemEntry>>& Entries() const { return mItems; } + + private: + ~ClipboardItem() = default; + + nsCOMPtr<nsISupports> mOwner; + dom::PresentationStyle mPresentationStyle; + nsTArray<RefPtr<ItemEntry>> mItems; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_ClipboardItem_h_ diff --git a/dom/events/CommandEvent.cpp b/dom/events/CommandEvent.cpp new file mode 100644 index 0000000000..b048a959f8 --- /dev/null +++ b/dom/events/CommandEvent.cpp @@ -0,0 +1,42 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#include "mozilla/dom/CommandEvent.h" +#include "mozilla/MiscEvents.h" +#include "prtime.h" + +namespace mozilla::dom { + +CommandEvent::CommandEvent(EventTarget* aOwner, nsPresContext* aPresContext, + WidgetCommandEvent* aEvent) + : Event(aOwner, aPresContext, aEvent ? aEvent : new WidgetCommandEvent()) { + if (aEvent) { + mEventIsInternal = false; + } else { + mEventIsInternal = true; + } +} + +void CommandEvent::GetCommand(nsAString& aCommand) { + nsAtom* command = mEvent->AsCommandEvent()->mCommand; + if (command) { + command->ToString(aCommand); + } else { + aCommand.Truncate(); + } +} + +} // namespace mozilla::dom + +using namespace mozilla; +using namespace mozilla::dom; + +already_AddRefed<CommandEvent> NS_NewDOMCommandEvent( + EventTarget* aOwner, nsPresContext* aPresContext, + WidgetCommandEvent* aEvent) { + RefPtr<CommandEvent> it = new CommandEvent(aOwner, aPresContext, aEvent); + return it.forget(); +} diff --git a/dom/events/CommandEvent.h b/dom/events/CommandEvent.h new file mode 100644 index 0000000000..944b708c3a --- /dev/null +++ b/dom/events/CommandEvent.h @@ -0,0 +1,40 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#ifndef mozilla_dom_CommandEvent_h_ +#define mozilla_dom_CommandEvent_h_ + +#include "mozilla/EventForwards.h" +#include "mozilla/dom/CommandEventBinding.h" +#include "mozilla/dom/Event.h" + +namespace mozilla::dom { + +class CommandEvent : public Event { + public: + CommandEvent(EventTarget* aOwner, nsPresContext* aPresContext, + WidgetCommandEvent* aEvent); + + NS_INLINE_DECL_REFCOUNTING_INHERITED(CommandEvent, Event) + + virtual JSObject* WrapObjectInternal( + JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override { + return CommandEvent_Binding::Wrap(aCx, this, aGivenProto); + } + + void GetCommand(nsAString& aCommand); + + protected: + ~CommandEvent() = default; +}; + +} // namespace mozilla::dom + +already_AddRefed<mozilla::dom::CommandEvent> NS_NewDOMCommandEvent( + mozilla::dom::EventTarget* aOwner, nsPresContext* aPresContext, + mozilla::WidgetCommandEvent* aEvent); + +#endif // mozilla_dom_CommandEvent_h_ diff --git a/dom/events/CompositionEvent.cpp b/dom/events/CompositionEvent.cpp new file mode 100644 index 0000000000..7e52b9e161 --- /dev/null +++ b/dom/events/CompositionEvent.cpp @@ -0,0 +1,108 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#include "mozilla/dom/CompositionEvent.h" +#include "mozilla/TextEvents.h" +#include "prtime.h" + +namespace mozilla::dom { + +CompositionEvent::CompositionEvent(EventTarget* aOwner, + nsPresContext* aPresContext, + WidgetCompositionEvent* aEvent) + : UIEvent(aOwner, aPresContext, + aEvent ? aEvent + : new WidgetCompositionEvent(false, eVoidEvent, nullptr)) { + NS_ASSERTION(mEvent->mClass == eCompositionEventClass, "event type mismatch"); + + if (aEvent) { + mEventIsInternal = false; + } else { + mEventIsInternal = true; + + // XXX compositionstart is cancelable in draft of DOM3 Events. + // However, it doesn't make sence for us, we cannot cancel composition + // when we sends compositionstart event. + mEvent->mFlags.mCancelable = false; + } + + // XXX Do we really need to duplicate the data value? + mData = mEvent->AsCompositionEvent()->mData; + // TODO: Native event should have locale information. +} + +// static +already_AddRefed<CompositionEvent> CompositionEvent::Constructor( + const GlobalObject& aGlobal, const nsAString& aType, + const CompositionEventInit& aParam) { + nsCOMPtr<EventTarget> t = do_QueryInterface(aGlobal.GetAsSupports()); + RefPtr<CompositionEvent> e = new CompositionEvent(t, nullptr, nullptr); + bool trusted = e->Init(t); + e->InitCompositionEvent(aType, aParam.mBubbles, aParam.mCancelable, + aParam.mView, aParam.mData, u""_ns); + e->mDetail = aParam.mDetail; + e->SetTrusted(trusted); + e->SetComposed(aParam.mComposed); + return e.forget(); +} + +NS_IMPL_CYCLE_COLLECTION_INHERITED(CompositionEvent, UIEvent, mRanges) + +NS_IMPL_ADDREF_INHERITED(CompositionEvent, UIEvent) +NS_IMPL_RELEASE_INHERITED(CompositionEvent, UIEvent) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(CompositionEvent) +NS_INTERFACE_MAP_END_INHERITING(UIEvent) + +void CompositionEvent::GetData(nsAString& aData) const { aData = mData; } + +void CompositionEvent::GetLocale(nsAString& aLocale) const { + aLocale = mLocale; +} + +void CompositionEvent::InitCompositionEvent(const nsAString& aType, + bool aCanBubble, bool aCancelable, + nsGlobalWindowInner* aView, + const nsAString& aData, + const nsAString& aLocale) { + NS_ENSURE_TRUE_VOID(!mEvent->mFlags.mIsBeingDispatched); + + UIEvent::InitUIEvent(aType, aCanBubble, aCancelable, aView, 0); + mData = aData; + mLocale = aLocale; +} + +void CompositionEvent::GetRanges(TextClauseArray& aRanges) { + // If the mRanges is not empty, we return the cached value. + if (!mRanges.IsEmpty()) { + aRanges = mRanges.Clone(); + return; + } + RefPtr<TextRangeArray> textRangeArray = mEvent->AsCompositionEvent()->mRanges; + if (!textRangeArray) { + return; + } + nsCOMPtr<nsPIDOMWindowInner> window = do_QueryInterface(mOwner); + const TextRange* targetRange = textRangeArray->GetTargetClause(); + for (size_t i = 0; i < textRangeArray->Length(); i++) { + const TextRange& range = textRangeArray->ElementAt(i); + mRanges.AppendElement(new TextClause(window, range, targetRange)); + } + aRanges = mRanges.Clone(); +} + +} // namespace mozilla::dom + +using namespace mozilla; +using namespace mozilla::dom; + +already_AddRefed<CompositionEvent> NS_NewDOMCompositionEvent( + EventTarget* aOwner, nsPresContext* aPresContext, + WidgetCompositionEvent* aEvent) { + RefPtr<CompositionEvent> event = + new CompositionEvent(aOwner, aPresContext, aEvent); + return event.forget(); +} diff --git a/dom/events/CompositionEvent.h b/dom/events/CompositionEvent.h new file mode 100644 index 0000000000..400829104a --- /dev/null +++ b/dom/events/CompositionEvent.h @@ -0,0 +1,58 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#ifndef mozilla_dom_CompositionEvent_h_ +#define mozilla_dom_CompositionEvent_h_ + +#include "mozilla/dom/CompositionEventBinding.h" +#include "mozilla/dom/TextClause.h" +#include "mozilla/dom/TypedArray.h" +#include "mozilla/dom/UIEvent.h" +#include "mozilla/EventForwards.h" + +namespace mozilla::dom { + +using TextClauseArray = nsTArray<RefPtr<TextClause>>; + +class CompositionEvent : public UIEvent { + public: + CompositionEvent(EventTarget* aOwner, nsPresContext* aPresContext, + WidgetCompositionEvent* aEvent); + + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(CompositionEvent, UIEvent) + + static already_AddRefed<CompositionEvent> Constructor( + const GlobalObject& aGlobal, const nsAString& aType, + const CompositionEventInit& aParam); + + virtual JSObject* WrapObjectInternal( + JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override { + return CompositionEvent_Binding::Wrap(aCx, this, aGivenProto); + } + + void InitCompositionEvent(const nsAString& aType, bool aCanBubble, + bool aCancelable, nsGlobalWindowInner* aView, + const nsAString& aData, const nsAString& aLocale); + void GetData(nsAString&) const; + void GetLocale(nsAString&) const; + void GetRanges(TextClauseArray& aRanges); + + protected: + ~CompositionEvent() = default; + + nsString mData; + nsString mLocale; + TextClauseArray mRanges; +}; + +} // namespace mozilla::dom + +already_AddRefed<mozilla::dom::CompositionEvent> NS_NewDOMCompositionEvent( + mozilla::dom::EventTarget* aOwner, nsPresContext* aPresContext, + mozilla::WidgetCompositionEvent* aEvent); + +#endif // mozilla_dom_CompositionEvent_h_ diff --git a/dom/events/ConstructibleEventTarget.cpp b/dom/events/ConstructibleEventTarget.cpp new file mode 100644 index 0000000000..827b3faaa8 --- /dev/null +++ b/dom/events/ConstructibleEventTarget.cpp @@ -0,0 +1,17 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#include "mozilla/dom/ConstructibleEventTarget.h" +#include "mozilla/dom/EventTargetBinding.h" + +namespace mozilla::dom { + +JSObject* ConstructibleEventTarget::WrapObject( + JSContext* cx, JS::Handle<JSObject*> aGivenProto) { + return EventTarget_Binding::Wrap(cx, this, aGivenProto); +} + +} // namespace mozilla::dom diff --git a/dom/events/ConstructibleEventTarget.h b/dom/events/ConstructibleEventTarget.h new file mode 100644 index 0000000000..fbaafac821 --- /dev/null +++ b/dom/events/ConstructibleEventTarget.h @@ -0,0 +1,30 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#ifndef mozilla_dom_ConstructibleEventTarget_h_ +#define mozilla_dom_ConstructibleEventTarget_h_ + +#include "mozilla/DOMEventTargetHelper.h" +#include "js/RootingAPI.h" + +namespace mozilla::dom { + +class ConstructibleEventTarget : public DOMEventTargetHelper { + public: + // Not worrying about isupports and cycle collection here. This does mean + // ConstructibleEventTarget will show up in CC and refcount logs as a + // DOMEventTargetHelper, but that's probably OK. + + explicit ConstructibleEventTarget(nsIGlobalObject* aGlobalObject) + : DOMEventTargetHelper(aGlobalObject) {} + + virtual JSObject* WrapObject(JSContext* cx, + JS::Handle<JSObject*> aGivenProto) override; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_ConstructibleEventTarget_h_ diff --git a/dom/events/ContentEventHandler.cpp b/dom/events/ContentEventHandler.cpp new file mode 100644 index 0000000000..e413e70897 --- /dev/null +++ b/dom/events/ContentEventHandler.cpp @@ -0,0 +1,3307 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#include "ContentEventHandler.h" + +#include "mozilla/Assertions.h" +#include "mozilla/CheckedInt.h" +#include "mozilla/ContentIterator.h" +#include "mozilla/IMEStateManager.h" +#include "mozilla/IntegerRange.h" +#include "mozilla/Maybe.h" +#include "mozilla/PresShell.h" +#include "mozilla/RangeBoundary.h" +#include "mozilla/RangeUtils.h" +#include "mozilla/TextComposition.h" +#include "mozilla/TextEditor.h" +#include "mozilla/TextEvents.h" +#include "mozilla/dom/Element.h" +#include "mozilla/dom/HTMLBRElement.h" +#include "mozilla/dom/HTMLUnknownElement.h" +#include "mozilla/dom/Selection.h" +#include "mozilla/dom/Text.h" +#include "nsCaret.h" +#include "nsCOMPtr.h" +#include "nsContentUtils.h" +#include "nsCopySupport.h" +#include "nsElementTable.h" +#include "nsFocusManager.h" +#include "nsFontMetrics.h" +#include "nsFrameSelection.h" +#include "nsIFrame.h" +#include "nsLayoutUtils.h" +#include "nsPresContext.h" +#include "nsQueryObject.h" +#include "nsRange.h" +#include "nsTextFragment.h" +#include "nsTextFrame.h" +#include "nsView.h" +#include "mozilla/ViewportUtils.h" + +#include <algorithm> + +// Work around conflicting define in rpcndr.h +#if defined(small) +# undef small +#endif // defined(small) + +namespace mozilla { + +using namespace dom; +using namespace widget; + +/******************************************************************/ +/* ContentEventHandler::RawRange */ +/******************************************************************/ + +void ContentEventHandler::RawRange::AssertStartIsBeforeOrEqualToEnd() { + MOZ_ASSERT( + *nsContentUtils::ComparePoints( + mStart.Container(), + *mStart.Offset(NodePosition::OffsetFilter::kValidOrInvalidOffsets), + mEnd.Container(), + *mEnd.Offset(NodePosition::OffsetFilter::kValidOrInvalidOffsets)) <= + 0); +} + +nsresult ContentEventHandler::RawRange::SetStart( + const RawRangeBoundary& aStart) { + nsINode* newRoot = RangeUtils::ComputeRootNode(aStart.Container()); + if (!newRoot) { + return NS_ERROR_DOM_INVALID_NODE_TYPE_ERR; + } + + if (!aStart.IsSetAndValid()) { + return NS_ERROR_DOM_INDEX_SIZE_ERR; + } + + // Collapse if not positioned yet, or if positioned in another document. + if (!IsPositioned() || newRoot != mRoot) { + mRoot = newRoot; + mStart.CopyFrom(aStart, RangeBoundaryIsMutationObserved::Yes); + mEnd.CopyFrom(aStart, RangeBoundaryIsMutationObserved::Yes); + return NS_OK; + } + + mStart.CopyFrom(aStart, RangeBoundaryIsMutationObserved::Yes); + AssertStartIsBeforeOrEqualToEnd(); + return NS_OK; +} + +nsresult ContentEventHandler::RawRange::SetEnd(const RawRangeBoundary& aEnd) { + nsINode* newRoot = RangeUtils::ComputeRootNode(aEnd.Container()); + if (!newRoot) { + return NS_ERROR_DOM_INVALID_NODE_TYPE_ERR; + } + + if (!aEnd.IsSetAndValid()) { + return NS_ERROR_DOM_INDEX_SIZE_ERR; + } + + // Collapse if not positioned yet, or if positioned in another document. + if (!IsPositioned() || newRoot != mRoot) { + mRoot = newRoot; + mStart.CopyFrom(aEnd, RangeBoundaryIsMutationObserved::Yes); + mEnd.CopyFrom(aEnd, RangeBoundaryIsMutationObserved::Yes); + return NS_OK; + } + + mEnd.CopyFrom(aEnd, RangeBoundaryIsMutationObserved::Yes); + AssertStartIsBeforeOrEqualToEnd(); + return NS_OK; +} + +nsresult ContentEventHandler::RawRange::SetEndAfter(nsINode* aEndContainer) { + return SetEnd(RangeUtils::GetRawRangeBoundaryAfter(aEndContainer)); +} + +void ContentEventHandler::RawRange::SetStartAndEnd(const nsRange* aRange) { + DebugOnly<nsresult> rv = + SetStartAndEnd(aRange->StartRef().AsRaw(), aRange->EndRef().AsRaw()); + MOZ_ASSERT(!aRange->IsPositioned() || NS_SUCCEEDED(rv)); +} + +nsresult ContentEventHandler::RawRange::SetStartAndEnd( + const RawRangeBoundary& aStart, const RawRangeBoundary& aEnd) { + nsINode* newStartRoot = RangeUtils::ComputeRootNode(aStart.Container()); + if (!newStartRoot) { + return NS_ERROR_DOM_INVALID_NODE_TYPE_ERR; + } + if (!aStart.IsSetAndValid()) { + return NS_ERROR_DOM_INDEX_SIZE_ERR; + } + + if (aStart.Container() == aEnd.Container()) { + if (!aEnd.IsSetAndValid()) { + return NS_ERROR_DOM_INDEX_SIZE_ERR; + } + MOZ_ASSERT(*aStart.Offset(RawRangeBoundary::OffsetFilter::kValidOffsets) <= + *aEnd.Offset(RawRangeBoundary::OffsetFilter::kValidOffsets)); + mRoot = newStartRoot; + mStart.CopyFrom(aStart, RangeBoundaryIsMutationObserved::Yes); + mEnd.CopyFrom(aEnd, RangeBoundaryIsMutationObserved::Yes); + return NS_OK; + } + + nsINode* newEndRoot = RangeUtils::ComputeRootNode(aEnd.Container()); + if (!newEndRoot) { + return NS_ERROR_DOM_INVALID_NODE_TYPE_ERR; + } + if (!aEnd.IsSetAndValid()) { + return NS_ERROR_DOM_INDEX_SIZE_ERR; + } + + // If they have different root, this should be collapsed at the end point. + if (newStartRoot != newEndRoot) { + mRoot = newEndRoot; + mStart.CopyFrom(aEnd, RangeBoundaryIsMutationObserved::Yes); + mEnd.CopyFrom(aEnd, RangeBoundaryIsMutationObserved::Yes); + return NS_OK; + } + + // Otherwise, set the range as specified. + mRoot = newStartRoot; + mStart.CopyFrom(aStart, RangeBoundaryIsMutationObserved::Yes); + mEnd.CopyFrom(aEnd, RangeBoundaryIsMutationObserved::Yes); + AssertStartIsBeforeOrEqualToEnd(); + return NS_OK; +} + +nsresult ContentEventHandler::RawRange::SelectNodeContents( + const nsINode* aNodeToSelectContents) { + nsINode* const newRoot = + RangeUtils::ComputeRootNode(const_cast<nsINode*>(aNodeToSelectContents)); + if (!newRoot) { + return NS_ERROR_DOM_INVALID_NODE_TYPE_ERR; + } + mRoot = newRoot; + mStart = RangeBoundary(const_cast<nsINode*>(aNodeToSelectContents), nullptr); + mEnd = RangeBoundary(const_cast<nsINode*>(aNodeToSelectContents), + aNodeToSelectContents->GetLastChild()); + return NS_OK; +} + +/******************************************************************/ +/* ContentEventHandler */ +/******************************************************************/ + +// NOTE +// +// ContentEventHandler *creates* ranges as following rules: +// 1. Start of range: +// 1.1. Cases: [textNode or text[Node or textNode[ +// When text node is start of a range, start node is the text node and +// start offset is any number between 0 and the length of the text. +// 1.2. Case: [<element>: +// When start of an element node is start of a range, start node is +// parent of the element and start offset is the element's index in the +// parent. +// 1.3. Case: <element/>[ +// When after an empty element node is start of a range, start node is +// parent of the element and start offset is the element's index in the +// parent + 1. +// 1.4. Case: <element>[ +// When start of a non-empty element is start of a range, start node is +// the element and start offset is 0. +// 1.5. Case: <root>[ +// When start of a range is 0 and there are no nodes causing text, +// start node is the root node and start offset is 0. +// 1.6. Case: [</root> +// When start of a range is out of bounds, start node is the root node +// and start offset is number of the children. +// 2. End of range: +// 2.1. Cases: ]textNode or text]Node or textNode] +// When a text node is end of a range, end node is the text node and +// end offset is any number between 0 and the length of the text. +// 2.2. Case: ]<element> +// When before an element node (meaning before the open tag of the +// element) is end of a range, end node is previous node causing text. +// Note that this case shouldn't be handled directly. If rule 2.1 and +// 2.3 are handled correctly, the loop with ContentIterator shouldn't +// reach the element node since the loop should've finished already at +// handling the last node which caused some text. +// 2.3. Case: <element>] +// When a line break is caused before a non-empty element node and it's +// end of a range, end node is the element and end offset is 0. +// (i.e., including open tag of the element) +// 2.4. Cases: <element/>] +// When after an empty element node is end of a range, end node is +// parent of the element node and end offset is the element's index in +// the parent + 1. (i.e., including close tag of the element or empty +// element) +// 2.5. Case: ]</root> +// When end of a range is out of bounds, end node is the root node and +// end offset is number of the children. +// +// ContentEventHandler *treats* ranges as following additional rules: +// 1. When the start node is an element node which doesn't have children, +// it includes a line break caused before itself (i.e., includes its open +// tag). For example, if start position is { <br>, 0 }, the line break +// caused by <br> should be included into the flatten text. +// 2. When the end node is an element node which doesn't have children, +// it includes the end (i.e., includes its close tag except empty element). +// Although, currently, any close tags don't cause line break, this also +// includes its open tag. For example, if end position is { <br>, 0 }, the +// line break caused by the <br> should be included into the flatten text. + +ContentEventHandler::ContentEventHandler(nsPresContext* aPresContext) + : mDocument(aPresContext->Document()) {} + +nsresult ContentEventHandler::InitBasic(bool aRequireFlush) { + NS_ENSURE_TRUE(mDocument, NS_ERROR_NOT_AVAILABLE); + if (aRequireFlush) { + // If text frame which has overflowing selection underline is dirty, + // we need to flush the pending reflow here. + mDocument->FlushPendingNotifications(FlushType::Layout); + } + return NS_OK; +} + +nsresult ContentEventHandler::InitRootContent( + const Selection& aNormalSelection) { + // Root content should be computed with normal selection because normal + // selection is typically has at least one range but the other selections + // not so. If there is a range, computing its root is easy, but if + // there are no ranges, we need to use ancestor limit instead. + MOZ_ASSERT(aNormalSelection.Type() == SelectionType::eNormal); + + if (!aNormalSelection.RangeCount()) { + // If there is no selection range, we should compute the selection root + // from ancestor limiter or root content of the document. + mRootElement = + Element::FromNodeOrNull(aNormalSelection.GetAncestorLimiter()); + if (!mRootElement) { + mRootElement = mDocument->GetRootElement(); + if (NS_WARN_IF(!mRootElement)) { + return NS_ERROR_NOT_AVAILABLE; + } + } + return NS_OK; + } + + RefPtr<const nsRange> range(aNormalSelection.GetRangeAt(0)); + if (NS_WARN_IF(!range)) { + return NS_ERROR_UNEXPECTED; + } + + // If there is a selection, we should retrieve the selection root from + // the range since when the window is inactivated, the ancestor limiter + // of selection was cleared by blur event handler of EditorBase but the + // selection range still keeps storing the nodes. If the active element of + // the deactive window is <input> or <textarea>, we can compute the + // selection root from them. + nsCOMPtr<nsINode> startNode = range->GetStartContainer(); + nsINode* endNode = range->GetEndContainer(); + if (NS_WARN_IF(!startNode) || NS_WARN_IF(!endNode)) { + return NS_ERROR_FAILURE; + } + + // See bug 537041 comment 5, the range could have removed node. + if (NS_WARN_IF(startNode->GetComposedDoc() != mDocument)) { + return NS_ERROR_FAILURE; + } + + NS_ASSERTION(startNode->GetComposedDoc() == endNode->GetComposedDoc(), + "firstNormalSelectionRange crosses the document boundary"); + + RefPtr<PresShell> presShell = mDocument->GetPresShell(); + mRootElement = + Element::FromNodeOrNull(startNode->GetSelectionRootContent(presShell)); + if (NS_WARN_IF(!mRootElement)) { + return NS_ERROR_FAILURE; + } + + return NS_OK; +} + +nsresult ContentEventHandler::InitCommon(EventMessage aEventMessage, + SelectionType aSelectionType, + bool aRequireFlush) { + if (mSelection && mSelection->Type() == aSelectionType) { + return NS_OK; + } + + mSelection = nullptr; + mRootElement = nullptr; + mFirstSelectedRawRange.Clear(); + + nsresult rv = InitBasic(aRequireFlush); + NS_ENSURE_SUCCESS(rv, rv); + + RefPtr<nsFrameSelection> frameSel; + if (PresShell* presShell = mDocument->GetPresShell()) { + frameSel = presShell->GetLastFocusedFrameSelection(); + } + if (NS_WARN_IF(!frameSel)) { + return NS_ERROR_NOT_AVAILABLE; + } + + mSelection = frameSel->GetSelection(aSelectionType); + if (NS_WARN_IF(!mSelection)) { + return NS_ERROR_NOT_AVAILABLE; + } + + RefPtr<Selection> normalSelection; + if (mSelection->Type() == SelectionType::eNormal) { + normalSelection = mSelection; + } else { + normalSelection = frameSel->GetSelection(SelectionType::eNormal); + if (NS_WARN_IF(!normalSelection)) { + return NS_ERROR_NOT_AVAILABLE; + } + } + + rv = InitRootContent(*normalSelection); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (mSelection->RangeCount()) { + mFirstSelectedRawRange.SetStartAndEnd(mSelection->GetRangeAt(0)); + return NS_OK; + } + + // Even if there are no selection ranges, it's usual case if aSelectionType + // is a special selection or we're handling eQuerySelectedText. + if (aSelectionType != SelectionType::eNormal || + aEventMessage == eQuerySelectedText) { + MOZ_ASSERT(!mFirstSelectedRawRange.IsPositioned()); + return NS_OK; + } + + // But otherwise, we need to assume that there is a selection range at the + // beginning of the root content if aSelectionType is eNormal. + rv = mFirstSelectedRawRange.CollapseTo(RawRangeBoundary(mRootElement, 0u)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return NS_ERROR_UNEXPECTED; + } + return NS_OK; +} + +nsresult ContentEventHandler::Init(WidgetQueryContentEvent* aEvent) { + NS_ASSERTION(aEvent, "aEvent must not be null"); + MOZ_ASSERT(aEvent->mMessage == eQuerySelectedText || + aEvent->mInput.mSelectionType == SelectionType::eNormal); + + if (NS_WARN_IF(!aEvent->mInput.IsValidOffset()) || + NS_WARN_IF(!aEvent->mInput.IsValidEventMessage(aEvent->mMessage))) { + return NS_ERROR_FAILURE; + } + + // Note that we should ignore WidgetQueryContentEvent::Input::mSelectionType + // if the event isn't eQuerySelectedText. + SelectionType selectionType = aEvent->mMessage == eQuerySelectedText + ? aEvent->mInput.mSelectionType + : SelectionType::eNormal; + if (NS_WARN_IF(selectionType == SelectionType::eNone)) { + return NS_ERROR_FAILURE; + } + + nsresult rv = + InitCommon(aEvent->mMessage, selectionType, aEvent->NeedsToFlushLayout()); + NS_ENSURE_SUCCESS(rv, rv); + + // Be aware, WidgetQueryContentEvent::mInput::mOffset should be made absolute + // offset before sending it to ContentEventHandler because querying selection + // every time may be expensive. So, if the caller caches selection, it + // should initialize the event with the cached value. + if (aEvent->mInput.mRelativeToInsertionPoint) { + MOZ_ASSERT(selectionType == SelectionType::eNormal); + RefPtr<TextComposition> composition = + IMEStateManager::GetTextCompositionFor(aEvent->mWidget); + if (composition) { + uint32_t compositionStart = composition->NativeOffsetOfStartComposition(); + if (NS_WARN_IF(!aEvent->mInput.MakeOffsetAbsolute(compositionStart))) { + return NS_ERROR_FAILURE; + } + } else { + LineBreakType lineBreakType = GetLineBreakType(aEvent); + uint32_t selectionStart = 0; + rv = GetStartOffset(mFirstSelectedRawRange, &selectionStart, + lineBreakType); + if (NS_WARN_IF(NS_FAILED(rv))) { + return NS_ERROR_FAILURE; + } + if (NS_WARN_IF(!aEvent->mInput.MakeOffsetAbsolute(selectionStart))) { + return NS_ERROR_FAILURE; + } + } + } + + // Ideally, we should emplace only when we return succeeded event. + // However, we need to emplace here since it's hard to store the various + // result. Intead, `HandleQueryContentEvent()` will reset `mReply` if + // corresponding handler returns error. + aEvent->EmplaceReply(); + + aEvent->mReply->mContentsRoot = mRootElement.get(); + aEvent->mReply->mIsEditableContent = + mRootElement && mRootElement->IsEditable(); + + nsRect r; + nsIFrame* frame = nsCaret::GetGeometry(mSelection, &r); + if (!frame) { + frame = mRootElement->GetPrimaryFrame(); + if (NS_WARN_IF(!frame)) { + return NS_ERROR_FAILURE; + } + } + aEvent->mReply->mFocusedWidget = frame->GetNearestWidget(); + + return NS_OK; +} + +nsresult ContentEventHandler::Init(WidgetSelectionEvent* aEvent) { + NS_ASSERTION(aEvent, "aEvent must not be null"); + + nsresult rv = InitCommon(aEvent->mMessage); + NS_ENSURE_SUCCESS(rv, rv); + + aEvent->mSucceeded = false; + + return NS_OK; +} + +nsIContent* ContentEventHandler::GetFocusedContent() { + nsCOMPtr<nsPIDOMWindowOuter> window = mDocument->GetWindow(); + nsCOMPtr<nsPIDOMWindowOuter> focusedWindow; + return nsFocusManager::GetFocusedDescendant( + window, nsFocusManager::eIncludeAllDescendants, + getter_AddRefs(focusedWindow)); +} + +nsresult ContentEventHandler::QueryContentRect( + nsIContent* aContent, WidgetQueryContentEvent* aEvent) { + MOZ_ASSERT(aContent, "aContent must not be null"); + + nsIFrame* frame = aContent->GetPrimaryFrame(); + NS_ENSURE_TRUE(frame, NS_ERROR_FAILURE); + + // get rect for first frame + nsRect resultRect(nsPoint(0, 0), frame->GetRect().Size()); + nsresult rv = ConvertToRootRelativeOffset(frame, resultRect); + NS_ENSURE_SUCCESS(rv, rv); + + nsPresContext* presContext = frame->PresContext(); + + // account for any additional frames + while ((frame = frame->GetNextContinuation())) { + nsRect frameRect(nsPoint(0, 0), frame->GetRect().Size()); + rv = ConvertToRootRelativeOffset(frame, frameRect); + NS_ENSURE_SUCCESS(rv, rv); + resultRect.UnionRect(resultRect, frameRect); + } + + aEvent->mReply->mRect = LayoutDeviceIntRect::FromAppUnitsToOutside( + resultRect, presContext->AppUnitsPerDevPixel()); + // Returning empty rect may cause native IME confused, let's make sure to + // return non-empty rect. + EnsureNonEmptyRect(aEvent->mReply->mRect); + + return NS_OK; +} + +// Editor places a padding <br> element under its root content if the editor +// doesn't have any text. This happens even for single line editors. +// When we get text content and when we change the selection, +// we don't want to include the padding <br> elements at the end. +static bool IsContentBR(const nsIContent& aContent) { + const HTMLBRElement* brElement = HTMLBRElement::FromNode(aContent); + return brElement && !brElement->IsPaddingForEmptyLastLine() && + !brElement->IsPaddingForEmptyEditor(); +} + +static bool IsPaddingBR(const nsIContent& aContent) { + return aContent.IsHTMLElement(nsGkAtoms::br) && !IsContentBR(aContent); +} + +static void ConvertToNativeNewlines(nsString& aString) { +#if defined(XP_WIN) + aString.ReplaceSubstring(u"\n"_ns, u"\r\n"_ns); +#endif +} + +static void AppendString(nsString& aString, const Text& aTextNode) { + const uint32_t oldXPLength = aString.Length(); + aTextNode.TextFragment().AppendTo(aString); + if (aTextNode.HasFlag(NS_MAYBE_MASKED)) { + TextEditor::MaskString(aString, aTextNode, oldXPLength, 0); + } +} + +static void AppendSubString(nsString& aString, const Text& aTextNode, + uint32_t aXPOffset, uint32_t aXPLength) { + const uint32_t oldXPLength = aString.Length(); + aTextNode.TextFragment().AppendTo(aString, aXPOffset, aXPLength); + if (aTextNode.HasFlag(NS_MAYBE_MASKED)) { + TextEditor::MaskString(aString, aTextNode, oldXPLength, aXPOffset); + } +} + +#if defined(XP_WIN) +static uint32_t CountNewlinesInXPLength(const Text& aTextNode, + uint32_t aXPLength) { + const nsTextFragment& textFragment = aTextNode.TextFragment(); + // For automated tests, we should abort on debug build. + MOZ_ASSERT(aXPLength == UINT32_MAX || aXPLength <= textFragment.GetLength(), + "aXPLength is out-of-bounds"); + const uint32_t length = std::min(aXPLength, textFragment.GetLength()); + uint32_t newlines = 0; + for (uint32_t i = 0; i < length; ++i) { + if (textFragment.CharAt(i) == '\n') { + ++newlines; + } + } + return newlines; +} + +static uint32_t CountNewlinesInXPLength(const nsAString& aText) { + uint32_t count = 0; + const char16_t* end = aText.EndReading(); + for (const char16_t* iter = aText.BeginReading(); iter < end; ++iter) { + if (*iter == '\n') { + count++; + } + } + return count; +} + +static uint32_t CountNewlinesInNativeLength(const Text& aTextNode, + uint32_t aNativeLength) { + const nsTextFragment& textFragment = aTextNode.TextFragment(); + // For automated tests, we should abort on debug build. + MOZ_ASSERT((aNativeLength == UINT32_MAX || + aNativeLength <= textFragment.GetLength() * 2), + "aNativeLength is unexpected value"); + const uint32_t xpLength = textFragment.GetLength(); + uint32_t newlines = 0; + for (uint32_t i = 0, nativeOffset = 0; + i < xpLength && nativeOffset < aNativeLength; ++i, ++nativeOffset) { + // For automated tests, we should abort on debug build. + MOZ_ASSERT(i < xpLength, "i is out-of-bounds"); + if (textFragment.CharAt(i) == '\n') { + ++newlines; + ++nativeOffset; + } + } + return newlines; +} +#endif + +/* static */ +uint32_t ContentEventHandler::GetNativeTextLength(const Text& aTextNode, + uint32_t aStartOffset, + uint32_t aEndOffset) { + MOZ_ASSERT(aEndOffset >= aStartOffset, + "aEndOffset must be equals or larger than aStartOffset"); + if (aStartOffset == aEndOffset) { + return 0; + } + return GetTextLength(aTextNode, LINE_BREAK_TYPE_NATIVE, aEndOffset) - + GetTextLength(aTextNode, LINE_BREAK_TYPE_NATIVE, aStartOffset); +} + +/* static */ +uint32_t ContentEventHandler::GetNativeTextLength(const Text& aTextNode, + uint32_t aMaxLength) { + return GetTextLength(aTextNode, LINE_BREAK_TYPE_NATIVE, aMaxLength); +} + +/* static inline */ +uint32_t ContentEventHandler::GetBRLength(LineBreakType aLineBreakType) { +#if defined(XP_WIN) + // Length of \r\n + return (aLineBreakType == LINE_BREAK_TYPE_NATIVE) ? 2 : 1; +#else + return 1; +#endif +} + +/* static */ +uint32_t ContentEventHandler::GetTextLength(const Text& aTextNode, + LineBreakType aLineBreakType, + uint32_t aMaxLength) { + const uint32_t textLengthDifference = +#if defined(XP_WIN) + // On Windows, the length of a native newline ("\r\n") is twice the length + // of the XP newline ("\n"), so XP length is equal to the length of the + // native offset plus the number of newlines encountered in the string. + (aLineBreakType == LINE_BREAK_TYPE_NATIVE) + ? CountNewlinesInXPLength(aTextNode, aMaxLength) + : 0; +#else + // On other platforms, the native and XP newlines are the same. + 0; +#endif + + const uint32_t length = + std::min(aTextNode.TextFragment().GetLength(), aMaxLength); + return length + textLengthDifference; +} + +static uint32_t ConvertToXPOffset(const Text& aTextNode, + uint32_t aNativeOffset) { +#if defined(XP_WIN) + // On Windows, the length of a native newline ("\r\n") is twice the length of + // the XP newline ("\n"), so XP offset is equal to the length of the native + // offset minus the number of newlines encountered in the string. + return aNativeOffset - CountNewlinesInNativeLength(aTextNode, aNativeOffset); +#else + // On other platforms, the native and XP newlines are the same. + return aNativeOffset; +#endif +} + +/* static */ +uint32_t ContentEventHandler::GetNativeTextLength(const nsAString& aText) { + const uint32_t textLengthDifference = +#if defined(XP_WIN) + // On Windows, the length of a native newline ("\r\n") is twice the length + // of the XP newline ("\n"), so XP length is equal to the length of the + // native offset plus the number of newlines encountered in the string. + CountNewlinesInXPLength(aText); +#else + // On other platforms, the native and XP newlines are the same. + 0; +#endif + return aText.Length() + textLengthDifference; +} + +/* static */ +bool ContentEventHandler::ShouldBreakLineBefore(const nsIContent& aContent, + const Element* aRootElement) { + // We don't need to append linebreak at the start of the root element. + if (&aContent == aRootElement) { + return false; + } + + // If it's not an HTML element (including other markup language's elements), + // we shouldn't insert like break before that for now. Becoming this is a + // problem must be edge case. E.g., when ContentEventHandler is used with + // MathML or SVG elements. + if (!aContent.IsHTMLElement()) { + return false; + } + + // If the element is <br>, we need to check if the <br> is caused by web + // content. Otherwise, i.e., it's caused by internal reason of Gecko, + // it shouldn't be exposed as a line break to flatten text. + if (aContent.IsHTMLElement(nsGkAtoms::br)) { + return IsContentBR(aContent); + } + + // Note that ideally, we should refer the style of the primary frame of + // aContent for deciding if it's an inline. However, it's difficult + // IMEContentObserver to notify IME of text change caused by style change. + // Therefore, currently, we should check only from the tag for now. + if (aContent.IsAnyOfHTMLElements( + nsGkAtoms::a, nsGkAtoms::abbr, nsGkAtoms::acronym, nsGkAtoms::b, + nsGkAtoms::bdi, nsGkAtoms::bdo, nsGkAtoms::big, nsGkAtoms::cite, + nsGkAtoms::code, nsGkAtoms::data, nsGkAtoms::del, nsGkAtoms::dfn, + nsGkAtoms::em, nsGkAtoms::font, nsGkAtoms::i, nsGkAtoms::ins, + nsGkAtoms::kbd, nsGkAtoms::mark, nsGkAtoms::s, nsGkAtoms::samp, + nsGkAtoms::small, nsGkAtoms::span, nsGkAtoms::strike, + nsGkAtoms::strong, nsGkAtoms::sub, nsGkAtoms::sup, nsGkAtoms::time, + nsGkAtoms::tt, nsGkAtoms::u, nsGkAtoms::var)) { + return false; + } + + // If the element is unknown element, we shouldn't insert line breaks before + // it since unknown elements should be ignored. + RefPtr<HTMLUnknownElement> unknownHTMLElement = + do_QueryObject(const_cast<nsIContent*>(&aContent)); + return !unknownHTMLElement; +} + +nsresult ContentEventHandler::GenerateFlatTextContent( + const Element* aElement, nsString& aString, LineBreakType aLineBreakType) { + MOZ_ASSERT(aString.IsEmpty()); + + RawRange rawRange; + nsresult rv = rawRange.SelectNodeContents(aElement); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + return GenerateFlatTextContent(rawRange, aString, aLineBreakType); +} + +nsresult ContentEventHandler::GenerateFlatTextContent( + const RawRange& aRawRange, nsString& aString, + LineBreakType aLineBreakType) { + MOZ_ASSERT(aString.IsEmpty()); + + if (aRawRange.Collapsed()) { + return NS_OK; + } + + nsINode* startNode = aRawRange.GetStartContainer(); + nsINode* endNode = aRawRange.GetEndContainer(); + if (NS_WARN_IF(!startNode) || NS_WARN_IF(!endNode)) { + return NS_ERROR_FAILURE; + } + + if (startNode == endNode && startNode->IsText()) { + AppendSubString(aString, *startNode->AsText(), aRawRange.StartOffset(), + aRawRange.EndOffset() - aRawRange.StartOffset()); + ConvertToNativeNewlines(aString); + return NS_OK; + } + + PreContentIterator preOrderIter; + nsresult rv = + preOrderIter.Init(aRawRange.Start().AsRaw(), aRawRange.End().AsRaw()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + for (; !preOrderIter.IsDone(); preOrderIter.Next()) { + nsINode* node = preOrderIter.GetCurrentNode(); + if (NS_WARN_IF(!node)) { + break; + } + if (!node->IsContent()) { + continue; + } + + if (const Text* textNode = Text::FromNode(node)) { + if (textNode == startNode) { + AppendSubString(aString, *textNode, aRawRange.StartOffset(), + textNode->TextLength() - aRawRange.StartOffset()); + } else if (textNode == endNode) { + AppendSubString(aString, *textNode, 0, aRawRange.EndOffset()); + } else { + AppendString(aString, *textNode); + } + } else if (ShouldBreakLineBefore(*node->AsContent(), mRootElement)) { + aString.Append(char16_t('\n')); + } + } + if (aLineBreakType == LINE_BREAK_TYPE_NATIVE) { + ConvertToNativeNewlines(aString); + } + return NS_OK; +} + +static FontRange* AppendFontRange(nsTArray<FontRange>& aFontRanges, + uint32_t aBaseOffset) { + FontRange* fontRange = aFontRanges.AppendElement(); + fontRange->mStartOffset = aBaseOffset; + return fontRange; +} + +/* static */ +uint32_t ContentEventHandler::GetTextLengthInRange( + const Text& aTextNode, uint32_t aXPStartOffset, uint32_t aXPEndOffset, + LineBreakType aLineBreakType) { + return aLineBreakType == LINE_BREAK_TYPE_NATIVE + ? GetNativeTextLength(aTextNode, aXPStartOffset, aXPEndOffset) + : aXPEndOffset - aXPStartOffset; +} + +/* static */ +void ContentEventHandler::AppendFontRanges(FontRangeArray& aFontRanges, + const Text& aTextNode, + uint32_t aBaseOffset, + uint32_t aXPStartOffset, + uint32_t aXPEndOffset, + LineBreakType aLineBreakType) { + nsIFrame* frame = aTextNode.GetPrimaryFrame(); + if (!frame) { + // It is a non-rendered content, create an empty range for it. + AppendFontRange(aFontRanges, aBaseOffset); + return; + } + + uint32_t baseOffset = aBaseOffset; +#ifdef DEBUG + { + nsTextFrame* text = do_QueryFrame(frame); + MOZ_ASSERT(text, "Not a text frame"); + } +#endif + auto* curr = static_cast<nsTextFrame*>(frame); + while (curr) { + uint32_t frameXPStart = std::max( + static_cast<uint32_t>(curr->GetContentOffset()), aXPStartOffset); + uint32_t frameXPEnd = + std::min(static_cast<uint32_t>(curr->GetContentEnd()), aXPEndOffset); + if (frameXPStart >= frameXPEnd) { + curr = curr->GetNextContinuation(); + continue; + } + + gfxSkipCharsIterator iter = curr->EnsureTextRun(nsTextFrame::eInflated); + gfxTextRun* textRun = curr->GetTextRun(nsTextFrame::eInflated); + + nsTextFrame* next = nullptr; + if (frameXPEnd < aXPEndOffset) { + next = curr->GetNextContinuation(); + while (next && next->GetTextRun(nsTextFrame::eInflated) == textRun) { + frameXPEnd = std::min(static_cast<uint32_t>(next->GetContentEnd()), + aXPEndOffset); + next = + frameXPEnd < aXPEndOffset ? next->GetNextContinuation() : nullptr; + } + } + + gfxTextRun::Range skipRange(iter.ConvertOriginalToSkipped(frameXPStart), + iter.ConvertOriginalToSkipped(frameXPEnd)); + uint32_t lastXPEndOffset = frameXPStart; + for (gfxTextRun::GlyphRunIterator runIter(textRun, skipRange); + !runIter.AtEnd(); runIter.NextRun()) { + gfxFont* font = runIter.GlyphRun()->mFont.get(); + uint32_t startXPOffset = + iter.ConvertSkippedToOriginal(runIter.StringStart()); + // It is possible that the first glyph run has exceeded the frame, + // because the whole frame is filled by skipped chars. + if (startXPOffset >= frameXPEnd) { + break; + } + + if (startXPOffset > lastXPEndOffset) { + // Create range for skipped leading chars. + AppendFontRange(aFontRanges, baseOffset); + baseOffset += GetTextLengthInRange(aTextNode, lastXPEndOffset, + startXPOffset, aLineBreakType); + } + + FontRange* fontRange = AppendFontRange(aFontRanges, baseOffset); + fontRange->mFontName.Append(NS_ConvertUTF8toUTF16(font->GetName())); + + ParentLayerToScreenScale2D cumulativeResolution = + ParentLayerToParentLayerScale( + frame->PresShell()->GetCumulativeResolution()) * + nsLayoutUtils::GetTransformToAncestorScaleCrossProcessForFrameMetrics( + frame); + float scale = + std::max(cumulativeResolution.xScale, cumulativeResolution.yScale); + + fontRange->mFontSize = font->GetAdjustedSize() * scale; + + // The converted original offset may exceed the range, + // hence we need to clamp it. + uint32_t endXPOffset = iter.ConvertSkippedToOriginal(runIter.StringEnd()); + endXPOffset = std::min(frameXPEnd, endXPOffset); + baseOffset += GetTextLengthInRange(aTextNode, startXPOffset, endXPOffset, + aLineBreakType); + lastXPEndOffset = endXPOffset; + } + if (lastXPEndOffset < frameXPEnd) { + // Create range for skipped trailing chars. It also handles case + // that the whole frame contains only skipped chars. + AppendFontRange(aFontRanges, baseOffset); + baseOffset += GetTextLengthInRange(aTextNode, lastXPEndOffset, frameXPEnd, + aLineBreakType); + } + + curr = next; + } +} + +nsresult ContentEventHandler::GenerateFlatFontRanges( + const RawRange& aRawRange, FontRangeArray& aFontRanges, uint32_t& aLength, + LineBreakType aLineBreakType) { + MOZ_ASSERT(aFontRanges.IsEmpty(), "aRanges must be empty array"); + + if (aRawRange.Collapsed()) { + return NS_OK; + } + + nsINode* startNode = aRawRange.GetStartContainer(); + nsINode* endNode = aRawRange.GetEndContainer(); + if (NS_WARN_IF(!startNode) || NS_WARN_IF(!endNode)) { + return NS_ERROR_FAILURE; + } + + // baseOffset is the flattened offset of each content node. + uint32_t baseOffset = 0; + PreContentIterator preOrderIter; + nsresult rv = + preOrderIter.Init(aRawRange.Start().AsRaw(), aRawRange.End().AsRaw()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + for (; !preOrderIter.IsDone(); preOrderIter.Next()) { + nsINode* node = preOrderIter.GetCurrentNode(); + if (NS_WARN_IF(!node)) { + break; + } + if (!node->IsContent()) { + continue; + } + nsIContent* content = node->AsContent(); + + if (const Text* textNode = Text::FromNode(content)) { + const uint32_t startOffset = + textNode != startNode ? 0 : aRawRange.StartOffset(); + const uint32_t endOffset = + textNode != endNode ? textNode->TextLength() : aRawRange.EndOffset(); + AppendFontRanges(aFontRanges, *textNode, baseOffset, startOffset, + endOffset, aLineBreakType); + baseOffset += GetTextLengthInRange(*textNode, startOffset, endOffset, + aLineBreakType); + } else if (ShouldBreakLineBefore(*content, mRootElement)) { + if (aFontRanges.IsEmpty()) { + MOZ_ASSERT(baseOffset == 0); + FontRange* fontRange = AppendFontRange(aFontRanges, baseOffset); + if (nsIFrame* frame = content->GetPrimaryFrame()) { + const nsFont& font = frame->GetParent()->StyleFont()->mFont; + const StyleFontFamilyList& fontList = font.family.families; + MOZ_ASSERT(!fontList.list.IsEmpty(), "Empty font family?"); + const StyleSingleFontFamily* fontName = + fontList.list.IsEmpty() ? nullptr : &fontList.list.AsSpan()[0]; + nsAutoCString name; + if (fontName) { + fontName->AppendToString(name, false); + } + AppendUTF8toUTF16(name, fontRange->mFontName); + + ParentLayerToScreenScale2D cumulativeResolution = + ParentLayerToParentLayerScale( + frame->PresShell()->GetCumulativeResolution()) * + nsLayoutUtils:: + GetTransformToAncestorScaleCrossProcessForFrameMetrics(frame); + + float scale = std::max(cumulativeResolution.xScale, + cumulativeResolution.yScale); + + fontRange->mFontSize = frame->PresContext()->CSSPixelsToDevPixels( + font.size.ToCSSPixels() * scale); + } + } + baseOffset += GetBRLength(aLineBreakType); + } + } + + aLength = baseOffset; + return NS_OK; +} + +nsresult ContentEventHandler::ExpandToClusterBoundary( + Text& aTextNode, bool aForward, uint32_t* aXPOffset) const { + // XXX This method assumes that the frame boundaries must be cluster + // boundaries. It's false, but no problem now, maybe. + if (*aXPOffset == 0 || *aXPOffset == aTextNode.TextLength()) { + return NS_OK; + } + + NS_ASSERTION(*aXPOffset <= aTextNode.TextLength(), "offset is out of range."); + + MOZ_DIAGNOSTIC_ASSERT(mDocument->GetPresShell()); + int32_t offsetInFrame; + CaretAssociationHint hint = + aForward ? CARET_ASSOCIATE_BEFORE : CARET_ASSOCIATE_AFTER; + nsIFrame* frame = nsFrameSelection::GetFrameForNodeOffset( + &aTextNode, int32_t(*aXPOffset), hint, &offsetInFrame); + if (frame) { + auto [startOffset, endOffset] = frame->GetOffsets(); + if (*aXPOffset == static_cast<uint32_t>(startOffset) || + *aXPOffset == static_cast<uint32_t>(endOffset)) { + return NS_OK; + } + if (!frame->IsTextFrame()) { + return NS_ERROR_FAILURE; + } + nsTextFrame* textFrame = static_cast<nsTextFrame*>(frame); + int32_t newOffsetInFrame = *aXPOffset - startOffset; + newOffsetInFrame += aForward ? -1 : 1; + // PeekOffsetCharacter() should respect cluster but ignore user-select + // style. If it returns "FOUND", we should use the result. Otherwise, + // we shouldn't use the result because the offset was moved to reversed + // direction. + nsTextFrame::PeekOffsetCharacterOptions options; + options.mRespectClusters = true; + options.mIgnoreUserStyleAll = true; + if (textFrame->PeekOffsetCharacter(aForward, &newOffsetInFrame, options) == + nsIFrame::FOUND) { + *aXPOffset = startOffset + newOffsetInFrame; + return NS_OK; + } + } + + // If the frame isn't available, we only can check surrogate pair... + if (aTextNode.TextFragment().IsLowSurrogateFollowingHighSurrogateAt( + *aXPOffset)) { + *aXPOffset += aForward ? 1 : -1; + } + return NS_OK; +} + +nsresult ContentEventHandler::SetRawRangeFromFlatTextOffset( + RawRange* aRawRange, uint32_t aOffset, uint32_t aLength, + LineBreakType aLineBreakType, bool aExpandToClusterBoundaries, + uint32_t* aNewOffset, Text** aLastTextNode) { + if (aNewOffset) { + *aNewOffset = aOffset; + } + if (aLastTextNode) { + *aLastTextNode = nullptr; + } + + // Special case like <br contenteditable> + if (!mRootElement->HasChildren()) { + nsresult rv = aRawRange->CollapseTo(RawRangeBoundary(mRootElement, 0u)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + PreContentIterator preOrderIter; + nsresult rv = preOrderIter.Init(mRootElement); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + uint32_t offset = 0; + uint32_t endOffset = aOffset + aLength; + bool startSet = false; + for (; !preOrderIter.IsDone(); preOrderIter.Next()) { + nsINode* node = preOrderIter.GetCurrentNode(); + if (NS_WARN_IF(!node)) { + break; + } + // FYI: mRootElement shouldn't cause any text. So, we can skip it simply. + if (node == mRootElement || !node->IsContent()) { + continue; + } + nsIContent* const content = node->AsContent(); + Text* const contentAsText = Text::FromNode(content); + + if (aLastTextNode && contentAsText) { + NS_IF_RELEASE(*aLastTextNode); + NS_ADDREF(*aLastTextNode = contentAsText); + } + + uint32_t textLength = contentAsText + ? GetTextLength(*contentAsText, aLineBreakType) + : (ShouldBreakLineBefore(*content, mRootElement) + ? GetBRLength(aLineBreakType) + : 0); + if (!textLength) { + continue; + } + + // When the start offset is in between accumulated offset and the last + // offset of the node, the node is the start node of the range. + if (!startSet && aOffset <= offset + textLength) { + nsINode* startNode = nullptr; + Maybe<uint32_t> startNodeOffset; + if (contentAsText) { + // Rule #1.1: [textNode or text[Node or textNode[ + uint32_t xpOffset = aOffset - offset; + if (aLineBreakType == LINE_BREAK_TYPE_NATIVE) { + xpOffset = ConvertToXPOffset(*contentAsText, xpOffset); + } + + if (aExpandToClusterBoundaries) { + const uint32_t oldXPOffset = xpOffset; + nsresult rv = + ExpandToClusterBoundary(*contentAsText, false, &xpOffset); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + if (aNewOffset) { + // This is correct since a cluster shouldn't include line break. + *aNewOffset -= (oldXPOffset - xpOffset); + } + } + startNode = contentAsText; + startNodeOffset = Some(xpOffset); + } else if (aOffset < offset + textLength) { + // Rule #1.2 [<element> + startNode = content->GetParent(); + if (NS_WARN_IF(!startNode)) { + return NS_ERROR_FAILURE; + } + startNodeOffset = startNode->ComputeIndexOf(content); + if (MOZ_UNLIKELY(NS_WARN_IF(startNodeOffset.isNothing()))) { + // The content is being removed from the parent! + return NS_ERROR_FAILURE; + } + } else if (!content->HasChildren()) { + // Rule #1.3: <element/>[ + startNode = content->GetParent(); + if (NS_WARN_IF(!startNode)) { + return NS_ERROR_FAILURE; + } + startNodeOffset = startNode->ComputeIndexOf(content); + if (MOZ_UNLIKELY(NS_WARN_IF(startNodeOffset.isNothing()))) { + // The content is being removed from the parent! + return NS_ERROR_FAILURE; + } + MOZ_ASSERT(*startNodeOffset != UINT32_MAX); + ++(*startNodeOffset); + } else { + // Rule #1.4: <element>[ + startNode = content; + startNodeOffset = Some(0); + } + NS_ASSERTION(startNode, "startNode must not be nullptr"); + MOZ_ASSERT(startNodeOffset.isSome(), + "startNodeOffset must not be Nothing"); + rv = aRawRange->SetStart(startNode, *startNodeOffset); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + startSet = true; + + if (!aLength) { + rv = aRawRange->SetEnd(startNode, *startNodeOffset); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + return NS_OK; + } + } + + // When the end offset is in the content, the node is the end node of the + // range. + if (endOffset <= offset + textLength) { + MOZ_ASSERT(startSet, "The start of the range should've been set already"); + if (contentAsText) { + // Rule #2.1: ]textNode or text]Node or textNode] + uint32_t xpOffset = endOffset - offset; + if (aLineBreakType == LINE_BREAK_TYPE_NATIVE) { + const uint32_t xpOffsetCurrent = + ConvertToXPOffset(*contentAsText, xpOffset); + if (xpOffset && GetBRLength(aLineBreakType) > 1) { + MOZ_ASSERT(GetBRLength(aLineBreakType) == 2); + const uint32_t xpOffsetPre = + ConvertToXPOffset(*contentAsText, xpOffset - 1); + // If previous character's XP offset is same as current character's, + // it means that the end offset is between \r and \n. So, the + // range end should be after the \n. + if (xpOffsetPre == xpOffsetCurrent) { + xpOffset = xpOffsetCurrent + 1; + } else { + xpOffset = xpOffsetCurrent; + } + } + } + if (aExpandToClusterBoundaries) { + nsresult rv = + ExpandToClusterBoundary(*contentAsText, true, &xpOffset); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + NS_ASSERTION(xpOffset <= INT32_MAX, "The end node offset is too large"); + nsresult rv = aRawRange->SetEnd(contentAsText, xpOffset); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + return NS_OK; + } + + if (endOffset == offset) { + // Rule #2.2: ]<element> + // NOTE: Please don't crash on release builds because it must be + // overreaction but we shouldn't allow this bug when some + // automated tests find this. + MOZ_ASSERT(false, + "This case should've already been handled at " + "the last node which caused some text"); + return NS_ERROR_FAILURE; + } + + if (content->HasChildren() && + ShouldBreakLineBefore(*content, mRootElement)) { + // Rule #2.3: </element>] + rv = aRawRange->SetEnd(content, 0); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + return NS_OK; + } + + // Rule #2.4: <element/>] + nsINode* endNode = content->GetParent(); + if (NS_WARN_IF(!endNode)) { + return NS_ERROR_FAILURE; + } + const Maybe<uint32_t> indexInParent = endNode->ComputeIndexOf(content); + if (MOZ_UNLIKELY(NS_WARN_IF(indexInParent.isNothing()))) { + // The content is being removed from the parent! + return NS_ERROR_FAILURE; + } + MOZ_ASSERT(*indexInParent != UINT32_MAX); + rv = aRawRange->SetEnd(endNode, *indexInParent + 1); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + return NS_OK; + } + + offset += textLength; + } + + if (!startSet) { + if (!offset) { + // Rule #1.5: <root>[</root> + // When there are no nodes causing text, the start of the DOM range + // should be start of the root node since clicking on such editor (e.g., + // <div contenteditable><span></span></div>) sets caret to the start of + // the editor (i.e., before <span> in the example). + rv = aRawRange->SetStart(mRootElement, 0); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + if (!aLength) { + rv = aRawRange->SetEnd(mRootElement, 0); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + return NS_OK; + } + } else { + // Rule #1.5: [</root> + rv = aRawRange->SetStart(mRootElement, mRootElement->GetChildCount()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + if (aNewOffset) { + *aNewOffset = offset; + } + } + // Rule #2.5: ]</root> + rv = aRawRange->SetEnd(mRootElement, mRootElement->GetChildCount()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + return NS_OK; +} + +/* static */ +LineBreakType ContentEventHandler::GetLineBreakType( + WidgetQueryContentEvent* aEvent) { + return GetLineBreakType(aEvent->mUseNativeLineBreak); +} + +/* static */ +LineBreakType ContentEventHandler::GetLineBreakType( + WidgetSelectionEvent* aEvent) { + return GetLineBreakType(aEvent->mUseNativeLineBreak); +} + +/* static */ +LineBreakType ContentEventHandler::GetLineBreakType(bool aUseNativeLineBreak) { + return aUseNativeLineBreak ? LINE_BREAK_TYPE_NATIVE : LINE_BREAK_TYPE_XP; +} + +nsresult ContentEventHandler::HandleQueryContentEvent( + WidgetQueryContentEvent* aEvent) { + nsresult rv = NS_ERROR_NOT_IMPLEMENTED; + switch (aEvent->mMessage) { + case eQuerySelectedText: + rv = OnQuerySelectedText(aEvent); + break; + case eQueryTextContent: + rv = OnQueryTextContent(aEvent); + break; + case eQueryCaretRect: + rv = OnQueryCaretRect(aEvent); + break; + case eQueryTextRect: + rv = OnQueryTextRect(aEvent); + break; + case eQueryTextRectArray: + rv = OnQueryTextRectArray(aEvent); + break; + case eQueryEditorRect: + rv = OnQueryEditorRect(aEvent); + break; + case eQueryContentState: + rv = OnQueryContentState(aEvent); + break; + case eQuerySelectionAsTransferable: + rv = OnQuerySelectionAsTransferable(aEvent); + break; + case eQueryCharacterAtPoint: + rv = OnQueryCharacterAtPoint(aEvent); + break; + case eQueryDOMWidgetHittest: + rv = OnQueryDOMWidgetHittest(aEvent); + break; + default: + break; + } + if (NS_FAILED(rv)) { + aEvent->mReply.reset(); // Mark the query failed. + return rv; + } + + MOZ_ASSERT(aEvent->Succeeded()); + return NS_OK; +} + +// Similar to nsFrameSelection::GetFrameForNodeOffset, +// but this is more flexible for OnQueryTextRect to use +static Result<nsIFrame*, nsresult> GetFrameForTextRect(const nsINode* aNode, + int32_t aNodeOffset, + bool aHint) { + const nsIContent* content = nsIContent::FromNodeOrNull(aNode); + if (NS_WARN_IF(!content)) { + return Err(NS_ERROR_UNEXPECTED); + } + nsIFrame* frame = content->GetPrimaryFrame(); + // The node may be invisible, e.g., `display: none`, invisible text node + // around block elements, etc. Therefore, don't warn when we don't find + // a primary frame. + if (!frame) { + return nullptr; + } + int32_t childNodeOffset = 0; + nsIFrame* returnFrame = nullptr; + nsresult rv = frame->GetChildFrameContainingOffset( + aNodeOffset, aHint, &childNodeOffset, &returnFrame); + if (NS_FAILED(rv)) { + return Err(rv); + } + return returnFrame; +} + +nsresult ContentEventHandler::OnQuerySelectedText( + WidgetQueryContentEvent* aEvent) { + nsresult rv = Init(aEvent); + if (NS_FAILED(rv)) { + return rv; + } + + MOZ_ASSERT(aEvent->mReply->mOffsetAndData.isNothing()); + + if (!mFirstSelectedRawRange.IsPositioned()) { + MOZ_ASSERT(aEvent->mReply->mOffsetAndData.isNothing()); + MOZ_ASSERT_IF(mSelection, !mSelection->RangeCount()); + // This is special case that `mReply` is emplaced, but mOffsetAndData is + // not emplaced but treated as succeeded because of no selection ranges + // is a usual case. + return NS_OK; + } + + nsINode* const startNode = mFirstSelectedRawRange.GetStartContainer(); + nsINode* const endNode = mFirstSelectedRawRange.GetEndContainer(); + + // Make sure the selection is within the root content range. + if (!startNode->IsInclusiveDescendantOf(mRootElement) || + !endNode->IsInclusiveDescendantOf(mRootElement)) { + return NS_ERROR_NOT_AVAILABLE; + } + + LineBreakType lineBreakType = GetLineBreakType(aEvent); + uint32_t startOffset = 0; + if (NS_WARN_IF(NS_FAILED(GetStartOffset(mFirstSelectedRawRange, &startOffset, + lineBreakType)))) { + return NS_ERROR_FAILURE; + } + + const RangeBoundary& anchorRef = mSelection->RangeCount() > 0 + ? mSelection->AnchorRef() + : mFirstSelectedRawRange.Start(); + const RangeBoundary& focusRef = mSelection->RangeCount() > 0 + ? mSelection->FocusRef() + : mFirstSelectedRawRange.End(); + if (NS_WARN_IF(!anchorRef.IsSet()) || NS_WARN_IF(!focusRef.IsSet())) { + return NS_ERROR_FAILURE; + } + + if (mSelection->RangeCount()) { + // If there is only one selection range, the anchor/focus node and offset + // are the information of the range. Therefore, we have the direction + // information. + if (mSelection->RangeCount() == 1) { + // The selection's points should always be comparable, independent of the + // selection (see nsISelectionController.idl). + Maybe<int32_t> compare = + nsContentUtils::ComparePoints(anchorRef, focusRef); + if (compare.isNothing()) { + return NS_ERROR_FAILURE; + } + + aEvent->mReply->mReversed = compare.value() > 0; + } + // However, if there are 2 or more selection ranges, we have no information + // of that. + else { + aEvent->mReply->mReversed = false; + } + + nsString selectedString; + if (!mFirstSelectedRawRange.Collapsed() && + NS_WARN_IF(NS_FAILED(GenerateFlatTextContent( + mFirstSelectedRawRange, selectedString, lineBreakType)))) { + return NS_ERROR_FAILURE; + } + aEvent->mReply->mOffsetAndData.emplace(startOffset, selectedString, + OffsetAndDataFor::SelectedString); + } else { + NS_ASSERTION(anchorRef == focusRef, + "When mSelection doesn't have selection, " + "mFirstSelectedRawRange must be collapsed"); + + aEvent->mReply->mReversed = false; + aEvent->mReply->mOffsetAndData.emplace(startOffset, EmptyString(), + OffsetAndDataFor::SelectedString); + } + + Result<nsIFrame*, nsresult> frameForTextRectOrError = GetFrameForTextRect( + focusRef.Container(), + focusRef.Offset(RangeBoundary::OffsetFilter::kValidOffsets).valueOr(0), + true); + if (NS_WARN_IF(frameForTextRectOrError.isErr()) || + !frameForTextRectOrError.inspect()) { + aEvent->mReply->mWritingMode = WritingMode(); + } else { + aEvent->mReply->mWritingMode = + frameForTextRectOrError.inspect()->GetWritingMode(); + } + + MOZ_ASSERT(aEvent->Succeeded()); + return NS_OK; +} + +nsresult ContentEventHandler::OnQueryTextContent( + WidgetQueryContentEvent* aEvent) { + nsresult rv = Init(aEvent); + if (NS_FAILED(rv)) { + return rv; + } + + MOZ_ASSERT(aEvent->mReply->mOffsetAndData.isNothing()); + + LineBreakType lineBreakType = GetLineBreakType(aEvent); + + RawRange rawRange; + uint32_t startOffset = 0; + if (NS_WARN_IF(NS_FAILED(SetRawRangeFromFlatTextOffset( + &rawRange, aEvent->mInput.mOffset, aEvent->mInput.mLength, + lineBreakType, false, &startOffset)))) { + return NS_ERROR_FAILURE; + } + + nsString textInRange; + if (NS_WARN_IF(NS_FAILED( + GenerateFlatTextContent(rawRange, textInRange, lineBreakType)))) { + return NS_ERROR_FAILURE; + } + + aEvent->mReply->mOffsetAndData.emplace(startOffset, textInRange, + OffsetAndDataFor::EditorString); + + if (aEvent->mWithFontRanges) { + uint32_t fontRangeLength; + if (NS_WARN_IF(NS_FAILED( + GenerateFlatFontRanges(rawRange, aEvent->mReply->mFontRanges, + fontRangeLength, lineBreakType)))) { + return NS_ERROR_FAILURE; + } + + MOZ_ASSERT(fontRangeLength == aEvent->mReply->DataLength(), + "Font ranges doesn't match the string"); + } + + MOZ_ASSERT(aEvent->Succeeded()); + return NS_OK; +} + +void ContentEventHandler::EnsureNonEmptyRect(nsRect& aRect) const { + // See the comment in ContentEventHandler.h why this doesn't set them to + // one device pixel. + aRect.height = std::max(1, aRect.height); + aRect.width = std::max(1, aRect.width); +} + +void ContentEventHandler::EnsureNonEmptyRect(LayoutDeviceIntRect& aRect) const { + aRect.height = std::max(1, aRect.height); + aRect.width = std::max(1, aRect.width); +} + +ContentEventHandler::FrameAndNodeOffset +ContentEventHandler::GetFirstFrameInRangeForTextRect( + const RawRange& aRawRange) { + NodePosition nodePosition; + PreContentIterator preOrderIter; + nsresult rv = + preOrderIter.Init(aRawRange.Start().AsRaw(), aRawRange.End().AsRaw()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return FrameAndNodeOffset(); + } + for (; !preOrderIter.IsDone(); preOrderIter.Next()) { + nsINode* node = preOrderIter.GetCurrentNode(); + if (NS_WARN_IF(!node)) { + break; + } + + auto* content = nsIContent::FromNode(node); + if (MOZ_UNLIKELY(!content)) { + continue; + } + + // If the node is invisible (e.g., the node is or is in an invisible node or + // it's a white-space only text node around a block boundary), we should + // ignore it. + if (!content->GetPrimaryFrame()) { + continue; + } + + if (auto* textNode = Text::FromNode(content)) { + // If the range starts at the end of a text node, we need to find + // next node which causes text. + const uint32_t offsetInNode = textNode == aRawRange.GetStartContainer() + ? aRawRange.StartOffset() + : 0u; + if (offsetInNode < textNode->TextDataLength()) { + nodePosition = {textNode, offsetInNode}; + break; + } + continue; + } + + // If the element node causes a line break before it, it's the first + // node causing text. + if (ShouldBreakLineBefore(*content, mRootElement) || + IsPaddingBR(*content)) { + nodePosition = {content, 0u}; + } + } + + if (!nodePosition.IsSetAndValid()) { + return FrameAndNodeOffset(); + } + + Result<nsIFrame*, nsresult> firstFrameOrError = GetFrameForTextRect( + nodePosition.Container(), + *nodePosition.Offset(NodePosition::OffsetFilter::kValidOffsets), true); + if (NS_WARN_IF(firstFrameOrError.isErr()) || !firstFrameOrError.inspect()) { + return FrameAndNodeOffset(); + } + return FrameAndNodeOffset( + firstFrameOrError.inspect(), + *nodePosition.Offset(NodePosition::OffsetFilter::kValidOffsets)); +} + +ContentEventHandler::FrameAndNodeOffset +ContentEventHandler::GetLastFrameInRangeForTextRect(const RawRange& aRawRange) { + NodePosition nodePosition; + PreContentIterator preOrderIter; + nsresult rv = + preOrderIter.Init(aRawRange.Start().AsRaw(), aRawRange.End().AsRaw()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return FrameAndNodeOffset(); + } + + const RangeBoundary& endPoint = aRawRange.End(); + MOZ_ASSERT(endPoint.IsSetAndValid()); + // If the end point is start of a text node or specified by its parent and + // index, the node shouldn't be included into the range. For example, + // with this case, |<p>abc[<br>]def</p>|, the range ends at 3rd children of + // <p> (see the range creation rules, "2.4. Cases: <element/>]"). This causes + // following frames: + // +----+-----+ + // | abc|[<br>| + // +----+-----+ + // +----+ + // |]def| + // +----+ + // So, if this method includes the 2nd text frame's rect to its result, the + // caller will return too tall rect which includes 2 lines in this case isn't + // expected by native IME (e.g., popup of IME will be positioned at bottom + // of "d" instead of right-bottom of "c"). Therefore, this method shouldn't + // include the last frame when its content isn't really in aRawRange. + nsINode* nextNodeOfRangeEnd = nullptr; + if (endPoint.Container()->IsText()) { + // Don't set nextNodeOfRangeEnd to the start node of aRawRange because if + // the container of the end is same as start node of the range, the text + // node shouldn't be next of range end even if the offset is 0. This + // could occur with empty text node. + if (endPoint.IsStartOfContainer() && + aRawRange.GetStartContainer() != endPoint.Container()) { + nextNodeOfRangeEnd = endPoint.Container(); + } + } else if (endPoint.IsSetAndValid()) { + nextNodeOfRangeEnd = endPoint.GetChildAtOffset(); + } + + for (preOrderIter.Last(); !preOrderIter.IsDone(); preOrderIter.Prev()) { + nsINode* node = preOrderIter.GetCurrentNode(); + if (NS_WARN_IF(!node)) { + break; + } + + if (node == nextNodeOfRangeEnd) { + continue; + } + + auto* content = nsIContent::FromNode(node); + if (MOZ_UNLIKELY(!content)) { + continue; + } + + // If the node is invisible (e.g., the node is or is in an invisible node or + // it's a white-space only text node around a block boundary), we should + // ignore it. + if (!content->GetPrimaryFrame()) { + continue; + } + + if (auto* textNode = Text::FromNode(node)) { + nodePosition = {textNode, textNode == aRawRange.GetEndContainer() + ? aRawRange.EndOffset() + : textNode->TextDataLength()}; + + // If the text node is empty or the last node of the range but the index + // is 0, we should store current position but continue looking for + // previous node (If there are no nodes before it, we should use current + // node position for returning its frame). + if (*nodePosition.Offset(NodePosition::OffsetFilter::kValidOffsets) == + 0) { + continue; + } + break; + } + + if (ShouldBreakLineBefore(*content, mRootElement) || + IsPaddingBR(*content)) { + nodePosition = {content, 0u}; + break; + } + } + + if (!nodePosition.IsSet()) { + return FrameAndNodeOffset(); + } + + Result<nsIFrame*, nsresult> lastFrameOrError = GetFrameForTextRect( + nodePosition.Container(), + *nodePosition.Offset(NodePosition::OffsetFilter::kValidOffsets), true); + if (NS_WARN_IF(lastFrameOrError.isErr()) || !lastFrameOrError.inspect()) { + return FrameAndNodeOffset(); + } + + // If the last frame is a text frame, we need to check if the range actually + // includes at least one character in the range. Therefore, if it's not a + // text frame, we need to do nothing anymore. + if (!lastFrameOrError.inspect()->IsTextFrame()) { + return FrameAndNodeOffset( + lastFrameOrError.inspect(), + *nodePosition.Offset(NodePosition::OffsetFilter::kValidOffsets)); + } + + int32_t start = lastFrameOrError.inspect()->GetOffsets().first; + + // If the start offset in the node is same as the computed offset in the + // node and it's not 0, the frame shouldn't be added to the text rect. So, + // this should return previous text frame and its last offset if there is + // at least one text frame. + if (*nodePosition.Offset(NodePosition::OffsetFilter::kValidOffsets) && + *nodePosition.Offset(NodePosition::OffsetFilter::kValidOffsets) == + static_cast<uint32_t>(start)) { + const uint32_t newNodePositionOffset = + *nodePosition.Offset(NodePosition::OffsetFilter::kValidOffsets); + MOZ_ASSERT(newNodePositionOffset != 0); + nodePosition = {nodePosition.Container(), newNodePositionOffset - 1u}; + lastFrameOrError = GetFrameForTextRect( + nodePosition.Container(), + *nodePosition.Offset(NodePosition::OffsetFilter::kValidOffsets), true); + if (NS_WARN_IF(lastFrameOrError.isErr()) || !lastFrameOrError.inspect()) { + return FrameAndNodeOffset(); + } + } + + return FrameAndNodeOffset( + lastFrameOrError.inspect(), + *nodePosition.Offset(NodePosition::OffsetFilter::kValidOffsets)); +} + +ContentEventHandler::FrameRelativeRect +ContentEventHandler::GetLineBreakerRectBefore(nsIFrame* aFrame) { + // Note that this method should be called only with an element's frame whose + // open tag causes a line break or moz-<br> for computing empty last line's + // rect. + MOZ_ASSERT(aFrame->GetContent()); + MOZ_ASSERT(ShouldBreakLineBefore(*aFrame->GetContent(), mRootElement) || + IsPaddingBR(*aFrame->GetContent())); + + nsIFrame* frameForFontMetrics = aFrame; + + // If it's not a <br> frame, this method computes the line breaker's rect + // outside the frame. Therefore, we need to compute with parent frame's + // font metrics in such case. + if (!aFrame->IsBrFrame() && aFrame->GetParent()) { + frameForFontMetrics = aFrame->GetParent(); + } + + // Note that <br> element's rect is decided with line-height but we need + // a rect only with font height. Additionally, <br> frame's width and + // height are 0 in quirks mode if it's not an empty line. So, we cannot + // use frame rect information even if it's a <br> frame. + + RefPtr<nsFontMetrics> fontMetrics = + nsLayoutUtils::GetInflatedFontMetricsForFrame(frameForFontMetrics); + if (NS_WARN_IF(!fontMetrics)) { + return FrameRelativeRect(); + } + + const WritingMode kWritingMode = frameForFontMetrics->GetWritingMode(); + + auto caretBlockAxisMetrics = + aFrame->GetCaretBlockAxisMetrics(kWritingMode, *fontMetrics); + nscoord inlineOffset = 0; + + // If aFrame isn't a <br> frame, caret should be at outside of it because + // the line break is before its open tag. For example, case of + // |<div><p>some text</p></div>|, caret is before <p> element and in <div> + // element, the caret should be left of top-left corner of <p> element like: + // + // +-<div>------------------- <div>'s border box + // | I +-<p>----------------- <p>'s border box + // | I | + // | I | + // | | + // ^- caret + // + // However, this is a hack for unusual scenario. This hack shouldn't be + // used as far as possible. + if (!aFrame->IsBrFrame()) { + if (kWritingMode.IsVertical() && !kWritingMode.IsLineInverted()) { + // above of top-right corner of aFrame. + caretBlockAxisMetrics.mOffset = + aFrame->GetRect().XMost() - caretBlockAxisMetrics.mExtent; + } else { + // above (For vertical) or left (For horizontal) of top-left corner of + // aFrame. + caretBlockAxisMetrics.mOffset = 0; + } + inlineOffset = -aFrame->PresContext()->AppUnitsPerDevPixel(); + } + FrameRelativeRect result(aFrame); + if (kWritingMode.IsVertical()) { + result.mRect.x = caretBlockAxisMetrics.mOffset; + result.mRect.y = inlineOffset; + result.mRect.width = caretBlockAxisMetrics.mExtent; + } else { + result.mRect.x = inlineOffset; + result.mRect.y = caretBlockAxisMetrics.mOffset; + result.mRect.height = caretBlockAxisMetrics.mExtent; + } + return result; +} + +ContentEventHandler::FrameRelativeRect +ContentEventHandler::GuessLineBreakerRectAfter(const Text& aTextNode) { + FrameRelativeRect result; + const int32_t length = static_cast<int32_t>(aTextNode.TextLength()); + if (NS_WARN_IF(length < 0)) { + return result; + } + // Get the last nsTextFrame which is caused by aTextNode. Note that + // a text node can cause multiple text frames, e.g., the text is too long + // and wrapped by its parent block or the text has line breakers and its + // white-space property respects the line breakers (e.g., |pre|). + Result<nsIFrame*, nsresult> lastTextFrameOrError = + GetFrameForTextRect(&aTextNode, length, true); + if (NS_WARN_IF(lastTextFrameOrError.isErr()) || + !lastTextFrameOrError.inspect()) { + return result; + } + const nsRect kLastTextFrameRect = lastTextFrameOrError.inspect()->GetRect(); + if (lastTextFrameOrError.inspect()->GetWritingMode().IsVertical()) { + // Below of the last text frame. + result.mRect.SetRect(0, kLastTextFrameRect.height, kLastTextFrameRect.width, + 0); + } else { + // Right of the last text frame (not bidi-aware). + result.mRect.SetRect(kLastTextFrameRect.width, 0, 0, + kLastTextFrameRect.height); + } + result.mBaseFrame = lastTextFrameOrError.unwrap(); + return result; +} + +ContentEventHandler::FrameRelativeRect +ContentEventHandler::GuessFirstCaretRectIn(nsIFrame* aFrame) { + const WritingMode kWritingMode = aFrame->GetWritingMode(); + nsPresContext* presContext = aFrame->PresContext(); + + // Computes the font height, but if it's not available, we should use + // default font size of Firefox. The default font size in default settings + // is 16px. + RefPtr<nsFontMetrics> fontMetrics = + nsLayoutUtils::GetInflatedFontMetricsForFrame(aFrame); + const nscoord kMaxHeight = fontMetrics + ? fontMetrics->MaxHeight() + : 16 * presContext->AppUnitsPerDevPixel(); + + nsRect caretRect; + const nsRect kContentRect = aFrame->GetContentRect() - aFrame->GetPosition(); + caretRect.y = kContentRect.y; + if (!kWritingMode.IsVertical()) { + if (kWritingMode.IsBidiLTR()) { + caretRect.x = kContentRect.x; + } else { + // Move 1px left for the space of caret itself. + const nscoord kOnePixel = presContext->AppUnitsPerDevPixel(); + caretRect.x = kContentRect.XMost() - kOnePixel; + } + caretRect.height = kMaxHeight; + // However, don't add kOnePixel here because it may cause 2px width at + // aligning the edge to device pixels. + caretRect.width = 1; + } else { + if (kWritingMode.IsVerticalLR()) { + caretRect.x = kContentRect.x; + } else { + caretRect.x = kContentRect.XMost() - kMaxHeight; + } + caretRect.width = kMaxHeight; + // Don't add app units for a device pixel because it may cause 2px height + // at aligning the edge to device pixels. + caretRect.height = 1; + } + return FrameRelativeRect(caretRect, aFrame); +} + +// static +LayoutDeviceIntRect ContentEventHandler::GetCaretRectBefore( + const LayoutDeviceIntRect& aCharRect, const WritingMode& aWritingMode) { + LayoutDeviceIntRect caretRectBefore(aCharRect); + if (aWritingMode.IsVertical()) { + caretRectBefore.height = 1; + } else { + // TODO: Make here bidi-aware. + caretRectBefore.width = 1; + } + return caretRectBefore; +} + +// static +nsRect ContentEventHandler::GetCaretRectBefore( + const nsRect& aCharRect, const WritingMode& aWritingMode) { + nsRect caretRectBefore(aCharRect); + if (aWritingMode.IsVertical()) { + // For making the height 1 device pixel after aligning the rect edges to + // device pixels, don't add one device pixel in app units here. + caretRectBefore.height = 1; + } else { + // TODO: Make here bidi-aware. + // For making the width 1 device pixel after aligning the rect edges to + // device pixels, don't add one device pixel in app units here. + caretRectBefore.width = 1; + } + return caretRectBefore; +} + +// static +LayoutDeviceIntRect ContentEventHandler::GetCaretRectAfter( + const LayoutDeviceIntRect& aCharRect, const WritingMode& aWritingMode) { + LayoutDeviceIntRect caretRectAfter(aCharRect); + if (aWritingMode.IsVertical()) { + caretRectAfter.y = aCharRect.YMost() + 1; + caretRectAfter.height = 1; + } else { + // TODO: Make here bidi-aware. + caretRectAfter.x = aCharRect.XMost() + 1; + caretRectAfter.width = 1; + } + return caretRectAfter; +} + +// static +nsRect ContentEventHandler::GetCaretRectAfter(nsPresContext& aPresContext, + const nsRect& aCharRect, + const WritingMode& aWritingMode) { + nsRect caretRectAfter(aCharRect); + const nscoord onePixel = aPresContext.AppUnitsPerDevPixel(); + if (aWritingMode.IsVertical()) { + caretRectAfter.y = aCharRect.YMost() + onePixel; + // For making the height 1 device pixel after aligning the rect edges to + // device pixels, don't add one device pixel in app units here. + caretRectAfter.height = 1; + } else { + // TODO: Make here bidi-aware. + caretRectAfter.x = aCharRect.XMost() + onePixel; + // For making the width 1 device pixel after aligning the rect edges to + // device pixels, don't add one device pixel in app units here. + caretRectAfter.width = 1; + } + return caretRectAfter; +} + +nsresult ContentEventHandler::OnQueryTextRectArray( + WidgetQueryContentEvent* aEvent) { + nsresult rv = Init(aEvent); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + MOZ_ASSERT(aEvent->mReply->mOffsetAndData.isNothing()); + + LineBreakType lineBreakType = GetLineBreakType(aEvent); + const uint32_t kBRLength = GetBRLength(lineBreakType); + + WritingMode lastVisibleFrameWritingMode; + LayoutDeviceIntRect rect; + uint32_t offset = aEvent->mInput.mOffset; + const uint32_t kEndOffset = aEvent->mInput.EndOffset(); + bool wasLineBreaker = false; + // lastCharRect stores the last charRect value (see below for the detail of + // charRect). + nsRect lastCharRect; + // lastFrame is base frame of lastCharRect. + // TODO: We should look for this if the first text is not visible. However, + // users cannot put caret invisible text and users cannot type in it + // at least only with user's operations. Therefore, we don't need to + // fix this immediately. + nsIFrame* lastFrame = nullptr; + nsAutoString flattenedAllText; + flattenedAllText.SetIsVoid(true); + while (offset < kEndOffset) { + RefPtr<Text> lastTextNode; + RawRange rawRange; + nsresult rv = + SetRawRangeFromFlatTextOffset(&rawRange, offset, 1, lineBreakType, true, + nullptr, getter_AddRefs(lastTextNode)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // TODO: When we crossed parent block boundary now, we should fill pending + // character rects with caret rect after the last visible character + // rect. + + // If the range is collapsed, offset has already reached the end of the + // contents. + if (rawRange.Collapsed()) { + break; + } + + // Get the first frame which causes some text after the offset. + FrameAndNodeOffset firstFrame = GetFirstFrameInRangeForTextRect(rawRange); + + // If GetFirstFrameInRangeForTextRect() does not return valid frame, that + // means that the offset reached the end of contents or there is no visible + // frame in the range generating flattened text. + if (!firstFrame.IsValid()) { + if (flattenedAllText.IsVoid()) { + flattenedAllText.SetIsVoid(false); + if (NS_WARN_IF(NS_FAILED(GenerateFlatTextContent( + mRootElement, flattenedAllText, lineBreakType)))) { + NS_WARNING("ContentEventHandler::GenerateFlatTextContent() failed"); + return NS_ERROR_FAILURE; + } + } + // If we've reached end of the root, append caret rect at the end of + // the root later. + if (offset >= flattenedAllText.Length()) { + break; + } + // Otherwise, we're in an invisible node. If the node is followed by a + // block boundary causing a line break, we can use the boundary. + // Otherwise, if the node follows a block boundary of a parent block, we + // can use caret rect at previous visible frame causing flattened text. + const uint32_t remainingLengthInCurrentRange = [&]() { + if (lastTextNode) { + if (rawRange.GetStartContainer() == lastTextNode) { + if (rawRange.StartOffset() < lastTextNode->TextDataLength()) { + return lastTextNode->TextDataLength() - rawRange.StartOffset(); + } + return 0u; + } + // Must be there are not nodes which may cause generating text. + // Therefore, we can skip all nodes before the last found text node + // and all text in the last text node. + return lastTextNode->TextDataLength(); + } + if (rawRange.GetStartContainer() && + rawRange.GetStartContainer()->IsContent() && + ShouldBreakLineBefore(*rawRange.GetStartContainer()->AsContent(), + mRootElement)) { + if (kBRLength != 1u && offset - aEvent->mInput.mOffset < kBRLength) { + // Don't return kBRLength if start position is less than the length + // of a line-break because the offset may be between CRLF on + // Windows. In the case, we will be again here and gets same + // result and we need to pay the penalty only once. Therefore, we + // can keep going without complicated check. + return 1u; + } + return kBRLength; + } + return 0u; + }(); + offset += std::max(1u, remainingLengthInCurrentRange); + continue; + } + + nsIContent* firstContent = firstFrame.mFrame->GetContent(); + if (NS_WARN_IF(!firstContent)) { + return NS_ERROR_FAILURE; + } + + bool startsBetweenLineBreaker = false; + nsAutoString chars; + lastVisibleFrameWritingMode = firstFrame->GetWritingMode(); + + nsIFrame* baseFrame = firstFrame; + // charRect should have each character rect or line breaker rect relative + // to the base frame. + AutoTArray<nsRect, 16> charRects; + + // If the first frame is a text frame, the result should be computed with + // the frame's API. + if (firstFrame->IsTextFrame()) { + rv = firstFrame->GetCharacterRectsInRange(firstFrame.mOffsetInNode, + kEndOffset - offset, charRects); + if (NS_WARN_IF(NS_FAILED(rv)) || NS_WARN_IF(charRects.IsEmpty())) { + return rv; + } + // Assign the characters whose rects are computed by the call of + // nsTextFrame::GetCharacterRectsInRange(). + AppendSubString(chars, *firstContent->AsText(), firstFrame.mOffsetInNode, + charRects.Length()); + if (NS_WARN_IF(chars.Length() != charRects.Length())) { + return NS_ERROR_UNEXPECTED; + } + if (kBRLength > 1 && chars[0] == '\n' && + offset == aEvent->mInput.mOffset && offset) { + // If start of range starting from previous offset of query range is + // same as the start of query range, the query range starts from + // between a line breaker (i.e., the range starts between "\r" and + // "\n"). + RawRange rawRangeToPrevOffset; + nsresult rv = SetRawRangeFromFlatTextOffset(&rawRangeToPrevOffset, + aEvent->mInput.mOffset - 1, + 1, lineBreakType, true); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + startsBetweenLineBreaker = + rawRange.GetStartContainer() == + rawRangeToPrevOffset.GetStartContainer() && + rawRange.StartOffset() == rawRangeToPrevOffset.StartOffset(); + } + } + // Other contents should cause a line breaker rect before it. + // Note that moz-<br> element does not cause any text, however, + // it represents empty line at the last of current block. Therefore, + // we need to compute its rect too. + else if (ShouldBreakLineBefore(*firstContent, mRootElement) || + IsPaddingBR(*firstContent)) { + nsRect brRect; + // If the frame is not a <br> frame, we need to compute the caret rect + // with last character's rect before firstContent if there is. + // For example, if caret is after "c" of |<p>abc</p><p>def</p>|, IME may + // query a line breaker's rect after "c". Then, if we compute it only + // with the 2nd <p>'s block frame, the result will be: + // +-<p>--------------------------------+ + // |abc | + // +------------------------------------+ + // + // I+-<p>--------------------------------+ + // |def | + // +------------------------------------+ + // However, users expect popup windows of IME should be positioned at + // right-bottom of "c" like this: + // +-<p>--------------------------------+ + // |abcI | + // +------------------------------------+ + // + // +-<p>--------------------------------+ + // |def | + // +------------------------------------+ + // Therefore, if the first frame isn't a <br> frame and there is a text + // node before the first node in the queried range, we should compute the + // first rect with the previous character's rect. + // If we already compute a character's rect in the queried range, we can + // compute it with the cached last character's rect. (However, don't + // use this path if it's a <br> frame because trusting <br> frame's rect + // is better than guessing the rect from the previous character.) + if (!firstFrame->IsBrFrame() && !aEvent->mReply->mRectArray.IsEmpty()) { + baseFrame = lastFrame; + brRect = lastCharRect; + if (!wasLineBreaker) { + brRect = GetCaretRectAfter(*baseFrame->PresContext(), brRect, + lastVisibleFrameWritingMode); + } + } + // If it's not a <br> frame and it's the first character rect at the + // queried range, we need the previous character rect of the start of + // the queried range if there is a visible text node. + else if (!firstFrame->IsBrFrame() && lastTextNode && + lastTextNode->GetPrimaryFrame()) { + FrameRelativeRect brRectRelativeToLastTextFrame = + GuessLineBreakerRectAfter(*lastTextNode); + if (NS_WARN_IF(!brRectRelativeToLastTextFrame.IsValid())) { + return NS_ERROR_FAILURE; + } + // Look for the last text frame for lastTextNode. + nsIFrame* primaryFrame = lastTextNode->GetPrimaryFrame(); + if (NS_WARN_IF(!primaryFrame)) { + return NS_ERROR_FAILURE; + } + baseFrame = primaryFrame->LastContinuation(); + if (NS_WARN_IF(!baseFrame)) { + return NS_ERROR_FAILURE; + } + brRect = brRectRelativeToLastTextFrame.RectRelativeTo(baseFrame); + } + // Otherwise, we need to compute the line breaker's rect only with the + // first frame's rect. But this may be unexpected. For example, + // |<div contenteditable>[<p>]abc</p></div>|. In this case, caret is + // before "a", therefore, users expect the rect left of "a". However, + // we don't have enough information about the next character here and + // this isn't usual case (e.g., IME typically tries to query the rect + // of "a" or caret rect for computing its popup position). Therefore, + // we shouldn't do more complicated hack here unless we'll get some bug + // reports actually. + else { + FrameRelativeRect relativeBRRect = GetLineBreakerRectBefore(firstFrame); + brRect = relativeBRRect.RectRelativeTo(firstFrame); + } + charRects.AppendElement(brRect); + chars.AssignLiteral("\n"); + if (kBRLength > 1 && offset == aEvent->mInput.mOffset && offset) { + // If the first frame for the previous offset of the query range and + // the first frame for the start of query range are same, that means + // the start offset is between the first line breaker (i.e., the range + // starts between "\r" and "\n"). + nsresult rv = SetRawRangeFromFlatTextOffset( + &rawRange, aEvent->mInput.mOffset - 1, 1, lineBreakType, true); + if (NS_WARN_IF(NS_FAILED(rv))) { + return NS_ERROR_UNEXPECTED; + } + FrameAndNodeOffset frameForPrevious = + GetFirstFrameInRangeForTextRect(rawRange); + startsBetweenLineBreaker = frameForPrevious.mFrame == firstFrame.mFrame; + } + } else { + NS_WARNING( + "The frame is neither a text frame nor a frame whose content " + "causes a line break"); + return NS_ERROR_FAILURE; + } + + for (size_t i = 0; i < charRects.Length() && offset < kEndOffset; i++) { + nsRect charRect = charRects[i]; + // Store lastCharRect before applying CSS transform because it may be + // used for computing a line breaker rect. Then, the computed line + // breaker rect will be applied CSS transform again. Therefore, + // the value of lastCharRect should be raw rect value relative to the + // base frame. + lastCharRect = charRect; + lastFrame = baseFrame; + rv = ConvertToRootRelativeOffset(baseFrame, charRect); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsPresContext* presContext = baseFrame->PresContext(); + rect = LayoutDeviceIntRect::FromAppUnitsToOutside( + charRect, presContext->AppUnitsPerDevPixel()); + if (nsPresContext* rootContext = + presContext->GetInProcessRootContentDocumentPresContext()) { + rect = RoundedOut(ViewportUtils::DocumentRelativeLayoutToVisual( + rect, rootContext->PresShell())); + } + // Returning empty rect may cause native IME confused, let's make sure to + // return non-empty rect. + EnsureNonEmptyRect(rect); + + // If we found some invisible characters followed by current visible + // character, make their rects same as caret rect before the first visible + // character because IME may want to put their UI next to the rect of the + // invisible character for next input. + // Note that chars do not contain the invisible characters. + if (i == 0u && MOZ_LIKELY(offset > aEvent->mInput.mOffset)) { + const uint32_t offsetInRange = + offset - CheckedInt<uint32_t>(aEvent->mInput.mOffset).value(); + if (offsetInRange > aEvent->mReply->mRectArray.Length()) { + LayoutDeviceIntRect caretRectBefore = + GetCaretRectBefore(rect, lastVisibleFrameWritingMode); + for ([[maybe_unused]] uint32_t index : IntegerRange<uint32_t>( + offsetInRange - aEvent->mReply->mRectArray.Length())) { + aEvent->mReply->mRectArray.AppendElement(caretRectBefore); + } + MOZ_ASSERT(aEvent->mReply->mRectArray.Length() == offsetInRange); + } + } + + aEvent->mReply->mRectArray.AppendElement(rect); + offset++; + + // If it's not a line breaker or the line breaker length is same as + // XP line breaker's, we need to do nothing for current character. + wasLineBreaker = chars[i] == '\n'; + if (!wasLineBreaker || kBRLength == 1) { + continue; + } + + MOZ_ASSERT(kBRLength == 2); + + // If it's already reached the end of query range, we don't need to do + // anymore. + if (offset == kEndOffset) { + break; + } + + // If the query range starts from between a line breaker, i.e., it starts + // between "\r" and "\n", the appended rect was for the "\n". Therefore, + // we don't need to append same rect anymore for current "\r\n". + if (startsBetweenLineBreaker) { + continue; + } + + // The appended rect was for "\r" of "\r\n". Therefore, we need to + // append same rect for "\n" too because querying rect of "\r" and "\n" + // should return same rect. E.g., IME may query previous character's + // rect of first character of a line. + aEvent->mReply->mRectArray.AppendElement(rect); + offset++; + } + } + + // If we've not handled some invisible character rects, fill them as caret + // rect after the last visible character. + if (!aEvent->mReply->mRectArray.IsEmpty()) { + const uint32_t offsetInRange = + offset - CheckedInt<uint32_t>(aEvent->mInput.mOffset).value(); + if (offsetInRange > aEvent->mReply->mRectArray.Length()) { + LayoutDeviceIntRect caretRectAfter = + GetCaretRectAfter(aEvent->mReply->mRectArray.LastElement(), + lastVisibleFrameWritingMode); + for ([[maybe_unused]] uint32_t index : IntegerRange<uint32_t>( + offsetInRange - aEvent->mReply->mRectArray.Length())) { + aEvent->mReply->mRectArray.AppendElement(caretRectAfter); + } + MOZ_ASSERT(aEvent->mReply->mRectArray.Length() == offsetInRange); + } + } + + // If the query range is longer than actual content length, we should append + // caret rect at the end of the content as the last character rect because + // native IME may want to query character rect at the end of contents for + // deciding the position of a popup window (e.g., suggest window for next + // word). Note that when this method hasn't appended character rects, it + // means that the offset is too large or the query range is collapsed. + if (offset < kEndOffset || aEvent->mReply->mRectArray.IsEmpty()) { + // If we've already retrieved some character rects before current offset, + // we can guess the last rect from the last character's rect unless it's a + // line breaker. (If it's a line breaker, the caret rect is in next line.) + if (!aEvent->mReply->mRectArray.IsEmpty() && !wasLineBreaker) { + rect = GetCaretRectAfter(aEvent->mReply->mRectArray.LastElement(), + lastVisibleFrameWritingMode); + aEvent->mReply->mRectArray.AppendElement(rect); + } else { + // Note that don't use eQueryCaretRect here because if caret is at the + // end of the content, it returns actual caret rect instead of computing + // the rect itself. It means that the result depends on caret position. + // So, we shouldn't use it for consistency result in automated tests. + WidgetQueryContentEvent queryTextRectEvent(eQueryTextRect, *aEvent); + WidgetQueryContentEvent::Options options(*aEvent); + queryTextRectEvent.InitForQueryTextRect(offset, 1, options); + if (NS_WARN_IF(NS_FAILED(OnQueryTextRect(&queryTextRectEvent))) || + NS_WARN_IF(queryTextRectEvent.Failed())) { + return NS_ERROR_FAILURE; + } + if (queryTextRectEvent.mReply->mWritingMode.IsVertical()) { + queryTextRectEvent.mReply->mRect.height = 1; + } else { + queryTextRectEvent.mReply->mRect.width = 1; + } + aEvent->mReply->mRectArray.AppendElement( + queryTextRectEvent.mReply->mRect); + } + } + + MOZ_ASSERT(aEvent->Succeeded()); + return NS_OK; +} + +nsresult ContentEventHandler::OnQueryTextRect(WidgetQueryContentEvent* aEvent) { + // If mLength is 0 (this may be caused by bug of native IME), we should + // redirect this event to OnQueryCaretRect(). + if (!aEvent->mInput.mLength) { + return OnQueryCaretRect(aEvent); + } + + nsresult rv = Init(aEvent); + if (NS_FAILED(rv)) { + return rv; + } + + MOZ_ASSERT(aEvent->mReply->mOffsetAndData.isNothing()); + + LineBreakType lineBreakType = GetLineBreakType(aEvent); + RawRange rawRange; + RefPtr<Text> lastTextNode; + uint32_t startOffset = 0; + if (NS_WARN_IF(NS_FAILED(SetRawRangeFromFlatTextOffset( + &rawRange, aEvent->mInput.mOffset, aEvent->mInput.mLength, + lineBreakType, true, &startOffset, getter_AddRefs(lastTextNode))))) { + return NS_ERROR_FAILURE; + } + nsString string; + if (NS_WARN_IF(NS_FAILED( + GenerateFlatTextContent(rawRange, string, lineBreakType)))) { + return NS_ERROR_FAILURE; + } + aEvent->mReply->mOffsetAndData.emplace(startOffset, string, + OffsetAndDataFor::EditorString); + + // used to iterate over all contents and their frames + PostContentIterator postOrderIter; + rv = postOrderIter.Init(rawRange.Start().AsRaw(), rawRange.End().AsRaw()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return NS_ERROR_FAILURE; + } + + // Get the first frame which causes some text after the offset. + FrameAndNodeOffset firstFrame = GetFirstFrameInRangeForTextRect(rawRange); + + // If GetFirstFrameInRangeForTextRect() does not return valid frame, that + // means that there are no visible frames having text or the offset reached + // the end of contents. + if (!firstFrame.IsValid()) { + nsAutoString allText; + rv = GenerateFlatTextContent(mRootElement, allText, lineBreakType); + // If the offset doesn't reach the end of contents but there is no frames + // for the node, that means that current offset's node is hidden by CSS or + // something. Ideally, we should handle it with the last visible text + // node's last character's rect, but it's not usual cases in actual web + // services. Therefore, currently, we should make this case fail. + if (NS_WARN_IF(NS_FAILED(rv)) || + static_cast<uint32_t>(aEvent->mInput.mOffset) < allText.Length()) { + return NS_ERROR_FAILURE; + } + + // Look for the last frame which should be included text rects. + rv = rawRange.SelectNodeContents(mRootElement); + if (NS_WARN_IF(NS_FAILED(rv))) { + return NS_ERROR_UNEXPECTED; + } + nsRect rect; + FrameAndNodeOffset lastFrame = GetLastFrameInRangeForTextRect(rawRange); + // If there is at least one frame which can be used for computing a rect + // for a character or a line breaker, we should use it for guessing the + // caret rect at the end of the contents. + nsPresContext* presContext; + if (lastFrame) { + presContext = lastFrame->PresContext(); + if (NS_WARN_IF(!lastFrame->GetContent())) { + return NS_ERROR_FAILURE; + } + FrameRelativeRect relativeRect; + // If there is a <br> frame at the end, it represents an empty line at + // the end with moz-<br> or content <br> in a block level element. + if (lastFrame->IsBrFrame()) { + relativeRect = GetLineBreakerRectBefore(lastFrame); + } + // If there is a text frame at the end, use its information. + else if (lastFrame->IsTextFrame()) { + const Text* textNode = Text::FromNode(lastFrame->GetContent()); + MOZ_ASSERT(textNode); + if (textNode) { + relativeRect = GuessLineBreakerRectAfter(*textNode); + } + } + // If there is an empty frame which is neither a text frame nor a <br> + // frame at the end, guess caret rect in it. + else { + relativeRect = GuessFirstCaretRectIn(lastFrame); + } + if (NS_WARN_IF(!relativeRect.IsValid())) { + return NS_ERROR_FAILURE; + } + rect = relativeRect.RectRelativeTo(lastFrame); + rv = ConvertToRootRelativeOffset(lastFrame, rect); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + aEvent->mReply->mWritingMode = lastFrame->GetWritingMode(); + } + // Otherwise, if there are no contents in mRootElement, guess caret rect in + // its frame (with its font height and content box). + else { + nsIFrame* rootContentFrame = mRootElement->GetPrimaryFrame(); + if (NS_WARN_IF(!rootContentFrame)) { + return NS_ERROR_FAILURE; + } + presContext = rootContentFrame->PresContext(); + FrameRelativeRect relativeRect = GuessFirstCaretRectIn(rootContentFrame); + if (NS_WARN_IF(!relativeRect.IsValid())) { + return NS_ERROR_FAILURE; + } + rect = relativeRect.RectRelativeTo(rootContentFrame); + rv = ConvertToRootRelativeOffset(rootContentFrame, rect); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + aEvent->mReply->mWritingMode = rootContentFrame->GetWritingMode(); + } + aEvent->mReply->mRect = LayoutDeviceIntRect::FromAppUnitsToOutside( + rect, presContext->AppUnitsPerDevPixel()); + if (nsPresContext* rootContext = + presContext->GetInProcessRootContentDocumentPresContext()) { + aEvent->mReply->mRect = + RoundedOut(ViewportUtils::DocumentRelativeLayoutToVisual( + aEvent->mReply->mRect, rootContext->PresShell())); + } + EnsureNonEmptyRect(aEvent->mReply->mRect); + + MOZ_ASSERT(aEvent->Succeeded()); + return NS_OK; + } + + nsRect rect, frameRect; + nsPoint ptOffset; + + // If the first frame is a text frame, the result should be computed with + // the frame's rect but not including the rect before start point of the + // queried range. + if (firstFrame->IsTextFrame()) { + rect.SetRect(nsPoint(0, 0), firstFrame->GetRect().Size()); + rv = ConvertToRootRelativeOffset(firstFrame, rect); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + frameRect = rect; + // Exclude the rect before start point of the queried range. + firstFrame->GetPointFromOffset(firstFrame.mOffsetInNode, &ptOffset); + if (firstFrame->GetWritingMode().IsVertical()) { + rect.y += ptOffset.y; + rect.height -= ptOffset.y; + } else { + rect.x += ptOffset.x; + rect.width -= ptOffset.x; + } + } + // If first frame causes a line breaker but it's not a <br> frame, we cannot + // compute proper rect only with the frame because typically caret is at + // right of the last character of it. For example, if caret is after "c" of + // |<p>abc</p><p>def</p>|, IME may query a line breaker's rect after "c". + // Then, if we compute it only with the 2nd <p>'s block frame, the result + // will be: + // +-<p>--------------------------------+ + // |abc | + // +------------------------------------+ + // + // I+-<p>--------------------------------+ + // |def | + // +------------------------------------+ + // However, users expect popup windows of IME should be positioned at + // right-bottom of "c" like this: + // +-<p>--------------------------------+ + // |abcI | + // +------------------------------------+ + // + // +-<p>--------------------------------+ + // |def | + // +------------------------------------+ + // Therefore, if the first frame isn't a <br> frame and there is a visible + // text node before the first node in the queried range, we should compute the + // first rect with the previous character's rect. + else if (!firstFrame->IsBrFrame() && lastTextNode && + lastTextNode->GetPrimaryFrame()) { + FrameRelativeRect brRectAfterLastChar = + GuessLineBreakerRectAfter(*lastTextNode); + if (NS_WARN_IF(!brRectAfterLastChar.IsValid())) { + return NS_ERROR_FAILURE; + } + rect = brRectAfterLastChar.mRect; + rv = ConvertToRootRelativeOffset(brRectAfterLastChar.mBaseFrame, rect); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + frameRect = rect; + } + // Otherwise, we need to compute the line breaker's rect only with the + // first frame's rect. But this may be unexpected. For example, + // |<div contenteditable>[<p>]abc</p></div>|. In this case, caret is before + // "a", therefore, users expect the rect left of "a". However, we don't + // have enough information about the next character here and this isn't + // usual case (e.g., IME typically tries to query the rect of "a" or caret + // rect for computing its popup position). Therefore, we shouldn't do + // more complicated hack here unless we'll get some bug reports actually. + else { + FrameRelativeRect relativeRect = GetLineBreakerRectBefore(firstFrame); + if (NS_WARN_IF(!relativeRect.IsValid())) { + return NS_ERROR_FAILURE; + } + rect = relativeRect.RectRelativeTo(firstFrame); + rv = ConvertToRootRelativeOffset(firstFrame, rect); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + frameRect = rect; + } + // UnionRect() requires non-empty rect. So, let's make sure to get non-emtpy + // rect from the first frame. + EnsureNonEmptyRect(rect); + + // Get the last frame which causes some text in the range. + FrameAndNodeOffset lastFrame = GetLastFrameInRangeForTextRect(rawRange); + if (NS_WARN_IF(!lastFrame.IsValid())) { + return NS_ERROR_FAILURE; + } + + // iterate over all covered frames + for (nsIFrame* frame = firstFrame; frame != lastFrame;) { + frame = frame->GetNextContinuation(); + if (!frame) { + do { + postOrderIter.Next(); + nsINode* node = postOrderIter.GetCurrentNode(); + if (!node) { + break; + } + if (!node->IsContent()) { + continue; + } + nsIFrame* primaryFrame = node->AsContent()->GetPrimaryFrame(); + // The node may be hidden by CSS. + if (!primaryFrame) { + continue; + } + // We should take only text frame's rect and br frame's rect. We can + // always use frame rect of text frame and GetLineBreakerRectBefore() + // can return exactly correct rect only for <br> frame for now. On the + // other hand, GetLineBreakRectBefore() returns guessed caret rect for + // the other frames. We shouldn't include such odd rect to the result. + if (primaryFrame->IsTextFrame() || primaryFrame->IsBrFrame()) { + frame = primaryFrame; + } + } while (!frame && !postOrderIter.IsDone()); + if (!frame) { + break; + } + } + if (frame->IsTextFrame()) { + frameRect.SetRect(nsPoint(0, 0), frame->GetRect().Size()); + } else { + MOZ_ASSERT(frame->IsBrFrame()); + FrameRelativeRect relativeRect = GetLineBreakerRectBefore(frame); + if (NS_WARN_IF(!relativeRect.IsValid())) { + return NS_ERROR_FAILURE; + } + frameRect = relativeRect.RectRelativeTo(frame); + } + rv = ConvertToRootRelativeOffset(frame, frameRect); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + // UnionRect() requires non-empty rect. So, let's make sure to get + // non-emtpy rect from the frame. + EnsureNonEmptyRect(frameRect); + if (frame != lastFrame) { + // not last frame, so just add rect to previous result + rect.UnionRect(rect, frameRect); + } + } + + // Get the ending frame rect. + // FYI: If first frame and last frame are same, frameRect is already set + // to the rect excluding the text before the query range. + if (firstFrame.mFrame != lastFrame.mFrame) { + frameRect.SetRect(nsPoint(0, 0), lastFrame->GetRect().Size()); + rv = ConvertToRootRelativeOffset(lastFrame, frameRect); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + // Shrink the last frame for cutting off the text after the query range. + if (lastFrame->IsTextFrame()) { + lastFrame->GetPointFromOffset(lastFrame.mOffsetInNode, &ptOffset); + if (lastFrame->GetWritingMode().IsVertical()) { + frameRect.height -= lastFrame->GetRect().height - ptOffset.y; + } else { + frameRect.width -= lastFrame->GetRect().width - ptOffset.x; + } + // UnionRect() requires non-empty rect. So, let's make sure to get + // non-empty rect from the last frame. + EnsureNonEmptyRect(frameRect); + + if (firstFrame.mFrame == lastFrame.mFrame) { + rect.IntersectRect(rect, frameRect); + } else { + rect.UnionRect(rect, frameRect); + } + } + + nsPresContext* presContext = lastFrame->PresContext(); + aEvent->mReply->mRect = LayoutDeviceIntRect::FromAppUnitsToOutside( + rect, presContext->AppUnitsPerDevPixel()); + if (nsPresContext* rootContext = + presContext->GetInProcessRootContentDocumentPresContext()) { + aEvent->mReply->mRect = + RoundedOut(ViewportUtils::DocumentRelativeLayoutToVisual( + aEvent->mReply->mRect, rootContext->PresShell())); + } + // Returning empty rect may cause native IME confused, let's make sure to + // return non-empty rect. + EnsureNonEmptyRect(aEvent->mReply->mRect); + aEvent->mReply->mWritingMode = lastFrame->GetWritingMode(); + + MOZ_ASSERT(aEvent->Succeeded()); + return NS_OK; +} + +nsresult ContentEventHandler::OnQueryEditorRect( + WidgetQueryContentEvent* aEvent) { + nsresult rv = Init(aEvent); + if (NS_FAILED(rv)) { + return rv; + } + + if (NS_WARN_IF(NS_FAILED(QueryContentRect(mRootElement, aEvent)))) { + return NS_ERROR_FAILURE; + } + + MOZ_ASSERT(aEvent->Succeeded()); + return NS_OK; +} + +nsresult ContentEventHandler::OnQueryCaretRect( + WidgetQueryContentEvent* aEvent) { + nsresult rv = Init(aEvent); + if (NS_FAILED(rv)) { + return rv; + } + + // When the selection is collapsed and the queried offset is current caret + // position, we should return the "real" caret rect. + if (mSelection->IsCollapsed()) { + nsRect caretRect; + nsIFrame* caretFrame = nsCaret::GetGeometry(mSelection, &caretRect); + if (caretFrame) { + uint32_t offset; + rv = GetStartOffset(mFirstSelectedRawRange, &offset, + GetLineBreakType(aEvent)); + NS_ENSURE_SUCCESS(rv, rv); + if (offset == aEvent->mInput.mOffset) { + rv = ConvertToRootRelativeOffset(caretFrame, caretRect); + NS_ENSURE_SUCCESS(rv, rv); + nsPresContext* presContext = caretFrame->PresContext(); + aEvent->mReply->mRect = LayoutDeviceIntRect::FromAppUnitsToOutside( + caretRect, presContext->AppUnitsPerDevPixel()); + if (nsPresContext* rootContext = + presContext->GetInProcessRootContentDocumentPresContext()) { + aEvent->mReply->mRect = + RoundedOut(ViewportUtils::DocumentRelativeLayoutToVisual( + aEvent->mReply->mRect, rootContext->PresShell())); + } + // Returning empty rect may cause native IME confused, let's make sure + // to return non-empty rect. + EnsureNonEmptyRect(aEvent->mReply->mRect); + aEvent->mReply->mWritingMode = caretFrame->GetWritingMode(); + aEvent->mReply->mOffsetAndData.emplace( + aEvent->mInput.mOffset, EmptyString(), + OffsetAndDataFor::SelectedString); + + MOZ_ASSERT(aEvent->Succeeded()); + return NS_OK; + } + } + } + + // Otherwise, we should guess the caret rect from the character's rect. + WidgetQueryContentEvent queryTextRectEvent(eQueryTextRect, *aEvent); + WidgetQueryContentEvent::Options options(*aEvent); + queryTextRectEvent.InitForQueryTextRect(aEvent->mInput.mOffset, 1, options); + if (NS_WARN_IF(NS_FAILED(OnQueryTextRect(&queryTextRectEvent))) || + NS_WARN_IF(queryTextRectEvent.Failed())) { + return NS_ERROR_FAILURE; + } + queryTextRectEvent.mReply->TruncateData(); + aEvent->mReply->mOffsetAndData = + std::move(queryTextRectEvent.mReply->mOffsetAndData); + aEvent->mReply->mWritingMode = + std::move(queryTextRectEvent.mReply->mWritingMode); + aEvent->mReply->mRect = GetCaretRectBefore(queryTextRectEvent.mReply->mRect, + aEvent->mReply->mWritingMode); + + MOZ_ASSERT(aEvent->Succeeded()); + return NS_OK; +} + +nsresult ContentEventHandler::OnQueryContentState( + WidgetQueryContentEvent* aEvent) { + if (NS_FAILED(Init(aEvent))) { + return NS_ERROR_FAILURE; + } + MOZ_ASSERT(aEvent->mReply.isSome()); + MOZ_ASSERT(aEvent->Succeeded()); + return NS_OK; +} + +nsresult ContentEventHandler::OnQuerySelectionAsTransferable( + WidgetQueryContentEvent* aEvent) { + nsresult rv = Init(aEvent); + if (NS_FAILED(rv)) { + return rv; + } + + MOZ_ASSERT(aEvent->mReply.isSome()); + + if (mSelection->IsCollapsed()) { + MOZ_ASSERT(!aEvent->mReply->mTransferable); + return NS_OK; + } + + if (NS_WARN_IF(NS_FAILED(nsCopySupport::GetTransferableForSelection( + mSelection, mDocument, + getter_AddRefs(aEvent->mReply->mTransferable))))) { + return NS_ERROR_FAILURE; + } + + MOZ_ASSERT(aEvent->Succeeded()); + return NS_OK; +} + +nsresult ContentEventHandler::OnQueryCharacterAtPoint( + WidgetQueryContentEvent* aEvent) { + nsresult rv = Init(aEvent); + if (NS_FAILED(rv)) { + return rv; + } + + MOZ_ASSERT(aEvent->mReply->mOffsetAndData.isNothing()); + MOZ_ASSERT(aEvent->mReply->mTentativeCaretOffset.isNothing()); + + PresShell* presShell = mDocument->GetPresShell(); + NS_ENSURE_TRUE(presShell, NS_ERROR_FAILURE); + nsIFrame* rootFrame = presShell->GetRootFrame(); + NS_ENSURE_TRUE(rootFrame, NS_ERROR_FAILURE); + nsIWidget* rootWidget = rootFrame->GetNearestWidget(); + NS_ENSURE_TRUE(rootWidget, NS_ERROR_FAILURE); + + // The root frame's widget might be different, e.g., the event was fired on + // a popup but the rootFrame is the document root. + if (rootWidget != aEvent->mWidget) { + MOZ_ASSERT(aEvent->mWidget, "The event must have the widget"); + nsView* view = nsView::GetViewFor(aEvent->mWidget); + NS_ENSURE_TRUE(view, NS_ERROR_FAILURE); + rootFrame = view->GetFrame(); + NS_ENSURE_TRUE(rootFrame, NS_ERROR_FAILURE); + rootWidget = rootFrame->GetNearestWidget(); + NS_ENSURE_TRUE(rootWidget, NS_ERROR_FAILURE); + } + + WidgetQueryContentEvent queryCharAtPointOnRootWidgetEvent( + true, eQueryCharacterAtPoint, rootWidget); + queryCharAtPointOnRootWidgetEvent.mUseNativeLineBreak = + aEvent->mUseNativeLineBreak; + queryCharAtPointOnRootWidgetEvent.mRefPoint = aEvent->mRefPoint; + if (rootWidget != aEvent->mWidget) { + queryCharAtPointOnRootWidgetEvent.mRefPoint += + aEvent->mWidget->WidgetToScreenOffset() - + rootWidget->WidgetToScreenOffset(); + } + nsPoint ptInRoot = nsLayoutUtils::GetEventCoordinatesRelativeTo( + &queryCharAtPointOnRootWidgetEvent, RelativeTo{rootFrame}); + + nsIFrame* targetFrame = + nsLayoutUtils::GetFrameForPoint(RelativeTo{rootFrame}, ptInRoot); + if (!targetFrame || !targetFrame->GetContent() || + !targetFrame->GetContent()->IsInclusiveDescendantOf(mRootElement)) { + // There is no character at the point. + MOZ_ASSERT(aEvent->Succeeded()); + return NS_OK; + } + nsPoint ptInTarget = ptInRoot + rootFrame->GetOffsetToCrossDoc(targetFrame); + int32_t rootAPD = rootFrame->PresContext()->AppUnitsPerDevPixel(); + int32_t targetAPD = targetFrame->PresContext()->AppUnitsPerDevPixel(); + ptInTarget = ptInTarget.ScaleToOtherAppUnits(rootAPD, targetAPD); + + nsIFrame::ContentOffsets tentativeCaretOffsets = + targetFrame->GetContentOffsetsFromPoint(ptInTarget); + if (!tentativeCaretOffsets.content || + !tentativeCaretOffsets.content->IsInclusiveDescendantOf(mRootElement)) { + // There is no character nor tentative caret point at the point. + MOZ_ASSERT(aEvent->Succeeded()); + return NS_OK; + } + + uint32_t tentativeCaretOffset = 0; + if (NS_WARN_IF(NS_FAILED(GetFlatTextLengthInRange( + NodePosition(mRootElement, 0u), NodePosition(tentativeCaretOffsets), + mRootElement, &tentativeCaretOffset, GetLineBreakType(aEvent))))) { + return NS_ERROR_FAILURE; + } + + aEvent->mReply->mTentativeCaretOffset.emplace(tentativeCaretOffset); + if (!targetFrame->IsTextFrame()) { + // There is no character at the point but there is tentative caret point. + MOZ_ASSERT(aEvent->Succeeded()); + return NS_OK; + } + + nsTextFrame* textframe = static_cast<nsTextFrame*>(targetFrame); + nsIFrame::ContentOffsets contentOffsets = + textframe->GetCharacterOffsetAtFramePoint(ptInTarget); + NS_ENSURE_TRUE(contentOffsets.content, NS_ERROR_FAILURE); + uint32_t offset = 0; + if (NS_WARN_IF(NS_FAILED(GetFlatTextLengthInRange( + NodePosition(mRootElement, 0u), NodePosition(contentOffsets), + mRootElement, &offset, GetLineBreakType(aEvent))))) { + return NS_ERROR_FAILURE; + } + + WidgetQueryContentEvent queryTextRectEvent(true, eQueryTextRect, + aEvent->mWidget); + WidgetQueryContentEvent::Options options(*aEvent); + queryTextRectEvent.InitForQueryTextRect(offset, 1, options); + if (NS_WARN_IF(NS_FAILED(OnQueryTextRect(&queryTextRectEvent))) || + NS_WARN_IF(queryTextRectEvent.Failed())) { + return NS_ERROR_FAILURE; + } + + aEvent->mReply->mOffsetAndData = + std::move(queryTextRectEvent.mReply->mOffsetAndData); + aEvent->mReply->mRect = queryTextRectEvent.mReply->mRect; + + MOZ_ASSERT(aEvent->Succeeded()); + return NS_OK; +} + +nsresult ContentEventHandler::OnQueryDOMWidgetHittest( + WidgetQueryContentEvent* aEvent) { + NS_ASSERTION(aEvent, "aEvent must not be null"); + + nsresult rv = InitBasic(); + if (NS_FAILED(rv)) { + return rv; + } + + aEvent->mReply->mWidgetIsHit = false; + + NS_ENSURE_TRUE(aEvent->mWidget, NS_ERROR_FAILURE); + + PresShell* presShell = mDocument->GetPresShell(); + NS_ENSURE_TRUE(presShell, NS_ERROR_FAILURE); + nsIFrame* docFrame = presShell->GetRootFrame(); + NS_ENSURE_TRUE(docFrame, NS_ERROR_FAILURE); + + LayoutDeviceIntPoint eventLoc = + aEvent->mRefPoint + aEvent->mWidget->WidgetToScreenOffset(); + CSSIntRect docFrameRect = docFrame->GetScreenRect(); + CSSIntPoint eventLocCSS( + docFrame->PresContext()->DevPixelsToIntCSSPixels(eventLoc.x) - + docFrameRect.x, + docFrame->PresContext()->DevPixelsToIntCSSPixels(eventLoc.y) - + docFrameRect.y); + + if (Element* contentUnderMouse = mDocument->ElementFromPointHelper( + eventLocCSS.x, eventLocCSS.y, false, false, ViewportType::Visual)) { + if (nsIFrame* targetFrame = contentUnderMouse->GetPrimaryFrame()) { + if (aEvent->mWidget == targetFrame->GetNearestWidget()) { + aEvent->mReply->mWidgetIsHit = true; + } + } + } + + MOZ_ASSERT(aEvent->Succeeded()); + return NS_OK; +} + +/* static */ +nsresult ContentEventHandler::GetFlatTextLengthInRange( + const NodePosition& aStartPosition, const NodePosition& aEndPosition, + const Element* aRootElement, uint32_t* aLength, + LineBreakType aLineBreakType, bool aIsRemovingNode /* = false */) { + if (NS_WARN_IF(!aRootElement) || NS_WARN_IF(!aStartPosition.IsSet()) || + NS_WARN_IF(!aEndPosition.IsSet()) || NS_WARN_IF(!aLength)) { + return NS_ERROR_INVALID_ARG; + } + + if (aStartPosition == aEndPosition) { + *aLength = 0; + return NS_OK; + } + + PreContentIterator preOrderIter; + + // Working with ContentIterator, we may need to adjust the end position for + // including it forcibly. + NodePosition endPosition(aEndPosition); + + // This may be called for retrieving the text of removed nodes. Even in this + // case, the node thinks it's still in the tree because UnbindFromTree() will + // be called after here. However, the node was already removed from the + // array of children of its parent. So, be careful to handle this case. + if (aIsRemovingNode) { + DebugOnly<nsIContent*> parent = aStartPosition.Container()->GetParent(); + MOZ_ASSERT( + parent && + parent->ComputeIndexOf(aStartPosition.Container()).isNothing(), + "At removing the node, the node shouldn't be in the array of children " + "of its parent"); + MOZ_ASSERT(aStartPosition.Container() == endPosition.Container(), + "At removing the node, start and end node should be same"); + MOZ_ASSERT(*aStartPosition.Offset( + NodePosition::OffsetFilter::kValidOrInvalidOffsets) == 0, + "When the node is being removed, the start offset should be 0"); + MOZ_ASSERT( + static_cast<uint32_t>(*endPosition.Offset( + NodePosition::OffsetFilter::kValidOrInvalidOffsets)) == + endPosition.Container()->GetChildCount(), + "When the node is being removed, the end offset should be child count"); + nsresult rv = preOrderIter.Init(aStartPosition.Container()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } else { + RawRange prevRawRange; + nsresult rv = prevRawRange.SetStart(aStartPosition.AsRaw()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // When the end position is immediately after non-root element's open tag, + // we need to include a line break caused by the open tag. + if (endPosition.Container() != aRootElement && + endPosition.IsImmediatelyAfterOpenTag()) { + if (endPosition.Container()->HasChildren()) { + // When the end node has some children, move the end position to before + // the open tag of its first child. + nsINode* firstChild = endPosition.Container()->GetFirstChild(); + if (NS_WARN_IF(!firstChild)) { + return NS_ERROR_FAILURE; + } + endPosition = NodePositionBefore(firstChild, 0u); + } else { + // When the end node is empty, move the end position after the node. + nsIContent* parentContent = endPosition.Container()->GetParent(); + if (NS_WARN_IF(!parentContent)) { + return NS_ERROR_FAILURE; + } + Maybe<uint32_t> indexInParent = + parentContent->ComputeIndexOf(endPosition.Container()); + if (MOZ_UNLIKELY(NS_WARN_IF(indexInParent.isNothing()))) { + return NS_ERROR_FAILURE; + } + MOZ_ASSERT(*indexInParent != UINT32_MAX); + endPosition = NodePositionBefore(parentContent, *indexInParent + 1u); + } + } + + if (endPosition.IsSetAndValid()) { + // Offset is within node's length; set end of range to that offset + rv = prevRawRange.SetEnd(endPosition.AsRaw()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + rv = preOrderIter.Init(prevRawRange.Start().AsRaw(), + prevRawRange.End().AsRaw()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } else if (endPosition.Container() != aRootElement) { + // Offset is past node's length; set end of range to end of node + rv = prevRawRange.SetEndAfter(endPosition.Container()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + rv = preOrderIter.Init(prevRawRange.Start().AsRaw(), + prevRawRange.End().AsRaw()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } else { + // Offset is past the root node; set end of range to end of root node + rv = preOrderIter.Init(const_cast<Element*>(aRootElement)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + } + + *aLength = 0; + for (; !preOrderIter.IsDone(); preOrderIter.Next()) { + nsINode* node = preOrderIter.GetCurrentNode(); + if (NS_WARN_IF(!node)) { + break; + } + if (!node->IsContent()) { + continue; + } + nsIContent* content = node->AsContent(); + + if (const Text* textNode = Text::FromNode(content)) { + // Note: our range always starts from offset 0 + if (node == endPosition.Container()) { + // NOTE: We should have an offset here, as endPosition.Container() is a + // nsINode::eTEXT, which always has an offset. + *aLength += GetTextLength( + *textNode, aLineBreakType, + *endPosition.Offset( + NodePosition::OffsetFilter::kValidOrInvalidOffsets)); + } else { + *aLength += GetTextLength(*textNode, aLineBreakType); + } + } else if (ShouldBreakLineBefore(*content, aRootElement)) { + // If the start position is start of this node but doesn't include the + // open tag, don't append the line break length. + if (node == aStartPosition.Container() && + !aStartPosition.IsBeforeOpenTag()) { + continue; + } + // If the end position is before the open tag, don't append the line + // break length. + if (node == endPosition.Container() && endPosition.IsBeforeOpenTag()) { + continue; + } + *aLength += GetBRLength(aLineBreakType); + } + } + return NS_OK; +} + +nsresult ContentEventHandler::GetStartOffset(const RawRange& aRawRange, + uint32_t* aOffset, + LineBreakType aLineBreakType) { + // To match the "no skip start" hack in ContentIterator::Init, when range + // offset is 0 and the range node is not a container, we have to assume the + // range _includes_ the node, which means the start offset should _not_ + // include the node. + // + // For example, for this content: <br>abc, and range (<br>, 0)-("abc", 1), the + // range includes the linebreak from <br>, so the start offset should _not_ + // include <br>, and the start offset should be 0. + // + // However, for this content: <p/>abc, and range (<p>, 0)-("abc", 1), the + // range does _not_ include the linebreak from <p> because <p> is a container, + // so the start offset _should_ include <p>, and the start offset should be 1. + + nsINode* startNode = aRawRange.GetStartContainer(); + bool startIsContainer = true; + if (startNode->IsHTMLElement()) { + nsAtom* name = startNode->NodeInfo()->NameAtom(); + startIsContainer = + nsHTMLElement::IsContainer(nsHTMLTags::AtomTagToId(name)); + } + const NodePosition& startPos = + startIsContainer ? NodePosition(startNode, aRawRange.StartOffset()) + : NodePositionBefore(startNode, aRawRange.StartOffset()); + return GetFlatTextLengthInRange(NodePosition(mRootElement, 0u), startPos, + mRootElement, aOffset, aLineBreakType); +} + +nsresult ContentEventHandler::AdjustCollapsedRangeMaybeIntoTextNode( + RawRange& aRawRange) { + MOZ_ASSERT(aRawRange.Collapsed()); + + if (!aRawRange.Collapsed()) { + return NS_ERROR_INVALID_ARG; + } + + const RangeBoundary& startPoint = aRawRange.Start(); + if (NS_WARN_IF(!startPoint.IsSet())) { + return NS_ERROR_INVALID_ARG; + } + + // If the node does not have children like a text node, we don't need to + // modify aRawRange. + if (!startPoint.Container()->HasChildren()) { + return NS_OK; + } + + // If the container is not a text node but it has a text node at the offset, + // we should adjust the range into the text node. + // NOTE: This is emulating similar situation of EditorBase. + if (startPoint.IsStartOfContainer()) { + // If the range is the start of the container, adjusted the range to the + // start of the first child. + if (!startPoint.Container()->GetFirstChild()->IsText()) { + return NS_OK; + } + nsresult rv = aRawRange.CollapseTo( + RawRangeBoundary(startPoint.Container()->GetFirstChild(), 0u)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + return NS_OK; + } + + if (!startPoint.IsSetAndValid()) { + return NS_OK; + } + + // If start of the range is next to a child node, adjust the range to the + // end of the previous child (i.e., startPoint.Ref()). + if (!startPoint.Ref()->IsText()) { + return NS_OK; + } + nsresult rv = aRawRange.CollapseTo( + RawRangeBoundary(startPoint.Ref(), startPoint.Ref()->Length())); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + return NS_OK; +} + +nsresult ContentEventHandler::ConvertToRootRelativeOffset(nsIFrame* aFrame, + nsRect& aRect) { + NS_ASSERTION(aFrame, "aFrame must not be null"); + + nsPresContext* thisPC = aFrame->PresContext(); + nsPresContext* rootPC = thisPC->GetRootPresContext(); + if (NS_WARN_IF(!rootPC)) { + return NS_ERROR_FAILURE; + } + nsIFrame* rootFrame = rootPC->PresShell()->GetRootFrame(); + if (NS_WARN_IF(!rootFrame)) { + return NS_ERROR_FAILURE; + } + + aRect = nsLayoutUtils::TransformFrameRectToAncestor(aFrame, aRect, rootFrame); + + // TransformFrameRectToAncestor returned the rect in the ancestor's appUnits, + // but we want it in aFrame's units (in case of different full-zoom factors), + // so convert back. + aRect = aRect.ScaleToOtherAppUnitsRoundOut(rootPC->AppUnitsPerDevPixel(), + thisPC->AppUnitsPerDevPixel()); + + return NS_OK; +} + +static void AdjustRangeForSelection(const Element* aRootElement, + nsINode** aNode, + Maybe<uint32_t>* aNodeOffset) { + nsINode* node = *aNode; + Maybe<uint32_t> nodeOffset = *aNodeOffset; + if (aRootElement == node || NS_WARN_IF(!node->GetParent()) || + !node->IsText()) { + return; + } + + // When the offset is at the end of the text node, set it to after the + // text node, to make sure the caret is drawn on a new line when the last + // character of the text node is '\n' in <textarea>. + const uint32_t textLength = node->AsContent()->TextLength(); + MOZ_ASSERT(nodeOffset.isNothing() || *nodeOffset <= textLength, + "Offset is past length of text node"); + if (nodeOffset.isNothing() || *nodeOffset != textLength) { + return; + } + + Element* rootParentElement = aRootElement->GetParentElement(); + if (NS_WARN_IF(!rootParentElement)) { + return; + } + // If the root node is not an anonymous div of <textarea>, we don't need to + // do this hack. If you did this, ContentEventHandler couldn't distinguish + // if the range includes open tag of the next node in some cases, e.g., + // textNode]<p></p> vs. textNode<p>]</p> + if (!rootParentElement->IsHTMLElement(nsGkAtoms::textarea)) { + return; + } + + // If the node is being removed from its parent, it holds the ex-parent, + // but the parent have already removed the child from its child chain. + // Therefore `ComputeIndexOf` may fail, but I don't want to make Beta/Nightly + // crash at accessing `Maybe::operator*` so that here checks `isSome`, but + // crashing only in debug builds may help to debug something complicated + // situation, therefore, `MOZ_ASSERT` is put here. + *aNode = node->GetParent(); + Maybe<uint32_t> index = (*aNode)->ComputeIndexOf(node); + MOZ_ASSERT(index.isSome()); + if (index.isSome()) { + MOZ_ASSERT(*index != UINT32_MAX); + *aNodeOffset = Some(*index + 1u); + } else { + *aNodeOffset = Some(0u); + } +} + +nsresult ContentEventHandler::OnSelectionEvent(WidgetSelectionEvent* aEvent) { + aEvent->mSucceeded = false; + + // Get selection to manipulate + // XXX why do we need to get them from ISM? This method should work fine + // without ISM. + nsresult rv = IMEStateManager::GetFocusSelectionAndRootElement( + getter_AddRefs(mSelection), getter_AddRefs(mRootElement)); + if (rv != NS_ERROR_NOT_AVAILABLE) { + NS_ENSURE_SUCCESS(rv, rv); + } else { + rv = Init(aEvent); + NS_ENSURE_SUCCESS(rv, rv); + } + + // Get range from offset and length + RawRange rawRange; + rv = SetRawRangeFromFlatTextOffset(&rawRange, aEvent->mOffset, + aEvent->mLength, GetLineBreakType(aEvent), + aEvent->mExpandToClusterBoundary); + NS_ENSURE_SUCCESS(rv, rv); + + nsINode* startNode = rawRange.GetStartContainer(); + nsINode* endNode = rawRange.GetEndContainer(); + Maybe<uint32_t> startNodeOffset = Some(rawRange.StartOffset()); + Maybe<uint32_t> endNodeOffset = Some(rawRange.EndOffset()); + AdjustRangeForSelection(mRootElement, &startNode, &startNodeOffset); + AdjustRangeForSelection(mRootElement, &endNode, &endNodeOffset); + if (NS_WARN_IF(!startNode) || NS_WARN_IF(!endNode) || + NS_WARN_IF(startNodeOffset.isNothing()) || + NS_WARN_IF(endNodeOffset.isNothing())) { + return NS_ERROR_UNEXPECTED; + } + + if (aEvent->mReversed) { + nsCOMPtr<nsINode> startNodeStrong(startNode); + nsCOMPtr<nsINode> endNodeStrong(endNode); + ErrorResult error; + MOZ_KnownLive(mSelection) + ->SetBaseAndExtentInLimiter(*endNodeStrong, *endNodeOffset, + *startNodeStrong, *startNodeOffset, error); + if (NS_WARN_IF(error.Failed())) { + return error.StealNSResult(); + } + } else { + nsCOMPtr<nsINode> startNodeStrong(startNode); + nsCOMPtr<nsINode> endNodeStrong(endNode); + ErrorResult error; + MOZ_KnownLive(mSelection) + ->SetBaseAndExtentInLimiter(*startNodeStrong, *startNodeOffset, + *endNodeStrong, *endNodeOffset, error); + if (NS_WARN_IF(error.Failed())) { + return error.StealNSResult(); + } + } + + // `ContentEventHandler` is a `MOZ_STACK_CLASS`, so `mSelection` is known to + // be alive. + MOZ_KnownLive(mSelection) + ->ScrollIntoView(nsISelectionController::SELECTION_FOCUS_REGION, + ScrollAxis(), ScrollAxis(), 0); + aEvent->mSucceeded = true; + return NS_OK; +} + +nsRect ContentEventHandler::FrameRelativeRect::RectRelativeTo( + nsIFrame* aDestFrame) const { + if (!mBaseFrame || NS_WARN_IF(!aDestFrame)) { + return nsRect(); + } + + if (NS_WARN_IF(aDestFrame->PresContext() != mBaseFrame->PresContext())) { + return nsRect(); + } + + if (aDestFrame == mBaseFrame) { + return mRect; + } + + nsIFrame* rootFrame = mBaseFrame->PresShell()->GetRootFrame(); + nsRect baseFrameRectInRootFrame = nsLayoutUtils::TransformFrameRectToAncestor( + mBaseFrame, nsRect(), rootFrame); + nsRect destFrameRectInRootFrame = nsLayoutUtils::TransformFrameRectToAncestor( + aDestFrame, nsRect(), rootFrame); + nsPoint difference = + destFrameRectInRootFrame.TopLeft() - baseFrameRectInRootFrame.TopLeft(); + return mRect - difference; +} + +} // namespace mozilla diff --git a/dom/events/ContentEventHandler.h b/dom/events/ContentEventHandler.h new file mode 100644 index 0000000000..135ee650ba --- /dev/null +++ b/dom/events/ContentEventHandler.h @@ -0,0 +1,444 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#ifndef mozilla_ContentEventHandler_h_ +#define mozilla_ContentEventHandler_h_ + +#include "mozilla/EventForwards.h" +#include "mozilla/dom/Selection.h" +#include "nsCOMPtr.h" +#include "nsIFrame.h" +#include "nsINode.h" + +class nsPresContext; +class nsRange; + +struct nsRect; + +namespace mozilla { + +namespace dom { +class Element; +class Text; +} // namespace dom + +enum LineBreakType { LINE_BREAK_TYPE_NATIVE, LINE_BREAK_TYPE_XP }; + +/* + * Query Content Event Handler + * ContentEventHandler is a helper class for EventStateManager. + * The platforms request some content informations, e.g., the selected text, + * the offset of the selected text and the text for specified range. + * This class answers to NS_QUERY_* events from actual contents. + */ + +class MOZ_STACK_CLASS ContentEventHandler { + private: + /** + * RawRange is a helper class of ContentEventHandler class. The caller is + * responsible for making sure the start/end nodes are in document order. + * This is enforced by assertions in DEBUG builds. + */ + class MOZ_STACK_CLASS RawRange final { + public: + RawRange() = default; + + void Clear() { + mRoot = nullptr; + mStart = RangeBoundary(); + mEnd = RangeBoundary(); + } + + bool IsPositioned() const { return mStart.IsSet() && mEnd.IsSet(); } + bool Collapsed() const { return mStart == mEnd && IsPositioned(); } + nsINode* GetStartContainer() const { return mStart.Container(); } + nsINode* GetEndContainer() const { return mEnd.Container(); } + uint32_t StartOffset() const { + return *mStart.Offset( + RangeBoundary::OffsetFilter::kValidOrInvalidOffsets); + } + uint32_t EndOffset() const { + return *mEnd.Offset(RangeBoundary::OffsetFilter::kValidOrInvalidOffsets); + } + nsIContent* StartRef() const { return mStart.Ref(); } + nsIContent* EndRef() const { return mEnd.Ref(); } + + const RangeBoundary& Start() const { return mStart; } + const RangeBoundary& End() const { return mEnd; } + + // XXX: Make these use RangeBoundaries... + nsresult CollapseTo(const RawRangeBoundary& aBoundary) { + return SetStartAndEnd(aBoundary, aBoundary); + } + nsresult SetStart(const RawRangeBoundary& aStart); + nsresult SetEnd(const RawRangeBoundary& aEnd); + + // NOTE: These helpers can hide performance problems, as they perform a + // search to find aStartOffset in aStartContainer. + nsresult SetStart(nsINode* aStartContainer, uint32_t aStartOffset) { + return SetStart(RawRangeBoundary(aStartContainer, aStartOffset)); + } + nsresult SetEnd(nsINode* aEndContainer, uint32_t aEndOffset) { + return SetEnd(RawRangeBoundary(aEndContainer, aEndOffset)); + } + + nsresult SetEndAfter(nsINode* aEndContainer); + void SetStartAndEnd(const nsRange* aRange); + nsresult SetStartAndEnd(const RawRangeBoundary& aStart, + const RawRangeBoundary& aEnd); + + nsresult SelectNodeContents(const nsINode* aNodeToSelectContents); + + private: + inline void AssertStartIsBeforeOrEqualToEnd(); + + nsCOMPtr<nsINode> mRoot; + + RangeBoundary mStart; + RangeBoundary mEnd; + }; + + public: + using Element = dom::Element; + using Selection = dom::Selection; + + explicit ContentEventHandler(nsPresContext* aPresContext); + + // Handle aEvent in the current process. + MOZ_CAN_RUN_SCRIPT nsresult + HandleQueryContentEvent(WidgetQueryContentEvent* aEvent); + + // eQuerySelectedText event handler + MOZ_CAN_RUN_SCRIPT nsresult + OnQuerySelectedText(WidgetQueryContentEvent* aEvent); + // eQueryTextContent event handler + MOZ_CAN_RUN_SCRIPT nsresult + OnQueryTextContent(WidgetQueryContentEvent* aEvent); + // eQueryCaretRect event handler + MOZ_CAN_RUN_SCRIPT nsresult OnQueryCaretRect(WidgetQueryContentEvent* aEvent); + // eQueryTextRect event handler + MOZ_CAN_RUN_SCRIPT nsresult OnQueryTextRect(WidgetQueryContentEvent* aEvent); + // eQueryTextRectArray event handler + MOZ_CAN_RUN_SCRIPT nsresult + OnQueryTextRectArray(WidgetQueryContentEvent* aEvent); + // eQueryEditorRect event handler + MOZ_CAN_RUN_SCRIPT nsresult + OnQueryEditorRect(WidgetQueryContentEvent* aEvent); + // eQueryContentState event handler + MOZ_CAN_RUN_SCRIPT nsresult + OnQueryContentState(WidgetQueryContentEvent* aEvent); + // eQuerySelectionAsTransferable event handler + MOZ_CAN_RUN_SCRIPT nsresult + OnQuerySelectionAsTransferable(WidgetQueryContentEvent* aEvent); + // eQueryCharacterAtPoint event handler + MOZ_CAN_RUN_SCRIPT nsresult + OnQueryCharacterAtPoint(WidgetQueryContentEvent* aEvent); + // eQueryDOMWidgetHittest event handler + MOZ_CAN_RUN_SCRIPT nsresult + OnQueryDOMWidgetHittest(WidgetQueryContentEvent* aEvent); + + // NS_SELECTION_* event + MOZ_CAN_RUN_SCRIPT nsresult OnSelectionEvent(WidgetSelectionEvent* aEvent); + + protected: + RefPtr<dom::Document> mDocument; + // mSelection is typically normal selection but if OnQuerySelectedText() + // is called, i.e., handling eQuerySelectedText, it's the specified selection + // by WidgetQueryContentEvent::mInput::mSelectionType. + RefPtr<Selection> mSelection; + // mFirstSelectedRawRange is initialized from the first range of mSelection, + // if it exists. Otherwise, it is reset by Clear(). + RawRange mFirstSelectedRawRange; + RefPtr<Element> mRootElement; + + MOZ_CAN_RUN_SCRIPT nsresult Init(WidgetQueryContentEvent* aEvent); + MOZ_CAN_RUN_SCRIPT nsresult Init(WidgetSelectionEvent* aEvent); + + nsresult InitBasic(bool aRequireFlush = true); + MOZ_CAN_RUN_SCRIPT nsresult + InitCommon(EventMessage aEventMessage, + SelectionType aSelectionType = SelectionType::eNormal, + bool aRequireFlush = true); + /** + * InitRootContent() computes the root content of current focused editor. + * + * @param aNormalSelection This must be a Selection instance whose type is + * SelectionType::eNormal. + */ + MOZ_CAN_RUN_SCRIPT nsresult + InitRootContent(const Selection& aNormalSelection); + + public: + // FlatText means the text that is generated from DOM tree. The BR elements + // are replaced to native linefeeds. Other elements are ignored. + + // NodePosition stores a pair of node and offset in the node. + // When mNode is an element and mOffset is 0, the start position means after + // the open tag of mNode. + // This is useful to receive one or more sets of them instead of nsRange. + // This type is intended to be used for short-lived operations, and is thus + // marked MOZ_STACK_CLASS. + struct MOZ_STACK_CLASS NodePosition : public RangeBoundary { + // Only when mNode is an element node and mOffset is 0, mAfterOpenTag is + // referred. + bool mAfterOpenTag = true; + + NodePosition() = default; + + NodePosition(nsINode* aContainer, uint32_t aOffset) + : RangeBoundary(aContainer, aOffset) {} + + NodePosition(nsINode* aContainer, nsIContent* aRef) + : RangeBoundary(aContainer, aRef) {} + + explicit NodePosition(const nsIFrame::ContentOffsets& aContentOffsets) + : RangeBoundary(aContentOffsets.content, aContentOffsets.offset) {} + + public: + bool operator==(const NodePosition& aOther) const { + return RangeBoundary::operator==(aOther) && + mAfterOpenTag == aOther.mAfterOpenTag; + } + + bool IsBeforeOpenTag() const { + return IsSet() && Container()->IsElement() && !Ref() && !mAfterOpenTag; + } + bool IsImmediatelyAfterOpenTag() const { + return IsSet() && Container()->IsElement() && !Ref() && mAfterOpenTag; + } + }; + + // NodePositionBefore isn't good name if Container() isn't an element node nor + // Offset() is not 0, though, when Container() is an element node and mOffset + // is 0, this is treated as before the open tag of Container(). + struct NodePositionBefore final : public NodePosition { + NodePositionBefore(nsINode* aContainer, uint32_t aOffset) + : NodePosition(aContainer, aOffset) { + mAfterOpenTag = false; + } + + NodePositionBefore(nsINode* aContainer, nsIContent* aRef) + : NodePosition(aContainer, aRef) { + mAfterOpenTag = false; + } + }; + + // Get the flatten text length in the range. + // @param aStartPosition Start node and offset in the node of the range. + // @param aEndPosition End node and offset in the node of the range. + // @param aRootElement The root element of the editor or document. + // aRootElement won't cause any text including + // line breaks. + // @param aLength The result of the flatten text length of the + // range. + // @param aLineBreakType Whether this computes flatten text length with + // native line breakers on the platform or + // with XP line breaker (\n). + // @param aIsRemovingNode Should be true only when this is called from + // nsIMutationObserver::ContentRemoved(). + // When this is true, aStartPosition.mNode should + // be the root node of removing nodes and mOffset + // should be 0 and aEndPosition.mNode should be + // same as aStartPosition.mNode and mOffset should + // be number of the children of mNode. + static nsresult GetFlatTextLengthInRange(const NodePosition& aStartPosition, + const NodePosition& aEndPosition, + const Element* aRootElement, + uint32_t* aLength, + LineBreakType aLineBreakType, + bool aIsRemovingNode = false); + // Computes the native text length between aStartOffset and aEndOffset of + // aTextNode. + static uint32_t GetNativeTextLength(const dom::Text& aTextNode, + uint32_t aStartOffset, + uint32_t aEndOffset); + // Get the native text length of aTextNode. + static uint32_t GetNativeTextLength(const dom::Text& aTextNode, + uint32_t aMaxLength = UINT32_MAX); + + static uint32_t GetNativeTextLength(const nsAString& aText); + + protected: + // Get the text length of aTextNode. + static uint32_t GetTextLength(const dom::Text& aTextNode, + LineBreakType aLineBreakType, + uint32_t aMaxLength = UINT32_MAX); + // Get the text length of a given range of a content node in + // the given line break type. + static uint32_t GetTextLengthInRange(const dom::Text& aTextNode, + uint32_t aXPStartOffset, + uint32_t aXPEndOffset, + LineBreakType aLineBreakType); + // Get the contents in aElement (meaning all children of aElement) as plain + // text. E.g., specifying mRootElement gets whole text in it. + // Note that the result is not same as .textContent. The result is + // optimized for native IMEs. For example, <br> element and some block + // elements causes "\n" (or "\r\n"), see also ShouldBreakLineBefore(). + nsresult GenerateFlatTextContent(const Element* aElement, nsString& aString, + LineBreakType aLineBreakType); + // Get the contents of aRange as plain text. + nsresult GenerateFlatTextContent(const RawRange& aRawRange, nsString& aString, + LineBreakType aLineBreakType); + // Get offset of start of aRange. Note that the result includes the length + // of line breaker caused by the start of aContent because aRange never + // includes the line breaker caused by its start node. + nsresult GetStartOffset(const RawRange& aRawRange, uint32_t* aOffset, + LineBreakType aLineBreakType); + // Check if we should insert a line break before aContent. + // This should return false only when aContent is an html element which + // is typically used in a paragraph like <em>. + static bool ShouldBreakLineBefore(const nsIContent& aContent, + const Element* aRootElement); + // Get the line breaker length. + static inline uint32_t GetBRLength(LineBreakType aLineBreakType); + static LineBreakType GetLineBreakType(WidgetQueryContentEvent* aEvent); + static LineBreakType GetLineBreakType(WidgetSelectionEvent* aEvent); + static LineBreakType GetLineBreakType(bool aUseNativeLineBreak); + // Returns focused content (including its descendant documents). + nsIContent* GetFocusedContent(); + // QueryContentRect() sets the rect of aContent's frame(s) to aEvent. + nsresult QueryContentRect(nsIContent* aContent, + WidgetQueryContentEvent* aEvent); + // Initialize aRawRange from the offset of FlatText and the text length. + // If aExpandToClusterBoundaries is true, the start offset and the end one are + // expanded to nearest cluster boundaries. + nsresult SetRawRangeFromFlatTextOffset(RawRange* aRawRange, uint32_t aOffset, + uint32_t aLength, + LineBreakType aLineBreakType, + bool aExpandToClusterBoundaries, + uint32_t* aNewOffset = nullptr, + dom::Text** aLastTextNode = nullptr); + // If the aCollapsedRawRange isn't in text node but next to a text node, + // this method modifies it in the text node. Otherwise, not modified. + nsresult AdjustCollapsedRangeMaybeIntoTextNode(RawRange& aCollapsedRawRange); + // Convert the frame relative offset to be relative to the root frame of the + // root presContext (but still measured in appUnits of aFrame's presContext). + nsresult ConvertToRootRelativeOffset(nsIFrame* aFrame, nsRect& aRect); + // Expand aXPOffset to the nearest offset in cluster boundary. aForward is + // true, it is expanded to forward. + // FYI: Due to `nsFrameSelection::GetFrameForNodeOffset()`, this cannot + // take `const dom::Text&`. + nsresult ExpandToClusterBoundary(dom::Text& aTextNode, bool aForward, + uint32_t* aXPOffset) const; + + using FontRangeArray = nsTArray<mozilla::FontRange>; + static void AppendFontRanges(FontRangeArray& aFontRanges, + const dom::Text& aTextNode, uint32_t aBaseOffset, + uint32_t aXPStartOffset, uint32_t aXPEndOffset, + LineBreakType aLineBreakType); + nsresult GenerateFlatFontRanges(const RawRange& aRawRange, + FontRangeArray& aFontRanges, + uint32_t& aLength, + LineBreakType aLineBreakType); + nsresult QueryTextRectByRange(const RawRange& aRawRange, + LayoutDeviceIntRect& aRect, + WritingMode& aWritingMode); + + struct MOZ_STACK_CLASS FrameAndNodeOffset final { + // mFrame is safe since this can live in only stack class and + // ContentEventHandler doesn't modify layout after + // ContentEventHandler::Init() flushes pending layout. In other words, + // this struct shouldn't be used before calling + // ContentEventHandler::Init(). + nsIFrame* mFrame; + // offset in the node of mFrame + int32_t mOffsetInNode; + + FrameAndNodeOffset() : mFrame(nullptr), mOffsetInNode(-1) {} + + FrameAndNodeOffset(nsIFrame* aFrame, int32_t aStartOffsetInNode) + : mFrame(aFrame), mOffsetInNode(aStartOffsetInNode) {} + + nsIFrame* operator->() { return mFrame; } + const nsIFrame* operator->() const { return mFrame; } + operator nsIFrame*() { return mFrame; } + operator const nsIFrame*() const { return mFrame; } + bool IsValid() const { return mFrame && mOffsetInNode >= 0; } + }; + // Get first frame after the start of the given range for computing text rect. + // This returns invalid FrameAndNodeOffset if there is no content which + // should affect to computing text rect in the range. mOffsetInNode is start + // offset in the frame. + FrameAndNodeOffset GetFirstFrameInRangeForTextRect(const RawRange& aRawRange); + + // Get last frame before the end of the given range for computing text rect. + // This returns invalid FrameAndNodeOffset if there is no content which + // should affect to computing text rect in the range. mOffsetInNode is end + // offset in the frame. + FrameAndNodeOffset GetLastFrameInRangeForTextRect(const RawRange& aRawRange); + + struct MOZ_STACK_CLASS FrameRelativeRect final { + // mRect is relative to the mBaseFrame's position. + nsRect mRect; + nsIFrame* mBaseFrame; + + FrameRelativeRect() : mBaseFrame(nullptr) {} + + explicit FrameRelativeRect(nsIFrame* aBaseFrame) : mBaseFrame(aBaseFrame) {} + + FrameRelativeRect(const nsRect& aRect, nsIFrame* aBaseFrame) + : mRect(aRect), mBaseFrame(aBaseFrame) {} + + bool IsValid() const { return mBaseFrame != nullptr; } + + // Returns an nsRect relative to aBaseFrame instead of mBaseFrame. + nsRect RectRelativeTo(nsIFrame* aBaseFrame) const; + }; + + // Returns a rect for line breaker before the node of aFrame (If aFrame is + // a <br> frame or a block level frame, it causes a line break at its + // element's open tag, see also ShouldBreakLineBefore()). Note that this + // doesn't check if aFrame should cause line break in non-debug build. + FrameRelativeRect GetLineBreakerRectBefore(nsIFrame* aFrame); + + // Returns a line breaker rect after aTextNode as there is a line breaker + // immediately after aTextNode. This is useful when following block + // element causes a line break before it and it needs to compute the line + // breaker's rect. For example, if there is |<p>abc</p><p>def</p>|, the + // rect of 2nd <p>'s line breaker should be at right of "c" in the first + // <p>, not the start of 2nd <p>. The result is relative to the last text + // frame which represents the last character of aTextNode. + FrameRelativeRect GuessLineBreakerRectAfter(const dom::Text& aTextNode); + + // Returns a guessed first rect. I.e., it may be different from actual + // caret when selection is collapsed at start of aFrame. For example, this + // guess the caret rect only with the content box of aFrame and its font + // height like: + // +-aFrame----------------- (border box) + // | + // | +--------------------- (content box) + // | | I + // ^ guessed caret rect + // However, actual caret is computed with more information like line-height, + // child frames of aFrame etc. But this does not emulate actual caret + // behavior exactly for simpler and faster code because it's difficult and + // we're not sure it's worthwhile to do it with complicated implementation. + FrameRelativeRect GuessFirstCaretRectIn(nsIFrame* aFrame); + + // Make aRect non-empty. If width and/or height is 0, these methods set them + // to 1. Note that it doesn't set nsRect's width nor height to one device + // pixel because using nsRect::ToOutsidePixels() makes actual width or height + // to 2 pixels because x and y may not be aligned to device pixels. + void EnsureNonEmptyRect(nsRect& aRect) const; + void EnsureNonEmptyRect(LayoutDeviceIntRect& aRect) const; + + /** + * Compute caret rect before or after a character rect. + */ + static LayoutDeviceIntRect GetCaretRectBefore( + const LayoutDeviceIntRect& aCharRect, const WritingMode& aWritingMode); + static LayoutDeviceIntRect GetCaretRectAfter( + const LayoutDeviceIntRect& aCharRect, const WritingMode& aWritingMode); + static nsRect GetCaretRectBefore(const nsRect& aCharRect, + const WritingMode& aWritingMode); + static nsRect GetCaretRectAfter(nsPresContext& aPresContext, + const nsRect& aCharRect, + const WritingMode& aWritingMode); +}; + +} // namespace mozilla + +#endif // mozilla_ContentEventHandler_h_ diff --git a/dom/events/CustomEvent.cpp b/dom/events/CustomEvent.cpp new file mode 100644 index 0000000000..c4f1b30044 --- /dev/null +++ b/dom/events/CustomEvent.cpp @@ -0,0 +1,83 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#include "CustomEvent.h" +#include "mozilla/dom/CustomEventBinding.h" + +#include "mozilla/dom/BindingUtils.h" +#include "mozilla/HoldDropJSObjects.h" +#include "nsContentUtils.h" + +using namespace mozilla; +using namespace mozilla::dom; + +CustomEvent::CustomEvent(mozilla::dom::EventTarget* aOwner, + nsPresContext* aPresContext, + mozilla::WidgetEvent* aEvent) + : Event(aOwner, aPresContext, aEvent), mDetail(JS::NullValue()) { + mozilla::HoldJSObjects(this); +} + +CustomEvent::~CustomEvent() { mozilla::DropJSObjects(this); } + +NS_IMPL_CYCLE_COLLECTION_CLASS(CustomEvent) +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(CustomEvent, Event) + mozilla::DropJSObjects(tmp); +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(CustomEvent, Event) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN_INHERITED(CustomEvent, Event) + NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mDetail) +NS_IMPL_CYCLE_COLLECTION_TRACE_END + +NS_IMPL_ADDREF_INHERITED(CustomEvent, Event) +NS_IMPL_RELEASE_INHERITED(CustomEvent, Event) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(CustomEvent) +NS_INTERFACE_MAP_END_INHERITING(Event) + +already_AddRefed<CustomEvent> CustomEvent::Constructor( + const GlobalObject& aGlobal, const nsAString& aType, + const CustomEventInit& aParam) { + nsCOMPtr<mozilla::dom::EventTarget> t = + do_QueryInterface(aGlobal.GetAsSupports()); + RefPtr<CustomEvent> e = new CustomEvent(t, nullptr, nullptr); + bool trusted = e->Init(t); + JS::Rooted<JS::Value> detail(aGlobal.Context(), aParam.mDetail); + e->InitCustomEvent(aGlobal.Context(), aType, aParam.mBubbles, + aParam.mCancelable, detail); + e->SetTrusted(trusted); + e->SetComposed(aParam.mComposed); + return e.forget(); +} + +JSObject* CustomEvent::WrapObjectInternal(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return mozilla::dom::CustomEvent_Binding::Wrap(aCx, this, aGivenProto); +} + +void CustomEvent::InitCustomEvent(JSContext* aCx, const nsAString& aType, + bool aCanBubble, bool aCancelable, + JS::Handle<JS::Value> aDetail) { + NS_ENSURE_TRUE_VOID(!mEvent->mFlags.mIsBeingDispatched); + + Event::InitEvent(aType, aCanBubble, aCancelable); + mDetail = aDetail; +} + +void CustomEvent::GetDetail(JSContext* aCx, + JS::MutableHandle<JS::Value> aRetval) { + aRetval.set(mDetail); +} + +already_AddRefed<CustomEvent> NS_NewDOMCustomEvent( + EventTarget* aOwner, nsPresContext* aPresContext, + mozilla::WidgetEvent* aEvent) { + RefPtr<CustomEvent> it = new CustomEvent(aOwner, aPresContext, aEvent); + return it.forget(); +} diff --git a/dom/events/CustomEvent.h b/dom/events/CustomEvent.h new file mode 100644 index 0000000000..f046ea323e --- /dev/null +++ b/dom/events/CustomEvent.h @@ -0,0 +1,52 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#ifndef CustomEvent_h__ +#define CustomEvent_h__ + +#include "js/Value.h" +#include "mozilla/dom/Event.h" + +namespace mozilla::dom { + +struct CustomEventInit; + +class CustomEvent final : public Event { + private: + virtual ~CustomEvent(); + + JS::Heap<JS::Value> mDetail; + + public: + explicit CustomEvent(mozilla::dom::EventTarget* aOwner, + nsPresContext* aPresContext = nullptr, + mozilla::WidgetEvent* aEvent = nullptr); + + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS_INHERITED(CustomEvent, Event) + + static already_AddRefed<CustomEvent> Constructor( + const GlobalObject& aGlobal, const nsAString& aType, + const CustomEventInit& aParam); + + virtual JSObject* WrapObjectInternal( + JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override; + + CustomEvent* AsCustomEvent() override { return this; } + + void GetDetail(JSContext* aCx, JS::MutableHandle<JS::Value> aRetval); + + void InitCustomEvent(JSContext* aCx, const nsAString& aType, bool aCanBubble, + bool aCancelable, JS::Handle<JS::Value> aDetail); +}; + +} // namespace mozilla::dom + +already_AddRefed<mozilla::dom::CustomEvent> NS_NewDOMCustomEvent( + mozilla::dom::EventTarget* aOwner, nsPresContext* aPresContext, + mozilla::WidgetEvent* aEvent); + +#endif // CustomEvent_h__ diff --git a/dom/events/DOMEventTargetHelper.cpp b/dom/events/DOMEventTargetHelper.cpp new file mode 100644 index 0000000000..9f8d9ed942 --- /dev/null +++ b/dom/events/DOMEventTargetHelper.cpp @@ -0,0 +1,265 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#include "nsContentUtils.h" +#include "mozilla/dom/Document.h" +#include "mozilla/Sprintf.h" +#include "mozilla/dom/Event.h" +#include "mozilla/DOMEventTargetHelper.h" +#include "mozilla/EventDispatcher.h" +#include "mozilla/EventListenerManager.h" +#include "mozilla/Likely.h" +#include "MainThreadUtils.h" + +namespace mozilla { + +using namespace dom; + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(DOMEventTargetHelper) + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INTERNAL(DOMEventTargetHelper) + if (MOZ_UNLIKELY(cb.WantDebugInfo())) { + char name[512]; + nsAutoString uri; + if (tmp->GetOwner() && tmp->GetOwner()->GetExtantDoc()) { + Unused << tmp->GetOwner()->GetExtantDoc()->GetDocumentURI(uri); + } + + nsXPCOMCycleCollectionParticipant* participant = nullptr; + CallQueryInterface(tmp, &participant); + + SprintfLiteral(name, "%s %s", participant->ClassName(), + NS_ConvertUTF16toUTF8(uri).get()); + cb.DescribeRefCountedNode(tmp->mRefCnt.get(), name); + } else { + NS_IMPL_CYCLE_COLLECTION_DESCRIBE(DOMEventTargetHelper, tmp->mRefCnt.get()) + } + + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mListenerManager) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(DOMEventTargetHelper) + NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER + if (tmp->mListenerManager) { + tmp->mListenerManager->Disconnect(); + NS_IMPL_CYCLE_COLLECTION_UNLINK(mListenerManager) + } + tmp->MaybeDontKeepAlive(); +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_BEGIN(DOMEventTargetHelper) + bool hasLiveWrapper = tmp->HasKnownLiveWrapper(); + if (hasLiveWrapper || tmp->IsCertainlyAliveForCC()) { + if (tmp->mListenerManager) { + tmp->mListenerManager->MarkForCC(); + } + if (!hasLiveWrapper && tmp->PreservingWrapper()) { + tmp->MarkWrapperLive(); + } + return true; + } +NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_END + +NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_IN_CC_BEGIN(DOMEventTargetHelper) + return tmp->HasKnownLiveWrapperAndDoesNotNeedTracing( + static_cast<dom::EventTarget*>(tmp)); +NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_IN_CC_END + +NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_THIS_BEGIN(DOMEventTargetHelper) + return tmp->HasKnownLiveWrapper(); +NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_THIS_END + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(DOMEventTargetHelper) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, EventTarget) + NS_INTERFACE_MAP_ENTRY(GlobalTeardownObserver) + NS_INTERFACE_MAP_ENTRY(dom::EventTarget) + NS_INTERFACE_MAP_ENTRY_CONCRETE(DOMEventTargetHelper) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTING_ADDREF(DOMEventTargetHelper) +NS_IMPL_CYCLE_COLLECTING_RELEASE_WITH_LAST_RELEASE(DOMEventTargetHelper, + LastRelease()) + +DOMEventTargetHelper::DOMEventTargetHelper() = default; + +DOMEventTargetHelper::DOMEventTargetHelper(nsPIDOMWindowInner* aWindow) + : GlobalTeardownObserver(aWindow ? aWindow->AsGlobal() : nullptr) {} + +DOMEventTargetHelper::DOMEventTargetHelper(nsIGlobalObject* aGlobalObject) + : GlobalTeardownObserver(aGlobalObject) {} + +DOMEventTargetHelper::DOMEventTargetHelper(DOMEventTargetHelper* aOther) + : GlobalTeardownObserver(aOther ? aOther->GetParentObject() : nullptr, + aOther ? aOther->HasOrHasHadOwner() : false) {} + +DOMEventTargetHelper::~DOMEventTargetHelper() { + if (mListenerManager) { + mListenerManager->Disconnect(); + } + ReleaseWrapper(this); +} + +void DOMEventTargetHelper::DisconnectFromOwner() { + GlobalTeardownObserver::DisconnectFromOwner(); + + // Event listeners can't be handled anymore, so we can release them here. + if (mListenerManager) { + mListenerManager->Disconnect(); + mListenerManager = nullptr; + } + + MaybeDontKeepAlive(); +} + +nsPIDOMWindowInner* DOMEventTargetHelper::GetWindowIfCurrent() const { + if (NS_FAILED(CheckCurrentGlobalCorrectness())) { + return nullptr; + } + + return GetOwner(); +} + +Document* DOMEventTargetHelper::GetDocumentIfCurrent() const { + nsPIDOMWindowInner* win = GetWindowIfCurrent(); + if (!win) { + return nullptr; + } + + return win->GetDoc(); +} + +bool DOMEventTargetHelper::ComputeDefaultWantsUntrusted(ErrorResult& aRv) { + bool wantsUntrusted; + nsresult rv = WantsUntrusted(&wantsUntrusted); + if (NS_FAILED(rv)) { + aRv.Throw(rv); + return false; + } + return wantsUntrusted; +} + +bool DOMEventTargetHelper::DispatchEvent(Event& aEvent, CallerType aCallerType, + ErrorResult& aRv) { + nsEventStatus status = nsEventStatus_eIgnore; + nsresult rv = EventDispatcher::DispatchDOMEvent( + static_cast<EventTarget*>(this), nullptr, &aEvent, nullptr, &status); + bool retval = !aEvent.DefaultPrevented(aCallerType); + if (NS_FAILED(rv)) { + aRv.Throw(rv); + } + return retval; +} + +nsresult DOMEventTargetHelper::DispatchTrustedEvent( + const nsAString& aEventName) { + RefPtr<Event> event = NS_NewDOMEvent(this, nullptr, nullptr); + event->InitEvent(aEventName, false, false); + + return DispatchTrustedEvent(event); +} + +nsresult DOMEventTargetHelper::DispatchTrustedEvent(Event* event) { + event->SetTrusted(true); + + ErrorResult rv; + DispatchEvent(*event, rv); + return rv.StealNSResult(); +} + +void DOMEventTargetHelper::GetEventTargetParent( + EventChainPreVisitor& aVisitor) { + aVisitor.mCanHandle = true; + aVisitor.SetParentTarget(nullptr, false); +} + +nsresult DOMEventTargetHelper::PostHandleEvent( + EventChainPostVisitor& aVisitor) { + return NS_OK; +} + +EventListenerManager* DOMEventTargetHelper::GetOrCreateListenerManager() { + if (!mListenerManager) { + mListenerManager = new EventListenerManager(this); + } + + return mListenerManager; +} + +EventListenerManager* DOMEventTargetHelper::GetExistingListenerManager() const { + return mListenerManager; +} + +nsresult DOMEventTargetHelper::WantsUntrusted(bool* aRetVal) { + nsresult rv = CheckCurrentGlobalCorrectness(); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<Document> doc = GetDocumentIfCurrent(); + // We can let listeners on workers to always handle all the events. + *aRetVal = (doc && !nsContentUtils::IsChromeDoc(doc)) || !NS_IsMainThread(); + return rv; +} + +void DOMEventTargetHelper::EventListenerAdded(nsAtom* aType) { + MaybeUpdateKeepAlive(); +} + +void DOMEventTargetHelper::EventListenerRemoved(nsAtom* aType) { + MaybeUpdateKeepAlive(); +} + +void DOMEventTargetHelper::KeepAliveIfHasListenersFor(nsAtom* aType) { + mKeepingAliveTypes.AppendElement(aType); + MaybeUpdateKeepAlive(); +} + +void DOMEventTargetHelper::IgnoreKeepAliveIfHasListenersFor(nsAtom* aType) { + mKeepingAliveTypes.RemoveElement(aType); + MaybeUpdateKeepAlive(); +} + +void DOMEventTargetHelper::MaybeUpdateKeepAlive() { + bool shouldBeKeptAlive = false; + + if (NS_SUCCEEDED(CheckCurrentGlobalCorrectness())) { + if (!mKeepingAliveTypes.IsEmpty()) { + for (uint32_t i = 0; i < mKeepingAliveTypes.Length(); ++i) { + if (HasListenersFor(mKeepingAliveTypes[i])) { + shouldBeKeptAlive = true; + break; + } + } + } + } + + if (shouldBeKeptAlive == mIsKeptAlive) { + return; + } + + mIsKeptAlive = shouldBeKeptAlive; + if (mIsKeptAlive) { + AddRef(); + } else { + Release(); + } +} + +void DOMEventTargetHelper::MaybeDontKeepAlive() { + if (mIsKeptAlive) { + mIsKeptAlive = false; + Release(); + } +} + +bool DOMEventTargetHelper::HasListenersFor(const nsAString& aType) const { + return mListenerManager && mListenerManager->HasListenersFor(aType); +} + +bool DOMEventTargetHelper::HasListenersFor(nsAtom* aTypeWithOn) const { + return mListenerManager && mListenerManager->HasListenersFor(aTypeWithOn); +} + +} // namespace mozilla diff --git a/dom/events/DOMEventTargetHelper.h b/dom/events/DOMEventTargetHelper.h new file mode 100644 index 0000000000..3734fe0ef4 --- /dev/null +++ b/dom/events/DOMEventTargetHelper.h @@ -0,0 +1,167 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#ifndef mozilla_DOMEventTargetHelper_h_ +#define mozilla_DOMEventTargetHelper_h_ + +#include "mozilla/Attributes.h" +#include "mozilla/dom/EventTarget.h" +#include "mozilla/GlobalTeardownObserver.h" +#include "mozilla/LinkedList.h" +#include "mozilla/RefPtr.h" +#include "nsAtom.h" +#include "nsCOMPtr.h" +#include "nsCycleCollectionParticipant.h" +#include "nsDebug.h" +#include "nsGkAtoms.h" +#include "nsID.h" +#include "nsIGlobalObject.h" +#include "nsIScriptGlobalObject.h" +#include "nsISupports.h" +#include "nsISupportsUtils.h" +#include "nsPIDOMWindow.h" +#include "nsStringFwd.h" +#include "nsTArray.h" + +class nsCycleCollectionTraversalCallback; + +namespace mozilla { + +class ErrorResult; +class EventChainPostVisitor; +class EventChainPreVisitor; +class EventListenerManager; + +namespace dom { +class Document; +class Event; +enum class CallerType : uint32_t; +} // namespace dom + +#define NS_DOMEVENTTARGETHELPER_IID \ + { \ + 0xa28385c6, 0x9451, 0x4d7e, { \ + 0xa3, 0xdd, 0xf4, 0xb6, 0x87, 0x2f, 0xa4, 0x76 \ + } \ + } + +class DOMEventTargetHelper : public dom::EventTarget, + public GlobalTeardownObserver { + public: + DOMEventTargetHelper(); + explicit DOMEventTargetHelper(nsPIDOMWindowInner* aWindow); + explicit DOMEventTargetHelper(nsIGlobalObject* aGlobalObject); + explicit DOMEventTargetHelper(DOMEventTargetHelper* aOther); + + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_SKIPPABLE_WRAPPERCACHE_CLASS_AMBIGUOUS( + DOMEventTargetHelper, dom::EventTarget) + + virtual EventListenerManager* GetExistingListenerManager() const override; + virtual EventListenerManager* GetOrCreateListenerManager() override; + + bool ComputeDefaultWantsUntrusted(ErrorResult& aRv) override; + + using EventTarget::DispatchEvent; + // TODO: Convert this to MOZ_CAN_RUN_SCRIPT (bug 1415230) + MOZ_CAN_RUN_SCRIPT_BOUNDARY bool DispatchEvent(dom::Event& aEvent, + dom::CallerType aCallerType, + ErrorResult& aRv) override; + + void GetEventTargetParent(EventChainPreVisitor& aVisitor) override; + + nsresult PostHandleEvent(EventChainPostVisitor& aVisitor) override; + + NS_DECLARE_STATIC_IID_ACCESSOR(NS_DOMEVENTTARGETHELPER_IID) + + nsIGlobalObject* GetOwnerGlobal() const override { + return GlobalTeardownObserver::GetOwnerGlobal(); + } + + static DOMEventTargetHelper* FromSupports(nsISupports* aSupports) { + dom::EventTarget* target = static_cast<dom::EventTarget*>(aSupports); +#ifdef DEBUG + { + nsCOMPtr<dom::EventTarget> target_qi = do_QueryInterface(aSupports); + + // If this assertion fires the QI implementation for the object in + // question doesn't use the EventTarget pointer as the + // nsISupports pointer. That must be fixed, or we'll crash... + NS_ASSERTION(target_qi == target, "Uh, fix QI!"); + } +#endif + + return static_cast<DOMEventTargetHelper*>(target); + } + + bool HasListenersFor(const nsAString& aType) const; + + bool HasListenersFor(nsAtom* aTypeWithOn) const; + + virtual nsPIDOMWindowOuter* GetOwnerGlobalForBindingsInternal() override { + return nsPIDOMWindowOuter::GetFromCurrentInner(GetOwner()); + } + + // Like GetOwner, but only returns non-null if the window being returned is + // current (in the "current document" sense of the HTML spec). + nsPIDOMWindowInner* GetWindowIfCurrent() const; + // Returns the document associated with this event target, if that document is + // the current document of its browsing context. Will return null otherwise. + mozilla::dom::Document* GetDocumentIfCurrent() const; + + void DisconnectFromOwner() override; + using EventTarget::GetParentObject; + + virtual void EventListenerAdded(nsAtom* aType) override; + + virtual void EventListenerRemoved(nsAtom* aType) override; + + // Dispatch a trusted, non-cancellable and non-bubbling event to |this|. + nsresult DispatchTrustedEvent(const nsAString& aEventName); + + protected: + virtual ~DOMEventTargetHelper(); + + nsresult WantsUntrusted(bool* aRetVal); + + void MaybeUpdateKeepAlive(); + void MaybeDontKeepAlive(); + + // If this method returns true your object is kept alive until it returns + // false. You can use this method instead using + // NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_BEGIN macro. + virtual bool IsCertainlyAliveForCC() const { return mIsKeptAlive; } + + RefPtr<EventListenerManager> mListenerManager; + // Make |event| trusted and dispatch |aEvent| to |this|. + nsresult DispatchTrustedEvent(dom::Event* aEvent); + + virtual void LastRelease() {} + + void KeepAliveIfHasListenersFor(nsAtom* aType); + + void IgnoreKeepAliveIfHasListenersFor(nsAtom* aType); + + private: + nsTArray<RefPtr<nsAtom>> mKeepingAliveTypes; + + bool mIsKeptAlive = false; +}; + +NS_DEFINE_STATIC_IID_ACCESSOR(DOMEventTargetHelper, NS_DOMEVENTTARGETHELPER_IID) + +} // namespace mozilla + +// WebIDL event handlers +#define IMPL_EVENT_HANDLER(_event) \ + inline mozilla::dom::EventHandlerNonNull* GetOn##_event() { \ + return GetEventHandler(nsGkAtoms::on##_event); \ + } \ + inline void SetOn##_event(mozilla::dom::EventHandlerNonNull* aCallback) { \ + SetEventHandler(nsGkAtoms::on##_event, aCallback); \ + } + +#endif // mozilla_DOMEventTargetHelper_h_ diff --git a/dom/events/DataTransfer.cpp b/dom/events/DataTransfer.cpp new file mode 100644 index 0000000000..42edbdf33c --- /dev/null +++ b/dom/events/DataTransfer.cpp @@ -0,0 +1,1488 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#include "mozilla/ArrayUtils.h" +#include "mozilla/BasePrincipal.h" +#include "mozilla/BasicEvents.h" +#include "mozilla/CheckedInt.h" +#include "mozilla/Span.h" +#include "mozilla/StaticPrefs_dom.h" +#include "DataTransfer.h" + +#include "nsISupportsPrimitives.h" +#include "nsIScriptSecurityManager.h" +#include "mozilla/dom/DOMStringList.h" +#include "nsArray.h" +#include "nsError.h" +#include "nsIDragService.h" +#include "nsIClipboard.h" +#include "nsIXPConnect.h" +#include "nsContentUtils.h" +#include "nsIContent.h" +#include "nsIObjectInputStream.h" +#include "nsIObjectOutputStream.h" +#include "nsIStorageStream.h" +#include "nsStringStream.h" +#include "nsCRT.h" +#include "nsIScriptObjectPrincipal.h" +#include "nsIScriptContext.h" +#include "mozilla/dom/Document.h" +#include "nsIScriptGlobalObject.h" +#include "nsQueryObject.h" +#include "nsVariant.h" +#include "mozilla/dom/ContentChild.h" +#include "mozilla/dom/DataTransferBinding.h" +#include "mozilla/dom/DataTransferItemList.h" +#include "mozilla/dom/Directory.h" +#include "mozilla/dom/Element.h" +#include "mozilla/dom/FileList.h" +#include "mozilla/dom/IPCBlobUtils.h" +#include "mozilla/dom/BindingUtils.h" +#include "mozilla/dom/OSFileSystem.h" +#include "mozilla/dom/Promise.h" +#include "mozilla/dom/WindowContext.h" +#include "mozilla/Unused.h" +#include "nsComponentManagerUtils.h" +#include "nsNetUtil.h" +#include "nsReadableUtils.h" + +namespace mozilla::dom { + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(DataTransfer) + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(DataTransfer) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mParent) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mItems) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mDragTarget) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mDragImage) + NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER +NS_IMPL_CYCLE_COLLECTION_UNLINK_END +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(DataTransfer) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mParent) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mItems) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDragTarget) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDragImage) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTING_ADDREF(DataTransfer) +NS_IMPL_CYCLE_COLLECTING_RELEASE(DataTransfer) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(DataTransfer) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY + NS_INTERFACE_MAP_ENTRY(mozilla::dom::DataTransfer) + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +// the size of the array +const char DataTransfer::sEffects[8][9] = { + "none", "copy", "move", "copyMove", "link", "copyLink", "linkMove", "all"}; + +// Used for custom clipboard types. +enum CustomClipboardTypeId { + eCustomClipboardTypeId_None, + eCustomClipboardTypeId_String +}; + +static DataTransfer::Mode ModeForEvent(EventMessage aEventMessage) { + switch (aEventMessage) { + case eCut: + case eCopy: + case eDragStart: + // For these events, we want to be able to add data to the data transfer, + // Otherwise, the data is already present. + return DataTransfer::Mode::ReadWrite; + case eDrop: + case ePaste: + case ePasteNoFormatting: + case eEditorInput: + // For these events we want to be able to read the data which is stored in + // the DataTransfer, rather than just the type information. + return DataTransfer::Mode::ReadOnly; + default: + return StaticPrefs::dom_events_dataTransfer_protected_enabled() + ? DataTransfer::Mode::Protected + : DataTransfer::Mode::ReadOnly; + } +} + +DataTransfer::DataTransfer(nsISupports* aParent, EventMessage aEventMessage, + bool aIsExternal, int32_t aClipboardType) + : mParent(aParent), + mDropEffect(nsIDragService::DRAGDROP_ACTION_NONE), + mEffectAllowed(nsIDragService::DRAGDROP_ACTION_UNINITIALIZED), + mEventMessage(aEventMessage), + mCursorState(false), + mMode(ModeForEvent(aEventMessage)), + mIsExternal(aIsExternal), + mUserCancelled(false), + mIsCrossDomainSubFrameDrop(false), + mClipboardType(aClipboardType), + mDragImageX(0), + mDragImageY(0) { + mItems = new DataTransferItemList(this); + + // For external usage, cache the data from the native clipboard or drag. + if (mIsExternal && mMode != Mode::ReadWrite) { + if (aEventMessage == ePasteNoFormatting) { + mEventMessage = ePaste; + CacheExternalClipboardFormats(true); + } else if (aEventMessage == ePaste) { + CacheExternalClipboardFormats(false); + } else if (aEventMessage >= eDragDropEventFirst && + aEventMessage <= eDragDropEventLast) { + CacheExternalDragFormats(); + } + } +} + +DataTransfer::DataTransfer(nsISupports* aParent, EventMessage aEventMessage, + nsITransferable* aTransferable) + : mParent(aParent), + mTransferable(aTransferable), + mDropEffect(nsIDragService::DRAGDROP_ACTION_NONE), + mEffectAllowed(nsIDragService::DRAGDROP_ACTION_UNINITIALIZED), + mEventMessage(aEventMessage), + mCursorState(false), + mMode(ModeForEvent(aEventMessage)), + mIsExternal(true), + mUserCancelled(false), + mIsCrossDomainSubFrameDrop(false), + mClipboardType(-1), + mDragImageX(0), + mDragImageY(0) { + mItems = new DataTransferItemList(this); + + // XXX Currently, we cannot make DataTransfer grabs mTransferable for long + // time because nsITransferable is not cycle collectable but this may + // be grabbed by JS. Additionally, the data initializing path is too + // complicated (too optimized) for D&D and clipboard. They are cached + // only formats first, then, data of all items will be filled by the + // items later and by themselves. However, we shouldn't duplicate such + // path for saving the maintenance cost. Therefore, we need to treat + // that DataTransfer and its items are in external mode. Finally, + // release mTransferable and make them in internal mode. + CacheTransferableFormats(); + FillAllExternalData(); + // Now, we have all necessary data of mTransferable. So, we can work as + // internal mode. + mIsExternal = false; + // Release mTransferable because it won't be referred anymore. + mTransferable = nullptr; +} + +DataTransfer::DataTransfer(nsISupports* aParent, EventMessage aEventMessage, + const nsAString& aString) + : mParent(aParent), + mDropEffect(nsIDragService::DRAGDROP_ACTION_NONE), + mEffectAllowed(nsIDragService::DRAGDROP_ACTION_UNINITIALIZED), + mEventMessage(aEventMessage), + mCursorState(false), + mMode(ModeForEvent(aEventMessage)), + mIsExternal(false), + mUserCancelled(false), + mIsCrossDomainSubFrameDrop(false), + mClipboardType(-1), + mDragImageX(0), + mDragImageY(0) { + mItems = new DataTransferItemList(this); + + nsCOMPtr<nsIPrincipal> sysPrincipal = nsContentUtils::GetSystemPrincipal(); + + RefPtr<nsVariantCC> variant = new nsVariantCC(); + variant->SetAsAString(aString); + DebugOnly<nsresult> rvIgnored = + SetDataWithPrincipal(u"text/plain"_ns, variant, 0, sysPrincipal, false); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored), + "Failed to set given string to the DataTransfer object"); +} + +DataTransfer::DataTransfer(nsISupports* aParent, EventMessage aEventMessage, + const uint32_t aEffectAllowed, bool aCursorState, + bool aIsExternal, bool aUserCancelled, + bool aIsCrossDomainSubFrameDrop, + int32_t aClipboardType, DataTransferItemList* aItems, + Element* aDragImage, uint32_t aDragImageX, + uint32_t aDragImageY, bool aShowFailAnimation) + : mParent(aParent), + mDropEffect(nsIDragService::DRAGDROP_ACTION_NONE), + mEffectAllowed(aEffectAllowed), + mEventMessage(aEventMessage), + mCursorState(aCursorState), + mMode(ModeForEvent(aEventMessage)), + mIsExternal(aIsExternal), + mUserCancelled(aUserCancelled), + mIsCrossDomainSubFrameDrop(aIsCrossDomainSubFrameDrop), + mClipboardType(aClipboardType), + mDragImage(aDragImage), + mDragImageX(aDragImageX), + mDragImageY(aDragImageY), + mShowFailAnimation(aShowFailAnimation) { + MOZ_ASSERT(mParent); + MOZ_ASSERT(aItems); + + // We clone the items array after everything else, so that it has a valid + // mParent value + mItems = aItems->Clone(this); + // The items are copied from aItems into mItems. There is no need to copy + // the actual data in the items as the data transfer will be read only. The + // dragstart event is the only time when items are + // modifiable, but those events should have been using the first constructor + // above. + NS_ASSERTION(aEventMessage != eDragStart, + "invalid event type for DataTransfer constructor"); +} + +DataTransfer::~DataTransfer() = default; + +// static +already_AddRefed<DataTransfer> DataTransfer::Constructor( + const GlobalObject& aGlobal) { + RefPtr<DataTransfer> transfer = + new DataTransfer(aGlobal.GetAsSupports(), eCopy, /* is external */ false, + /* clipboard type */ -1); + transfer->mEffectAllowed = nsIDragService::DRAGDROP_ACTION_NONE; + return transfer.forget(); +} + +JSObject* DataTransfer::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return DataTransfer_Binding::Wrap(aCx, this, aGivenProto); +} + +void DataTransfer::SetDropEffect(const nsAString& aDropEffect) { + // the drop effect can only be 'none', 'copy', 'move' or 'link'. + for (uint32_t e = 0; e <= nsIDragService::DRAGDROP_ACTION_LINK; e++) { + if (aDropEffect.EqualsASCII(sEffects[e])) { + // don't allow copyMove + if (e != (nsIDragService::DRAGDROP_ACTION_COPY | + nsIDragService::DRAGDROP_ACTION_MOVE)) { + mDropEffect = e; + } + break; + } + } +} + +void DataTransfer::SetEffectAllowed(const nsAString& aEffectAllowed) { + if (aEffectAllowed.EqualsLiteral("uninitialized")) { + mEffectAllowed = nsIDragService::DRAGDROP_ACTION_UNINITIALIZED; + return; + } + + static_assert(nsIDragService::DRAGDROP_ACTION_NONE == 0, + "DRAGDROP_ACTION_NONE constant is wrong"); + static_assert(nsIDragService::DRAGDROP_ACTION_COPY == 1, + "DRAGDROP_ACTION_COPY constant is wrong"); + static_assert(nsIDragService::DRAGDROP_ACTION_MOVE == 2, + "DRAGDROP_ACTION_MOVE constant is wrong"); + static_assert(nsIDragService::DRAGDROP_ACTION_LINK == 4, + "DRAGDROP_ACTION_LINK constant is wrong"); + + for (uint32_t e = 0; e < ArrayLength(sEffects); e++) { + if (aEffectAllowed.EqualsASCII(sEffects[e])) { + mEffectAllowed = e; + break; + } + } +} + +void DataTransfer::GetMozTriggeringPrincipalURISpec( + nsAString& aPrincipalURISpec) { + nsCOMPtr<nsIDragSession> dragSession = nsContentUtils::GetDragSession(); + if (!dragSession) { + aPrincipalURISpec.Truncate(0); + return; + } + + nsCOMPtr<nsIPrincipal> principal; + dragSession->GetTriggeringPrincipal(getter_AddRefs(principal)); + if (!principal) { + aPrincipalURISpec.Truncate(0); + return; + } + + nsAutoCString spec; + principal->GetAsciiSpec(spec); + CopyUTF8toUTF16(spec, aPrincipalURISpec); +} + +nsIContentSecurityPolicy* DataTransfer::GetMozCSP() { + nsCOMPtr<nsIDragSession> dragSession = nsContentUtils::GetDragSession(); + if (!dragSession) { + return nullptr; + } + nsCOMPtr<nsIContentSecurityPolicy> csp; + dragSession->GetCsp(getter_AddRefs(csp)); + return csp; +} + +already_AddRefed<FileList> DataTransfer::GetFiles( + nsIPrincipal& aSubjectPrincipal) { + return mItems->Files(&aSubjectPrincipal); +} + +void DataTransfer::GetTypes(nsTArray<nsString>& aTypes, + CallerType aCallerType) const { + // When called from bindings, aTypes will be empty, but since we might have + // Gecko-internal callers too, clear it to be safe. + aTypes.Clear(); + + return mItems->GetTypes(aTypes, aCallerType); +} + +bool DataTransfer::HasType(const nsAString& aType) const { + return mItems->HasType(aType); +} + +bool DataTransfer::HasFile() const { return mItems->HasFile(); } + +void DataTransfer::GetData(const nsAString& aFormat, nsAString& aData, + nsIPrincipal& aSubjectPrincipal, + ErrorResult& aRv) const { + // return an empty string if data for the format was not found + aData.Truncate(); + + nsCOMPtr<nsIVariant> data; + nsresult rv = + GetDataAtInternal(aFormat, 0, &aSubjectPrincipal, getter_AddRefs(data)); + if (NS_FAILED(rv)) { + if (rv != NS_ERROR_DOM_INDEX_SIZE_ERR) { + aRv.Throw(rv); + } + return; + } + + if (data) { + nsAutoString stringdata; + data->GetAsAString(stringdata); + + // for the URL type, parse out the first URI from the list. The URIs are + // separated by newlines + nsAutoString lowercaseFormat; + nsContentUtils::ASCIIToLower(aFormat, lowercaseFormat); + + if (lowercaseFormat.EqualsLiteral("url")) { + int32_t lastidx = 0, idx; + int32_t length = stringdata.Length(); + while (lastidx < length) { + idx = stringdata.FindChar('\n', lastidx); + // lines beginning with # are comments + if (stringdata[lastidx] == '#') { + if (idx == -1) { + break; + } + } else { + if (idx == -1) { + aData.Assign(Substring(stringdata, lastidx)); + } else { + aData.Assign(Substring(stringdata, lastidx, idx - lastidx)); + } + aData = + nsContentUtils::TrimWhitespace<nsCRT::IsAsciiSpace>(aData, true); + return; + } + lastidx = idx + 1; + } + } else { + aData = stringdata; + } + } +} + +void DataTransfer::SetData(const nsAString& aFormat, const nsAString& aData, + nsIPrincipal& aSubjectPrincipal, ErrorResult& aRv) { + RefPtr<nsVariantCC> variant = new nsVariantCC(); + variant->SetAsAString(aData); + + aRv = SetDataAtInternal(aFormat, variant, 0, &aSubjectPrincipal); +} + +void DataTransfer::ClearData(const Optional<nsAString>& aFormat, + nsIPrincipal& aSubjectPrincipal, + ErrorResult& aRv) { + if (IsReadOnly()) { + aRv.Throw(NS_ERROR_DOM_NO_MODIFICATION_ALLOWED_ERR); + return; + } + + if (MozItemCount() == 0) { + return; + } + + if (aFormat.WasPassed()) { + MozClearDataAtHelper(aFormat.Value(), 0, aSubjectPrincipal, aRv); + } else { + MozClearDataAtHelper(u""_ns, 0, aSubjectPrincipal, aRv); + } +} + +void DataTransfer::SetMozCursor(const nsAString& aCursorState) { + // Lock the cursor to an arrow during the drag. + mCursorState = aCursorState.EqualsLiteral("default"); +} + +already_AddRefed<nsINode> DataTransfer::GetMozSourceNode() { + nsCOMPtr<nsIDragSession> dragSession = nsContentUtils::GetDragSession(); + if (!dragSession) { + return nullptr; + } + + nsCOMPtr<nsINode> sourceNode; + dragSession->GetSourceNode(getter_AddRefs(sourceNode)); + if (sourceNode && !nsContentUtils::LegacyIsCallerNativeCode() && + !nsContentUtils::CanCallerAccess(sourceNode)) { + return nullptr; + } + + return sourceNode.forget(); +} + +already_AddRefed<WindowContext> DataTransfer::GetSourceTopWindowContext() { + nsCOMPtr<nsIDragSession> dragSession = nsContentUtils::GetDragSession(); + if (!dragSession) { + return nullptr; + } + + RefPtr<WindowContext> sourceTopWindowContext; + dragSession->GetSourceTopWindowContext( + getter_AddRefs(sourceTopWindowContext)); + return sourceTopWindowContext.forget(); +} + +already_AddRefed<DOMStringList> DataTransfer::MozTypesAt( + uint32_t aIndex, ErrorResult& aRv) const { + // Only the first item is valid for clipboard events + if (aIndex > 0 && (mEventMessage == eCut || mEventMessage == eCopy || + mEventMessage == ePaste)) { + aRv.Throw(NS_ERROR_DOM_INDEX_SIZE_ERR); + return nullptr; + } + + RefPtr<DOMStringList> types = new DOMStringList(); + if (aIndex < MozItemCount()) { + // note that you can retrieve the types regardless of their principal + const nsTArray<RefPtr<DataTransferItem>>& items = + *mItems->MozItemsAt(aIndex); + + bool addFile = false; + for (uint32_t i = 0; i < items.Length(); i++) { + // NOTE: The reason why we get the internal type here is because we want + // kFileMime to appear in the types list for backwards compatibility + // reasons. + nsAutoString type; + items[i]->GetInternalType(type); + if (NS_WARN_IF(!types->Add(type))) { + aRv.Throw(NS_ERROR_FAILURE); + return nullptr; + } + + if (items[i]->Kind() == DataTransferItem::KIND_FILE) { + addFile = true; + } + } + + if (addFile) { + types->Add(u"Files"_ns); + } + } + + return types.forget(); +} + +nsresult DataTransfer::GetDataAtNoSecurityCheck(const nsAString& aFormat, + uint32_t aIndex, + nsIVariant** aData) const { + return GetDataAtInternal(aFormat, aIndex, + nsContentUtils::GetSystemPrincipal(), aData); +} + +nsresult DataTransfer::GetDataAtInternal(const nsAString& aFormat, + uint32_t aIndex, + nsIPrincipal* aSubjectPrincipal, + nsIVariant** aData) const { + *aData = nullptr; + + if (aFormat.IsEmpty()) { + return NS_OK; + } + + if (aIndex >= MozItemCount()) { + return NS_ERROR_DOM_INDEX_SIZE_ERR; + } + + // Only the first item is valid for clipboard events + if (aIndex > 0 && (mEventMessage == eCut || mEventMessage == eCopy || + mEventMessage == ePaste)) { + return NS_ERROR_DOM_INDEX_SIZE_ERR; + } + + nsAutoString format; + GetRealFormat(aFormat, format); + + MOZ_ASSERT(aSubjectPrincipal); + + RefPtr<DataTransferItem> item = mItems->MozItemByTypeAt(format, aIndex); + if (!item) { + // The index exists but there's no data for the specified format, in this + // case we just return undefined + return NS_OK; + } + + // If we have chrome only content, and we aren't chrome, don't allow access + if (!aSubjectPrincipal->IsSystemPrincipal() && item->ChromeOnly()) { + return NS_OK; + } + + // DataTransferItem::Data() handles the principal checks + ErrorResult result; + nsCOMPtr<nsIVariant> data = item->Data(aSubjectPrincipal, result); + if (NS_WARN_IF(!data || result.Failed())) { + return result.StealNSResult(); + } + + data.forget(aData); + return NS_OK; +} + +void DataTransfer::MozGetDataAt(JSContext* aCx, const nsAString& aFormat, + uint32_t aIndex, + JS::MutableHandle<JS::Value> aRetval, + mozilla::ErrorResult& aRv) { + nsCOMPtr<nsIVariant> data; + aRv = GetDataAtInternal(aFormat, aIndex, nsContentUtils::GetSystemPrincipal(), + getter_AddRefs(data)); + if (aRv.Failed()) { + return; + } + + if (!data) { + aRetval.setNull(); + return; + } + + JS::Rooted<JS::Value> result(aCx); + if (!VariantToJsval(aCx, data, aRetval)) { + aRv = NS_ERROR_FAILURE; + return; + } +} + +/* static */ +bool DataTransfer::PrincipalMaySetData(const nsAString& aType, + nsIVariant* aData, + nsIPrincipal* aPrincipal) { + if (!aPrincipal->IsSystemPrincipal()) { + DataTransferItem::eKind kind = DataTransferItem::KindFromData(aData); + if (kind == DataTransferItem::KIND_OTHER) { + NS_WARNING("Disallowing adding non string/file types to DataTransfer"); + return false; + } + + // Don't allow adding internal types of the form */x-moz-*, but + // special-case the url types as they are simple variations of urls. + // In addition, allow x-moz-place flavors to be added by WebExtensions. + if (FindInReadable(kInternal_Mimetype_Prefix, aType) && + !StringBeginsWith(aType, u"text/x-moz-url"_ns)) { + auto principal = BasePrincipal::Cast(aPrincipal); + if (!principal->AddonPolicy() || + !StringBeginsWith(aType, u"text/x-moz-place"_ns)) { + NS_WARNING("Disallowing adding this type to DataTransfer"); + return false; + } + } + } + + return true; +} + +void DataTransfer::TypesListMayHaveChanged() { + DataTransfer_Binding::ClearCachedTypesValue(this); +} + +already_AddRefed<DataTransfer> DataTransfer::MozCloneForEvent( + const nsAString& aEvent, ErrorResult& aRv) { + RefPtr<nsAtom> atomEvt = NS_Atomize(aEvent); + if (!atomEvt) { + aRv.Throw(NS_ERROR_OUT_OF_MEMORY); + return nullptr; + } + EventMessage eventMessage = nsContentUtils::GetEventMessage(atomEvt); + + RefPtr<DataTransfer> dt; + nsresult rv = Clone(mParent, eventMessage, false, false, getter_AddRefs(dt)); + if (NS_FAILED(rv)) { + aRv.Throw(rv); + return nullptr; + } + return dt.forget(); +} + +// The order of the types matters. `kFileMime` needs to be one of the first two +// types. +static const char* kNonPlainTextExternalFormats[] = { + kCustomTypesMime, kFileMime, kHTMLMime, kRTFMime, kURLMime, + kURLDataMime, kTextMime, kPNGImageMime, kPDFJSMime}; + +/* static */ +void DataTransfer::GetExternalClipboardFormats(const int32_t& aWhichClipboard, + const bool& aPlainTextOnly, + nsTArray<nsCString>* aResult) { + MOZ_ASSERT(aResult); + + // NOTE: When you change this method, you may need to change + // GetExternalTransferableFormats() too since those methods should + // work similarly. + + nsCOMPtr<nsIClipboard> clipboard = + do_GetService("@mozilla.org/widget/clipboard;1"); + if (!clipboard || aWhichClipboard < 0) { + return; + } + + if (aPlainTextOnly) { + bool hasType; + AutoTArray<nsCString, 1> textMime = {nsDependentCString(kTextMime)}; + nsresult rv = + clipboard->HasDataMatchingFlavors(textMime, aWhichClipboard, &hasType); + NS_SUCCEEDED(rv); + if (hasType) { + aResult->AppendElement(kTextMime); + } + return; + } + + // If not plain text only, then instead check all the other types + for (uint32_t f = 0; f < mozilla::ArrayLength(kNonPlainTextExternalFormats); + ++f) { + bool hasType; + AutoTArray<nsCString, 1> format = { + nsDependentCString(kNonPlainTextExternalFormats[f])}; + nsresult rv = + clipboard->HasDataMatchingFlavors(format, aWhichClipboard, &hasType); + NS_SUCCEEDED(rv); + if (hasType) { + aResult->AppendElement(kNonPlainTextExternalFormats[f]); + } + } +} + +/* static */ +void DataTransfer::GetExternalTransferableFormats( + nsITransferable* aTransferable, bool aPlainTextOnly, + nsTArray<nsCString>* aResult) { + MOZ_ASSERT(aTransferable); + MOZ_ASSERT(aResult); + + aResult->Clear(); + + // NOTE: When you change this method, you may need to change + // GetExternalClipboardFormats() too since those methods should + // work similarly. + + AutoTArray<nsCString, 10> flavors; + aTransferable->FlavorsTransferableCanExport(flavors); + + if (aPlainTextOnly) { + auto index = flavors.IndexOf(nsLiteralCString(kTextMime)); + if (index != flavors.NoIndex) { + aResult->AppendElement(nsLiteralCString(kTextMime)); + } + return; + } + + // If not plain text only, then instead check all the other types + for (const char* format : kNonPlainTextExternalFormats) { + auto index = flavors.IndexOf(nsCString(format)); + if (index != flavors.NoIndex) { + aResult->AppendElement(nsCString(format)); + } + } +} + +nsresult DataTransfer::SetDataAtInternal(const nsAString& aFormat, + nsIVariant* aData, uint32_t aIndex, + nsIPrincipal* aSubjectPrincipal) { + if (aFormat.IsEmpty()) { + return NS_OK; + } + + if (IsReadOnly()) { + return NS_ERROR_DOM_NO_MODIFICATION_ALLOWED_ERR; + } + + // Specifying an index less than the current length will replace an existing + // item. Specifying an index equal to the current length will add a new item. + if (aIndex > MozItemCount()) { + return NS_ERROR_DOM_INDEX_SIZE_ERR; + } + + // Only the first item is valid for clipboard events + if (aIndex > 0 && (mEventMessage == eCut || mEventMessage == eCopy || + mEventMessage == ePaste)) { + return NS_ERROR_DOM_INDEX_SIZE_ERR; + } + + // Don't allow the custom type to be assigned. + if (aFormat.EqualsLiteral(kCustomTypesMime)) { + return NS_ERROR_DOM_NOT_SUPPORTED_ERR; + } + + if (!PrincipalMaySetData(aFormat, aData, aSubjectPrincipal)) { + return NS_ERROR_DOM_SECURITY_ERR; + } + + return SetDataWithPrincipal(aFormat, aData, aIndex, aSubjectPrincipal); +} + +void DataTransfer::MozSetDataAt(JSContext* aCx, const nsAString& aFormat, + JS::Handle<JS::Value> aData, uint32_t aIndex, + ErrorResult& aRv) { + nsCOMPtr<nsIVariant> data; + aRv = nsContentUtils::XPConnect()->JSValToVariant(aCx, aData, + getter_AddRefs(data)); + if (!aRv.Failed()) { + aRv = SetDataAtInternal(aFormat, data, aIndex, + nsContentUtils::GetSystemPrincipal()); + } +} + +void DataTransfer::MozClearDataAt(const nsAString& aFormat, uint32_t aIndex, + ErrorResult& aRv) { + if (IsReadOnly()) { + aRv.Throw(NS_ERROR_DOM_NO_MODIFICATION_ALLOWED_ERR); + return; + } + + if (aIndex >= MozItemCount()) { + aRv.Throw(NS_ERROR_DOM_INDEX_SIZE_ERR); + return; + } + + // Only the first item is valid for clipboard events + if (aIndex > 0 && (mEventMessage == eCut || mEventMessage == eCopy || + mEventMessage == ePaste)) { + aRv.Throw(NS_ERROR_DOM_INDEX_SIZE_ERR); + return; + } + + MozClearDataAtHelper(aFormat, aIndex, *nsContentUtils::GetSystemPrincipal(), + aRv); + + // If we just cleared the 0-th index, and there are still more than 1 indexes + // remaining, MozClearDataAt should cause the 1st index to become the 0th + // index. This should _only_ happen when the MozClearDataAt function is + // explicitly called by script, as this behavior is inconsistent with spec. + // (however, so is the MozClearDataAt API) + + if (aIndex == 0 && mItems->MozItemCount() > 1 && + mItems->MozItemsAt(0)->Length() == 0) { + mItems->PopIndexZero(); + } +} + +void DataTransfer::MozClearDataAtHelper(const nsAString& aFormat, + uint32_t aIndex, + nsIPrincipal& aSubjectPrincipal, + ErrorResult& aRv) { + MOZ_ASSERT(!IsReadOnly()); + MOZ_ASSERT(aIndex < MozItemCount()); + MOZ_ASSERT(aIndex == 0 || (mEventMessage != eCut && mEventMessage != eCopy && + mEventMessage != ePaste)); + + nsAutoString format; + GetRealFormat(aFormat, format); + + mItems->MozRemoveByTypeAt(format, aIndex, aSubjectPrincipal, aRv); +} + +void DataTransfer::SetDragImage(Element& aImage, int32_t aX, int32_t aY) { + if (!IsReadOnly()) { + mDragImage = &aImage; + mDragImageX = aX; + mDragImageY = aY; + } +} + +void DataTransfer::UpdateDragImage(Element& aImage, int32_t aX, int32_t aY) { + if (mEventMessage < eDragDropEventFirst || + mEventMessage > eDragDropEventLast) { + return; + } + + nsCOMPtr<nsIDragSession> dragSession = nsContentUtils::GetDragSession(); + if (dragSession) { + dragSession->UpdateDragImage(&aImage, aX, aY); + } +} + +void DataTransfer::AddElement(Element& aElement, ErrorResult& aRv) { + if (IsReadOnly()) { + aRv.Throw(NS_ERROR_DOM_NO_MODIFICATION_ALLOWED_ERR); + return; + } + + mDragTarget = &aElement; +} + +nsresult DataTransfer::Clone(nsISupports* aParent, EventMessage aEventMessage, + bool aUserCancelled, + bool aIsCrossDomainSubFrameDrop, + DataTransfer** aNewDataTransfer) { + RefPtr<DataTransfer> newDataTransfer = new DataTransfer( + aParent, aEventMessage, mEffectAllowed, mCursorState, mIsExternal, + aUserCancelled, aIsCrossDomainSubFrameDrop, mClipboardType, mItems, + mDragImage, mDragImageX, mDragImageY, mShowFailAnimation); + + newDataTransfer.forget(aNewDataTransfer); + return NS_OK; +} + +already_AddRefed<nsIArray> DataTransfer::GetTransferables( + nsINode* aDragTarget) { + MOZ_ASSERT(aDragTarget); + + Document* doc = aDragTarget->GetComposedDoc(); + if (!doc) { + return nullptr; + } + + return GetTransferables(doc->GetLoadContext()); +} + +already_AddRefed<nsIArray> DataTransfer::GetTransferables( + nsILoadContext* aLoadContext) { + nsCOMPtr<nsIMutableArray> transArray = nsArray::Create(); + if (!transArray) { + return nullptr; + } + + uint32_t count = MozItemCount(); + for (uint32_t i = 0; i < count; i++) { + nsCOMPtr<nsITransferable> transferable = GetTransferable(i, aLoadContext); + if (transferable) { + transArray->AppendElement(transferable); + } + } + + return transArray.forget(); +} + +already_AddRefed<nsITransferable> DataTransfer::GetTransferable( + uint32_t aIndex, nsILoadContext* aLoadContext) { + if (aIndex >= MozItemCount()) { + return nullptr; + } + + const nsTArray<RefPtr<DataTransferItem>>& item = *mItems->MozItemsAt(aIndex); + uint32_t count = item.Length(); + if (!count) { + return nullptr; + } + + nsCOMPtr<nsITransferable> transferable = + do_CreateInstance("@mozilla.org/widget/transferable;1"); + if (!transferable) { + return nullptr; + } + transferable->Init(aLoadContext); + + nsCOMPtr<nsIStorageStream> storageStream; + nsCOMPtr<nsIObjectOutputStream> stream; + + bool added = false; + bool handlingCustomFormats = true; + + // When writing the custom data, we need to ensure that there is sufficient + // space for a (uint32_t) data ending type, and the null byte character at + // the end of the nsCString. We claim that space upfront and store it in + // baseLength. This value will be set to zero if a write error occurs + // indicating that the data and length are no longer valid. + const uint32_t baseLength = sizeof(uint32_t) + 1; + uint32_t totalCustomLength = baseLength; + + /* + * Two passes are made here to iterate over all of the types. First, look for + * any types that are not in the list of known types. For this pass, + * handlingCustomFormats will be true. Data that corresponds to unknown types + * will be pulled out and inserted into a single type (kCustomTypesMime) by + * writing the data into a stream. + * + * The second pass will iterate over the formats looking for known types. + * These are added as is. The unknown types are all then inserted as a single + * type (kCustomTypesMime) in the same position of the first custom type. This + * model is used to maintain the format order as best as possible. + * + * The format of the kCustomTypesMime type is one or more of the following + * stored sequentially: + * <32-bit> type (only none or string is supported) + * <32-bit> length of format + * <wide string> format + * <32-bit> length of data + * <wide string> data + * A type of eCustomClipboardTypeId_None ends the list, without any following + * data. + */ + do { + for (uint32_t f = 0; f < count; f++) { + RefPtr<DataTransferItem> formatitem = item[f]; + nsCOMPtr<nsIVariant> variant = formatitem->DataNoSecurityCheck(); + if (!variant) { // skip empty items + continue; + } + + nsAutoString type; + formatitem->GetInternalType(type); + + // If the data is of one of the well-known formats, use it directly. + bool isCustomFormat = true; + for (const char* format : kKnownFormats) { + if (type.EqualsASCII(format)) { + isCustomFormat = false; + break; + } + } + + uint32_t lengthInBytes; + nsCOMPtr<nsISupports> convertedData; + + if (handlingCustomFormats) { + if (!ConvertFromVariant(variant, getter_AddRefs(convertedData), + &lengthInBytes)) { + continue; + } + + // When handling custom types, add the data to the stream if this is a + // custom type. If totalCustomLength is 0, then a write error occurred + // on a previous item, so ignore any others. + if (isCustomFormat && totalCustomLength > 0) { + // If it isn't a string, just ignore it. The dataTransfer is cached in + // the drag sesion during drag-and-drop, so non-strings will be + // available when dragging locally. + nsCOMPtr<nsISupportsString> str(do_QueryInterface(convertedData)); + if (str) { + nsAutoString data; + str->GetData(data); + + if (!stream) { + // Create a storage stream to write to. + NS_NewStorageStream(1024, UINT32_MAX, + getter_AddRefs(storageStream)); + + nsCOMPtr<nsIOutputStream> outputStream; + storageStream->GetOutputStream(0, getter_AddRefs(outputStream)); + + stream = NS_NewObjectOutputStream(outputStream); + } + + CheckedInt<uint32_t> formatLength = + CheckedInt<uint32_t>(type.Length()) * + sizeof(nsString::char_type); + + // The total size of the stream is the format length, the data + // length, two integers to hold the lengths and one integer for + // the string flag. Guard against large data by ignoring any that + // don't fit. + CheckedInt<uint32_t> newSize = formatLength + totalCustomLength + + lengthInBytes + + (sizeof(uint32_t) * 3); + if (newSize.isValid()) { + // If a write error occurs, set totalCustomLength to 0 so that + // further processing gets ignored. + nsresult rv = stream->Write32(eCustomClipboardTypeId_String); + if (NS_WARN_IF(NS_FAILED(rv))) { + totalCustomLength = 0; + continue; + } + rv = stream->Write32(formatLength.value()); + if (NS_WARN_IF(NS_FAILED(rv))) { + totalCustomLength = 0; + continue; + } + MOZ_ASSERT(formatLength.isValid() && + formatLength.value() == + type.Length() * sizeof(nsString::char_type), + "Why is formatLength off?"); + rv = stream->WriteBytes( + AsBytes(Span(type.BeginReading(), type.Length()))); + if (NS_WARN_IF(NS_FAILED(rv))) { + totalCustomLength = 0; + continue; + } + rv = stream->Write32(lengthInBytes); + if (NS_WARN_IF(NS_FAILED(rv))) { + totalCustomLength = 0; + continue; + } + // XXXbz it's not obvious to me that lengthInBytes is the actual + // length of "data" if the variant contained an nsISupportsString + // as VTYPE_INTERFACE, say. We used lengthInBytes above for + // sizing, so just keep doing that. + rv = stream->WriteBytes( + Span(reinterpret_cast<const uint8_t*>(data.BeginReading()), + lengthInBytes)); + if (NS_WARN_IF(NS_FAILED(rv))) { + totalCustomLength = 0; + continue; + } + + totalCustomLength = newSize.value(); + } + } + } + } else if (isCustomFormat && stream) { + // This is the second pass of the loop (handlingCustomFormats is false). + // When encountering the first custom format, append all of the stream + // at this position. If totalCustomLength is 0 indicating a write error + // occurred, or no data has been added to it, don't output anything, + if (totalCustomLength > baseLength) { + // Write out an end of data terminator. + nsresult rv = stream->Write32(eCustomClipboardTypeId_None); + if (NS_SUCCEEDED(rv)) { + nsCOMPtr<nsIInputStream> inputStream; + storageStream->NewInputStream(0, getter_AddRefs(inputStream)); + + RefPtr<nsStringBuffer> stringBuffer = + nsStringBuffer::Alloc(totalCustomLength); + + // Subtract off the null terminator when reading. + totalCustomLength--; + + // Read the data from the stream and add a null-terminator as + // ToString needs it. + uint32_t amountRead; + rv = inputStream->Read(static_cast<char*>(stringBuffer->Data()), + totalCustomLength, &amountRead); + if (NS_SUCCEEDED(rv)) { + static_cast<char*>(stringBuffer->Data())[amountRead] = 0; + + nsCString str; + stringBuffer->ToString(totalCustomLength, str); + nsCOMPtr<nsISupportsCString> strSupports( + do_CreateInstance(NS_SUPPORTS_CSTRING_CONTRACTID)); + strSupports->SetData(str); + + nsresult rv = + transferable->SetTransferData(kCustomTypesMime, strSupports); + if (NS_FAILED(rv)) { + return nullptr; + } + + added = true; + } + } + } + + // Clear the stream so it doesn't get used again. + stream = nullptr; + } else { + // This is the second pass of the loop and a known type is encountered. + // Add it as is. + if (!ConvertFromVariant(variant, getter_AddRefs(convertedData), + &lengthInBytes)) { + continue; + } + + NS_ConvertUTF16toUTF8 format(type); + + // If a converter is set for a format, set the converter for the + // transferable and don't add the item + nsCOMPtr<nsIFormatConverter> converter = + do_QueryInterface(convertedData); + if (converter) { + transferable->AddDataFlavor(format.get()); + transferable->SetConverter(converter); + continue; + } + + nsresult rv = + transferable->SetTransferData(format.get(), convertedData); + if (NS_FAILED(rv)) { + return nullptr; + } + + added = true; + } + } + + handlingCustomFormats = !handlingCustomFormats; + } while (!handlingCustomFormats); + + // only return the transferable if data was successfully added to it + if (added) { + return transferable.forget(); + } + + return nullptr; +} + +bool DataTransfer::ConvertFromVariant(nsIVariant* aVariant, + nsISupports** aSupports, + uint32_t* aLength) const { + *aSupports = nullptr; + *aLength = 0; + + uint16_t type = aVariant->GetDataType(); + if (type == nsIDataType::VTYPE_INTERFACE || + type == nsIDataType::VTYPE_INTERFACE_IS) { + nsCOMPtr<nsISupports> data; + if (NS_FAILED(aVariant->GetAsISupports(getter_AddRefs(data)))) { + return false; + } + + // For flavour data providers, use 0 as the length. + if (nsCOMPtr<nsIFlavorDataProvider> fdp = do_QueryInterface(data)) { + fdp.forget(aSupports); + *aLength = 0; + return true; + } + + // Only use the underlying BlobImpl for transferables. + if (RefPtr<Blob> blob = do_QueryObject(data)) { + RefPtr<BlobImpl> blobImpl = blob->Impl(); + blobImpl.forget(aSupports); + } else { + data.forget(aSupports); + } + + *aLength = sizeof(nsISupports*); + return true; + } + + nsAutoString str; + nsresult rv = aVariant->GetAsAString(str); + if (NS_FAILED(rv)) { + return false; + } + + nsCOMPtr<nsISupportsString> strSupports( + do_CreateInstance(NS_SUPPORTS_STRING_CONTRACTID)); + if (!strSupports) { + return false; + } + + strSupports->SetData(str); + + strSupports.forget(aSupports); + + // each character is two bytes + *aLength = str.Length() * 2; + + return true; +} + +void DataTransfer::Disconnect() { + SetMode(Mode::Protected); + if (StaticPrefs::dom_events_dataTransfer_protected_enabled()) { + ClearAll(); + } +} + +void DataTransfer::ClearAll() { mItems->ClearAllItems(); } + +uint32_t DataTransfer::MozItemCount() const { return mItems->MozItemCount(); } + +nsresult DataTransfer::SetDataWithPrincipal(const nsAString& aFormat, + nsIVariant* aData, uint32_t aIndex, + nsIPrincipal* aPrincipal, + bool aHidden) { + nsAutoString format; + GetRealFormat(aFormat, format); + + ErrorResult rv; + RefPtr<DataTransferItem> item = + mItems->SetDataWithPrincipal(format, aData, aIndex, aPrincipal, + /* aInsertOnly = */ false, aHidden, rv); + return rv.StealNSResult(); +} + +void DataTransfer::SetDataWithPrincipalFromOtherProcess( + const nsAString& aFormat, nsIVariant* aData, uint32_t aIndex, + nsIPrincipal* aPrincipal, bool aHidden) { + if (aFormat.EqualsLiteral(kCustomTypesMime)) { + FillInExternalCustomTypes(aData, aIndex, aPrincipal); + } else { + nsAutoString format; + GetRealFormat(aFormat, format); + + ErrorResult rv; + RefPtr<DataTransferItem> item = + mItems->SetDataWithPrincipal(format, aData, aIndex, aPrincipal, + /* aInsertOnly = */ false, aHidden, rv); + if (NS_WARN_IF(rv.Failed())) { + rv.SuppressException(); + } + } +} + +void DataTransfer::GetRealFormat(const nsAString& aInFormat, + nsAString& aOutFormat) const { + // For compatibility, treat text/unicode as equivalent to text/plain + nsAutoString lowercaseFormat; + nsContentUtils::ASCIIToLower(aInFormat, lowercaseFormat); + if (lowercaseFormat.EqualsLiteral("text") || + lowercaseFormat.EqualsLiteral("text/unicode")) { + aOutFormat.AssignLiteral("text/plain"); + return; + } + + if (lowercaseFormat.EqualsLiteral("url")) { + aOutFormat.AssignLiteral("text/uri-list"); + return; + } + + aOutFormat.Assign(lowercaseFormat); +} + +nsresult DataTransfer::CacheExternalData(const char* aFormat, uint32_t aIndex, + nsIPrincipal* aPrincipal, + bool aHidden) { + ErrorResult rv; + RefPtr<DataTransferItem> item; + + if (strcmp(aFormat, kTextMime) == 0) { + item = mItems->SetDataWithPrincipal(u"text/plain"_ns, nullptr, aIndex, + aPrincipal, false, aHidden, rv); + if (NS_WARN_IF(rv.Failed())) { + return rv.StealNSResult(); + } + return NS_OK; + } + + if (strcmp(aFormat, kURLDataMime) == 0) { + item = mItems->SetDataWithPrincipal(u"text/uri-list"_ns, nullptr, aIndex, + aPrincipal, false, aHidden, rv); + if (NS_WARN_IF(rv.Failed())) { + return rv.StealNSResult(); + } + return NS_OK; + } + + nsAutoString format; + GetRealFormat(NS_ConvertUTF8toUTF16(aFormat), format); + item = mItems->SetDataWithPrincipal(format, nullptr, aIndex, aPrincipal, + false, aHidden, rv); + if (NS_WARN_IF(rv.Failed())) { + return rv.StealNSResult(); + } + return NS_OK; +} + +void DataTransfer::CacheExternalDragFormats() { + // Called during the constructor to cache the formats available from an + // external drag. The data associated with each format will be set to null. + // This data will instead only be retrieved in FillInExternalDragData when + // asked for, as it may be time consuming for the source application to + // generate it. + + nsCOMPtr<nsIDragSession> dragSession = nsContentUtils::GetDragSession(); + if (!dragSession) { + return; + } + + // make sure that the system principal is used for external drags + nsIScriptSecurityManager* ssm = nsContentUtils::GetSecurityManager(); + nsCOMPtr<nsIPrincipal> sysPrincipal; + ssm->GetSystemPrincipal(getter_AddRefs(sysPrincipal)); + + // there isn't a way to get a list of the formats that might be available on + // all platforms, so just check for the types that can actually be imported + // XXXndeakin there are some other formats but those are platform specific. + // NOTE: kFileMime must have index 0 + // TODO: should this be `kNonPlainTextExternalFormats` instead? + static const char* formats[] = {kFileMime, kHTMLMime, kURLMime, + kURLDataMime, kTextMime, kPNGImageMime}; + + uint32_t count; + dragSession->GetNumDropItems(&count); + for (uint32_t c = 0; c < count; c++) { + bool hasFileData = false; + dragSession->IsDataFlavorSupported(kFileMime, &hasFileData); + + // First, check for the special format that holds custom types. + bool supported; + dragSession->IsDataFlavorSupported(kCustomTypesMime, &supported); + if (supported) { + FillInExternalCustomTypes(c, sysPrincipal); + } + + for (uint32_t f = 0; f < ArrayLength(formats); f++) { + // IsDataFlavorSupported doesn't take an index as an argument and just + // checks if any of the items support a particular flavor, even though + // the GetData method does take an index. Here, we just assume that + // every item being dragged has the same set of flavors. + bool supported; + dragSession->IsDataFlavorSupported(formats[f], &supported); + // if the format is supported, add an item to the array with null as + // the data. When retrieved, GetRealData will read the data. + if (supported) { + CacheExternalData(formats[f], c, sysPrincipal, + /* hidden = */ f && hasFileData); + } + } + } +} + +void DataTransfer::CacheExternalClipboardFormats(bool aPlainTextOnly) { + // Called during the constructor for paste events to cache the formats + // available on the clipboard. As with CacheExternalDragFormats, the + // data will only be retrieved when needed. + NS_ASSERTION(mEventMessage == ePaste, + "caching clipboard data for invalid event"); + + nsCOMPtr<nsIPrincipal> sysPrincipal = nsContentUtils::GetSystemPrincipal(); + + nsTArray<nsCString> typesArray; + + if (XRE_IsContentProcess()) { + ContentChild::GetSingleton()->SendGetExternalClipboardFormats( + mClipboardType, aPlainTextOnly, &typesArray); + } else { + GetExternalClipboardFormats(mClipboardType, aPlainTextOnly, &typesArray); + } + + if (aPlainTextOnly) { + // The only thing that will be in types is kTextMime + MOZ_ASSERT(typesArray.IsEmpty() || typesArray.Length() == 1); + if (typesArray.Length() == 1) { + CacheExternalData(kTextMime, 0, sysPrincipal, false); + } + return; + } + + CacheExternalData(typesArray, sysPrincipal); +} + +void DataTransfer::CacheTransferableFormats() { + nsCOMPtr<nsIPrincipal> sysPrincipal = nsContentUtils::GetSystemPrincipal(); + + AutoTArray<nsCString, 10> typesArray; + GetExternalTransferableFormats(mTransferable, false, &typesArray); + + CacheExternalData(typesArray, sysPrincipal); +} + +void DataTransfer::CacheExternalData(const nsTArray<nsCString>& aTypes, + nsIPrincipal* aPrincipal) { + bool hasFileData = false; + for (const nsCString& type : aTypes) { + if (type.EqualsLiteral(kCustomTypesMime)) { + FillInExternalCustomTypes(0, aPrincipal); + } else if (type.EqualsLiteral(kFileMime) && XRE_IsContentProcess() && + !StaticPrefs::dom_events_dataTransfer_mozFile_enabled()) { + // We will be ignoring any application/x-moz-file files found in the paste + // datatransfer within e10s, as they will fail top be sent over IPC. + // Because of that, we will unset hasFileData, whether or not it would + // have been set. (bug 1308007) + hasFileData = false; + continue; + } else { + // We expect that if kFileMime is supported, then it will be the either at + // index 0 or at index 1 in the aTypes returned by + // GetExternalClipboardFormats + if (type.EqualsLiteral(kFileMime)) { + hasFileData = true; + } + + // If we aren't the file data, and we have file data, we want to be hidden + CacheExternalData( + type.get(), 0, aPrincipal, + /* hidden = */ !type.EqualsLiteral(kFileMime) && hasFileData); + } + } +} + +void DataTransfer::FillAllExternalData() { + if (mIsExternal) { + for (uint32_t i = 0; i < MozItemCount(); ++i) { + const nsTArray<RefPtr<DataTransferItem>>& items = *mItems->MozItemsAt(i); + for (uint32_t j = 0; j < items.Length(); ++j) { + MOZ_ASSERT(items[j]->Index() == i); + + items[j]->FillInExternalData(); + } + } + } +} + +void DataTransfer::FillInExternalCustomTypes(uint32_t aIndex, + nsIPrincipal* aPrincipal) { + RefPtr<DataTransferItem> item = new DataTransferItem( + this, NS_LITERAL_STRING_FROM_CSTRING(kCustomTypesMime), + DataTransferItem::KIND_STRING); + item->SetIndex(aIndex); + + nsCOMPtr<nsIVariant> variant = item->DataNoSecurityCheck(); + if (!variant) { + return; + } + + FillInExternalCustomTypes(variant, aIndex, aPrincipal); +} + +void DataTransfer::FillInExternalCustomTypes(nsIVariant* aData, uint32_t aIndex, + nsIPrincipal* aPrincipal) { + char* chrs; + uint32_t len = 0; + nsresult rv = aData->GetAsStringWithSize(&len, &chrs); + if (NS_FAILED(rv)) { + return; + } + + CheckedInt<int32_t> checkedLen(len); + if (!checkedLen.isValid()) { + return; + } + + nsCOMPtr<nsIInputStream> stringStream; + NS_NewByteInputStream(getter_AddRefs(stringStream), + Span(chrs, checkedLen.value()), NS_ASSIGNMENT_ADOPT); + + nsCOMPtr<nsIObjectInputStream> stream = NS_NewObjectInputStream(stringStream); + + uint32_t type; + do { + rv = stream->Read32(&type); + NS_ENSURE_SUCCESS_VOID(rv); + if (type == eCustomClipboardTypeId_String) { + uint32_t formatLength; + rv = stream->Read32(&formatLength); + NS_ENSURE_SUCCESS_VOID(rv); + char* formatBytes; + rv = stream->ReadBytes(formatLength, &formatBytes); + NS_ENSURE_SUCCESS_VOID(rv); + nsAutoString format; + format.Adopt(reinterpret_cast<char16_t*>(formatBytes), + formatLength / sizeof(char16_t)); + + uint32_t dataLength; + rv = stream->Read32(&dataLength); + NS_ENSURE_SUCCESS_VOID(rv); + char* dataBytes; + rv = stream->ReadBytes(dataLength, &dataBytes); + NS_ENSURE_SUCCESS_VOID(rv); + nsAutoString data; + data.Adopt(reinterpret_cast<char16_t*>(dataBytes), + dataLength / sizeof(char16_t)); + + RefPtr<nsVariantCC> variant = new nsVariantCC(); + rv = variant->SetAsAString(data); + NS_ENSURE_SUCCESS_VOID(rv); + + SetDataWithPrincipal(format, variant, aIndex, aPrincipal); + } + } while (type != eCustomClipboardTypeId_None); +} + +void DataTransfer::SetMode(DataTransfer::Mode aMode) { + if (!StaticPrefs::dom_events_dataTransfer_protected_enabled() && + aMode == Mode::Protected) { + mMode = Mode::ReadOnly; + } else { + mMode = aMode; + } +} + +} // namespace mozilla::dom diff --git a/dom/events/DataTransfer.h b/dom/events/DataTransfer.h new file mode 100644 index 0000000000..39d549f7f0 --- /dev/null +++ b/dom/events/DataTransfer.h @@ -0,0 +1,529 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#ifndef mozilla_dom_DataTransfer_h +#define mozilla_dom_DataTransfer_h + +#include "nsString.h" +#include "nsTArray.h" +#include "nsIVariant.h" +#include "nsIPrincipal.h" +#include "nsIDragService.h" +#include "nsITransferable.h" +#include "nsCycleCollectionParticipant.h" + +#include "mozilla/ArrayUtils.h" +#include "mozilla/Assertions.h" +#include "mozilla/Attributes.h" +#include "mozilla/EventForwards.h" +#include "mozilla/dom/BindingDeclarations.h" +#include "mozilla/dom/DataTransferItemList.h" +#include "mozilla/dom/File.h" + +class nsINode; +class nsITransferable; +class nsILoadContext; + +namespace mozilla { + +class EventStateManager; + +namespace dom { + +class IPCDataTransfer; +class DataTransferItem; +class DataTransferItemList; +class DOMStringList; +class Element; +class FileList; +class Promise; +template <typename T> +class Optional; +class WindowContext; + +#define NS_DATATRANSFER_IID \ + { \ + 0x6c5f90d1, 0xa886, 0x42c8, { \ + 0x85, 0x06, 0x10, 0xbe, 0x5c, 0x0d, 0xc6, 0x77 \ + } \ + } + +/** + * See <https://html.spec.whatwg.org/multipage/dnd.html#datatransfer>. + */ +class DataTransfer final : public nsISupports, public nsWrapperCache { + public: + NS_DECLARE_STATIC_IID_ACCESSOR(NS_DATATRANSFER_IID) + + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + + NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(DataTransfer) + + friend class mozilla::EventStateManager; + + /// An enum which represents which "Drag Data Store Mode" the DataTransfer is + /// in according to the spec. + enum class Mode : uint8_t { + ReadWrite, + ReadOnly, + Protected, + }; + + protected: + // hide the default constructor + DataTransfer(); + + // this constructor is used only by the Clone method to copy the fields as + // needed to a new data transfer. + // NOTE: Do not call this method directly. + DataTransfer(nsISupports* aParent, EventMessage aEventMessage, + const uint32_t aEffectAllowed, bool aCursorState, + bool aIsExternal, bool aUserCancelled, + bool aIsCrossDomainSubFrameDrop, int32_t aClipboardType, + DataTransferItemList* aItems, Element* aDragImage, + uint32_t aDragImageX, uint32_t aDragImageY, + bool aShowFailAnimation); + + ~DataTransfer(); + + static const char sEffects[8][9]; + + public: + // Constructor for DataTransfer. + // + // aIsExternal must only be true when used to create a dataTransfer for a + // paste, a drag or an input that was started without using a data transfer. + // The case of a drag will occur when an external drag occurs, that is, a + // drag where the source is another application, or a drag is started by + // calling the drag service directly. For clipboard operations, + // aClipboardType indicates which clipboard to use, from nsIClipboard, or -1 + // for non-clipboard operations, or if access to the system clipboard should + // not be allowed. + DataTransfer(nsISupports* aParent, EventMessage aEventMessage, + bool aIsExternal, int32_t aClipboardType); + DataTransfer(nsISupports* aParent, EventMessage aEventMessage, + nsITransferable* aTransferable); + DataTransfer(nsISupports* aParent, EventMessage aEventMessage, + const nsAString& aString); + + virtual JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + nsISupports* GetParentObject() const { return mParent; } + + void SetParentObject(nsISupports* aNewParent) { + MOZ_ASSERT(aNewParent); + // Setting the parent after we've been wrapped is pointless, so + // make sure we aren't wrapped yet. + MOZ_ASSERT(!GetWrapperPreserveColor()); + mParent = aNewParent; + } + + static already_AddRefed<DataTransfer> Constructor( + const GlobalObject& aGlobal); + + /** + * The actual effect that will be used, and should always be one of the + * possible values of effectAllowed. + * + * For dragstart, drag and dragleave events, the dropEffect is initialized + * to none. Any value assigned to the dropEffect will be set, but the value + * isn't used for anything. + * + * For the dragenter and dragover events, the dropEffect will be initialized + * based on what action the user is requesting. How this is determined is + * platform specific, but typically the user can press modifier keys to + * adjust which action is desired. Within an event handler for the dragenter + * and dragover events, the dropEffect should be modified if the action the + * user is requesting is not the one that is desired. + * + * For the drop and dragend events, the dropEffect will be initialized to + * the action that was desired, which will be the value that the dropEffect + * had after the last dragenter or dragover event. + * + * Possible values: + * copy - a copy of the source item is made at the new location + * move - an item is moved to a new location + * link - a link is established to the source at the new location + * none - the item may not be dropped + * + * Assigning any other value has no effect and retains the old value. + */ + void GetDropEffect(nsAString& aDropEffect) { + aDropEffect.AssignASCII(sEffects[mDropEffect]); + } + void SetDropEffect(const nsAString& aDropEffect); + + /* + * Specifies the effects that are allowed for this drag. You may set this in + * the dragstart event to set the desired effects for the source, and within + * the dragenter and dragover events to set the desired effects for the + * target. The value is not used for other events. + * + * Possible values: + * copy - a copy of the source item is made at the new location + * move - an item is moved to a new location + * link - a link is established to the source at the new location + * copyLink, copyMove, linkMove, all - combinations of the above + * none - the item may not be dropped + * uninitialized - the default value when the effect has not been set, + * equivalent to all. + * + * Assigning any other value has no effect and retains the old value. + */ + void GetEffectAllowed(nsAString& aEffectAllowed) { + if (mEffectAllowed == nsIDragService::DRAGDROP_ACTION_UNINITIALIZED) { + aEffectAllowed.AssignLiteral("uninitialized"); + } else { + aEffectAllowed.AssignASCII(sEffects[mEffectAllowed]); + } + } + void SetEffectAllowed(const nsAString& aEffectAllowed); + + /** + * Set the image to be used for dragging if a custom one is desired. Most of + * the time, this would not be set, as a default image is created from the + * node that was dragged. + * + * If the node is an HTML img element, an HTML canvas element or a XUL image + * element, the image data is used. Otherwise, image should be a visible + * node and the drag image will be created from this. If image is null, any + * custom drag image is cleared and the default is used instead. + * + * The coordinates specify the offset into the image where the mouse cursor + * should be. To center the image for instance, use values that are half the + * width and height. + * + * @param image a node to use + * @param x the horizontal offset + * @param y the vertical offset + */ + void SetDragImage(Element& aElement, int32_t aX, int32_t aY); + void UpdateDragImage(Element& aElement, int32_t aX, int32_t aY); + + void GetTypes(nsTArray<nsString>& aTypes, CallerType aCallerType) const; + bool HasType(const nsAString& aType) const; + bool HasFile() const; + + void GetData(const nsAString& aFormat, nsAString& aData, + nsIPrincipal& aSubjectPrincipal, ErrorResult& aRv) const; + + void SetData(const nsAString& aFormat, const nsAString& aData, + nsIPrincipal& aSubjectPrincipal, ErrorResult& aRv); + + void ClearData(const mozilla::dom::Optional<nsAString>& aFormat, + nsIPrincipal& aSubjectPrincipal, mozilla::ErrorResult& aRv); + + /** + * Holds a list of all the local files available on this data transfer. + * A dataTransfer containing no files will return an empty list, and an + * invalid index access on the resulting file list will return null. + */ + already_AddRefed<FileList> GetFiles(nsIPrincipal& aSubjectPrincipal); + + void AddElement(Element& aElement, mozilla::ErrorResult& aRv); + + uint32_t MozItemCount() const; + + void GetMozCursor(nsAString& aCursor) { + if (mCursorState) { + aCursor.AssignLiteral("default"); + } else { + aCursor.AssignLiteral("auto"); + } + } + void SetMozCursor(const nsAString& aCursor); + + already_AddRefed<DOMStringList> MozTypesAt(uint32_t aIndex, + mozilla::ErrorResult& aRv) const; + + void MozClearDataAt(const nsAString& aFormat, uint32_t aIndex, + mozilla::ErrorResult& aRv); + + void MozSetDataAt(JSContext* aCx, const nsAString& aFormat, + JS::Handle<JS::Value> aData, uint32_t aIndex, + mozilla::ErrorResult& aRv); + + void MozGetDataAt(JSContext* aCx, const nsAString& aFormat, uint32_t aIndex, + JS::MutableHandle<JS::Value> aRetval, + mozilla::ErrorResult& aRv); + + bool MozUserCancelled() const { return mUserCancelled; } + + already_AddRefed<nsINode> GetMozSourceNode(); + + already_AddRefed<WindowContext> GetSourceTopWindowContext(); + + /* + * Integer version of dropEffect, set to one of the constants in + * nsIDragService. + */ + uint32_t DropEffectInt() const { return mDropEffect; } + void SetDropEffectInt(uint32_t aDropEffectInt) { + MOZ_RELEASE_ASSERT(aDropEffectInt < ArrayLength(sEffects), + "Bogus drop effect value"); + mDropEffect = aDropEffectInt; + } + + /* + * Integer version of effectAllowed, set to one or a combination of the + * constants in nsIDragService. + */ + uint32_t EffectAllowedInt() const { return mEffectAllowed; } + + void GetMozTriggeringPrincipalURISpec(nsAString& aPrincipalURISpec); + + nsIContentSecurityPolicy* GetMozCSP(); + + mozilla::dom::Element* GetDragTarget() const { return mDragTarget; } + + nsresult GetDataAtNoSecurityCheck(const nsAString& aFormat, uint32_t aIndex, + nsIVariant** aData) const; + + DataTransferItemList* Items() const { return mItems; } + + // Returns the current "Drag Data Store Mode" of the DataTransfer. This + // determines what modifications may be performed on the DataTransfer, and + // what data may be read from it. + Mode GetMode() const { return mMode; } + void SetMode(Mode aMode); + + // Helper method. Is true if the DataTransfer's mode is ReadOnly or Protected, + // which means that the DataTransfer cannot be modified. + bool IsReadOnly() const { return mMode != Mode::ReadWrite; } + // Helper method. Is true if the DataTransfer's mode is Protected, which means + // that DataTransfer type information may be read, but data may not be. + bool IsProtected() const { return mMode == Mode::Protected; } + + nsITransferable* GetTransferable() const { return mTransferable; } + int32_t ClipboardType() const { return mClipboardType; } + EventMessage GetEventMessage() const { return mEventMessage; } + bool IsCrossDomainSubFrameDrop() const { return mIsCrossDomainSubFrameDrop; } + + // converts the data into an array of nsITransferable objects to be used for + // drag and drop or clipboard operations. + already_AddRefed<nsIArray> GetTransferables(nsINode* aDragTarget); + + already_AddRefed<nsIArray> GetTransferables(nsILoadContext* aLoadContext); + + // converts the data for a single item at aIndex into an nsITransferable + // object. + already_AddRefed<nsITransferable> GetTransferable( + uint32_t aIndex, nsILoadContext* aLoadContext); + + // converts the data in the variant to an nsISupportString if possible or + // an nsISupports or null otherwise. + bool ConvertFromVariant(nsIVariant* aVariant, nsISupports** aSupports, + uint32_t* aLength) const; + + // Disconnects the DataTransfer from the Drag Data Store. If the + // dom.dataTransfer.disconnect pref is enabled, this will clear the + // DataTransfer and set it to the `Protected` state, otherwise this method is + // a no-op. + void Disconnect(); + + // clears all of the data + void ClearAll(); + + // Similar to SetData except also specifies the principal to store. + // aData may be null when called from CacheExternalDragFormats or + // CacheExternalClipboardFormats. + nsresult SetDataWithPrincipal(const nsAString& aFormat, nsIVariant* aData, + uint32_t aIndex, nsIPrincipal* aPrincipal, + bool aHidden = false); + + // Variation of SetDataWithPrincipal with handles extracting + // kCustomTypesMime data into separate types. + // + // @param aHidden if true and `aFormat != kCustomTypesMime`, the data will be + // hidden from non-chrome code. + // TODO: unclear, whether `aHidden` should be considered for + // the custom types. + void SetDataWithPrincipalFromOtherProcess(const nsAString& aFormat, + nsIVariant* aData, uint32_t aIndex, + nsIPrincipal* aPrincipal, + bool aHidden); + + // returns a weak reference to the drag image + Element* GetDragImage(int32_t* aX, int32_t* aY) const { + *aX = mDragImageX; + *aY = mDragImageY; + return mDragImage; + } + + // This method makes a copy of the DataTransfer object, with a few properties + // changed, and the mode updated to reflect the correct mode for the given + // event. This method is used during the drag operation to generate the + // DataTransfer objects for each event after `dragstart`. Event objects will + // lazily clone the DataTransfer stored in the DragSession (which is a clone + // of the DataTransfer used in the `dragstart` event) when requested. + nsresult Clone(nsISupports* aParent, EventMessage aEventMessage, + bool aUserCancelled, bool aIsCrossDomainSubFrameDrop, + DataTransfer** aResult); + + // converts some formats used for compatibility in aInFormat into aOutFormat. + // Text becomes text/plain, and URL becomes text/uri-list + void GetRealFormat(const nsAString& aInFormat, nsAString& aOutFormat) const; + + static bool PrincipalMaySetData(const nsAString& aFormat, nsIVariant* aData, + nsIPrincipal* aPrincipal); + + // Notify the DataTransfer that the list returned from GetTypes may have + // changed. This can happen due to items we care about for purposes of + // GetTypes being added or removed or changing item kinds. + void TypesListMayHaveChanged(); + + // Testing method used to emulate internal DataTransfer management. + // NOTE: Please don't use this. See the comments in the webidl for more. + already_AddRefed<DataTransfer> MozCloneForEvent(const nsAString& aEvent, + ErrorResult& aRv); + + void SetMozShowFailAnimation(bool aShouldAnimate) { + mShowFailAnimation = aShouldAnimate; + } + bool MozShowFailAnimation() const { return mShowFailAnimation; } + + // Retrieve a list of clipboard formats supported + // + // If kFileMime is supported, then it will be placed either at + // index 0 or at index 1 in aResult + static void GetExternalClipboardFormats(const int32_t& aWhichClipboard, + const bool& aPlainTextOnly, + nsTArray<nsCString>* aResult); + + // Retrieve a list of supporting formats in aTransferable. + // + // If kFileMime is supported, then it will be placed either at + // index 0 or at index 1 in aResult + static void GetExternalTransferableFormats(nsITransferable* aTransferable, + bool aPlainTextOnly, + nsTArray<nsCString>* aResult); + + // Formats that are "known" and won't be converted to the kCustomTypesMime. + static inline const char* const kKnownFormats[] = {kTextMime, + kHTMLMime, + kNativeHTMLMime, + kRTFMime, + kURLMime, + kURLDataMime, + kURLDescriptionMime, + kURLPrivateMime, + kPNGImageMime, + kJPEGImageMime, + kGIFImageMime, + kNativeImageMime, + kFileMime, + kFilePromiseMime, + kFilePromiseURLMime, + kFilePromiseDestFilename, + kFilePromiseDirectoryMime, + kMozTextInternal, + kHTMLContext, + kHTMLInfo, + kImageRequestMime, + kPDFJSMime}; + + protected: + // caches text and uri-list data formats that exist in the drag service or + // clipboard for retrieval later. + nsresult CacheExternalData(const char* aFormat, uint32_t aIndex, + nsIPrincipal* aPrincipal, bool aHidden); + + // caches the formats that exist in the drag service that were added by an + // external drag + void CacheExternalDragFormats(); + + // caches the formats that exist in the clipboard + void CacheExternalClipboardFormats(bool aPlainTextOnly); + + // caches the formats that exist in mTransferable + void CacheTransferableFormats(); + + // caches the formats specified by aTypes. + void CacheExternalData(const nsTArray<nsCString>& aTypes, + nsIPrincipal* aPrincipal); + + FileList* GetFilesInternal(ErrorResult& aRv, nsIPrincipal* aSubjectPrincipal); + nsresult GetDataAtInternal(const nsAString& aFormat, uint32_t aIndex, + nsIPrincipal* aSubjectPrincipal, + nsIVariant** aData) const; + + nsresult SetDataAtInternal(const nsAString& aFormat, nsIVariant* aData, + uint32_t aIndex, nsIPrincipal* aSubjectPrincipal); + + friend class ContentParent; + friend class Clipboard; + + void FillAllExternalData(); + + void FillInExternalCustomTypes(uint32_t aIndex, nsIPrincipal* aPrincipal); + + void FillInExternalCustomTypes(nsIVariant* aData, uint32_t aIndex, + nsIPrincipal* aPrincipal); + + void MozClearDataAtHelper(const nsAString& aFormat, uint32_t aIndex, + nsIPrincipal& aSubjectPrincipal, + mozilla::ErrorResult& aRv); + + nsCOMPtr<nsISupports> mParent; + + // If DataTransfer is initialized with an instance of nsITransferable, it's + // grabbed with this member **until** the constructor fills all data of all + // items. + nsCOMPtr<nsITransferable> mTransferable; + + // the drop effect and effect allowed + uint32_t mDropEffect; + uint32_t mEffectAllowed; + + // the event message this data transfer is for. This will correspond to an + // event->mMessage value. + EventMessage mEventMessage; + + // Indicates the behavior of the cursor during drag operations + bool mCursorState; + + // The current "Drag Data Store Mode" which the DataTransfer is in. + Mode mMode; + + // true for drags started without a data transfer, for example, those from + // another application. + bool mIsExternal; + + // true if the user cancelled the drag. Used only for the dragend event. + bool mUserCancelled; + + // true if this is a cross-domain drop from a subframe where access to the + // data should be prevented + bool mIsCrossDomainSubFrameDrop; + + // Indicates which clipboard type to use for clipboard operations. Ignored for + // drag and drop. + int32_t mClipboardType; + + // The items contained with the DataTransfer + RefPtr<DataTransferItemList> mItems; + + // the target of the drag. The drag and dragend events will fire at this. + nsCOMPtr<mozilla::dom::Element> mDragTarget; + + // the custom drag image and coordinates within the image. If mDragImage is + // null, the default image is created from the drag target. + nsCOMPtr<mozilla::dom::Element> mDragImage; + uint32_t mDragImageX; + uint32_t mDragImageY; + + // Whether to animate the drag back to its starting point if it fails. + // Not supported everywhere. + bool mShowFailAnimation = true; +}; + +NS_DEFINE_STATIC_IID_ACCESSOR(DataTransfer, NS_DATATRANSFER_IID) + +} // namespace dom +} // namespace mozilla + +#endif /* mozilla_dom_DataTransfer_h */ diff --git a/dom/events/DataTransferItem.cpp b/dom/events/DataTransferItem.cpp new file mode 100644 index 0000000000..94869b4c4e --- /dev/null +++ b/dom/events/DataTransferItem.cpp @@ -0,0 +1,615 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#include "DataTransferItem.h" +#include "DataTransferItemList.h" + +#include "mozilla/Attributes.h" +#include "mozilla/BasePrincipal.h" +#include "mozilla/ContentEvents.h" +#include "mozilla/EventForwards.h" +#include "mozilla/StaticPrefs_dom.h" +#include "mozilla/dom/BlobImpl.h" +#include "mozilla/dom/DataTransferItemBinding.h" +#include "mozilla/dom/Directory.h" +#include "mozilla/dom/Event.h" +#include "mozilla/dom/FileSystem.h" +#include "mozilla/dom/FileSystemDirectoryEntry.h" +#include "mozilla/dom/FileSystemFileEntry.h" +#include "imgIContainer.h" +#include "imgITools.h" +#include "nsComponentManagerUtils.h" +#include "nsIClipboard.h" +#include "nsIFile.h" +#include "nsIInputStream.h" +#include "nsISupportsPrimitives.h" +#include "nsIScriptObjectPrincipal.h" +#include "nsNetUtil.h" +#include "nsQueryObject.h" +#include "nsContentUtils.h" +#include "nsThreadUtils.h" +#include "nsVariant.h" + +namespace { + +struct FileMimeNameData { + const char* mMimeName; + const char* mFileName; +}; + +FileMimeNameData kFileMimeNameMap[] = { + {kFileMime, "GenericFileName"}, + {kPNGImageMime, "GenericImageNamePNG"}, +}; + +} // anonymous namespace + +namespace mozilla::dom { + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(DataTransferItem, mData, mPrincipal, + mDataTransfer, mCachedFile) +NS_IMPL_CYCLE_COLLECTING_ADDREF(DataTransferItem) +NS_IMPL_CYCLE_COLLECTING_RELEASE(DataTransferItem) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(DataTransferItem) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +JSObject* DataTransferItem::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return DataTransferItem_Binding::Wrap(aCx, this, aGivenProto); +} + +already_AddRefed<DataTransferItem> DataTransferItem::Clone( + DataTransfer* aDataTransfer) const { + MOZ_ASSERT(aDataTransfer); + + RefPtr<DataTransferItem> it = new DataTransferItem(aDataTransfer, mType); + + // Copy over all of the fields + it->mKind = mKind; + it->mIndex = mIndex; + it->mData = mData; + it->mPrincipal = mPrincipal; + it->mChromeOnly = mChromeOnly; + + return it.forget(); +} + +void DataTransferItem::SetData(nsIVariant* aData) { + // Invalidate our file cache, we will regenerate it with the new data + mCachedFile = nullptr; + + if (!aData) { + // We are holding a temporary null which will later be filled. + // These are provided by the system, and have guaranteed properties about + // their kind based on their type. + MOZ_ASSERT(!mType.IsEmpty()); + // This type should not be provided by the OS. + MOZ_ASSERT(!mType.EqualsASCII(kNativeImageMime)); + + mKind = KIND_STRING; + for (uint32_t i = 0; i < ArrayLength(kFileMimeNameMap); ++i) { + if (mType.EqualsASCII(kFileMimeNameMap[i].mMimeName)) { + mKind = KIND_FILE; + break; + } + } + + mData = nullptr; + return; + } + + mData = aData; + mKind = KindFromData(mData); +} + +/* static */ DataTransferItem::eKind DataTransferItem::KindFromData( + nsIVariant* aData) { + nsCOMPtr<nsISupports> supports; + nsresult rv = aData->GetAsISupports(getter_AddRefs(supports)); + if (NS_SUCCEEDED(rv) && supports) { + // Check if we have one of the supported file data formats + if (RefPtr<Blob>(do_QueryObject(supports)) || + nsCOMPtr<BlobImpl>(do_QueryInterface(supports)) || + nsCOMPtr<nsIFile>(do_QueryInterface(supports))) { + return KIND_FILE; + } + + if (StaticPrefs::dom_events_dataTransfer_imageAsFile_enabled()) { + // Firefox internally uses imgIContainer to represent images being + // copied/dragged. These need to be encoded to PNG files. + if (nsCOMPtr<imgIContainer>(do_QueryInterface(supports))) { + return KIND_FILE; + } + } + } + + nsAutoString string; + // If we can't get the data type as a string, that means that the object + // should be considered to be of the "other" type. This is impossible + // through the APIs defined by the spec, but we provide extra Moz* APIs, + // which allow setting of non-string data. We determine whether we can + // consider it a string, by calling GetAsAString, and checking for success. + rv = aData->GetAsAString(string); + if (NS_SUCCEEDED(rv)) { + return KIND_STRING; + } + + return KIND_OTHER; +} + +void DataTransferItem::FillInExternalData() { + if (mData) { + return; + } + + NS_ConvertUTF16toUTF8 utf8format(mType); + const char* format = utf8format.get(); + if (strcmp(format, "text/uri-list") == 0) { + format = kURLDataMime; + } + + nsCOMPtr<nsITransferable> trans = mDataTransfer->GetTransferable(); + if (!trans) { + trans = do_CreateInstance("@mozilla.org/widget/transferable;1"); + if (NS_WARN_IF(!trans)) { + return; + } + + trans->Init(nullptr); + trans->AddDataFlavor(format); + + if (mDataTransfer->GetEventMessage() == ePaste) { + MOZ_ASSERT(mIndex == 0, "index in clipboard must be 0"); + + nsCOMPtr<nsIClipboard> clipboard = + do_GetService("@mozilla.org/widget/clipboard;1"); + if (!clipboard || mDataTransfer->ClipboardType() < 0) { + return; + } + + nsresult rv = clipboard->GetData(trans, mDataTransfer->ClipboardType()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + } else { + nsCOMPtr<nsIDragSession> dragSession = nsContentUtils::GetDragSession(); + if (!dragSession) { + return; + } + + nsresult rv = dragSession->GetData(trans, mIndex); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + } + } + + nsCOMPtr<nsISupports> data; + nsresult rv = trans->GetTransferData(format, getter_AddRefs(data)); + if (NS_WARN_IF(NS_FAILED(rv) || !data)) { + return; + } + + // Fill the variant + RefPtr<nsVariantCC> variant = new nsVariantCC(); + + eKind oldKind = Kind(); + if (oldKind == KIND_FILE) { + // Because this is an external piece of data, mType is one of kFileMime, + // kPNGImageMime, kJPEGImageMime, or kGIFImageMime. Some of these types + // are passed in as a nsIInputStream which must be converted to a + // dom::File before storing. + if (nsCOMPtr<nsIInputStream> istream = do_QueryInterface(data)) { + RefPtr<File> file = CreateFileFromInputStream(istream); + if (NS_WARN_IF(!file)) { + return; + } + data = do_QueryObject(file); + } + variant->SetAsISupports(data); + } else { + // We have an external piece of string data. Extract it and store it in the + // variant + MOZ_ASSERT(oldKind == KIND_STRING); + + nsCOMPtr<nsISupportsString> supportsstr = do_QueryInterface(data); + if (supportsstr) { + nsAutoString str; + supportsstr->GetData(str); + variant->SetAsAString(str); + } else { + nsCOMPtr<nsISupportsCString> supportscstr = do_QueryInterface(data); + if (supportscstr) { + nsAutoCString str; + supportscstr->GetData(str); + variant->SetAsACString(str); + } + } + } + + SetData(variant); + + if (oldKind != Kind()) { + NS_WARNING( + "Clipboard data provided by the OS does not match predicted kind"); + mDataTransfer->TypesListMayHaveChanged(); + } +} + +void DataTransferItem::GetType(nsAString& aType) { + // If we don't have a File, we can just put whatever our recorded internal + // type is. + if (Kind() != KIND_FILE) { + aType = mType; + return; + } + + // If we do have a File, then we need to look at our File object to discover + // what its mime type is. We can use the System Principal here, as this + // information should be avaliable even if the data is currently inaccessible + // (for example during a dragover). + // + // XXX: This seems inefficient, as it seems like we should be able to get this + // data without getting the entire File object, which may require talking to + // the OS. + ErrorResult rv; + RefPtr<File> file = GetAsFile(*nsContentUtils::GetSystemPrincipal(), rv); + MOZ_ASSERT(!rv.Failed(), "Failed to get file data with system principal"); + + // If we don't actually have a file, fall back to returning the internal type. + if (NS_WARN_IF(!file)) { + aType = mType; + return; + } + + file->GetType(aType); +} + +already_AddRefed<File> DataTransferItem::GetAsFile( + nsIPrincipal& aSubjectPrincipal, ErrorResult& aRv) { + // This is done even if we have an mCachedFile, as it performs the necessary + // permissions checks to ensure that we are allowed to access this type. + nsCOMPtr<nsIVariant> data = Data(&aSubjectPrincipal, aRv); + if (NS_WARN_IF(!data || aRv.Failed())) { + return nullptr; + } + + // We have to check our kind after getting the data, because if we have + // external data and the OS lied to us (which unfortunately does happen + // sometimes), then we might not have the same type of data as we did coming + // into this function. + if (NS_WARN_IF(mKind != KIND_FILE)) { + return nullptr; + } + + // Generate the dom::File from the stored data, caching it so that the + // same object is returned in the future. + if (!mCachedFile) { + nsCOMPtr<nsISupports> supports; + aRv = data->GetAsISupports(getter_AddRefs(supports)); + MOZ_ASSERT(!aRv.Failed() && supports, + "File objects should be stored as nsISupports variants"); + if (aRv.Failed() || !supports) { + return nullptr; + } + + if (RefPtr<Blob> blob = do_QueryObject(supports)) { + mCachedFile = blob->ToFile(); + } else { + nsCOMPtr<nsIGlobalObject> global = GetGlobalFromDataTransfer(); + if (NS_WARN_IF(!global)) { + return nullptr; + } + + if (nsCOMPtr<BlobImpl> blobImpl = do_QueryInterface(supports)) { + MOZ_ASSERT(blobImpl->IsFile()); + mCachedFile = File::Create(global, blobImpl); + if (NS_WARN_IF(!mCachedFile)) { + return nullptr; + } + } else if (nsCOMPtr<nsIFile> ifile = do_QueryInterface(supports)) { + mCachedFile = File::CreateFromFile(global, ifile); + if (NS_WARN_IF(!mCachedFile)) { + return nullptr; + } + } else if (nsCOMPtr<imgIContainer> img = do_QueryInterface(supports)) { + nsCOMPtr<imgITools> imgTools = + do_CreateInstance("@mozilla.org/image/tools;1"); + + nsCOMPtr<nsIInputStream> inputStream; + nsresult rv = imgTools->EncodeImage(img, "image/png"_ns, u""_ns, + getter_AddRefs(inputStream)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return nullptr; + } + + mCachedFile = CreateFileFromInputStream( + inputStream, "GenericImageNamePNG", u"image/png"_ns); + if (NS_WARN_IF(!mCachedFile)) { + return nullptr; + } + } else { + MOZ_ASSERT(false, "One of the above code paths should be taken"); + return nullptr; + } + } + } + + RefPtr<File> file = mCachedFile; + return file.forget(); +} + +already_AddRefed<FileSystemEntry> DataTransferItem::GetAsEntry( + nsIPrincipal& aSubjectPrincipal, ErrorResult& aRv) { + RefPtr<File> file = GetAsFile(aSubjectPrincipal, aRv); + if (NS_WARN_IF(aRv.Failed()) || !file) { + return nullptr; + } + + nsCOMPtr<nsIGlobalObject> global = GetGlobalFromDataTransfer(); + if (NS_WARN_IF(!global)) { + return nullptr; + } + + RefPtr<FileSystem> fs = FileSystem::Create(global); + RefPtr<FileSystemEntry> entry; + BlobImpl* impl = file->Impl(); + MOZ_ASSERT(impl); + + if (impl->IsDirectory()) { + nsAutoString fullpath; + impl->GetMozFullPathInternal(fullpath, aRv); + if (aRv.Failed()) { + aRv.SuppressException(); + return nullptr; + } + + nsCOMPtr<nsIFile> directoryFile; + // fullPath is already in unicode, we don't have to use + // NS_NewNativeLocalFile. + nsresult rv = + NS_NewLocalFile(fullpath, true, getter_AddRefs(directoryFile)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return nullptr; + } + + RefPtr<Directory> directory = Directory::Create(global, directoryFile); + entry = new FileSystemDirectoryEntry(global, directory, nullptr, fs); + } else { + entry = new FileSystemFileEntry(global, file, nullptr, fs); + } + + Sequence<RefPtr<FileSystemEntry>> entries; + if (!entries.AppendElement(entry, fallible)) { + return nullptr; + } + + fs->CreateRoot(entries); + return entry.forget(); +} + +already_AddRefed<File> DataTransferItem::CreateFileFromInputStream( + nsIInputStream* aStream) { + const char* key = nullptr; + for (uint32_t i = 0; i < ArrayLength(kFileMimeNameMap); ++i) { + if (mType.EqualsASCII(kFileMimeNameMap[i].mMimeName)) { + key = kFileMimeNameMap[i].mFileName; + break; + } + } + if (!key) { + MOZ_ASSERT_UNREACHABLE("Unsupported mime type"); + key = "GenericFileName"; + } + + return CreateFileFromInputStream(aStream, key, mType); +} + +already_AddRefed<File> DataTransferItem::CreateFileFromInputStream( + nsIInputStream* aStream, const char* aFileNameKey, + const nsAString& aContentType) { + nsAutoString fileName; + nsresult rv = nsContentUtils::GetLocalizedString( + nsContentUtils::eDOM_PROPERTIES, aFileNameKey, fileName); + if (NS_WARN_IF(NS_FAILED(rv))) { + return nullptr; + } + + uint64_t available; + void* data = nullptr; + rv = NS_ReadInputStreamToBuffer(aStream, &data, -1, &available); + if (NS_WARN_IF(NS_FAILED(rv))) { + return nullptr; + } + + nsCOMPtr<nsIGlobalObject> global = GetGlobalFromDataTransfer(); + if (NS_WARN_IF(!global)) { + return nullptr; + } + + return File::CreateMemoryFileWithLastModifiedNow(global, data, available, + fileName, aContentType); +} + +void DataTransferItem::GetAsString(FunctionStringCallback* aCallback, + nsIPrincipal& aSubjectPrincipal, + ErrorResult& aRv) { + if (!aCallback) { + return; + } + + // Theoretically this should be done inside of the runnable, as it might be an + // expensive operation on some systems, however we wouldn't get access to the + // NS_ERROR_DOM_SECURITY_ERROR messages which may be raised by this method. + nsCOMPtr<nsIVariant> data = Data(&aSubjectPrincipal, aRv); + if (NS_WARN_IF(!data || aRv.Failed())) { + return; + } + + // We have to check our kind after getting the data, because if we have + // external data and the OS lied to us (which unfortunately does happen + // sometimes), then we might not have the same type of data as we did coming + // into this function. + if (NS_WARN_IF(mKind != KIND_STRING)) { + return; + } + + nsAutoString stringData; + nsresult rv = data->GetAsAString(stringData); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + // Dispatch the callback to the main thread + class GASRunnable final : public Runnable { + public: + GASRunnable(FunctionStringCallback* aCallback, const nsAString& aStringData) + : mozilla::Runnable("GASRunnable"), + mCallback(aCallback), + mStringData(aStringData) {} + + // MOZ_CAN_RUN_SCRIPT_BOUNDARY until runnables are opted into + // MOZ_CAN_RUN_SCRIPT. See bug 1535398. + MOZ_CAN_RUN_SCRIPT_BOUNDARY + NS_IMETHOD Run() override { + ErrorResult rv; + mCallback->Call(mStringData, rv); + NS_WARNING_ASSERTION(!rv.Failed(), "callback failed"); + return rv.StealNSResult(); + } + + private: + const RefPtr<FunctionStringCallback> mCallback; + nsString mStringData; + }; + + RefPtr<GASRunnable> runnable = new GASRunnable(aCallback, stringData); + + // DataTransfer.mParent might be EventTarget, nsIGlobalObject, ClipboardEvent + // nsPIDOMWindowOuter, null + nsISupports* parent = mDataTransfer->GetParentObject(); + nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(parent); + if (parent && !global) { + if (nsCOMPtr<dom::EventTarget> target = do_QueryInterface(parent)) { + global = target->GetOwnerGlobal(); + } else if (RefPtr<Event> event = do_QueryObject(parent)) { + global = event->GetParentObject(); + } + } + if (global) { + rv = global->Dispatch(TaskCategory::Other, runnable.forget()); + } else { + rv = NS_DispatchToMainThread(runnable); + } + if (NS_FAILED(rv)) { + NS_WARNING( + "Dispatch to main thread Failed in " + "DataTransferItem::GetAsString!"); + } +} + +already_AddRefed<nsIVariant> DataTransferItem::DataNoSecurityCheck() { + if (!mData) { + FillInExternalData(); + } + nsCOMPtr<nsIVariant> data = mData; + return data.forget(); +} + +already_AddRefed<nsIVariant> DataTransferItem::Data(nsIPrincipal* aPrincipal, + ErrorResult& aRv) { + MOZ_ASSERT(aPrincipal); + + // If the inbound principal is system, we can skip the below checks, as + // they will trivially succeed. + if (aPrincipal->IsSystemPrincipal()) { + return DataNoSecurityCheck(); + } + + // We should not allow raw data to be accessed from a Protected DataTransfer. + // We don't prevent this access if the accessing document is Chrome. + if (mDataTransfer->IsProtected()) { + return nullptr; + } + + nsCOMPtr<nsIVariant> variant = DataNoSecurityCheck(); + + MOZ_ASSERT(!ChromeOnly(), + "Non-chrome code shouldn't see a ChromeOnly DataTransferItem"); + if (ChromeOnly()) { + aRv.Throw(NS_ERROR_DOM_SECURITY_ERR); + return nullptr; + } + + bool checkItemPrincipal = mDataTransfer->IsCrossDomainSubFrameDrop() || + (mDataTransfer->GetEventMessage() != eDrop && + mDataTransfer->GetEventMessage() != ePaste && + mDataTransfer->GetEventMessage() != eEditorInput); + + // Check if the caller is allowed to access the drag data. Callers with + // chrome privileges can always read the data. During the + // drop event, allow retrieving the data except in the case where the + // source of the drag is in a child frame of the caller. In that case, + // we only allow access to data of the same principal. During other events, + // only allow access to the data with the same principal. + // + // We don't want to fail with an exception in this siutation, rather we want + // to just pretend as though the stored data is "nullptr". This is consistent + // with Chrome's behavior and is less surprising for web applications which + // don't expect execptions to be raised when performing certain operations. + if (Principal() && checkItemPrincipal && !aPrincipal->Subsumes(Principal())) { + return nullptr; + } + + if (!variant) { + return nullptr; + } + + nsCOMPtr<nsISupports> data; + nsresult rv = variant->GetAsISupports(getter_AddRefs(data)); + if (NS_SUCCEEDED(rv) && data) { + nsCOMPtr<EventTarget> pt = do_QueryInterface(data); + if (pt) { + nsIGlobalObject* go = pt->GetOwnerGlobal(); + if (NS_WARN_IF(!go)) { + return nullptr; + } + + nsCOMPtr<nsIScriptObjectPrincipal> sp = do_QueryInterface(go); + MOZ_ASSERT(sp, "This cannot fail on the main thread."); + + nsIPrincipal* dataPrincipal = sp->GetPrincipal(); + if (NS_WARN_IF(!dataPrincipal || !aPrincipal->Equals(dataPrincipal))) { + return nullptr; + } + } + } + + return variant.forget(); +} + +already_AddRefed<nsIGlobalObject> +DataTransferItem::GetGlobalFromDataTransfer() { + nsCOMPtr<nsIGlobalObject> global; + // This is annoying, but DataTransfer may have various things as parent. + nsCOMPtr<EventTarget> target = + do_QueryInterface(mDataTransfer->GetParentObject()); + if (target) { + global = target->GetOwnerGlobal(); + } else { + RefPtr<Event> event = do_QueryObject(mDataTransfer->GetParentObject()); + if (event) { + global = event->GetParentObject(); + } + } + + return global.forget(); +} + +} // namespace mozilla::dom diff --git a/dom/events/DataTransferItem.h b/dom/events/DataTransferItem.h new file mode 100644 index 0000000000..3978aaa7a9 --- /dev/null +++ b/dom/events/DataTransferItem.h @@ -0,0 +1,137 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#ifndef mozilla_dom_DataTransferItem_h +#define mozilla_dom_DataTransferItem_h + +#include "mozilla/dom/DataTransfer.h" +#include "mozilla/dom/DOMString.h" +#include "mozilla/dom/File.h" + +namespace mozilla { +class ErrorResult; + +namespace dom { + +class DataTransfer; +class FileSystemEntry; +class FunctionStringCallback; + +class DataTransferItem final : public nsISupports, public nsWrapperCache { + public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(DataTransferItem); + + public: + // The spec only talks about the "file" and "string" kinds. Due to the Moz* + // APIs, it is possible to attach any type to a DataTransferItem, meaning that + // we can have other kinds then just FILE and STRING. These others are simply + // marked as "other" and can only be produced throug the Moz* APIs. + enum eKind { + KIND_FILE, + KIND_STRING, + KIND_OTHER, + }; + + DataTransferItem(DataTransfer* aDataTransfer, const nsAString& aType, + eKind aKind = KIND_OTHER) + : mIndex(0), + mChromeOnly(false), + mKind(aKind), + mType(aType), + mDataTransfer(aDataTransfer) { + MOZ_ASSERT(mDataTransfer, "Must be associated with a DataTransfer"); + } + + already_AddRefed<DataTransferItem> Clone(DataTransfer* aDataTransfer) const; + + virtual JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + void GetAsString(FunctionStringCallback* aCallback, + nsIPrincipal& aSubjectPrincipal, ErrorResult& aRv); + + void GetKind(nsAString& aKind) const { + switch (mKind) { + case KIND_FILE: + aKind = u"file"_ns; + return; + case KIND_STRING: + aKind = u"string"_ns; + return; + default: + aKind = u"other"_ns; + return; + } + } + + void GetInternalType(nsAString& aType) const { aType = mType; } + bool IsInternalType(const nsAString& aType) const { return aType == mType; } + + void GetType(nsAString& aType); + + eKind Kind() const { return mKind; } + + already_AddRefed<File> GetAsFile(nsIPrincipal& aSubjectPrincipal, + ErrorResult& aRv); + + already_AddRefed<FileSystemEntry> GetAsEntry(nsIPrincipal& aSubjectPrincipal, + ErrorResult& aRv); + + DataTransfer* GetParentObject() const { return mDataTransfer; } + + nsIPrincipal* Principal() const { return mPrincipal; } + void SetPrincipal(nsIPrincipal* aPrincipal) { mPrincipal = aPrincipal; } + + // @return cached data, if available. + // otherwise: if available, `mDataTransfer`'s transferable data. + // otherwise the data is retrieved from the clipboard (for + // paste events) or the drag session. + already_AddRefed<nsIVariant> DataNoSecurityCheck(); + // Data may return null if the clipboard state has changed since the type was + // detected. + already_AddRefed<nsIVariant> Data(nsIPrincipal* aPrincipal, ErrorResult& aRv); + + // Note: This can modify the mKind. Callers of this method must let the + // relevant DataTransfer know, because its types list can change as a result. + void SetData(nsIVariant* aData); + + uint32_t Index() const { return mIndex; } + void SetIndex(uint32_t aIndex) { mIndex = aIndex; } + void FillInExternalData(); + + bool ChromeOnly() const { return mChromeOnly; } + void SetChromeOnly(bool aChromeOnly) { mChromeOnly = aChromeOnly; } + + static eKind KindFromData(nsIVariant* aData); + + private: + ~DataTransferItem() = default; + already_AddRefed<File> CreateFileFromInputStream(nsIInputStream* aStream); + already_AddRefed<File> CreateFileFromInputStream( + nsIInputStream* aStream, const char* aFileNameKey, + const nsAString& aContentType); + + already_AddRefed<nsIGlobalObject> GetGlobalFromDataTransfer(); + + // The index in the 2d mIndexedItems array + uint32_t mIndex; + + bool mChromeOnly; + eKind mKind; + const nsString mType; + nsCOMPtr<nsIVariant> mData; + nsCOMPtr<nsIPrincipal> mPrincipal; + RefPtr<DataTransfer> mDataTransfer; + + // File cache for nsIFile application/x-moz-file entries. + RefPtr<File> mCachedFile; +}; + +} // namespace dom +} // namespace mozilla + +#endif /* mozilla_dom_DataTransferItem_h */ diff --git a/dom/events/DataTransferItemList.cpp b/dom/events/DataTransferItemList.cpp new file mode 100644 index 0000000000..f5c84f75f8 --- /dev/null +++ b/dom/events/DataTransferItemList.cpp @@ -0,0 +1,642 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#include "DataTransferItemList.h" + +#include "nsContentUtils.h" +#include "nsIGlobalObject.h" +#include "nsIScriptObjectPrincipal.h" +#include "nsIScriptGlobalObject.h" +#include "nsIScriptContext.h" +#include "nsQueryObject.h" +#include "nsVariant.h" +#include "mozilla/BasePrincipal.h" +#include "mozilla/ContentEvents.h" +#include "mozilla/EventForwards.h" +#include "mozilla/storage/Variant.h" +#include "mozilla/dom/DataTransferItemListBinding.h" + +namespace mozilla::dom { + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(DataTransferItemList, mDataTransfer, + mItems, mIndexedItems, mFiles) +NS_IMPL_CYCLE_COLLECTING_ADDREF(DataTransferItemList) +NS_IMPL_CYCLE_COLLECTING_RELEASE(DataTransferItemList) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(DataTransferItemList) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +JSObject* DataTransferItemList::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return DataTransferItemList_Binding::Wrap(aCx, this, aGivenProto); +} + +already_AddRefed<DataTransferItemList> DataTransferItemList::Clone( + DataTransfer* aDataTransfer) const { + RefPtr<DataTransferItemList> list = new DataTransferItemList(aDataTransfer); + + // We need to clone the mItems and mIndexedItems lists while keeping the same + // correspondences between the mIndexedItems and mItems lists (namely, if an + // item is in mIndexedItems, and mItems it must have the same new identity) + + // First, we copy over indexedItems, and clone every entry. Then, we go over + // mItems. For every entry, we use its mIndex property to locate it in + // mIndexedItems on the original DataTransferItemList, and then copy over the + // reference from the same index pair on the new DataTransferItemList + + list->mIndexedItems.SetLength(mIndexedItems.Length()); + list->mItems.SetLength(mItems.Length()); + + // Copy over mIndexedItems, cloning every entry + for (uint32_t i = 0; i < mIndexedItems.Length(); i++) { + const nsTArray<RefPtr<DataTransferItem>>& items = mIndexedItems[i]; + nsTArray<RefPtr<DataTransferItem>>& newItems = list->mIndexedItems[i]; + newItems.SetLength(items.Length()); + for (uint32_t j = 0; j < items.Length(); j++) { + newItems[j] = items[j]->Clone(aDataTransfer); + } + } + + // Copy over mItems, getting the actual entries from mIndexedItems + for (uint32_t i = 0; i < mItems.Length(); i++) { + uint32_t index = mItems[i]->Index(); + MOZ_ASSERT(index < mIndexedItems.Length()); + uint32_t subIndex = mIndexedItems[index].IndexOf(mItems[i]); + + // Copy over the reference + list->mItems[i] = list->mIndexedItems[index][subIndex]; + } + + return list.forget(); +} + +void DataTransferItemList::Remove(uint32_t aIndex, + nsIPrincipal& aSubjectPrincipal, + ErrorResult& aRv) { + if (mDataTransfer->IsReadOnly()) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return; + } + + if (aIndex >= Length()) { + return; + } + + ClearDataHelper(mItems[aIndex], aIndex, -1, aSubjectPrincipal, aRv); +} + +DataTransferItem* DataTransferItemList::IndexedGetter(uint32_t aIndex, + bool& aFound) const { + if (aIndex >= mItems.Length()) { + aFound = false; + return nullptr; + } + + MOZ_ASSERT(mItems[aIndex]); + aFound = true; + return mItems[aIndex]; +} + +uint32_t DataTransferItemList::MozItemCount() const { + uint32_t length = mIndexedItems.Length(); + // XXX: Compat hack - Index 0 always exists due to changes in internals, but + // if it is empty, scripts using the moz* APIs should see it as not existing. + if (length == 1 && mIndexedItems[0].IsEmpty()) { + return 0; + } + return length; +} + +void DataTransferItemList::Clear(nsIPrincipal& aSubjectPrincipal, + ErrorResult& aRv) { + if (NS_WARN_IF(mDataTransfer->IsReadOnly())) { + return; + } + + uint32_t count = Length(); + for (uint32_t i = 0; i < count; i++) { + // We always remove the last item first, to avoid moving items around in + // memory as much + Remove(Length() - 1, aSubjectPrincipal, aRv); + ENSURE_SUCCESS_VOID(aRv); + } + + MOZ_ASSERT(Length() == 0); +} + +DataTransferItem* DataTransferItemList::Add(const nsAString& aData, + const nsAString& aType, + nsIPrincipal& aSubjectPrincipal, + ErrorResult& aRv) { + if (NS_WARN_IF(mDataTransfer->IsReadOnly())) { + return nullptr; + } + + RefPtr<nsVariantCC> data(new nsVariantCC()); + data->SetAsAString(aData); + + nsAutoString format; + mDataTransfer->GetRealFormat(aType, format); + + if (!DataTransfer::PrincipalMaySetData(format, data, &aSubjectPrincipal)) { + aRv.Throw(NS_ERROR_DOM_SECURITY_ERR); + return nullptr; + } + + // We add the textual data to index 0. We set aInsertOnly to true, as we don't + // want to update an existing entry if it is already present, as per the spec. + RefPtr<DataTransferItem> item = + SetDataWithPrincipal(format, data, 0, &aSubjectPrincipal, + /* aInsertOnly = */ true, + /* aHidden = */ false, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + MOZ_ASSERT(item->Kind() != DataTransferItem::KIND_FILE); + + return item; +} + +DataTransferItem* DataTransferItemList::Add(File& aData, + nsIPrincipal& aSubjectPrincipal, + ErrorResult& aRv) { + if (mDataTransfer->IsReadOnly()) { + return nullptr; + } + + nsCOMPtr<nsISupports> supports = do_QueryObject(&aData); + nsCOMPtr<nsIWritableVariant> data = new nsVariantCC(); + data->SetAsISupports(supports); + + nsAutoString type; + aData.GetType(type); + + if (!DataTransfer::PrincipalMaySetData(type, data, &aSubjectPrincipal)) { + aRv.Throw(NS_ERROR_DOM_SECURITY_ERR); + return nullptr; + } + + // We need to add this as a new item, as multiple files can't exist in the + // same item in the Moz DataTransfer layout. It will be appended at the end of + // the internal specced layout. + uint32_t index = mIndexedItems.Length(); + RefPtr<DataTransferItem> item = + SetDataWithPrincipal(type, data, index, &aSubjectPrincipal, + /* aInsertOnly = */ true, + /* aHidden = */ false, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + MOZ_ASSERT(item->Kind() == DataTransferItem::KIND_FILE); + + return item; +} + +already_AddRefed<FileList> DataTransferItemList::Files( + nsIPrincipal* aPrincipal) { + // The DataTransfer can hold data with varying principals, coming from + // different windows. This means that permissions checks need to be made when + // accessing data from the DataTransfer. With the accessor methods, this is + // checked by DataTransferItem::Data(), however with files, we keep a cached + // live copy of the files list for spec compliance. + // + // A DataTransfer is only exposed to one webpage, chrome code and expanded + // principals related to WebExtensions content scripts or user scripts. + // The chrome code should be able to see all files on the DataTransfer, while + // the webpage and WebExtensions content scripts and user scripts should only + // be able to see the files they can see. + // + // As chrome code doesn't need as strict spec compliance as web visible code, + // we generate a new FileList object every time you access the Files list from + // chrome code, but re-use the cached one when accessing from content code. + // + // For WebExtensions content scripts (expanded principals subsuming both + // the attached web page principal and the extension principal) and + // WebExtensions user scripts (expanded principals subsuming the attached + // web page principal but not the extension principal) we also don't cache + // the FileList as for chrome code (because the webpage principal and other + // extension content scripts/user scripts principals would not be able to + // access the cached FileList when accessed by a different expanded principal + // first, see Bug 1707214). + // + // It is not legal to expose an identical DataTransfer object is to multiple + // different principals without using the `Clone` method or similar to copy it + // first. If that happens, this method will assert, and return nullptr in + // release builds. If this functionality is required in the future, a more + // advanced caching mechanism for the FileList objects will be required. + RefPtr<FileList> files; + if (aPrincipal->IsSystemPrincipal() || + // WebExtensions content scripts and user scripts. + nsContentUtils::IsExpandedPrincipal(aPrincipal)) { + files = new FileList(mDataTransfer); + GenerateFiles(files, aPrincipal); + return files.forget(); + } + + if (!mFiles) { + mFiles = new FileList(mDataTransfer); + mFilesPrincipal = aPrincipal; + RegenerateFiles(); + } + + if (!aPrincipal->Subsumes(mFilesPrincipal)) { + MOZ_ASSERT(false, + "This DataTransfer should only be accessed by the system " + "and a single principal"); + return nullptr; + } + + files = mFiles; + return files.forget(); +} + +void DataTransferItemList::MozRemoveByTypeAt(const nsAString& aType, + uint32_t aIndex, + nsIPrincipal& aSubjectPrincipal, + ErrorResult& aRv) { + if (NS_WARN_IF(mDataTransfer->IsReadOnly() || + aIndex >= mIndexedItems.Length())) { + return; + } + + bool removeAll = aType.IsEmpty(); + + nsTArray<RefPtr<DataTransferItem>>& items = mIndexedItems[aIndex]; + uint32_t count = items.Length(); + // We remove the last item of the list repeatedly - that way we don't + // have to worry about modifying the loop iterator + if (removeAll) { + for (uint32_t i = 0; i < count; ++i) { + uint32_t index = items.Length() - 1; + MOZ_ASSERT(index == count - i - 1); + + ClearDataHelper(items[index], -1, index, aSubjectPrincipal, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + } + + // items is no longer a valid reference, as removing the last element from + // it via ClearDataHelper invalidated it. so we can't MOZ_ASSERT that the + // length is now 0. + return; + } + + for (uint32_t i = 0; i < count; ++i) { + // NOTE: As this is a moz-prefixed API, it works based on internal types. + nsAutoString type; + items[i]->GetInternalType(type); + if (type == aType) { + ClearDataHelper(items[i], -1, i, aSubjectPrincipal, aRv); + return; + } + } +} + +DataTransferItem* DataTransferItemList::MozItemByTypeAt(const nsAString& aType, + uint32_t aIndex) { + if (NS_WARN_IF(aIndex >= mIndexedItems.Length())) { + return nullptr; + } + + uint32_t count = mIndexedItems[aIndex].Length(); + for (uint32_t i = 0; i < count; i++) { + RefPtr<DataTransferItem> item = mIndexedItems[aIndex][i]; + // NOTE: As this is a moz-prefixed API it works on internal types + nsString type; + item->GetInternalType(type); + if (type.Equals(aType)) { + return item; + } + } + + return nullptr; +} + +already_AddRefed<DataTransferItem> DataTransferItemList::SetDataWithPrincipal( + const nsAString& aType, nsIVariant* aData, uint32_t aIndex, + nsIPrincipal* aPrincipal, bool aInsertOnly, bool aHidden, + ErrorResult& aRv) { + if (aIndex < mIndexedItems.Length()) { + nsTArray<RefPtr<DataTransferItem>>& items = mIndexedItems[aIndex]; + uint32_t count = items.Length(); + for (uint32_t i = 0; i < count; i++) { + RefPtr<DataTransferItem> item = items[i]; + nsString type; + item->GetInternalType(type); + if (type.Equals(aType)) { + if (NS_WARN_IF(aInsertOnly)) { + aRv.Throw(NS_ERROR_DOM_NOT_SUPPORTED_ERR); + return nullptr; + } + + // don't allow replacing data that has a stronger principal + bool subsumes; + if (NS_WARN_IF(item->Principal() && aPrincipal && + (NS_FAILED(aPrincipal->Subsumes(item->Principal(), + &subsumes)) || + !subsumes))) { + aRv.Throw(NS_ERROR_DOM_SECURITY_ERR); + return nullptr; + } + item->SetPrincipal(aPrincipal); + + DataTransferItem::eKind oldKind = item->Kind(); + item->SetData(aData); + + mDataTransfer->TypesListMayHaveChanged(); + + if (aIndex != 0) { + // If the item changes from being a file to not a file or vice-versa, + // its presence in the mItems array may need to change. + if (item->Kind() == DataTransferItem::KIND_FILE && + oldKind != DataTransferItem::KIND_FILE) { + // not file => file + mItems.AppendElement(item); + } else if (item->Kind() != DataTransferItem::KIND_FILE && + oldKind == DataTransferItem::KIND_FILE) { + // file => not file + mItems.RemoveElement(item); + } + } + + // Regenerate the Files array if we have modified a file's status + if (item->Kind() == DataTransferItem::KIND_FILE || + oldKind == DataTransferItem::KIND_FILE) { + RegenerateFiles(); + } + + return item.forget(); + } + } + } else { + // Make sure that we aren't adding past the end of the mIndexedItems array. + // XXX Should this be a MOZ_ASSERT instead? + aIndex = mIndexedItems.Length(); + } + + // Add the new item + RefPtr<DataTransferItem> item = + AppendNewItem(aIndex, aType, aData, aPrincipal, aHidden); + + if (item->Kind() == DataTransferItem::KIND_FILE) { + RegenerateFiles(); + } + + return item.forget(); +} + +DataTransferItem* DataTransferItemList::AppendNewItem(uint32_t aIndex, + const nsAString& aType, + nsIVariant* aData, + nsIPrincipal* aPrincipal, + bool aHidden) { + if (mIndexedItems.Length() <= aIndex) { + MOZ_ASSERT(mIndexedItems.Length() == aIndex); + mIndexedItems.AppendElement(); + } + RefPtr<DataTransferItem> item = new DataTransferItem(mDataTransfer, aType); + item->SetIndex(aIndex); + item->SetPrincipal(aPrincipal); + item->SetData(aData); + item->SetChromeOnly(aHidden); + + mIndexedItems[aIndex].AppendElement(item); + + // We only want to add the item to the main mItems list if the index we are + // adding to is 0, or the item we are adding is a file. If we add an item + // which is not a file to a non-zero index, invariants could be broken. + // (namely the invariant that there are not 2 non-file entries in the items + // array with the same type). + // + // We also want to update our DataTransfer's type list any time we're adding a + // KIND_FILE item, or an item at index 0. + if (item->Kind() == DataTransferItem::KIND_FILE || aIndex == 0) { + if (!aHidden) { + mItems.AppendElement(item); + } + mDataTransfer->TypesListMayHaveChanged(); + } + + return item; +} + +void DataTransferItemList::GetTypes(nsTArray<nsString>& aTypes, + CallerType aCallerType) const { + MOZ_ASSERT(aTypes.IsEmpty()); + + if (mIndexedItems.IsEmpty()) { + return; + } + + bool foundFile = false; + for (const RefPtr<DataTransferItem>& item : mIndexedItems[0]) { + MOZ_ASSERT(item); + + // XXX Why don't we check the caller type with item's permission only + // for "Files"? + if (!foundFile) { + foundFile = item->Kind() == DataTransferItem::KIND_FILE; + } + + if (item->ChromeOnly() && aCallerType != CallerType::System) { + continue; + } + + // NOTE: The reason why we get the internal type here is because we want + // kFileMime to appear in the types list for backwards compatibility + // reasons. + nsAutoString type; + item->GetInternalType(type); + if (item->Kind() != DataTransferItem::KIND_FILE || + type.EqualsASCII(kFileMime)) { + aTypes.AppendElement(type); + } + } + + // Additional files will be added at a non-zero index. + if (!foundFile) { + for (uint32_t i = 1; i < mIndexedItems.Length(); i++) { + for (const RefPtr<DataTransferItem>& item : mIndexedItems[i]) { + MOZ_ASSERT(item); + + foundFile = item->Kind() == DataTransferItem::KIND_FILE; + if (foundFile) { + break; + } + } + } + } + + if (foundFile) { + aTypes.AppendElement(u"Files"_ns); + } +} + +bool DataTransferItemList::HasType(const nsAString& aType) const { + MOZ_ASSERT(!aType.EqualsASCII("Files"), "Use HasFile instead"); + if (mIndexedItems.IsEmpty()) { + return false; + } + + for (const RefPtr<DataTransferItem>& item : mIndexedItems[0]) { + if (item->IsInternalType(aType)) { + return true; + } + } + return false; +} + +bool DataTransferItemList::HasFile() const { + if (mIndexedItems.IsEmpty()) { + return false; + } + + for (const RefPtr<DataTransferItem>& item : mIndexedItems[0]) { + if (item->Kind() == DataTransferItem::KIND_FILE) { + return true; + } + } + return false; +} + +const nsTArray<RefPtr<DataTransferItem>>* DataTransferItemList::MozItemsAt( + uint32_t aIndex) // -- INDEXED +{ + if (aIndex >= mIndexedItems.Length()) { + return nullptr; + } + + return &mIndexedItems[aIndex]; +} + +void DataTransferItemList::PopIndexZero() { + MOZ_ASSERT(mIndexedItems.Length() > 1); + MOZ_ASSERT(mIndexedItems[0].IsEmpty()); + + mIndexedItems.RemoveElementAt(0); + + // Update the index of every element which has now been shifted + for (uint32_t i = 0; i < mIndexedItems.Length(); i++) { + nsTArray<RefPtr<DataTransferItem>>& items = mIndexedItems[i]; + for (uint32_t j = 0; j < items.Length(); j++) { + items[j]->SetIndex(i); + } + } +} + +void DataTransferItemList::ClearAllItems() { + // We always need to have index 0, so don't delete that one + mItems.Clear(); + mIndexedItems.Clear(); + mIndexedItems.SetLength(1); + mDataTransfer->TypesListMayHaveChanged(); + + // Re-generate files (into an empty list) + RegenerateFiles(); +} + +void DataTransferItemList::ClearDataHelper(DataTransferItem* aItem, + uint32_t aIndexHint, + uint32_t aMozOffsetHint, + nsIPrincipal& aSubjectPrincipal, + ErrorResult& aRv) { + MOZ_ASSERT(aItem); + if (NS_WARN_IF(mDataTransfer->IsReadOnly())) { + return; + } + + if (aItem->Principal() && !aSubjectPrincipal.Subsumes(aItem->Principal())) { + aRv.Throw(NS_ERROR_DOM_SECURITY_ERR); + return; + } + + // Check if the aIndexHint is actually the index, and then remove the item + // from aItems + bool found; + if (IndexedGetter(aIndexHint, found) == aItem) { + mItems.RemoveElementAt(aIndexHint); + } else { + mItems.RemoveElement(aItem); + } + + // Check if the aMozIndexHint and aMozOffsetHint are actually the index and + // offset, and then remove them from mIndexedItems + MOZ_ASSERT(aItem->Index() < mIndexedItems.Length()); + nsTArray<RefPtr<DataTransferItem>>& items = mIndexedItems[aItem->Index()]; + if (aMozOffsetHint < items.Length() && aItem == items[aMozOffsetHint]) { + items.RemoveElementAt(aMozOffsetHint); + } else { + items.RemoveElement(aItem); + } + + mDataTransfer->TypesListMayHaveChanged(); + + // Check if we should remove the index. We never remove index 0. + if (items.Length() == 0 && aItem->Index() != 0) { + mIndexedItems.RemoveElementAt(aItem->Index()); + + // Update the index of every element which has now been shifted + for (uint32_t i = aItem->Index(); i < mIndexedItems.Length(); i++) { + nsTArray<RefPtr<DataTransferItem>>& items = mIndexedItems[i]; + for (uint32_t j = 0; j < items.Length(); j++) { + items[j]->SetIndex(i); + } + } + } + + // Give the removed item the invalid index + aItem->SetIndex(-1); + + if (aItem->Kind() == DataTransferItem::KIND_FILE) { + RegenerateFiles(); + } +} + +void DataTransferItemList::RegenerateFiles() { + // We don't want to regenerate the files list unless we already have a files + // list. That way we can avoid the unnecessary work if the user never touches + // the files list. + if (mFiles) { + // We clear the list rather than performing smaller updates, because it + // simplifies the logic greatly on this code path, which should be very + // infrequently used. + mFiles->Clear(); + + DataTransferItemList::GenerateFiles(mFiles, mFilesPrincipal); + } +} + +void DataTransferItemList::GenerateFiles(FileList* aFiles, + nsIPrincipal* aFilesPrincipal) { + MOZ_ASSERT(aFiles); + MOZ_ASSERT(aFilesPrincipal); + + // For non-system principals, the Files list should be empty if the + // DataTransfer is protected. + if (!aFilesPrincipal->IsSystemPrincipal() && mDataTransfer->IsProtected()) { + return; + } + + uint32_t count = Length(); + for (uint32_t i = 0; i < count; i++) { + bool found; + RefPtr<DataTransferItem> item = IndexedGetter(i, found); + MOZ_ASSERT(found); + + if (item->Kind() == DataTransferItem::KIND_FILE) { + RefPtr<File> file = item->GetAsFile(*aFilesPrincipal, IgnoreErrors()); + if (NS_WARN_IF(!file)) { + continue; + } + aFiles->Append(file); + } + } +} + +} // namespace mozilla::dom diff --git a/dom/events/DataTransferItemList.h b/dom/events/DataTransferItemList.h new file mode 100644 index 0000000000..82977999db --- /dev/null +++ b/dom/events/DataTransferItemList.h @@ -0,0 +1,125 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#ifndef mozilla_dom_DataTransferItemList_h +#define mozilla_dom_DataTransferItemList_h + +#include "mozilla/dom/DataTransfer.h" +#include "mozilla/dom/DataTransferItem.h" +#include "mozilla/dom/FileList.h" + +class nsIVariant; + +namespace mozilla::dom { + +class DataTransfer; +class DataTransferItem; + +class DataTransferItemList final : public nsISupports, public nsWrapperCache { + public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(DataTransferItemList); + + explicit DataTransferItemList(DataTransfer* aDataTransfer) + : mDataTransfer(aDataTransfer) { + MOZ_ASSERT(aDataTransfer); + // We always allocate an index 0 in our DataTransferItemList. This is done + // in order to maintain the invariants according to the spec. Mainly, within + // the spec's list, there is intended to be a single copy of each mime type, + // for string typed items. File typed items are allowed to have duplicates. + // In the old moz* system, this was modeled by having multiple indexes, each + // of which was independent. Files were fetched from all indexes, but + // strings were only fetched from the first index. In order to maintain this + // correlation and avoid breaking code with the new changes, index 0 is now + // always present and used to store strings, and all file items are given + // their own index starting at index 1. + mIndexedItems.SetLength(1); + } + + already_AddRefed<DataTransferItemList> Clone( + DataTransfer* aDataTransfer) const; + + virtual JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + uint32_t Length() const { return mItems.Length(); }; + + DataTransferItem* Add(const nsAString& aData, const nsAString& aType, + nsIPrincipal& aSubjectPrincipal, ErrorResult& rv); + DataTransferItem* Add(File& aData, nsIPrincipal& aSubjectPrincipal, + ErrorResult& aRv); + + void Remove(uint32_t aIndex, nsIPrincipal& aSubjectPrincipal, + ErrorResult& aRv); + + DataTransferItem* IndexedGetter(uint32_t aIndex, bool& aFound) const; + + DataTransfer* GetParentObject() const { return mDataTransfer; } + + void Clear(nsIPrincipal& aSubjectPrincipal, ErrorResult& aRv); + + // @param aHidden true, iff the item should be hidden from non-chrome code. + already_AddRefed<DataTransferItem> SetDataWithPrincipal( + const nsAString& aType, nsIVariant* aData, uint32_t aIndex, + nsIPrincipal* aPrincipal, bool aInsertOnly, bool aHidden, + ErrorResult& aRv); + + already_AddRefed<FileList> Files(nsIPrincipal* aPrincipal); + + // Moz-style helper methods for interacting with the stored data + void MozRemoveByTypeAt(const nsAString& aType, uint32_t aIndex, + nsIPrincipal& aSubjectPrincipal, ErrorResult& aRv); + DataTransferItem* MozItemByTypeAt(const nsAString& aType, uint32_t aIndex); + const nsTArray<RefPtr<DataTransferItem>>* MozItemsAt(uint32_t aIndex); + uint32_t MozItemCount() const; + + // Causes everything in indexes above 0 to shift down one index. + void PopIndexZero(); + + // Delete every item in the DataTransferItemList, without checking for + // permissions or read-only status (for internal use only). + void ClearAllItems(); + + void GetTypes(nsTArray<nsString>& aTypes, CallerType aCallerType) const; + bool HasType(const nsAString& aType) const; + bool HasFile() const; + + private: + void ClearDataHelper(DataTransferItem* aItem, uint32_t aIndexHint, + uint32_t aMozOffsetHint, nsIPrincipal& aSubjectPrincipal, + ErrorResult& aRv); + + // @param aHidden true, iff the item should be hidden from non-chrome code. + DataTransferItem* AppendNewItem(uint32_t aIndex, const nsAString& aType, + nsIVariant* aData, nsIPrincipal* aPrincipal, + bool aHidden); + void RegenerateFiles(); + void GenerateFiles(FileList* aFiles, nsIPrincipal* aFilesPrincipal); + + ~DataTransferItemList() = default; + + RefPtr<DataTransfer> mDataTransfer; + RefPtr<FileList> mFiles; + // The principal for which mFiles is cached + nsCOMPtr<nsIPrincipal> mFilesPrincipal; + // mItems is the list of items that corresponds to the spec concept of a + // DataTransferItemList. That is, this is the thing the spec's indexed getter + // operates on. The items in here are a subset of the items present in the + // arrays that live in mIndexedItems. + nsTArray<RefPtr<DataTransferItem>> mItems; + // mIndexedItems represents all our items. For any given index, all items at + // that index have different types in the GetType() sense. That means that + // representing multiple items with the same type (e.g. multiple files) + // requires using multiple indices. + // + // There is always a (possibly empty) list of items at index 0, so + // mIndexedItems.Length() >= 1 at all times. + nsTArray<nsTArray<RefPtr<DataTransferItem>>> mIndexedItems; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_DataTransferItemList_h diff --git a/dom/events/DeviceMotionEvent.cpp b/dom/events/DeviceMotionEvent.cpp new file mode 100644 index 0000000000..73fcc30867 --- /dev/null +++ b/dom/events/DeviceMotionEvent.cpp @@ -0,0 +1,130 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#include "mozilla/dom/DeviceMotionEvent.h" +#include "nsContentUtils.h" + +namespace mozilla::dom { + +/****************************************************************************** + * DeviceMotionEvent + *****************************************************************************/ + +NS_IMPL_CYCLE_COLLECTION_INHERITED(DeviceMotionEvent, Event, mAcceleration, + mAccelerationIncludingGravity, mRotationRate) + +NS_IMPL_ADDREF_INHERITED(DeviceMotionEvent, Event) +NS_IMPL_RELEASE_INHERITED(DeviceMotionEvent, Event) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(DeviceMotionEvent) +NS_INTERFACE_MAP_END_INHERITING(Event) + +void DeviceMotionEvent::InitDeviceMotionEvent( + const nsAString& aType, bool aCanBubble, bool aCancelable, + const DeviceAccelerationInit& aAcceleration, + const DeviceAccelerationInit& aAccelIncludingGravity, + const DeviceRotationRateInit& aRotationRate, + const Nullable<double>& aInterval) { + InitDeviceMotionEvent(aType, aCanBubble, aCancelable, aAcceleration, + aAccelIncludingGravity, aRotationRate, aInterval, + Nullable<uint64_t>()); +} + +void DeviceMotionEvent::InitDeviceMotionEvent( + const nsAString& aType, bool aCanBubble, bool aCancelable, + const DeviceAccelerationInit& aAcceleration, + const DeviceAccelerationInit& aAccelIncludingGravity, + const DeviceRotationRateInit& aRotationRate, + const Nullable<double>& aInterval, const Nullable<uint64_t>& aTimeStamp) { + NS_ENSURE_TRUE_VOID(!mEvent->mFlags.mIsBeingDispatched); + + Event::InitEvent(aType, aCanBubble, aCancelable); + + mAcceleration = new DeviceAcceleration(this, aAcceleration.mX, + aAcceleration.mY, aAcceleration.mZ); + + mAccelerationIncludingGravity = new DeviceAcceleration( + this, aAccelIncludingGravity.mX, aAccelIncludingGravity.mY, + aAccelIncludingGravity.mZ); + + mRotationRate = new DeviceRotationRate( + this, aRotationRate.mAlpha, aRotationRate.mBeta, aRotationRate.mGamma); + mInterval = aInterval; + if (!aTimeStamp.IsNull()) { + static mozilla::TimeStamp sInitialNow = mozilla::TimeStamp::Now(); + static uint64_t sInitialEventTime = aTimeStamp.Value(); + mEvent->mTimeStamp = + sInitialNow + mozilla::TimeDuration::FromMicroseconds( + aTimeStamp.Value() - sInitialEventTime); + } +} + +already_AddRefed<DeviceMotionEvent> DeviceMotionEvent::Constructor( + const GlobalObject& aGlobal, const nsAString& aType, + const DeviceMotionEventInit& aEventInitDict) { + nsCOMPtr<EventTarget> t = do_QueryInterface(aGlobal.GetAsSupports()); + RefPtr<DeviceMotionEvent> e = new DeviceMotionEvent(t, nullptr, nullptr); + e->InitEvent(aType, aEventInitDict.mBubbles, aEventInitDict.mCancelable); + bool trusted = e->Init(t); + + e->mAcceleration = new DeviceAcceleration(e, aEventInitDict.mAcceleration.mX, + aEventInitDict.mAcceleration.mY, + aEventInitDict.mAcceleration.mZ); + + e->mAccelerationIncludingGravity = + new DeviceAcceleration(e, aEventInitDict.mAccelerationIncludingGravity.mX, + aEventInitDict.mAccelerationIncludingGravity.mY, + aEventInitDict.mAccelerationIncludingGravity.mZ); + + e->mRotationRate = new DeviceRotationRate( + e, aEventInitDict.mRotationRate.mAlpha, + aEventInitDict.mRotationRate.mBeta, aEventInitDict.mRotationRate.mGamma); + + e->mInterval = aEventInitDict.mInterval; + e->SetTrusted(trusted); + e->SetComposed(aEventInitDict.mComposed); + return e.forget(); +} + +/****************************************************************************** + * DeviceAcceleration + *****************************************************************************/ + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(DeviceAcceleration, mOwner) + +DeviceAcceleration::DeviceAcceleration(DeviceMotionEvent* aOwner, + const Nullable<double>& aX, + const Nullable<double>& aY, + const Nullable<double>& aZ) + : mOwner(aOwner), mX(aX), mY(aY), mZ(aZ) {} + +DeviceAcceleration::~DeviceAcceleration() = default; + +/****************************************************************************** + * DeviceRotationRate + *****************************************************************************/ + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(DeviceRotationRate, mOwner) + +DeviceRotationRate::DeviceRotationRate(DeviceMotionEvent* aOwner, + const Nullable<double>& aAlpha, + const Nullable<double>& aBeta, + const Nullable<double>& aGamma) + : mOwner(aOwner), mAlpha(aAlpha), mBeta(aBeta), mGamma(aGamma) {} + +DeviceRotationRate::~DeviceRotationRate() = default; + +} // namespace mozilla::dom + +using namespace mozilla; +using namespace mozilla::dom; + +already_AddRefed<DeviceMotionEvent> NS_NewDOMDeviceMotionEvent( + EventTarget* aOwner, nsPresContext* aPresContext, WidgetEvent* aEvent) { + RefPtr<DeviceMotionEvent> it = + new DeviceMotionEvent(aOwner, aPresContext, aEvent); + return it.forget(); +} diff --git a/dom/events/DeviceMotionEvent.h b/dom/events/DeviceMotionEvent.h new file mode 100644 index 0000000000..3e0debba88 --- /dev/null +++ b/dom/events/DeviceMotionEvent.h @@ -0,0 +1,129 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#ifndef mozilla_dom_DeviceMotionEvent_h_ +#define mozilla_dom_DeviceMotionEvent_h_ + +#include "mozilla/Attributes.h" +#include "mozilla/dom/DeviceMotionEventBinding.h" +#include "mozilla/dom/Event.h" + +namespace mozilla::dom { + +class DeviceRotationRate final : public nsWrapperCache { + public: + NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(DeviceRotationRate) + NS_DECL_CYCLE_COLLECTION_NATIVE_WRAPPERCACHE_CLASS(DeviceRotationRate) + + DeviceRotationRate(DeviceMotionEvent* aOwner, const Nullable<double>& aAlpha, + const Nullable<double>& aBeta, + const Nullable<double>& aGamma); + + DeviceMotionEvent* GetParentObject() const { return mOwner; } + + virtual JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override { + return DeviceRotationRate_Binding::Wrap(aCx, this, aGivenProto); + } + + Nullable<double> GetAlpha() const { return mAlpha; } + Nullable<double> GetBeta() const { return mBeta; } + Nullable<double> GetGamma() const { return mGamma; } + + private: + ~DeviceRotationRate(); + + protected: + RefPtr<DeviceMotionEvent> mOwner; + Nullable<double> mAlpha, mBeta, mGamma; +}; + +class DeviceAcceleration final : public nsWrapperCache { + public: + NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(DeviceAcceleration) + NS_DECL_CYCLE_COLLECTION_NATIVE_WRAPPERCACHE_CLASS(DeviceAcceleration) + + DeviceAcceleration(DeviceMotionEvent* aOwner, const Nullable<double>& aX, + const Nullable<double>& aY, const Nullable<double>& aZ); + + DeviceMotionEvent* GetParentObject() const { return mOwner; } + + virtual JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override { + return DeviceAcceleration_Binding::Wrap(aCx, this, aGivenProto); + } + + Nullable<double> GetX() const { return mX; } + Nullable<double> GetY() const { return mY; } + Nullable<double> GetZ() const { return mZ; } + + private: + ~DeviceAcceleration(); + + protected: + RefPtr<DeviceMotionEvent> mOwner; + Nullable<double> mX, mY, mZ; +}; + +class DeviceMotionEvent final : public Event { + public: + DeviceMotionEvent(EventTarget* aOwner, nsPresContext* aPresContext, + WidgetEvent* aEvent) + : Event(aOwner, aPresContext, aEvent) {} + + NS_DECL_ISUPPORTS_INHERITED + + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(DeviceMotionEvent, Event) + + virtual JSObject* WrapObjectInternal( + JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override { + return DeviceMotionEvent_Binding::Wrap(aCx, this, aGivenProto); + } + + DeviceAcceleration* GetAcceleration() const { return mAcceleration; } + + DeviceAcceleration* GetAccelerationIncludingGravity() const { + return mAccelerationIncludingGravity; + } + + DeviceRotationRate* GetRotationRate() const { return mRotationRate; } + + Nullable<double> GetInterval() const { return mInterval; } + + void InitDeviceMotionEvent( + const nsAString& aType, bool aCanBubble, bool aCancelable, + const DeviceAccelerationInit& aAcceleration, + const DeviceAccelerationInit& aAccelerationIncludingGravity, + const DeviceRotationRateInit& aRotationRate, + const Nullable<double>& aInterval); + + void InitDeviceMotionEvent( + const nsAString& aType, bool aCanBubble, bool aCancelable, + const DeviceAccelerationInit& aAcceleration, + const DeviceAccelerationInit& aAccelerationIncludingGravity, + const DeviceRotationRateInit& aRotationRate, + const Nullable<double>& aInterval, const Nullable<uint64_t>& aTimeStamp); + + static already_AddRefed<DeviceMotionEvent> Constructor( + const GlobalObject& aGlobal, const nsAString& aType, + const DeviceMotionEventInit& aEventInitDict); + + protected: + ~DeviceMotionEvent() = default; + + RefPtr<DeviceAcceleration> mAcceleration; + RefPtr<DeviceAcceleration> mAccelerationIncludingGravity; + RefPtr<DeviceRotationRate> mRotationRate; + Nullable<double> mInterval; +}; + +} // namespace mozilla::dom + +already_AddRefed<mozilla::dom::DeviceMotionEvent> NS_NewDOMDeviceMotionEvent( + mozilla::dom::EventTarget* aOwner, nsPresContext* aPresContext, + mozilla::WidgetEvent* aEvent); + +#endif // mozilla_dom_DeviceMotionEvent_h_ diff --git a/dom/events/DragEvent.cpp b/dom/events/DragEvent.cpp new file mode 100644 index 0000000000..aa576e2f8e --- /dev/null +++ b/dom/events/DragEvent.cpp @@ -0,0 +1,98 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#include "mozilla/dom/DragEvent.h" +#include "mozilla/dom/MouseEventBinding.h" +#include "mozilla/MouseEvents.h" +#include "nsContentUtils.h" +#include "prtime.h" + +namespace mozilla::dom { + +DragEvent::DragEvent(EventTarget* aOwner, nsPresContext* aPresContext, + WidgetDragEvent* aEvent) + : MouseEvent( + aOwner, aPresContext, + aEvent ? aEvent : new WidgetDragEvent(false, eVoidEvent, nullptr)) { + if (aEvent) { + mEventIsInternal = false; + } else { + mEventIsInternal = true; + mEvent->mRefPoint = LayoutDeviceIntPoint(0, 0); + mEvent->AsMouseEvent()->mInputSource = + MouseEvent_Binding::MOZ_SOURCE_UNKNOWN; + } +} + +void DragEvent::InitDragEvent(const nsAString& aType, bool aCanBubble, + bool aCancelable, nsGlobalWindowInner* aView, + int32_t aDetail, int32_t aScreenX, + int32_t aScreenY, int32_t aClientX, + int32_t aClientY, bool aCtrlKey, bool aAltKey, + bool aShiftKey, bool aMetaKey, uint16_t aButton, + EventTarget* aRelatedTarget, + DataTransfer* aDataTransfer) { + NS_ENSURE_TRUE_VOID(!mEvent->mFlags.mIsBeingDispatched); + + MouseEvent::InitMouseEvent(aType, aCanBubble, aCancelable, aView, aDetail, + aScreenX, aScreenY, aClientX, aClientY, aCtrlKey, + aAltKey, aShiftKey, aMetaKey, aButton, + aRelatedTarget); + if (mEventIsInternal) { + mEvent->AsDragEvent()->mDataTransfer = aDataTransfer; + } +} + +DataTransfer* DragEvent::GetDataTransfer() { + // the dataTransfer field of the event caches the DataTransfer associated + // with the drag. It is initialized when an attempt is made to retrieve it + // rather that when the event is created to avoid duplicating the data when + // no listener ever uses it. + if (!mEvent || mEvent->mClass != eDragEventClass) { + NS_WARNING("Tried to get dataTransfer from non-drag event!"); + return nullptr; + } + + WidgetDragEvent* dragEvent = mEvent->AsDragEvent(); + // for synthetic events, just use the supplied data transfer object even if + // null + if (!mEventIsInternal) { + nsresult rv = nsContentUtils::SetDataTransferInEvent(dragEvent); + NS_ENSURE_SUCCESS(rv, nullptr); + } + + return dragEvent->mDataTransfer; +} + +// static +already_AddRefed<DragEvent> DragEvent::Constructor( + const GlobalObject& aGlobal, const nsAString& aType, + const DragEventInit& aParam) { + nsCOMPtr<EventTarget> t = do_QueryInterface(aGlobal.GetAsSupports()); + RefPtr<DragEvent> e = new DragEvent(t, nullptr, nullptr); + bool trusted = e->Init(t); + e->InitDragEvent(aType, aParam.mBubbles, aParam.mCancelable, aParam.mView, + aParam.mDetail, aParam.mScreenX, aParam.mScreenY, + aParam.mClientX, aParam.mClientY, aParam.mCtrlKey, + aParam.mAltKey, aParam.mShiftKey, aParam.mMetaKey, + aParam.mButton, aParam.mRelatedTarget, aParam.mDataTransfer); + e->InitializeExtraMouseEventDictionaryMembers(aParam); + e->SetTrusted(trusted); + e->SetComposed(aParam.mComposed); + return e.forget(); +} + +} // namespace mozilla::dom + +using namespace mozilla; +using namespace mozilla::dom; + +already_AddRefed<DragEvent> NS_NewDOMDragEvent(EventTarget* aOwner, + nsPresContext* aPresContext, + WidgetDragEvent* aEvent) { + RefPtr<DragEvent> event = new DragEvent(aOwner, aPresContext, aEvent); + return event.forget(); +} diff --git a/dom/events/DragEvent.h b/dom/events/DragEvent.h new file mode 100644 index 0000000000..02dd881bd5 --- /dev/null +++ b/dom/events/DragEvent.h @@ -0,0 +1,55 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#ifndef mozilla_dom_DragEvent_h_ +#define mozilla_dom_DragEvent_h_ + +#include "mozilla/dom/MouseEvent.h" +#include "mozilla/dom/DragEventBinding.h" +#include "mozilla/EventForwards.h" + +namespace mozilla::dom { + +class DataTransfer; + +class DragEvent : public MouseEvent { + public: + DragEvent(EventTarget* aOwner, nsPresContext* aPresContext, + WidgetDragEvent* aEvent); + + NS_INLINE_DECL_REFCOUNTING_INHERITED(DragEvent, MouseEvent) + + virtual JSObject* WrapObjectInternal( + JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override { + return DragEvent_Binding::Wrap(aCx, this, aGivenProto); + } + + DragEvent* AsDragEvent() override { return this; } + + DataTransfer* GetDataTransfer(); + + void InitDragEvent(const nsAString& aType, bool aCanBubble, bool aCancelable, + nsGlobalWindowInner* aView, int32_t aDetail, + int32_t aScreenX, int32_t aScreenY, int32_t aClientX, + int32_t aClientY, bool aCtrlKey, bool aAltKey, + bool aShiftKey, bool aMetaKey, uint16_t aButton, + EventTarget* aRelatedTarget, DataTransfer* aDataTransfer); + + static already_AddRefed<DragEvent> Constructor(const GlobalObject& aGlobal, + const nsAString& aType, + const DragEventInit& aParam); + + protected: + ~DragEvent() = default; +}; + +} // namespace mozilla::dom + +already_AddRefed<mozilla::dom::DragEvent> NS_NewDOMDragEvent( + mozilla::dom::EventTarget* aOwner, nsPresContext* aPresContext, + mozilla::WidgetDragEvent* aEvent); + +#endif // mozilla_dom_DragEvent_h_ diff --git a/dom/events/Event.cpp b/dom/events/Event.cpp new file mode 100644 index 0000000000..c7763a17aa --- /dev/null +++ b/dom/events/Event.cpp @@ -0,0 +1,889 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#include "AccessCheck.h" +#include "base/basictypes.h" +#include "ipc/IPCMessageUtils.h" +#include "ipc/IPCMessageUtilsSpecializations.h" +#include "mozilla/EventDispatcher.h" +#include "mozilla/BasePrincipal.h" +#include "mozilla/ContentEvents.h" +#include "mozilla/DOMEventTargetHelper.h" +#include "mozilla/EventStateManager.h" +#include "mozilla/InternalMutationEvent.h" +#include "mozilla/dom/Performance.h" +#include "mozilla/dom/WorkerPrivate.h" +#include "mozilla/MiscEvents.h" +#include "mozilla/MouseEvents.h" +#include "mozilla/PointerLockManager.h" +#include "mozilla/Preferences.h" +#include "mozilla/PresShell.h" +#include "mozilla/TextEvents.h" +#include "mozilla/TouchEvents.h" +#include "mozilla/ViewportUtils.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/DocumentInlines.h" +#include "mozilla/dom/Event.h" +#include "mozilla/dom/ShadowRoot.h" +#include "mozilla/dom/WorkerScope.h" +#include "mozilla/StaticPrefs_dom.h" +#include "mozilla/SVGUtils.h" +#include "mozilla/SVGOuterSVGFrame.h" +#include "nsContentUtils.h" +#include "nsCOMPtr.h" +#include "nsDeviceContext.h" +#include "nsError.h" +#include "nsGlobalWindow.h" +#include "nsIFrame.h" +#include "nsIContent.h" +#include "nsIContentInlines.h" +#include "nsIScrollableFrame.h" +#include "nsJSEnvironment.h" +#include "nsLayoutUtils.h" +#include "nsPIWindowRoot.h" +#include "nsRFPService.h" + +namespace mozilla::dom { + +Event::Event(EventTarget* aOwner, nsPresContext* aPresContext, + WidgetEvent* aEvent) { + ConstructorInit(aOwner, aPresContext, aEvent); +} + +Event::Event(nsPIDOMWindowInner* aParent) { + ConstructorInit(nsGlobalWindowInner::Cast(aParent), nullptr, nullptr); +} + +void Event::ConstructorInit(EventTarget* aOwner, nsPresContext* aPresContext, + WidgetEvent* aEvent) { + SetOwner(aOwner); + mIsMainThreadEvent = NS_IsMainThread(); + + mPrivateDataDuplicated = false; + mWantsPopupControlCheck = false; + + if (aEvent) { + mEvent = aEvent; + mEventIsInternal = false; + } else { + mEventIsInternal = true; + /* + A derived class might want to allocate its own type of aEvent + (derived from WidgetEvent). To do this, it should take care to pass + a non-nullptr aEvent to this ctor, e.g.: + + FooEvent::FooEvent(..., WidgetEvent* aEvent) + : Event(..., aEvent ? aEvent : new WidgetEvent()) + + Then, to override the mEventIsInternal assignments done by the + base ctor, it should do this in its own ctor: + + FooEvent::FooEvent(..., WidgetEvent* aEvent) + ... + { + ... + if (aEvent) { + mEventIsInternal = false; + } + else { + mEventIsInternal = true; + } + ... + } + */ + mEvent = new WidgetEvent(false, eVoidEvent); + } + + InitPresContextData(aPresContext); +} + +void Event::InitPresContextData(nsPresContext* aPresContext) { + mPresContext = aPresContext; + // Get the explicit original target (if it's anonymous make it null) + { + nsCOMPtr<nsIContent> content = GetTargetFromFrame(); + mExplicitOriginalTarget = content; + if (content && content->IsInNativeAnonymousSubtree()) { + mExplicitOriginalTarget = nullptr; + } + } +} + +Event::~Event() { + NS_ASSERT_OWNINGTHREAD(Event); + + if (mEventIsInternal && mEvent) { + delete mEvent; + } +} + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(Event) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY + NS_INTERFACE_MAP_ENTRY(nsISupports) + NS_INTERFACE_MAP_ENTRY(Event) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTING_ADDREF(Event) +NS_IMPL_CYCLE_COLLECTING_RELEASE(Event) + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(Event) + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(Event) + if (tmp->mEventIsInternal) { + tmp->mEvent->mTarget = nullptr; + tmp->mEvent->mCurrentTarget = nullptr; + tmp->mEvent->mOriginalTarget = nullptr; + tmp->mEvent->mRelatedTarget = nullptr; + tmp->mEvent->mOriginalRelatedTarget = nullptr; + switch (tmp->mEvent->mClass) { + case eDragEventClass: { + WidgetDragEvent* dragEvent = tmp->mEvent->AsDragEvent(); + dragEvent->mDataTransfer = nullptr; + break; + } + case eClipboardEventClass: + tmp->mEvent->AsClipboardEvent()->mClipboardData = nullptr; + break; + case eEditorInputEventClass: { + InternalEditorInputEvent* inputEvent = + tmp->mEvent->AsEditorInputEvent(); + inputEvent->mDataTransfer = nullptr; + inputEvent->mTargetRanges.Clear(); + break; + } + case eMutationEventClass: + tmp->mEvent->AsMutationEvent()->mRelatedNode = nullptr; + break; + default: + break; + } + + if (WidgetMouseEvent* mouseEvent = tmp->mEvent->AsMouseEvent()) { + mouseEvent->mClickTarget = nullptr; + } + } + NS_IMPL_CYCLE_COLLECTION_UNLINK(mPresContext); + NS_IMPL_CYCLE_COLLECTION_UNLINK(mExplicitOriginalTarget); + NS_IMPL_CYCLE_COLLECTION_UNLINK(mOwner); + NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(Event) + if (tmp->mEventIsInternal) { + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mEvent->mTarget) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mEvent->mCurrentTarget) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mEvent->mOriginalTarget) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mEvent->mRelatedTarget) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mEvent->mOriginalRelatedTarget); + switch (tmp->mEvent->mClass) { + case eDragEventClass: { + WidgetDragEvent* dragEvent = tmp->mEvent->AsDragEvent(); + NS_CYCLE_COLLECTION_NOTE_EDGE_NAME(cb, "mEvent->mDataTransfer"); + cb.NoteXPCOMChild(dragEvent->mDataTransfer); + break; + } + case eClipboardEventClass: + NS_CYCLE_COLLECTION_NOTE_EDGE_NAME(cb, "mEvent->mClipboardData"); + cb.NoteXPCOMChild(tmp->mEvent->AsClipboardEvent()->mClipboardData); + break; + case eEditorInputEventClass: + NS_CYCLE_COLLECTION_NOTE_EDGE_NAME(cb, "mEvent->mDataTransfer"); + cb.NoteXPCOMChild(tmp->mEvent->AsEditorInputEvent()->mDataTransfer); + NS_IMPL_CYCLE_COLLECTION_TRAVERSE( + mEvent->AsEditorInputEvent()->mTargetRanges); + break; + case eMutationEventClass: + NS_CYCLE_COLLECTION_NOTE_EDGE_NAME(cb, "mEvent->mRelatedNode"); + cb.NoteXPCOMChild(tmp->mEvent->AsMutationEvent()->mRelatedNode); + break; + default: + break; + } + + if (WidgetMouseEvent* mouseEvent = tmp->mEvent->AsMouseEvent()) { + NS_CYCLE_COLLECTION_NOTE_EDGE_NAME(cb, "mEvent->mClickTarget"); + cb.NoteXPCOMChild(mouseEvent->mClickTarget); + } + } + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPresContext) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mExplicitOriginalTarget) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mOwner) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +JSObject* Event::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) { + return WrapObjectInternal(aCx, aGivenProto); +} + +JSObject* Event::WrapObjectInternal(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return Event_Binding::Wrap(aCx, this, aGivenProto); +} + +void Event::GetType(nsAString& aType) const { + GetWidgetEventType(mEvent, aType); +} + +EventTarget* Event::GetTarget() const { return mEvent->GetDOMEventTarget(); } + +already_AddRefed<Document> Event::GetDocument() const { + nsCOMPtr<EventTarget> eventTarget = GetTarget(); + + if (!eventTarget) { + return nullptr; + } + + nsCOMPtr<nsPIDOMWindowInner> win = + do_QueryInterface(eventTarget->GetOwnerGlobal()); + + if (!win) { + return nullptr; + } + + nsCOMPtr<Document> doc; + doc = win->GetExtantDoc(); + + return doc.forget(); +} + +EventTarget* Event::GetCurrentTarget() const { + return mEvent->GetCurrentDOMEventTarget(); +} + +void Event::ComposedPath(nsTArray<RefPtr<EventTarget>>& aPath) { + EventDispatcher::GetComposedPathFor(mEvent, aPath); +} + +// +// Get the actual event target node (may have been retargeted for mouse events) +// +already_AddRefed<nsIContent> Event::GetTargetFromFrame() { + if (!mPresContext) { + return nullptr; + } + + // Get the mTarget frame (have to get the ESM first) + nsIFrame* targetFrame = mPresContext->EventStateManager()->GetEventTarget(); + if (!targetFrame) { + return nullptr; + } + + // get the real content + nsCOMPtr<nsIContent> realEventContent; + targetFrame->GetContentForEvent(mEvent, getter_AddRefs(realEventContent)); + return realEventContent.forget(); +} + +EventTarget* Event::GetExplicitOriginalTarget() const { + if (mExplicitOriginalTarget) { + return mExplicitOriginalTarget; + } + return GetTarget(); +} + +EventTarget* Event::GetOriginalTarget() const { + return mEvent->GetOriginalDOMEventTarget(); +} + +EventTarget* Event::GetComposedTarget() const { + EventTarget* et = GetOriginalTarget(); + nsCOMPtr<nsIContent> content = do_QueryInterface(et); + if (!content) { + return et; + } + nsIContent* nonChrome = content->FindFirstNonChromeOnlyAccessContent(); + return nonChrome ? static_cast<EventTarget*>(nonChrome) + : static_cast<EventTarget*>(content->GetComposedDoc()); +} + +void Event::SetTrusted(bool aTrusted) { mEvent->mFlags.mIsTrusted = aTrusted; } + +bool Event::ShouldIgnoreChromeEventTargetListener() const { + MOZ_ASSERT(NS_IsMainThread()); + if (!XRE_IsParentProcess()) { + return false; + } + if (EventTarget* currentTarget = GetCurrentTarget(); + NS_WARN_IF(!currentTarget) || !currentTarget->IsRootWindow()) { + return false; + } + EventTarget* et = GetOriginalTarget(); + if (NS_WARN_IF(!et)) { + return false; + } + nsIGlobalObject* global = et->GetOwnerGlobal(); + if (NS_WARN_IF(!global)) { + return false; + } + nsPIDOMWindowInner* win = global->AsInnerWindow(); + if (NS_WARN_IF(!win)) { + return false; + } + BrowsingContext* bc = win->GetBrowsingContext(); + if (NS_WARN_IF(!bc)) { + return false; + } + // If this is a content event on an nsWindowRoot, then we also handle this in + // InProcessBrowserChildMessageManager, so we can ignore this event. + return bc->IsContent(); +} + +bool Event::Init(mozilla::dom::EventTarget* aGlobal) { + if (!mIsMainThreadEvent) { + return IsCurrentThreadRunningChromeWorker(); + } + bool trusted = false; + nsCOMPtr<nsPIDOMWindowInner> w = do_QueryInterface(aGlobal); + if (w) { + nsCOMPtr<Document> d = w->GetExtantDoc(); + if (d) { + trusted = nsContentUtils::IsChromeDoc(d); + nsPresContext* presContext = d->GetPresContext(); + if (presContext) { + InitPresContextData(presContext); + } + } + } + return trusted; +} + +// static +already_AddRefed<Event> Event::Constructor(const GlobalObject& aGlobal, + const nsAString& aType, + const EventInit& aParam) { + nsCOMPtr<mozilla::dom::EventTarget> t = + do_QueryInterface(aGlobal.GetAsSupports()); + return Constructor(t, aType, aParam); +} + +// static +already_AddRefed<Event> Event::Constructor(EventTarget* aEventTarget, + const nsAString& aType, + const EventInit& aParam) { + RefPtr<Event> e = new Event(aEventTarget, nullptr, nullptr); + bool trusted = e->Init(aEventTarget); + e->InitEvent(aType, aParam.mBubbles, aParam.mCancelable); + e->SetTrusted(trusted); + e->SetComposed(aParam.mComposed); + return e.forget(); +} + +uint16_t Event::EventPhase() const { + // Note, remember to check that this works also + // if or when Bug 235441 is fixed. + if ((mEvent->mCurrentTarget && mEvent->mCurrentTarget == mEvent->mTarget) || + mEvent->mFlags.InTargetPhase()) { + return Event_Binding::AT_TARGET; + } + if (mEvent->mFlags.mInCapturePhase) { + return Event_Binding::CAPTURING_PHASE; + } + if (mEvent->mFlags.mInBubblingPhase) { + return Event_Binding::BUBBLING_PHASE; + } + return Event_Binding::NONE; +} + +void Event::StopPropagation() { mEvent->StopPropagation(); } + +void Event::StopImmediatePropagation() { mEvent->StopImmediatePropagation(); } + +void Event::StopCrossProcessForwarding() { + mEvent->StopCrossProcessForwarding(); +} + +void Event::PreventDefault() { + // This method is called only from C++ code which must handle default action + // of this event. So, pass true always. + PreventDefaultInternal(true); +} + +void Event::PreventDefault(JSContext* aCx, CallerType aCallerType) { + // Note that at handling default action, another event may be dispatched. + // Then, JS in content mey be call preventDefault() + // even in the event is in system event group. Therefore, don't refer + // mInSystemGroup here. + nsIPrincipal* principal = + mIsMainThreadEvent ? nsContentUtils::SubjectPrincipal(aCx) : nullptr; + + PreventDefaultInternal(aCallerType == CallerType::System, principal); +} + +void Event::PreventDefaultInternal(bool aCalledByDefaultHandler, + nsIPrincipal* aPrincipal) { + if (!mEvent->mFlags.mCancelable) { + return; + } + if (mEvent->mFlags.mInPassiveListener) { + nsCOMPtr<nsPIDOMWindowInner> win(do_QueryInterface(mOwner)); + if (win) { + if (Document* doc = win->GetExtantDoc()) { + if (!doc->HasWarnedAbout( + Document::ePreventDefaultFromPassiveListener)) { + AutoTArray<nsString, 1> params; + GetType(*params.AppendElement()); + doc->WarnOnceAbout(Document::ePreventDefaultFromPassiveListener, + false, params); + } + } + } + return; + } + + mEvent->PreventDefault(aCalledByDefaultHandler, aPrincipal); + + if (!IsTrusted()) { + return; + } + + WidgetDragEvent* dragEvent = mEvent->AsDragEvent(); + if (!dragEvent) { + return; + } + + nsIPrincipal* principal = nullptr; + nsCOMPtr<nsINode> node = + nsINode::FromEventTargetOrNull(mEvent->mCurrentTarget); + if (node) { + principal = node->NodePrincipal(); + } else { + nsCOMPtr<nsIScriptObjectPrincipal> sop = + do_QueryInterface(mEvent->mCurrentTarget); + if (sop) { + principal = sop->GetPrincipal(); + } + } + if (principal && !principal->IsSystemPrincipal()) { + dragEvent->mDefaultPreventedOnContent = true; + } +} + +void Event::SetEventType(const nsAString& aEventTypeArg) { + mEvent->mSpecifiedEventTypeString.Truncate(); + if (mIsMainThreadEvent) { + mEvent->mSpecifiedEventType = nsContentUtils::GetEventMessageAndAtom( + aEventTypeArg, mEvent->mClass, &(mEvent->mMessage)); + mEvent->SetDefaultComposed(); + } else { + mEvent->mSpecifiedEventType = NS_Atomize(u"on"_ns + aEventTypeArg); + mEvent->mMessage = eUnidentifiedEvent; + mEvent->SetComposed(aEventTypeArg); + } + mEvent->SetDefaultComposedInNativeAnonymousContent(); +} + +already_AddRefed<EventTarget> Event::EnsureWebAccessibleRelatedTarget( + EventTarget* aRelatedTarget) { + nsCOMPtr<EventTarget> relatedTarget = aRelatedTarget; + if (relatedTarget) { + nsIContent* content = nsIContent::FromEventTarget(relatedTarget); + if (content && content->ChromeOnlyAccess() && + !nsContentUtils::CanAccessNativeAnon()) { + content = content->FindFirstNonChromeOnlyAccessContent(); + relatedTarget = content; + } + + if (relatedTarget) { + relatedTarget = relatedTarget->GetTargetForDOMEvent(); + } + } + return relatedTarget.forget(); +} + +void Event::InitEvent(const nsAString& aEventTypeArg, + mozilla::CanBubble aCanBubbleArg, + mozilla::Cancelable aCancelableArg, + mozilla::Composed aComposedArg) { + // Make sure this event isn't already being dispatched. + NS_ENSURE_TRUE_VOID(!mEvent->mFlags.mIsBeingDispatched); + + if (IsTrusted()) { + // Ensure the caller is permitted to dispatch trusted DOM events. + if (!nsContentUtils::ThreadsafeIsCallerChrome()) { + SetTrusted(false); + } + } + + SetEventType(aEventTypeArg); + + mEvent->mFlags.mBubbles = aCanBubbleArg == CanBubble::eYes; + mEvent->mFlags.mCancelable = aCancelableArg == Cancelable::eYes; + if (aComposedArg != Composed::eDefault) { + mEvent->mFlags.mComposed = aComposedArg == Composed::eYes; + } + + mEvent->mFlags.mDefaultPrevented = false; + mEvent->mFlags.mDefaultPreventedByContent = false; + mEvent->mFlags.mDefaultPreventedByChrome = false; + mEvent->mFlags.mPropagationStopped = false; + mEvent->mFlags.mImmediatePropagationStopped = false; + + // Clearing the old targets, so that the event is targeted correctly when + // re-dispatching it. + mEvent->mTarget = nullptr; + mEvent->mOriginalTarget = nullptr; +} + +void Event::DuplicatePrivateData() { + NS_ASSERTION(mEvent, "No WidgetEvent for Event duplication!"); + if (mEventIsInternal) { + return; + } + + mEvent = mEvent->Duplicate(); + mPresContext = nullptr; + mEventIsInternal = true; + mPrivateDataDuplicated = true; +} + +void Event::SetTarget(EventTarget* aTarget) { mEvent->mTarget = aTarget; } + +bool Event::IsDispatchStopped() { return mEvent->PropagationStopped(); } + +WidgetEvent* Event::WidgetEventPtr() { return mEvent; } + +// static +Maybe<CSSIntPoint> Event::GetScreenCoords(nsPresContext* aPresContext, + WidgetEvent* aEvent, + LayoutDeviceIntPoint aPoint) { + if (PointerLockManager::IsLocked()) { + return Some(EventStateManager::sLastScreenPoint); + } + + if (!aEvent || (aEvent->mClass != eMouseEventClass && + aEvent->mClass != eMouseScrollEventClass && + aEvent->mClass != eWheelEventClass && + aEvent->mClass != ePointerEventClass && + aEvent->mClass != eTouchEventClass && + aEvent->mClass != eDragEventClass && + aEvent->mClass != eSimpleGestureEventClass)) { + return Nothing(); + } + + // Doing a straight conversion from LayoutDeviceIntPoint to CSSIntPoint + // seem incorrect, but it is needed to maintain legacy functionality. + WidgetGUIEvent* guiEvent = aEvent->AsGUIEvent(); + if (!aPresContext || !(guiEvent && guiEvent->mWidget)) { + return Some(CSSIntPoint(aPoint.x, aPoint.y)); + } + + // (Potentially) transform the point from the coordinate space of an + // out-of-process iframe to the coordinate space of the native + // window. The transform can only be applied to a point whose components + // are floating-point values, so convert the integer point first, then + // transform, and then round the result back to an integer point. + LayoutDevicePoint floatPoint(aPoint); + LayoutDevicePoint topLevelPoint = + guiEvent->mWidget->WidgetToTopLevelWidgetTransform().TransformPoint( + floatPoint); + LayoutDeviceIntPoint rounded = RoundedToInt(topLevelPoint); + + nsPoint pt = LayoutDevicePixel::ToAppUnits( + rounded, aPresContext->DeviceContext()->AppUnitsPerDevPixel()); + + pt += LayoutDevicePixel::ToAppUnits( + guiEvent->mWidget->TopLevelWidgetToScreenOffset(), + aPresContext->DeviceContext()->AppUnitsPerDevPixel()); + + return Some(CSSPixel::FromAppUnitsRounded(pt)); +} + +// static +CSSIntPoint Event::GetPageCoords(nsPresContext* aPresContext, + WidgetEvent* aEvent, + LayoutDeviceIntPoint aPoint, + CSSIntPoint aDefaultPoint) { + CSSIntPoint pagePoint = + Event::GetClientCoords(aPresContext, aEvent, aPoint, aDefaultPoint); + + // If there is some scrolling, add scroll info to client point. + if (aPresContext && aPresContext->GetPresShell()) { + PresShell* presShell = aPresContext->PresShell(); + nsIScrollableFrame* scrollframe = + presShell->GetRootScrollFrameAsScrollable(); + if (scrollframe) { + pagePoint += + CSSIntPoint::FromAppUnitsRounded(scrollframe->GetScrollPosition()); + } + } + + return pagePoint; +} + +// static +CSSIntPoint Event::GetClientCoords(nsPresContext* aPresContext, + WidgetEvent* aEvent, + LayoutDeviceIntPoint aPoint, + CSSIntPoint aDefaultPoint) { + if (PointerLockManager::IsLocked()) { + return EventStateManager::sLastClientPoint; + } + + if (!aEvent || + (aEvent->mClass != eMouseEventClass && + aEvent->mClass != eMouseScrollEventClass && + aEvent->mClass != eWheelEventClass && + aEvent->mClass != eTouchEventClass && + aEvent->mClass != eDragEventClass && + aEvent->mClass != ePointerEventClass && + aEvent->mClass != eSimpleGestureEventClass) || + !aPresContext || !aEvent->AsGUIEvent()->mWidget) { + return aDefaultPoint; + } + + PresShell* presShell = aPresContext->GetPresShell(); + if (!presShell) { + return CSSIntPoint(0, 0); + } + nsIFrame* rootFrame = presShell->GetRootFrame(); + if (!rootFrame) { + return CSSIntPoint(0, 0); + } + nsPoint pt = nsLayoutUtils::GetEventCoordinatesRelativeTo( + aEvent, aPoint, RelativeTo{rootFrame}); + + return CSSIntPoint::FromAppUnitsRounded(pt); +} + +// static +CSSIntPoint Event::GetOffsetCoords(nsPresContext* aPresContext, + WidgetEvent* aEvent, + LayoutDeviceIntPoint aPoint, + CSSIntPoint aDefaultPoint) { + if (!aEvent->mTarget) { + return GetPageCoords(aPresContext, aEvent, aPoint, aDefaultPoint); + } + nsCOMPtr<nsIContent> content = nsIContent::FromEventTarget(aEvent->mTarget); + if (!content || !aPresContext) { + return CSSIntPoint(); + } + RefPtr<PresShell> presShell = aPresContext->GetPresShell(); + if (!presShell) { + return CSSIntPoint(); + } + presShell->FlushPendingNotifications(FlushType::Layout); + nsIFrame* frame = content->GetPrimaryFrame(); + if (!frame) { + return CSSIntPoint(); + } + // For compat, see https://github.com/w3c/csswg-drafts/issues/1508. In SVG we + // just return the coordinates of the outer SVG box. This is all kinda + // unfortunate. + if (frame->HasAnyStateBits(NS_FRAME_SVG_LAYOUT) && + StaticPrefs::dom_events_offset_in_svg_relative_to_svg_root()) { + frame = SVGUtils::GetOuterSVGFrame(frame); + if (!frame) { + return CSSIntPoint(); + } + } + nsIFrame* rootFrame = presShell->GetRootFrame(); + if (!rootFrame) { + return CSSIntPoint(); + } + CSSIntPoint clientCoords = + GetClientCoords(aPresContext, aEvent, aPoint, aDefaultPoint); + nsPoint pt = CSSPixel::ToAppUnits(clientCoords); + if (nsLayoutUtils::TransformPoint(RelativeTo{rootFrame}, RelativeTo{frame}, + pt) == nsLayoutUtils::TRANSFORM_SUCCEEDED) { + pt -= frame->GetPaddingRectRelativeToSelf().TopLeft(); + return CSSPixel::FromAppUnitsRounded(pt); + } + return CSSIntPoint(); +} + +// To be called ONLY by Event::GetType (which has the additional +// logic for handling user-defined events). +// static +const char16_t* Event::GetEventName(EventMessage aEventType) { + switch (aEventType) { +#define MESSAGE_TO_EVENT(name_, _message, _type, _struct) \ + case _message: \ + return u"" #name_; +#include "mozilla/EventNameList.h" +#undef MESSAGE_TO_EVENT + default: + break; + } + // XXXldb We can hit this case for WidgetEvent objects that we didn't + // create and that are not user defined events since this function and + // SetEventType are incomplete. (But fixing that requires fixing the + // arrays in nsEventListenerManager too, since the events for which + // this is a problem generally *are* created by Event.) + return nullptr; +} + +bool Event::DefaultPrevented(CallerType aCallerType) const { + NS_ENSURE_TRUE(mEvent, false); + + // If preventDefault() has never been called, just return false. + if (!mEvent->DefaultPrevented()) { + return false; + } + + // If preventDefault() has been called by content, return true. Otherwise, + // i.e., preventDefault() has been called by chrome, return true only when + // this is called by chrome. + return mEvent->DefaultPreventedByContent() || + aCallerType == CallerType::System; +} + +bool Event::ReturnValue(CallerType aCallerType) const { + return !DefaultPrevented(aCallerType); +} + +void Event::SetReturnValue(bool aReturnValue, CallerType aCallerType) { + if (!aReturnValue) { + PreventDefaultInternal(aCallerType == CallerType::System); + } +} + +double Event::TimeStamp() { + if (mEvent->mTimeStamp.IsNull()) { + return 0.0; + } + + if (mIsMainThreadEvent) { + if (NS_WARN_IF(!mOwner)) { + return 0.0; + } + + nsCOMPtr<nsPIDOMWindowInner> win = do_QueryInterface(mOwner); + if (NS_WARN_IF(!win)) { + return 0.0; + } + + Performance* perf = win->GetPerformance(); + if (NS_WARN_IF(!perf)) { + return 0.0; + } + + double ret = + perf->GetDOMTiming()->TimeStampToDOMHighRes(mEvent->mTimeStamp); + MOZ_ASSERT(mOwner->PrincipalOrNull()); + + return nsRFPService::ReduceTimePrecisionAsMSecs( + ret, perf->GetRandomTimelineSeed(), perf->GetRTPCallerType()); + } + + WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate(); + MOZ_ASSERT(workerPrivate); + + double ret = workerPrivate->TimeStampToDOMHighRes(mEvent->mTimeStamp); + + return nsRFPService::ReduceTimePrecisionAsMSecs( + ret, workerPrivate->GetRandomTimelineSeed(), + workerPrivate->GlobalScope()->GetRTPCallerType()); +} + +void Event::Serialize(IPC::MessageWriter* aWriter, + bool aSerializeInterfaceType) { + if (aSerializeInterfaceType) { + IPC::WriteParam(aWriter, u"event"_ns); + } + + nsString type; + GetType(type); + IPC::WriteParam(aWriter, type); + + IPC::WriteParam(aWriter, Bubbles()); + IPC::WriteParam(aWriter, Cancelable()); + IPC::WriteParam(aWriter, IsTrusted()); + IPC::WriteParam(aWriter, Composed()); + + // No timestamp serialization for now! +} + +bool Event::Deserialize(IPC::MessageReader* aReader) { + nsString type; + NS_ENSURE_TRUE(IPC::ReadParam(aReader, &type), false); + + bool bubbles = false; + NS_ENSURE_TRUE(IPC::ReadParam(aReader, &bubbles), false); + + bool cancelable = false; + NS_ENSURE_TRUE(IPC::ReadParam(aReader, &cancelable), false); + + bool trusted = false; + NS_ENSURE_TRUE(IPC::ReadParam(aReader, &trusted), false); + + bool composed = false; + NS_ENSURE_TRUE(IPC::ReadParam(aReader, &composed), false); + + InitEvent(type, bubbles, cancelable); + SetTrusted(trusted); + SetComposed(composed); + + return true; +} + +void Event::SetOwner(EventTarget* aOwner) { + mOwner = nullptr; + + if (!aOwner) { + return; + } + + nsCOMPtr<nsINode> n = do_QueryInterface(aOwner); + if (n) { + mOwner = n->OwnerDoc()->GetScopeObject(); + return; + } + + nsCOMPtr<nsPIDOMWindowInner> w = do_QueryInterface(aOwner); + if (w) { + mOwner = do_QueryInterface(w); + return; + } + + nsCOMPtr<DOMEventTargetHelper> eth = do_QueryInterface(aOwner); + if (eth) { + mOwner = eth->GetParentObject(); + return; + } + +#ifdef DEBUG + nsCOMPtr<nsPIWindowRoot> root = do_QueryInterface(aOwner); + MOZ_ASSERT(root, "Unexpected EventTarget!"); +#endif +} + +void Event::GetWidgetEventType(WidgetEvent* aEvent, nsAString& aType) { + if (!aEvent->mSpecifiedEventTypeString.IsEmpty()) { + aType = aEvent->mSpecifiedEventTypeString; + return; + } + + const char16_t* name = GetEventName(aEvent->mMessage); + + if (name) { + aType.AssignLiteral(name, nsString::char_traits::length(name)); + return; + } else if (aEvent->mMessage == eUnidentifiedEvent && + aEvent->mSpecifiedEventType) { + // Remove "on" + aType = Substring(nsDependentAtomString(aEvent->mSpecifiedEventType), 2); + aEvent->mSpecifiedEventTypeString = aType; + return; + } + + aType.Truncate(); +} + +bool Event::IsDragExitEnabled(JSContext* aCx, JSObject* aGlobal) { + return StaticPrefs::dom_event_dragexit_enabled() || + nsContentUtils::IsSystemCaller(aCx); +} + +} // namespace mozilla::dom + +using namespace mozilla; +using namespace mozilla::dom; + +already_AddRefed<Event> NS_NewDOMEvent(EventTarget* aOwner, + nsPresContext* aPresContext, + WidgetEvent* aEvent) { + RefPtr<Event> it = new Event(aOwner, aPresContext, aEvent); + return it.forget(); +} diff --git a/dom/events/Event.h b/dom/events/Event.h new file mode 100644 index 0000000000..2b68a46f5f --- /dev/null +++ b/dom/events/Event.h @@ -0,0 +1,409 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#ifndef mozilla_dom_Event_h_ +#define mozilla_dom_Event_h_ + +#include <cstdint> +#include "Units.h" +#include "js/TypeDecls.h" +#include "mozilla/AlreadyAddRefed.h" +#include "mozilla/Assertions.h" +#include "mozilla/Attributes.h" +#include "mozilla/BasicEvents.h" +#include "mozilla/RefPtr.h" +#include "mozilla/Maybe.h" +#include "mozilla/dom/BindingDeclarations.h" +#include "nsCOMPtr.h" +#include "nsCycleCollectionParticipant.h" +#include "nsID.h" +#include "nsISupports.h" +#include "nsStringFwd.h" +#include "nsWrapperCache.h" + +class PickleIterator; +class nsCycleCollectionTraversalCallback; +class nsIContent; +class nsIGlobalObject; +class nsIPrincipal; +class nsPIDOMWindowInner; +class nsPresContext; + +namespace IPC { +class Message; +class MessageReader; +class MessageWriter; +} // namespace IPC + +namespace mozilla::dom { + +class BeforeUnloadEvent; +class CustomEvent; +class Document; +class DragEvent; +class EventTarget; +class EventMessageAutoOverride; +// ExtendableEvent is a ServiceWorker event that is not +// autogenerated since it has some extra methods. +class ExtendableEvent; +class KeyboardEvent; +class MouseEvent; +class MessageEvent; +class TimeEvent; +class UIEvent; +class WantsPopupControlCheck; +class XULCommandEvent; +struct EventInit; + +#define GENERATED_EVENT(EventClass_) class EventClass_; +#include "mozilla/dom/GeneratedEventList.h" +#undef GENERATED_EVENT + +// IID for Event +#define NS_EVENT_IID \ + { \ + 0x71139716, 0x4d91, 0x4dee, { \ + 0xba, 0xf9, 0xe3, 0x3b, 0x80, 0xc1, 0x61, 0x61 \ + } \ + } + +class Event : public nsISupports, public nsWrapperCache { + public: + NS_DECLARE_STATIC_IID_ACCESSOR(NS_EVENT_IID) + + Event(EventTarget* aOwner, nsPresContext* aPresContext, WidgetEvent* aEvent); + explicit Event(nsPIDOMWindowInner* aWindow); + + protected: + virtual ~Event(); + + private: + void ConstructorInit(EventTarget* aOwner, nsPresContext* aPresContext, + WidgetEvent* aEvent); + + public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(Event) + + nsIGlobalObject* GetParentObject() const { return mOwner; } + + JSObject* WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) final; + + virtual JSObject* WrapObjectInternal(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto); + +#define GENERATED_EVENT(EventClass_) \ + virtual EventClass_* As##EventClass_() { return nullptr; } +#include "mozilla/dom/GeneratedEventList.h" +#undef GENERATED_EVENT + + // ExtendableEvent is a ServiceWorker event that is not + // autogenerated since it has some extra methods. + virtual ExtendableEvent* AsExtendableEvent() { return nullptr; } + + virtual TimeEvent* AsTimeEvent() { return nullptr; } + + // BeforeUnloadEvent is not autogenerated because it has a setter. + virtual BeforeUnloadEvent* AsBeforeUnloadEvent() { return nullptr; } + + // KeyboardEvent has all sorts of non-autogeneratable bits so far. + virtual KeyboardEvent* AsKeyboardEvent() { return nullptr; } + + // DragEvent has a non-autogeneratable initDragEvent. + virtual DragEvent* AsDragEvent() { return nullptr; } + + // XULCommandEvent has a non-autogeneratable initCommandEvent. + virtual XULCommandEvent* AsXULCommandEvent() { return nullptr; } + + // MouseEvent has a non-autogeneratable initMouseEvent and other + // non-autogeneratable methods. + virtual MouseEvent* AsMouseEvent() { return nullptr; } + + // UIEvent has a non-autogeneratable initUIEvent. + virtual UIEvent* AsUIEvent() { return nullptr; } + + // CustomEvent has a non-autogeneratable initCustomEvent. + virtual CustomEvent* AsCustomEvent() { return nullptr; } + + // MessageEvent has a non-autogeneratable initMessageEvent and more. + virtual MessageEvent* AsMessageEvent() { return nullptr; } + + void InitEvent(const nsAString& aEventTypeArg, bool aCanBubble, + bool aCancelable) { + InitEvent(aEventTypeArg, aCanBubble ? CanBubble::eYes : CanBubble::eNo, + aCancelable ? Cancelable::eYes : Cancelable::eNo); + } + + void InitEvent(const nsAString& aEventTypeArg, mozilla::CanBubble, + mozilla::Cancelable, + mozilla::Composed = mozilla::Composed::eDefault); + + void SetTarget(EventTarget* aTarget); + virtual void DuplicatePrivateData(); + bool IsDispatchStopped(); + WidgetEvent* WidgetEventPtr(); + const WidgetEvent* WidgetEventPtr() const { + return const_cast<Event*>(this)->WidgetEventPtr(); + } + virtual void Serialize(IPC::MessageWriter* aWriter, + bool aSerializeInterfaceType); + virtual bool Deserialize(IPC::MessageReader* aReader); + void SetOwner(EventTarget* aOwner); + void StopCrossProcessForwarding(); + void SetTrusted(bool aTrusted); + + // When listening to chrome EventTargets, in the parent process, nsWindowRoot + // might receive events we've already handled via + // InProcessBrowserChildMessageManager, and handlers should call this to avoid + // handling the same event twice. + bool ShouldIgnoreChromeEventTargetListener() const; + + void InitPresContextData(nsPresContext* aPresContext); + + // Returns true if the event should be trusted. + bool Init(EventTarget* aGlobal); + + static const char16_t* GetEventName(EventMessage aEventType); + static CSSIntPoint GetClientCoords(nsPresContext* aPresContext, + WidgetEvent* aEvent, + LayoutDeviceIntPoint aPoint, + CSSIntPoint aDefaultPoint); + static CSSIntPoint GetPageCoords(nsPresContext* aPresContext, + WidgetEvent* aEvent, + LayoutDeviceIntPoint aPoint, + CSSIntPoint aDefaultPoint); + static Maybe<CSSIntPoint> GetScreenCoords(nsPresContext* aPresContext, + WidgetEvent* aEvent, + LayoutDeviceIntPoint aPoint); + MOZ_CAN_RUN_SCRIPT_BOUNDARY + static CSSIntPoint GetOffsetCoords(nsPresContext* aPresContext, + WidgetEvent* aEvent, + LayoutDeviceIntPoint aPoint, + CSSIntPoint aDefaultPoint); + + static already_AddRefed<Event> Constructor(EventTarget* aEventTarget, + const nsAString& aType, + const EventInit& aParam); + + static already_AddRefed<Event> Constructor(const GlobalObject& aGlobal, + const nsAString& aType, + const EventInit& aParam); + + void GetType(nsAString& aType) const; + + EventTarget* GetTarget() const; + EventTarget* GetCurrentTarget() const; + + // This method returns the document which is associated with the event target. + already_AddRefed<Document> GetDocument() const; + + void ComposedPath(nsTArray<RefPtr<EventTarget>>& aPath); + + uint16_t EventPhase() const; + + void StopPropagation(); + + void StopImmediatePropagation(); + + bool Bubbles() const { return mEvent->mFlags.mBubbles; } + + bool Cancelable() const { return mEvent->mFlags.mCancelable; } + + bool Composed() const { return mEvent->mFlags.mComposed; } + + bool CancelBubble() const { return mEvent->PropagationStopped(); } + void SetCancelBubble(bool aCancelBubble) { + if (aCancelBubble) { + mEvent->StopPropagation(); + } + } + + // For C++ consumers only! + void PreventDefault(); + + // You MUST NOT call PreventDefault(JSContext*, CallerType) from C++ code. A + // call of this method always sets Event.defaultPrevented true for web + // contents. If default action handler calls this, web applications see wrong + // defaultPrevented value. + virtual void PreventDefault(JSContext* aCx, CallerType aCallerType); + + // You MUST NOT call DefaultPrevented(CallerType) from C++ code. This may + // return false even if PreventDefault() has been called. + // See comments in its implementation for the details. + bool DefaultPrevented(CallerType aCallerType) const; + + bool DefaultPrevented() const { return mEvent->DefaultPrevented(); } + + bool DefaultPreventedByChrome() const { + return mEvent->mFlags.mDefaultPreventedByChrome; + } + + bool DefaultPreventedByContent() const { + return mEvent->mFlags.mDefaultPreventedByContent; + } + + void PreventMultipleActions() { + mEvent->mFlags.mMultipleActionsPrevented = true; + } + + bool MultipleActionsPrevented() const { + return mEvent->mFlags.mMultipleActionsPrevented; + } + + bool ReturnValue(CallerType aCallerType) const; + + void SetReturnValue(bool aReturnValue, CallerType aCallerType); + + bool IsTrusted() const { return mEvent->IsTrusted(); } + + bool IsSynthesized() const { return mEvent->mFlags.mIsSynthesizedForTests; } + + bool IsSafeToBeDispatchedAsynchronously() const { + // If mEvent is not created by dom::Event nor its subclasses, its lifetime + // is not guaranteed. So, only when mEventIsInternal is true, it's safe + // to be dispatched asynchronously. + return mEventIsInternal; + } + + double TimeStamp(); + + EventTarget* GetOriginalTarget() const; + EventTarget* GetExplicitOriginalTarget() const; + EventTarget* GetComposedTarget() const; + + /** + * @param aCalledByDefaultHandler Should be true when this is called by + * C++ or Chrome. Otherwise, e.g., called + * by a call of Event.preventDefault() in + * content script, false. + */ + void PreventDefaultInternal(bool aCalledByDefaultHandler, + nsIPrincipal* aPrincipal = nullptr); + + bool IsMainThreadEvent() { return mIsMainThreadEvent; } + + void MarkUninitialized() { + mEvent->mMessage = eVoidEvent; + mEvent->mSpecifiedEventTypeString.Truncate(); + mEvent->mSpecifiedEventType = nullptr; + } + + /** + * For WidgetEvent, return it's type in string. + * + * @param aEvent is a WidgetEvent to get its type. + * @param aType is a string where to return the type. + */ + static void GetWidgetEventType(WidgetEvent* aEvent, nsAString& aType); + + void RequestReplyFromRemoteContent() { + mEvent->MarkAsWaitingReplyFromRemoteProcess(); + } + + bool IsWaitingReplyFromRemoteContent() const { + return mEvent->IsWaitingReplyFromRemoteProcess(); + } + + bool IsReplyEventFromRemoteContent() const { + return mEvent->IsHandledInRemoteProcess(); + } + + static bool IsDragExitEnabled(JSContext* aCx, JSObject* aGlobal); + + protected: + // Internal helper functions + void SetEventType(const nsAString& aEventTypeArg); + already_AddRefed<nsIContent> GetTargetFromFrame(); + + friend class EventMessageAutoOverride; + friend class PopupBlocker; + friend class WantsPopupControlCheck; + void SetWantsPopupControlCheck(bool aCheck) { + mWantsPopupControlCheck = aCheck; + } + + bool GetWantsPopupControlCheck() { + return IsTrusted() && mWantsPopupControlCheck; + } + + void SetComposed(bool aComposed) { mEvent->SetComposed(aComposed); } + + already_AddRefed<EventTarget> EnsureWebAccessibleRelatedTarget( + EventTarget* aRelatedTarget); + + mozilla::WidgetEvent* mEvent; + RefPtr<nsPresContext> mPresContext; + nsCOMPtr<EventTarget> mExplicitOriginalTarget; + nsCOMPtr<nsIGlobalObject> mOwner; + bool mEventIsInternal; + bool mPrivateDataDuplicated; + bool mIsMainThreadEvent; + // True when popup control check should rely on event.type, not + // WidgetEvent.mMessage. + bool mWantsPopupControlCheck; +}; + +/** + * RAII helper-class to override an event's message (i.e. its DOM-exposed + * type), for as long as the object is alive. Restores the original + * EventMessage when destructed. + * + * Notable requirements: + * - The original & overriding messages must be known (not eUnidentifiedEvent). + * - The original & overriding messages must be different. + * - The passed-in Event must outlive this RAII helper. + */ +class MOZ_RAII EventMessageAutoOverride { + public: + explicit EventMessageAutoOverride(Event* aEvent, + EventMessage aOverridingMessage) + : mEvent(aEvent), mOrigMessage(mEvent->mEvent->mMessage) { + MOZ_ASSERT(aOverridingMessage != mOrigMessage, + "Don't use this class if you're not actually overriding"); + MOZ_ASSERT(aOverridingMessage != eUnidentifiedEvent, + "Only use this class with a valid overriding EventMessage"); + MOZ_ASSERT(mOrigMessage != eUnidentifiedEvent && + mEvent->mEvent->mSpecifiedEventTypeString.IsEmpty(), + "Only use this class on events whose overridden type is " + "known (so we can restore it properly)"); + + mEvent->mEvent->mMessage = aOverridingMessage; + } + + ~EventMessageAutoOverride() { mEvent->mEvent->mMessage = mOrigMessage; } + + protected: + // Non-owning ref, which should be safe since we're a stack-allocated object + // with limited lifetime. Whoever creates us should keep mEvent alive. + Event* const MOZ_NON_OWNING_REF mEvent; + const EventMessage mOrigMessage; +}; + +class MOZ_STACK_CLASS WantsPopupControlCheck { + public: + explicit WantsPopupControlCheck(Event* aEvent) : mEvent(aEvent) { + mOriginalWantsPopupControlCheck = mEvent->GetWantsPopupControlCheck(); + mEvent->SetWantsPopupControlCheck(mEvent->IsTrusted()); + } + + ~WantsPopupControlCheck() { + mEvent->SetWantsPopupControlCheck(mOriginalWantsPopupControlCheck); + } + + private: + Event* mEvent; + bool mOriginalWantsPopupControlCheck; +}; + +NS_DEFINE_STATIC_IID_ACCESSOR(Event, NS_EVENT_IID) + +} // namespace mozilla::dom + +already_AddRefed<mozilla::dom::Event> NS_NewDOMEvent( + mozilla::dom::EventTarget* aOwner, nsPresContext* aPresContext, + mozilla::WidgetEvent* aEvent); + +#endif // mozilla_dom_Event_h_ diff --git a/dom/events/EventDispatcher.cpp b/dom/events/EventDispatcher.cpp new file mode 100644 index 0000000000..37e659763a --- /dev/null +++ b/dom/events/EventDispatcher.cpp @@ -0,0 +1,1539 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#include "nsPresContext.h" +#include "nsContentUtils.h" +#include "nsDocShell.h" +#include "nsError.h" +#include <new> +#include "nsIContent.h" +#include "nsIContentInlines.h" +#include "mozilla/dom/Document.h" +#include "nsINode.h" +#include "nsIScriptObjectPrincipal.h" +#include "nsPIDOMWindow.h" +#include "nsRefreshDriver.h" +#include "AnimationEvent.h" +#include "BeforeUnloadEvent.h" +#include "ClipboardEvent.h" +#include "CommandEvent.h" +#include "CompositionEvent.h" +#include "DeviceMotionEvent.h" +#include "DragEvent.h" +#include "KeyboardEvent.h" +#include "mozilla/BasePrincipal.h" +#include "mozilla/ContentEvents.h" +#include "mozilla/dom/CloseEvent.h" +#include "mozilla/dom/CustomEvent.h" +#include "mozilla/dom/DeviceOrientationEvent.h" +#include "mozilla/dom/EventTarget.h" +#include "mozilla/dom/FocusEvent.h" +#include "mozilla/dom/HashChangeEvent.h" +#include "mozilla/dom/InputEvent.h" +#include "mozilla/dom/MessageEvent.h" +#include "mozilla/dom/MouseScrollEvent.h" +#include "mozilla/dom/MutationEvent.h" +#include "mozilla/dom/NotifyPaintEvent.h" +#include "mozilla/dom/PageTransitionEvent.h" +#include "mozilla/dom/PerformanceEventTiming.h" +#include "mozilla/dom/PointerEvent.h" +#include "mozilla/dom/RootedDictionary.h" +#include "mozilla/dom/ScrollAreaEvent.h" +#include "mozilla/dom/SimpleGestureEvent.h" +#include "mozilla/dom/ScriptSettings.h" +#include "mozilla/dom/StorageEvent.h" +#include "mozilla/dom/TimeEvent.h" +#include "mozilla/dom/TouchEvent.h" +#include "mozilla/dom/TransitionEvent.h" +#include "mozilla/dom/WheelEvent.h" +#include "mozilla/dom/WorkerPrivate.h" +#include "mozilla/dom/XULCommandEvent.h" +#include "mozilla/EventDispatcher.h" +#include "mozilla/EventListenerManager.h" +#include "mozilla/InternalMutationEvent.h" +#include "mozilla/ipc/MessageChannel.h" +#include "mozilla/MiscEvents.h" +#include "mozilla/MouseEvents.h" +#include "mozilla/ProfilerLabels.h" +#include "mozilla/ProfilerMarkers.h" +#include "mozilla/ScopeExit.h" +#include "mozilla/Telemetry.h" +#include "mozilla/TextEvents.h" +#include "mozilla/TouchEvents.h" +#include "mozilla/Unused.h" + +namespace mozilla { + +using namespace dom; + +class ELMCreationDetector { + public: + ELMCreationDetector() + // We can do this optimization only in the main thread. + : mNonMainThread(!NS_IsMainThread()), + mInitialCount(mNonMainThread + ? 0 + : EventListenerManager::sMainThreadCreatedCount) {} + + bool MayHaveNewListenerManager() { + return mNonMainThread || + mInitialCount != EventListenerManager::sMainThreadCreatedCount; + } + + bool IsMainThread() { return !mNonMainThread; } + + private: + bool mNonMainThread; + uint32_t mInitialCount; +}; + +static bool IsEventTargetChrome(EventTarget* aEventTarget, + Document** aDocument = nullptr) { + if (aDocument) { + *aDocument = nullptr; + } + + Document* doc = nullptr; + if (nsINode* node = nsINode::FromEventTargetOrNull(aEventTarget)) { + doc = node->OwnerDoc(); + } else if (nsPIDOMWindowInner* window = + nsPIDOMWindowInner::FromEventTargetOrNull(aEventTarget)) { + doc = window->GetExtantDoc(); + } + + // nsContentUtils::IsChromeDoc is null-safe. + bool isChrome = false; + if (doc) { + isChrome = nsContentUtils::IsChromeDoc(doc); + if (aDocument) { + nsCOMPtr<Document> retVal = doc; + retVal.swap(*aDocument); + } + } else if (nsCOMPtr<nsIScriptObjectPrincipal> sop = + do_QueryInterface(aEventTarget->GetOwnerGlobal())) { + isChrome = sop->GetPrincipal()->IsSystemPrincipal(); + } + return isChrome; +} + +// EventTargetChainItem represents a single item in the event target chain. +class EventTargetChainItem { + public: + explicit EventTargetChainItem(EventTarget* aTarget) + : mTarget(aTarget), mItemFlags(0) { + MOZ_COUNT_CTOR(EventTargetChainItem); + } + + MOZ_COUNTED_DTOR(EventTargetChainItem) + + static EventTargetChainItem* Create(nsTArray<EventTargetChainItem>& aChain, + EventTarget* aTarget, + EventTargetChainItem* aChild = nullptr) { + // The last item which can handle the event must be aChild. + MOZ_ASSERT(GetLastCanHandleEventTarget(aChain) == aChild); + MOZ_ASSERT(!aTarget || aTarget == aTarget->GetTargetForEventTargetChain()); + EventTargetChainItem* etci = aChain.AppendElement(aTarget); + return etci; + } + + static void DestroyLast(nsTArray<EventTargetChainItem>& aChain, + EventTargetChainItem* aItem) { + MOZ_ASSERT(&aChain.LastElement() == aItem); + aChain.RemoveLastElement(); + } + + static EventTargetChainItem* GetFirstCanHandleEventTarget( + nsTArray<EventTargetChainItem>& aChain) { + return &aChain[GetFirstCanHandleEventTargetIdx(aChain)]; + } + + static uint32_t GetFirstCanHandleEventTargetIdx( + nsTArray<EventTargetChainItem>& aChain) { + // aChain[i].PreHandleEventOnly() = true only when the target element wants + // PreHandleEvent and set mCanHandle=false. So we find the first element + // which can handle the event. + for (uint32_t i = 0; i < aChain.Length(); ++i) { + if (!aChain[i].PreHandleEventOnly()) { + return i; + } + } + MOZ_ASSERT(false); + return 0; + } + + static EventTargetChainItem* GetLastCanHandleEventTarget( + nsTArray<EventTargetChainItem>& aChain) { + // Fine the last item which can handle the event. + for (int32_t i = aChain.Length() - 1; i >= 0; --i) { + if (!aChain[i].PreHandleEventOnly()) { + return &aChain[i]; + } + } + return nullptr; + } + + bool IsValid() const { + NS_WARNING_ASSERTION(!!(mTarget), "Event target is not valid!"); + return !!(mTarget); + } + + EventTarget* GetNewTarget() const { return mNewTarget; } + + void SetNewTarget(EventTarget* aNewTarget) { mNewTarget = aNewTarget; } + + EventTarget* GetRetargetedRelatedTarget() { return mRetargetedRelatedTarget; } + + void SetRetargetedRelatedTarget(EventTarget* aTarget) { + mRetargetedRelatedTarget = aTarget; + } + + void SetRetargetedTouchTarget( + Maybe<nsTArray<RefPtr<EventTarget>>>&& aTargets) { + mRetargetedTouchTargets = std::move(aTargets); + } + + bool HasRetargetTouchTargets() const { + return mRetargetedTouchTargets.isSome() || mInitialTargetTouches.isSome(); + } + + void RetargetTouchTargets(WidgetTouchEvent* aTouchEvent, Event* aDOMEvent) { + MOZ_ASSERT(HasRetargetTouchTargets()); + MOZ_ASSERT(aTouchEvent, + "mRetargetedTouchTargets should be empty when dispatching " + "non-touch events."); + + if (mRetargetedTouchTargets.isSome()) { + WidgetTouchEvent::TouchArray& touches = aTouchEvent->mTouches; + MOZ_ASSERT(!touches.Length() || + touches.Length() == mRetargetedTouchTargets->Length()); + for (uint32_t i = 0; i < touches.Length(); ++i) { + touches[i]->mTarget = mRetargetedTouchTargets->ElementAt(i); + } + } + + if (aDOMEvent) { + // The number of touch objects in targetTouches list may change depending + // on the retargeting. + TouchEvent* touchDOMEvent = static_cast<TouchEvent*>(aDOMEvent); + TouchList* targetTouches = touchDOMEvent->GetExistingTargetTouches(); + if (targetTouches) { + targetTouches->Clear(); + if (mInitialTargetTouches.isSome()) { + for (uint32_t i = 0; i < mInitialTargetTouches->Length(); ++i) { + Touch* touch = mInitialTargetTouches->ElementAt(i); + if (touch) { + touch->mTarget = touch->mOriginalTarget; + } + targetTouches->Append(touch); + } + } + } + } + } + + void SetInitialTargetTouches( + Maybe<nsTArray<RefPtr<dom::Touch>>>&& aInitialTargetTouches) { + mInitialTargetTouches = std::move(aInitialTargetTouches); + } + + void SetForceContentDispatch(bool aForce) { + mFlags.mForceContentDispatch = aForce; + } + + bool ForceContentDispatch() const { return mFlags.mForceContentDispatch; } + + void SetWantsWillHandleEvent(bool aWants) { + mFlags.mWantsWillHandleEvent = aWants; + } + + bool WantsWillHandleEvent() const { return mFlags.mWantsWillHandleEvent; } + + void SetWantsPreHandleEvent(bool aWants) { + mFlags.mWantsPreHandleEvent = aWants; + } + + bool WantsPreHandleEvent() const { return mFlags.mWantsPreHandleEvent; } + + void SetPreHandleEventOnly(bool aWants) { + mFlags.mPreHandleEventOnly = aWants; + } + + bool PreHandleEventOnly() const { return mFlags.mPreHandleEventOnly; } + + void SetRootOfClosedTree(bool aSet) { mFlags.mRootOfClosedTree = aSet; } + + bool IsRootOfClosedTree() const { return mFlags.mRootOfClosedTree; } + + void SetItemInShadowTree(bool aSet) { mFlags.mItemInShadowTree = aSet; } + + bool IsItemInShadowTree() const { return mFlags.mItemInShadowTree; } + + void SetIsSlotInClosedTree(bool aSet) { mFlags.mIsSlotInClosedTree = aSet; } + + bool IsSlotInClosedTree() const { return mFlags.mIsSlotInClosedTree; } + + void SetIsChromeHandler(bool aSet) { mFlags.mIsChromeHandler = aSet; } + + bool IsChromeHandler() const { return mFlags.mIsChromeHandler; } + + void SetMayHaveListenerManager(bool aMayHave) { + mFlags.mMayHaveManager = aMayHave; + } + + bool MayHaveListenerManager() { return mFlags.mMayHaveManager; } + + EventTarget* CurrentTarget() const { return mTarget; } + + /** + * Dispatches event through the event target chain. + * Handles capture, target and bubble phases both in default + * and system event group and calls also PostHandleEvent for each + * item in the chain. + */ + MOZ_CAN_RUN_SCRIPT + static void HandleEventTargetChain(nsTArray<EventTargetChainItem>& aChain, + EventChainPostVisitor& aVisitor, + EventDispatchingCallback* aCallback, + ELMCreationDetector& aCd); + + /** + * Resets aVisitor object and calls GetEventTargetParent. + * Copies mItemFlags and mItemData to the current EventTargetChainItem. + */ + void GetEventTargetParent(EventChainPreVisitor& aVisitor); + + /** + * Calls PreHandleEvent for those items which called SetWantsPreHandleEvent. + */ + void PreHandleEvent(EventChainVisitor& aVisitor); + + /** + * If the current item in the event target chain has an event listener + * manager, this method calls EventListenerManager::HandleEvent(). + */ + void HandleEvent(EventChainPostVisitor& aVisitor, ELMCreationDetector& aCd) { + if (WantsWillHandleEvent()) { + mTarget->WillHandleEvent(aVisitor); + } + if (aVisitor.mEvent->PropagationStopped()) { + return; + } + if (aVisitor.mEvent->mFlags.mOnlySystemGroupDispatch && + !aVisitor.mEvent->mFlags.mInSystemGroup) { + return; + } + if (aVisitor.mEvent->mFlags.mOnlySystemGroupDispatchInContent && + !aVisitor.mEvent->mFlags.mInSystemGroup && !IsCurrentTargetChrome()) { + return; + } + if (!mManager) { + if (!MayHaveListenerManager() && !aCd.MayHaveNewListenerManager()) { + return; + } + mManager = mTarget->GetExistingListenerManager(); + } + if (mManager) { + NS_ASSERTION(aVisitor.mEvent->mCurrentTarget == nullptr, + "CurrentTarget should be null!"); + + mManager->HandleEvent(aVisitor.mPresContext, aVisitor.mEvent, + &aVisitor.mDOMEvent, CurrentTarget(), + &aVisitor.mEventStatus, IsItemInShadowTree()); + NS_ASSERTION(aVisitor.mEvent->mCurrentTarget == nullptr, + "CurrentTarget should be null!"); + } + } + + /** + * Copies mItemFlags and mItemData to aVisitor and calls PostHandleEvent. + */ + MOZ_CAN_RUN_SCRIPT void PostHandleEvent(EventChainPostVisitor& aVisitor); + + private: + const nsCOMPtr<EventTarget> mTarget; + nsCOMPtr<EventTarget> mRetargetedRelatedTarget; + Maybe<nsTArray<RefPtr<EventTarget>>> mRetargetedTouchTargets; + Maybe<nsTArray<RefPtr<dom::Touch>>> mInitialTargetTouches; + + class EventTargetChainFlags { + public: + explicit EventTargetChainFlags() { SetRawFlags(0); } + // Cached flags for each EventTargetChainItem which are set when calling + // GetEventTargetParent to create event target chain. They are used to + // manage or speedup event dispatching. + bool mForceContentDispatch : 1; + bool mWantsWillHandleEvent : 1; + bool mMayHaveManager : 1; + bool mChechedIfChrome : 1; + bool mIsChromeContent : 1; + bool mWantsPreHandleEvent : 1; + bool mPreHandleEventOnly : 1; + bool mRootOfClosedTree : 1; + bool mItemInShadowTree : 1; + bool mIsSlotInClosedTree : 1; + bool mIsChromeHandler : 1; + + private: + using RawFlags = uint32_t; + void SetRawFlags(RawFlags aRawFlags) { + static_assert( + sizeof(EventTargetChainFlags) <= sizeof(RawFlags), + "EventTargetChainFlags must not be bigger than the RawFlags"); + memcpy(this, &aRawFlags, sizeof(EventTargetChainFlags)); + } + } mFlags; + + uint16_t mItemFlags; + nsCOMPtr<nsISupports> mItemData; + // Event retargeting must happen whenever mNewTarget is non-null. + nsCOMPtr<EventTarget> mNewTarget; + // Cache mTarget's event listener manager. + RefPtr<EventListenerManager> mManager; + + bool IsCurrentTargetChrome() { + if (!mFlags.mChechedIfChrome) { + mFlags.mChechedIfChrome = true; + if (IsEventTargetChrome(mTarget)) { + mFlags.mIsChromeContent = true; + } + } + return mFlags.mIsChromeContent; + } +}; + +void EventTargetChainItem::GetEventTargetParent( + EventChainPreVisitor& aVisitor) { + aVisitor.Reset(); + mTarget->GetEventTargetParent(aVisitor); + SetForceContentDispatch(aVisitor.mForceContentDispatch); + SetWantsWillHandleEvent(aVisitor.mWantsWillHandleEvent); + SetMayHaveListenerManager(aVisitor.mMayHaveListenerManager); + SetWantsPreHandleEvent(aVisitor.mWantsPreHandleEvent); + SetPreHandleEventOnly(aVisitor.mWantsPreHandleEvent && !aVisitor.mCanHandle); + SetRootOfClosedTree(aVisitor.mRootOfClosedTree); + SetItemInShadowTree(aVisitor.mItemInShadowTree); + SetRetargetedRelatedTarget(aVisitor.mRetargetedRelatedTarget); + SetRetargetedTouchTarget(std::move(aVisitor.mRetargetedTouchTargets)); + mItemFlags = aVisitor.mItemFlags; + mItemData = aVisitor.mItemData; +} + +void EventTargetChainItem::PreHandleEvent(EventChainVisitor& aVisitor) { + if (!WantsPreHandleEvent()) { + return; + } + aVisitor.mItemFlags = mItemFlags; + aVisitor.mItemData = mItemData; + Unused << mTarget->PreHandleEvent(aVisitor); +} + +void EventTargetChainItem::PostHandleEvent(EventChainPostVisitor& aVisitor) { + aVisitor.mItemFlags = mItemFlags; + aVisitor.mItemData = mItemData; + mTarget->PostHandleEvent(aVisitor); +} + +void EventTargetChainItem::HandleEventTargetChain( + nsTArray<EventTargetChainItem>& aChain, EventChainPostVisitor& aVisitor, + EventDispatchingCallback* aCallback, ELMCreationDetector& aCd) { + // Save the target so that it can be restored later. + nsCOMPtr<EventTarget> firstTarget = aVisitor.mEvent->mTarget; + nsCOMPtr<EventTarget> firstRelatedTarget = aVisitor.mEvent->mRelatedTarget; + Maybe<AutoTArray<nsCOMPtr<EventTarget>, 10>> firstTouchTargets; + WidgetTouchEvent* touchEvent = nullptr; + if (aVisitor.mEvent->mClass == eTouchEventClass) { + touchEvent = aVisitor.mEvent->AsTouchEvent(); + if (!aVisitor.mEvent->mFlags.mInSystemGroup) { + firstTouchTargets.emplace(); + WidgetTouchEvent* touchEvent = aVisitor.mEvent->AsTouchEvent(); + WidgetTouchEvent::TouchArray& touches = touchEvent->mTouches; + for (uint32_t i = 0; i < touches.Length(); ++i) { + firstTouchTargets->AppendElement(touches[i]->mTarget); + } + } + } + + uint32_t chainLength = aChain.Length(); + EventTargetChainItem* chain = aChain.Elements(); + uint32_t firstCanHandleEventTargetIdx = + EventTargetChainItem::GetFirstCanHandleEventTargetIdx(aChain); + + // Capture + aVisitor.mEvent->mFlags.mInCapturePhase = true; + aVisitor.mEvent->mFlags.mInBubblingPhase = false; + for (uint32_t i = chainLength - 1; i > firstCanHandleEventTargetIdx; --i) { + EventTargetChainItem& item = chain[i]; + if (item.PreHandleEventOnly()) { + continue; + } + if ((!aVisitor.mEvent->mFlags.mNoContentDispatch || + item.ForceContentDispatch()) && + !aVisitor.mEvent->PropagationStopped()) { + item.HandleEvent(aVisitor, aCd); + } + + if (item.GetNewTarget()) { + // item is at anonymous boundary. Need to retarget for the child items. + for (uint32_t j = i; j > 0; --j) { + uint32_t childIndex = j - 1; + EventTarget* newTarget = chain[childIndex].GetNewTarget(); + if (newTarget) { + aVisitor.mEvent->mTarget = newTarget; + break; + } + } + } + + // https://dom.spec.whatwg.org/#dispatching-events + // Step 14.2 + // "Set event's relatedTarget to tuple's relatedTarget." + // Note, the initial retargeting was done already when creating + // event target chain, so we need to do this only after calling + // HandleEvent, not before, like in the specification. + if (item.GetRetargetedRelatedTarget()) { + bool found = false; + for (uint32_t j = i; j > 0; --j) { + uint32_t childIndex = j - 1; + EventTarget* relatedTarget = + chain[childIndex].GetRetargetedRelatedTarget(); + if (relatedTarget) { + found = true; + aVisitor.mEvent->mRelatedTarget = relatedTarget; + break; + } + } + if (!found) { + aVisitor.mEvent->mRelatedTarget = + aVisitor.mEvent->mOriginalRelatedTarget; + } + } + + if (item.HasRetargetTouchTargets()) { + bool found = false; + for (uint32_t j = i; j > 0; --j) { + uint32_t childIndex = j - 1; + if (chain[childIndex].HasRetargetTouchTargets()) { + found = true; + chain[childIndex].RetargetTouchTargets(touchEvent, + aVisitor.mDOMEvent); + break; + } + } + if (!found) { + WidgetTouchEvent::TouchArray& touches = touchEvent->mTouches; + for (uint32_t i = 0; i < touches.Length(); ++i) { + touches[i]->mTarget = touches[i]->mOriginalTarget; + } + } + } + } + + // Target + aVisitor.mEvent->mFlags.mInBubblingPhase = true; + EventTargetChainItem& targetItem = chain[firstCanHandleEventTargetIdx]; + // Need to explicitly retarget touch targets so that initial targets get set + // properly in case nothing else retargeted touches. + if (targetItem.HasRetargetTouchTargets()) { + targetItem.RetargetTouchTargets(touchEvent, aVisitor.mDOMEvent); + } + if (!aVisitor.mEvent->PropagationStopped() && + (!aVisitor.mEvent->mFlags.mNoContentDispatch || + targetItem.ForceContentDispatch())) { + targetItem.HandleEvent(aVisitor, aCd); + } + if (aVisitor.mEvent->mFlags.mInSystemGroup) { + targetItem.PostHandleEvent(aVisitor); + } + + // Bubble + aVisitor.mEvent->mFlags.mInCapturePhase = false; + for (uint32_t i = firstCanHandleEventTargetIdx + 1; i < chainLength; ++i) { + EventTargetChainItem& item = chain[i]; + if (item.PreHandleEventOnly()) { + continue; + } + EventTarget* newTarget = item.GetNewTarget(); + if (newTarget) { + // Item is at anonymous boundary. Need to retarget for the current item + // and for parent items. + aVisitor.mEvent->mTarget = newTarget; + } + + // https://dom.spec.whatwg.org/#dispatching-events + // Step 15.2 + // "Set event's relatedTarget to tuple's relatedTarget." + EventTarget* relatedTarget = item.GetRetargetedRelatedTarget(); + if (relatedTarget) { + aVisitor.mEvent->mRelatedTarget = relatedTarget; + } + + if (item.HasRetargetTouchTargets()) { + item.RetargetTouchTargets(touchEvent, aVisitor.mDOMEvent); + } + + if (aVisitor.mEvent->mFlags.mBubbles || newTarget) { + if ((!aVisitor.mEvent->mFlags.mNoContentDispatch || + item.ForceContentDispatch()) && + !aVisitor.mEvent->PropagationStopped()) { + item.HandleEvent(aVisitor, aCd); + } + if (aVisitor.mEvent->mFlags.mInSystemGroup) { + item.PostHandleEvent(aVisitor); + } + } + } + aVisitor.mEvent->mFlags.mInBubblingPhase = false; + + if (!aVisitor.mEvent->mFlags.mInSystemGroup && + aVisitor.mEvent->IsAllowedToDispatchInSystemGroup()) { + // Dispatch to the system event group. Make sure to clear the + // STOP_DISPATCH flag since this resets for each event group. + aVisitor.mEvent->mFlags.mPropagationStopped = false; + aVisitor.mEvent->mFlags.mImmediatePropagationStopped = false; + + // Setting back the original target of the event. + aVisitor.mEvent->mTarget = aVisitor.mEvent->mOriginalTarget; + aVisitor.mEvent->mRelatedTarget = aVisitor.mEvent->mOriginalRelatedTarget; + if (firstTouchTargets) { + WidgetTouchEvent::TouchArray& touches = touchEvent->mTouches; + for (uint32_t i = 0; i < touches.Length(); ++i) { + touches[i]->mTarget = touches[i]->mOriginalTarget; + } + } + + // Special handling if PresShell (or some other caller) + // used a callback object. + if (aCallback) { + aCallback->HandleEvent(aVisitor); + } + + // Retarget for system event group (which does the default handling too). + // Setting back the target which was used also for default event group. + aVisitor.mEvent->mTarget = firstTarget; + aVisitor.mEvent->mRelatedTarget = firstRelatedTarget; + if (firstTouchTargets) { + WidgetTouchEvent::TouchArray& touches = touchEvent->mTouches; + for (uint32_t i = 0; i < firstTouchTargets->Length(); ++i) { + touches[i]->mTarget = firstTouchTargets->ElementAt(i); + } + } + + aVisitor.mEvent->mFlags.mInSystemGroup = true; + HandleEventTargetChain(aChain, aVisitor, aCallback, aCd); + aVisitor.mEvent->mFlags.mInSystemGroup = false; + + // After dispatch, clear all the propagation flags so that + // system group listeners don't affect to the event. + aVisitor.mEvent->mFlags.mPropagationStopped = false; + aVisitor.mEvent->mFlags.mImmediatePropagationStopped = false; + } +} + +// There are often 2 nested event dispatches ongoing at the same time, so +// have 2 separate caches. +static const uint32_t kCachedMainThreadChainSize = 128; +struct CachedChains { + nsTArray<EventTargetChainItem> mChain1; + nsTArray<EventTargetChainItem> mChain2; +}; +static CachedChains* sCachedMainThreadChains = nullptr; + +/* static */ +void EventDispatcher::Shutdown() { + delete sCachedMainThreadChains; + sCachedMainThreadChains = nullptr; +} + +EventTargetChainItem* EventTargetChainItemForChromeTarget( + nsTArray<EventTargetChainItem>& aChain, nsINode* aNode, + EventTargetChainItem* aChild = nullptr) { + if (!aNode->IsInComposedDoc()) { + return nullptr; + } + nsPIDOMWindowInner* win = aNode->OwnerDoc()->GetInnerWindow(); + EventTarget* piTarget = win ? win->GetParentTarget() : nullptr; + NS_ENSURE_TRUE(piTarget, nullptr); + + EventTargetChainItem* etci = EventTargetChainItem::Create( + aChain, piTarget->GetTargetForEventTargetChain(), aChild); + if (!etci->IsValid()) { + EventTargetChainItem::DestroyLast(aChain, etci); + return nullptr; + } + return etci; +} + +/* static */ EventTargetChainItem* MayRetargetToChromeIfCanNotHandleEvent( + nsTArray<EventTargetChainItem>& aChain, EventChainPreVisitor& aPreVisitor, + EventTargetChainItem* aTargetEtci, EventTargetChainItem* aChildEtci, + nsINode* aContent) { + if (!aPreVisitor.mWantsPreHandleEvent) { + // Keep EventTargetChainItem if we need to call PreHandleEvent on it. + EventTargetChainItem::DestroyLast(aChain, aTargetEtci); + } + if (aPreVisitor.mAutomaticChromeDispatch && aContent) { + aPreVisitor.mRelatedTargetRetargetedInCurrentScope = false; + // Event target couldn't handle the event. Try to propagate to chrome. + EventTargetChainItem* chromeTargetEtci = + EventTargetChainItemForChromeTarget(aChain, aContent, aChildEtci); + if (chromeTargetEtci) { + // If we propagate to chrome, need to ensure we mark + // EventTargetChainItem to be chrome handler so that event.composedPath() + // can return the right value. + chromeTargetEtci->SetIsChromeHandler(true); + chromeTargetEtci->GetEventTargetParent(aPreVisitor); + return chromeTargetEtci; + } + } + return nullptr; +} + +static bool ShouldClearTargets(WidgetEvent* aEvent) { + if (nsIContent* finalTarget = + nsIContent::FromEventTargetOrNull(aEvent->mTarget)) { + if (finalTarget->SubtreeRoot()->IsShadowRoot()) { + return true; + } + } + + if (nsIContent* finalRelatedTarget = + nsIContent::FromEventTargetOrNull(aEvent->mRelatedTarget)) { + if (finalRelatedTarget->SubtreeRoot()->IsShadowRoot()) { + return true; + } + } + // XXXsmaug Check also all the touch objects. + + return false; +} + +static void DescribeEventTargetForProfilerMarker(const EventTarget* aTarget, + nsACString& aDescription) { + auto* node = aTarget->GetAsNode(); + if (node) { + if (node->IsElement()) { + nsAutoString nodeDescription; + node->AsElement()->Describe(nodeDescription, true); + aDescription = NS_ConvertUTF16toUTF8(nodeDescription); + } else if (node->IsDocument()) { + aDescription.AssignLiteral("document"); + } else if (node->IsText()) { + aDescription.AssignLiteral("text"); + } else if (node->IsDocumentFragment()) { + aDescription.AssignLiteral("document fragment"); + } + } else if (aTarget->IsInnerWindow() || aTarget->IsOuterWindow()) { + aDescription.AssignLiteral("window"); + } else if (aTarget->IsRootWindow()) { + aDescription.AssignLiteral("root window"); + } else { + // Probably something that inherits from DOMEventTargetHelper. + } +} + +/* static */ +nsresult EventDispatcher::Dispatch(nsISupports* aTarget, + nsPresContext* aPresContext, + WidgetEvent* aEvent, Event* aDOMEvent, + nsEventStatus* aEventStatus, + EventDispatchingCallback* aCallback, + nsTArray<EventTarget*>* aTargets) { + AUTO_PROFILER_LABEL("EventDispatcher::Dispatch", OTHER); + + NS_ASSERTION(aEvent, "Trying to dispatch without WidgetEvent!"); + NS_ENSURE_TRUE(!aEvent->mFlags.mIsBeingDispatched, + NS_ERROR_DOM_INVALID_STATE_ERR); + NS_ASSERTION(!aTargets || !aEvent->mMessage, "Wrong parameters!"); + + // If we're dispatching an already created DOMEvent object, make + // sure it is initialized! + // If aTargets is non-null, the event isn't going to be dispatched. + NS_ENSURE_TRUE(aEvent->mMessage || !aDOMEvent || aTargets, + NS_ERROR_DOM_INVALID_STATE_ERR); + + // Events shall not be fired while we are in stable state to prevent anything + // visible from the scripts. + MOZ_ASSERT(!nsContentUtils::IsInStableOrMetaStableState()); + NS_ENSURE_TRUE(!nsContentUtils::IsInStableOrMetaStableState(), + NS_ERROR_DOM_INVALID_STATE_ERR); + + nsCOMPtr<EventTarget> target = do_QueryInterface(aTarget); + + RefPtr<PerformanceEventTiming> eventTimingEntry; + // Similar to PerformancePaintTiming, we don't need to + // expose them for printing documents + if (aPresContext && !aPresContext->IsPrintingOrPrintPreview()) { + eventTimingEntry = + PerformanceEventTiming::TryGenerateEventTiming(target, aEvent); + } + + bool retargeted = false; + + if (aEvent->mFlags.mRetargetToNonNativeAnonymous) { + nsIContent* content = nsIContent::FromEventTargetOrNull(target); + if (content && content->IsInNativeAnonymousSubtree()) { + nsCOMPtr<EventTarget> newTarget = + content->FindFirstNonChromeOnlyAccessContent(); + NS_ENSURE_STATE(newTarget); + + aEvent->mOriginalTarget = target; + target = newTarget; + retargeted = true; + } + } + + if (aEvent->mFlags.mOnlyChromeDispatch) { + nsCOMPtr<Document> doc; + if (!IsEventTargetChrome(target, getter_AddRefs(doc)) && doc) { + nsPIDOMWindowInner* win = doc->GetInnerWindow(); + // If we can't dispatch the event to chrome, do nothing. + EventTarget* piTarget = win ? win->GetParentTarget() : nullptr; + if (!piTarget) { + return NS_OK; + } + + // Set the target to be the original dispatch target, + aEvent->mTarget = target; + // but use chrome event handler or BrowserChildMessageManager for event + // target chain. + target = piTarget; + } else if (NS_WARN_IF(!doc)) { + return NS_ERROR_UNEXPECTED; + } + } + +#ifdef DEBUG + if (NS_IsMainThread() && aEvent->mMessage != eVoidEvent && + !nsContentUtils::IsSafeToRunScript()) { + static const auto warn = [](bool aIsSystem) { + if (aIsSystem) { + NS_WARNING("Fix the caller!"); + } else { + MOZ_CRASH("This is unsafe! Fix the caller!"); + } + }; + if (nsINode* node = nsINode::FromEventTargetOrNull(target)) { + // If this is a node, it's possible that this is some sort of DOM tree + // that is never accessed by script (for example an SVG image or XBL + // binding document or whatnot). We really only want to warn/assert here + // if there might be actual scripted listeners for this event, so restrict + // the warnings/asserts to the case when script can or once could touch + // this node's document. + Document* doc = node->OwnerDoc(); + bool hasHadScriptHandlingObject; + nsIGlobalObject* global = + doc->GetScriptHandlingObject(hasHadScriptHandlingObject); + if (global || hasHadScriptHandlingObject) { + warn(nsContentUtils::IsChromeDoc(doc)); + } + } else if (nsCOMPtr<nsIGlobalObject> global = target->GetOwnerGlobal()) { + warn(global->PrincipalOrNull()->IsSystemPrincipal()); + } + } + + if (aDOMEvent) { + WidgetEvent* innerEvent = aDOMEvent->WidgetEventPtr(); + NS_ASSERTION(innerEvent == aEvent, + "The inner event of aDOMEvent is not the same as aEvent!"); + } +#endif + + nsresult rv = NS_OK; + bool externalDOMEvent = !!(aDOMEvent); + + // If we have a PresContext, make sure it doesn't die before + // event dispatching is finished. + RefPtr<nsPresContext> kungFuDeathGrip(aPresContext); + + ELMCreationDetector cd; + nsTArray<EventTargetChainItem> chain; + if (cd.IsMainThread()) { + if (!sCachedMainThreadChains) { + sCachedMainThreadChains = new CachedChains(); + } + + if (sCachedMainThreadChains->mChain1.Capacity() == + kCachedMainThreadChainSize) { + chain = std::move(sCachedMainThreadChains->mChain1); + } else if (sCachedMainThreadChains->mChain2.Capacity() == + kCachedMainThreadChainSize) { + chain = std::move(sCachedMainThreadChains->mChain2); + } else { + chain.SetCapacity(kCachedMainThreadChainSize); + } + } + + // Create the event target chain item for the event target. + EventTargetChainItem* targetEtci = EventTargetChainItem::Create( + chain, target->GetTargetForEventTargetChain()); + MOZ_ASSERT(&chain[0] == targetEtci); + if (!targetEtci->IsValid()) { + EventTargetChainItem::DestroyLast(chain, targetEtci); + return NS_ERROR_FAILURE; + } + + // Make sure that Event::target and Event::originalTarget + // point to the last item in the chain. + if (!aEvent->mTarget) { + // Note, CurrentTarget() points always to the object returned by + // GetTargetForEventTargetChain(). + aEvent->mTarget = targetEtci->CurrentTarget(); + } else { + // XXX But if the target is already set, use that. This is a hack + // for the 'load', 'beforeunload' and 'unload' events, + // which are dispatched to |window| but have document as their target. + // + // Make sure that the event target points to the right object. + aEvent->mTarget = aEvent->mTarget->GetTargetForEventTargetChain(); + NS_ENSURE_STATE(aEvent->mTarget); + } + + if (retargeted) { + aEvent->mOriginalTarget = + aEvent->mOriginalTarget->GetTargetForEventTargetChain(); + NS_ENSURE_STATE(aEvent->mOriginalTarget); + } else { + aEvent->mOriginalTarget = aEvent->mTarget; + } + + aEvent->mOriginalRelatedTarget = aEvent->mRelatedTarget; + + bool clearTargets = false; + + nsCOMPtr<nsIContent> content = + nsIContent::FromEventTargetOrNull(aEvent->mOriginalTarget); + bool isInAnon = content && content->IsInNativeAnonymousSubtree(); + + aEvent->mFlags.mIsBeingDispatched = true; + + // Create visitor object and start event dispatching. + // GetEventTargetParent for the original target. + nsEventStatus status = aDOMEvent && aDOMEvent->DefaultPrevented() + ? nsEventStatus_eConsumeNoDefault + : aEventStatus ? *aEventStatus + : nsEventStatus_eIgnore; + nsCOMPtr<EventTarget> targetForPreVisitor = aEvent->mTarget; + EventChainPreVisitor preVisitor(aPresContext, aEvent, aDOMEvent, status, + isInAnon, targetForPreVisitor); + targetEtci->GetEventTargetParent(preVisitor); + + if (!preVisitor.mCanHandle) { + targetEtci = MayRetargetToChromeIfCanNotHandleEvent( + chain, preVisitor, targetEtci, nullptr, content); + } + if (!preVisitor.mCanHandle) { + // The original target and chrome target (mAutomaticChromeDispatch=true) + // can not handle the event but we still have to call their PreHandleEvent. + for (uint32_t i = 0; i < chain.Length(); ++i) { + chain[i].PreHandleEvent(preVisitor); + } + + clearTargets = ShouldClearTargets(aEvent); + } else { + // At least the original target can handle the event. + // Setting the retarget to the |target| simplifies retargeting code. + nsCOMPtr<EventTarget> t = aEvent->mTarget; + targetEtci->SetNewTarget(t); + // In order to not change the targetTouches array passed to TouchEvents + // when dispatching events from JS, we need to store the initial Touch + // objects on the list. + if (aEvent->mClass == eTouchEventClass && aDOMEvent) { + TouchEvent* touchEvent = static_cast<TouchEvent*>(aDOMEvent); + TouchList* targetTouches = touchEvent->GetExistingTargetTouches(); + if (targetTouches) { + Maybe<nsTArray<RefPtr<dom::Touch>>> initialTargetTouches; + initialTargetTouches.emplace(); + for (uint32_t i = 0; i < targetTouches->Length(); ++i) { + initialTargetTouches->AppendElement(targetTouches->Item(i)); + } + targetEtci->SetInitialTargetTouches(std::move(initialTargetTouches)); + targetTouches->Clear(); + } + } + EventTargetChainItem* topEtci = targetEtci; + targetEtci = nullptr; + while (preVisitor.GetParentTarget()) { + EventTarget* parentTarget = preVisitor.GetParentTarget(); + EventTargetChainItem* parentEtci = + EventTargetChainItem::Create(chain, parentTarget, topEtci); + if (!parentEtci->IsValid()) { + EventTargetChainItem::DestroyLast(chain, parentEtci); + rv = NS_ERROR_FAILURE; + break; + } + + parentEtci->SetIsSlotInClosedTree(preVisitor.mParentIsSlotInClosedTree); + parentEtci->SetIsChromeHandler(preVisitor.mParentIsChromeHandler); + + // Item needs event retargetting. + if (preVisitor.mEventTargetAtParent) { + // Need to set the target of the event + // so that also the next retargeting works. + preVisitor.mTargetInKnownToBeHandledScope = preVisitor.mEvent->mTarget; + preVisitor.mEvent->mTarget = preVisitor.mEventTargetAtParent; + parentEtci->SetNewTarget(preVisitor.mEventTargetAtParent); + } + + if (preVisitor.mRetargetedRelatedTarget) { + preVisitor.mEvent->mRelatedTarget = preVisitor.mRetargetedRelatedTarget; + } + + parentEtci->GetEventTargetParent(preVisitor); + if (preVisitor.mCanHandle) { + preVisitor.mTargetInKnownToBeHandledScope = preVisitor.mEvent->mTarget; + topEtci = parentEtci; + } else { + bool ignoreBecauseOfShadowDOM = preVisitor.mIgnoreBecauseOfShadowDOM; + nsCOMPtr<nsINode> disabledTarget = + nsINode::FromEventTargetOrNull(parentTarget); + parentEtci = MayRetargetToChromeIfCanNotHandleEvent( + chain, preVisitor, parentEtci, topEtci, disabledTarget); + if (parentEtci && preVisitor.mCanHandle) { + preVisitor.mTargetInKnownToBeHandledScope = + preVisitor.mEvent->mTarget; + EventTargetChainItem* item = + EventTargetChainItem::GetFirstCanHandleEventTarget(chain); + if (!ignoreBecauseOfShadowDOM) { + // If we ignored the target because of Shadow DOM retargeting, we + // shouldn't treat the target to be in the event path at all. + item->SetNewTarget(parentTarget); + } + topEtci = parentEtci; + continue; + } + break; + } + } + if (NS_SUCCEEDED(rv)) { + if (aTargets) { + aTargets->Clear(); + uint32_t numTargets = chain.Length(); + EventTarget** targets = aTargets->AppendElements(numTargets); + for (uint32_t i = 0; i < numTargets; ++i) { + targets[i] = chain[i].CurrentTarget()->GetTargetForDOMEvent(); + } + } else { + // Event target chain is created. PreHandle the chain. + for (uint32_t i = 0; i < chain.Length(); ++i) { + chain[i].PreHandleEvent(preVisitor); + } + + RefPtr<nsRefreshDriver> refreshDriver; + if (aEvent->IsTrusted() && + (aEvent->mMessage == eKeyPress || + aEvent->mMessage == eMouseClick) && + aPresContext && aPresContext->GetRootPresContext()) { + refreshDriver = aPresContext->GetRootPresContext()->RefreshDriver(); + if (refreshDriver) { + refreshDriver->EnterUserInputProcessing(); + } + } + auto cleanup = MakeScopeExit([&] { + if (refreshDriver) { + refreshDriver->ExitUserInputProcessing(); + } + }); + + clearTargets = ShouldClearTargets(aEvent); + + // Handle the chain. + EventChainPostVisitor postVisitor(preVisitor); + MOZ_RELEASE_ASSERT(!aEvent->mPath); + aEvent->mPath = &chain; + + if (profiler_is_active()) { + // Add a profiler label and a profiler marker for the actual + // dispatch of the event. + // This is a very hot code path, so we need to make sure not to + // do this extra work when we're not profiling. + if (!postVisitor.mDOMEvent) { + // This is tiny bit slow, but happens only once per event. + // Similar code also in EventListenerManager. + nsCOMPtr<EventTarget> et = aEvent->mOriginalTarget; + RefPtr<Event> event = + EventDispatcher::CreateEvent(et, aPresContext, aEvent, u""_ns); + event.swap(postVisitor.mDOMEvent); + } + nsAutoString typeStr; + postVisitor.mDOMEvent->GetType(typeStr); + AUTO_PROFILER_LABEL_DYNAMIC_LOSSY_NSSTRING( + "EventDispatcher::Dispatch", OTHER, typeStr); + + nsCOMPtr<nsIDocShell> docShell; + docShell = nsContentUtils::GetDocShellForEventTarget(aEvent->mTarget); + MarkerInnerWindowId innerWindowId; + if (nsCOMPtr<nsPIDOMWindowInner> inner = + do_QueryInterface(aEvent->mTarget->GetOwnerGlobal())) { + innerWindowId = MarkerInnerWindowId{inner->WindowID()}; + } + + struct DOMEventMarker { + static constexpr Span<const char> MarkerTypeName() { + return MakeStringSpan("DOMEvent"); + } + static void StreamJSONMarkerData( + baseprofiler::SpliceableJSONWriter& aWriter, + const ProfilerString16View& aEventType, + const nsCString& aTarget, const TimeStamp& aStartTime, + const TimeStamp& aEventTimeStamp) { + aWriter.StringProperty("eventType", + NS_ConvertUTF16toUTF8(aEventType)); + if (!aTarget.IsEmpty()) { + aWriter.StringProperty("target", aTarget); + } + // This is the event processing latency, which is the time from + // when the event was created, to when it was started to be + // processed. Note that the computation of this latency is + // deferred until serialization time, at the expense of some extra + // memory. + aWriter.DoubleProperty( + "latency", (aStartTime - aEventTimeStamp).ToMilliseconds()); + } + static MarkerSchema MarkerTypeDisplay() { + using MS = MarkerSchema; + MS schema{MS::Location::MarkerChart, MS::Location::MarkerTable, + MS::Location::TimelineOverview}; + schema.SetChartLabel("{marker.data.eventType}"); + schema.SetTooltipLabel("{marker.data.eventType} - DOMEvent"); + schema.SetTableLabel( + "{marker.data.eventType} - {marker.data.target}"); + schema.AddKeyLabelFormatSearchable("target", "Event Target", + MS::Format::String, + MS::Searchable::Searchable); + schema.AddKeyLabelFormat("latency", "Latency", + MS::Format::Duration); + schema.AddKeyLabelFormatSearchable("eventType", "Event Type", + MS::Format::String, + MS::Searchable::Searchable); + return schema; + } + }; + + nsAutoCString target; + DescribeEventTargetForProfilerMarker(aEvent->mTarget, target); + + auto startTime = TimeStamp::Now(); + profiler_add_marker("DOMEvent", geckoprofiler::category::DOM, + {MarkerTiming::IntervalStart(), + MarkerInnerWindowId(innerWindowId)}, + DOMEventMarker{}, typeStr, target, startTime, + aEvent->mTimeStamp); + + EventTargetChainItem::HandleEventTargetChain(chain, postVisitor, + aCallback, cd); + + profiler_add_marker( + "DOMEvent", geckoprofiler::category::DOM, + {MarkerTiming::IntervalEnd(), std::move(innerWindowId)}, + DOMEventMarker{}, typeStr, target, startTime, aEvent->mTimeStamp); + } else { + EventTargetChainItem::HandleEventTargetChain(chain, postVisitor, + aCallback, cd); + } + aEvent->mPath = nullptr; + + if (aEvent->IsTrusted() && + (aEvent->mMessage == eKeyPress || + aEvent->mMessage == eMouseClick) && + aPresContext && aPresContext->GetRootPresContext()) { + nsRefreshDriver* driver = + aPresContext->GetRootPresContext()->RefreshDriver(); + if (driver && driver->HasPendingTick()) { + switch (aEvent->mMessage) { + case eKeyPress: + driver->RegisterCompositionPayload( + {layers::CompositionPayloadType::eKeyPress, + aEvent->mTimeStamp}); + break; + case eMouseClick: { + if (aEvent->AsMouseEvent()->mInputSource == + MouseEvent_Binding::MOZ_SOURCE_MOUSE || + aEvent->AsMouseEvent()->mInputSource == + MouseEvent_Binding::MOZ_SOURCE_TOUCH) { + driver->RegisterCompositionPayload( + {layers::CompositionPayloadType::eMouseUpFollowedByClick, + aEvent->mTimeStamp}); + } + break; + } + default: + break; + } + } + } + + preVisitor.mEventStatus = postVisitor.mEventStatus; + // If the DOM event was created during event flow. + if (!preVisitor.mDOMEvent && postVisitor.mDOMEvent) { + preVisitor.mDOMEvent = postVisitor.mDOMEvent; + } + } + } + } + + // Note, EventTargetChainItem objects are deleted when the chain goes out of + // the scope. + + aEvent->mFlags.mIsBeingDispatched = false; + aEvent->mFlags.mDispatchedAtLeastOnce = true; + + if (eventTimingEntry) { + eventTimingEntry->FinalizeEventTiming(aEvent->mTarget); + } + // https://dom.spec.whatwg.org/#concept-event-dispatch + // step 10. If clearTargets, then: + // 1. Set event's target to null. + // 2. Set event's relatedTarget to null. + // 3. Set event's touch target list to the empty list. + if (clearTargets) { + aEvent->mTarget = nullptr; + aEvent->mOriginalTarget = nullptr; + aEvent->mRelatedTarget = nullptr; + aEvent->mOriginalRelatedTarget = nullptr; + // XXXsmaug Check also all the touch objects. + } + + if (!externalDOMEvent && preVisitor.mDOMEvent) { + // A dom::Event was created while dispatching the event. + // Duplicate private data if someone holds a pointer to it. + nsrefcnt rc = 0; + NS_RELEASE2(preVisitor.mDOMEvent, rc); + if (preVisitor.mDOMEvent) { + preVisitor.mDOMEvent->DuplicatePrivateData(); + } + } + + if (aEventStatus) { + *aEventStatus = preVisitor.mEventStatus; + } + + if (cd.IsMainThread() && chain.Capacity() == kCachedMainThreadChainSize && + sCachedMainThreadChains) { + if (sCachedMainThreadChains->mChain1.Capacity() != + kCachedMainThreadChainSize) { + chain.ClearAndRetainStorage(); + chain.SwapElements(sCachedMainThreadChains->mChain1); + } else if (sCachedMainThreadChains->mChain2.Capacity() != + kCachedMainThreadChainSize) { + chain.ClearAndRetainStorage(); + chain.SwapElements(sCachedMainThreadChains->mChain2); + } + } + + return rv; +} + +/* static */ +nsresult EventDispatcher::DispatchDOMEvent(nsISupports* aTarget, + WidgetEvent* aEvent, + Event* aDOMEvent, + nsPresContext* aPresContext, + nsEventStatus* aEventStatus) { + if (aDOMEvent) { + WidgetEvent* innerEvent = aDOMEvent->WidgetEventPtr(); + NS_ENSURE_TRUE(innerEvent, NS_ERROR_ILLEGAL_VALUE); + + // Don't modify the event if it's being dispatched right now. + if (innerEvent->mFlags.mIsBeingDispatched) { + return NS_ERROR_DOM_INVALID_STATE_ERR; + } + + bool dontResetTrusted = false; + if (innerEvent->mFlags.mDispatchedAtLeastOnce) { + innerEvent->mTarget = nullptr; + innerEvent->mOriginalTarget = nullptr; + } else { + dontResetTrusted = aDOMEvent->IsTrusted(); + } + + if (!dontResetTrusted) { + // Check security state to determine if dispatcher is trusted + bool trusted = NS_IsMainThread() + ? nsContentUtils::LegacyIsCallerChromeOrNativeCode() + : IsCurrentThreadRunningChromeWorker(); + aDOMEvent->SetTrusted(trusted); + } + + return EventDispatcher::Dispatch(aTarget, aPresContext, innerEvent, + aDOMEvent, aEventStatus); + } else if (aEvent) { + return EventDispatcher::Dispatch(aTarget, aPresContext, aEvent, aDOMEvent, + aEventStatus); + } + return NS_ERROR_ILLEGAL_VALUE; +} + +/* static */ already_AddRefed<dom::Event> EventDispatcher::CreateEvent( + EventTarget* aOwner, nsPresContext* aPresContext, WidgetEvent* aEvent, + const nsAString& aEventType, CallerType aCallerType) { + if (aEvent) { + switch (aEvent->mClass) { + case eMutationEventClass: + return NS_NewDOMMutationEvent(aOwner, aPresContext, + aEvent->AsMutationEvent()); + case eGUIEventClass: + case eScrollPortEventClass: + case eUIEventClass: + return NS_NewDOMUIEvent(aOwner, aPresContext, aEvent->AsGUIEvent()); + case eScrollAreaEventClass: + return NS_NewDOMScrollAreaEvent(aOwner, aPresContext, + aEvent->AsScrollAreaEvent()); + case eKeyboardEventClass: + return NS_NewDOMKeyboardEvent(aOwner, aPresContext, + aEvent->AsKeyboardEvent()); + case eCompositionEventClass: + return NS_NewDOMCompositionEvent(aOwner, aPresContext, + aEvent->AsCompositionEvent()); + case eMouseEventClass: + return NS_NewDOMMouseEvent(aOwner, aPresContext, + aEvent->AsMouseEvent()); + case eFocusEventClass: + return NS_NewDOMFocusEvent(aOwner, aPresContext, + aEvent->AsFocusEvent()); + case eMouseScrollEventClass: + return NS_NewDOMMouseScrollEvent(aOwner, aPresContext, + aEvent->AsMouseScrollEvent()); + case eWheelEventClass: + return NS_NewDOMWheelEvent(aOwner, aPresContext, + aEvent->AsWheelEvent()); + case eEditorInputEventClass: + return NS_NewDOMInputEvent(aOwner, aPresContext, + aEvent->AsEditorInputEvent()); + case eDragEventClass: + return NS_NewDOMDragEvent(aOwner, aPresContext, aEvent->AsDragEvent()); + case eClipboardEventClass: + return NS_NewDOMClipboardEvent(aOwner, aPresContext, + aEvent->AsClipboardEvent()); + case eSMILTimeEventClass: + return NS_NewDOMTimeEvent(aOwner, aPresContext, + aEvent->AsSMILTimeEvent()); + case eCommandEventClass: + return NS_NewDOMCommandEvent(aOwner, aPresContext, + aEvent->AsCommandEvent()); + case eSimpleGestureEventClass: + return NS_NewDOMSimpleGestureEvent(aOwner, aPresContext, + aEvent->AsSimpleGestureEvent()); + case ePointerEventClass: + return NS_NewDOMPointerEvent(aOwner, aPresContext, + aEvent->AsPointerEvent()); + case eTouchEventClass: + return NS_NewDOMTouchEvent(aOwner, aPresContext, + aEvent->AsTouchEvent()); + case eTransitionEventClass: + return NS_NewDOMTransitionEvent(aOwner, aPresContext, + aEvent->AsTransitionEvent()); + case eAnimationEventClass: + return NS_NewDOMAnimationEvent(aOwner, aPresContext, + aEvent->AsAnimationEvent()); + default: + // For all other types of events, create a vanilla event object. + return NS_NewDOMEvent(aOwner, aPresContext, aEvent); + } + } + + // And if we didn't get an event, check the type argument. + + if (aEventType.LowerCaseEqualsLiteral("mouseevent") || + aEventType.LowerCaseEqualsLiteral("mouseevents")) { + return NS_NewDOMMouseEvent(aOwner, aPresContext, nullptr); + } + if (aEventType.LowerCaseEqualsLiteral("dragevent")) { + return NS_NewDOMDragEvent(aOwner, aPresContext, nullptr); + } + if (aEventType.LowerCaseEqualsLiteral("keyboardevent")) { + return NS_NewDOMKeyboardEvent(aOwner, aPresContext, nullptr); + } + if (aEventType.LowerCaseEqualsLiteral("compositionevent") || + aEventType.LowerCaseEqualsLiteral("textevent")) { + return NS_NewDOMCompositionEvent(aOwner, aPresContext, nullptr); + } + if (aEventType.LowerCaseEqualsLiteral("mutationevent") || + aEventType.LowerCaseEqualsLiteral("mutationevents")) { + return NS_NewDOMMutationEvent(aOwner, aPresContext, nullptr); + } + if (aEventType.LowerCaseEqualsLiteral("deviceorientationevent")) { + DeviceOrientationEventInit init; + RefPtr<Event> event = + DeviceOrientationEvent::Constructor(aOwner, u""_ns, init); + event->MarkUninitialized(); + return event.forget(); + } + if (aEventType.LowerCaseEqualsLiteral("devicemotionevent")) { + return NS_NewDOMDeviceMotionEvent(aOwner, aPresContext, nullptr); + } + if (aEventType.LowerCaseEqualsLiteral("uievent") || + aEventType.LowerCaseEqualsLiteral("uievents")) { + return NS_NewDOMUIEvent(aOwner, aPresContext, nullptr); + } + if (aEventType.LowerCaseEqualsLiteral("event") || + aEventType.LowerCaseEqualsLiteral("events") || + aEventType.LowerCaseEqualsLiteral("htmlevents") || + aEventType.LowerCaseEqualsLiteral("svgevents")) { + return NS_NewDOMEvent(aOwner, aPresContext, nullptr); + } + if (aEventType.LowerCaseEqualsLiteral("messageevent")) { + RefPtr<Event> event = new MessageEvent(aOwner, aPresContext, nullptr); + return event.forget(); + } + if (aEventType.LowerCaseEqualsLiteral("beforeunloadevent")) { + return NS_NewDOMBeforeUnloadEvent(aOwner, aPresContext, nullptr); + } + if (aEventType.LowerCaseEqualsLiteral("touchevent") && + TouchEvent::LegacyAPIEnabled( + nsContentUtils::GetDocShellForEventTarget(aOwner), + aCallerType == CallerType::System)) { + return NS_NewDOMTouchEvent(aOwner, aPresContext, nullptr); + } + if (aEventType.LowerCaseEqualsLiteral("hashchangeevent")) { + HashChangeEventInit init; + RefPtr<Event> event = HashChangeEvent::Constructor(aOwner, u""_ns, init); + event->MarkUninitialized(); + return event.forget(); + } + if (aEventType.LowerCaseEqualsLiteral("customevent")) { + return NS_NewDOMCustomEvent(aOwner, aPresContext, nullptr); + } + if (aEventType.LowerCaseEqualsLiteral("storageevent")) { + RefPtr<Event> event = + StorageEvent::Constructor(aOwner, u""_ns, StorageEventInit()); + event->MarkUninitialized(); + return event.forget(); + } + if (aEventType.LowerCaseEqualsLiteral("focusevent")) { + RefPtr<Event> event = NS_NewDOMFocusEvent(aOwner, aPresContext, nullptr); + event->MarkUninitialized(); + return event.forget(); + } + + // Only allow these events for chrome + if (aCallerType == CallerType::System) { + if (aEventType.LowerCaseEqualsLiteral("simplegestureevent")) { + return NS_NewDOMSimpleGestureEvent(aOwner, aPresContext, nullptr); + } + if (aEventType.LowerCaseEqualsLiteral("xulcommandevent") || + aEventType.LowerCaseEqualsLiteral("xulcommandevents")) { + return NS_NewDOMXULCommandEvent(aOwner, aPresContext, nullptr); + } + } + + // NEW EVENT TYPES SHOULD NOT BE ADDED HERE; THEY SHOULD USE ONLY EVENT + // CONSTRUCTORS + + return nullptr; +} + +struct CurrentTargetPathInfo { + uint32_t mIndex; + int32_t mHiddenSubtreeLevel; +}; + +static CurrentTargetPathInfo TargetPathInfo( + const nsTArray<EventTargetChainItem>& aEventPath, + const EventTarget& aCurrentTarget) { + int32_t currentTargetHiddenSubtreeLevel = 0; + for (uint32_t index = aEventPath.Length(); index--;) { + const EventTargetChainItem& item = aEventPath.ElementAt(index); + if (item.PreHandleEventOnly()) { + continue; + } + + if (item.IsRootOfClosedTree()) { + currentTargetHiddenSubtreeLevel++; + } + + if (item.CurrentTarget() == &aCurrentTarget) { + return {index, currentTargetHiddenSubtreeLevel}; + } + + if (item.IsSlotInClosedTree()) { + currentTargetHiddenSubtreeLevel--; + } + } + MOZ_ASSERT_UNREACHABLE("No target found?"); + return {0, 0}; +} + +// https://dom.spec.whatwg.org/#dom-event-composedpath +void EventDispatcher::GetComposedPathFor(WidgetEvent* aEvent, + nsTArray<RefPtr<EventTarget>>& aPath) { + MOZ_ASSERT(aPath.IsEmpty()); + nsTArray<EventTargetChainItem>* path = aEvent->mPath; + if (!path || path->IsEmpty() || !aEvent->mCurrentTarget) { + return; + } + + EventTarget* currentTarget = + aEvent->mCurrentTarget->GetTargetForEventTargetChain(); + if (!currentTarget) { + return; + } + + CurrentTargetPathInfo currentTargetInfo = + TargetPathInfo(*path, *currentTarget); + + { + int32_t maxHiddenLevel = currentTargetInfo.mHiddenSubtreeLevel; + int32_t currentHiddenLevel = currentTargetInfo.mHiddenSubtreeLevel; + for (uint32_t index = currentTargetInfo.mIndex; index--;) { + EventTargetChainItem& item = path->ElementAt(index); + if (item.PreHandleEventOnly()) { + continue; + } + + if (item.IsRootOfClosedTree()) { + currentHiddenLevel++; + } + + if (currentHiddenLevel <= maxHiddenLevel) { + aPath.AppendElement(item.CurrentTarget()->GetTargetForDOMEvent()); + } + + if (item.IsChromeHandler()) { + break; + } + + if (item.IsSlotInClosedTree()) { + currentHiddenLevel--; + maxHiddenLevel = std::min(maxHiddenLevel, currentHiddenLevel); + } + } + + aPath.Reverse(); + } + + aPath.AppendElement(currentTarget->GetTargetForDOMEvent()); + + { + int32_t maxHiddenLevel = currentTargetInfo.mHiddenSubtreeLevel; + int32_t currentHiddenLevel = currentTargetInfo.mHiddenSubtreeLevel; + for (uint32_t index = currentTargetInfo.mIndex + 1; index < path->Length(); + ++index) { + EventTargetChainItem& item = path->ElementAt(index); + if (item.PreHandleEventOnly()) { + continue; + } + + if (item.IsSlotInClosedTree()) { + currentHiddenLevel++; + } + + if (item.IsChromeHandler()) { + break; + } + + if (currentHiddenLevel <= maxHiddenLevel) { + aPath.AppendElement(item.CurrentTarget()->GetTargetForDOMEvent()); + } + + if (item.IsRootOfClosedTree()) { + currentHiddenLevel--; + maxHiddenLevel = std::min(maxHiddenLevel, currentHiddenLevel); + } + } + } +} + +} // namespace mozilla diff --git a/dom/events/EventDispatcher.h b/dom/events/EventDispatcher.h new file mode 100644 index 0000000000..9a9a0f2361 --- /dev/null +++ b/dom/events/EventDispatcher.h @@ -0,0 +1,385 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#ifdef MOZILLA_INTERNAL_API +# ifndef mozilla_EventDispatcher_h_ +# define mozilla_EventDispatcher_h_ + +# include "mozilla/dom/BindingDeclarations.h" +# include "mozilla/dom/Touch.h" +# include "mozilla/EventForwards.h" +# include "mozilla/Maybe.h" +# include "nsCOMPtr.h" +# include "nsTArray.h" + +// Microsoft's API Name hackery sucks +# undef CreateEvent + +class nsIContent; +class nsPresContext; + +template <class E> +class nsCOMArray; + +namespace mozilla { +namespace dom { +class Event; +class EventTarget; +} // namespace dom + +/** + * About event dispatching: + * When either EventDispatcher::Dispatch or + * EventDispatcher::DispatchDOMEvent is called an event target chain is + * created. EventDispatcher creates the chain by calling GetEventTargetParent + * on each event target and the creation continues until either the mCanHandle + * member of the EventChainPreVisitor object is false or the mParentTarget + * does not point to a new target. The event target chain is created in the + * heap. + * + * If the event needs retargeting, mEventTargetAtParent must be set in + * GetEventTargetParent. + * + * The capture, target and bubble phases of the event dispatch are handled + * by iterating through the event target chain. Iteration happens twice, + * first for the default event group and then for the system event group. + * While dispatching the event for the system event group PostHandleEvent + * is called right after calling event listener for the current event target. + */ + +class MOZ_STACK_CLASS EventChainVisitor { + public: + // For making creators of this class instances guarantee the lifetime of + // aPresContext, this needs to be marked as MOZ_CAN_RUN_SCRIPT. + MOZ_CAN_RUN_SCRIPT + EventChainVisitor(nsPresContext* aPresContext, WidgetEvent* aEvent, + dom::Event* aDOMEvent, + nsEventStatus aEventStatus = nsEventStatus_eIgnore) + : mPresContext(aPresContext), + mEvent(aEvent), + mDOMEvent(aDOMEvent), + mEventStatus(aEventStatus), + mItemFlags(0) {} + + /** + * The prescontext, possibly nullptr. + * Note that the lifetime of mPresContext is guaranteed by the creators. + */ + MOZ_KNOWN_LIVE nsPresContext* const mPresContext; + + /** + * The WidgetEvent which is being dispatched. Never nullptr. + */ + WidgetEvent* const mEvent; + + /** + * The DOM Event assiciated with the mEvent. Possibly nullptr if a DOM Event + * is not (yet) created. + */ + dom::Event* mDOMEvent; + + /** + * The status of the event. + * @see nsEventStatus.h + */ + nsEventStatus mEventStatus; + + /** + * Bits for items in the event target chain. + * Set in GetEventTargetParent() and used in PostHandleEvent(). + * + * @note These bits are different for each item in the event target chain. + * It is up to the Pre/PostHandleEvent implementation to decide how to + * use these bits. + * + * @note Using uint16_t because that is used also in EventTargetChainItem. + */ + uint16_t mItemFlags; + + /** + * Data for items in the event target chain. + * Set in GetEventTargetParent() and used in PostHandleEvent(). + * + * @note This data is different for each item in the event target chain. + * It is up to the Pre/PostHandleEvent implementation to decide how to + * use this. + */ + nsCOMPtr<nsISupports> mItemData; +}; + +class MOZ_STACK_CLASS EventChainPreVisitor final : public EventChainVisitor { + public: + MOZ_CAN_RUN_SCRIPT + EventChainPreVisitor(nsPresContext* aPresContext, WidgetEvent* aEvent, + dom::Event* aDOMEvent, nsEventStatus aEventStatus, + bool aIsInAnon, + dom::EventTarget* aTargetInKnownToBeHandledScope) + : EventChainVisitor(aPresContext, aEvent, aDOMEvent, aEventStatus), + mCanHandle(true), + mAutomaticChromeDispatch(true), + mForceContentDispatch(false), + mRelatedTargetIsInAnon(false), + mOriginalTargetIsInAnon(aIsInAnon), + mWantsWillHandleEvent(false), + mMayHaveListenerManager(true), + mWantsPreHandleEvent(false), + mRootOfClosedTree(false), + mItemInShadowTree(false), + mParentIsSlotInClosedTree(false), + mParentIsChromeHandler(false), + mRelatedTargetRetargetedInCurrentScope(false), + mIgnoreBecauseOfShadowDOM(false), + mParentTarget(nullptr), + mEventTargetAtParent(nullptr), + mRetargetedRelatedTarget(nullptr), + mTargetInKnownToBeHandledScope(aTargetInKnownToBeHandledScope) {} + + void Reset() { + mItemFlags = 0; + mItemData = nullptr; + mCanHandle = true; + mAutomaticChromeDispatch = true; + mForceContentDispatch = false; + mWantsWillHandleEvent = false; + mMayHaveListenerManager = true; + mWantsPreHandleEvent = false; + mRootOfClosedTree = false; + mItemInShadowTree = false; + mParentIsSlotInClosedTree = false; + mParentIsChromeHandler = false; + // Note, we don't clear mRelatedTargetRetargetedInCurrentScope explicitly, + // since it is used during event path creation to indicate whether + // relatedTarget may need to be retargeted. + mIgnoreBecauseOfShadowDOM = false; + mParentTarget = nullptr; + mEventTargetAtParent = nullptr; + mRetargetedRelatedTarget = nullptr; + mRetargetedTouchTargets.reset(); + } + + dom::EventTarget* GetParentTarget() { return mParentTarget; } + + void SetParentTarget(dom::EventTarget* aParentTarget, bool aIsChromeHandler) { + mParentTarget = aParentTarget; + if (mParentTarget) { + mParentIsChromeHandler = aIsChromeHandler; + } + } + + void IgnoreCurrentTargetBecauseOfShadowDOMRetargeting() { + mCanHandle = false; + mIgnoreBecauseOfShadowDOM = true; + SetParentTarget(nullptr, false); + mEventTargetAtParent = nullptr; + } + + /** + * Member that must be set in GetEventTargetParent by event targets. If set to + * false, indicates that this event target will not be handling the event and + * construction of the event target chain is complete. The target that sets + * mCanHandle to false is NOT included in the event target chain. + */ + bool mCanHandle; + + /** + * If mCanHandle is false and mAutomaticChromeDispatch is also false + * event will not be dispatched to the chrome event handler. + */ + bool mAutomaticChromeDispatch; + + /** + * If mForceContentDispatch is set to true, + * content dispatching is not disabled for this event target. + * FIXME! This is here for backward compatibility. Bug 329119 + */ + bool mForceContentDispatch; + + /** + * true if it is known that related target is or is a descendant of an + * element which is anonymous for events. + */ + bool mRelatedTargetIsInAnon; + + /** + * true if the original target of the event is inside anonymous content. + * This is set before calling GetEventTargetParent on event targets. + */ + bool mOriginalTargetIsInAnon; + + /** + * Whether or not EventTarget::WillHandleEvent will be + * called. Default is false; + */ + bool mWantsWillHandleEvent; + + /** + * If it is known that the current target doesn't have a listener manager + * when GetEventTargetParent is called, set this to false. + */ + bool mMayHaveListenerManager; + + /** + * Whether or not EventTarget::PreHandleEvent will be called. Default is + * false; + */ + bool mWantsPreHandleEvent; + + /** + * True if the current target is either closed ShadowRoot or root of + * chrome only access tree (for example native anonymous content). + */ + bool mRootOfClosedTree; + + /** + * If target is node and its root is a shadow root. + * https://dom.spec.whatwg.org/#event-path-item-in-shadow-tree + */ + bool mItemInShadowTree; + + /** + * True if mParentTarget is HTMLSlotElement in a closed shadow tree and the + * current target is assigned to that slot. + */ + bool mParentIsSlotInClosedTree; + + /** + * True if mParentTarget is a chrome handler in the event path. + */ + bool mParentIsChromeHandler; + + /** + * True if event's related target has been already retargeted in the + * current 'scope'. This should be set to false initially and whenever + * event path creation crosses shadow boundary. + */ + bool mRelatedTargetRetargetedInCurrentScope; + + /** + * True if Shadow DOM relatedTarget retargeting causes the current item + * to not show up in the event path. + */ + bool mIgnoreBecauseOfShadowDOM; + + private: + /** + * Parent item in the event target chain. + */ + dom::EventTarget* mParentTarget; + + public: + /** + * If the event needs to be retargeted, this is the event target, + * which should be used when the event is handled at mParentTarget. + */ + dom::EventTarget* mEventTargetAtParent; + + /** + * If the related target of the event needs to be retargeted, set this + * to a new EventTarget. + */ + dom::EventTarget* mRetargetedRelatedTarget; + + /** + * If mEvent is a WidgetTouchEvent and its mTouches needs retargeting, + * set the targets to this array. The array should contain one entry per + * each object in WidgetTouchEvent::mTouches. + */ + mozilla::Maybe<nsTArray<RefPtr<dom::EventTarget>>> mRetargetedTouchTargets; + + /** + * Set to the value of mEvent->mTarget of the previous scope in case of + * Shadow DOM or such, and if there is no anonymous content this just points + * to the initial target. + */ + dom::EventTarget* mTargetInKnownToBeHandledScope; +}; + +class MOZ_STACK_CLASS EventChainPostVisitor final + : public mozilla::EventChainVisitor { + public: + // Note that for making guarantee the lifetime of mPresContext and mDOMEvent, + // creators should guarantee that aOther won't be deleted while the instance + // of this class is alive. + MOZ_CAN_RUN_SCRIPT + explicit EventChainPostVisitor(EventChainVisitor& aOther) + : EventChainVisitor(aOther.mPresContext, aOther.mEvent, + MOZ_KnownLive(aOther.mDOMEvent), + aOther.mEventStatus) {} +}; + +/** + * If an EventDispatchingCallback object is passed to Dispatch, + * its HandleEvent method is called after handling the default event group, + * before handling the system event group. + * This is used in PresShell. + */ +class MOZ_STACK_CLASS EventDispatchingCallback { + public: + MOZ_CAN_RUN_SCRIPT + virtual void HandleEvent(EventChainPostVisitor& aVisitor) = 0; +}; + +/** + * The generic class for event dispatching. + * Must not be used outside Gecko! + */ +class EventDispatcher { + public: + /** + * aTarget should QI to EventTarget. + * If the target of aEvent is set before calling this method, the target of + * aEvent is used as the target (unless there is event + * retargeting) and the originalTarget of the DOM Event. + * aTarget is always used as the starting point for constructing the event + * target chain, no matter what the value of aEvent->mTarget is. + * In other words, aEvent->mTarget is only a property of the event and it has + * nothing to do with the construction of the event target chain. + * Neither aTarget nor aEvent is allowed to be nullptr. + * + * If aTargets is non-null, event target chain will be created, but + * event won't be handled. In this case aEvent->mMessage should be + * eVoidEvent. + * @note Use this method when dispatching a WidgetEvent. + */ + MOZ_CAN_RUN_SCRIPT static nsresult Dispatch( + nsISupports* aTarget, nsPresContext* aPresContext, WidgetEvent* aEvent, + dom::Event* aDOMEvent = nullptr, nsEventStatus* aEventStatus = nullptr, + EventDispatchingCallback* aCallback = nullptr, + nsTArray<dom::EventTarget*>* aTargets = nullptr); + + /** + * Dispatches an event. + * If aDOMEvent is not nullptr, it is used for dispatching + * (aEvent can then be nullptr) and (if aDOMEvent is not |trusted| already), + * the |trusted| flag is set if the caller uses the system principal. + * Otherwise this works like EventDispatcher::Dispatch. + * @note Use this method when dispatching a dom::Event. + */ + MOZ_CAN_RUN_SCRIPT static nsresult DispatchDOMEvent( + nsISupports* aTarget, WidgetEvent* aEvent, dom::Event* aDOMEvent, + nsPresContext* aPresContext, nsEventStatus* aEventStatus); + + /** + * Creates a DOM Event. Returns null if the event type is unsupported. + */ + static already_AddRefed<dom::Event> CreateEvent( + dom::EventTarget* aOwner, nsPresContext* aPresContext, + WidgetEvent* aEvent, const nsAString& aEventType, + dom::CallerType aCallerType = dom::CallerType::System); + + static void GetComposedPathFor(WidgetEvent* aEvent, + nsTArray<RefPtr<dom::EventTarget>>& aPath); + + /** + * Called at shutting down. + */ + static void Shutdown(); +}; + +} // namespace mozilla + +# endif // mozilla_EventDispatcher_h_ +#endif diff --git a/dom/events/EventListenerManager.cpp b/dom/events/EventListenerManager.cpp new file mode 100644 index 0000000000..02ad070612 --- /dev/null +++ b/dom/events/EventListenerManager.cpp @@ -0,0 +1,2088 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +// Microsoft's API Name hackery sucks +#undef CreateEvent + +#include "js/loader/LoadedScript.h" +#include "mozilla/BasicEvents.h" +#include "mozilla/CycleCollectedJSRuntime.h" +#include "mozilla/DOMEventTargetHelper.h" +#include "mozilla/EventDispatcher.h" +#include "mozilla/EventListenerManager.h" +#include "mozilla/HalSensor.h" +#include "mozilla/InternalMutationEvent.h" +#include "mozilla/JSEventHandler.h" +#include "mozilla/Maybe.h" +#include "mozilla/MemoryReporting.h" +#include "mozilla/Preferences.h" +#include "mozilla/PresShell.h" +#include "mozilla/dom/AbortSignal.h" +#include "mozilla/dom/BindingUtils.h" +#include "mozilla/dom/EventCallbackDebuggerNotification.h" +#include "mozilla/dom/Element.h" +#include "mozilla/dom/Event.h" +#include "mozilla/dom/EventTargetBinding.h" +#include "mozilla/dom/PopupBlocker.h" +#include "mozilla/dom/ScriptLoader.h" +#include "mozilla/dom/ScriptSettings.h" +#include "mozilla/dom/TouchEvent.h" +#include "mozilla/dom/UserActivation.h" +#include "mozilla/ScopeExit.h" +#include "mozilla/StaticPrefs_dom.h" +#include "mozilla/TimeStamp.h" +#include "mozilla/dom/ChromeUtils.h" + +#include "EventListenerService.h" +#include "nsCOMPtr.h" +#include "nsContentUtils.h" +#include "nsDOMCID.h" +#include "nsError.h" +#include "nsGenericHTMLElement.h" +#include "nsGkAtoms.h" +#include "nsIContent.h" +#include "nsIContentSecurityPolicy.h" +#include "mozilla/dom/Document.h" +#include "nsIScriptGlobalObject.h" +#include "nsISupports.h" +#include "nsJSUtils.h" +#include "nsNameSpaceManager.h" +#include "nsPIDOMWindow.h" +#include "nsPrintfCString.h" +#include "nsSandboxFlags.h" +#include "xpcpublic.h" +#include "nsIFrame.h" +#include "nsDisplayList.h" + +namespace mozilla { + +using namespace dom; +using namespace hal; + +#define EVENT_TYPE_EQUALS(ls, message, userType, allEvents) \ + ((ls->mEventMessage == message && \ + (ls->mEventMessage != eUnidentifiedEvent || ls->mTypeAtom == userType)) || \ + (allEvents && ls->mAllEvents)) + +static const uint32_t kAllMutationBits = + NS_EVENT_BITS_MUTATION_SUBTREEMODIFIED | + NS_EVENT_BITS_MUTATION_NODEINSERTED | NS_EVENT_BITS_MUTATION_NODEREMOVED | + NS_EVENT_BITS_MUTATION_NODEREMOVEDFROMDOCUMENT | + NS_EVENT_BITS_MUTATION_NODEINSERTEDINTODOCUMENT | + NS_EVENT_BITS_MUTATION_ATTRMODIFIED | + NS_EVENT_BITS_MUTATION_CHARACTERDATAMODIFIED; + +static uint32_t MutationBitForEventType(EventMessage aEventType) { + switch (aEventType) { + case eLegacySubtreeModified: + return NS_EVENT_BITS_MUTATION_SUBTREEMODIFIED; + case eLegacyNodeInserted: + return NS_EVENT_BITS_MUTATION_NODEINSERTED; + case eLegacyNodeRemoved: + return NS_EVENT_BITS_MUTATION_NODEREMOVED; + case eLegacyNodeRemovedFromDocument: + return NS_EVENT_BITS_MUTATION_NODEREMOVEDFROMDOCUMENT; + case eLegacyNodeInsertedIntoDocument: + return NS_EVENT_BITS_MUTATION_NODEINSERTEDINTODOCUMENT; + case eLegacyAttrModified: + return NS_EVENT_BITS_MUTATION_ATTRMODIFIED; + case eLegacyCharacterDataModified: + return NS_EVENT_BITS_MUTATION_CHARACTERDATAMODIFIED; + default: + break; + } + return 0; +} + +uint32_t EventListenerManager::sMainThreadCreatedCount = 0; + +EventListenerManagerBase::EventListenerManagerBase() + : mMayHavePaintEventListener(false), + mMayHaveMutationListeners(false), + mMayHaveCapturingListeners(false), + mMayHaveSystemGroupListeners(false), + mMayHaveTouchEventListener(false), + mMayHaveMouseEnterLeaveEventListener(false), + mMayHavePointerEnterLeaveEventListener(false), + mMayHaveSelectionChangeEventListener(false), + mMayHaveFormSelectEventListener(false), + mMayHaveTransitionEventListener(false), + mClearingListeners(false), + mIsMainThreadELM(NS_IsMainThread()), + mMayHaveListenersForUntrustedEvents(false) { + ClearNoListenersForEvents(); + static_assert(sizeof(EventListenerManagerBase) == sizeof(uint64_t), + "Keep the size of EventListenerManagerBase size compact!"); +} + +EventListenerManager::EventListenerManager(EventTarget* aTarget) + : EventListenerManagerBase(), mTarget(aTarget) { + NS_ASSERTION(aTarget, "unexpected null pointer"); + + if (mIsMainThreadELM) { + ++sMainThreadCreatedCount; + } +} + +EventListenerManager::~EventListenerManager() { + // If your code fails this assertion, a possible reason is that + // a class did not call our Disconnect() manually. Note that + // this class can have Disconnect called in one of two ways: + // if it is part of a cycle, then in Unlink() (such a cycle + // would be with one of the listeners, not mTarget which is weak). + // If not part of a cycle, then Disconnect must be called manually, + // typically from the destructor of the owner class (mTarget). + // XXX azakai: Is there any reason to not just call Disconnect + // from right here, if not previously called? + NS_ASSERTION(!mTarget, "didn't call Disconnect"); + RemoveAllListenersSilently(); +} + +void EventListenerManager::RemoveAllListenersSilently() { + if (mClearingListeners) { + return; + } + mClearingListeners = true; + mListeners.Clear(); + mClearingListeners = false; +} + +inline void ImplCycleCollectionTraverse( + nsCycleCollectionTraversalCallback& aCallback, + EventListenerManager::Listener& aField, const char* aName, + unsigned aFlags) { + if (MOZ_UNLIKELY(aCallback.WantDebugInfo())) { + nsAutoCString name; + name.AppendASCII(aName); + if (aField.mTypeAtom) { + name.AppendLiteral(" event="); + name.Append(nsAtomCString(aField.mTypeAtom)); + name.AppendLiteral(" listenerType="); + name.AppendInt(aField.mListenerType); + name.AppendLiteral(" "); + } + CycleCollectionNoteChild(aCallback, aField.mListener.GetISupports(), + name.get(), aFlags); + } else { + CycleCollectionNoteChild(aCallback, aField.mListener.GetISupports(), aName, + aFlags); + } + + CycleCollectionNoteChild(aCallback, aField.mSignalFollower.get(), + "mSignalFollower", aFlags); +} + +NS_IMPL_CYCLE_COLLECTION_CLASS(EventListenerManager) + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(EventListenerManager) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mListeners) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(EventListenerManager) + tmp->Disconnect(); +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +nsPIDOMWindowInner* EventListenerManager::GetInnerWindowForTarget() { + if (nsINode* node = nsINode::FromEventTargetOrNull(mTarget)) { + // XXX sXBL/XBL2 issue -- do we really want the owner here? What + // if that's the XBL document? + return node->OwnerDoc()->GetInnerWindow(); + } + + nsCOMPtr<nsPIDOMWindowInner> window = GetTargetAsInnerWindow(); + return window; +} + +already_AddRefed<nsPIDOMWindowInner> +EventListenerManager::GetTargetAsInnerWindow() const { + nsCOMPtr<nsPIDOMWindowInner> window = + nsPIDOMWindowInner::FromEventTargetOrNull(mTarget); + return window.forget(); +} + +void EventListenerManager::AddEventListenerInternal( + EventListenerHolder aListenerHolder, EventMessage aEventMessage, + nsAtom* aTypeAtom, const EventListenerFlags& aFlags, bool aHandler, + bool aAllEvents, AbortSignal* aSignal) { + MOZ_ASSERT((aEventMessage && aTypeAtom) || aAllEvents, // all-events listener + "Missing type"); + + if (!aListenerHolder || mClearingListeners) { + return; + } + + if (aSignal && aSignal->Aborted()) { + return; + } + + // Since there is no public API to call us with an EventListenerHolder, we + // know that there's an EventListenerHolder on the stack holding a strong ref + // to the listener. + + Listener* listener; + uint32_t count = mListeners.Length(); + for (uint32_t i = 0; i < count; i++) { + listener = &mListeners.ElementAt(i); + // mListener == aListenerHolder is the last one, since it can be a bit slow. + if (listener->mListenerIsHandler == aHandler && + listener->mFlags.EqualsForAddition(aFlags) && + EVENT_TYPE_EQUALS(listener, aEventMessage, aTypeAtom, aAllEvents) && + listener->mListener == aListenerHolder) { + return; + } + } + + ClearNoListenersForEvents(); + mNoListenerForEventAtom = nullptr; + + listener = + aAllEvents ? mListeners.InsertElementAt(0) : mListeners.AppendElement(); + listener->mEventMessage = aEventMessage; + listener->mTypeAtom = aTypeAtom; + listener->mFlags = aFlags; + listener->mListenerIsHandler = aHandler; + listener->mHandlerIsString = false; + listener->mAllEvents = aAllEvents; + + if (listener->mFlags.mAllowUntrustedEvents) { + mMayHaveListenersForUntrustedEvents = true; + } + + // Detect the type of event listener. + if (aFlags.mListenerIsJSListener) { + MOZ_ASSERT(!aListenerHolder.HasWebIDLCallback()); + listener->mListenerType = Listener::eJSEventListener; + } else if (aListenerHolder.HasWebIDLCallback()) { + listener->mListenerType = Listener::eWebIDLListener; + } else { + listener->mListenerType = Listener::eNativeListener; + } + listener->mListener = std::move(aListenerHolder); + + if (aSignal) { + listener->mSignalFollower = new ListenerSignalFollower(this, listener); + listener->mSignalFollower->Follow(aSignal); + } + + if (aFlags.mInSystemGroup) { + mMayHaveSystemGroupListeners = true; + } + if (aFlags.mCapture) { + mMayHaveCapturingListeners = true; + } + + // Events which are not supported in the running environment is mapped to + // eUnidentifiedEvent. Then, we need to consider the proper event message + // with comparing the atom. + { + EventMessage resolvedEventMessage = aEventMessage; + if (resolvedEventMessage == eUnidentifiedEvent && aTypeAtom->IsStatic()) { + // TouchEvents are registered only when + // nsContentUtils::InitializeTouchEventTable() is called. + if (aTypeAtom == nsGkAtoms::ontouchstart) { + resolvedEventMessage = eTouchStart; + } else if (aTypeAtom == nsGkAtoms::ontouchend) { + resolvedEventMessage = eTouchEnd; + } else if (aTypeAtom == nsGkAtoms::ontouchmove) { + resolvedEventMessage = eTouchMove; + } else if (aTypeAtom == nsGkAtoms::ontouchcancel) { + resolvedEventMessage = eTouchCancel; + } + } + + switch (resolvedEventMessage) { + case eAfterPaint: + mMayHavePaintEventListener = true; + if (nsPIDOMWindowInner* window = GetInnerWindowForTarget()) { + window->SetHasPaintEventListeners(); + } + break; + case eLegacySubtreeModified: + case eLegacyNodeInserted: + case eLegacyNodeRemoved: + case eLegacyNodeRemovedFromDocument: + case eLegacyNodeInsertedIntoDocument: + case eLegacyAttrModified: + case eLegacyCharacterDataModified: + // For mutation listeners, we need to update the global bit on the DOM + // window. Otherwise we won't actually fire the mutation event. + mMayHaveMutationListeners = true; + // Go from our target to the nearest enclosing DOM window. + if (nsPIDOMWindowInner* window = GetInnerWindowForTarget()) { + if (Document* doc = window->GetExtantDoc()) { + doc->WarnOnceAbout(DeprecatedOperations::eMutationEvent); + } + // If resolvedEventMessage is eLegacySubtreeModified, we need to + // listen all mutations. nsContentUtils::HasMutationListeners relies + // on this. + window->SetMutationListeners( + (resolvedEventMessage == eLegacySubtreeModified) + ? kAllMutationBits + : MutationBitForEventType(resolvedEventMessage)); + } + break; + case ePointerEnter: + case ePointerLeave: + mMayHavePointerEnterLeaveEventListener = true; + if (nsPIDOMWindowInner* window = GetInnerWindowForTarget()) { + NS_WARNING_ASSERTION( + !nsContentUtils::IsChromeDoc(window->GetExtantDoc()), + "Please do not use pointerenter/leave events in chrome. " + "They are slower than pointerover/out!"); + window->SetHasPointerEnterLeaveEventListeners(); + } + break; + case eGamepadButtonDown: + case eGamepadButtonUp: + case eGamepadAxisMove: + case eGamepadConnected: + case eGamepadDisconnected: + if (nsPIDOMWindowInner* window = GetInnerWindowForTarget()) { + window->SetHasGamepadEventListener(); + } + break; + case eDeviceOrientation: + case eDeviceOrientationAbsolute: + case eUserProximity: + case eDeviceLight: + case eDeviceMotion: +#if defined(MOZ_WIDGET_ANDROID) + case eOrientationChange: +#endif // #if defined(MOZ_WIDGET_ANDROID) + EnableDevice(resolvedEventMessage); + break; + case eTouchStart: + case eTouchEnd: + case eTouchMove: + case eTouchCancel: + mMayHaveTouchEventListener = true; + // we don't want touchevent listeners added by scrollbars to flip this + // flag so we ignore listeners created with system event flag + if (!aFlags.mInSystemGroup) { + if (nsPIDOMWindowInner* window = GetInnerWindowForTarget()) { + window->SetHasTouchEventListeners(); + } + } + break; + case eMouseEnter: + case eMouseLeave: + mMayHaveMouseEnterLeaveEventListener = true; + if (nsPIDOMWindowInner* window = GetInnerWindowForTarget()) { + NS_WARNING_ASSERTION( + !nsContentUtils::IsChromeDoc(window->GetExtantDoc()), + "Please do not use mouseenter/leave events in chrome. " + "They are slower than mouseover/out!"); + window->SetHasMouseEnterLeaveEventListeners(); + } + break; + case eEditorBeforeInput: + if (nsPIDOMWindowInner* window = GetInnerWindowForTarget()) { + window->SetHasBeforeInputEventListenersForTelemetry(); + } + break; + case eSelectionChange: + mMayHaveSelectionChangeEventListener = true; + if (nsPIDOMWindowInner* window = GetInnerWindowForTarget()) { + window->SetHasSelectionChangeEventListeners(); + } + break; + case eFormSelect: + mMayHaveFormSelectEventListener = true; + if (nsPIDOMWindowInner* window = GetInnerWindowForTarget()) { + window->SetHasFormSelectEventListeners(); + } + break; + case eMarqueeStart: + if (nsPIDOMWindowInner* window = GetInnerWindowForTarget()) { + if (Document* doc = window->GetExtantDoc()) { + doc->SetUseCounter(eUseCounter_custom_onstart); + } + } + break; + case eMarqueeBounce: + if (nsPIDOMWindowInner* window = GetInnerWindowForTarget()) { + if (Document* doc = window->GetExtantDoc()) { + doc->SetUseCounter(eUseCounter_custom_onbounce); + } + } + break; + case eMarqueeFinish: + if (nsPIDOMWindowInner* window = GetInnerWindowForTarget()) { + if (Document* doc = window->GetExtantDoc()) { + doc->SetUseCounter(eUseCounter_custom_onfinish); + } + } + break; + case eScrollPortOverflow: + if (nsPIDOMWindowInner* window = GetInnerWindowForTarget()) { + if (Document* doc = window->GetExtantDoc()) { + doc->SetUseCounter(eUseCounter_custom_onoverflow); + } + } + break; + case eScrollPortUnderflow: + if (nsPIDOMWindowInner* window = GetInnerWindowForTarget()) { + if (Document* doc = window->GetExtantDoc()) { + doc->SetUseCounter(eUseCounter_custom_onunderflow); + } + } + break; + case eLegacyMouseLineOrPageScroll: + if (nsPIDOMWindowInner* window = GetInnerWindowForTarget()) { + if (Document* doc = window->GetExtantDoc()) { + doc->SetUseCounter(eUseCounter_custom_ondommousescroll); + } + } + break; + case eLegacyMousePixelScroll: + if (nsPIDOMWindowInner* window = GetInnerWindowForTarget()) { + if (Document* doc = window->GetExtantDoc()) { + doc->SetUseCounter(eUseCounter_custom_onmozmousepixelscroll); + } + } + break; + case eTransitionStart: + case eTransitionRun: + case eTransitionEnd: + case eTransitionCancel: + case eWebkitTransitionEnd: + mMayHaveTransitionEventListener = true; + if (nsPIDOMWindowInner* window = GetInnerWindowForTarget()) { + window->SetHasTransitionEventListeners(); + } + break; + case eFormCheckboxStateChange: + nsContentUtils::SetMayHaveFormCheckboxStateChangeListeners(); + break; + case eFormRadioStateChange: + nsContentUtils::SetMayHaveFormRadioStateChangeListeners(); + break; + default: + // XXX Use NS_ASSERTION here to print resolvedEventMessage since + // MOZ_ASSERT can take only string literal, not pointer to + // characters. + NS_ASSERTION( + resolvedEventMessage < eLegacyMutationEventFirst || + resolvedEventMessage > eLegacyMutationEventLast, + nsPrintfCString("You added new mutation event, but it's not " + "handled above, resolvedEventMessage=%s", + ToChar(resolvedEventMessage)) + .get()); + NS_ASSERTION(aTypeAtom != nsGkAtoms::onpointerenter, + nsPrintfCString("resolvedEventMessage=%s", + ToChar(resolvedEventMessage)) + .get()); + NS_ASSERTION(aTypeAtom != nsGkAtoms::onpointerleave, + nsPrintfCString("resolvedEventMessage=%s", + ToChar(resolvedEventMessage)) + .get()); + NS_ASSERTION( + resolvedEventMessage < eGamepadEventFirst || + resolvedEventMessage > eGamepadEventLast, + nsPrintfCString("You added new gamepad event, but it's not " + "handled above, resolvedEventMessage=%s", + ToChar(resolvedEventMessage)) + .get()); + NS_ASSERTION(aTypeAtom != nsGkAtoms::ondeviceorientation, + nsPrintfCString("resolvedEventMessage=%s", + ToChar(resolvedEventMessage)) + .get()); + NS_ASSERTION(aTypeAtom != nsGkAtoms::ondeviceorientationabsolute, + nsPrintfCString("resolvedEventMessage=%s", + ToChar(resolvedEventMessage)) + .get()); + NS_ASSERTION(aTypeAtom != nsGkAtoms::onuserproximity, + nsPrintfCString("resolvedEventMessage=%s", + ToChar(resolvedEventMessage)) + .get()); + NS_ASSERTION(aTypeAtom != nsGkAtoms::ondevicelight, + nsPrintfCString("resolvedEventMessage=%s", + ToChar(resolvedEventMessage)) + .get()); + NS_ASSERTION(aTypeAtom != nsGkAtoms::ondevicemotion, + nsPrintfCString("resolvedEventMessage=%s", + ToChar(resolvedEventMessage)) + .get()); +#if defined(MOZ_WIDGET_ANDROID) + NS_ASSERTION(aTypeAtom != nsGkAtoms::onorientationchange, + nsPrintfCString("resolvedEventMessage=%s", + ToChar(resolvedEventMessage)) + .get()); +#endif // #if defined(MOZ_WIDGET_ANDROID) + NS_ASSERTION(aTypeAtom != nsGkAtoms::ontouchstart, + nsPrintfCString("resolvedEventMessage=%s", + ToChar(resolvedEventMessage)) + .get()); + NS_ASSERTION(aTypeAtom != nsGkAtoms::ontouchend, + nsPrintfCString("resolvedEventMessage=%s", + ToChar(resolvedEventMessage)) + .get()); + NS_ASSERTION(aTypeAtom != nsGkAtoms::ontouchmove, + nsPrintfCString("resolvedEventMessage=%s", + ToChar(resolvedEventMessage)) + .get()); + NS_ASSERTION(aTypeAtom != nsGkAtoms::ontouchcancel, + nsPrintfCString("resolvedEventMessage=%s", + ToChar(resolvedEventMessage)) + .get()); + NS_ASSERTION(aTypeAtom != nsGkAtoms::onmouseenter, + nsPrintfCString("resolvedEventMessage=%s", + ToChar(resolvedEventMessage)) + .get()); + NS_ASSERTION(aTypeAtom != nsGkAtoms::onmouseleave, + nsPrintfCString("resolvedEventMessage=%s", + ToChar(resolvedEventMessage)) + .get()); + NS_ASSERTION(aTypeAtom != nsGkAtoms::onbeforeinput, + nsPrintfCString("resolvedEventMessage=%s", + ToChar(resolvedEventMessage)) + .get()); + NS_ASSERTION(aTypeAtom != nsGkAtoms::onselectionchange, + nsPrintfCString("resolvedEventMessage=%s", + ToChar(resolvedEventMessage)) + .get()); + NS_ASSERTION(aTypeAtom != nsGkAtoms::onselect, + nsPrintfCString("resolvedEventMessage=%s", + ToChar(resolvedEventMessage)) + .get()); + NS_ASSERTION(aTypeAtom != nsGkAtoms::onstart, + nsPrintfCString("resolvedEventMessage=%s", + ToChar(resolvedEventMessage)) + .get()); + NS_ASSERTION(aTypeAtom != nsGkAtoms::onbounce, + nsPrintfCString("resolvedEventMessage=%s", + ToChar(resolvedEventMessage)) + .get()); + NS_ASSERTION(aTypeAtom != nsGkAtoms::onfinish, + nsPrintfCString("resolvedEventMessage=%s", + ToChar(resolvedEventMessage)) + .get()); + NS_ASSERTION(aTypeAtom != nsGkAtoms::onoverflow, + nsPrintfCString("resolvedEventMessage=%s", + ToChar(resolvedEventMessage)) + .get()); + NS_ASSERTION(aTypeAtom != nsGkAtoms::onunderflow, + nsPrintfCString("resolvedEventMessage=%s", + ToChar(resolvedEventMessage)) + .get()); + NS_ASSERTION(aTypeAtom != nsGkAtoms::onDOMMouseScroll, + nsPrintfCString("resolvedEventMessage=%s", + ToChar(resolvedEventMessage)) + .get()); + NS_ASSERTION(aTypeAtom != nsGkAtoms::onMozMousePixelScroll, + nsPrintfCString("resolvedEventMessage=%s", + ToChar(resolvedEventMessage)) + .get()); + break; + } + } + + if (IsApzAwareListener(listener)) { + ProcessApzAwareEventListenerAdd(); + } + + if (mTarget) { + mTarget->EventListenerAdded(aTypeAtom); + } + + if (mIsMainThreadELM && mTarget) { + EventListenerService::NotifyAboutMainThreadListenerChange(mTarget, + aTypeAtom); + } +} + +void EventListenerManager::ProcessApzAwareEventListenerAdd() { + Document* doc = nullptr; + + // Mark the node as having apz aware listeners + if (nsINode* node = nsINode::FromEventTargetOrNull(mTarget)) { + node->SetMayBeApzAware(); + doc = node->OwnerDoc(); + } + + // Schedule a paint so event regions on the layer tree gets updated + if (!doc) { + if (nsCOMPtr<nsPIDOMWindowInner> window = GetTargetAsInnerWindow()) { + doc = window->GetExtantDoc(); + } + } + if (!doc) { + if (nsCOMPtr<DOMEventTargetHelper> helper = do_QueryInterface(mTarget)) { + if (nsPIDOMWindowInner* window = helper->GetOwner()) { + doc = window->GetExtantDoc(); + } + } + } + + if (doc && gfxPlatform::AsyncPanZoomEnabled()) { + PresShell* presShell = doc->GetPresShell(); + if (presShell) { + nsIFrame* f = presShell->GetRootFrame(); + if (f) { + f->SchedulePaint(); + } + } + } +} + +bool EventListenerManager::IsDeviceType(EventMessage aEventMessage) { + switch (aEventMessage) { + case eDeviceOrientation: + case eDeviceOrientationAbsolute: + case eDeviceMotion: + case eDeviceLight: + case eUserProximity: +#if defined(MOZ_WIDGET_ANDROID) + case eOrientationChange: +#endif + return true; + default: + break; + } + return false; +} + +void EventListenerManager::EnableDevice(EventMessage aEventMessage) { + nsCOMPtr<nsPIDOMWindowInner> window = GetTargetAsInnerWindow(); + if (!window) { + return; + } + + switch (aEventMessage) { + case eDeviceOrientation: +#ifdef MOZ_WIDGET_ANDROID + // Falls back to SENSOR_ROTATION_VECTOR and SENSOR_ORIENTATION if + // unavailable on device. + window->EnableDeviceSensor(SENSOR_GAME_ROTATION_VECTOR); + window->EnableDeviceSensor(SENSOR_ROTATION_VECTOR); +#else + window->EnableDeviceSensor(SENSOR_ORIENTATION); +#endif + break; + case eDeviceOrientationAbsolute: +#ifdef MOZ_WIDGET_ANDROID + // Falls back to SENSOR_ORIENTATION if unavailable on device. + window->EnableDeviceSensor(SENSOR_ROTATION_VECTOR); +#else + window->EnableDeviceSensor(SENSOR_ORIENTATION); +#endif + break; + case eUserProximity: + window->EnableDeviceSensor(SENSOR_PROXIMITY); + break; + case eDeviceLight: + window->EnableDeviceSensor(SENSOR_LIGHT); + break; + case eDeviceMotion: + window->EnableDeviceSensor(SENSOR_ACCELERATION); + window->EnableDeviceSensor(SENSOR_LINEAR_ACCELERATION); + window->EnableDeviceSensor(SENSOR_GYROSCOPE); + break; +#if defined(MOZ_WIDGET_ANDROID) + case eOrientationChange: + window->EnableOrientationChangeListener(); + break; +#endif + default: + NS_WARNING("Enabling an unknown device sensor."); + break; + } +} + +void EventListenerManager::DisableDevice(EventMessage aEventMessage) { + nsCOMPtr<nsPIDOMWindowInner> window = GetTargetAsInnerWindow(); + if (!window) { + return; + } + + switch (aEventMessage) { + case eDeviceOrientation: +#ifdef MOZ_WIDGET_ANDROID + // Disable all potential fallback sensors. + window->DisableDeviceSensor(SENSOR_GAME_ROTATION_VECTOR); + window->DisableDeviceSensor(SENSOR_ROTATION_VECTOR); +#endif + window->DisableDeviceSensor(SENSOR_ORIENTATION); + break; + case eDeviceOrientationAbsolute: +#ifdef MOZ_WIDGET_ANDROID + window->DisableDeviceSensor(SENSOR_ROTATION_VECTOR); +#endif + window->DisableDeviceSensor(SENSOR_ORIENTATION); + break; + case eDeviceMotion: + window->DisableDeviceSensor(SENSOR_ACCELERATION); + window->DisableDeviceSensor(SENSOR_LINEAR_ACCELERATION); + window->DisableDeviceSensor(SENSOR_GYROSCOPE); + break; + case eUserProximity: + window->DisableDeviceSensor(SENSOR_PROXIMITY); + break; + case eDeviceLight: + window->DisableDeviceSensor(SENSOR_LIGHT); + break; +#if defined(MOZ_WIDGET_ANDROID) + case eOrientationChange: + window->DisableOrientationChangeListener(); + break; +#endif + default: + NS_WARNING("Disabling an unknown device sensor."); + break; + } +} + +void EventListenerManager::NotifyEventListenerRemoved(nsAtom* aUserType) { + // If the following code is changed, other callsites of EventListenerRemoved + // and NotifyAboutMainThreadListenerChange should be changed too. + ClearNoListenersForEvents(); + mNoListenerForEventAtom = nullptr; + if (mTarget) { + mTarget->EventListenerRemoved(aUserType); + } + if (mIsMainThreadELM && mTarget) { + EventListenerService::NotifyAboutMainThreadListenerChange(mTarget, + aUserType); + } +} + +void EventListenerManager::RemoveEventListenerInternal( + EventListenerHolder aListenerHolder, EventMessage aEventMessage, + nsAtom* aUserType, const EventListenerFlags& aFlags, bool aAllEvents) { + if (!aListenerHolder || !aEventMessage || mClearingListeners) { + return; + } + + Listener* listener; + + uint32_t count = mListeners.Length(); + bool deviceType = IsDeviceType(aEventMessage); + + RefPtr<EventListenerManager> kungFuDeathGrip(this); + + for (uint32_t i = 0; i < count; ++i) { + listener = &mListeners.ElementAt(i); + if (EVENT_TYPE_EQUALS(listener, aEventMessage, aUserType, aAllEvents)) { + if (listener->mListener == aListenerHolder && + listener->mFlags.EqualsForRemoval(aFlags)) { + mListeners.RemoveElementAt(i); + NotifyEventListenerRemoved(aUserType); + if (!aAllEvents && deviceType) { + DisableDevice(aEventMessage); + } + return; + } + } + } +} + +bool EventListenerManager::Listener::MatchesEventMessage( + const WidgetEvent* aEvent, EventMessage aEventMessage) const { + MOZ_ASSERT(aEventMessage == aEvent->mMessage || + aEventMessage == EventListenerManager::GetLegacyEventMessage( + aEvent->mMessage), + "aEvent and aEventMessage should agree, modulo legacyness"); + + if (MOZ_UNLIKELY(mAllEvents)) { + return true; + } + + // This is slightly different from EVENT_TYPE_EQUALS in that it returns + // true even when aEvent->mMessage == eUnidentifiedEvent and + // mEventMessage != eUnidentifiedEvent as long as the atoms are + // the same. + if (aEvent->mMessage == eUnidentifiedEvent) { + return mTypeAtom == aEvent->mSpecifiedEventType; + } + return mEventMessage == aEventMessage; +} + +static bool IsDefaultPassiveWhenOnRoot(EventMessage aMessage) { + if (aMessage == eTouchStart || aMessage == eTouchMove) { + return StaticPrefs::dom_event_default_to_passive_touch_listeners(); + } + if (aMessage == eWheel || aMessage == eLegacyMouseLineOrPageScroll || + aMessage == eLegacyMousePixelScroll) { + return StaticPrefs::dom_event_default_to_passive_wheel_listeners(); + } + return false; +} + +static bool IsRootEventTarget(EventTarget* aTarget) { + if (!aTarget) { + return false; + } + if (aTarget->IsInnerWindow()) { + return true; + } + const nsINode* node = nsINode::FromEventTarget(aTarget); + if (!node) { + return false; + } + Document* doc = node->OwnerDoc(); + return node == doc || node == doc->GetRootElement() || node == doc->GetBody(); +} + +void EventListenerManager::MaybeMarkPassive(EventMessage aMessage, + EventListenerFlags& aFlags) { + if (!mIsMainThreadELM) { + return; + } + if (!IsDefaultPassiveWhenOnRoot(aMessage)) { + return; + } + if (!IsRootEventTarget(mTarget)) { + return; + } + aFlags.mPassive = true; +} + +void EventListenerManager::AddEventListenerByType( + EventListenerHolder aListenerHolder, const nsAString& aType, + const EventListenerFlags& aFlags, const Optional<bool>& aPassive, + AbortSignal* aSignal) { + RefPtr<nsAtom> atom; + EventMessage message = + GetEventMessageAndAtomForListener(aType, getter_AddRefs(atom)); + + EventListenerFlags flags = aFlags; + if (aPassive.WasPassed()) { + flags.mPassive = aPassive.Value(); + } else { + MaybeMarkPassive(message, flags); + } + + AddEventListenerInternal(std::move(aListenerHolder), message, atom, flags, + false, false, aSignal); +} + +void EventListenerManager::RemoveEventListenerByType( + EventListenerHolder aListenerHolder, const nsAString& aType, + const EventListenerFlags& aFlags) { + RefPtr<nsAtom> atom; + EventMessage message = + GetEventMessageAndAtomForListener(aType, getter_AddRefs(atom)); + RemoveEventListenerInternal(std::move(aListenerHolder), message, atom, + aFlags); +} + +EventListenerManager::Listener* EventListenerManager::FindEventHandler( + EventMessage aEventMessage, nsAtom* aTypeAtom) { + // Run through the listeners for this type and see if a script + // listener is registered + Listener* listener; + uint32_t count = mListeners.Length(); + for (uint32_t i = 0; i < count; ++i) { + listener = &mListeners.ElementAt(i); + if (listener->mListenerIsHandler && + EVENT_TYPE_EQUALS(listener, aEventMessage, aTypeAtom, false)) { + return listener; + } + } + return nullptr; +} + +EventListenerManager::Listener* EventListenerManager::SetEventHandlerInternal( + nsAtom* aName, const TypedEventHandler& aTypedHandler, + bool aPermitUntrustedEvents) { + MOZ_ASSERT(aName); + + EventMessage eventMessage = GetEventMessage(aName); + Listener* listener = FindEventHandler(eventMessage, aName); + + if (!listener) { + // If we didn't find a script listener or no listeners existed + // create and add a new one. + EventListenerFlags flags; + flags.mListenerIsJSListener = true; + MaybeMarkPassive(eventMessage, flags); + + nsCOMPtr<JSEventHandler> jsEventHandler; + NS_NewJSEventHandler(mTarget, aName, aTypedHandler, + getter_AddRefs(jsEventHandler)); + AddEventListenerInternal(EventListenerHolder(jsEventHandler), eventMessage, + aName, flags, true); + + listener = FindEventHandler(eventMessage, aName); + } else { + JSEventHandler* jsEventHandler = listener->GetJSEventHandler(); + MOZ_ASSERT(jsEventHandler, + "How can we have an event handler with no JSEventHandler?"); + + bool same = jsEventHandler->GetTypedEventHandler() == aTypedHandler; + // Possibly the same listener, but update still the context and scope. + jsEventHandler->SetHandler(aTypedHandler); + if (mTarget && !same) { + mTarget->EventListenerRemoved(aName); + mTarget->EventListenerAdded(aName); + } + if (mIsMainThreadELM && mTarget) { + EventListenerService::NotifyAboutMainThreadListenerChange(mTarget, aName); + } + } + + // Set flag to indicate possible need for compilation later + listener->mHandlerIsString = !aTypedHandler.HasEventHandler(); + if (aPermitUntrustedEvents) { + listener->mFlags.mAllowUntrustedEvents = true; + mMayHaveListenersForUntrustedEvents = true; + } + + return listener; +} + +nsresult EventListenerManager::SetEventHandler(nsAtom* aName, + const nsAString& aBody, + bool aDeferCompilation, + bool aPermitUntrustedEvents, + Element* aElement) { + auto removeEventHandler = MakeScopeExit([&] { RemoveEventHandler(aName); }); + + nsCOMPtr<Document> doc; + nsCOMPtr<nsIScriptGlobalObject> global = + GetScriptGlobalAndDocument(getter_AddRefs(doc)); + + if (!global) { + // This can happen; for example this document might have been + // loaded as data. + return NS_OK; + } + + nsresult rv = NS_OK; + // return early preventing the event listener from being added + // 'doc' is fetched above + if (doc) { + // Don't allow adding an event listener if the document is sandboxed + // without 'allow-scripts'. + if (doc->HasScriptsBlockedBySandbox()) { + return NS_ERROR_DOM_SECURITY_ERR; + } + + // Perform CSP check + nsCOMPtr<nsIContentSecurityPolicy> csp = doc->GetCsp(); + unsigned lineNum = 0; + unsigned columnNum = 0; + + JSContext* cx = nsContentUtils::GetCurrentJSContext(); + if (cx && !JS::DescribeScriptedCaller(cx, nullptr, &lineNum, &columnNum)) { + JS_ClearPendingException(cx); + } + + if (csp) { + bool allowsInlineScript = true; + rv = csp->GetAllowsInline( + nsIContentSecurityPolicy::SCRIPT_SRC_ATTR_DIRECTIVE, + true, // aHasUnsafeHash + u""_ns, // aNonce + true, // aParserCreated (true because attribute event handler) + aElement, + nullptr, // nsICSPEventListener + aBody, lineNum, columnNum, &allowsInlineScript); + NS_ENSURE_SUCCESS(rv, rv); + + // return early if CSP wants us to block inline scripts + if (!allowsInlineScript) { + return NS_OK; + } + } + } + + // This might be the first reference to this language in the global + // We must init the language before we attempt to fetch its context. + if (NS_FAILED(global->EnsureScriptEnvironment())) { + NS_WARNING("Failed to setup script environment for this language"); + // but fall through and let the inevitable failure below handle it. + } + + nsIScriptContext* context = global->GetScriptContext(); + NS_ENSURE_TRUE(context, NS_ERROR_FAILURE); + NS_ENSURE_STATE(global->HasJSGlobal()); + + removeEventHandler.release(); + + Listener* listener = SetEventHandlerInternal(aName, TypedEventHandler(), + aPermitUntrustedEvents); + + if (!aDeferCompilation) { + return CompileEventHandlerInternal(listener, &aBody, aElement); + } + + return NS_OK; +} + +void EventListenerManager::RemoveEventHandler(nsAtom* aName) { + if (mClearingListeners) { + return; + } + + EventMessage eventMessage = GetEventMessage(aName); + Listener* listener = FindEventHandler(eventMessage, aName); + + if (listener) { + mListeners.RemoveElementAt(uint32_t(listener - &mListeners.ElementAt(0))); + NotifyEventListenerRemoved(aName); + if (IsDeviceType(eventMessage)) { + DisableDevice(eventMessage); + } + } +} + +nsresult EventListenerManager::CompileEventHandlerInternal( + Listener* aListener, const nsAString* aBody, Element* aElement) { + MOZ_ASSERT(aListener->GetJSEventHandler()); + MOZ_ASSERT(aListener->mHandlerIsString, + "Why are we compiling a non-string JS listener?"); + JSEventHandler* jsEventHandler = aListener->GetJSEventHandler(); + MOZ_ASSERT(!jsEventHandler->GetTypedEventHandler().HasEventHandler(), + "What is there to compile?"); + + nsresult result = NS_OK; + nsCOMPtr<Document> doc; + nsCOMPtr<nsIScriptGlobalObject> global = + GetScriptGlobalAndDocument(getter_AddRefs(doc)); + NS_ENSURE_STATE(global); + + // Activate JSAPI, and make sure that exceptions are reported on the right + // Window. + AutoJSAPI jsapi; + if (NS_WARN_IF(!jsapi.Init(global))) { + return NS_ERROR_UNEXPECTED; + } + JSContext* cx = jsapi.cx(); + + RefPtr<nsAtom> typeAtom = aListener->mTypeAtom; + nsAtom* attrName = typeAtom; + + // Flag us as not a string so we don't keep trying to compile strings which + // can't be compiled. + aListener->mHandlerIsString = false; + + // mTarget may not be an Element if it's a window and we're + // getting an inline event listener forwarded from <html:body> or + // <html:frameset> or <xul:window> or the like. + // XXX I don't like that we have to reference content from + // here. The alternative is to store the event handler string on + // the JSEventHandler itself, and that still doesn't address + // the arg names issue. + RefPtr<Element> element = Element::FromEventTargetOrNull(mTarget); + MOZ_ASSERT(element || aBody, "Where will we get our body?"); + nsAutoString handlerBody; + const nsAString* body = aBody; + if (!aBody) { + if (aListener->mTypeAtom == nsGkAtoms::onSVGLoad) { + attrName = nsGkAtoms::onload; + } else if (aListener->mTypeAtom == nsGkAtoms::onSVGScroll) { + attrName = nsGkAtoms::onscroll; + } else if (aListener->mTypeAtom == nsGkAtoms::onbeginEvent) { + attrName = nsGkAtoms::onbegin; + } else if (aListener->mTypeAtom == nsGkAtoms::onrepeatEvent) { + attrName = nsGkAtoms::onrepeat; + } else if (aListener->mTypeAtom == nsGkAtoms::onendEvent) { + attrName = nsGkAtoms::onend; + } else if (aListener->mTypeAtom == nsGkAtoms::onwebkitAnimationEnd) { + attrName = nsGkAtoms::onwebkitanimationend; + } else if (aListener->mTypeAtom == nsGkAtoms::onwebkitAnimationIteration) { + attrName = nsGkAtoms::onwebkitanimationiteration; + } else if (aListener->mTypeAtom == nsGkAtoms::onwebkitAnimationStart) { + attrName = nsGkAtoms::onwebkitanimationstart; + } else if (aListener->mTypeAtom == nsGkAtoms::onwebkitTransitionEnd) { + attrName = nsGkAtoms::onwebkittransitionend; + } + + element->GetAttr(kNameSpaceID_None, attrName, handlerBody); + body = &handlerBody; + aElement = element; + } + aListener = nullptr; + + nsAutoCString url("-moz-evil:lying-event-listener"_ns); + MOZ_ASSERT(body); + MOZ_ASSERT(aElement); + nsIURI* uri = aElement->OwnerDoc()->GetDocumentURI(); + if (uri) { + uri->GetSpec(url); + } + + nsCOMPtr<nsPIDOMWindowInner> win = + nsPIDOMWindowInner::FromEventTargetOrNull(mTarget); + uint32_t argCount; + const char** argNames; + nsContentUtils::GetEventArgNames(aElement->GetNameSpaceID(), typeAtom, win, + &argCount, &argNames); + + // Wrap the event target, so that we can use it as the scope for the event + // handler. Note that mTarget is different from aElement in the <body> case, + // where mTarget is a Window. + // + // The wrapScope doesn't really matter here, because the target will create + // its reflector in the proper scope, and then we'll enter that realm. + JS::Rooted<JSObject*> wrapScope(cx, global->GetGlobalJSObject()); + JS::Rooted<JS::Value> v(cx); + { + JSAutoRealm ar(cx, wrapScope); + nsresult rv = nsContentUtils::WrapNative(cx, mTarget, &v, + /* aAllowWrapping = */ false); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + JS::Rooted<JSObject*> target(cx, &v.toObject()); + JSAutoRealm ar(cx, target); + + // Now that we've entered the realm we actually care about, create our + // scope chain. Note that we start with |element|, not aElement, because + // mTarget is different from aElement in the <body> case, where mTarget is a + // Window, and in that case we do not want the scope chain to include the body + // or the document. + JS::RootedVector<JSObject*> scopeChain(cx); + if (!nsJSUtils::GetScopeChainForElement(cx, element, &scopeChain)) { + return NS_ERROR_OUT_OF_MEMORY; + } + + nsDependentAtomString str(attrName); + // Most of our names are short enough that we don't even have to malloc + // the JS string stuff, so don't worry about playing games with + // refcounting XPCOM stringbuffers. + JS::Rooted<JSString*> jsStr( + cx, JS_NewUCStringCopyN(cx, str.BeginReading(), str.Length())); + NS_ENSURE_TRUE(jsStr, NS_ERROR_OUT_OF_MEMORY); + + // Get the reflector for |aElement|, so that we can pass to setElement. + if (NS_WARN_IF(!GetOrCreateDOMReflector(cx, aElement, &v))) { + return NS_ERROR_FAILURE; + } + + RefPtr<JS::loader::ScriptFetchOptions> fetchOptions = + new JS::loader::ScriptFetchOptions( + CORS_NONE, aElement->OwnerDoc()->GetReferrerPolicy(), + aElement->OwnerDoc()->NodePrincipal()); + + RefPtr<JS::loader::EventScript> eventScript = + new JS::loader::EventScript(fetchOptions, uri); + + JS::CompileOptions options(cx); + // Use line 0 to make the function body starts from line 1. + options.setIntroductionType("eventHandler") + .setFileAndLine(url.get(), 0) + .setDeferDebugMetadata(true); + + JS::Rooted<JSObject*> handler(cx); + result = nsJSUtils::CompileFunction(jsapi, scopeChain, options, + nsAtomCString(typeAtom), argCount, + argNames, *body, handler.address()); + NS_ENSURE_SUCCESS(result, result); + NS_ENSURE_TRUE(handler, NS_ERROR_FAILURE); + + JS::Rooted<JS::Value> privateValue(cx, JS::PrivateValue(eventScript)); + result = nsJSUtils::UpdateFunctionDebugMetadata(jsapi, handler, options, + jsStr, privateValue); + NS_ENSURE_SUCCESS(result, result); + + MOZ_ASSERT(js::IsObjectInContextCompartment(handler, cx)); + JS::Rooted<JSObject*> handlerGlobal(cx, JS::CurrentGlobalOrNull(cx)); + + if (jsEventHandler->EventName() == nsGkAtoms::onerror && win) { + RefPtr<OnErrorEventHandlerNonNull> handlerCallback = + new OnErrorEventHandlerNonNull(static_cast<JSContext*>(nullptr), + handler, handlerGlobal, + /* aIncumbentGlobal = */ nullptr); + jsEventHandler->SetHandler(handlerCallback); + } else if (jsEventHandler->EventName() == nsGkAtoms::onbeforeunload && win) { + RefPtr<OnBeforeUnloadEventHandlerNonNull> handlerCallback = + new OnBeforeUnloadEventHandlerNonNull(static_cast<JSContext*>(nullptr), + handler, handlerGlobal, + /* aIncumbentGlobal = */ nullptr); + jsEventHandler->SetHandler(handlerCallback); + } else { + RefPtr<EventHandlerNonNull> handlerCallback = new EventHandlerNonNull( + static_cast<JSContext*>(nullptr), handler, handlerGlobal, + /* aIncumbentGlobal = */ nullptr); + jsEventHandler->SetHandler(handlerCallback); + } + + return result; +} + +nsresult EventListenerManager::HandleEventSubType(Listener* aListener, + Event* aDOMEvent, + EventTarget* aCurrentTarget) { + nsresult result = NS_OK; + // strong ref + EventListenerHolder listenerHolder(aListener->mListener.Clone()); + + // If this is a script handler and we haven't yet + // compiled the event handler itself + if ((aListener->mListenerType == Listener::eJSEventListener) && + aListener->mHandlerIsString) { + result = CompileEventHandlerInternal(aListener, nullptr, nullptr); + aListener = nullptr; + } + + if (NS_SUCCEEDED(result)) { + Maybe<EventCallbackDebuggerNotificationGuard> dbgGuard; + if (dom::ChromeUtils::IsDevToolsOpened()) { + dbgGuard.emplace(aCurrentTarget, aDOMEvent); + } + nsAutoMicroTask mt; + + // Event::currentTarget is set in EventDispatcher. + if (listenerHolder.HasWebIDLCallback()) { + ErrorResult rv; + listenerHolder.GetWebIDLCallback()->HandleEvent(aCurrentTarget, + *aDOMEvent, rv); + result = rv.StealNSResult(); + } else { + // listenerHolder is holding a stack ref here. + result = MOZ_KnownLive(listenerHolder.GetXPCOMCallback()) + ->HandleEvent(aDOMEvent); + } + } + + return result; +} + +/* static */ EventMessage EventListenerManager::GetLegacyEventMessage( + EventMessage aEventMessage) { + // webkit-prefixed legacy events: + if (aEventMessage == eTransitionEnd) { + return eWebkitTransitionEnd; + } + if (aEventMessage == eAnimationStart) { + return eWebkitAnimationStart; + } + if (aEventMessage == eAnimationEnd) { + return eWebkitAnimationEnd; + } + if (aEventMessage == eAnimationIteration) { + return eWebkitAnimationIteration; + } + + switch (aEventMessage) { + case eFullscreenChange: + return eMozFullscreenChange; + case eFullscreenError: + return eMozFullscreenError; + default: + return aEventMessage; + } +} + +EventMessage EventListenerManager::GetEventMessage(nsAtom* aEventName) const { + if (mIsMainThreadELM) { + return nsContentUtils::GetEventMessage(aEventName); + } + + // The nsContentUtils event message hashtables aren't threadsafe, so just fall + // back to eUnidentifiedEvent. + return eUnidentifiedEvent; +} + +EventMessage EventListenerManager::GetEventMessageAndAtomForListener( + const nsAString& aType, nsAtom** aAtom) { + if (mIsMainThreadELM) { + return nsContentUtils::GetEventMessageAndAtomForListener(aType, aAtom); + } + + *aAtom = NS_Atomize(u"on"_ns + aType).take(); + return eUnidentifiedEvent; +} + +already_AddRefed<nsPIDOMWindowInner> EventListenerManager::WindowFromListener( + Listener* aListener, bool aItemInShadowTree) { + nsCOMPtr<nsPIDOMWindowInner> innerWindow; + if (!aItemInShadowTree) { + if (aListener->mListener.HasWebIDLCallback()) { + CallbackObject* callback = aListener->mListener.GetWebIDLCallback(); + nsIGlobalObject* global = nullptr; + if (callback) { + global = callback->IncumbentGlobalOrNull(); + } + if (global) { + innerWindow = global->AsInnerWindow(); // Can be nullptr + } + } else { + // Can't get the global from + // listener->mListener.GetXPCOMCallback(). + // In most cases, it would be the same as for + // the target, so let's do that. + innerWindow = GetInnerWindowForTarget(); // Can be nullptr + } + } + return innerWindow.forget(); +} + +/** + * Causes a check for event listeners and processing by them if they exist. + * @param an event listener + */ + +void EventListenerManager::HandleEventInternal(nsPresContext* aPresContext, + WidgetEvent* aEvent, + Event** aDOMEvent, + EventTarget* aCurrentTarget, + nsEventStatus* aEventStatus, + bool aItemInShadowTree) { + MOZ_ASSERT_IF(aEvent->mMessage != eUnidentifiedEvent, mIsMainThreadELM); + + // Set the value of the internal PreventDefault flag properly based on + // aEventStatus + if (!aEvent->DefaultPrevented() && + *aEventStatus == nsEventStatus_eConsumeNoDefault) { + // Assume that if only aEventStatus claims that the event has already been + // consumed, the consumer is default event handler. + aEvent->PreventDefault(); + } + + Maybe<AutoHandlingUserInputStatePusher> userInputStatePusher; + Maybe<AutoPopupStatePusher> popupStatePusher; + if (mIsMainThreadELM) { + userInputStatePusher.emplace(UserActivation::IsUserInteractionEvent(aEvent), + aEvent); + popupStatePusher.emplace( + PopupBlocker::GetEventPopupControlState(aEvent, *aDOMEvent)); + } + + bool hasListener = false; + bool hasListenerForCurrentGroup = false; + bool usingLegacyMessage = false; + bool hasRemovedListener = false; + EventMessage eventMessage = aEvent->mMessage; + + while (true) { + Maybe<EventMessageAutoOverride> legacyAutoOverride; + for (Listener& listenerRef : mListeners.EndLimitedRange()) { + if (aEvent->mFlags.mImmediatePropagationStopped) { + break; + } + Listener* listener = &listenerRef; + if (!listener->MatchesEventMessage(aEvent, eventMessage)) { + continue; + } + if (listener->mListenerType == Listener::eNoListener) { + // The listener has been removed, it cannot handle anything. + continue; + } + if (!listener->mEnabled) { + // The listener has been disabled, for example by devtools. + continue; + } + hasListener = true; + if (!listener->MatchesEventGroup(aEvent)) { + continue; + } + hasListenerForCurrentGroup = true; + + // Check that the phase is same in event and event listener. Also check + // that the event is trusted or that the listener allows untrusted events. + if (!listener->MatchesEventPhase(aEvent) || + !listener->AllowsEventTrustedness(aEvent)) { + continue; + } + if (!*aDOMEvent) { + // Lazily create the DOM event. + // This is tiny bit slow, but happens only once per event. + // Similar code also in EventDispatcher. + nsCOMPtr<EventTarget> et = aEvent->mOriginalTarget; + RefPtr<Event> event = + EventDispatcher::CreateEvent(et, aPresContext, aEvent, u""_ns); + event.forget(aDOMEvent); + } + if (!*aDOMEvent) { + continue; + } + if (!aEvent->mCurrentTarget) { + aEvent->mCurrentTarget = aCurrentTarget->GetTargetForDOMEvent(); + if (!aEvent->mCurrentTarget) { + break; + } + } + if (usingLegacyMessage && !legacyAutoOverride) { + // Override the aDOMEvent's event-message (its .type) until we + // finish traversing listeners (when legacyAutoOverride destructs) + legacyAutoOverride.emplace(*aDOMEvent, eventMessage); + } + + aEvent->mFlags.mInPassiveListener = listener->mFlags.mPassive; + Maybe<Listener> listenerHolder; + if (listener->mFlags.mOnce) { + // Move the listener to the stack before handling the event. + // The order is important, otherwise the listener could be + // called again inside the listener. + listenerHolder.emplace(std::move(*listener)); + listener = listenerHolder.ptr(); + hasRemovedListener = true; + } + + nsCOMPtr<nsPIDOMWindowInner> innerWindow = + WindowFromListener(listener, aItemInShadowTree); + mozilla::dom::Event* oldWindowEvent = nullptr; + if (innerWindow) { + oldWindowEvent = innerWindow->SetEvent(*aDOMEvent); + } + + nsresult rv = HandleEventSubType(listener, *aDOMEvent, aCurrentTarget); + + if (innerWindow) { + Unused << innerWindow->SetEvent(oldWindowEvent); + } + + if (NS_FAILED(rv)) { + aEvent->mFlags.mExceptionWasRaised = true; + } + aEvent->mFlags.mInPassiveListener = false; + } + + // If we didn't find any matching listeners, and our event has a legacy + // version, we'll now switch to looking for that legacy version and we'll + // recheck our listeners. + if (hasListenerForCurrentGroup || usingLegacyMessage || + !aEvent->IsTrusted()) { + // No need to recheck listeners, because we already found a match, we + // already rechecked them, or it is not a trusted event. + break; + } + EventMessage legacyEventMessage = GetLegacyEventMessage(eventMessage); + if (legacyEventMessage == eventMessage) { + break; // There's no legacy version of our event; no need to recheck. + } + MOZ_ASSERT( + GetLegacyEventMessage(legacyEventMessage) == legacyEventMessage, + "Legacy event messages should not themselves have legacy versions"); + + // Recheck our listeners, using the legacy event message we just looked up: + eventMessage = legacyEventMessage; + usingLegacyMessage = true; + } + + aEvent->mCurrentTarget = nullptr; + + if (hasRemovedListener) { + // If there are any once listeners replaced with a placeholder in + // the loop above, we need to clean up them here. Note that, this + // could clear once listeners handled in some outer level as well, + // but that should not affect the result. + mListeners.NonObservingRemoveElementsBy([](const Listener& aListener) { + return aListener.mListenerType == Listener::eNoListener; + }); + NotifyEventListenerRemoved(aEvent->mSpecifiedEventType); + if (IsDeviceType(aEvent->mMessage)) { + // This is a device-type event, we need to check whether we can + // disable device after removing the once listeners. + const auto [begin, end] = mListeners.NonObservingRange(); + const bool hasAnyListener = + std::any_of(begin, end, [aEvent](const Listener& listenerRef) { + const Listener* listener = &listenerRef; + return EVENT_TYPE_EQUALS(listener, aEvent->mMessage, + aEvent->mSpecifiedEventType, + /* all events */ false); + }); + + if (!hasAnyListener) { + DisableDevice(aEvent->mMessage); + } + } + } + + if (mIsMainThreadELM && !hasListener) { + if (aEvent->mMessage != eUnidentifiedEvent) { + mNoListenerForEvents[2] = mNoListenerForEvents[1]; + mNoListenerForEvents[1] = mNoListenerForEvents[0]; + mNoListenerForEvents[0] = aEvent->mMessage; + } else { + mNoListenerForEventAtom = aEvent->mSpecifiedEventType; + } + } + + if (aEvent->DefaultPrevented()) { + *aEventStatus = nsEventStatus_eConsumeNoDefault; + } +} + +void EventListenerManager::Disconnect() { + mTarget = nullptr; + RemoveAllListenersSilently(); +} + +void EventListenerManager::AddEventListener(const nsAString& aType, + EventListenerHolder aListenerHolder, + bool aUseCapture, + bool aWantsUntrusted) { + EventListenerFlags flags; + flags.mCapture = aUseCapture; + flags.mAllowUntrustedEvents = aWantsUntrusted; + return AddEventListenerByType(std::move(aListenerHolder), aType, flags); +} + +void EventListenerManager::AddEventListener( + const nsAString& aType, EventListenerHolder aListenerHolder, + const dom::AddEventListenerOptionsOrBoolean& aOptions, + bool aWantsUntrusted) { + EventListenerFlags flags; + Optional<bool> passive; + AbortSignal* signal = nullptr; + if (aOptions.IsBoolean()) { + flags.mCapture = aOptions.GetAsBoolean(); + } else { + const auto& options = aOptions.GetAsAddEventListenerOptions(); + flags.mCapture = options.mCapture; + flags.mInSystemGroup = options.mMozSystemGroup; + flags.mOnce = options.mOnce; + if (options.mPassive.WasPassed()) { + passive.Construct(options.mPassive.Value()); + } + + if (options.mSignal.WasPassed()) { + signal = &options.mSignal.Value(); + } + } + + flags.mAllowUntrustedEvents = aWantsUntrusted; + return AddEventListenerByType(std::move(aListenerHolder), aType, flags, + passive, signal); +} + +void EventListenerManager::RemoveEventListener( + const nsAString& aType, EventListenerHolder aListenerHolder, + bool aUseCapture) { + EventListenerFlags flags; + flags.mCapture = aUseCapture; + RemoveEventListenerByType(std::move(aListenerHolder), aType, flags); +} + +void EventListenerManager::RemoveEventListener( + const nsAString& aType, EventListenerHolder aListenerHolder, + const dom::EventListenerOptionsOrBoolean& aOptions) { + EventListenerFlags flags; + if (aOptions.IsBoolean()) { + flags.mCapture = aOptions.GetAsBoolean(); + } else { + const auto& options = aOptions.GetAsEventListenerOptions(); + flags.mCapture = options.mCapture; + flags.mInSystemGroup = options.mMozSystemGroup; + } + RemoveEventListenerByType(std::move(aListenerHolder), aType, flags); +} + +void EventListenerManager::AddListenerForAllEvents(EventListener* aDOMListener, + bool aUseCapture, + bool aWantsUntrusted, + bool aSystemEventGroup) { + EventListenerFlags flags; + flags.mCapture = aUseCapture; + flags.mAllowUntrustedEvents = aWantsUntrusted; + flags.mInSystemGroup = aSystemEventGroup; + AddEventListenerInternal(EventListenerHolder(aDOMListener), eAllEvents, + nullptr, flags, false, true); +} + +void EventListenerManager::RemoveListenerForAllEvents( + EventListener* aDOMListener, bool aUseCapture, bool aSystemEventGroup) { + EventListenerFlags flags; + flags.mCapture = aUseCapture; + flags.mInSystemGroup = aSystemEventGroup; + RemoveEventListenerInternal(EventListenerHolder(aDOMListener), eAllEvents, + nullptr, flags, true); +} + +bool EventListenerManager::HasMutationListeners() { + if (mMayHaveMutationListeners) { + uint32_t count = mListeners.Length(); + for (uint32_t i = 0; i < count; ++i) { + Listener* listener = &mListeners.ElementAt(i); + if (listener->mEventMessage >= eLegacyMutationEventFirst && + listener->mEventMessage <= eLegacyMutationEventLast) { + return true; + } + } + } + + return false; +} + +uint32_t EventListenerManager::MutationListenerBits() { + uint32_t bits = 0; + if (mMayHaveMutationListeners) { + uint32_t count = mListeners.Length(); + for (uint32_t i = 0; i < count; ++i) { + Listener* listener = &mListeners.ElementAt(i); + if (listener->mEventMessage >= eLegacyMutationEventFirst && + listener->mEventMessage <= eLegacyMutationEventLast) { + if (listener->mEventMessage == eLegacySubtreeModified) { + return kAllMutationBits; + } + bits |= MutationBitForEventType(listener->mEventMessage); + } + } + } + return bits; +} + +bool EventListenerManager::HasListenersFor(const nsAString& aEventName) const { + RefPtr<nsAtom> atom = NS_Atomize(u"on"_ns + aEventName); + return HasListenersFor(atom); +} + +bool EventListenerManager::HasListenersFor(nsAtom* aEventNameWithOn) const { + return HasListenersForInternal(aEventNameWithOn, false); +} + +bool EventListenerManager::HasNonSystemGroupListenersFor( + nsAtom* aEventNameWithOn) const { + return HasListenersForInternal(aEventNameWithOn, true); +} + +bool EventListenerManager::HasListenersForInternal( + nsAtom* aEventNameWithOn, bool aIgnoreSystemGroup) const { +#ifdef DEBUG + nsAutoString name; + aEventNameWithOn->ToString(name); +#endif + NS_ASSERTION(StringBeginsWith(name, u"on"_ns), + "Event name does not start with 'on'"); + uint32_t count = mListeners.Length(); + for (uint32_t i = 0; i < count; ++i) { + const Listener* listener = &mListeners.ElementAt(i); + if (listener->mTypeAtom == aEventNameWithOn) { + if (aIgnoreSystemGroup && listener->mFlags.mInSystemGroup) { + continue; + } + return true; + } + } + return false; +} + +bool EventListenerManager::HasListeners() const { + return !mListeners.IsEmpty(); +} + +nsresult EventListenerManager::GetListenerInfo( + nsTArray<RefPtr<nsIEventListenerInfo>>& aList) { + nsCOMPtr<EventTarget> target = mTarget; + NS_ENSURE_STATE(target); + aList.Clear(); + for (const Listener& listener : mListeners.ForwardRange()) { + // If this is a script handler and we haven't yet + // compiled the event handler itself go ahead and compile it + if (listener.mListenerType == Listener::eJSEventListener && + listener.mHandlerIsString) { + CompileEventHandlerInternal(const_cast<Listener*>(&listener), nullptr, + nullptr); + } + nsAutoString eventType; + if (listener.mAllEvents) { + eventType.SetIsVoid(true); + } else if (listener.mListenerType == Listener::eNoListener) { + continue; + } else { + eventType.Assign(Substring(nsDependentAtomString(listener.mTypeAtom), 2)); + } + + JS::Rooted<JSObject*> callback(RootingCx()); + JS::Rooted<JSObject*> callbackGlobal(RootingCx()); + if (JSEventHandler* handler = listener.GetJSEventHandler()) { + if (handler->GetTypedEventHandler().HasEventHandler()) { + CallbackFunction* callbackFun = handler->GetTypedEventHandler().Ptr(); + callback = callbackFun->CallableOrNull(); + callbackGlobal = callbackFun->CallbackGlobalOrNull(); + if (!callback) { + // This will be null for cross-compartment event listeners + // which have been destroyed. + continue; + } + } + } else if (listener.mListenerType == Listener::eWebIDLListener) { + EventListener* listenerCallback = listener.mListener.GetWebIDLCallback(); + callback = listenerCallback->CallbackOrNull(); + callbackGlobal = listenerCallback->CallbackGlobalOrNull(); + if (!callback) { + // This will be null for cross-compartment event listeners + // which have been destroyed. + continue; + } + } + + RefPtr<EventListenerInfo> info = new EventListenerInfo( + this, eventType, callback, callbackGlobal, listener.mFlags.mCapture, + listener.mFlags.mAllowUntrustedEvents, listener.mFlags.mInSystemGroup, + listener.mListenerIsHandler); + aList.AppendElement(info.forget()); + } + return NS_OK; +} + +EventListenerManager::Listener* EventListenerManager::GetListenerFor( + nsAString& aType, JSObject* aListener, bool aCapturing, + bool aAllowsUntrusted, bool aInSystemEventGroup, bool aIsHandler) { + NS_ENSURE_TRUE(aListener, nullptr); + + for (Listener& listener : mListeners.ForwardRange()) { + if ((aType.IsVoid() && !listener.mAllEvents) || + !Substring(nsDependentAtomString(listener.mTypeAtom), 2) + .Equals(aType) || + listener.mListenerType == Listener::eNoListener) { + continue; + } + + if (listener.mFlags.mCapture != aCapturing || + listener.mFlags.mAllowUntrustedEvents != aAllowsUntrusted || + listener.mFlags.mInSystemGroup != aInSystemEventGroup) { + continue; + } + + if (aIsHandler) { + if (JSEventHandler* handler = listener.GetJSEventHandler()) { + if (handler->GetTypedEventHandler().HasEventHandler()) { + if (handler->GetTypedEventHandler().Ptr()->CallableOrNull() == + aListener) { + return &listener; + } + } + } + } else if (listener.mListenerType == Listener::eWebIDLListener && + listener.mListener.GetWebIDLCallback()->CallbackOrNull() == + aListener) { + return &listener; + } + } + return nullptr; +} + +nsresult EventListenerManager::IsListenerEnabled( + nsAString& aType, JSObject* aListener, bool aCapturing, + bool aAllowsUntrusted, bool aInSystemEventGroup, bool aIsHandler, + bool* aEnabled) { + Listener* listener = + GetListenerFor(aType, aListener, aCapturing, aAllowsUntrusted, + aInSystemEventGroup, aIsHandler); + NS_ENSURE_TRUE(listener, NS_ERROR_NOT_AVAILABLE); + *aEnabled = listener->mEnabled; + return NS_OK; +} + +nsresult EventListenerManager::SetListenerEnabled( + nsAString& aType, JSObject* aListener, bool aCapturing, + bool aAllowsUntrusted, bool aInSystemEventGroup, bool aIsHandler, + bool aEnabled) { + Listener* listener = + GetListenerFor(aType, aListener, aCapturing, aAllowsUntrusted, + aInSystemEventGroup, aIsHandler); + NS_ENSURE_TRUE(listener, NS_ERROR_NOT_AVAILABLE); + listener->mEnabled = aEnabled; + if (aEnabled) { + // We may have enabled some listener, clear the cache for which events + // we don't have listeners. + ClearNoListenersForEvents(); + mNoListenerForEventAtom = nullptr; + } + return NS_OK; +} + +bool EventListenerManager::HasUnloadListeners() { + uint32_t count = mListeners.Length(); + for (uint32_t i = 0; i < count; ++i) { + Listener* listener = &mListeners.ElementAt(i); + if (listener->mEventMessage == eUnload) { + return true; + } + } + return false; +} + +bool EventListenerManager::HasBeforeUnloadListeners() { + uint32_t count = mListeners.Length(); + for (uint32_t i = 0; i < count; ++i) { + Listener* listener = &mListeners.ElementAt(i); + if (listener->mEventMessage == eBeforeUnload) { + return true; + } + } + return false; +} + +void EventListenerManager::SetEventHandler(nsAtom* aEventName, + EventHandlerNonNull* aHandler) { + if (!aHandler) { + RemoveEventHandler(aEventName); + return; + } + + // Untrusted events are always permitted for non-chrome script + // handlers. + SetEventHandlerInternal( + aEventName, TypedEventHandler(aHandler), + !mIsMainThreadELM || !nsContentUtils::IsCallerChrome()); +} + +void EventListenerManager::SetEventHandler( + OnErrorEventHandlerNonNull* aHandler) { + if (!aHandler) { + RemoveEventHandler(nsGkAtoms::onerror); + return; + } + + // Untrusted events are always permitted on workers and for non-chrome script + // on the main thread. + bool allowUntrusted = !mIsMainThreadELM || !nsContentUtils::IsCallerChrome(); + + SetEventHandlerInternal(nsGkAtoms::onerror, TypedEventHandler(aHandler), + allowUntrusted); +} + +void EventListenerManager::SetEventHandler( + OnBeforeUnloadEventHandlerNonNull* aHandler) { + if (!aHandler) { + RemoveEventHandler(nsGkAtoms::onbeforeunload); + return; + } + + // Untrusted events are always permitted for non-chrome script + // handlers. + SetEventHandlerInternal( + nsGkAtoms::onbeforeunload, TypedEventHandler(aHandler), + !mIsMainThreadELM || !nsContentUtils::IsCallerChrome()); +} + +const TypedEventHandler* EventListenerManager::GetTypedEventHandler( + nsAtom* aEventName) { + EventMessage eventMessage = GetEventMessage(aEventName); + Listener* listener = FindEventHandler(eventMessage, aEventName); + + if (!listener) { + return nullptr; + } + + JSEventHandler* jsEventHandler = listener->GetJSEventHandler(); + + if (listener->mHandlerIsString) { + CompileEventHandlerInternal(listener, nullptr, nullptr); + } + + const TypedEventHandler& typedHandler = + jsEventHandler->GetTypedEventHandler(); + return typedHandler.HasEventHandler() ? &typedHandler : nullptr; +} + +size_t EventListenerManager::SizeOfIncludingThis( + MallocSizeOf aMallocSizeOf) const { + size_t n = aMallocSizeOf(this); + n += mListeners.ShallowSizeOfExcludingThis(aMallocSizeOf); + uint32_t count = mListeners.Length(); + for (uint32_t i = 0; i < count; ++i) { + JSEventHandler* jsEventHandler = + mListeners.ElementAt(i).GetJSEventHandler(); + if (jsEventHandler) { + n += jsEventHandler->SizeOfIncludingThis(aMallocSizeOf); + } + } + return n; +} + +void EventListenerManager::MarkForCC() { + uint32_t count = mListeners.Length(); + for (uint32_t i = 0; i < count; ++i) { + const Listener& listener = mListeners.ElementAt(i); + JSEventHandler* jsEventHandler = listener.GetJSEventHandler(); + if (jsEventHandler) { + const TypedEventHandler& typedHandler = + jsEventHandler->GetTypedEventHandler(); + if (typedHandler.HasEventHandler()) { + typedHandler.Ptr()->MarkForCC(); + } + } else if (listener.mListenerType == Listener::eWebIDLListener) { + listener.mListener.GetWebIDLCallback()->MarkForCC(); + } + } + if (mRefCnt.IsPurple()) { + mRefCnt.RemovePurple(); + } +} + +void EventListenerManager::TraceListeners(JSTracer* aTrc) { + uint32_t count = mListeners.Length(); + for (uint32_t i = 0; i < count; ++i) { + const Listener& listener = mListeners.ElementAt(i); + JSEventHandler* jsEventHandler = listener.GetJSEventHandler(); + if (jsEventHandler) { + const TypedEventHandler& typedHandler = + jsEventHandler->GetTypedEventHandler(); + if (typedHandler.HasEventHandler()) { + mozilla::TraceScriptHolder(typedHandler.Ptr(), aTrc); + } + } else if (listener.mListenerType == Listener::eWebIDLListener) { + mozilla::TraceScriptHolder(listener.mListener.GetWebIDLCallback(), aTrc); + } + // We might have eWrappedJSListener, but that is the legacy type for + // JS implemented event listeners, and trickier to handle here. + } +} + +bool EventListenerManager::HasNonSystemGroupListenersForUntrustedKeyEvents() { + uint32_t count = mListeners.Length(); + for (uint32_t i = 0; i < count; ++i) { + Listener* listener = &mListeners.ElementAt(i); + if (!listener->mFlags.mInSystemGroup && + listener->mFlags.mAllowUntrustedEvents && + (listener->mTypeAtom == nsGkAtoms::onkeydown || + listener->mTypeAtom == nsGkAtoms::onkeypress || + listener->mTypeAtom == nsGkAtoms::onkeyup)) { + return true; + } + } + return false; +} + +bool EventListenerManager:: + HasNonPassiveNonSystemGroupListenersForUntrustedKeyEvents() { + uint32_t count = mListeners.Length(); + for (uint32_t i = 0; i < count; ++i) { + Listener* listener = &mListeners.ElementAt(i); + if (!listener->mFlags.mPassive && !listener->mFlags.mInSystemGroup && + listener->mFlags.mAllowUntrustedEvents && + (listener->mTypeAtom == nsGkAtoms::onkeydown || + listener->mTypeAtom == nsGkAtoms::onkeypress || + listener->mTypeAtom == nsGkAtoms::onkeyup)) { + return true; + } + } + return false; +} + +bool EventListenerManager::HasApzAwareListeners() { + uint32_t count = mListeners.Length(); + for (uint32_t i = 0; i < count; ++i) { + Listener* listener = &mListeners.ElementAt(i); + if (IsApzAwareListener(listener)) { + return true; + } + } + return false; +} + +bool EventListenerManager::IsApzAwareListener(Listener* aListener) { + return !aListener->mFlags.mPassive && mIsMainThreadELM && + IsApzAwareEvent(aListener->mTypeAtom); +} + +static bool IsWheelEventType(nsAtom* aEvent) { + if (aEvent == nsGkAtoms::onwheel || aEvent == nsGkAtoms::onDOMMouseScroll || + aEvent == nsGkAtoms::onmousewheel || + aEvent == nsGkAtoms::onMozMousePixelScroll) { + return true; + } + return false; +} + +bool EventListenerManager::IsApzAwareEvent(nsAtom* aEvent) { + if (IsWheelEventType(aEvent)) { + return true; + } + // In theory we should schedule a repaint if the touch event pref changes, + // because the event regions might be out of date. In practice that seems like + // overkill because users generally shouldn't be flipping this pref, much + // less expecting touch listeners on the page to immediately start preventing + // scrolling without so much as a repaint. Tests that we write can work + // around this constraint easily enough. + if (aEvent == nsGkAtoms::ontouchstart || aEvent == nsGkAtoms::ontouchmove) { + return TouchEvent::PrefEnabled( + nsContentUtils::GetDocShellForEventTarget(mTarget)); + } + return false; +} + +bool EventListenerManager::HasNonPassiveWheelListener() { + MOZ_ASSERT(NS_IsMainThread()); + uint32_t count = mListeners.Length(); + for (uint32_t i = 0; i < count; ++i) { + Listener* listener = &mListeners.ElementAt(i); + if (!listener->mFlags.mPassive && IsWheelEventType(listener->mTypeAtom)) { + return true; + } + } + return false; +} + +void EventListenerManager::RemoveAllListeners() { + while (!mListeners.IsEmpty()) { + size_t idx = mListeners.Length() - 1; + RefPtr<nsAtom> type = mListeners.ElementAt(idx).mTypeAtom; + EventMessage message = mListeners.ElementAt(idx).mEventMessage; + mListeners.RemoveElementAt(idx); + NotifyEventListenerRemoved(type); + if (IsDeviceType(message)) { + DisableDevice(message); + } + } +} + +already_AddRefed<nsIScriptGlobalObject> +EventListenerManager::GetScriptGlobalAndDocument(Document** aDoc) { + nsCOMPtr<Document> doc; + nsCOMPtr<nsPIDOMWindowInner> win; + if (nsINode* node = nsINode::FromEventTargetOrNull(mTarget)) { + // Try to get context from doc + doc = node->OwnerDoc(); + if (doc->IsLoadedAsData()) { + return nullptr; + } + + win = do_QueryInterface(doc->GetScopeObject()); + } else if ((win = GetTargetAsInnerWindow())) { + doc = win->GetExtantDoc(); + } + + if (!win || !win->IsCurrentInnerWindow()) { + return nullptr; + } + + doc.forget(aDoc); + nsCOMPtr<nsIScriptGlobalObject> global = do_QueryInterface(win); + return global.forget(); +} + +EventListenerManager::ListenerSignalFollower::ListenerSignalFollower( + EventListenerManager* aListenerManager, + EventListenerManager::Listener* aListener) + : dom::AbortFollower(), + mListenerManager(aListenerManager), + mListener(aListener->mListener.Clone()), + mTypeAtom(aListener->mTypeAtom), + mEventMessage(aListener->mEventMessage), + mAllEvents(aListener->mAllEvents), + mFlags(aListener->mFlags){}; + +NS_IMPL_CYCLE_COLLECTION_CLASS(EventListenerManager::ListenerSignalFollower) + +NS_IMPL_CYCLE_COLLECTING_ADDREF(EventListenerManager::ListenerSignalFollower) +NS_IMPL_CYCLE_COLLECTING_RELEASE(EventListenerManager::ListenerSignalFollower) + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN( + EventListenerManager::ListenerSignalFollower) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mListener) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN( + EventListenerManager::ListenerSignalFollower) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mListener) + tmp->mListenerManager = nullptr; +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION( + EventListenerManager::ListenerSignalFollower) + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +void EventListenerManager::ListenerSignalFollower::RunAbortAlgorithm() { + if (mListenerManager) { + RefPtr<EventListenerManager> elm = mListenerManager; + mListenerManager = nullptr; + elm->RemoveEventListenerInternal(std::move(mListener), mEventMessage, + mTypeAtom, mFlags, mAllEvents); + } +} + +} // namespace mozilla diff --git a/dom/events/EventListenerManager.h b/dom/events/EventListenerManager.h new file mode 100644 index 0000000000..92688f1613 --- /dev/null +++ b/dom/events/EventListenerManager.h @@ -0,0 +1,704 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#ifndef mozilla_EventListenerManager_h_ +#define mozilla_EventListenerManager_h_ + +#include "mozilla/BasicEvents.h" +#include "mozilla/JSEventHandler.h" +#include "mozilla/MemoryReporting.h" +#include "mozilla/dom/AbortFollower.h" +#include "mozilla/dom/EventListenerBinding.h" +#include "nsCOMPtr.h" +#include "nsCycleCollectionParticipant.h" +#include "nsGkAtoms.h" +#include "nsIDOMEventListener.h" +#include "nsTArray.h" +#include "nsTObserverArray.h" + +class nsIEventListenerInfo; +class nsPIDOMWindowInner; +class JSTracer; + +struct EventTypeData; + +namespace mozilla { + +class ELMCreationDetector; +class EventListenerManager; +class ListenerSignalFollower; + +namespace dom { +class Event; +class EventTarget; +class Element; +} // namespace dom + +using EventListenerHolder = + dom::CallbackObjectHolder<dom::EventListener, nsIDOMEventListener>; + +struct EventListenerFlags { + friend class EventListenerManager; + + private: + // If mListenerIsJSListener is true, the listener is implemented by JS. + // Otherwise, it's implemented by native code or JS but it's wrapped. + bool mListenerIsJSListener : 1; + + public: + // If mCapture is true, it means the listener captures the event. Otherwise, + // it's listening at bubbling phase. + bool mCapture : 1; + // If mInSystemGroup is true, the listener is listening to the events in the + // system group. + bool mInSystemGroup : 1; + // If mAllowUntrustedEvents is true, the listener is listening to the + // untrusted events too. + bool mAllowUntrustedEvents : 1; + // If mPassive is true, the listener will not be calling preventDefault on the + // event. (If it does call preventDefault, we should ignore it). + bool mPassive : 1; + // If mOnce is true, the listener will be removed from the manager before it + // is invoked, so that it would only be invoked once. + bool mOnce : 1; + + EventListenerFlags() + : mListenerIsJSListener(false), + mCapture(false), + mInSystemGroup(false), + mAllowUntrustedEvents(false), + mPassive(false), + mOnce(false) {} + + bool EqualsForAddition(const EventListenerFlags& aOther) const { + return (mCapture == aOther.mCapture && + mInSystemGroup == aOther.mInSystemGroup && + mListenerIsJSListener == aOther.mListenerIsJSListener && + mAllowUntrustedEvents == aOther.mAllowUntrustedEvents); + // Don't compare mPassive or mOnce + } + + bool EqualsForRemoval(const EventListenerFlags& aOther) const { + return (mCapture == aOther.mCapture && + mInSystemGroup == aOther.mInSystemGroup && + mListenerIsJSListener == aOther.mListenerIsJSListener); + // Don't compare mAllowUntrustedEvents, mPassive, or mOnce + } +}; + +inline EventListenerFlags TrustedEventsAtBubble() { + EventListenerFlags flags; + return flags; +} + +inline EventListenerFlags TrustedEventsAtCapture() { + EventListenerFlags flags; + flags.mCapture = true; + return flags; +} + +inline EventListenerFlags AllEventsAtBubble() { + EventListenerFlags flags; + flags.mAllowUntrustedEvents = true; + return flags; +} + +inline EventListenerFlags AllEventsAtCapture() { + EventListenerFlags flags; + flags.mCapture = true; + flags.mAllowUntrustedEvents = true; + return flags; +} + +inline EventListenerFlags TrustedEventsAtSystemGroupBubble() { + EventListenerFlags flags; + flags.mInSystemGroup = true; + return flags; +} + +inline EventListenerFlags TrustedEventsAtSystemGroupCapture() { + EventListenerFlags flags; + flags.mCapture = true; + flags.mInSystemGroup = true; + return flags; +} + +inline EventListenerFlags AllEventsAtSystemGroupBubble() { + EventListenerFlags flags; + flags.mInSystemGroup = true; + flags.mAllowUntrustedEvents = true; + return flags; +} + +inline EventListenerFlags AllEventsAtSystemGroupCapture() { + EventListenerFlags flags; + flags.mCapture = true; + flags.mInSystemGroup = true; + flags.mAllowUntrustedEvents = true; + return flags; +} + +class EventListenerManagerBase { + protected: + EventListenerManagerBase(); + + void ClearNoListenersForEvents() { + mNoListenerForEvents[0] = eVoidEvent; + mNoListenerForEvents[1] = eVoidEvent; + mNoListenerForEvents[2] = eVoidEvent; + } + + EventMessage mNoListenerForEvents[3]; + uint16_t mMayHavePaintEventListener : 1; + uint16_t mMayHaveMutationListeners : 1; + uint16_t mMayHaveCapturingListeners : 1; + uint16_t mMayHaveSystemGroupListeners : 1; + uint16_t mMayHaveTouchEventListener : 1; + uint16_t mMayHaveMouseEnterLeaveEventListener : 1; + uint16_t mMayHavePointerEnterLeaveEventListener : 1; + uint16_t mMayHaveSelectionChangeEventListener : 1; + uint16_t mMayHaveFormSelectEventListener : 1; + uint16_t mMayHaveTransitionEventListener : 1; + uint16_t mClearingListeners : 1; + uint16_t mIsMainThreadELM : 1; + uint16_t mMayHaveListenersForUntrustedEvents : 1; + // 3 unused flags. +}; + +/* + * Event listener manager + */ + +class EventListenerManager final : public EventListenerManagerBase { + ~EventListenerManager(); + + public: + struct Listener; + class ListenerSignalFollower : public dom::AbortFollower { + public: + explicit ListenerSignalFollower(EventListenerManager* aListenerManager, + Listener* aListener); + + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_CLASS(ListenerSignalFollower) + + void RunAbortAlgorithm() override; + + void Disconnect() { + mListenerManager = nullptr; + mListener.Reset(); + Unfollow(); + } + + protected: + ~ListenerSignalFollower() = default; + + EventListenerManager* mListenerManager; + EventListenerHolder mListener; + RefPtr<nsAtom> mTypeAtom; + EventMessage mEventMessage; + bool mAllEvents; + EventListenerFlags mFlags; + }; + + struct Listener { + RefPtr<ListenerSignalFollower> mSignalFollower; + EventListenerHolder mListener; + RefPtr<nsAtom> mTypeAtom; + EventMessage mEventMessage; + + enum ListenerType : uint8_t { + // No listener. + eNoListener, + // A generic C++ implementation of nsIDOMEventListener. + eNativeListener, + // An event handler attribute using JSEventHandler. + eJSEventListener, + // A scripted EventListener. + eWebIDLListener, + }; + ListenerType mListenerType; + + bool mListenerIsHandler : 1; + bool mHandlerIsString : 1; + bool mAllEvents : 1; + bool mEnabled : 1; + + EventListenerFlags mFlags; + + JSEventHandler* GetJSEventHandler() const { + return (mListenerType == eJSEventListener) + ? static_cast<JSEventHandler*>(mListener.GetXPCOMCallback()) + : nullptr; + } + + Listener() + : mEventMessage(eVoidEvent), + mListenerType(eNoListener), + mListenerIsHandler(false), + mHandlerIsString(false), + mAllEvents(false), + mEnabled(true) {} + + Listener(Listener&& aOther) + : mSignalFollower(std::move(aOther.mSignalFollower)), + mListener(std::move(aOther.mListener)), + mTypeAtom(std::move(aOther.mTypeAtom)), + mEventMessage(aOther.mEventMessage), + mListenerType(aOther.mListenerType), + mListenerIsHandler(aOther.mListenerIsHandler), + mHandlerIsString(aOther.mHandlerIsString), + mAllEvents(aOther.mAllEvents), + mEnabled(aOther.mEnabled) { + aOther.mEventMessage = eVoidEvent; + aOther.mListenerType = eNoListener; + aOther.mListenerIsHandler = false; + aOther.mHandlerIsString = false; + aOther.mAllEvents = false; + aOther.mEnabled = true; + } + + ~Listener() { + if ((mListenerType == eJSEventListener) && mListener) { + static_cast<JSEventHandler*>(mListener.GetXPCOMCallback()) + ->Disconnect(); + } + if (mSignalFollower) { + mSignalFollower->Disconnect(); + } + } + + MOZ_ALWAYS_INLINE bool MatchesEventMessage( + const WidgetEvent* aEvent, EventMessage aEventMessage) const; + + MOZ_ALWAYS_INLINE bool MatchesEventGroup(const WidgetEvent* aEvent) const { + return mFlags.mInSystemGroup == aEvent->mFlags.mInSystemGroup; + } + + MOZ_ALWAYS_INLINE bool MatchesEventPhase(const WidgetEvent* aEvent) const { + return ((mFlags.mCapture && aEvent->mFlags.mInCapturePhase) || + (!mFlags.mCapture && aEvent->mFlags.mInBubblingPhase)); + } + + // Allow only trusted events, except when listener permits untrusted + // events. + MOZ_ALWAYS_INLINE bool AllowsEventTrustedness( + const WidgetEvent* aEvent) const { + return aEvent->IsTrusted() || mFlags.mAllowUntrustedEvents; + } + }; + + explicit EventListenerManager(dom::EventTarget* aTarget); + + NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(EventListenerManager) + + NS_DECL_CYCLE_COLLECTION_NATIVE_CLASS(EventListenerManager) + + void AddEventListener(const nsAString& aType, nsIDOMEventListener* aListener, + bool aUseCapture, bool aWantsUntrusted) { + AddEventListener(aType, EventListenerHolder(aListener), aUseCapture, + aWantsUntrusted); + } + void AddEventListener(const nsAString& aType, dom::EventListener* aListener, + const dom::AddEventListenerOptionsOrBoolean& aOptions, + bool aWantsUntrusted) { + AddEventListener(aType, EventListenerHolder(aListener), aOptions, + aWantsUntrusted); + } + void RemoveEventListener(const nsAString& aType, + nsIDOMEventListener* aListener, bool aUseCapture) { + RemoveEventListener(aType, EventListenerHolder(aListener), aUseCapture); + } + void RemoveEventListener(const nsAString& aType, + dom::EventListener* aListener, + const dom::EventListenerOptionsOrBoolean& aOptions) { + RemoveEventListener(aType, EventListenerHolder(aListener), aOptions); + } + + void AddListenerForAllEvents(dom::EventListener* aListener, bool aUseCapture, + bool aWantsUntrusted, bool aSystemEventGroup); + void RemoveListenerForAllEvents(dom::EventListener* aListener, + bool aUseCapture, bool aSystemEventGroup); + + /** + * Sets events listeners of all types. + * @param an event listener + */ + void AddEventListenerByType(nsIDOMEventListener* aListener, + const nsAString& type, + const EventListenerFlags& aFlags) { + AddEventListenerByType(EventListenerHolder(aListener), type, aFlags); + } + void AddEventListenerByType(dom::EventListener* aListener, + const nsAString& type, + const EventListenerFlags& aFlags) { + AddEventListenerByType(EventListenerHolder(aListener), type, aFlags); + } + void AddEventListenerByType( + EventListenerHolder aListener, const nsAString& type, + const EventListenerFlags& aFlags, + const dom::Optional<bool>& aPassive = dom::Optional<bool>(), + dom::AbortSignal* aSignal = nullptr); + void RemoveEventListenerByType(nsIDOMEventListener* aListener, + const nsAString& type, + const EventListenerFlags& aFlags) { + RemoveEventListenerByType(EventListenerHolder(aListener), type, aFlags); + } + void RemoveEventListenerByType(dom::EventListener* aListener, + const nsAString& type, + const EventListenerFlags& aFlags) { + RemoveEventListenerByType(EventListenerHolder(aListener), type, aFlags); + } + void RemoveEventListenerByType(EventListenerHolder aListener, + const nsAString& type, + const EventListenerFlags& aFlags); + + /** + * Sets the current "inline" event listener for aName to be a + * function compiled from aFunc if !aDeferCompilation. If + * aDeferCompilation, then we assume that we can get the string from + * mTarget later and compile lazily. + * + * aElement, if not null, is the element the string is associated with. + */ + // XXXbz does that play correctly with nodes being adopted across + // documents? Need to double-check the spec here. + nsresult SetEventHandler(nsAtom* aName, const nsAString& aFunc, + bool aDeferCompilation, bool aPermitUntrustedEvents, + dom::Element* aElement); + /** + * Remove the current "inline" event listener for aName. + */ + void RemoveEventHandler(nsAtom* aName); + + // We only get called from the event dispatch code, which knows to be careful + // with what it's doing. We could annotate ourselves as MOZ_CAN_RUN_SCRIPT, + // but then the event dispatch code would need a ton of MOZ_KnownLive for + // things that come from slightly complicated stack-lifetime data structures. + MOZ_CAN_RUN_SCRIPT_BOUNDARY + void HandleEvent(nsPresContext* aPresContext, WidgetEvent* aEvent, + dom::Event** aDOMEvent, dom::EventTarget* aCurrentTarget, + nsEventStatus* aEventStatus, bool aItemInShadowTree) { + if (!mMayHaveCapturingListeners && !aEvent->mFlags.mInBubblingPhase) { + return; + } + + if (!mMayHaveSystemGroupListeners && aEvent->mFlags.mInSystemGroup) { + return; + } + + if (!aEvent->IsTrusted() && !mMayHaveListenersForUntrustedEvents) { + return; + } + + // Check if we already know that there is no event listener for the event. + if (aEvent->mMessage == eUnidentifiedEvent) { + if (mNoListenerForEventAtom == aEvent->mSpecifiedEventType) { + return; + } + } else if (mNoListenerForEvents[0] == aEvent->mMessage || + mNoListenerForEvents[1] == aEvent->mMessage || + mNoListenerForEvents[2] == aEvent->mMessage) { + return; + } + + if (mListeners.IsEmpty() || aEvent->PropagationStopped()) { + return; + } + + HandleEventInternal(aPresContext, aEvent, aDOMEvent, aCurrentTarget, + aEventStatus, aItemInShadowTree); + } + + /** + * Tells the event listener manager that its target (which owns it) is + * no longer using it (and could go away). + */ + void Disconnect(); + + /** + * Allows us to quickly determine if we have mutation listeners registered. + */ + bool HasMutationListeners(); + + /** + * Allows us to quickly determine whether we have unload listeners registered. + */ + bool HasUnloadListeners(); + + /** + * Allows us to quickly determine whether we have beforeunload listeners + * registered. + */ + bool HasBeforeUnloadListeners(); + + /** + * Returns the mutation bits depending on which mutation listeners are + * registered to this listener manager. + * @note If a listener is an nsIDOMMutationListener, all possible mutation + * event bits are returned. All bits are also returned if one of the + * event listeners is registered to handle DOMSubtreeModified events. + */ + uint32_t MutationListenerBits(); + + /** + * Returns true if there is at least one event listener for aEventName. + */ + bool HasListenersFor(const nsAString& aEventName) const; + + /** + * Returns true if there is at least one event listener for aEventNameWithOn. + * Note that aEventNameWithOn must start with "on"! + */ + bool HasListenersFor(nsAtom* aEventNameWithOn) const; + + /** + * Similar to HasListenersFor, but ignores system group listeners. + */ + bool HasNonSystemGroupListenersFor(nsAtom* aEventNameWithOn) const; + + /** + * Returns true if there is at least one event listener. + */ + bool HasListeners() const; + + /** + * Sets aList to the list of nsIEventListenerInfo objects representing the + * listeners managed by this listener manager. + */ + nsresult GetListenerInfo(nsTArray<RefPtr<nsIEventListenerInfo>>& aList); + + nsresult IsListenerEnabled(nsAString& aType, JSObject* aListener, + bool aCapturing, bool aAllowsUntrusted, + bool aInSystemEventGroup, bool aIsHandler, + bool* aEnabled); + + nsresult SetListenerEnabled(nsAString& aType, JSObject* aListener, + bool aCapturing, bool aAllowsUntrusted, + bool aInSystemEventGroup, bool aIsHandler, + bool aEnabled); + + uint32_t GetIdentifierForEvent(nsAtom* aEvent); + + /** + * Returns true if there may be a paint event listener registered, + * false if there definitely isn't. + */ + bool MayHavePaintEventListener() const { return mMayHavePaintEventListener; } + + /** + * Returns true if there may be a touch event listener registered, + * false if there definitely isn't. + */ + bool MayHaveTouchEventListener() const { return mMayHaveTouchEventListener; } + + bool MayHaveMouseEnterLeaveEventListener() const { + return mMayHaveMouseEnterLeaveEventListener; + } + bool MayHavePointerEnterLeaveEventListener() const { + return mMayHavePointerEnterLeaveEventListener; + } + bool MayHaveSelectionChangeEventListener() const { + return mMayHaveSelectionChangeEventListener; + } + bool MayHaveFormSelectEventListener() const { + return mMayHaveFormSelectEventListener; + } + bool MayHaveTransitionEventListener() { + return mMayHaveTransitionEventListener; + } + + size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const; + + uint32_t ListenerCount() const { return mListeners.Length(); } + + void MarkForCC(); + + void TraceListeners(JSTracer* aTrc); + + dom::EventTarget* GetTarget() { return mTarget; } + + bool HasNonSystemGroupListenersForUntrustedKeyEvents(); + bool HasNonPassiveNonSystemGroupListenersForUntrustedKeyEvents(); + + bool HasApzAwareListeners(); + bool IsApzAwareListener(Listener* aListener); + bool IsApzAwareEvent(nsAtom* aEvent); + + bool HasNonPassiveWheelListener(); + + /** + * Remove all event listeners from the event target this EventListenerManager + * is for. + */ + void RemoveAllListeners(); + + protected: + MOZ_CAN_RUN_SCRIPT + void HandleEventInternal(nsPresContext* aPresContext, WidgetEvent* aEvent, + dom::Event** aDOMEvent, + dom::EventTarget* aCurrentTarget, + nsEventStatus* aEventStatus, bool aItemInShadowTree); + + MOZ_CAN_RUN_SCRIPT + nsresult HandleEventSubType(Listener* aListener, dom::Event* aDOMEvent, + dom::EventTarget* aCurrentTarget); + + /** + * If the given EventMessage has a legacy version that we support, then this + * function returns that legacy version. Otherwise, this function simply + * returns the passed-in EventMessage. + */ + static EventMessage GetLegacyEventMessage(EventMessage aEventMessage); + + /** + * Get the event message for the given event name. + */ + EventMessage GetEventMessage(nsAtom* aEventName) const; + + /** + * Get the event message and atom for the given event type. + */ + EventMessage GetEventMessageAndAtomForListener(const nsAString& aType, + nsAtom** aAtom); + + void ProcessApzAwareEventListenerAdd(); + + /** + * Compile the "inline" event listener for aListener. The + * body of the listener can be provided in aBody; if this is null we + * will look for it on mTarget. If aBody is provided, aElement should be + * as well; otherwise it will also be inferred from mTarget. + */ + nsresult CompileEventHandlerInternal(Listener* aListener, + const nsAString* aBody, + dom::Element* aElement); + + /** + * Find the Listener for the "inline" event listener for aTypeAtom. + */ + Listener* FindEventHandler(EventMessage aEventMessage, nsAtom* aTypeAtom); + + /** + * Set the "inline" event listener for aName to aHandler. aHandler may be + * have no actual handler set to indicate that we should lazily get and + * compile the string for this listener, but in that case aContext and + * aScopeGlobal must be non-null. Otherwise, aContext and aScopeGlobal are + * allowed to be null. + */ + Listener* SetEventHandlerInternal(nsAtom* aName, + const TypedEventHandler& aHandler, + bool aPermitUntrustedEvents); + + bool IsDeviceType(EventMessage aEventMessage); + void EnableDevice(EventMessage aEventMessage); + void DisableDevice(EventMessage aEventMessage); + + bool HasListenersForInternal(nsAtom* aEventNameWithOn, + bool aIgnoreSystemGroup) const; + + Listener* GetListenerFor(nsAString& aType, JSObject* aListener, + bool aCapturing, bool aAllowsUntrusted, + bool aInSystemEventGroup, bool aIsHandler); + + public: + /** + * Set the "inline" event listener for aEventName to aHandler. If + * aHandler is null, this will actually remove the event listener + */ + void SetEventHandler(nsAtom* aEventName, dom::EventHandlerNonNull* aHandler); + void SetEventHandler(dom::OnErrorEventHandlerNonNull* aHandler); + void SetEventHandler(dom::OnBeforeUnloadEventHandlerNonNull* aHandler); + + /** + * Get the value of the "inline" event listener for aEventName. + * This may cause lazy compilation if the listener is uncompiled. + * + * Note: It's the caller's responsibility to make sure to call the right one + * of these methods. In particular, "onerror" events use + * OnErrorEventHandlerNonNull for some event targets and EventHandlerNonNull + * for others. + */ + dom::EventHandlerNonNull* GetEventHandler(nsAtom* aEventName) { + const TypedEventHandler* typedHandler = GetTypedEventHandler(aEventName); + return typedHandler ? typedHandler->NormalEventHandler() : nullptr; + } + + dom::OnErrorEventHandlerNonNull* GetOnErrorEventHandler() { + const TypedEventHandler* typedHandler = + GetTypedEventHandler(nsGkAtoms::onerror); + return typedHandler ? typedHandler->OnErrorEventHandler() : nullptr; + } + + dom::OnBeforeUnloadEventHandlerNonNull* GetOnBeforeUnloadEventHandler() { + const TypedEventHandler* typedHandler = + GetTypedEventHandler(nsGkAtoms::onbeforeunload); + return typedHandler ? typedHandler->OnBeforeUnloadEventHandler() : nullptr; + } + + private: + already_AddRefed<nsPIDOMWindowInner> WindowFromListener( + Listener* aListener, bool aItemInShadowTree); + + protected: + /** + * Helper method for implementing the various Get*EventHandler above. Will + * return null if we don't have an event handler for this event name. + */ + const TypedEventHandler* GetTypedEventHandler(nsAtom* aEventName); + + void AddEventListener(const nsAString& aType, EventListenerHolder aListener, + const dom::AddEventListenerOptionsOrBoolean& aOptions, + bool aWantsUntrusted); + void AddEventListener(const nsAString& aType, EventListenerHolder aListener, + bool aUseCapture, bool aWantsUntrusted); + void RemoveEventListener(const nsAString& aType, + EventListenerHolder aListener, + const dom::EventListenerOptionsOrBoolean& aOptions); + void RemoveEventListener(const nsAString& aType, + EventListenerHolder aListener, bool aUseCapture); + + void AddEventListenerInternal(EventListenerHolder aListener, + EventMessage aEventMessage, nsAtom* aTypeAtom, + const EventListenerFlags& aFlags, + bool aHandler = false, bool aAllEvents = false, + dom::AbortSignal* aSignal = nullptr); + void RemoveEventListenerInternal(EventListenerHolder aListener, + EventMessage aEventMessage, + nsAtom* aUserType, + const EventListenerFlags& aFlags, + bool aAllEvents = false); + void RemoveAllListenersSilently(); + void NotifyEventListenerRemoved(nsAtom* aUserType); + const EventTypeData* GetTypeDataForIID(const nsIID& aIID); + const EventTypeData* GetTypeDataForEventName(nsAtom* aName); + nsPIDOMWindowInner* GetInnerWindowForTarget(); + already_AddRefed<nsPIDOMWindowInner> GetTargetAsInnerWindow() const; + + bool ListenerCanHandle(const Listener* aListener, const WidgetEvent* aEvent, + EventMessage aEventMessage) const; + + // BE AWARE, a lot of instances of EventListenerManager will be created. + // Therefor, we need to keep this class compact. When you add integer + // members, please add them to EventListemerManagerBase and check the size + // at build time. + + already_AddRefed<nsIScriptGlobalObject> GetScriptGlobalAndDocument( + mozilla::dom::Document** aDoc); + + void MaybeMarkPassive(EventMessage aMessage, EventListenerFlags& aFlags); + + nsAutoTObserverArray<Listener, 2> mListeners; + dom::EventTarget* MOZ_NON_OWNING_REF mTarget; + RefPtr<nsAtom> mNoListenerForEventAtom; + + friend class ELMCreationDetector; + static uint32_t sMainThreadCreatedCount; +}; + +} // namespace mozilla + +#endif // mozilla_EventListenerManager_h_ diff --git a/dom/events/EventListenerService.cpp b/dom/events/EventListenerService.cpp new file mode 100644 index 0000000000..4336476aa9 --- /dev/null +++ b/dom/events/EventListenerService.cpp @@ -0,0 +1,409 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#include "EventListenerService.h" +#include "mozilla/BasicEvents.h" +#include "mozilla/EventDispatcher.h" +#include "mozilla/EventListenerManager.h" +#include "mozilla/HoldDropJSObjects.h" +#include "mozilla/JSEventHandler.h" +#include "mozilla/Maybe.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/EventListenerBinding.h" +#include "mozilla/dom/ScriptSettings.h" +#include "nsArrayUtils.h" +#include "nsCOMArray.h" +#include "nsINode.h" +#include "nsJSUtils.h" +#include "nsServiceManagerUtils.h" +#include "nsArray.h" +#include "nsThreadUtils.h" + +namespace mozilla { + +using namespace dom; + +/****************************************************************************** + * mozilla::EventListenerChange + ******************************************************************************/ + +NS_IMPL_ISUPPORTS(EventListenerChange, nsIEventListenerChange) + +EventListenerChange::~EventListenerChange() = default; + +EventListenerChange::EventListenerChange(EventTarget* aTarget) + : mTarget(aTarget) {} + +void EventListenerChange::AddChangedListenerName(nsAtom* aEventName) { + mChangedListenerNames.AppendElement(aEventName); +} + +NS_IMETHODIMP +EventListenerChange::GetTarget(EventTarget** aTarget) { + NS_ENSURE_ARG_POINTER(aTarget); + NS_ADDREF(*aTarget = mTarget); + return NS_OK; +} + +NS_IMETHODIMP +EventListenerChange::GetCountOfEventListenerChangesAffectingAccessibility( + uint32_t* aCount) { + *aCount = 0; + + size_t length = mChangedListenerNames.Length(); + for (size_t i = 0; i < length; i++) { + RefPtr<nsAtom> listenerName = mChangedListenerNames[i]; + + // These are the event listener changes which may make an element + // accessible or inaccessible. + if (listenerName == nsGkAtoms::onclick || + listenerName == nsGkAtoms::onmousedown || + listenerName == nsGkAtoms::onmouseup) { + *aCount += 1; + } + } + + return NS_OK; +} + +/****************************************************************************** + * mozilla::EventListenerInfo + ******************************************************************************/ + +EventListenerInfo::EventListenerInfo( + EventListenerManager* aListenerManager, const nsAString& aType, + JS::Handle<JSObject*> aScriptedListener, + JS::Handle<JSObject*> aScriptedListenerGlobal, bool aCapturing, + bool aAllowsUntrusted, bool aInSystemEventGroup, bool aIsHandler) + : mListenerManager(aListenerManager), + mType(aType), + mScriptedListener(aScriptedListener), + mScriptedListenerGlobal(aScriptedListenerGlobal), + mCapturing(aCapturing), + mAllowsUntrusted(aAllowsUntrusted), + mInSystemEventGroup(aInSystemEventGroup), + mIsHandler(aIsHandler) { + if (aScriptedListener) { + MOZ_ASSERT(JS_IsGlobalObject(aScriptedListenerGlobal)); + js::AssertSameCompartment(aScriptedListener, aScriptedListenerGlobal); + } + + HoldJSObjects(this); +} + +EventListenerInfo::~EventListenerInfo() { DropJSObjects(this); } + +NS_IMPL_CYCLE_COLLECTION_WITH_JS_MEMBERS(EventListenerInfo, (mListenerManager), + (mScriptedListener, + mScriptedListenerGlobal)) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(EventListenerInfo) + NS_INTERFACE_MAP_ENTRY(nsIEventListenerInfo) + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTING_ADDREF(EventListenerInfo) +NS_IMPL_CYCLE_COLLECTING_RELEASE(EventListenerInfo) + +NS_IMETHODIMP +EventListenerInfo::GetType(nsAString& aType) { + aType = mType; + return NS_OK; +} + +NS_IMETHODIMP +EventListenerInfo::GetCapturing(bool* aCapturing) { + *aCapturing = mCapturing; + return NS_OK; +} + +NS_IMETHODIMP +EventListenerInfo::GetAllowsUntrusted(bool* aAllowsUntrusted) { + *aAllowsUntrusted = mAllowsUntrusted; + return NS_OK; +} + +NS_IMETHODIMP +EventListenerInfo::GetInSystemEventGroup(bool* aInSystemEventGroup) { + *aInSystemEventGroup = mInSystemEventGroup; + return NS_OK; +} + +NS_IMETHODIMP +EventListenerInfo::GetEnabled(bool* aEnabled) { + NS_ENSURE_STATE(mListenerManager); + return mListenerManager->IsListenerEnabled( + mType, mScriptedListener, mCapturing, mAllowsUntrusted, + mInSystemEventGroup, mIsHandler, aEnabled); +} + +NS_IMETHODIMP +EventListenerInfo::SetEnabled(bool aEnabled) { + NS_ENSURE_STATE(mListenerManager); + return mListenerManager->SetListenerEnabled( + mType, mScriptedListener, mCapturing, mAllowsUntrusted, + mInSystemEventGroup, mIsHandler, aEnabled); +} + +NS_IMETHODIMP +EventListenerInfo::GetListenerObject(JSContext* aCx, + JS::MutableHandle<JS::Value> aObject) { + Maybe<JSAutoRealm> ar; + GetJSVal(aCx, ar, aObject); + return NS_OK; +} + +/****************************************************************************** + * mozilla::EventListenerService + ******************************************************************************/ + +NS_IMPL_ISUPPORTS(EventListenerService, nsIEventListenerService) + +bool EventListenerInfo::GetJSVal(JSContext* aCx, Maybe<JSAutoRealm>& aAr, + JS::MutableHandle<JS::Value> aJSVal) { + if (mScriptedListener) { + aJSVal.setObject(*mScriptedListener); + aAr.emplace(aCx, mScriptedListenerGlobal); + return true; + } + + aJSVal.setNull(); + return false; +} + +NS_IMETHODIMP +EventListenerInfo::ToSource(nsAString& aResult) { + aResult.SetIsVoid(true); + + AutoSafeJSContext cx; + Maybe<JSAutoRealm> ar; + JS::Rooted<JS::Value> v(cx); + if (GetJSVal(cx, ar, &v)) { + JSString* str = JS_ValueToSource(cx, v); + if (str) { + nsAutoJSString autoStr; + if (autoStr.init(cx, str)) { + aResult.Assign(autoStr); + } + } + } + return NS_OK; +} + +EventListenerService* EventListenerService::sInstance = nullptr; + +EventListenerService::EventListenerService() { + MOZ_ASSERT(!sInstance); + sInstance = this; +} + +EventListenerService::~EventListenerService() { + MOZ_ASSERT(sInstance == this); + sInstance = nullptr; +} + +NS_IMETHODIMP +EventListenerService::GetListenerInfoFor( + EventTarget* aEventTarget, + nsTArray<RefPtr<nsIEventListenerInfo>>& aOutArray) { + NS_ENSURE_ARG_POINTER(aEventTarget); + + EventListenerManager* elm = aEventTarget->GetExistingListenerManager(); + if (elm) { + elm->GetListenerInfo(aOutArray); + } + + return NS_OK; +} + +NS_IMETHODIMP +EventListenerService::GetEventTargetChainFor( + EventTarget* aEventTarget, bool aComposed, + nsTArray<RefPtr<EventTarget>>& aOutArray) { + NS_ENSURE_ARG(aEventTarget); + WidgetEvent event(true, eVoidEvent); + event.SetComposed(aComposed); + nsTArray<EventTarget*> targets; + nsresult rv = EventDispatcher::Dispatch(aEventTarget, nullptr, &event, + nullptr, nullptr, nullptr, &targets); + NS_ENSURE_SUCCESS(rv, rv); + aOutArray.AppendElements(targets); + return NS_OK; +} + +NS_IMETHODIMP +EventListenerService::HasListenersFor(EventTarget* aEventTarget, + const nsAString& aType, bool* aRetVal) { + NS_ENSURE_TRUE(aEventTarget, NS_ERROR_UNEXPECTED); + + EventListenerManager* elm = aEventTarget->GetExistingListenerManager(); + *aRetVal = elm && elm->HasListenersFor(aType); + return NS_OK; +} + +static already_AddRefed<EventListener> ToEventListener( + JSContext* aCx, JS::Handle<JS::Value> aValue) { + if (NS_WARN_IF(!aValue.isObject())) { + return nullptr; + } + + JS::Rooted<JSObject*> obj(aCx, &aValue.toObject()); + JS::Rooted<JSObject*> global(aCx, JS::CurrentGlobalOrNull(aCx)); + RefPtr<EventListener> listener = + new EventListener(aCx, obj, global, GetIncumbentGlobal()); + return listener.forget(); +} + +NS_IMETHODIMP +EventListenerService::AddSystemEventListener(EventTarget* aTarget, + const nsAString& aType, + JS::Handle<JS::Value> aListener, + bool aUseCapture, JSContext* aCx) { + MOZ_ASSERT(aTarget, "Missing target"); + + NS_ENSURE_TRUE(aTarget, NS_ERROR_UNEXPECTED); + + RefPtr<EventListener> listener = ToEventListener(aCx, aListener); + if (!listener) { + return NS_ERROR_UNEXPECTED; + } + + EventListenerManager* manager = aTarget->GetOrCreateListenerManager(); + NS_ENSURE_STATE(manager); + + EventListenerFlags flags = aUseCapture ? TrustedEventsAtSystemGroupCapture() + : TrustedEventsAtSystemGroupBubble(); + manager->AddEventListenerByType(listener, aType, flags); + return NS_OK; +} + +NS_IMETHODIMP +EventListenerService::RemoveSystemEventListener(EventTarget* aTarget, + const nsAString& aType, + JS::Handle<JS::Value> aListener, + bool aUseCapture, + JSContext* aCx) { + MOZ_ASSERT(aTarget, "Missing target"); + + NS_ENSURE_TRUE(aTarget, NS_ERROR_UNEXPECTED); + + RefPtr<EventListener> listener = ToEventListener(aCx, aListener); + if (!listener) { + return NS_ERROR_UNEXPECTED; + } + + EventListenerManager* manager = aTarget->GetExistingListenerManager(); + if (manager) { + EventListenerFlags flags = aUseCapture ? TrustedEventsAtSystemGroupCapture() + : TrustedEventsAtSystemGroupBubble(); + manager->RemoveEventListenerByType(listener, aType, flags); + } + + return NS_OK; +} + +NS_IMETHODIMP +EventListenerService::AddListenerForAllEvents( + EventTarget* aTarget, JS::Handle<JS::Value> aListener, bool aUseCapture, + bool aWantsUntrusted, bool aSystemEventGroup, JSContext* aCx) { + NS_ENSURE_STATE(aTarget); + + RefPtr<EventListener> listener = ToEventListener(aCx, aListener); + if (!listener) { + return NS_ERROR_UNEXPECTED; + } + + EventListenerManager* manager = aTarget->GetOrCreateListenerManager(); + NS_ENSURE_STATE(manager); + manager->AddListenerForAllEvents(listener, aUseCapture, aWantsUntrusted, + aSystemEventGroup); + return NS_OK; +} + +NS_IMETHODIMP +EventListenerService::RemoveListenerForAllEvents( + EventTarget* aTarget, JS::Handle<JS::Value> aListener, bool aUseCapture, + bool aSystemEventGroup, JSContext* aCx) { + NS_ENSURE_STATE(aTarget); + + RefPtr<EventListener> listener = ToEventListener(aCx, aListener); + if (!listener) { + return NS_ERROR_UNEXPECTED; + } + + EventListenerManager* manager = aTarget->GetExistingListenerManager(); + if (manager) { + manager->RemoveListenerForAllEvents(listener, aUseCapture, + aSystemEventGroup); + } + return NS_OK; +} + +NS_IMETHODIMP +EventListenerService::AddListenerChangeListener( + nsIListenerChangeListener* aListener) { + if (!mChangeListeners.Contains(aListener)) { + mChangeListeners.AppendElement(aListener); + } + return NS_OK; +}; + +NS_IMETHODIMP +EventListenerService::RemoveListenerChangeListener( + nsIListenerChangeListener* aListener) { + mChangeListeners.RemoveElement(aListener); + return NS_OK; +}; + +void EventListenerService::NotifyAboutMainThreadListenerChangeInternal( + dom::EventTarget* aTarget, nsAtom* aName) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aTarget); + if (mChangeListeners.IsEmpty()) { + return; + } + + if (!mPendingListenerChanges) { + mPendingListenerChanges = nsArrayBase::Create(); + nsCOMPtr<nsIRunnable> runnable = + NewRunnableMethod("EventListenerService::NotifyPendingChanges", this, + &EventListenerService::NotifyPendingChanges); + if (nsCOMPtr<nsIGlobalObject> global = aTarget->GetOwnerGlobal()) { + global->Dispatch(TaskCategory::Other, runnable.forget()); + } else if (nsINode* node = nsINode::FromEventTarget(aTarget)) { + node->OwnerDoc()->Dispatch(TaskCategory::Other, runnable.forget()); + } else { + NS_DispatchToCurrentThread(runnable); + } + } + + RefPtr<EventListenerChange> changes = + mPendingListenerChangesSet.LookupOrInsertWith(aTarget, [&] { + auto c = MakeRefPtr<EventListenerChange>(aTarget); + mPendingListenerChanges->AppendElement(c); + return c; + }); + changes->AddChangedListenerName(aName); +} + +void EventListenerService::NotifyPendingChanges() { + nsCOMPtr<nsIMutableArray> changes; + mPendingListenerChanges.swap(changes); + mPendingListenerChangesSet.Clear(); + + for (nsCOMPtr<nsIListenerChangeListener> listener : + mChangeListeners.EndLimitedRange()) { + listener->ListenersChanged(changes); + } +} + +} // namespace mozilla + +nsresult NS_NewEventListenerService(nsIEventListenerService** aResult) { + *aResult = new mozilla::EventListenerService(); + NS_ADDREF(*aResult); + return NS_OK; +} diff --git a/dom/events/EventListenerService.h b/dom/events/EventListenerService.h new file mode 100644 index 0000000000..8b0b95f130 --- /dev/null +++ b/dom/events/EventListenerService.h @@ -0,0 +1,108 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#ifndef mozilla_EventListenerService_h_ +#define mozilla_EventListenerService_h_ + +#include "jsapi.h" +#include "mozilla/Attributes.h" +#include "nsCycleCollectionParticipant.h" +#include "EventListenerManager.h" +#include "nsIEventListenerService.h" +#include "nsString.h" +#include "nsTObserverArray.h" +#include "nsTHashMap.h" +#include "nsGkAtoms.h" + +class nsIMutableArray; + +namespace mozilla { +namespace dom { +class EventTarget; +} // namespace dom + +template <typename T> +class Maybe; + +class EventListenerChange final : public nsIEventListenerChange { + public: + explicit EventListenerChange(dom::EventTarget* aTarget); + + void AddChangedListenerName(nsAtom* aEventName); + + NS_DECL_ISUPPORTS + NS_DECL_NSIEVENTLISTENERCHANGE + + protected: + virtual ~EventListenerChange(); + nsCOMPtr<dom::EventTarget> mTarget; + nsTArray<RefPtr<nsAtom>> mChangedListenerNames; +}; + +class EventListenerInfo final : public nsIEventListenerInfo { + public: + EventListenerInfo(EventListenerManager* aListenerManager, + const nsAString& aType, + JS::Handle<JSObject*> aScriptedListener, + JS::Handle<JSObject*> aScriptedListenerGlobal, + bool aCapturing, bool aAllowsUntrusted, + bool aInSystemEventGroup, bool aIsHandler); + + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(EventListenerInfo) + NS_DECL_NSIEVENTLISTENERINFO + + protected: + virtual ~EventListenerInfo(); + + bool GetJSVal(JSContext* aCx, Maybe<JSAutoRealm>& aAr, + JS::MutableHandle<JS::Value> aJSVal); + + RefPtr<EventListenerManager> mListenerManager; + nsString mType; + JS::Heap<JSObject*> mScriptedListener; // May be null. + // mScriptedListener may be a cross-compartment wrapper so we cannot use it + // with JSAutoRealm because CCWs are not associated with a single realm. We + // use this global instead (must be same-compartment with mScriptedListener + // and must be non-null if mScriptedListener is non-null). + JS::Heap<JSObject*> mScriptedListenerGlobal; + bool mCapturing; + bool mAllowsUntrusted; + bool mInSystemEventGroup; + bool mIsHandler; +}; + +class EventListenerService final : public nsIEventListenerService { + ~EventListenerService(); + + public: + EventListenerService(); + NS_DECL_ISUPPORTS + NS_DECL_NSIEVENTLISTENERSERVICE + + static void NotifyAboutMainThreadListenerChange(dom::EventTarget* aTarget, + nsAtom* aName) { + if (sInstance) { + sInstance->NotifyAboutMainThreadListenerChangeInternal(aTarget, aName); + } + } + + void NotifyPendingChanges(); + + private: + void NotifyAboutMainThreadListenerChangeInternal(dom::EventTarget* aTarget, + nsAtom* aName); + nsTObserverArray<nsCOMPtr<nsIListenerChangeListener>> mChangeListeners; + nsCOMPtr<nsIMutableArray> mPendingListenerChanges; + nsTHashMap<nsISupportsHashKey, RefPtr<EventListenerChange>> + mPendingListenerChangesSet; + + static EventListenerService* sInstance; +}; + +} // namespace mozilla + +#endif // mozilla_EventListenerService_h_ diff --git a/dom/events/EventNameList.h b/dom/events/EventNameList.h new file mode 100644 index 0000000000..109a9ec395 --- /dev/null +++ b/dom/events/EventNameList.h @@ -0,0 +1,594 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +/* + * This file contains the list of event names that are exposed via IDL + * on various objects. It is designed to be used as inline input to + * various consumers through the magic of C preprocessing. + * + * Each entry consists of 4 pieces of information: + * 1) The name of the event + * 2) The event message + * 3) The event type (see the EventNameType enum in nsContentUtils.h) + * 4) The event struct type for this event. + * Items 2-4 might be empty strings for events for which they don't make sense. + * + * Event names that are exposed as content attributes on HTML elements + * and as IDL attributes on Elements, Documents and Windows and have + * no forwarding behavior should be enclosed in the EVENT macro. + * + * Event names that are exposed as content attributes on HTML elements + * and as IDL attributes on Elements, Documents and Windows and are + * forwarded from <body> and <frameset> to the Window should be + * enclosed in the FORWARDED_EVENT macro. If this macro is not + * defined, it will be defined to be equivalent to EVENT. + * + * Event names that are exposed as IDL attributes on Windows only + * should be enclosed in the WINDOW_ONLY_EVENT macro. If this macro + * is not defined, it will be defined to the empty string. + * + * Event names that are exposed as content and IDL attributes on + * <body> and <frameset>, which forward them to the Window, and are + * exposed as IDL attributes on the Window should be enclosed in the + * WINDOW_EVENT macro. If this macro is not defined, it will be + * defined to be equivalent to WINDOW_ONLY_EVENT. + * + * Touch-specific event names should be enclosed in TOUCH_EVENT. They + * are otherwise equivalent to those enclosed in EVENT. If + * TOUCH_EVENT is not defined, it will be defined to the empty string. + * + * Event names that are only exposed as IDL attributes on Documents + * should be enclosed in the DOCUMENT_ONLY_EVENT macro. If this macro is + * not defined, it will be defined to the empty string. + * + * Event names that are not exposed as IDL attributes at all should be + * enclosed in NON_IDL_EVENT. If NON_IDL_EVENT is not defined, it + * will be defined to the empty string. + * + * If you change which macros event names are enclosed in, please + * update the tests for bug 689564 and bug 659350 as needed. + */ + +#ifdef MESSAGE_TO_EVENT +# ifdef EVENT +# error "Don't define EVENT" +# endif /* EVENT */ +# ifdef WINDOW_ONLY_EVENT +# error "Don't define WINDOW_ONLY_EVENT" +# endif /* WINDOW_ONLY_EVENT */ +# ifdef TOUCH_EVENT +# error "Don't define TOUCH_EVENT" +# endif /* TOUCH_EVENT */ +# ifdef DOCUMENT_ONLY_EVENT +# error "Don't define DOCUMENT_ONLY_EVENT" +# endif /* DOCUMENT_ONLY_EVENT */ +# ifdef NON_IDL_EVENT +# error "Don't define NON_IDL_EVENT" +# endif /* NON_IDL_EVENT */ + +# define EVENT MESSAGE_TO_EVENT +# define WINDOW_ONLY_EVENT MESSAGE_TO_EVENT +# define TOUCH_EVENT MESSAGE_TO_EVENT +# define DOCUMENT_ONLY_EVENT MESSAGE_TO_EVENT +# define NON_IDL_EVENT MESSAGE_TO_EVENT +#endif /* MESSAGE_TO_EVENT */ + +#ifdef DEFINED_FORWARDED_EVENT +# error "Don't define DEFINED_FORWARDED_EVENT" +#endif /* DEFINED_FORWARDED_EVENT */ + +#ifndef FORWARDED_EVENT +# define FORWARDED_EVENT EVENT +# define DEFINED_FORWARDED_EVENT +#endif /* FORWARDED_EVENT */ + +#ifdef DEFINED_WINDOW_ONLY_EVENT +# error "Don't define DEFINED_WINDOW_ONLY_EVENT" +#endif /* DEFINED_WINDOW_ONLY_EVENT */ + +#ifndef WINDOW_ONLY_EVENT +# define WINDOW_ONLY_EVENT(_name, _message, _type, _struct) +# define DEFINED_WINDOW_ONLY_EVENT +#endif /* WINDOW_ONLY_EVENT */ + +#ifdef DEFINED_WINDOW_EVENT +# error "Don't define DEFINED_WINDOW_EVENT" +#endif /* DEFINED_WINDOW_EVENT */ + +#ifndef WINDOW_EVENT +# define WINDOW_EVENT WINDOW_ONLY_EVENT +# define DEFINED_WINDOW_EVENT +#endif /* WINDOW_EVENT */ + +#ifdef DEFINED_TOUCH_EVENT +# error "Don't define DEFINED_TOUCH_EVENT" +#endif /* DEFINED_TOUCH_EVENT */ + +#ifndef TOUCH_EVENT +# define TOUCH_EVENT(_name, _message, _type, _struct) +# define DEFINED_TOUCH_EVENT +#endif /* TOUCH_EVENT */ + +#ifdef DEFINED_DOCUMENT_ONLY_EVENT +# error "Don't define DEFINED_DOCUMENT_ONLY_EVENT" +#endif /* DEFINED_DOCUMENT_ONLY_EVENT */ + +#ifndef DOCUMENT_ONLY_EVENT +# define DOCUMENT_ONLY_EVENT(_name, _message, _type, _struct) +# define DEFINED_DOCUMENT_ONLY_EVENT +#endif /* DOCUMENT_ONLY_EVENT */ + +#ifdef DEFINED_NON_IDL_EVENT +# error "Don't define DEFINED_NON_IDL_EVENT" +#endif /* DEFINED_NON_IDL_EVENT */ + +#ifndef NON_IDL_EVENT +# define NON_IDL_EVENT(_name, _message, _type, _struct) +# define DEFINED_NON_IDL_EVENT +#endif /* NON_IDL_EVENT */ + +#ifdef DEFINED_ERROR_EVENT +# error "Don't define DEFINED_ERROR_EVENT" +#endif /* DEFINED_ERROR_EVENT */ + +#ifndef ERROR_EVENT +# define ERROR_EVENT FORWARDED_EVENT +# define DEFINED_ERROR_EVENT +#endif /* ERROR_EVENT */ + +#ifdef DEFINED_BEFOREUNLOAD_EVENT +# error "Don't define DEFINED_BEFOREUNLOAD_EVENT" +#endif /* DEFINED_BEFOREUNLOAD_EVENT */ + +#ifndef BEFOREUNLOAD_EVENT +# define BEFOREUNLOAD_EVENT WINDOW_EVENT +# define DEFINED_BEFOREUNLOAD_EVENT +#endif /* BEFOREUNLOAD_EVENT */ + +EVENT(abort, eImageAbort, EventNameType_All, eBasicEventClass) +EVENT(beforetoggle, eBeforeToggle, EventNameType_HTMLXUL, eBasicEventClass) +EVENT(bounce, eMarqueeBounce, EventNameType_HTMLMarqueeOnly, eBasicEventClass) +EVENT(canplay, eCanPlay, EventNameType_HTML, eBasicEventClass) +EVENT(canplaythrough, eCanPlayThrough, EventNameType_HTML, eBasicEventClass) +EVENT(change, eFormChange, EventNameType_HTMLXUL, eBasicEventClass) +EVENT(CheckboxStateChange, eFormCheckboxStateChange, EventNameType_None, + eBasicEventClass) +EVENT(RadioStateChange, eFormRadioStateChange, EventNameType_None, + eBasicEventClass) +EVENT(auxclick, eMouseAuxClick, EventNameType_All, eMouseEventClass) +EVENT(click, eMouseClick, EventNameType_All, eMouseEventClass) +EVENT(close, eClose, EventNameType_HTMLXUL, eBasicEventClass) +EVENT(contextmenu, eContextMenu, + EventNameType_HTMLXUL | EventNameType_SVGGraphic, eMouseEventClass) +NON_IDL_EVENT(mouselongtap, eMouseLongTap, EventNameType_HTMLXUL, + eMouseEventClass) +EVENT(cuechange, eCueChange, EventNameType_All, eBasicEventClass) +EVENT(dblclick, eMouseDoubleClick, EventNameType_HTMLXUL, eMouseEventClass) +EVENT(drag, eDrag, EventNameType_HTMLXUL, eDragEventClass) +EVENT(dragend, eDragEnd, EventNameType_HTMLXUL, eDragEventClass) +EVENT(dragenter, eDragEnter, EventNameType_HTMLXUL, eDragEventClass) +EVENT(dragexit, eDragExit, EventNameType_HTMLXUL, eDragEventClass) +EVENT(dragleave, eDragLeave, EventNameType_HTMLXUL, eDragEventClass) +EVENT(dragover, eDragOver, EventNameType_HTMLXUL, eDragEventClass) +EVENT(dragstart, eDragStart, EventNameType_HTMLXUL, eDragEventClass) +EVENT(drop, eDrop, EventNameType_HTMLXUL, eDragEventClass) +EVENT(durationchange, eDurationChange, EventNameType_HTML, eBasicEventClass) +EVENT(emptied, eEmptied, EventNameType_HTML, eBasicEventClass) +EVENT(ended, eEnded, EventNameType_HTML, eBasicEventClass) +EVENT(finish, eMarqueeFinish, EventNameType_HTMLMarqueeOnly, eBasicEventClass) +EVENT(formdata, eFormData, EventNameType_HTML, eBasicEventClass) +EVENT(fullscreenchange, eFullscreenChange, EventNameType_HTML, eBasicEventClass) +EVENT(fullscreenerror, eFullscreenError, EventNameType_HTML, eBasicEventClass) +EVENT(beforeinput, eEditorBeforeInput, EventNameType_HTMLXUL, + eEditorInputEventClass) +EVENT(input, eEditorInput, EventNameType_HTMLXUL, eEditorInputEventClass) +EVENT(invalid, eFormInvalid, EventNameType_HTMLXUL, eBasicEventClass) +EVENT(keydown, eKeyDown, EventNameType_HTMLXUL, eKeyboardEventClass) +EVENT(keypress, eKeyPress, EventNameType_HTMLXUL, eKeyboardEventClass) +EVENT(keyup, eKeyUp, EventNameType_HTMLXUL, eKeyboardEventClass) +NON_IDL_EVENT(mozaccesskeynotfound, eAccessKeyNotFound, EventNameType_None, + eKeyboardEventClass) +EVENT(loadeddata, eLoadedData, EventNameType_HTML, eBasicEventClass) +EVENT(loadedmetadata, eLoadedMetaData, EventNameType_HTML, eBasicEventClass) +EVENT(loadend, eLoadEnd, EventNameType_HTML, eBasicEventClass) +EVENT(loadstart, eLoadStart, EventNameType_HTML, eBasicEventClass) +EVENT(mousedown, eMouseDown, EventNameType_All, eMouseEventClass) +EVENT(mouseenter, eMouseEnter, EventNameType_All, eMouseEventClass) +EVENT(mouseleave, eMouseLeave, EventNameType_All, eMouseEventClass) +EVENT(mousemove, eMouseMove, EventNameType_All, eMouseEventClass) +EVENT(mouseout, eMouseOut, EventNameType_All, eMouseEventClass) +EVENT(mouseover, eMouseOver, EventNameType_All, eMouseEventClass) +EVENT(mouseup, eMouseUp, EventNameType_All, eMouseEventClass) +EVENT(mozfullscreenchange, eMozFullscreenChange, EventNameType_HTML, + eBasicEventClass) +EVENT(mozfullscreenerror, eMozFullscreenError, EventNameType_HTML, + eBasicEventClass) +EVENT(mozpointerlockchange, eMozPointerLockChange, EventNameType_HTML, + eBasicEventClass) +EVENT(mozpointerlockerror, eMozPointerLockError, EventNameType_HTML, + eBasicEventClass) +EVENT(pointerlockchange, ePointerLockChange, EventNameType_HTML, + eBasicEventClass) +EVENT(pointerlockerror, ePointerLockError, EventNameType_HTML, eBasicEventClass) +EVENT(pointerdown, ePointerDown, EventNameType_All, ePointerEventClass) +EVENT(pointermove, ePointerMove, EventNameType_All, ePointerEventClass) +EVENT(pointerup, ePointerUp, EventNameType_All, ePointerEventClass) +EVENT(pointercancel, ePointerCancel, EventNameType_All, ePointerEventClass) +EVENT(pointerover, ePointerOver, EventNameType_All, ePointerEventClass) +EVENT(pointerout, ePointerOut, EventNameType_All, ePointerEventClass) +EVENT(pointerenter, ePointerEnter, EventNameType_All, ePointerEventClass) +EVENT(pointerleave, ePointerLeave, EventNameType_All, ePointerEventClass) +EVENT(gotpointercapture, ePointerGotCapture, EventNameType_All, + ePointerEventClass) +EVENT(lostpointercapture, ePointerLostCapture, EventNameType_All, + ePointerEventClass) +EVENT(selectstart, eSelectStart, EventNameType_HTMLXUL, eBasicEventClass) + +// Not supported yet; probably never because "wheel" is a better idea. +// EVENT(mousewheel) +EVENT(pause, ePause, EventNameType_HTML, eBasicEventClass) +EVENT(play, ePlay, EventNameType_HTML, eBasicEventClass) +EVENT(playing, ePlaying, EventNameType_HTML, eBasicEventClass) +EVENT(progress, eProgress, EventNameType_HTML, eBasicEventClass) +EVENT(ratechange, eRateChange, EventNameType_HTML, eBasicEventClass) +EVENT(reset, eFormReset, EventNameType_HTMLXUL, eBasicEventClass) +EVENT(securitypolicyviolation, eSecurityPolicyViolation, EventNameType_All, + eBasicEventClass) +EVENT(seeked, eSeeked, EventNameType_HTML, eBasicEventClass) +EVENT(seeking, eSeeking, EventNameType_HTML, eBasicEventClass) +EVENT(select, eFormSelect, EventNameType_HTMLXUL, eBasicEventClass) +EVENT(slotchange, eSlotChange, EventNameType_All, eBasicEventClass) +EVENT(stalled, eStalled, EventNameType_HTML, eBasicEventClass) +EVENT(start, eMarqueeStart, EventNameType_HTMLMarqueeOnly, eBasicEventClass) +EVENT(submit, eFormSubmit, EventNameType_HTMLXUL, eBasicEventClass) +EVENT(suspend, eSuspend, EventNameType_HTML, eBasicEventClass) +EVENT(timeupdate, eTimeUpdate, EventNameType_HTML, eBasicEventClass) +EVENT(toggle, eToggle, EventNameType_HTML, eBasicEventClass) +EVENT(volumechange, eVolumeChange, EventNameType_HTML, eBasicEventClass) +EVENT(waiting, eWaiting, EventNameType_HTML, eBasicEventClass) +EVENT(wheel, eWheel, EventNameType_All, eWheelEventClass) +EVENT(copy, eCopy, EventNameType_HTMLXUL | EventNameType_SVGGraphic, + eClipboardEventClass) +EVENT(cut, eCut, EventNameType_HTMLXUL | EventNameType_SVGGraphic, + eClipboardEventClass) +EVENT(paste, ePaste, EventNameType_HTMLXUL | EventNameType_SVGGraphic, + eClipboardEventClass) +// Gecko-specific extensions that apply to elements +EVENT(beforescriptexecute, eBeforeScriptExecute, EventNameType_HTMLXUL, + eBasicEventClass) +EVENT(afterscriptexecute, eAfterScriptExecute, EventNameType_HTMLXUL, + eBasicEventClass) + +FORWARDED_EVENT(blur, eBlur, EventNameType_HTMLXUL, eFocusEventClass) +ERROR_EVENT(error, eLoadError, EventNameType_All, eBasicEventClass) +FORWARDED_EVENT(focus, eFocus, EventNameType_HTMLXUL, eFocusEventClass) +FORWARDED_EVENT(focusin, eFocusIn, EventNameType_HTMLXUL, eFocusEventClass) +FORWARDED_EVENT(focusout, eFocusOut, EventNameType_HTMLXUL, eFocusEventClass) +FORWARDED_EVENT(load, eLoad, EventNameType_All, eBasicEventClass) +FORWARDED_EVENT(resize, eResize, EventNameType_All, eBasicEventClass) +FORWARDED_EVENT(scroll, eScroll, (EventNameType_HTMLXUL | EventNameType_SVGSVG), + eBasicEventClass) +NON_IDL_EVENT(mozvisualresize, eMozVisualResize, EventNameType_None, + eBasicEventClass) +NON_IDL_EVENT(mozvisualscroll, eMozVisualScroll, EventNameType_None, + eBasicEventClass) + +WINDOW_EVENT(afterprint, eAfterPrint, + EventNameType_XUL | EventNameType_HTMLBodyOrFramesetOnly, + eBasicEventClass) +WINDOW_EVENT(beforeprint, eBeforePrint, + EventNameType_XUL | EventNameType_HTMLBodyOrFramesetOnly, + eBasicEventClass) +BEFOREUNLOAD_EVENT(beforeunload, eBeforeUnload, + EventNameType_XUL | EventNameType_HTMLBodyOrFramesetOnly, + eBasicEventClass) +WINDOW_EVENT(hashchange, eHashChange, + EventNameType_XUL | EventNameType_HTMLBodyOrFramesetOnly, + eBasicEventClass) +WINDOW_EVENT(languagechange, eLanguageChange, + EventNameType_HTMLBodyOrFramesetOnly, eBasicEventClass) +// XXXbz Should the onmessage attribute on <body> really not work? If so, do we +// need a different macro to flag things like that (IDL, but not content +// attributes on body/frameset), or is just using EventNameType_None enough? +WINDOW_EVENT(message, eMessage, EventNameType_None, eBasicEventClass) +WINDOW_EVENT(messageerror, eMessageError, EventNameType_HTMLBodyOrFramesetOnly, + eBasicEventClass) +WINDOW_EVENT(offline, eOffline, + EventNameType_XUL | EventNameType_HTMLBodyOrFramesetOnly, + eBasicEventClass) +WINDOW_EVENT(online, eOnline, + EventNameType_XUL | EventNameType_HTMLBodyOrFramesetOnly, + eBasicEventClass) +#if defined(MOZ_WIDGET_ANDROID) +WINDOW_EVENT(orientationchange, eOrientationChange, + EventNameType_HTMLBodyOrFramesetOnly, eBasicEventClass) +#endif +WINDOW_EVENT(pagehide, ePageHide, EventNameType_HTMLBodyOrFramesetOnly, + eBasicEventClass) +WINDOW_EVENT(pageshow, ePageShow, EventNameType_HTMLBodyOrFramesetOnly, + eBasicEventClass) +WINDOW_EVENT(popstate, ePopState, + EventNameType_XUL | EventNameType_HTMLBodyOrFramesetOnly, + eBasicEventClass) +WINDOW_EVENT(rejectionhandled, eRejectionHandled, + EventNameType_HTMLBodyOrFramesetOnly, eBasicEventClass) +WINDOW_EVENT(storage, eStorage, EventNameType_HTMLBodyOrFramesetOnly, + eBasicEventClass) +WINDOW_EVENT(unhandledrejection, eUnhandledRejection, + EventNameType_HTMLBodyOrFramesetOnly, eBasicEventClass) +WINDOW_EVENT(unload, eUnload, + (EventNameType_XUL | EventNameType_SVGSVG | + EventNameType_HTMLBodyOrFramesetOnly), + eBasicEventClass) + +WINDOW_ONLY_EVENT(devicemotion, eDeviceMotion, EventNameType_None, + eBasicEventClass) +WINDOW_ONLY_EVENT(deviceorientation, eDeviceOrientation, EventNameType_None, + eBasicEventClass) +WINDOW_ONLY_EVENT(deviceorientationabsolute, eDeviceOrientationAbsolute, + EventNameType_None, eBasicEventClass) +WINDOW_ONLY_EVENT(userproximity, eUserProximity, EventNameType_None, + eBasicEventClass) +WINDOW_ONLY_EVENT(devicelight, eDeviceLight, EventNameType_None, + eBasicEventClass) +WINDOW_ONLY_EVENT(vrdisplayactivate, eVRDisplayActivate, EventNameType_None, + eBasicEventClass) +WINDOW_ONLY_EVENT(vrdisplaydeactivate, eVRDisplayDeactivate, EventNameType_None, + eBasicEventClass) +WINDOW_ONLY_EVENT(vrdisplayconnect, eVRDisplayConnect, EventNameType_None, + eBasicEventClass) +WINDOW_ONLY_EVENT(vrdisplaydisconnect, eVRDisplayDisconnect, EventNameType_None, + eBasicEventClass) +WINDOW_ONLY_EVENT(vrdisplaypresentchange, eVRDisplayPresentChange, + EventNameType_None, eBasicEventClass) + +TOUCH_EVENT(touchstart, eTouchStart, EventNameType_All, eTouchEventClass) +TOUCH_EVENT(touchend, eTouchEnd, EventNameType_All, eTouchEventClass) +TOUCH_EVENT(touchmove, eTouchMove, EventNameType_All, eTouchEventClass) +TOUCH_EVENT(touchcancel, eTouchCancel, EventNameType_All, eTouchEventClass) + +DOCUMENT_ONLY_EVENT(readystatechange, eReadyStateChange, EventNameType_HTMLXUL, + eBasicEventClass) +EVENT(selectionchange, eSelectionChange, EventNameType_HTMLXUL, + eBasicEventClass) +DOCUMENT_ONLY_EVENT(visibilitychange, eVisibilityChange, EventNameType_HTMLXUL, + eBasicEventClass) + +NON_IDL_EVENT(MozMouseHittest, eMouseHitTest, EventNameType_None, + eMouseEventClass) + +NON_IDL_EVENT(MozMouseExploreByTouch, eMouseExploreByTouch, EventNameType_None, + eMouseEventClass) + +NON_IDL_EVENT(DOMAttrModified, eLegacyAttrModified, EventNameType_HTMLXUL, + eMutationEventClass) +NON_IDL_EVENT(DOMCharacterDataModified, eLegacyCharacterDataModified, + EventNameType_HTMLXUL, eMutationEventClass) +NON_IDL_EVENT(DOMNodeInserted, eLegacyNodeInserted, EventNameType_HTMLXUL, + eMutationEventClass) +NON_IDL_EVENT(DOMNodeRemoved, eLegacyNodeRemoved, EventNameType_HTMLXUL, + eMutationEventClass) +NON_IDL_EVENT(DOMNodeInsertedIntoDocument, eLegacyNodeInsertedIntoDocument, + EventNameType_HTMLXUL, eMutationEventClass) +NON_IDL_EVENT(DOMNodeRemovedFromDocument, eLegacyNodeRemovedFromDocument, + EventNameType_HTMLXUL, eMutationEventClass) +NON_IDL_EVENT(DOMSubtreeModified, eLegacySubtreeModified, EventNameType_HTMLXUL, + eMutationEventClass) + +NON_IDL_EVENT(DOMActivate, eLegacyDOMActivate, EventNameType_HTMLXUL, + eUIEventClass) +NON_IDL_EVENT(DOMFocusIn, eLegacyDOMFocusIn, EventNameType_HTMLXUL, + eUIEventClass) +NON_IDL_EVENT(DOMFocusOut, eLegacyDOMFocusOut, EventNameType_HTMLXUL, + eUIEventClass) + +NON_IDL_EVENT(DOMMouseScroll, eLegacyMouseLineOrPageScroll, + EventNameType_HTMLXUL, eMouseScrollEventClass) +NON_IDL_EVENT(MozMousePixelScroll, eLegacyMousePixelScroll, + EventNameType_HTMLXUL, eMouseScrollEventClass) + +NON_IDL_EVENT(open, eOpen, EventNameType_None, eBasicEventClass) + +NON_IDL_EVENT(dataavailable, eMediaRecorderDataAvailable, EventNameType_None, + eBasicEventClass) + +NON_IDL_EVENT(stop, eMediaRecorderStop, EventNameType_None, eBasicEventClass) + +NON_IDL_EVENT(warning, eMediaRecorderWarning, EventNameType_None, + eBasicEventClass) + +// Events that only have on* attributes on XUL elements + +// "text" event is legacy event for modifying composition string in EditorBase. +// This shouldn't be used by web/xul apps. "compositionupdate" should be +// used instead. +NON_IDL_EVENT(text, eCompositionChange, EventNameType_XUL, + eCompositionEventClass) +NON_IDL_EVENT(compositionstart, eCompositionStart, EventNameType_XUL, + eCompositionEventClass) +NON_IDL_EVENT(compositionupdate, eCompositionUpdate, EventNameType_XUL, + eCompositionEventClass) +NON_IDL_EVENT(compositionend, eCompositionEnd, EventNameType_XUL, + eCompositionEventClass) +NON_IDL_EVENT(command, eXULCommand, EventNameType_XUL, eInputEventClass) +NON_IDL_EVENT(popupshowing, eXULPopupShowing, EventNameType_XUL, + eBasicEventClass) +NON_IDL_EVENT(popupshown, eXULPopupShown, EventNameType_XUL, eBasicEventClass) +NON_IDL_EVENT(popuphiding, eXULPopupHiding, EventNameType_XUL, eBasicEventClass) +NON_IDL_EVENT(popuphidden, eXULPopupHidden, EventNameType_XUL, eBasicEventClass) +NON_IDL_EVENT(broadcast, eXULBroadcast, EventNameType_XUL, eBasicEventClass) +NON_IDL_EVENT(commandupdate, eXULCommandUpdate, EventNameType_XUL, + eBasicEventClass) +NON_IDL_EVENT(overflow, eScrollPortOverflow, EventNameType_XUL, + eBasicEventClass) +NON_IDL_EVENT(underflow, eScrollPortUnderflow, EventNameType_XUL, + eBasicEventClass) +NON_IDL_EVENT(systemstatusbarclick, eXULSystemStatusBarClick, EventNameType_XUL, + eBasicEventClass) + +// Various SVG events +NON_IDL_EVENT(SVGLoad, eSVGLoad, EventNameType_None, eBasicEventClass) +NON_IDL_EVENT(SVGScroll, eSVGScroll, EventNameType_None, eBasicEventClass) + +// Only map the ID to the real event name when MESSAGE_TO_EVENT is defined. +#ifndef MESSAGE_TO_EVENT +EVENT(begin, eSMILBeginEvent, EventNameType_SMIL, eBasicEventClass) +#endif +NON_IDL_EVENT(beginEvent, eSMILBeginEvent, EventNameType_None, + eSMILTimeEventClass) +// Only map the ID to the real event name when MESSAGE_TO_EVENT is defined. +#ifndef MESSAGE_TO_EVENT +EVENT(end, eSMILEndEvent, EventNameType_SMIL, eBasicEventClass) +#endif +NON_IDL_EVENT(endEvent, eSMILEndEvent, EventNameType_None, eSMILTimeEventClass) +// Only map the ID to the real event name when MESSAGE_TO_EVENT is defined. +#ifndef MESSAGE_TO_EVENT +EVENT(repeat, eSMILRepeatEvent, EventNameType_SMIL, eBasicEventClass) +#endif +NON_IDL_EVENT(repeatEvent, eSMILRepeatEvent, EventNameType_None, + eSMILTimeEventClass) + +NON_IDL_EVENT(MozAfterPaint, eAfterPaint, EventNameType_None, eBasicEventClass) + +NON_IDL_EVENT(MozScrolledAreaChanged, eScrolledAreaChanged, EventNameType_None, + eScrollAreaEventClass) + +NON_IDL_EVENT(gamepadbuttondown, eGamepadButtonDown, EventNameType_None, + eBasicEventClass) +NON_IDL_EVENT(gamepadbuttonup, eGamepadButtonUp, EventNameType_None, + eBasicEventClass) +NON_IDL_EVENT(gamepadaxismove, eGamepadAxisMove, EventNameType_None, + eBasicEventClass) +WINDOW_EVENT(gamepadconnected, eGamepadConnected, EventNameType_None, + eBasicEventClass) +WINDOW_EVENT(gamepaddisconnected, eGamepadDisconnected, EventNameType_None, + eBasicEventClass) + +// Simple gesture events +NON_IDL_EVENT(MozSwipeGestureMayStart, eSwipeGestureMayStart, + EventNameType_None, eSimpleGestureEventClass) +NON_IDL_EVENT(MozSwipeGestureStart, eSwipeGestureStart, EventNameType_None, + eSimpleGestureEventClass) +NON_IDL_EVENT(MozSwipeGestureUpdate, eSwipeGestureUpdate, EventNameType_None, + eSimpleGestureEventClass) +NON_IDL_EVENT(MozSwipeGestureEnd, eSwipeGestureEnd, EventNameType_None, + eSimpleGestureEventClass) +NON_IDL_EVENT(MozSwipeGesture, eSwipeGesture, EventNameType_None, + eSimpleGestureEventClass) +NON_IDL_EVENT(MozMagnifyGestureStart, eMagnifyGestureStart, EventNameType_None, + eSimpleGestureEventClass) +NON_IDL_EVENT(MozMagnifyGestureUpdate, eMagnifyGestureUpdate, + EventNameType_None, eSimpleGestureEventClass) +NON_IDL_EVENT(MozMagnifyGesture, eMagnifyGesture, EventNameType_None, + eSimpleGestureEventClass) +NON_IDL_EVENT(MozRotateGestureStart, eRotateGestureStart, EventNameType_None, + eSimpleGestureEventClass) +NON_IDL_EVENT(MozRotateGestureUpdate, eRotateGestureUpdate, EventNameType_None, + eSimpleGestureEventClass) +NON_IDL_EVENT(MozRotateGesture, eRotateGesture, EventNameType_None, + eSimpleGestureEventClass) +NON_IDL_EVENT(MozTapGesture, eTapGesture, EventNameType_None, + eSimpleGestureEventClass) +NON_IDL_EVENT(MozPressTapGesture, ePressTapGesture, EventNameType_None, + eSimpleGestureEventClass) +NON_IDL_EVENT(MozEdgeUIStarted, eEdgeUIStarted, EventNameType_None, + eSimpleGestureEventClass) +NON_IDL_EVENT(MozEdgeUICanceled, eEdgeUICanceled, EventNameType_None, + eSimpleGestureEventClass) +NON_IDL_EVENT(MozEdgeUICompleted, eEdgeUICompleted, EventNameType_None, + eSimpleGestureEventClass) + +// CSS Transition & Animation events: +EVENT(transitionstart, eTransitionStart, EventNameType_All, + eTransitionEventClass) +EVENT(transitionrun, eTransitionRun, EventNameType_All, eTransitionEventClass) +EVENT(transitionend, eTransitionEnd, EventNameType_All, eTransitionEventClass) +EVENT(transitioncancel, eTransitionCancel, EventNameType_All, + eTransitionEventClass) +EVENT(animationstart, eAnimationStart, EventNameType_All, eAnimationEventClass) +EVENT(animationend, eAnimationEnd, EventNameType_All, eAnimationEventClass) +EVENT(animationiteration, eAnimationIteration, EventNameType_All, + eAnimationEventClass) +EVENT(animationcancel, eAnimationCancel, EventNameType_All, + eAnimationEventClass) + +// Webkit-prefixed versions of Transition & Animation events, for web compat: +EVENT(webkitAnimationEnd, eWebkitAnimationEnd, EventNameType_All, + eAnimationEventClass) +EVENT(webkitAnimationIteration, eWebkitAnimationIteration, EventNameType_All, + eAnimationEventClass) +EVENT(webkitAnimationStart, eWebkitAnimationStart, EventNameType_All, + eAnimationEventClass) +EVENT(webkitTransitionEnd, eWebkitTransitionEnd, EventNameType_All, + eTransitionEventClass) +#ifndef MESSAGE_TO_EVENT +// These are only here so that IsEventAttributeName() will return the right +// thing for these events. We could probably remove them if we used +// Element::GetEventNameForAttr on the input to IsEventAttributeName before +// looking it up in the hashtable... +EVENT(webkitanimationend, eUnidentifiedEvent, EventNameType_All, + eAnimationEventClass) +EVENT(webkitanimationiteration, eUnidentifiedEvent, EventNameType_All, + eAnimationEventClass) +EVENT(webkitanimationstart, eUnidentifiedEvent, EventNameType_All, + eAnimationEventClass) +EVENT(webkittransitionend, eUnidentifiedEvent, EventNameType_All, + eTransitionEventClass) +#endif + +NON_IDL_EVENT(audioprocess, eAudioProcess, EventNameType_None, eBasicEventClass) + +NON_IDL_EVENT(complete, eAudioComplete, EventNameType_None, eBasicEventClass) + +EVENT(scrollend, eScrollend, EventNameType_All, eBasicEventClass) + +#ifdef DEFINED_FORWARDED_EVENT +# undef DEFINED_FORWARDED_EVENT +# undef FORWARDED_EVENT +#endif /* DEFINED_FORWARDED_EVENT */ + +#ifdef DEFINED_WINDOW_EVENT +# undef DEFINED_WINDOW_EVENT +# undef WINDOW_EVENT +#endif /* DEFINED_WINDOW_EVENT */ + +#ifdef DEFINED_WINDOW_ONLY_EVENT +# undef DEFINED_WINDOW_ONLY_EVENT +# undef WINDOW_ONLY_EVENT +#endif /* DEFINED_WINDOW_ONLY_EVENT */ + +#ifdef DEFINED_TOUCH_EVENT +# undef DEFINED_TOUCH_EVENT +# undef TOUCH_EVENT +#endif /* DEFINED_TOUCH_EVENT */ + +#ifdef DEFINED_DOCUMENT_ONLY_EVENT +# undef DEFINED_DOCUMENT_ONLY_EVENT +# undef DOCUMENT_ONLY_EVENT +#endif /* DEFINED_DOCUMENT_ONLY_EVENT */ + +#ifdef DEFINED_NON_IDL_EVENT +# undef DEFINED_NON_IDL_EVENT +# undef NON_IDL_EVENT +#endif /* DEFINED_NON_IDL_EVENT */ + +#ifdef DEFINED_ERROR_EVENT +# undef DEFINED_ERROR_EVENT +# undef ERROR_EVENT +#endif /* DEFINED_ERROR_EVENT */ + +#ifdef DEFINED_BEFOREUNLOAD_EVENT +# undef DEFINED_BEFOREUNLOAD_EVENT +# undef BEFOREUNLOAD_EVENT +#endif /* BEFOREUNLOAD_EVENT */ + +#ifdef MESSAGE_TO_EVENT +# undef EVENT +# undef WINDOW_ONLY_EVENT +# undef TOUCH_EVENT +# undef DOCUMENT_ONLY_EVENT +# undef NON_IDL_EVENT +#endif /* MESSAGE_TO_EVENT */ diff --git a/dom/events/EventStateManager.cpp b/dom/events/EventStateManager.cpp new file mode 100644 index 0000000000..b633821d8b --- /dev/null +++ b/dom/events/EventStateManager.cpp @@ -0,0 +1,6695 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#include "EventStateManager.h" + +#include "mozilla/AsyncEventDispatcher.h" +#include "mozilla/Attributes.h" +#include "mozilla/EditorBase.h" +#include "mozilla/EventDispatcher.h" +#include "mozilla/EventForwards.h" +#include "mozilla/Hal.h" +#include "mozilla/HTMLEditor.h" +#include "mozilla/IMEStateManager.h" +#include "mozilla/MiscEvents.h" +#include "mozilla/MathAlgorithms.h" +#include "mozilla/MouseEvents.h" +#include "mozilla/PointerLockManager.h" +#include "mozilla/PresShell.h" +#include "mozilla/ScopeExit.h" +#include "mozilla/ScrollTypes.h" +#include "mozilla/TextComposition.h" +#include "mozilla/TextControlElement.h" +#include "mozilla/TextEditor.h" +#include "mozilla/TextEvents.h" +#include "mozilla/TouchEvents.h" +#include "mozilla/Telemetry.h" +#include "mozilla/UniquePtr.h" +#include "mozilla/dom/BrowserBridgeChild.h" +#include "mozilla/dom/BrowsingContext.h" +#include "mozilla/dom/CanonicalBrowsingContext.h" +#include "mozilla/dom/ContentChild.h" +#include "mozilla/dom/DOMIntersectionObserver.h" +#include "mozilla/dom/DragEvent.h" +#include "mozilla/dom/Event.h" +#include "mozilla/dom/FrameLoaderBinding.h" +#include "mozilla/dom/HTMLLabelElement.h" +#include "mozilla/dom/MouseEventBinding.h" +#include "mozilla/dom/BrowserChild.h" +#include "mozilla/dom/PointerEventHandler.h" +#include "mozilla/dom/UIEvent.h" +#include "mozilla/dom/UIEventBinding.h" +#include "mozilla/dom/UserActivation.h" +#include "mozilla/dom/WheelEventBinding.h" +#include "mozilla/glean/GleanMetrics.h" +#include "mozilla/StaticPrefs_accessibility.h" +#include "mozilla/StaticPrefs_browser.h" +#include "mozilla/StaticPrefs_dom.h" +#include "mozilla/StaticPrefs_layout.h" +#include "mozilla/StaticPrefs_mousewheel.h" +#include "mozilla/StaticPrefs_ui.h" +#include "mozilla/StaticPrefs_zoom.h" + +#include "ContentEventHandler.h" +#include "IMEContentObserver.h" +#include "WheelHandlingHelper.h" +#include "RemoteDragStartData.h" + +#include "nsCommandParams.h" +#include "nsCOMPtr.h" +#include "nsCopySupport.h" +#include "nsFocusManager.h" +#include "nsGenericHTMLElement.h" +#include "nsIClipboard.h" +#include "nsIContent.h" +#include "nsIContentInlines.h" +#include "mozilla/dom/Document.h" +#include "nsICookieJarSettings.h" +#include "nsIFrame.h" +#include "nsFrameLoaderOwner.h" +#include "nsIWidget.h" +#include "nsLiteralString.h" +#include "nsPresContext.h" +#include "nsGkAtoms.h" +#include "nsIFormControl.h" +#include "nsComboboxControlFrame.h" +#include "nsIScrollableFrame.h" +#include "nsIDOMXULControlElement.h" +#include "nsNameSpaceManager.h" +#include "nsIBaseWindow.h" +#include "nsFrameSelection.h" +#include "nsPIDOMWindow.h" +#include "nsPIWindowRoot.h" +#include "nsIWebNavigation.h" +#include "nsIContentViewer.h" +#include "nsFrameManager.h" +#include "nsIBrowserChild.h" +#include "nsMenuPopupFrame.h" + +#include "nsIObserverService.h" +#include "nsIDocShell.h" + +#include "nsSubDocumentFrame.h" +#include "nsLayoutUtils.h" +#include "nsIInterfaceRequestorUtils.h" +#include "nsUnicharUtils.h" +#include "nsContentUtils.h" + +#include "imgIContainer.h" +#include "nsIProperties.h" +#include "nsISupportsPrimitives.h" + +#include "nsServiceManagerUtils.h" +#include "nsITimer.h" +#include "nsFontMetrics.h" +#include "nsIDragService.h" +#include "nsIDragSession.h" +#include "mozilla/dom/DataTransfer.h" +#include "nsContentAreaDragDrop.h" +#include "nsTreeBodyFrame.h" +#include "nsIController.h" +#include "mozilla/Services.h" +#include "mozilla/dom/ContentParent.h" +#include "mozilla/dom/Record.h" +#include "mozilla/dom/Selection.h" + +#include "mozilla/Preferences.h" +#include "mozilla/LookAndFeel.h" +#include "mozilla/ProfilerLabels.h" +#include "Units.h" + +#ifdef XP_MACOSX +# import <ApplicationServices/ApplicationServices.h> +#endif + +namespace mozilla { + +using namespace dom; + +static const LayoutDeviceIntPoint kInvalidRefPoint = + LayoutDeviceIntPoint(-1, -1); + +static uint32_t gMouseOrKeyboardEventCounter = 0; +static nsITimer* gUserInteractionTimer = nullptr; +static nsITimerCallback* gUserInteractionTimerCallback = nullptr; + +static const double kCursorLoadingTimeout = 1000; // ms +static AutoWeakFrame gLastCursorSourceFrame; +static TimeStamp gLastCursorUpdateTime; +static TimeStamp gTypingStartTime; +static TimeStamp gTypingEndTime; +static int32_t gTypingInteractionKeyPresses = 0; +static dom::InteractionData gTypingInteraction = {}; + +static inline int32_t RoundDown(double aDouble) { + return (aDouble > 0) ? static_cast<int32_t>(floor(aDouble)) + : static_cast<int32_t>(ceil(aDouble)); +} + +static bool IsSelectingLink(nsIFrame* aTargetFrame) { + if (!aTargetFrame) { + return false; + } + const nsFrameSelection* frameSel = aTargetFrame->GetConstFrameSelection(); + if (!frameSel || !frameSel->GetDragState()) { + return false; + } + + if (!nsContentUtils::GetClosestLinkInFlatTree(aTargetFrame->GetContent())) { + return false; + } + return true; +} + +static UniquePtr<WidgetMouseEvent> CreateMouseOrPointerWidgetEvent( + WidgetMouseEvent* aMouseEvent, EventMessage aMessage, + EventTarget* aRelatedTarget); + +/******************************************************************/ +/* mozilla::UITimerCallback */ +/******************************************************************/ + +class UITimerCallback final : public nsITimerCallback, public nsINamed { + public: + UITimerCallback() : mPreviousCount(0) {} + NS_DECL_ISUPPORTS + NS_DECL_NSITIMERCALLBACK + NS_DECL_NSINAMED + private: + ~UITimerCallback() = default; + uint32_t mPreviousCount; +}; + +NS_IMPL_ISUPPORTS(UITimerCallback, nsITimerCallback, nsINamed) + +// If aTimer is nullptr, this method always sends "user-interaction-inactive" +// notification. +NS_IMETHODIMP +UITimerCallback::Notify(nsITimer* aTimer) { + nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService(); + if (!obs) return NS_ERROR_FAILURE; + if ((gMouseOrKeyboardEventCounter == mPreviousCount) || !aTimer) { + gMouseOrKeyboardEventCounter = 0; + obs->NotifyObservers(nullptr, "user-interaction-inactive", nullptr); + if (gUserInteractionTimer) { + gUserInteractionTimer->Cancel(); + NS_RELEASE(gUserInteractionTimer); + } + } else { + obs->NotifyObservers(nullptr, "user-interaction-active", nullptr); + EventStateManager::UpdateUserActivityTimer(); + + if (XRE_IsParentProcess()) { + hal::BatteryInformation batteryInfo; + hal::GetCurrentBatteryInformation(&batteryInfo); + glean::power_battery::percentage_when_user_active.AccumulateSamples( + {uint64_t(batteryInfo.level() * 100)}); + } + } + mPreviousCount = gMouseOrKeyboardEventCounter; + return NS_OK; +} + +NS_IMETHODIMP +UITimerCallback::GetName(nsACString& aName) { + aName.AssignLiteral("UITimerCallback_timer"); + return NS_OK; +} + +/******************************************************************/ +/* mozilla::OverOutElementsWrapper */ +/******************************************************************/ + +OverOutElementsWrapper::OverOutElementsWrapper() : mLastOverFrame(nullptr) {} + +OverOutElementsWrapper::~OverOutElementsWrapper() = default; + +NS_IMPL_CYCLE_COLLECTION(OverOutElementsWrapper, mLastOverElement, + mFirstOverEventElement, mFirstOutEventElement) +NS_IMPL_CYCLE_COLLECTING_ADDREF(OverOutElementsWrapper) +NS_IMPL_CYCLE_COLLECTING_RELEASE(OverOutElementsWrapper) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(OverOutElementsWrapper) + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +/******************************************************************/ +/* mozilla::EventStateManager */ +/******************************************************************/ + +static uint32_t sESMInstanceCount = 0; + +bool EventStateManager::sNormalLMouseEventInProcess = false; +int16_t EventStateManager::sCurrentMouseBtn = MouseButton::eNotPressed; +EventStateManager* EventStateManager::sActiveESM = nullptr; +Document* EventStateManager::sMouseOverDocument = nullptr; +AutoWeakFrame EventStateManager::sLastDragOverFrame = nullptr; +LayoutDeviceIntPoint EventStateManager::sPreLockPoint = + LayoutDeviceIntPoint(0, 0); +LayoutDeviceIntPoint EventStateManager::sLastRefPoint = kInvalidRefPoint; +CSSIntPoint EventStateManager::sLastScreenPoint = CSSIntPoint(0, 0); +LayoutDeviceIntPoint EventStateManager::sSynthCenteringPoint = kInvalidRefPoint; +CSSIntPoint EventStateManager::sLastClientPoint = CSSIntPoint(0, 0); +nsCOMPtr<nsIContent> EventStateManager::sDragOverContent = nullptr; + +EventStateManager::WheelPrefs* EventStateManager::WheelPrefs::sInstance = + nullptr; +EventStateManager::DeltaAccumulator* + EventStateManager::DeltaAccumulator::sInstance = nullptr; + +constexpr const StyleCursorKind kInvalidCursorKind = + static_cast<StyleCursorKind>(255); + +EventStateManager::EventStateManager() + : mLockCursor(kInvalidCursorKind), + mLastFrameConsumedSetCursor(false), + mCurrentTarget(nullptr), + // init d&d gesture state machine variables + mGestureDownPoint(0, 0), + mGestureModifiers(0), + mGestureDownButtons(0), + mPresContext(nullptr), + mLClickCount(0), + mMClickCount(0), + mRClickCount(0), + mShouldAlwaysUseLineDeltas(false), + mShouldAlwaysUseLineDeltasInitialized(false), + mGestureDownInTextControl(false), + mInTouchDrag(false), + m_haveShutdown(false) { + if (sESMInstanceCount == 0) { + gUserInteractionTimerCallback = new UITimerCallback(); + if (gUserInteractionTimerCallback) NS_ADDREF(gUserInteractionTimerCallback); + UpdateUserActivityTimer(); + } + ++sESMInstanceCount; +} + +nsresult EventStateManager::UpdateUserActivityTimer() { + if (!gUserInteractionTimerCallback) return NS_OK; + + if (!gUserInteractionTimer) { + gUserInteractionTimer = NS_NewTimer().take(); + } + + if (gUserInteractionTimer) { + gUserInteractionTimer->InitWithCallback( + gUserInteractionTimerCallback, + StaticPrefs::dom_events_user_interaction_interval(), + nsITimer::TYPE_ONE_SHOT); + } + return NS_OK; +} + +nsresult EventStateManager::Init() { + nsCOMPtr<nsIObserverService> observerService = + mozilla::services::GetObserverService(); + if (!observerService) return NS_ERROR_FAILURE; + + observerService->AddObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID, true); + + return NS_OK; +} + +bool EventStateManager::ShouldAlwaysUseLineDeltas() { + if (MOZ_UNLIKELY(!mShouldAlwaysUseLineDeltasInitialized)) { + mShouldAlwaysUseLineDeltasInitialized = true; + mShouldAlwaysUseLineDeltas = + !StaticPrefs::dom_event_wheel_deltaMode_lines_disabled(); + if (!mShouldAlwaysUseLineDeltas && mDocument) { + if (nsIPrincipal* principal = + mDocument->GetPrincipalForPrefBasedHacks()) { + mShouldAlwaysUseLineDeltas = principal->IsURIInPrefList( + "dom.event.wheel-deltaMode-lines.always-enabled"); + } + } + } + return mShouldAlwaysUseLineDeltas; +} + +EventStateManager::~EventStateManager() { + ReleaseCurrentIMEContentObserver(); + + if (sActiveESM == this) { + sActiveESM = nullptr; + } + + if (StaticPrefs::ui_click_hold_context_menus()) { + KillClickHoldTimer(); + } + + if (mDocument == sMouseOverDocument) { + sMouseOverDocument = nullptr; + } + + --sESMInstanceCount; + if (sESMInstanceCount == 0) { + WheelTransaction::Shutdown(); + if (gUserInteractionTimerCallback) { + gUserInteractionTimerCallback->Notify(nullptr); + NS_RELEASE(gUserInteractionTimerCallback); + } + if (gUserInteractionTimer) { + gUserInteractionTimer->Cancel(); + NS_RELEASE(gUserInteractionTimer); + } + WheelPrefs::Shutdown(); + DeltaAccumulator::Shutdown(); + } + + if (sDragOverContent && sDragOverContent->OwnerDoc() == mDocument) { + sDragOverContent = nullptr; + } + + if (!m_haveShutdown) { + Shutdown(); + + // Don't remove from Observer service in Shutdown because Shutdown also + // gets called from xpcom shutdown observer. And we don't want to remove + // from the service in that case. + + nsCOMPtr<nsIObserverService> observerService = + mozilla::services::GetObserverService(); + if (observerService) { + observerService->RemoveObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID); + } + } +} + +nsresult EventStateManager::Shutdown() { + m_haveShutdown = true; + return NS_OK; +} + +NS_IMETHODIMP +EventStateManager::Observe(nsISupports* aSubject, const char* aTopic, + const char16_t* someData) { + if (!nsCRT::strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID)) { + Shutdown(); + } + + return NS_OK; +} + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(EventStateManager) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIObserver) + NS_INTERFACE_MAP_ENTRY(nsIObserver) + NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTING_ADDREF(EventStateManager) +NS_IMPL_CYCLE_COLLECTING_RELEASE(EventStateManager) + +NS_IMPL_CYCLE_COLLECTION_WEAK(EventStateManager, mCurrentTargetContent, + mGestureDownContent, mGestureDownFrameOwner, + mLastLeftMouseDownContent, + mLastMiddleMouseDownContent, + mLastRightMouseDownContent, mActiveContent, + mHoverContent, mURLTargetContent, + mMouseEnterLeaveHelper, mPointersEnterLeaveHelper, + mDocument, mIMEContentObserver, mAccessKeys) + +void EventStateManager::ReleaseCurrentIMEContentObserver() { + if (mIMEContentObserver) { + mIMEContentObserver->DisconnectFromEventStateManager(); + } + mIMEContentObserver = nullptr; +} + +void EventStateManager::OnStartToObserveContent( + IMEContentObserver* aIMEContentObserver) { + if (mIMEContentObserver == aIMEContentObserver) { + return; + } + ReleaseCurrentIMEContentObserver(); + mIMEContentObserver = aIMEContentObserver; +} + +void EventStateManager::OnStopObservingContent( + IMEContentObserver* aIMEContentObserver) { + aIMEContentObserver->DisconnectFromEventStateManager(); + NS_ENSURE_TRUE_VOID(mIMEContentObserver == aIMEContentObserver); + mIMEContentObserver = nullptr; +} + +void EventStateManager::TryToFlushPendingNotificationsToIME() { + if (mIMEContentObserver) { + mIMEContentObserver->TryToFlushPendingNotifications(true); + } +} + +static bool IsMessageMouseUserActivity(EventMessage aMessage) { + return aMessage == eMouseMove || aMessage == eMouseUp || + aMessage == eMouseDown || aMessage == eMouseAuxClick || + aMessage == eMouseDoubleClick || aMessage == eMouseClick || + aMessage == eMouseActivate || aMessage == eMouseLongTap; +} + +static bool IsMessageGamepadUserActivity(EventMessage aMessage) { + return aMessage == eGamepadButtonDown || aMessage == eGamepadButtonUp || + aMessage == eGamepadAxisMove; +} + +// static +bool EventStateManager::IsKeyboardEventUserActivity(WidgetEvent* aEvent) { + // We ignore things that shouldn't cause popups, but also things that look + // like shortcut presses. In some obscure cases these may actually be + // website input, but any meaningful website will have other input anyway, + // and we can't very well tell whether shortcut input was supposed to be + // directed at chrome or the document. + + WidgetKeyboardEvent* keyEvent = aEvent->AsKeyboardEvent(); + // Access keys should be treated as page interaction. + if (keyEvent->ModifiersMatchWithAccessKey(AccessKeyType::eContent)) { + return true; + } + if (!keyEvent->CanTreatAsUserInput() || keyEvent->IsControl() || + keyEvent->IsMeta() || keyEvent->IsOS() || keyEvent->IsAlt()) { + return false; + } + // Deal with function keys: + switch (keyEvent->mKeyNameIndex) { + case KEY_NAME_INDEX_F1: + case KEY_NAME_INDEX_F2: + case KEY_NAME_INDEX_F3: + case KEY_NAME_INDEX_F4: + case KEY_NAME_INDEX_F5: + case KEY_NAME_INDEX_F6: + case KEY_NAME_INDEX_F7: + case KEY_NAME_INDEX_F8: + case KEY_NAME_INDEX_F9: + case KEY_NAME_INDEX_F10: + case KEY_NAME_INDEX_F11: + case KEY_NAME_INDEX_F12: + case KEY_NAME_INDEX_F13: + case KEY_NAME_INDEX_F14: + case KEY_NAME_INDEX_F15: + case KEY_NAME_INDEX_F16: + case KEY_NAME_INDEX_F17: + case KEY_NAME_INDEX_F18: + case KEY_NAME_INDEX_F19: + case KEY_NAME_INDEX_F20: + case KEY_NAME_INDEX_F21: + case KEY_NAME_INDEX_F22: + case KEY_NAME_INDEX_F23: + case KEY_NAME_INDEX_F24: + return false; + default: + return true; + } +} + +static void OnTypingInteractionEnded() { + // We don't consider a single keystroke to be typing. + if (gTypingInteractionKeyPresses > 1) { + gTypingInteraction.mInteractionCount += gTypingInteractionKeyPresses; + gTypingInteraction.mInteractionTimeInMilliseconds += static_cast<uint32_t>( + std::ceil((gTypingEndTime - gTypingStartTime).ToMilliseconds())); + } + + gTypingInteractionKeyPresses = 0; + gTypingStartTime = TimeStamp(); + gTypingEndTime = TimeStamp(); +} + +static void HandleKeyUpInteraction(WidgetKeyboardEvent* aKeyEvent) { + if (EventStateManager::IsKeyboardEventUserActivity(aKeyEvent)) { + TimeStamp now = TimeStamp::Now(); + if (gTypingEndTime.IsNull()) { + gTypingEndTime = now; + } + TimeDuration delay = now - gTypingEndTime; + // Has it been too long since the last keystroke to be considered typing? + if (gTypingInteractionKeyPresses > 0 && + delay > + TimeDuration::FromMilliseconds( + StaticPrefs::browser_places_interactions_typing_timeout_ms())) { + OnTypingInteractionEnded(); + } + gTypingInteractionKeyPresses++; + if (gTypingStartTime.IsNull()) { + gTypingStartTime = now; + } + gTypingEndTime = now; + } +} + +nsresult EventStateManager::PreHandleEvent(nsPresContext* aPresContext, + WidgetEvent* aEvent, + nsIFrame* aTargetFrame, + nsIContent* aTargetContent, + nsEventStatus* aStatus, + nsIContent* aOverrideClickTarget) { + NS_ENSURE_ARG_POINTER(aStatus); + NS_ENSURE_ARG(aPresContext); + if (!aEvent) { + NS_ERROR("aEvent is null. This should never happen."); + return NS_ERROR_NULL_POINTER; + } + + NS_WARNING_ASSERTION( + !aTargetFrame || !aTargetFrame->GetContent() || + aTargetFrame->GetContent() == aTargetContent || + aTargetFrame->GetContent()->GetFlattenedTreeParent() == + aTargetContent || + aTargetFrame->IsGeneratedContentFrame(), + "aTargetFrame should be related with aTargetContent"); +#if DEBUG + if (aTargetFrame && aTargetFrame->IsGeneratedContentFrame()) { + nsCOMPtr<nsIContent> targetContent; + aTargetFrame->GetContentForEvent(aEvent, getter_AddRefs(targetContent)); + MOZ_ASSERT(aTargetContent == targetContent, + "Unexpected target for generated content frame!"); + } +#endif + + mCurrentTarget = aTargetFrame; + mCurrentTargetContent = nullptr; + + // Do not take account eMouseEnterIntoWidget/ExitFromWidget so that loading + // a page when user is not active doesn't change the state to active. + WidgetMouseEvent* mouseEvent = aEvent->AsMouseEvent(); + if (aEvent->IsTrusted() && + ((mouseEvent && mouseEvent->IsReal() && + IsMessageMouseUserActivity(mouseEvent->mMessage)) || + aEvent->mClass == eWheelEventClass || + aEvent->mClass == ePointerEventClass || + aEvent->mClass == eTouchEventClass || + aEvent->mClass == eKeyboardEventClass || + (aEvent->mClass == eDragEventClass && aEvent->mMessage == eDrop) || + IsMessageGamepadUserActivity(aEvent->mMessage))) { + if (gMouseOrKeyboardEventCounter == 0) { + nsCOMPtr<nsIObserverService> obs = + mozilla::services::GetObserverService(); + if (obs) { + obs->NotifyObservers(nullptr, "user-interaction-active", nullptr); + UpdateUserActivityTimer(); + } + } + ++gMouseOrKeyboardEventCounter; + + nsCOMPtr<nsINode> node = aTargetContent; + if (node && + ((aEvent->mMessage == eKeyUp && IsKeyboardEventUserActivity(aEvent)) || + aEvent->mMessage == eMouseUp || aEvent->mMessage == eWheel || + aEvent->mMessage == eTouchEnd || aEvent->mMessage == ePointerUp || + aEvent->mMessage == eDrop)) { + Document* doc = node->OwnerDoc(); + while (doc) { + doc->SetUserHasInteracted(); + doc = nsContentUtils::IsChildOfSameType(doc) + ? doc->GetInProcessParentDocument() + : nullptr; + } + } + } + + WheelTransaction::OnEvent(aEvent); + + // Focus events don't necessarily need a frame. + if (!mCurrentTarget && !aTargetContent) { + NS_ERROR("mCurrentTarget and aTargetContent are null"); + return NS_ERROR_NULL_POINTER; + } +#ifdef DEBUG + if (aEvent->HasDragEventMessage() && PointerLockManager::IsLocked()) { + NS_ASSERTION(PointerLockManager::IsLocked(), + "Pointer is locked. Drag events should be suppressed when " + "the pointer is locked."); + } +#endif + // Store last known screenPoint and clientPoint so pointer lock + // can use these values as constants. + if (aEvent->IsTrusted() && + ((mouseEvent && mouseEvent->IsReal()) || + aEvent->mClass == eWheelEventClass) && + !PointerLockManager::IsLocked()) { + // XXX Probably doesn't matter much, but storing these in CSS pixels instead + // of device pixels means behavior can be a bit odd if you zoom while + // pointer-locked. + sLastScreenPoint = + Event::GetScreenCoords(aPresContext, aEvent, aEvent->mRefPoint) + .extract(); + sLastClientPoint = Event::GetClientCoords( + aPresContext, aEvent, aEvent->mRefPoint, CSSIntPoint(0, 0)); + } + + *aStatus = nsEventStatus_eIgnore; + + if (aEvent->mClass == eQueryContentEventClass) { + HandleQueryContentEvent(aEvent->AsQueryContentEvent()); + return NS_OK; + } + + WidgetTouchEvent* touchEvent = aEvent->AsTouchEvent(); + if (touchEvent && mInTouchDrag) { + if (touchEvent->mMessage == eTouchMove) { + GenerateDragGesture(aPresContext, touchEvent); + } else { + mInTouchDrag = false; + StopTrackingDragGesture(true); + } + } + + switch (aEvent->mMessage) { + case eContextMenu: + if (PointerLockManager::IsLocked()) { + return NS_ERROR_DOM_INVALID_STATE_ERR; + } + break; + case eMouseTouchDrag: + mInTouchDrag = true; + BeginTrackingDragGesture(aPresContext, mouseEvent, aTargetFrame); + break; + case eMouseDown: { + switch (mouseEvent->mButton) { + case MouseButton::ePrimary: + BeginTrackingDragGesture(aPresContext, mouseEvent, aTargetFrame); + mLClickCount = mouseEvent->mClickCount; + SetClickCount(mouseEvent, aStatus); + sNormalLMouseEventInProcess = true; + break; + case MouseButton::eMiddle: + mMClickCount = mouseEvent->mClickCount; + SetClickCount(mouseEvent, aStatus); + break; + case MouseButton::eSecondary: + mRClickCount = mouseEvent->mClickCount; + SetClickCount(mouseEvent, aStatus); + break; + } + NotifyTargetUserActivation(aEvent, aTargetContent); + break; + } + case eMouseUp: { + switch (mouseEvent->mButton) { + case MouseButton::ePrimary: + if (StaticPrefs::ui_click_hold_context_menus()) { + KillClickHoldTimer(); + } + mInTouchDrag = false; + StopTrackingDragGesture(true); + sNormalLMouseEventInProcess = false; + // then fall through... + [[fallthrough]]; + case MouseButton::eSecondary: + case MouseButton::eMiddle: + RefPtr<EventStateManager> esm = + ESMFromContentOrThis(aOverrideClickTarget); + esm->SetClickCount(mouseEvent, aStatus, aOverrideClickTarget); + break; + } + break; + } + case eMouseEnterIntoWidget: + PointerEventHandler::UpdateActivePointerState(mouseEvent, aTargetContent); + // In some cases on e10s eMouseEnterIntoWidget + // event was sent twice into child process of content. + // (From specific widget code (sending is not permanent) and + // from ESM::DispatchMouseOrPointerEvent (sending is permanent)). + // IsCrossProcessForwardingStopped() helps to suppress sending accidental + // event from widget code. + aEvent->StopCrossProcessForwarding(); + break; + case eMouseExitFromWidget: + // If this is a remote frame, we receive eMouseExitFromWidget from the + // parent the mouse exits our content. Since the parent may update the + // cursor while the mouse is outside our frame, and since PuppetWidget + // caches the current cursor internally, re-entering our content (say from + // over a window edge) wont update the cursor if the cached value and the + // current cursor match. So when the mouse exits a remote frame, clear the + // cached widget cursor so a proper update will occur when the mouse + // re-enters. + if (XRE_IsContentProcess()) { + ClearCachedWidgetCursor(mCurrentTarget); + } + + // IsCrossProcessForwardingStopped() helps to suppress double event + // sending into process of content. For more information see comment + // above, at eMouseEnterIntoWidget case. + aEvent->StopCrossProcessForwarding(); + + // If the event is not a top-level window or puppet widget exit, then it's + // not really an exit --- we may have traversed widget boundaries but + // we're still in our toplevel window or puppet widget. + if (mouseEvent->mExitFrom.value() != + WidgetMouseEvent::ePlatformTopLevel && + mouseEvent->mExitFrom.value() != WidgetMouseEvent::ePuppet) { + // Treat it as a synthetic move so we don't generate spurious + // "exit" or "move" events. Any necessary "out" or "over" events + // will be generated by GenerateMouseEnterExit + mouseEvent->mMessage = eMouseMove; + mouseEvent->mReason = WidgetMouseEvent::eSynthesized; + // then fall through... + } else { + MOZ_ASSERT_IF(XRE_IsParentProcess(), + mouseEvent->mExitFrom.value() == + WidgetMouseEvent::ePlatformTopLevel); + MOZ_ASSERT_IF(XRE_IsContentProcess(), mouseEvent->mExitFrom.value() == + WidgetMouseEvent::ePuppet); + // We should synthetize corresponding pointer events + GeneratePointerEnterExit(ePointerLeave, mouseEvent); + GenerateMouseEnterExit(mouseEvent); + // This is really an exit and should stop here + aEvent->mMessage = eVoidEvent; + break; + } + [[fallthrough]]; + case eMouseMove: + case ePointerDown: + if (aEvent->mMessage == ePointerDown) { + PointerEventHandler::UpdateActivePointerState(mouseEvent, + aTargetContent); + PointerEventHandler::ImplicitlyCapturePointer(aTargetFrame, aEvent); + if (mouseEvent->mInputSource != MouseEvent_Binding::MOZ_SOURCE_TOUCH) { + NotifyTargetUserActivation(aEvent, aTargetContent); + } + } + [[fallthrough]]; + case ePointerMove: { + if (!mInTouchDrag && + PointerEventHandler::IsDragAndDropEnabled(*mouseEvent)) { + GenerateDragGesture(aPresContext, mouseEvent); + } + // on the Mac, GenerateDragGesture() may not return until the drag + // has completed and so |aTargetFrame| may have been deleted (moving + // a bookmark, for example). If this is the case, however, we know + // that ClearFrameRefs() has been called and it cleared out + // |mCurrentTarget|. As a result, we should pass |mCurrentTarget| + // into UpdateCursor(). + UpdateCursor(aPresContext, aEvent, mCurrentTarget, aStatus); + + UpdateLastRefPointOfMouseEvent(mouseEvent); + if (PointerLockManager::IsLocked()) { + ResetPointerToWindowCenterWhilePointerLocked(mouseEvent); + } + UpdateLastPointerPosition(mouseEvent); + + GenerateMouseEnterExit(mouseEvent); + // Flush pending layout changes, so that later mouse move events + // will go to the right nodes. + FlushLayout(aPresContext); + break; + } + case ePointerGotCapture: + GenerateMouseEnterExit(mouseEvent); + break; + case eDragStart: + if (StaticPrefs::ui_click_hold_context_menus()) { + // an external drag gesture event came in, not generated internally + // by Gecko. Make sure we get rid of the click-hold timer. + KillClickHoldTimer(); + } + break; + case eDragOver: { + WidgetDragEvent* dragEvent = aEvent->AsDragEvent(); + MOZ_ASSERT(dragEvent); + if (dragEvent->mFlags.mIsSynthesizedForTests) { + dragEvent->InitDropEffectForTests(); + } + // Send the enter/exit events before eDrop. + GenerateDragDropEnterExit(aPresContext, dragEvent); + break; + } + case eDrop: + if (aEvent->mFlags.mIsSynthesizedForTests) { + MOZ_ASSERT(aEvent->AsDragEvent()); + aEvent->AsDragEvent()->InitDropEffectForTests(); + } + break; + + case eKeyPress: { + WidgetKeyboardEvent* keyEvent = aEvent->AsKeyboardEvent(); + if (keyEvent->ModifiersMatchWithAccessKey(AccessKeyType::eChrome) || + keyEvent->ModifiersMatchWithAccessKey(AccessKeyType::eContent)) { + // If the eKeyPress event will be sent to a remote process, this + // process needs to wait reply from the remote process for checking if + // preceding eKeyDown event is consumed. If preceding eKeyDown event + // is consumed in the remote process, BrowserChild won't send the event + // back to this process. So, only when this process receives a reply + // eKeyPress event in BrowserParent, we should handle accesskey in this + // process. + if (IsTopLevelRemoteTarget(GetFocusedElement())) { + // However, if there is no accesskey target for the key combination, + // we don't need to wait reply from the remote process. Otherwise, + // Mark the event as waiting reply from remote process and stop + // propagation in this process. + if (CheckIfEventMatchesAccessKey(keyEvent, aPresContext)) { + keyEvent->StopPropagation(); + keyEvent->MarkAsWaitingReplyFromRemoteProcess(); + } + } + // If the event target is in this process, we can handle accesskey now + // since if preceding eKeyDown event was consumed, eKeyPress event + // won't be dispatched by widget. So, coming eKeyPress event means + // that the preceding eKeyDown event wasn't consumed in this case. + else { + AutoTArray<uint32_t, 10> accessCharCodes; + keyEvent->GetAccessKeyCandidates(accessCharCodes); + + if (HandleAccessKey(keyEvent, aPresContext, accessCharCodes)) { + *aStatus = nsEventStatus_eConsumeNoDefault; + } + } + } + } + // then fall through... + [[fallthrough]]; + case eKeyDown: + if (aEvent->mMessage == eKeyDown) { + NotifyTargetUserActivation(aEvent, aTargetContent); + } + [[fallthrough]]; + case eKeyUp: { + Element* element = GetFocusedElement(); + if (element) { + mCurrentTargetContent = element; + } + + // NOTE: Don't refer TextComposition::IsComposing() since UI Events + // defines that KeyboardEvent.isComposing is true when it's + // dispatched after compositionstart and compositionend. + // TextComposition::IsComposing() is false even before + // compositionend if there is no composing string. + // And also don't expose other document's composition state. + // A native IME context is typically shared by multiple documents. + // So, don't use GetTextCompositionFor(nsIWidget*) here. + RefPtr<TextComposition> composition = + IMEStateManager::GetTextCompositionFor(aPresContext); + aEvent->AsKeyboardEvent()->mIsComposing = !!composition; + + // Widget may need to perform default action for specific keyboard + // event if it's not consumed. In this case, widget has already marked + // the event as "waiting reply from remote process". However, we need + // to reset it if the target (focused content) isn't in a remote process + // because PresShell needs to check if it's marked as so before + // dispatching events into the DOM tree. + if (aEvent->IsWaitingReplyFromRemoteProcess() && + !aEvent->PropagationStopped() && !IsTopLevelRemoteTarget(element)) { + aEvent->ResetWaitingReplyFromRemoteProcessState(); + } + } break; + case eWheel: + case eWheelOperationStart: + case eWheelOperationEnd: { + NS_ASSERTION(aEvent->IsTrusted(), + "Untrusted wheel event shouldn't be here"); + using DeltaModeCheckingState = WidgetWheelEvent::DeltaModeCheckingState; + + if (Element* element = GetFocusedElement()) { + mCurrentTargetContent = element; + } + + if (aEvent->mMessage != eWheel) { + break; + } + + WidgetWheelEvent* wheelEvent = aEvent->AsWheelEvent(); + WheelPrefs::GetInstance()->ApplyUserPrefsToDelta(wheelEvent); + + // If we won't dispatch a DOM event for this event, nothing to do anymore. + if (!wheelEvent->IsAllowedToDispatchDOMEvent()) { + break; + } + + if (StaticPrefs::dom_event_wheel_deltaMode_lines_always_disabled()) { + wheelEvent->mDeltaModeCheckingState = DeltaModeCheckingState::Unchecked; + } else if (ShouldAlwaysUseLineDeltas()) { + wheelEvent->mDeltaModeCheckingState = DeltaModeCheckingState::Checked; + } else { + wheelEvent->mDeltaModeCheckingState = DeltaModeCheckingState::Unknown; + } + + // Init lineOrPageDelta values for line scroll events for some devices + // on some platforms which might dispatch wheel events which don't + // have lineOrPageDelta values. And also, if delta values are + // customized by prefs, this recomputes them. + DeltaAccumulator::GetInstance()->InitLineOrPageDelta(aTargetFrame, this, + wheelEvent); + } break; + case eSetSelection: { + RefPtr<Element> focuedElement = GetFocusedElement(); + IMEStateManager::HandleSelectionEvent(aPresContext, focuedElement, + aEvent->AsSelectionEvent()); + break; + } + case eContentCommandCut: + case eContentCommandCopy: + case eContentCommandPaste: + case eContentCommandDelete: + case eContentCommandUndo: + case eContentCommandRedo: + case eContentCommandPasteTransferable: + case eContentCommandLookUpDictionary: + DoContentCommandEvent(aEvent->AsContentCommandEvent()); + break; + case eContentCommandInsertText: + DoContentCommandInsertTextEvent(aEvent->AsContentCommandEvent()); + break; + case eContentCommandScroll: + DoContentCommandScrollEvent(aEvent->AsContentCommandEvent()); + break; + case eCompositionStart: + if (aEvent->IsTrusted()) { + // If the event is trusted event, set the selected text to data of + // composition event. + WidgetCompositionEvent* compositionEvent = aEvent->AsCompositionEvent(); + WidgetQueryContentEvent querySelectedTextEvent( + true, eQuerySelectedText, compositionEvent->mWidget); + HandleQueryContentEvent(&querySelectedTextEvent); + if (querySelectedTextEvent.FoundSelection()) { + compositionEvent->mData = querySelectedTextEvent.mReply->DataRef(); + } + NS_ASSERTION(querySelectedTextEvent.Succeeded(), + "Failed to get selected text"); + } + break; + case eTouchStart: + SetGestureDownPoint(aEvent->AsTouchEvent()); + break; + case eTouchEnd: + NotifyTargetUserActivation(aEvent, aTargetContent); + break; + default: + break; + } + return NS_OK; +} + +void EventStateManager::NotifyTargetUserActivation(WidgetEvent* aEvent, + nsIContent* aTargetContent) { + if (!aEvent->IsTrusted()) { + return; + } + + WidgetMouseEvent* mouseEvent = aEvent->AsMouseEvent(); + if (mouseEvent && !mouseEvent->IsReal()) { + return; + } + + nsCOMPtr<nsINode> node = aTargetContent; + if (!node) { + return; + } + + Document* doc = node->OwnerDoc(); + if (!doc) { + return; + } + + // Don't gesture activate for key events for keys which are likely + // to be interaction with the browser, OS. + WidgetKeyboardEvent* keyEvent = aEvent->AsKeyboardEvent(); + if (keyEvent && !keyEvent->CanUserGestureActivateTarget()) { + return; + } + + // Touch gestures that end outside the drag target were touches that turned + // into scroll/pan/swipe actions. We don't want to gesture activate on such + // actions, we want to only gesture activate on touches that are taps. + // That is, touches that end in roughly the same place that they started. + if (aEvent->mMessage == eTouchEnd && aEvent->AsTouchEvent() && + IsEventOutsideDragThreshold(aEvent->AsTouchEvent())) { + return; + } + + // Do not treat the click on scrollbar as a user interaction with the web + // content. + if (StaticPrefs::dom_user_activation_ignore_scrollbars() && + (aEvent->mMessage == eMouseDown || aEvent->mMessage == ePointerDown) && + aTargetContent->IsInNativeAnonymousSubtree()) { + nsIContent* current = aTargetContent; + do { + nsIContent* root = current->GetClosestNativeAnonymousSubtreeRoot(); + if (!root) { + break; + } + if (root->IsXULElement(nsGkAtoms::scrollbar)) { + return; + } + current = root->GetParent(); + } while (current); + } + + MOZ_ASSERT(aEvent->mMessage == eKeyDown || aEvent->mMessage == eMouseDown || + aEvent->mMessage == ePointerDown || aEvent->mMessage == eTouchEnd); + doc->NotifyUserGestureActivation(); +} + +already_AddRefed<EventStateManager> EventStateManager::ESMFromContentOrThis( + nsIContent* aContent) { + if (aContent) { + PresShell* presShell = aContent->OwnerDoc()->GetPresShell(); + if (presShell) { + nsPresContext* prescontext = presShell->GetPresContext(); + if (prescontext) { + RefPtr<EventStateManager> esm = prescontext->EventStateManager(); + if (esm) { + return esm.forget(); + } + } + } + } + + RefPtr<EventStateManager> esm = this; + return esm.forget(); +} + +void EventStateManager::HandleQueryContentEvent( + WidgetQueryContentEvent* aEvent) { + switch (aEvent->mMessage) { + case eQuerySelectedText: + case eQueryTextContent: + case eQueryCaretRect: + case eQueryTextRect: + case eQueryEditorRect: + if (!IsTargetCrossProcess(aEvent)) { + break; + } + // Will not be handled locally, remote the event + GetCrossProcessTarget()->HandleQueryContentEvent(*aEvent); + return; + // Following events have not been supported in e10s mode yet. + case eQueryContentState: + case eQuerySelectionAsTransferable: + case eQueryCharacterAtPoint: + case eQueryDOMWidgetHittest: + case eQueryTextRectArray: + break; + default: + return; + } + + // If there is an IMEContentObserver, we need to handle QueryContentEvent + // with it. + if (mIMEContentObserver) { + RefPtr<IMEContentObserver> contentObserver = mIMEContentObserver; + contentObserver->HandleQueryContentEvent(aEvent); + return; + } + + ContentEventHandler handler(mPresContext); + handler.HandleQueryContentEvent(aEvent); +} + +static AccessKeyType GetAccessKeyTypeFor(nsISupports* aDocShell) { + nsCOMPtr<nsIDocShellTreeItem> treeItem(do_QueryInterface(aDocShell)); + if (!treeItem) { + return AccessKeyType::eNone; + } + + switch (treeItem->ItemType()) { + case nsIDocShellTreeItem::typeChrome: + return AccessKeyType::eChrome; + case nsIDocShellTreeItem::typeContent: + return AccessKeyType::eContent; + default: + return AccessKeyType::eNone; + } +} + +static bool IsAccessKeyTarget(Element* aElement, nsAString& aKey) { + // Use GetAttr because we want Unicode case=insensitive matching + // XXXbz shouldn't this be case-sensitive, per spec? + nsString contentKey; + if (!aElement || + !aElement->GetAttr(kNameSpaceID_None, nsGkAtoms::accesskey, contentKey) || + !contentKey.Equals(aKey, nsCaseInsensitiveStringComparator)) { + return false; + } + + if (!aElement->IsXULElement()) { + return true; + } + + // For XUL we do visibility checks. + nsIFrame* frame = aElement->GetPrimaryFrame(); + if (!frame) { + return false; + } + + if (frame->IsFocusable()) { + return true; + } + + if (!frame->IsVisibleConsideringAncestors()) { + return false; + } + + // XUL controls can be activated. + nsCOMPtr<nsIDOMXULControlElement> control = aElement->AsXULControl(); + if (control) { + return true; + } + + // XUL label elements are never focusable, so we need to check for them + // explicitly before giving up. + if (aElement->IsXULElement(nsGkAtoms::label)) { + return true; + } + + return false; +} + +bool EventStateManager::CheckIfEventMatchesAccessKey( + WidgetKeyboardEvent* aEvent, nsPresContext* aPresContext) { + AutoTArray<uint32_t, 10> accessCharCodes; + aEvent->GetAccessKeyCandidates(accessCharCodes); + return WalkESMTreeToHandleAccessKey(aEvent, aPresContext, accessCharCodes, + nullptr, eAccessKeyProcessingNormal, + false); +} + +bool EventStateManager::LookForAccessKeyAndExecute( + nsTArray<uint32_t>& aAccessCharCodes, bool aIsTrustedEvent, bool aIsRepeat, + bool aExecute) { + int32_t count, start = -1; + if (Element* focusedElement = GetFocusedElement()) { + start = mAccessKeys.IndexOf(focusedElement); + if (start == -1 && focusedElement->IsInNativeAnonymousSubtree()) { + start = mAccessKeys.IndexOf(Element::FromNodeOrNull( + focusedElement->GetClosestNativeAnonymousSubtreeRootParentOrHost())); + } + } + RefPtr<Element> element; + int32_t length = mAccessKeys.Count(); + for (uint32_t i = 0; i < aAccessCharCodes.Length(); ++i) { + uint32_t ch = aAccessCharCodes[i]; + nsAutoString accessKey; + AppendUCS4ToUTF16(ch, accessKey); + for (count = 1; count <= length; ++count) { + // mAccessKeys always stores Element instances. + MOZ_DIAGNOSTIC_ASSERT(length == mAccessKeys.Count()); + element = mAccessKeys[(start + count) % length]; + if (IsAccessKeyTarget(element, accessKey)) { + if (!aExecute) { + return true; + } + Document* doc = element->OwnerDoc(); + const bool shouldActivate = [&] { + if (!StaticPrefs::accessibility_accesskeycausesactivation()) { + return false; + } + if (aIsRepeat && nsContentUtils::IsChromeDoc(doc)) { + return false; + } + + // XXXedgar, Bug 1700646, maybe we could use other data structure to + // make searching target with same accesskey easier, and current setup + // could not ensure we cycle the target with tree order. + int32_t j = 0; + while (++j < length) { + Element* el = mAccessKeys[(start + count + j) % length]; + if (IsAccessKeyTarget(el, accessKey)) { + return false; + } + } + return true; + }(); + + // TODO(bug 1641171): This shouldn't be needed if we considered the + // accesskey combination properly. + if (aIsTrustedEvent) { + doc->NotifyUserGestureActivation(); + } + + auto result = + element->PerformAccesskey(shouldActivate, aIsTrustedEvent); + if (result.isOk()) { + if (result.unwrap() && aIsTrustedEvent) { + // If this is a child process, inform the parent that we want the + // focus, but pass false since we don't want to change the window + // order. + nsIDocShell* docShell = mPresContext->GetDocShell(); + nsCOMPtr<nsIBrowserChild> child = + docShell ? docShell->GetBrowserChild() : nullptr; + if (child) { + child->SendRequestFocus(false, CallerType::System); + } + } + return true; + } + } + } + } + return false; +} + +// static +void EventStateManager::GetAccessKeyLabelPrefix(Element* aElement, + nsAString& aPrefix) { + aPrefix.Truncate(); + nsAutoString separator, modifierText; + nsContentUtils::GetModifierSeparatorText(separator); + + AccessKeyType accessKeyType = + GetAccessKeyTypeFor(aElement->OwnerDoc()->GetDocShell()); + if (accessKeyType == AccessKeyType::eNone) { + return; + } + Modifiers modifiers = WidgetKeyboardEvent::AccessKeyModifiers(accessKeyType); + if (modifiers == MODIFIER_NONE) { + return; + } + + if (modifiers & MODIFIER_CONTROL) { + nsContentUtils::GetControlText(modifierText); + aPrefix.Append(modifierText + separator); + } + if (modifiers & MODIFIER_META) { + nsContentUtils::GetMetaText(modifierText); + aPrefix.Append(modifierText + separator); + } + if (modifiers & MODIFIER_OS) { + nsContentUtils::GetOSText(modifierText); + aPrefix.Append(modifierText + separator); + } + if (modifiers & MODIFIER_ALT) { + nsContentUtils::GetAltText(modifierText); + aPrefix.Append(modifierText + separator); + } + if (modifiers & MODIFIER_SHIFT) { + nsContentUtils::GetShiftText(modifierText); + aPrefix.Append(modifierText + separator); + } +} + +struct MOZ_STACK_CLASS AccessKeyInfo { + WidgetKeyboardEvent* event; + nsTArray<uint32_t>& charCodes; + + AccessKeyInfo(WidgetKeyboardEvent* aEvent, nsTArray<uint32_t>& aCharCodes) + : event(aEvent), charCodes(aCharCodes) {} +}; + +bool EventStateManager::WalkESMTreeToHandleAccessKey( + WidgetKeyboardEvent* aEvent, nsPresContext* aPresContext, + nsTArray<uint32_t>& aAccessCharCodes, nsIDocShellTreeItem* aBubbledFrom, + ProcessingAccessKeyState aAccessKeyState, bool aExecute) { + EnsureDocument(mPresContext); + nsCOMPtr<nsIDocShell> docShell = aPresContext->GetDocShell(); + if (NS_WARN_IF(!docShell) || NS_WARN_IF(!mDocument)) { + return false; + } + AccessKeyType accessKeyType = GetAccessKeyTypeFor(docShell); + if (accessKeyType == AccessKeyType::eNone) { + return false; + } + // Alt or other accesskey modifier is down, we may need to do an accesskey. + if (mAccessKeys.Count() > 0 && + aEvent->ModifiersMatchWithAccessKey(accessKeyType)) { + // Someone registered an accesskey. Find and activate it. + if (LookForAccessKeyAndExecute(aAccessCharCodes, aEvent->IsTrusted(), + aEvent->mIsRepeat, aExecute)) { + return true; + } + } + + int32_t childCount; + docShell->GetInProcessChildCount(&childCount); + for (int32_t counter = 0; counter < childCount; counter++) { + // Not processing the child which bubbles up the handling + nsCOMPtr<nsIDocShellTreeItem> subShellItem; + docShell->GetInProcessChildAt(counter, getter_AddRefs(subShellItem)); + if (aAccessKeyState == eAccessKeyProcessingUp && + subShellItem == aBubbledFrom) { + continue; + } + + nsCOMPtr<nsIDocShell> subDS = do_QueryInterface(subShellItem); + if (subDS && IsShellVisible(subDS)) { + // Guarantee subPresShell lifetime while we're handling access key + // since somebody may assume that it won't be deleted before the + // corresponding nsPresContext and EventStateManager. + RefPtr<PresShell> subPresShell = subDS->GetPresShell(); + + // Docshells need not have a presshell (eg. display:none + // iframes, docshells in transition between documents, etc). + if (!subPresShell) { + // Oh, well. Just move on to the next child + continue; + } + + RefPtr<nsPresContext> subPresContext = subPresShell->GetPresContext(); + + RefPtr<EventStateManager> esm = + static_cast<EventStateManager*>(subPresContext->EventStateManager()); + + if (esm && esm->WalkESMTreeToHandleAccessKey( + aEvent, subPresContext, aAccessCharCodes, nullptr, + eAccessKeyProcessingDown, aExecute)) { + return true; + } + } + } // if end . checking all sub docshell ends here. + + // bubble up the process to the parent docshell if necessary + if (eAccessKeyProcessingDown != aAccessKeyState) { + nsCOMPtr<nsIDocShellTreeItem> parentShellItem; + docShell->GetInProcessParent(getter_AddRefs(parentShellItem)); + nsCOMPtr<nsIDocShell> parentDS = do_QueryInterface(parentShellItem); + if (parentDS) { + // Guarantee parentPresShell lifetime while we're handling access key + // since somebody may assume that it won't be deleted before the + // corresponding nsPresContext and EventStateManager. + RefPtr<PresShell> parentPresShell = parentDS->GetPresShell(); + NS_ASSERTION(parentPresShell, + "Our PresShell exists but the parent's does not?"); + + RefPtr<nsPresContext> parentPresContext = + parentPresShell->GetPresContext(); + NS_ASSERTION(parentPresContext, "PresShell without PresContext"); + + RefPtr<EventStateManager> esm = static_cast<EventStateManager*>( + parentPresContext->EventStateManager()); + if (esm && esm->WalkESMTreeToHandleAccessKey( + aEvent, parentPresContext, aAccessCharCodes, docShell, + eAccessKeyProcessingDown, aExecute)) { + return true; + } + } + } // if end. bubble up process + + // If the content access key modifier is pressed, try remote children + if (aExecute && + aEvent->ModifiersMatchWithAccessKey(AccessKeyType::eContent) && + mDocument && mDocument->GetWindow()) { + // If the focus is currently on a node with a BrowserParent, the key event + // should've gotten forwarded to the child process and HandleAccessKey + // called from there. + if (BrowserParent::GetFrom(GetFocusedElement())) { + // If access key may be only in remote contents, this method won't handle + // access key synchronously. In this case, only reply event should reach + // here. + MOZ_ASSERT(aEvent->IsHandledInRemoteProcess() || + !aEvent->IsWaitingReplyFromRemoteProcess()); + } + // If focus is somewhere else, then we need to check the remote children. + // However, if the event has already been handled in a remote process, + // then, focus is moved from the remote process after posting the event. + // In such case, we shouldn't retry to handle access keys in remote + // processes. + else if (!aEvent->IsHandledInRemoteProcess()) { + AccessKeyInfo accessKeyInfo(aEvent, aAccessCharCodes); + nsContentUtils::CallOnAllRemoteChildren( + mDocument->GetWindow(), + [&accessKeyInfo](BrowserParent* aBrowserParent) -> CallState { + // Only forward accesskeys for the active tab. + if (aBrowserParent->GetDocShellIsActive()) { + // Even if there is no target for the accesskey in this process, + // the event may match with a content accesskey. If so, the + // keyboard event should be handled with reply event for + // preventing double action. (e.g., Alt+Shift+F on Windows may + // focus a content in remote and open "File" menu.) + accessKeyInfo.event->StopPropagation(); + accessKeyInfo.event->MarkAsWaitingReplyFromRemoteProcess(); + aBrowserParent->HandleAccessKey(*accessKeyInfo.event, + accessKeyInfo.charCodes); + return CallState::Stop; + } + + return CallState::Continue; + }); + } + } + + return false; +} // end of HandleAccessKey + +static BrowserParent* GetBrowserParentAncestor(BrowserParent* aBrowserParent) { + MOZ_ASSERT(aBrowserParent); + + BrowserBridgeParent* bbp = aBrowserParent->GetBrowserBridgeParent(); + if (!bbp) { + return nullptr; + } + + return bbp->Manager(); +} + +static void DispatchCrossProcessMouseExitEvents(WidgetMouseEvent* aMouseEvent, + BrowserParent* aRemoteTarget, + BrowserParent* aStopAncestor, + bool aIsReallyExit) { + MOZ_ASSERT(aMouseEvent); + MOZ_ASSERT(aRemoteTarget); + MOZ_ASSERT(aRemoteTarget != aStopAncestor); + MOZ_ASSERT_IF(aStopAncestor, nsContentUtils::GetCommonBrowserParentAncestor( + aRemoteTarget, aStopAncestor)); + + while (aRemoteTarget != aStopAncestor) { + UniquePtr<WidgetMouseEvent> mouseExitEvent = + CreateMouseOrPointerWidgetEvent(aMouseEvent, eMouseExitFromWidget, + aMouseEvent->mRelatedTarget); + mouseExitEvent->mExitFrom = + Some(aIsReallyExit ? WidgetMouseEvent::ePuppet + : WidgetMouseEvent::ePuppetParentToPuppetChild); + aRemoteTarget->SendRealMouseEvent(*mouseExitEvent); + + aRemoteTarget = GetBrowserParentAncestor(aRemoteTarget); + } +} + +void EventStateManager::DispatchCrossProcessEvent(WidgetEvent* aEvent, + BrowserParent* aRemoteTarget, + nsEventStatus* aStatus) { + MOZ_ASSERT(aEvent); + MOZ_ASSERT(aRemoteTarget); + MOZ_ASSERT(aStatus); + + BrowserParent* remote = aRemoteTarget; + + WidgetMouseEvent* mouseEvent = aEvent->AsMouseEvent(); + bool isContextMenuKey = mouseEvent && mouseEvent->IsContextMenuKeyEvent(); + if (aEvent->mClass == eKeyboardEventClass || isContextMenuKey) { + // APZ attaches a LayersId to hit-testable events, for keyboard events, + // we use focus. + BrowserParent* preciseRemote = BrowserParent::GetFocused(); + if (preciseRemote) { + remote = preciseRemote; + } + // else there is a race between layout and focus tracking, + // so fall back to delivering the event to the topmost child process. + } else if (aEvent->mLayersId.IsValid()) { + BrowserParent* preciseRemote = + BrowserParent::GetBrowserParentFromLayersId(aEvent->mLayersId); + if (preciseRemote) { + remote = preciseRemote; + } + // else there is a race between APZ and the LayersId to BrowserParent + // mapping, so fall back to delivering the event to the topmost child + // process. + } + + switch (aEvent->mClass) { + case eMouseEventClass: { + BrowserParent* oldRemote = BrowserParent::GetLastMouseRemoteTarget(); + + // If this is a eMouseExitFromWidget event, need to redirect the event to + // the last remote and and notify all its ancestors about the exit, if + // any. + if (mouseEvent->mMessage == eMouseExitFromWidget) { + MOZ_ASSERT(mouseEvent->mExitFrom.value() == WidgetMouseEvent::ePuppet); + MOZ_ASSERT(mouseEvent->mReason == WidgetMouseEvent::eReal); + MOZ_ASSERT(!mouseEvent->mLayersId.IsValid()); + MOZ_ASSERT(remote->GetBrowserHost()); + + if (oldRemote && oldRemote != remote) { + Unused << NS_WARN_IF(nsContentUtils::GetCommonBrowserParentAncestor( + remote, oldRemote) != remote); + remote = oldRemote; + } + + DispatchCrossProcessMouseExitEvents(mouseEvent, remote, nullptr, true); + return; + } + + if (BrowserParent* pointerLockedRemote = + PointerLockManager::GetLockedRemoteTarget()) { + remote = pointerLockedRemote; + } else if (BrowserParent* pointerCapturedRemote = + PointerEventHandler::GetPointerCapturingRemoteTarget( + mouseEvent->pointerId)) { + remote = pointerCapturedRemote; + } else if (BrowserParent* capturingRemote = + PresShell::GetCapturingRemoteTarget()) { + remote = capturingRemote; + } + + // If a mouse is over a remote target A, and then moves to + // remote target B, we'd deliver the event directly to remote target B + // after the moving, A would never get notified that the mouse left. + // So we generate a exit event to notify A after the move. + // XXXedgar, if the synthesized mouse events could deliver to the correct + // process directly (see + // https://bugzilla.mozilla.org/show_bug.cgi?id=1549355), we probably + // don't need to check mReason then. + if (mouseEvent->mReason == WidgetMouseEvent::eReal && + remote != oldRemote) { + MOZ_ASSERT(mouseEvent->mMessage != eMouseExitFromWidget); + if (oldRemote) { + BrowserParent* commonAncestor = + nsContentUtils::GetCommonBrowserParentAncestor(remote, oldRemote); + if (commonAncestor == oldRemote) { + // Mouse moves to the inner OOP frame, it is not a really exit. + DispatchCrossProcessMouseExitEvents( + mouseEvent, GetBrowserParentAncestor(remote), + GetBrowserParentAncestor(commonAncestor), false); + } else if (commonAncestor == remote) { + // Mouse moves to the outer OOP frame, it is a really exit. + DispatchCrossProcessMouseExitEvents(mouseEvent, oldRemote, + commonAncestor, true); + } else { + // Mouse moves to OOP frame in other subtree, it is a really exit, + // need to notify all its ancestors before common ancestor about the + // exit. + DispatchCrossProcessMouseExitEvents(mouseEvent, oldRemote, + commonAncestor, true); + if (commonAncestor) { + UniquePtr<WidgetMouseEvent> mouseExitEvent = + CreateMouseOrPointerWidgetEvent(mouseEvent, + eMouseExitFromWidget, + mouseEvent->mRelatedTarget); + mouseExitEvent->mExitFrom = + Some(WidgetMouseEvent::ePuppetParentToPuppetChild); + commonAncestor->SendRealMouseEvent(*mouseExitEvent); + } + } + } + + if (mouseEvent->mMessage != eMouseExitFromWidget && + mouseEvent->mMessage != eMouseEnterIntoWidget) { + // This is to make cursor would be updated correctly. + remote->MouseEnterIntoWidget(); + } + } + + remote->SendRealMouseEvent(*mouseEvent); + return; + } + case eKeyboardEventClass: { + auto* keyboardEvent = aEvent->AsKeyboardEvent(); + if (aEvent->mMessage == eKeyUp) { + HandleKeyUpInteraction(keyboardEvent); + } + remote->SendRealKeyEvent(*keyboardEvent); + return; + } + case eWheelEventClass: { + if (BrowserParent* pointerLockedRemote = + PointerLockManager::GetLockedRemoteTarget()) { + remote = pointerLockedRemote; + } + remote->SendMouseWheelEvent(*aEvent->AsWheelEvent()); + return; + } + case eTouchEventClass: { + // Let the child process synthesize a mouse event if needed, and + // ensure we don't synthesize one in this process. + *aStatus = nsEventStatus_eConsumeNoDefault; + remote->SendRealTouchEvent(*aEvent->AsTouchEvent()); + return; + } + case eDragEventClass: { + RefPtr<BrowserParent> browserParent = remote; + browserParent->Manager()->MaybeInvokeDragSession(browserParent); + + nsCOMPtr<nsIDragSession> dragSession = nsContentUtils::GetDragSession(); + uint32_t dropEffect = nsIDragService::DRAGDROP_ACTION_NONE; + uint32_t action = nsIDragService::DRAGDROP_ACTION_NONE; + nsCOMPtr<nsIPrincipal> principal; + nsCOMPtr<nsIContentSecurityPolicy> csp; + + if (dragSession) { + dragSession->DragEventDispatchedToChildProcess(); + dragSession->GetDragAction(&action); + dragSession->GetTriggeringPrincipal(getter_AddRefs(principal)); + dragSession->GetCsp(getter_AddRefs(csp)); + RefPtr<DataTransfer> initialDataTransfer = + dragSession->GetDataTransfer(); + if (initialDataTransfer) { + dropEffect = initialDataTransfer->DropEffectInt(); + } + } + + browserParent->SendRealDragEvent(*aEvent->AsDragEvent(), action, + dropEffect, principal, csp); + return; + } + default: { + MOZ_CRASH("Attempt to send non-whitelisted event?"); + } + } +} + +bool EventStateManager::IsRemoteTarget(nsIContent* target) { + return BrowserParent::GetFrom(target) || BrowserBridgeChild::GetFrom(target); +} + +bool EventStateManager::IsTopLevelRemoteTarget(nsIContent* target) { + return !!BrowserParent::GetFrom(target); +} + +bool EventStateManager::HandleCrossProcessEvent(WidgetEvent* aEvent, + nsEventStatus* aStatus) { + if (!aEvent->CanBeSentToRemoteProcess()) { + return false; + } + + MOZ_ASSERT(!aEvent->HasBeenPostedToRemoteProcess(), + "Why do we need to post same event to remote processes again?"); + + // Collect the remote event targets we're going to forward this + // event to. + // + // NB: the elements of |remoteTargets| must be unique, for correctness. + AutoTArray<RefPtr<BrowserParent>, 1> remoteTargets; + if (aEvent->mClass != eTouchEventClass || aEvent->mMessage == eTouchStart) { + // If this event only has one target, and it's remote, add it to + // the array. + nsIFrame* frame = aEvent->mMessage == eDragExit + ? sLastDragOverFrame.GetFrame() + : GetEventTarget(); + nsIContent* target = frame ? frame->GetContent() : nullptr; + if (BrowserParent* remoteTarget = BrowserParent::GetFrom(target)) { + remoteTargets.AppendElement(remoteTarget); + } + } else { + // This is a touch event with possibly multiple touch points. + // Each touch point may have its own target. So iterate through + // all of them and collect the unique set of targets for event + // forwarding. + // + // This loop is similar to the one used in + // PresShell::DispatchTouchEvent(). + const WidgetTouchEvent::TouchArray& touches = + aEvent->AsTouchEvent()->mTouches; + for (uint32_t i = 0; i < touches.Length(); ++i) { + Touch* touch = touches[i]; + // NB: the |mChanged| check is an optimization, subprocesses can + // compute this for themselves. If the touch hasn't changed, we + // may be able to avoid forwarding the event entirely (which is + // not free). + if (!touch || !touch->mChanged) { + continue; + } + nsCOMPtr<EventTarget> targetPtr = touch->mTarget; + if (!targetPtr) { + continue; + } + nsCOMPtr<nsIContent> target = do_QueryInterface(targetPtr); + BrowserParent* remoteTarget = BrowserParent::GetFrom(target); + if (remoteTarget && !remoteTargets.Contains(remoteTarget)) { + remoteTargets.AppendElement(remoteTarget); + } + } + } + + if (remoteTargets.Length() == 0) { + return false; + } + + // Dispatch the event to the remote target. + for (uint32_t i = 0; i < remoteTargets.Length(); ++i) { + DispatchCrossProcessEvent(aEvent, remoteTargets[i], aStatus); + } + return aEvent->HasBeenPostedToRemoteProcess(); +} + +// +// CreateClickHoldTimer +// +// Fire off a timer for determining if the user wants click-hold. This timer +// is a one-shot that will be cancelled when the user moves enough to fire +// a drag. +// +void EventStateManager::CreateClickHoldTimer(nsPresContext* inPresContext, + nsIFrame* inDownFrame, + WidgetGUIEvent* inMouseDownEvent) { + if (!inMouseDownEvent->IsTrusted() || + IsTopLevelRemoteTarget(mGestureDownContent) || + PointerLockManager::IsLocked()) { + return; + } + + // just to be anal (er, safe) + if (mClickHoldTimer) { + mClickHoldTimer->Cancel(); + mClickHoldTimer = nullptr; + } + + // if content clicked on has a popup, don't even start the timer + // since we'll end up conflicting and both will show. + if (mGestureDownContent && + nsContentUtils::HasNonEmptyAttr(mGestureDownContent, kNameSpaceID_None, + nsGkAtoms::popup)) { + return; + } + + int32_t clickHoldDelay = StaticPrefs::ui_click_hold_context_menus_delay(); + NS_NewTimerWithFuncCallback( + getter_AddRefs(mClickHoldTimer), sClickHoldCallback, this, clickHoldDelay, + nsITimer::TYPE_ONE_SHOT, "EventStateManager::CreateClickHoldTimer"); +} // CreateClickHoldTimer + +// +// KillClickHoldTimer +// +// Stop the timer that would show the context menu dead in its tracks +// +void EventStateManager::KillClickHoldTimer() { + if (mClickHoldTimer) { + mClickHoldTimer->Cancel(); + mClickHoldTimer = nullptr; + } +} + +// +// sClickHoldCallback +// +// This fires after the mouse has been down for a certain length of time. +// +void EventStateManager::sClickHoldCallback(nsITimer* aTimer, void* aESM) { + RefPtr<EventStateManager> self = static_cast<EventStateManager*>(aESM); + if (self) { + self->FireContextClick(); + } + + // NOTE: |aTimer| and |self->mAutoHideTimer| are invalid after calling + // ClosePopup(); + +} // sAutoHideCallback + +// +// FireContextClick +// +// If we're this far, our timer has fired, which means the mouse has been down +// for a certain period of time and has not moved enough to generate a +// dragGesture. We can be certain the user wants a context-click at this stage, +// so generate a dom event and fire it in. +// +// After the event fires, check if PreventDefault() has been set on the event +// which means that someone either ate the event or put up a context menu. This +// is our cue to stop tracking the drag gesture. If we always did this, +// draggable items w/out a context menu wouldn't be draggable after a certain +// length of time, which is _not_ what we want. +// +void EventStateManager::FireContextClick() { + if (!mGestureDownContent || !mPresContext || PointerLockManager::IsLocked()) { + return; + } + +#ifdef XP_MACOSX + // Hack to ensure that we don't show a context menu when the user + // let go of the mouse after a long cpu-hogging operation prevented + // us from handling any OS events. See bug 117589. + if (!CGEventSourceButtonState(kCGEventSourceStateCombinedSessionState, + kCGMouseButtonLeft)) + return; +#endif + + nsEventStatus status = nsEventStatus_eIgnore; + + // Dispatch to the DOM. We have to fake out the ESM and tell it that the + // current target frame is actually where the mouseDown occurred, otherwise it + // will use the frame the mouse is currently over which may or may not be + // the same. (Note: saari and I have decided that we don't have to reset + // |mCurrentTarget| when we're through because no one else is doing anything + // more with this event and it will get reset on the very next event to the + // correct frame). + mCurrentTarget = mPresContext->GetPrimaryFrameFor(mGestureDownContent); + // make sure the widget sticks around + nsCOMPtr<nsIWidget> targetWidget; + if (mCurrentTarget && (targetWidget = mCurrentTarget->GetNearestWidget())) { + NS_ASSERTION( + mPresContext == mCurrentTarget->PresContext(), + "a prescontext returned a primary frame that didn't belong to it?"); + + // before dispatching, check that we're not on something that + // doesn't get a context menu + bool allowedToDispatch = true; + + if (mGestureDownContent->IsAnyOfXULElements(nsGkAtoms::scrollbar, + nsGkAtoms::scrollbarbutton, + nsGkAtoms::button)) { + allowedToDispatch = false; + } else if (mGestureDownContent->IsXULElement(nsGkAtoms::toolbarbutton)) { + // a <toolbarbutton> that has the container attribute set + // will already have its own dropdown. + if (nsContentUtils::HasNonEmptyAttr( + mGestureDownContent, kNameSpaceID_None, nsGkAtoms::container)) { + allowedToDispatch = false; + } else { + // If the toolbar button has an open menu, don't attempt to open + // a second menu + if (mGestureDownContent->IsElement() && + mGestureDownContent->AsElement()->AttrValueIs( + kNameSpaceID_None, nsGkAtoms::open, nsGkAtoms::_true, + eCaseMatters)) { + allowedToDispatch = false; + } + } + } else if (mGestureDownContent->IsHTMLElement()) { + nsCOMPtr<nsIFormControl> formCtrl(do_QueryInterface(mGestureDownContent)); + + if (formCtrl) { + allowedToDispatch = + formCtrl->IsTextControl(/*aExcludePassword*/ false) || + formCtrl->ControlType() == FormControlType::InputFile; + } else if (mGestureDownContent->IsAnyOfHTMLElements( + nsGkAtoms::embed, nsGkAtoms::object, nsGkAtoms::label)) { + allowedToDispatch = false; + } + } + + if (allowedToDispatch) { + // init the event while mCurrentTarget is still good + WidgetMouseEvent event(true, eContextMenu, targetWidget, + WidgetMouseEvent::eReal); + event.mClickCount = 1; + FillInEventFromGestureDown(&event); + + // stop selection tracking, we're in control now + if (mCurrentTarget) { + RefPtr<nsFrameSelection> frameSel = mCurrentTarget->GetFrameSelection(); + + if (frameSel && frameSel->GetDragState()) { + // note that this can cause selection changed events to fire if we're + // in a text field, which will null out mCurrentTarget + frameSel->SetDragState(false); + } + } + + AutoHandlingUserInputStatePusher userInpStatePusher(true, &event); + + // dispatch to DOM + RefPtr<nsIContent> gestureDownContent = mGestureDownContent; + RefPtr<nsPresContext> presContext = mPresContext; + EventDispatcher::Dispatch(gestureDownContent, presContext, &event, + nullptr, &status); + + // We don't need to dispatch to frame handling because no frames + // watch eContextMenu except for nsMenuFrame and that's only for + // dismissal. That's just as well since we don't really know + // which frame to send it to. + } + } + + // now check if the event has been handled. If so, stop tracking a drag + if (status == nsEventStatus_eConsumeNoDefault) { + StopTrackingDragGesture(true); + } + + KillClickHoldTimer(); + +} // FireContextClick + +// +// BeginTrackingDragGesture +// +// Record that the mouse has gone down and that we should move to TRACKING state +// of d&d gesture tracker. +// +// We also use this to track click-hold context menus. When the mouse goes down, +// fire off a short timer. If the timer goes off and we have yet to fire the +// drag gesture (ie, the mouse hasn't moved a certain distance), then we can +// assume the user wants a click-hold, so fire a context-click event. We only +// want to cancel the drag gesture if the context-click event is handled. +// +void EventStateManager::BeginTrackingDragGesture(nsPresContext* aPresContext, + WidgetMouseEvent* inDownEvent, + nsIFrame* inDownFrame) { + if (!inDownEvent->mWidget) { + return; + } + + // Note that |inDownEvent| could be either a mouse down event or a + // synthesized mouse move event. + SetGestureDownPoint(inDownEvent); + + if (inDownFrame) { + inDownFrame->GetContentForEvent(inDownEvent, + getter_AddRefs(mGestureDownContent)); + + mGestureDownFrameOwner = inDownFrame->GetContent(); + if (!mGestureDownFrameOwner) { + mGestureDownFrameOwner = mGestureDownContent; + } + } + mGestureModifiers = inDownEvent->mModifiers; + mGestureDownButtons = inDownEvent->mButtons; + + if (inDownEvent->mMessage != eMouseTouchDrag && + StaticPrefs::ui_click_hold_context_menus()) { + // fire off a timer to track click-hold + CreateClickHoldTimer(aPresContext, inDownFrame, inDownEvent); + } +} + +void EventStateManager::SetGestureDownPoint(WidgetGUIEvent* aEvent) { + mGestureDownPoint = + GetEventRefPoint(aEvent) + aEvent->mWidget->WidgetToScreenOffset(); +} + +LayoutDeviceIntPoint EventStateManager::GetEventRefPoint( + WidgetEvent* aEvent) const { + auto touchEvent = aEvent->AsTouchEvent(); + return (touchEvent && !touchEvent->mTouches.IsEmpty()) + ? aEvent->AsTouchEvent()->mTouches[0]->mRefPoint + : aEvent->mRefPoint; +} + +void EventStateManager::BeginTrackingRemoteDragGesture( + nsIContent* aContent, RemoteDragStartData* aDragStartData) { + mGestureDownContent = aContent; + mGestureDownFrameOwner = aContent; + mGestureDownInTextControl = + aContent && aContent->IsInNativeAnonymousSubtree() && + TextControlElement::FromNodeOrNull( + aContent->GetClosestNativeAnonymousSubtreeRootParentOrHost()); + mGestureDownDragStartData = aDragStartData; +} + +// +// StopTrackingDragGesture +// +// Record that the mouse has gone back up so that we should leave the TRACKING +// state of d&d gesture tracker and return to the START state. +// +void EventStateManager::StopTrackingDragGesture(bool aClearInChildProcesses) { + mGestureDownContent = nullptr; + mGestureDownFrameOwner = nullptr; + mGestureDownInTextControl = false; + mGestureDownDragStartData = nullptr; + + // If a content process starts a drag but the mouse is released before the + // parent starts the actual drag, the content process will think a drag is + // still happening. Inform any child processes with active drags that the drag + // should be stopped. + if (aClearInChildProcesses) { + nsCOMPtr<nsIDragService> dragService = + do_GetService("@mozilla.org/widget/dragservice;1"); + if (dragService) { + nsCOMPtr<nsIDragSession> dragSession; + dragService->GetCurrentSession(getter_AddRefs(dragSession)); + if (!dragSession) { + // Only notify if there isn't a drag session active. + dragService->RemoveAllChildProcesses(); + } + } + } +} + +void EventStateManager::FillInEventFromGestureDown(WidgetMouseEvent* aEvent) { + NS_ASSERTION(aEvent->mWidget == mCurrentTarget->GetNearestWidget(), + "Incorrect widget in event"); + + // Set the coordinates in the new event to the coordinates of + // the old event, adjusted for the fact that the widget might be + // different + aEvent->mRefPoint = + mGestureDownPoint - aEvent->mWidget->WidgetToScreenOffset(); + aEvent->mModifiers = mGestureModifiers; + aEvent->mButtons = mGestureDownButtons; +} + +void EventStateManager::MaybeFirePointerCancel(WidgetInputEvent* aEvent) { + RefPtr<PresShell> presShell = mPresContext->GetPresShell(); + AutoWeakFrame targetFrame = mCurrentTarget; + + if (!presShell || !targetFrame) { + return; + } + + nsCOMPtr<nsIContent> content; + targetFrame->GetContentForEvent(aEvent, getter_AddRefs(content)); + if (!content) { + return; + } + + nsEventStatus status = nsEventStatus_eIgnore; + + if (WidgetMouseEvent* aMouseEvent = aEvent->AsMouseEvent()) { + WidgetPointerEvent event(*aMouseEvent); + PointerEventHandler::InitPointerEventFromMouse(&event, aMouseEvent, + ePointerCancel); + + event.convertToPointer = false; + presShell->HandleEventWithTarget(&event, targetFrame, content, &status); + } else if (WidgetTouchEvent* aTouchEvent = aEvent->AsTouchEvent()) { + WidgetPointerEvent event(aTouchEvent->IsTrusted(), ePointerCancel, + aTouchEvent->mWidget); + + PointerEventHandler::InitPointerEventFromTouch( + event, *aTouchEvent, *aTouchEvent->mTouches[0], true); + + event.convertToPointer = false; + presShell->HandleEventWithTarget(&event, targetFrame, content, &status); + } else { + MOZ_ASSERT(false); + } + + // HandleEventWithTarget clears out mCurrentTarget, which may be used in the + // caller GenerateDragGesture. We have to restore mCurrentTarget. + mCurrentTarget = targetFrame; +} + +bool EventStateManager::IsEventOutsideDragThreshold( + WidgetInputEvent* aEvent) const { + static int32_t sPixelThresholdX = 0; + static int32_t sPixelThresholdY = 0; + + if (!sPixelThresholdX) { + sPixelThresholdX = + LookAndFeel::GetInt(LookAndFeel::IntID::DragThresholdX, 0); + sPixelThresholdY = + LookAndFeel::GetInt(LookAndFeel::IntID::DragThresholdY, 0); + if (sPixelThresholdX <= 0) { + sPixelThresholdX = 5; + } + if (sPixelThresholdY <= 0) { + sPixelThresholdY = 5; + } + } + + LayoutDeviceIntPoint pt = + aEvent->mWidget->WidgetToScreenOffset() + GetEventRefPoint(aEvent); + LayoutDeviceIntPoint distance = pt - mGestureDownPoint; + return Abs(distance.x) > sPixelThresholdX || + Abs(distance.y) > sPixelThresholdY; +} + +// +// GenerateDragGesture +// +// If we're in the TRACKING state of the d&d gesture tracker, check the current +// position of the mouse in relation to the old one. If we've moved a sufficient +// amount from the mouse down, then fire off a drag gesture event. +void EventStateManager::GenerateDragGesture(nsPresContext* aPresContext, + WidgetInputEvent* aEvent) { + NS_ASSERTION(aPresContext, "This shouldn't happen."); + if (!IsTrackingDragGesture()) { + return; + } + + AutoWeakFrame targetFrameBefore = mCurrentTarget; + auto autoRestore = MakeScopeExit([&] { mCurrentTarget = targetFrameBefore; }); + mCurrentTarget = mGestureDownFrameOwner->GetPrimaryFrame(); + + if (!mCurrentTarget || !mCurrentTarget->GetNearestWidget()) { + StopTrackingDragGesture(true); + return; + } + + // Check if selection is tracking drag gestures, if so + // don't interfere! + if (mCurrentTarget) { + RefPtr<nsFrameSelection> frameSel = mCurrentTarget->GetFrameSelection(); + if (frameSel && frameSel->GetDragState()) { + StopTrackingDragGesture(true); + return; + } + } + + // If non-native code is capturing the mouse don't start a drag. + if (PresShell::IsMouseCapturePreventingDrag()) { + StopTrackingDragGesture(true); + return; + } + + if (!IsEventOutsideDragThreshold(aEvent)) { + // To keep the old behavior, flush layout even if we don't start dnd. + FlushLayout(aPresContext); + return; + } + + if (StaticPrefs::ui_click_hold_context_menus()) { + // stop the click-hold before we fire off the drag gesture, in case + // it takes a long time + KillClickHoldTimer(); + } + + nsCOMPtr<nsIDocShell> docshell = aPresContext->GetDocShell(); + if (!docshell) { + return; + } + + nsCOMPtr<nsPIDOMWindowOuter> window = docshell->GetWindow(); + if (!window) return; + + RefPtr<DataTransfer> dataTransfer = + new DataTransfer(window, eDragStart, false, -1); + auto protectDataTransfer = MakeScopeExit([&] { + if (dataTransfer) { + dataTransfer->Disconnect(); + } + }); + + RefPtr<Selection> selection; + RefPtr<RemoteDragStartData> remoteDragStartData; + nsCOMPtr<nsIContent> eventContent, targetContent; + nsCOMPtr<nsIPrincipal> principal; + nsCOMPtr<nsIContentSecurityPolicy> csp; + nsCOMPtr<nsICookieJarSettings> cookieJarSettings; + bool allowEmptyDataTransfer = false; + mCurrentTarget->GetContentForEvent(aEvent, getter_AddRefs(eventContent)); + if (eventContent) { + // If the content is a text node in a password field, we shouldn't + // allow to drag its raw text. Note that we've supported drag from + // password fields but dragging data was masked text. So, it doesn't + // make sense anyway. + if (eventContent->IsText() && eventContent->HasFlag(NS_MAYBE_MASKED)) { + // However, it makes sense to allow to drag selected password text + // when copying selected password is allowed because users may want + // to use drag and drop rather than copy and paste when web apps + // request to input password twice for conforming new password but + // they used password generator. + TextEditor* textEditor = + nsContentUtils::GetTextEditorFromAnonymousNodeWithoutCreation( + eventContent); + if (!textEditor || !textEditor->IsCopyToClipboardAllowed()) { + StopTrackingDragGesture(true); + return; + } + } + DetermineDragTargetAndDefaultData( + window, eventContent, dataTransfer, &allowEmptyDataTransfer, + getter_AddRefs(selection), getter_AddRefs(remoteDragStartData), + getter_AddRefs(targetContent), getter_AddRefs(principal), + getter_AddRefs(csp), getter_AddRefs(cookieJarSettings)); + } + + // Stop tracking the drag gesture now. This should stop us from + // reentering GenerateDragGesture inside DOM event processing. + // Pass false to avoid clearing the child process state since a real + // drag should be starting. + StopTrackingDragGesture(false); + + if (!targetContent) return; + + // Use our targetContent, now that we've determined it, as the + // parent object of the DataTransfer. + nsCOMPtr<nsIContent> parentContent = + targetContent->FindFirstNonChromeOnlyAccessContent(); + dataTransfer->SetParentObject(parentContent); + + sLastDragOverFrame = nullptr; + nsCOMPtr<nsIWidget> widget = mCurrentTarget->GetNearestWidget(); + + // get the widget from the target frame + WidgetDragEvent startEvent(aEvent->IsTrusted(), eDragStart, widget); + startEvent.mFlags.mIsSynthesizedForTests = + aEvent->mFlags.mIsSynthesizedForTests; + FillInEventFromGestureDown(&startEvent); + + startEvent.mDataTransfer = dataTransfer; + if (aEvent->AsMouseEvent()) { + startEvent.mInputSource = aEvent->AsMouseEvent()->mInputSource; + } else if (aEvent->AsTouchEvent()) { + startEvent.mInputSource = MouseEvent_Binding::MOZ_SOURCE_TOUCH; + } else { + MOZ_ASSERT(false); + } + + // Dispatch to the DOM. By setting mCurrentTarget we are faking + // out the ESM and telling it that the current target frame is + // actually where the mouseDown occurred, otherwise it will use + // the frame the mouse is currently over which may or may not be + // the same. + + // Hold onto old target content through the event and reset after. + nsCOMPtr<nsIContent> targetBeforeEvent = mCurrentTargetContent; + + // Set the current target to the content for the mouse down + mCurrentTargetContent = targetContent; + + // Dispatch the dragstart event to the DOM. + nsEventStatus status = nsEventStatus_eIgnore; + EventDispatcher::Dispatch(targetContent, aPresContext, &startEvent, nullptr, + &status); + + WidgetDragEvent* event = &startEvent; + + nsCOMPtr<nsIObserverService> observerService = + mozilla::services::GetObserverService(); + // Emit observer event to allow addons to modify the DataTransfer + // object. + if (observerService) { + observerService->NotifyObservers(dataTransfer, "on-datatransfer-available", + nullptr); + } + + if (status != nsEventStatus_eConsumeNoDefault) { + bool dragStarted = DoDefaultDragStart(aPresContext, event, dataTransfer, + allowEmptyDataTransfer, targetContent, + selection, remoteDragStartData, + principal, csp, cookieJarSettings); + if (dragStarted) { + sActiveESM = nullptr; + MaybeFirePointerCancel(aEvent); + aEvent->StopPropagation(); + } + } + + // Reset mCurretTargetContent to what it was + mCurrentTargetContent = targetBeforeEvent; + + // Now flush all pending notifications, for better responsiveness + // while dragging. + FlushLayout(aPresContext); +} // GenerateDragGesture + +void EventStateManager::DetermineDragTargetAndDefaultData( + nsPIDOMWindowOuter* aWindow, nsIContent* aSelectionTarget, + DataTransfer* aDataTransfer, bool* aAllowEmptyDataTransfer, + Selection** aSelection, RemoteDragStartData** aRemoteDragStartData, + nsIContent** aTargetNode, nsIPrincipal** aPrincipal, + nsIContentSecurityPolicy** aCsp, + nsICookieJarSettings** aCookieJarSettings) { + *aTargetNode = nullptr; + *aAllowEmptyDataTransfer = false; + nsCOMPtr<nsIContent> dragDataNode; + + nsIContent* editingElement = aSelectionTarget->IsEditable() + ? aSelectionTarget->GetEditingHost() + : nullptr; + + // In chrome, only allow dragging inside editable areas. + bool isChromeContext = !aWindow->GetBrowsingContext()->IsContent(); + if (isChromeContext && !editingElement) { + if (mGestureDownDragStartData) { + // A child process started a drag so use any data it assigned for the dnd + // session. + mGestureDownDragStartData->AddInitialDnDDataTo(aDataTransfer, aPrincipal, + aCsp, aCookieJarSettings); + mGestureDownDragStartData.forget(aRemoteDragStartData); + *aAllowEmptyDataTransfer = true; + } + } else { + mGestureDownDragStartData = nullptr; + + // GetDragData determines if a selection, link or image in the content + // should be dragged, and places the data associated with the drag in the + // data transfer. + // mGestureDownContent is the node where the mousedown event for the drag + // occurred, and aSelectionTarget is the node to use when a selection is + // used + bool canDrag; + bool wasAlt = (mGestureModifiers & MODIFIER_ALT) != 0; + nsresult rv = nsContentAreaDragDrop::GetDragData( + aWindow, mGestureDownContent, aSelectionTarget, wasAlt, aDataTransfer, + &canDrag, aSelection, getter_AddRefs(dragDataNode), aPrincipal, aCsp, + aCookieJarSettings); + if (NS_FAILED(rv) || !canDrag) { + return; + } + } + + // if GetDragData returned a node, use that as the node being dragged. + // Otherwise, if a selection is being dragged, use the node within the + // selection that was dragged. Otherwise, just use the mousedown target. + nsIContent* dragContent = mGestureDownContent; + if (dragDataNode) + dragContent = dragDataNode; + else if (*aSelection) + dragContent = aSelectionTarget; + + nsIContent* originalDragContent = dragContent; + + // If a selection isn't being dragged, look for an ancestor with the + // draggable property set. If one is found, use that as the target of the + // drag instead of the node that was clicked on. If a draggable node wasn't + // found, just use the clicked node. + if (!*aSelection) { + while (dragContent) { + if (auto htmlElement = nsGenericHTMLElement::FromNode(dragContent)) { + if (htmlElement->Draggable()) { + // We let draggable elements to trigger dnd even if there is no data + // in the DataTransfer. + *aAllowEmptyDataTransfer = true; + break; + } + } else { + if (dragContent->IsXULElement()) { + // All XUL elements are draggable, so if a XUL element is + // encountered, stop looking for draggable nodes and just use the + // original clicked node instead. + // XXXndeakin + // In the future, we will want to improve this so that XUL has a + // better way to specify whether something is draggable than just + // on/off. + dragContent = mGestureDownContent; + break; + } + // otherwise, it's not an HTML or XUL element, so just keep looking + } + dragContent = dragContent->GetFlattenedTreeParent(); + } + } + + // if no node in the hierarchy was found to drag, but the GetDragData method + // returned a node, use that returned node. Otherwise, nothing is draggable. + if (!dragContent && dragDataNode) dragContent = dragDataNode; + + if (dragContent) { + // if an ancestor node was used instead, clear the drag data + // XXXndeakin rework this a bit. Find a way to just not call GetDragData if + // we don't need to. + if (dragContent != originalDragContent) aDataTransfer->ClearAll(); + *aTargetNode = dragContent; + NS_ADDREF(*aTargetNode); + } +} + +bool EventStateManager::DoDefaultDragStart( + nsPresContext* aPresContext, WidgetDragEvent* aDragEvent, + DataTransfer* aDataTransfer, bool aAllowEmptyDataTransfer, + nsIContent* aDragTarget, Selection* aSelection, + RemoteDragStartData* aDragStartData, nsIPrincipal* aPrincipal, + nsIContentSecurityPolicy* aCsp, nsICookieJarSettings* aCookieJarSettings) { + nsCOMPtr<nsIDragService> dragService = + do_GetService("@mozilla.org/widget/dragservice;1"); + if (!dragService) return false; + + // Default handling for the dragstart event. + // + // First, check if a drag session already exists. This means that the drag + // service was called directly within a draggesture handler. In this case, + // don't do anything more, as it is assumed that the handler is managing + // drag and drop manually. Make sure to return true to indicate that a drag + // began. However, if we're handling drag session for synthesized events, + // we need to initialize some information of the session. Therefore, we + // need to keep going for synthesized case. + nsCOMPtr<nsIDragSession> dragSession; + dragService->GetCurrentSession(getter_AddRefs(dragSession)); + if (dragSession && !dragSession->IsSynthesizedForTests()) { + return true; + } + + // No drag session is currently active, so check if a handler added + // any items to be dragged. If not, there isn't anything to drag. + uint32_t count = 0; + if (aDataTransfer) { + count = aDataTransfer->MozItemCount(); + } + if (!aAllowEmptyDataTransfer && !count) { + return false; + } + + // Get the target being dragged, which may not be the same as the + // target of the mouse event. If one wasn't set in the + // aDataTransfer during the event handler, just use the original + // target instead. + nsCOMPtr<nsIContent> dragTarget = aDataTransfer->GetDragTarget(); + if (!dragTarget) { + dragTarget = aDragTarget; + if (!dragTarget) { + return false; + } + } + + // check which drag effect should initially be used. If the effect was not + // set, just use all actions, otherwise Windows won't allow a drop. + uint32_t action = aDataTransfer->EffectAllowedInt(); + if (action == nsIDragService::DRAGDROP_ACTION_UNINITIALIZED) { + action = nsIDragService::DRAGDROP_ACTION_COPY | + nsIDragService::DRAGDROP_ACTION_MOVE | + nsIDragService::DRAGDROP_ACTION_LINK; + } + + // get any custom drag image that was set + int32_t imageX, imageY; + RefPtr<Element> dragImage = aDataTransfer->GetDragImage(&imageX, &imageY); + + nsCOMPtr<nsIArray> transArray = aDataTransfer->GetTransferables(dragTarget); + if (!transArray) { + return false; + } + + RefPtr<DataTransfer> dataTransfer; + if (!dragSession) { + // After this function returns, the DataTransfer will be cleared so it + // appears empty to content. We need to pass a DataTransfer into the Drag + // Session, so we need to make a copy. + aDataTransfer->Clone(aDragTarget, eDrop, aDataTransfer->MozUserCancelled(), + false, getter_AddRefs(dataTransfer)); + + // Copy over the drop effect, as Clone doesn't copy it for us. + dataTransfer->SetDropEffectInt(aDataTransfer->DropEffectInt()); + } else { + MOZ_ASSERT(dragSession->IsSynthesizedForTests()); + MOZ_ASSERT(aDragEvent->mFlags.mIsSynthesizedForTests); + // If we're initializing synthesized drag session, we should use given + // DataTransfer as is because it'll be used with following drag events + // in any tests, therefore it should be set to nsIDragSession.dataTransfer + // because it and DragEvent.dataTransfer should be same instance. + dataTransfer = aDataTransfer; + } + + // XXXndeakin don't really want to create a new drag DOM event + // here, but we need something to pass to the InvokeDragSession + // methods. + RefPtr<DragEvent> event = + NS_NewDOMDragEvent(dragTarget, aPresContext, aDragEvent); + + // Use InvokeDragSessionWithSelection if a selection is being dragged, + // such that the image can be generated from the selected text. However, + // use InvokeDragSessionWithImage if a custom image was set or something + // other than a selection is being dragged. + if (!dragImage && aSelection) { + dragService->InvokeDragSessionWithSelection(aSelection, aPrincipal, aCsp, + aCookieJarSettings, transArray, + action, event, dataTransfer); + } else if (aDragStartData) { + MOZ_ASSERT(XRE_IsParentProcess()); + dragService->InvokeDragSessionWithRemoteImage( + dragTarget, aPrincipal, aCsp, aCookieJarSettings, transArray, action, + aDragStartData, event, dataTransfer); + } else { + dragService->InvokeDragSessionWithImage( + dragTarget, aPrincipal, aCsp, aCookieJarSettings, transArray, action, + dragImage, imageX, imageY, event, dataTransfer); + } + + return true; +} + +void EventStateManager::ChangeZoom(bool aIncrease) { + // Send the zoom change to the top level browser so it will be handled by the + // front end in the same way as other zoom actions. + nsIDocShell* docShell = mDocument->GetDocShell(); + if (!docShell) { + return; + } + + BrowsingContext* bc = docShell->GetBrowsingContext(); + if (!bc) { + return; + } + + if (XRE_IsParentProcess()) { + bc->Canonical()->DispatchWheelZoomChange(aIncrease); + } else if (BrowserChild* child = BrowserChild::GetFrom(docShell)) { + child->SendWheelZoomChange(aIncrease); + } +} + +void EventStateManager::DoScrollHistory(int32_t direction) { + nsCOMPtr<nsISupports> pcContainer(mPresContext->GetContainerWeak()); + if (pcContainer) { + nsCOMPtr<nsIWebNavigation> webNav(do_QueryInterface(pcContainer)); + if (webNav) { + // positive direction to go back one step, nonpositive to go forward + // This is doing user-initiated history traversal, hence we want + // to require that history entries we navigate to have user interaction. + if (direction > 0) + webNav->GoBack(StaticPrefs::browser_navigation_requireUserInteraction(), + true); + else + webNav->GoForward( + StaticPrefs::browser_navigation_requireUserInteraction(), true); + } + } +} + +void EventStateManager::DoScrollZoom(nsIFrame* aTargetFrame, + int32_t adjustment) { + // Exclude content in chrome docshells. + nsIContent* content = aTargetFrame->GetContent(); + if (content && !nsContentUtils::IsInChromeDocshell(content->OwnerDoc())) { + // Positive adjustment to decrease zoom, negative to increase + const bool increase = adjustment <= 0; + EnsureDocument(mPresContext); + ChangeZoom(increase); + } +} + +static nsIFrame* GetParentFrameToScroll(nsIFrame* aFrame) { + if (!aFrame) return nullptr; + + if (aFrame->StyleDisplay()->mPosition == StylePositionProperty::Fixed && + nsLayoutUtils::IsReallyFixedPos(aFrame)) + return aFrame->PresContext()->GetPresShell()->GetRootScrollFrame(); + + return aFrame->GetParent(); +} + +void EventStateManager::DispatchLegacyMouseScrollEvents( + nsIFrame* aTargetFrame, WidgetWheelEvent* aEvent, nsEventStatus* aStatus) { + MOZ_ASSERT(aEvent); + MOZ_ASSERT(aStatus); + + if (!aTargetFrame || *aStatus == nsEventStatus_eConsumeNoDefault) { + return; + } + + // Ignore mouse wheel transaction for computing legacy mouse wheel + // events' delta value. + // DOM event's delta vales are computed from CSS pixels. + auto scrollAmountInCSSPixels = + CSSIntSize::FromAppUnitsRounded(aEvent->mScrollAmount); + + // XXX We don't deal with fractional amount in legacy event, though the + // default action handler (DoScrollText()) deals with it. + // If we implemented such strict computation, we would need additional + // accumulated delta values. It would made the code more complicated. + // And also it would computes different delta values from older version. + // It doesn't make sense to implement such code for legacy events and + // rare cases. + int32_t scrollDeltaX, scrollDeltaY, pixelDeltaX, pixelDeltaY; + switch (aEvent->mDeltaMode) { + case WheelEvent_Binding::DOM_DELTA_PAGE: + scrollDeltaX = !aEvent->mLineOrPageDeltaX + ? 0 + : (aEvent->mLineOrPageDeltaX > 0 + ? UIEvent_Binding::SCROLL_PAGE_DOWN + : UIEvent_Binding::SCROLL_PAGE_UP); + scrollDeltaY = !aEvent->mLineOrPageDeltaY + ? 0 + : (aEvent->mLineOrPageDeltaY > 0 + ? UIEvent_Binding::SCROLL_PAGE_DOWN + : UIEvent_Binding::SCROLL_PAGE_UP); + pixelDeltaX = RoundDown(aEvent->mDeltaX * scrollAmountInCSSPixels.width); + pixelDeltaY = RoundDown(aEvent->mDeltaY * scrollAmountInCSSPixels.height); + break; + + case WheelEvent_Binding::DOM_DELTA_LINE: + scrollDeltaX = aEvent->mLineOrPageDeltaX; + scrollDeltaY = aEvent->mLineOrPageDeltaY; + pixelDeltaX = RoundDown(aEvent->mDeltaX * scrollAmountInCSSPixels.width); + pixelDeltaY = RoundDown(aEvent->mDeltaY * scrollAmountInCSSPixels.height); + break; + + case WheelEvent_Binding::DOM_DELTA_PIXEL: + scrollDeltaX = aEvent->mLineOrPageDeltaX; + scrollDeltaY = aEvent->mLineOrPageDeltaY; + pixelDeltaX = RoundDown(aEvent->mDeltaX); + pixelDeltaY = RoundDown(aEvent->mDeltaY); + break; + + default: + MOZ_CRASH("Invalid deltaMode value comes"); + } + + // Send the legacy events in following order: + // 1. Vertical scroll + // 2. Vertical pixel scroll (even if #1 isn't consumed) + // 3. Horizontal scroll (even if #1 and/or #2 are consumed) + // 4. Horizontal pixel scroll (even if #3 isn't consumed) + + AutoWeakFrame targetFrame(aTargetFrame); + + MOZ_ASSERT(*aStatus != nsEventStatus_eConsumeNoDefault && + !aEvent->DefaultPrevented(), + "If you make legacy events dispatched for default prevented wheel " + "event, you need to initialize stateX and stateY"); + EventState stateX, stateY; + if (scrollDeltaY) { + SendLineScrollEvent(aTargetFrame, aEvent, stateY, scrollDeltaY, + DELTA_DIRECTION_Y); + if (!targetFrame.IsAlive()) { + *aStatus = nsEventStatus_eConsumeNoDefault; + return; + } + } + + if (pixelDeltaY) { + SendPixelScrollEvent(aTargetFrame, aEvent, stateY, pixelDeltaY, + DELTA_DIRECTION_Y); + if (!targetFrame.IsAlive()) { + *aStatus = nsEventStatus_eConsumeNoDefault; + return; + } + } + + if (scrollDeltaX) { + SendLineScrollEvent(aTargetFrame, aEvent, stateX, scrollDeltaX, + DELTA_DIRECTION_X); + if (!targetFrame.IsAlive()) { + *aStatus = nsEventStatus_eConsumeNoDefault; + return; + } + } + + if (pixelDeltaX) { + SendPixelScrollEvent(aTargetFrame, aEvent, stateX, pixelDeltaX, + DELTA_DIRECTION_X); + if (!targetFrame.IsAlive()) { + *aStatus = nsEventStatus_eConsumeNoDefault; + return; + } + } + + if (stateY.mDefaultPrevented) { + *aStatus = nsEventStatus_eConsumeNoDefault; + aEvent->PreventDefault(!stateY.mDefaultPreventedByContent); + } + + if (stateX.mDefaultPrevented) { + *aStatus = nsEventStatus_eConsumeNoDefault; + aEvent->PreventDefault(!stateX.mDefaultPreventedByContent); + } +} + +void EventStateManager::SendLineScrollEvent(nsIFrame* aTargetFrame, + WidgetWheelEvent* aEvent, + EventState& aState, int32_t aDelta, + DeltaDirection aDeltaDirection) { + nsCOMPtr<nsIContent> targetContent = aTargetFrame->GetContent(); + if (!targetContent) { + targetContent = GetFocusedElement(); + if (!targetContent) { + return; + } + } + + while (targetContent->IsText()) { + targetContent = targetContent->GetFlattenedTreeParent(); + } + + WidgetMouseScrollEvent event(aEvent->IsTrusted(), + eLegacyMouseLineOrPageScroll, aEvent->mWidget); + event.mFlags.mDefaultPrevented = aState.mDefaultPrevented; + event.mFlags.mDefaultPreventedByContent = aState.mDefaultPreventedByContent; + event.mRefPoint = aEvent->mRefPoint; + event.mTimeStamp = aEvent->mTimeStamp; + event.mModifiers = aEvent->mModifiers; + event.mButtons = aEvent->mButtons; + event.mIsHorizontal = (aDeltaDirection == DELTA_DIRECTION_X); + event.mDelta = aDelta; + event.mInputSource = aEvent->mInputSource; + + RefPtr<nsPresContext> presContext = aTargetFrame->PresContext(); + nsEventStatus status = nsEventStatus_eIgnore; + EventDispatcher::Dispatch(targetContent, presContext, &event, nullptr, + &status); + aState.mDefaultPrevented = + event.DefaultPrevented() || status == nsEventStatus_eConsumeNoDefault; + aState.mDefaultPreventedByContent = event.DefaultPreventedByContent(); +} + +void EventStateManager::SendPixelScrollEvent(nsIFrame* aTargetFrame, + WidgetWheelEvent* aEvent, + EventState& aState, + int32_t aPixelDelta, + DeltaDirection aDeltaDirection) { + nsCOMPtr<nsIContent> targetContent = aTargetFrame->GetContent(); + if (!targetContent) { + targetContent = GetFocusedElement(); + if (!targetContent) { + return; + } + } + + while (targetContent->IsText()) { + targetContent = targetContent->GetFlattenedTreeParent(); + } + + WidgetMouseScrollEvent event(aEvent->IsTrusted(), eLegacyMousePixelScroll, + aEvent->mWidget); + event.mFlags.mDefaultPrevented = aState.mDefaultPrevented; + event.mFlags.mDefaultPreventedByContent = aState.mDefaultPreventedByContent; + event.mRefPoint = aEvent->mRefPoint; + event.mTimeStamp = aEvent->mTimeStamp; + event.mModifiers = aEvent->mModifiers; + event.mButtons = aEvent->mButtons; + event.mIsHorizontal = (aDeltaDirection == DELTA_DIRECTION_X); + event.mDelta = aPixelDelta; + event.mInputSource = aEvent->mInputSource; + + RefPtr<nsPresContext> presContext = aTargetFrame->PresContext(); + nsEventStatus status = nsEventStatus_eIgnore; + EventDispatcher::Dispatch(targetContent, presContext, &event, nullptr, + &status); + aState.mDefaultPrevented = + event.DefaultPrevented() || status == nsEventStatus_eConsumeNoDefault; + aState.mDefaultPreventedByContent = event.DefaultPreventedByContent(); +} + +nsIFrame* EventStateManager::ComputeScrollTargetAndMayAdjustWheelEvent( + nsIFrame* aTargetFrame, WidgetWheelEvent* aEvent, + ComputeScrollTargetOptions aOptions) { + return ComputeScrollTargetAndMayAdjustWheelEvent( + aTargetFrame, aEvent->mDeltaX, aEvent->mDeltaY, aEvent, aOptions); +} + +// Overload ComputeScrollTargetAndMayAdjustWheelEvent method to allow passing +// "test" dx and dy when looking for which scrollbarmediators to activate when +// two finger down on trackpad and before any actual motion +nsIFrame* EventStateManager::ComputeScrollTargetAndMayAdjustWheelEvent( + nsIFrame* aTargetFrame, double aDirectionX, double aDirectionY, + WidgetWheelEvent* aEvent, ComputeScrollTargetOptions aOptions) { + bool isAutoDir = false; + bool honoursRoot = false; + if (MAY_BE_ADJUSTED_BY_AUTO_DIR & aOptions) { + // If the scroll is respected as auto-dir, aDirection* should always be + // equivalent to the event's delta vlaues(Currently, there are only one case + // where aDirection*s have different values from the widget wheel event's + // original delta values and the only case isn't auto-dir, see + // ScrollbarsForWheel::TemporarilyActivateAllPossibleScrollTargets). + MOZ_ASSERT(aDirectionX == aEvent->mDeltaX && + aDirectionY == aEvent->mDeltaY); + + WheelDeltaAdjustmentStrategy strategy = + GetWheelDeltaAdjustmentStrategy(*aEvent); + switch (strategy) { + case WheelDeltaAdjustmentStrategy::eAutoDir: + isAutoDir = true; + honoursRoot = false; + break; + case WheelDeltaAdjustmentStrategy::eAutoDirWithRootHonour: + isAutoDir = true; + honoursRoot = true; + break; + default: + break; + } + } + + if (aOptions & PREFER_MOUSE_WHEEL_TRANSACTION) { + // If the user recently scrolled with the mousewheel, then they probably + // want to scroll the same view as before instead of the view under the + // cursor. WheelTransaction tracks the frame currently being + // scrolled with the mousewheel. We consider the transaction ended when the + // mouse moves more than "mousewheel.transaction.ignoremovedelay" + // milliseconds after the last scroll operation, or any time the mouse moves + // out of the frame, or when more than "mousewheel.transaction.timeout" + // milliseconds have passed after the last operation, even if the mouse + // hasn't moved. + nsIFrame* lastScrollFrame = WheelTransaction::GetScrollTargetFrame(); + if (lastScrollFrame) { + nsIScrollableFrame* scrollableFrame = + lastScrollFrame->GetScrollTargetFrame(); + if (scrollableFrame) { + nsIFrame* frameToScroll = do_QueryFrame(scrollableFrame); + MOZ_ASSERT(frameToScroll); + if (isAutoDir) { + ESMAutoDirWheelDeltaAdjuster adjuster(*aEvent, *lastScrollFrame, + honoursRoot); + // Note that calling this function will not always cause the delta to + // be adjusted, it only adjusts the delta when it should, because + // Adjust() internally calls ShouldBeAdjusted() before making + // adjustment. + adjuster.Adjust(); + } + return frameToScroll; + } + } + } + + // If the event doesn't cause scroll actually, we cannot find scroll target + // because we check if the event can cause scroll actually on each found + // scrollable frame. + if (!aDirectionX && !aDirectionY) { + return nullptr; + } + + bool checkIfScrollableX; + bool checkIfScrollableY; + if (isAutoDir) { + // Always check the frame's scrollability in both the two directions for an + // auto-dir scroll. That is, for an auto-dir scroll, + // PREFER_ACTUAL_SCROLLABLE_TARGET_ALONG_X_AXIS and + // PREFER_ACTUAL_SCROLLABLE_TARGET_ALONG_Y_AXIS should be ignored. + checkIfScrollableX = true; + checkIfScrollableY = true; + } else { + checkIfScrollableX = + aDirectionX && + (aOptions & PREFER_ACTUAL_SCROLLABLE_TARGET_ALONG_X_AXIS); + checkIfScrollableY = + aDirectionY && + (aOptions & PREFER_ACTUAL_SCROLLABLE_TARGET_ALONG_Y_AXIS); + } + + nsIFrame* scrollFrame = !(aOptions & START_FROM_PARENT) + ? aTargetFrame + : GetParentFrameToScroll(aTargetFrame); + for (; scrollFrame; scrollFrame = GetParentFrameToScroll(scrollFrame)) { + // Check whether the frame wants to provide us with a scrollable view. + nsIScrollableFrame* scrollableFrame = scrollFrame->GetScrollTargetFrame(); + if (!scrollableFrame) { + nsMenuPopupFrame* menuPopupFrame = do_QueryFrame(scrollFrame); + if (menuPopupFrame) { + return nullptr; + } + continue; + } + + nsIFrame* frameToScroll = do_QueryFrame(scrollableFrame); + MOZ_ASSERT(frameToScroll); + + if (!checkIfScrollableX && !checkIfScrollableY) { + return frameToScroll; + } + + // If the frame disregards the direction the user is trying to scroll, then + // it should just bubbles the scroll event up to its parental scroll frame + + Maybe<layers::ScrollDirection> disregardedDirection = + WheelHandlingUtils::GetDisregardedWheelScrollDirection(scrollFrame); + if (disregardedDirection) { + switch (disregardedDirection.ref()) { + case layers::ScrollDirection::eHorizontal: + if (checkIfScrollableX) { + continue; + } + break; + case layers::ScrollDirection::eVertical: + if (checkIfScrollableY) { + continue; + } + break; + } + } + + layers::ScrollDirections directions = + scrollableFrame->GetAvailableScrollingDirectionsForUserInputEvents(); + if ((!(directions.contains(layers::ScrollDirection::eVertical)) && + !(directions.contains(layers::ScrollDirection::eHorizontal))) || + (checkIfScrollableY && !checkIfScrollableX && + !(directions.contains(layers::ScrollDirection::eVertical))) || + (checkIfScrollableX && !checkIfScrollableY && + !(directions.contains(layers::ScrollDirection::eHorizontal)))) { + continue; + } + + // Computes whether the currently checked frame is scrollable by this wheel + // event. + bool canScroll = false; + if (isAutoDir) { + ESMAutoDirWheelDeltaAdjuster adjuster(*aEvent, *scrollFrame, honoursRoot); + if (adjuster.ShouldBeAdjusted()) { + adjuster.Adjust(); + canScroll = true; + } else if (WheelHandlingUtils::CanScrollOn(scrollableFrame, aDirectionX, + aDirectionY)) { + canScroll = true; + } + } else if (WheelHandlingUtils::CanScrollOn(scrollableFrame, aDirectionX, + aDirectionY)) { + canScroll = true; + } + + if (canScroll) { + return frameToScroll; + } + + // Where we are at is the block ending in a for loop. + // The current frame has been checked to be unscrollable by this wheel + // event, continue the loop to check its parent, if any. + } + + nsIFrame* newFrame = nsLayoutUtils::GetCrossDocParentFrameInProcess( + aTargetFrame->PresShell()->GetRootFrame()); + aOptions = + static_cast<ComputeScrollTargetOptions>(aOptions & ~START_FROM_PARENT); + if (!newFrame) { + return nullptr; + } + return ComputeScrollTargetAndMayAdjustWheelEvent(newFrame, aEvent, aOptions); +} + +nsSize EventStateManager::GetScrollAmount( + nsPresContext* aPresContext, WidgetWheelEvent* aEvent, + nsIScrollableFrame* aScrollableFrame) { + MOZ_ASSERT(aPresContext); + MOZ_ASSERT(aEvent); + + const bool isPage = aEvent->mDeltaMode == WheelEvent_Binding::DOM_DELTA_PAGE; + if (!aScrollableFrame) { + // If there is no scrollable frame, we should use root, see below. + aScrollableFrame = + aPresContext->PresShell()->GetRootScrollFrameAsScrollable(); + } + + if (aScrollableFrame) { + return isPage ? aScrollableFrame->GetPageScrollAmount() + : aScrollableFrame->GetLineScrollAmount(); + } + + // If there is no scrollable frame and page scrolling, use viewport size. + if (isPage) { + return aPresContext->GetVisibleArea().Size(); + } + + // Otherwise use root frame's font metrics. + // + // FIXME(emilio): Should this use the root element's style frame? The root + // frame will always have the initial font. Then again it should never matter + // for content, we should always have a root scrollable frame in html + // documents. + nsIFrame* rootFrame = aPresContext->PresShell()->GetRootFrame(); + if (!rootFrame) { + return nsSize(0, 0); + } + RefPtr<nsFontMetrics> fm = + nsLayoutUtils::GetInflatedFontMetricsForFrame(rootFrame); + NS_ENSURE_TRUE(fm, nsSize(0, 0)); + return nsSize(fm->AveCharWidth(), fm->MaxHeight()); +} + +void EventStateManager::DoScrollText(nsIScrollableFrame* aScrollableFrame, + WidgetWheelEvent* aEvent) { + MOZ_ASSERT(aScrollableFrame); + MOZ_ASSERT(aEvent); + + nsIFrame* scrollFrame = do_QueryFrame(aScrollableFrame); + MOZ_ASSERT(scrollFrame); + + AutoWeakFrame scrollFrameWeak(scrollFrame); + AutoWeakFrame eventFrameWeak(mCurrentTarget); + if (!WheelTransaction::WillHandleDefaultAction(aEvent, scrollFrameWeak, + eventFrameWeak)) { + return; + } + + // Default action's actual scroll amount should be computed from device + // pixels. + nsPresContext* pc = scrollFrame->PresContext(); + nsSize scrollAmount = GetScrollAmount(pc, aEvent, aScrollableFrame); + nsIntSize scrollAmountInDevPixels( + pc->AppUnitsToDevPixels(scrollAmount.width), + pc->AppUnitsToDevPixels(scrollAmount.height)); + nsIntPoint actualDevPixelScrollAmount = + DeltaAccumulator::GetInstance()->ComputeScrollAmountForDefaultAction( + aEvent, scrollAmountInDevPixels); + + // Don't scroll around the axis whose overflow style is hidden. + ScrollStyles overflowStyle = aScrollableFrame->GetScrollStyles(); + if (overflowStyle.mHorizontal == StyleOverflow::Hidden) { + actualDevPixelScrollAmount.x = 0; + } + if (overflowStyle.mVertical == StyleOverflow::Hidden) { + actualDevPixelScrollAmount.y = 0; + } + + ScrollSnapFlags snapFlags = ScrollSnapFlags::Disabled; + mozilla::ScrollOrigin origin = mozilla::ScrollOrigin::NotSpecified; + switch (aEvent->mDeltaMode) { + case WheelEvent_Binding::DOM_DELTA_LINE: + origin = mozilla::ScrollOrigin::MouseWheel; + snapFlags = ScrollSnapFlags::IntendedDirection; + break; + case WheelEvent_Binding::DOM_DELTA_PAGE: + origin = mozilla::ScrollOrigin::Pages; + snapFlags = ScrollSnapFlags::IntendedDirection | + ScrollSnapFlags::IntendedEndPosition; + break; + case WheelEvent_Binding::DOM_DELTA_PIXEL: + origin = mozilla::ScrollOrigin::Pixels; + break; + default: + MOZ_CRASH("Invalid deltaMode value comes"); + } + + // We shouldn't scroll more one page at once except when over one page scroll + // is allowed for the event. + nsSize pageSize = aScrollableFrame->GetPageScrollAmount(); + nsIntSize devPixelPageSize(pc->AppUnitsToDevPixels(pageSize.width), + pc->AppUnitsToDevPixels(pageSize.height)); + if (!WheelPrefs::GetInstance()->IsOverOnePageScrollAllowedX(aEvent) && + DeprecatedAbs(actualDevPixelScrollAmount.x.value) > + devPixelPageSize.width) { + actualDevPixelScrollAmount.x = (actualDevPixelScrollAmount.x >= 0) + ? devPixelPageSize.width + : -devPixelPageSize.width; + } + + if (!WheelPrefs::GetInstance()->IsOverOnePageScrollAllowedY(aEvent) && + DeprecatedAbs(actualDevPixelScrollAmount.y.value) > + devPixelPageSize.height) { + actualDevPixelScrollAmount.y = (actualDevPixelScrollAmount.y >= 0) + ? devPixelPageSize.height + : -devPixelPageSize.height; + } + + bool isDeltaModePixel = + (aEvent->mDeltaMode == WheelEvent_Binding::DOM_DELTA_PIXEL); + + ScrollMode mode; + switch (aEvent->mScrollType) { + case WidgetWheelEvent::SCROLL_DEFAULT: + if (isDeltaModePixel) { + mode = ScrollMode::Normal; + } else if (aEvent->mFlags.mHandledByAPZ) { + mode = ScrollMode::SmoothMsd; + } else { + mode = ScrollMode::Smooth; + } + break; + case WidgetWheelEvent::SCROLL_SYNCHRONOUSLY: + mode = ScrollMode::Instant; + break; + case WidgetWheelEvent::SCROLL_ASYNCHRONOUSLY: + mode = ScrollMode::Normal; + break; + case WidgetWheelEvent::SCROLL_SMOOTHLY: + mode = ScrollMode::Smooth; + break; + default: + MOZ_CRASH("Invalid mScrollType value comes"); + } + + nsIScrollableFrame::ScrollMomentum momentum = + aEvent->mIsMomentum ? nsIScrollableFrame::SYNTHESIZED_MOMENTUM_EVENT + : nsIScrollableFrame::NOT_MOMENTUM; + + nsIntPoint overflow; + aScrollableFrame->ScrollBy(actualDevPixelScrollAmount, + ScrollUnit::DEVICE_PIXELS, mode, &overflow, origin, + momentum, snapFlags); + + if (!scrollFrameWeak.IsAlive()) { + // If the scroll causes changing the layout, we can think that the event + // has been completely consumed by the content. Then, users probably don't + // want additional action. + aEvent->mOverflowDeltaX = aEvent->mOverflowDeltaY = 0; + } else if (isDeltaModePixel) { + aEvent->mOverflowDeltaX = overflow.x; + aEvent->mOverflowDeltaY = overflow.y; + } else { + aEvent->mOverflowDeltaX = + static_cast<double>(overflow.x) / scrollAmountInDevPixels.width; + aEvent->mOverflowDeltaY = + static_cast<double>(overflow.y) / scrollAmountInDevPixels.height; + } + + // If CSS overflow properties caused not to scroll, the overflowDelta* values + // should be same as delta* values since they may be used as gesture event by + // widget. However, if there is another scrollable element in the ancestor + // along the axis, probably users don't want the operation to cause + // additional action such as moving history. In such case, overflowDelta + // values should stay zero. + if (scrollFrameWeak.IsAlive()) { + if (aEvent->mDeltaX && overflowStyle.mHorizontal == StyleOverflow::Hidden && + !ComputeScrollTargetAndMayAdjustWheelEvent( + scrollFrame, aEvent, + COMPUTE_SCROLLABLE_ANCESTOR_ALONG_X_AXIS_WITH_AUTO_DIR)) { + aEvent->mOverflowDeltaX = aEvent->mDeltaX; + } + if (aEvent->mDeltaY && overflowStyle.mVertical == StyleOverflow::Hidden && + !ComputeScrollTargetAndMayAdjustWheelEvent( + scrollFrame, aEvent, + COMPUTE_SCROLLABLE_ANCESTOR_ALONG_Y_AXIS_WITH_AUTO_DIR)) { + aEvent->mOverflowDeltaY = aEvent->mDeltaY; + } + } + + NS_ASSERTION( + aEvent->mOverflowDeltaX == 0 || + (aEvent->mOverflowDeltaX > 0) == (aEvent->mDeltaX > 0), + "The sign of mOverflowDeltaX is different from the scroll direction"); + NS_ASSERTION( + aEvent->mOverflowDeltaY == 0 || + (aEvent->mOverflowDeltaY > 0) == (aEvent->mDeltaY > 0), + "The sign of mOverflowDeltaY is different from the scroll direction"); + + WheelPrefs::GetInstance()->CancelApplyingUserPrefsFromOverflowDelta(aEvent); +} + +void EventStateManager::DecideGestureEvent(WidgetGestureNotifyEvent* aEvent, + nsIFrame* targetFrame) { + NS_ASSERTION(aEvent->mMessage == eGestureNotify, + "DecideGestureEvent called with a non-gesture event"); + + /* Check the ancestor tree to decide if any frame is willing* to receive + * a MozPixelScroll event. If that's the case, the current touch gesture + * will be used as a pan gesture; otherwise it will be a regular + * mousedown/mousemove/click event. + * + * *willing: determine if it makes sense to pan the element using scroll + * events: + * - For web content: if there are any visible scrollbars on the touch point + * - For XUL: if it's an scrollable element that can currently scroll in some + * direction. + * + * Note: we'll have to one-off various cases to ensure a good usable behavior + */ + WidgetGestureNotifyEvent::PanDirection panDirection = + WidgetGestureNotifyEvent::ePanNone; + bool displayPanFeedback = false; + for (nsIFrame* current = targetFrame; current; + current = nsLayoutUtils::GetCrossDocParentFrame(current)) { + // e10s - mark remote content as pannable. This is a work around since + // we don't have access to remote frame scroll info here. Apz data may + // assist is solving this. + if (current && IsTopLevelRemoteTarget(current->GetContent())) { + panDirection = WidgetGestureNotifyEvent::ePanBoth; + // We don't know when we reach bounds, so just disable feedback for now. + displayPanFeedback = false; + break; + } + + LayoutFrameType currentFrameType = current->Type(); + + // Scrollbars should always be draggable + if (currentFrameType == LayoutFrameType::Scrollbar) { + panDirection = WidgetGestureNotifyEvent::ePanNone; + break; + } + + // Special check for trees + if (nsTreeBodyFrame* treeFrame = do_QueryFrame(current)) { + if (treeFrame->GetHorizontalOverflow()) { + panDirection = WidgetGestureNotifyEvent::ePanHorizontal; + } + if (treeFrame->GetVerticalOverflow()) { + panDirection = WidgetGestureNotifyEvent::ePanVertical; + } + break; + } + + if (nsIScrollableFrame* scrollableFrame = do_QueryFrame(current)) { + layers::ScrollDirections scrollbarVisibility = + scrollableFrame->GetScrollbarVisibility(); + + // Check if we have visible scrollbars + if (scrollbarVisibility.contains(layers::ScrollDirection::eVertical)) { + panDirection = WidgetGestureNotifyEvent::ePanVertical; + displayPanFeedback = true; + break; + } + + if (scrollbarVisibility.contains(layers::ScrollDirection::eHorizontal)) { + panDirection = WidgetGestureNotifyEvent::ePanHorizontal; + displayPanFeedback = true; + } + } // scrollableFrame + } // ancestor chain + aEvent->mDisplayPanFeedback = displayPanFeedback; + aEvent->mPanDirection = panDirection; +} + +#ifdef XP_MACOSX +static nsINode* GetCrossDocParentNode(nsINode* aChild) { + MOZ_ASSERT(aChild, "The child is null!"); + MOZ_ASSERT(XRE_IsParentProcess()); + + nsINode* parent = aChild->GetParentNode(); + if (parent && parent->IsContent() && aChild->IsContent()) { + parent = aChild->AsContent()->GetFlattenedTreeParent(); + } + + if (parent || !aChild->IsDocument()) { + return parent; + } + + return aChild->AsDocument()->GetEmbedderElement(); +} + +static bool NodeAllowsClickThrough(nsINode* aNode) { + while (aNode) { + if (aNode->IsAnyOfXULElements(nsGkAtoms::browser, nsGkAtoms::tree)) { + return false; + } + if (aNode->IsAnyOfXULElements(nsGkAtoms::scrollbar, nsGkAtoms::resizer)) { + return true; + } + aNode = GetCrossDocParentNode(aNode); + } + return true; +} +#endif + +void EventStateManager::PostHandleKeyboardEvent( + WidgetKeyboardEvent* aKeyboardEvent, nsIFrame* aTargetFrame, + nsEventStatus& aStatus) { + if (aStatus == nsEventStatus_eConsumeNoDefault) { + return; + } + + RefPtr<nsPresContext> presContext = mPresContext; + + if (!aKeyboardEvent->HasBeenPostedToRemoteProcess()) { + if (aKeyboardEvent->IsWaitingReplyFromRemoteProcess()) { + RefPtr<BrowserParent> remote = + aTargetFrame ? BrowserParent::GetFrom(aTargetFrame->GetContent()) + : nullptr; + if (remote) { + // remote is null-checked above in order to let pre-existing event + // targeting code's chrome vs. content decision override in case of + // disagreement in order not to disrupt non-Fission e10s mode in case + // there are still bugs in the Fission-mode code. That is, if remote + // is nullptr, the pre-existing event targeting code has deemed this + // event to belong to chrome rather than content. + BrowserParent* preciseRemote = BrowserParent::GetFocused(); + if (preciseRemote) { + remote = preciseRemote; + } + // else there was a race between layout and focus tracking + } + if (remote && !remote->IsReadyToHandleInputEvents()) { + // We need to dispatch the event to the browser element again if we were + // waiting for the key reply but the event wasn't sent to the content + // process due to the remote browser wasn't ready. + WidgetKeyboardEvent keyEvent(*aKeyboardEvent); + aKeyboardEvent->MarkAsHandledInRemoteProcess(); + RefPtr<Element> ownerElement = remote->GetOwnerElement(); + EventDispatcher::Dispatch(ownerElement, presContext, &keyEvent); + if (keyEvent.DefaultPrevented()) { + aKeyboardEvent->PreventDefault(!keyEvent.DefaultPreventedByContent()); + aStatus = nsEventStatus_eConsumeNoDefault; + return; + } + } + } + // The widget expects a reply for every keyboard event. If the event wasn't + // dispatched to a content process (non-e10s or no content process + // running), we need to short-circuit here. Otherwise, we need to wait for + // the content process to handle the event. + if (aKeyboardEvent->mWidget) { + aKeyboardEvent->mWidget->PostHandleKeyEvent(aKeyboardEvent); + } + if (aKeyboardEvent->DefaultPrevented()) { + aStatus = nsEventStatus_eConsumeNoDefault; + return; + } + } + + // XXX Currently, our automated tests don't support mKeyNameIndex. + // Therefore, we still need to handle this with keyCode. + switch (aKeyboardEvent->mKeyCode) { + case NS_VK_TAB: + case NS_VK_F6: + // This is to prevent keyboard scrolling while alt modifier in use. + if (!aKeyboardEvent->IsAlt()) { + aStatus = nsEventStatus_eConsumeNoDefault; + + // Handling the tab event after it was sent to content is bad, + // because to the FocusManager the remote-browser looks like one + // element, so we would just move the focus to the next element + // in chrome, instead of handling it in content. + if (aKeyboardEvent->HasBeenPostedToRemoteProcess()) { + break; + } + + EnsureDocument(presContext); + nsFocusManager* fm = nsFocusManager::GetFocusManager(); + if (fm && mDocument) { + // Shift focus forward or back depending on shift key + bool isDocMove = aKeyboardEvent->IsControl() || + aKeyboardEvent->mKeyCode == NS_VK_F6; + uint32_t dir = + aKeyboardEvent->IsShift() + ? (isDocMove ? static_cast<uint32_t>( + nsIFocusManager::MOVEFOCUS_BACKWARDDOC) + : static_cast<uint32_t>( + nsIFocusManager::MOVEFOCUS_BACKWARD)) + : (isDocMove ? static_cast<uint32_t>( + nsIFocusManager::MOVEFOCUS_FORWARDDOC) + : static_cast<uint32_t>( + nsIFocusManager::MOVEFOCUS_FORWARD)); + RefPtr<Element> result; + fm->MoveFocus(mDocument->GetWindow(), nullptr, dir, + nsIFocusManager::FLAG_BYKEY, getter_AddRefs(result)); + } + } + return; + case 0: + // We handle keys with no specific keycode value below. + break; + default: + return; + } + + switch (aKeyboardEvent->mKeyNameIndex) { + case KEY_NAME_INDEX_ZoomIn: + case KEY_NAME_INDEX_ZoomOut: + ChangeZoom(aKeyboardEvent->mKeyNameIndex == KEY_NAME_INDEX_ZoomIn); + aStatus = nsEventStatus_eConsumeNoDefault; + break; + default: + break; + } +} + +nsresult EventStateManager::PostHandleEvent(nsPresContext* aPresContext, + WidgetEvent* aEvent, + nsIFrame* aTargetFrame, + nsEventStatus* aStatus, + nsIContent* aOverrideClickTarget) { + NS_ENSURE_ARG(aPresContext); + NS_ENSURE_ARG_POINTER(aStatus); + + mCurrentTarget = aTargetFrame; + mCurrentTargetContent = nullptr; + + HandleCrossProcessEvent(aEvent, aStatus); + // NOTE: the above call may have destroyed aTargetFrame, please use + // mCurrentTarget henceforth. This is to avoid using it accidentally: + aTargetFrame = nullptr; + + // Most of the events we handle below require a frame. + // Add special cases here. + if (!mCurrentTarget && aEvent->mMessage != eMouseUp && + aEvent->mMessage != eMouseDown && aEvent->mMessage != eDragEnter && + aEvent->mMessage != eDragOver && aEvent->mMessage != ePointerUp && + aEvent->mMessage != ePointerCancel) { + return NS_OK; + } + + // Keep the prescontext alive, we might need it after event dispatch + RefPtr<nsPresContext> presContext = aPresContext; + nsresult ret = NS_OK; + + switch (aEvent->mMessage) { + case eMouseDown: { + WidgetMouseEvent* mouseEvent = aEvent->AsMouseEvent(); + if (mouseEvent->mButton == MouseButton::ePrimary && + !sNormalLMouseEventInProcess) { + // We got a mouseup event while a mousedown event was being processed. + // Make sure that the capturing content is cleared. + PresShell::ReleaseCapturingContent(); + break; + } + + // For remote content, capture the event in the parent process at the + // <xul:browser remote> element. This will ensure that subsequent + // mousemove/mouseup events will continue to be dispatched to this element + // and therefore forwarded to the child. + if (aEvent->HasBeenPostedToRemoteProcess() && + !PresShell::GetCapturingContent()) { + if (nsIContent* content = + mCurrentTarget ? mCurrentTarget->GetContent() : nullptr) { + PresShell::SetCapturingContent(content, CaptureFlags::None, aEvent); + } else { + PresShell::ReleaseCapturingContent(); + } + } + + // If MouseEvent::PreventClickEvent() was called by chrome script, + // we need to forget the clicking content and click count for the + // following eMouseUp event. + if (mouseEvent->mClickEventPrevented) { + RefPtr<EventStateManager> esm = + ESMFromContentOrThis(aOverrideClickTarget); + switch (mouseEvent->mButton) { + case MouseButton::ePrimary: + esm->mLastLeftMouseDownContent = nullptr; + esm->mLClickCount = 0; + break; + case MouseButton::eSecondary: + esm->mLastMiddleMouseDownContent = nullptr; + esm->mMClickCount = 0; + break; + case MouseButton::eMiddle: + esm->mLastRightMouseDownContent = nullptr; + esm->mRClickCount = 0; + break; + default: + break; + } + } + + nsCOMPtr<nsIContent> activeContent; + // When content calls PreventDefault on pointerdown, we also call + // PreventDefault on the subsequent mouse events to suppress default + // behaviors. Normally, aStatus should be nsEventStatus_eConsumeNoDefault + // when the event is DefaultPrevented but it's reset to + // nsEventStatus_eIgnore in EventStateManager::PreHandleEvent. So we also + // check if the event is DefaultPrevented. + if (nsEventStatus_eConsumeNoDefault != *aStatus && + !aEvent->DefaultPrevented()) { + nsCOMPtr<nsIContent> newFocus; + bool suppressBlur = false; + if (mCurrentTarget) { + mCurrentTarget->GetContentForEvent(aEvent, getter_AddRefs(newFocus)); + activeContent = mCurrentTarget->GetContent(); + + // In some cases, we do not want to even blur the current focused + // element. Those cases are: + // 1. -moz-user-focus CSS property is set to 'ignore'; + // 2. XUL control element has the disabled property set to 'true'. + // + // We can't use nsIFrame::IsFocusable() because we want to blur when + // we click on a visibility: none element. + // We can't use nsIContent::IsFocusable() because we want to blur when + // we click on a non-focusable element like a <div>. + // We have to use |aEvent->mTarget| to not make sure we do not check + // an anonymous node of the targeted element. + suppressBlur = + mCurrentTarget->StyleUI()->UserFocus() == StyleUserFocus::Ignore; + + if (!suppressBlur) { + if (Element* element = + Element::FromEventTargetOrNull(aEvent->mTarget)) { + if (nsCOMPtr<nsIDOMXULControlElement> xulControl = + element->AsXULControl()) { + bool disabled = false; + xulControl->GetDisabled(&disabled); + suppressBlur = disabled; + } + } + } + } + + // When a root content which isn't editable but has an editable HTML + // <body> element is clicked, we should redirect the focus to the + // the <body> element. E.g., when an user click bottom of the editor + // where is outside of the <body> element, the <body> should be focused + // and the user can edit immediately after that. + // + // NOTE: The newFocus isn't editable that also means it's not in + // designMode. In designMode, all contents are not focusable. + if (newFocus && !newFocus->IsEditable()) { + Document* doc = newFocus->GetComposedDoc(); + if (doc && newFocus == doc->GetRootElement()) { + nsIContent* bodyContent = + nsLayoutUtils::GetEditableRootContentByContentEditable(doc); + if (bodyContent && bodyContent->GetPrimaryFrame()) { + newFocus = bodyContent; + } + } + } + + // When the mouse is pressed, the default action is to focus the + // target. Look for the nearest enclosing focusable frame. + // + // TODO: Probably this should be moved to Element::PostHandleEvent. + for (; newFocus; newFocus = newFocus->GetFlattenedTreeParent()) { + if (!newFocus->IsElement()) { + continue; + } + + nsIFrame* frame = newFocus->GetPrimaryFrame(); + if (!frame) { + continue; + } + + // If the mousedown happened inside a popup, don't try to set focus on + // one of its containing elements + if (frame->IsMenuPopupFrame()) { + newFocus = nullptr; + break; + } + + if (frame->IsFocusable(/* aWithMouse = */ true)) { + break; + } + + if (ShadowRoot* root = newFocus->GetShadowRoot()) { + if (root->DelegatesFocus()) { + if (Element* firstFocusable = + root->GetFocusDelegate(/* aWithMouse */ true)) { + newFocus = firstFocusable; + break; + } + } + } + } + + MOZ_ASSERT_IF(newFocus, newFocus->IsElement()); + + if (RefPtr<nsFocusManager> fm = nsFocusManager::GetFocusManager()) { + // if something was found to focus, focus it. Otherwise, if the + // element that was clicked doesn't have -moz-user-focus: ignore, + // clear the existing focus. For -moz-user-focus: ignore, the focus + // is just left as is. + // Another effect of mouse clicking, handled in Selection, is that + // it should update the caret position to where the mouse was + // clicked. Because the focus is cleared when clicking on a + // non-focusable node, the next press of the tab key will cause + // focus to be shifted from the caret position instead of the root. + if (newFocus) { + // use the mouse flag and the noscroll flag so that the content + // doesn't unexpectedly scroll when clicking an element that is + // only half visible + uint32_t flags = + nsIFocusManager::FLAG_BYMOUSE | nsIFocusManager::FLAG_NOSCROLL; + // If this was a touch-generated event, pass that information: + if (mouseEvent->mInputSource == + MouseEvent_Binding::MOZ_SOURCE_TOUCH) { + flags |= nsIFocusManager::FLAG_BYTOUCH; + } + fm->SetFocus(MOZ_KnownLive(newFocus->AsElement()), flags); + } else if (!suppressBlur) { + // clear the focus within the frame and then set it as the + // focused frame + EnsureDocument(mPresContext); + if (mDocument) { + nsCOMPtr<nsPIDOMWindowOuter> outerWindow = mDocument->GetWindow(); +#ifdef XP_MACOSX + if (!activeContent || !activeContent->IsXULElement()) +#endif + fm->ClearFocus(outerWindow); + // Prevent switch frame if we're already not in the foreground tab + // and we're in a content process. + // TODO: If we were inactive frame in this tab, and now in + // background tab, we shouldn't make the tab foreground, but + // we should set focus to clicked document in the background + // tab. However, nsFocusManager does not have proper method + // for doing this. Therefore, we should skip setting focus + // to clicked document for now. + if (XRE_IsParentProcess() || IsInActiveTab(mDocument)) { + fm->SetFocusedWindow(outerWindow); + } + } + } + } + + // The rest is left button-specific. + if (mouseEvent->mButton != MouseButton::ePrimary) { + break; + } + + // The nearest enclosing element goes into the :active state. If we're + // not an element (so we're text or something) we need to obtain + // our parent element and put it into :active instead. + if (activeContent && !activeContent->IsElement()) { + if (nsIContent* par = activeContent->GetFlattenedTreeParent()) { + activeContent = par; + } + } + } else { + // if we're here, the event handler returned false, so stop + // any of our own processing of a drag. Workaround for bug 43258. + StopTrackingDragGesture(true); + } + // XXX Why do we always set this is active? Active window may be changed + // by a mousedown event listener. + SetActiveManager(this, activeContent); + } break; + case ePointerCancel: + case ePointerUp: { + WidgetPointerEvent* pointerEvent = aEvent->AsPointerEvent(); + MOZ_ASSERT(pointerEvent); + // Implicitly releasing capture for given pointer. ePointerLostCapture + // should be send after ePointerUp or ePointerCancel. + PointerEventHandler::ImplicitlyReleasePointerCapture(pointerEvent); + PointerEventHandler::UpdateActivePointerState(pointerEvent); + + if (pointerEvent->mMessage == ePointerCancel || + pointerEvent->mInputSource == MouseEvent_Binding::MOZ_SOURCE_TOUCH) { + // After pointercancel, pointer becomes invalid so we can remove + // relevant helper from table. Regarding pointerup with non-hoverable + // device, the pointer also becomes invalid. Hoverable (mouse/pen) + // pointers are valid all the time (not only between down/up). + GenerateMouseEnterExit(pointerEvent); + mPointersEnterLeaveHelper.Remove(pointerEvent->pointerId); + } + break; + } + case eMouseUp: { + // We can unconditionally stop capturing because + // we should never be capturing when the mouse button is up + PresShell::ReleaseCapturingContent(); + + ClearGlobalActiveContent(this); + WidgetMouseEvent* mouseUpEvent = aEvent->AsMouseEvent(); + if (mouseUpEvent && EventCausesClickEvents(*mouseUpEvent)) { + // Make sure to dispatch the click even if there is no frame for + // the current target element. This is required for Web compatibility. + RefPtr<EventStateManager> esm = + ESMFromContentOrThis(aOverrideClickTarget); + ret = + esm->PostHandleMouseUp(mouseUpEvent, aStatus, aOverrideClickTarget); + } + + if (PresShell* presShell = presContext->GetPresShell()) { + RefPtr<nsFrameSelection> frameSelection = presShell->FrameSelection(); + frameSelection->SetDragState(false); + } + } break; + case eWheelOperationEnd: { + MOZ_ASSERT(aEvent->IsTrusted()); + ScrollbarsForWheel::MayInactivate(); + WidgetWheelEvent* wheelEvent = aEvent->AsWheelEvent(); + nsIScrollableFrame* scrollTarget = + do_QueryFrame(ComputeScrollTargetAndMayAdjustWheelEvent( + mCurrentTarget, wheelEvent, + COMPUTE_DEFAULT_ACTION_TARGET_WITH_AUTO_DIR)); + // If the wheel event was handled by APZ, APZ will perform the scroll + // snap. + if (scrollTarget && !WheelTransaction::HandledByApz()) { + scrollTarget->ScrollSnap(); + } + } break; + case eWheel: + case eWheelOperationStart: { + MOZ_ASSERT(aEvent->IsTrusted()); + + if (*aStatus == nsEventStatus_eConsumeNoDefault) { + ScrollbarsForWheel::Inactivate(); + break; + } + + WidgetWheelEvent* wheelEvent = aEvent->AsWheelEvent(); + MOZ_ASSERT(wheelEvent); + + // When APZ is enabled, the actual scroll animation might be handled by + // the compositor. + WheelPrefs::Action action = + wheelEvent->mFlags.mHandledByAPZ + ? WheelPrefs::ACTION_NONE + : WheelPrefs::GetInstance()->ComputeActionFor(wheelEvent); + + WheelDeltaAdjustmentStrategy strategy = + GetWheelDeltaAdjustmentStrategy(*wheelEvent); + // Adjust the delta values of the wheel event if the current default + // action is to horizontalize scrolling. I.e., deltaY values are set to + // deltaX and deltaY and deltaZ values are set to 0. + // If horizontalized, the delta values will be restored and its overflow + // deltaX will become 0 when the WheelDeltaHorizontalizer instance is + // being destroyed. + WheelDeltaHorizontalizer horizontalizer(*wheelEvent); + if (WheelDeltaAdjustmentStrategy::eHorizontalize == strategy) { + horizontalizer.Horizontalize(); + } + + // Since ComputeScrollTargetAndMayAdjustWheelEvent() may adjust the delta + // if the event is auto-dir. So we use |ESMAutoDirWheelDeltaRestorer| + // here. + // An instance of |ESMAutoDirWheelDeltaRestorer| is used to monitor + // auto-dir adjustment which may happen during its lifetime. If the delta + // values is adjusted during its lifetime, the instance will restore the + // adjusted delta when it's being destrcuted. + ESMAutoDirWheelDeltaRestorer restorer(*wheelEvent); + nsIFrame* frameToScroll = ComputeScrollTargetAndMayAdjustWheelEvent( + mCurrentTarget, wheelEvent, + COMPUTE_DEFAULT_ACTION_TARGET_WITH_AUTO_DIR); + + switch (action) { + case WheelPrefs::ACTION_SCROLL: + case WheelPrefs::ACTION_HORIZONTALIZED_SCROLL: { + // For scrolling of default action, we should honor the mouse wheel + // transaction. + + ScrollbarsForWheel::PrepareToScrollText(this, mCurrentTarget, + wheelEvent); + + if (aEvent->mMessage != eWheel || + (!wheelEvent->mDeltaX && !wheelEvent->mDeltaY)) { + break; + } + + nsIScrollableFrame* scrollTarget = do_QueryFrame(frameToScroll); + ScrollbarsForWheel::SetActiveScrollTarget(scrollTarget); + + nsIFrame* rootScrollFrame = + !mCurrentTarget + ? nullptr + : mCurrentTarget->PresShell()->GetRootScrollFrame(); + nsIScrollableFrame* rootScrollableFrame = nullptr; + if (rootScrollFrame) { + rootScrollableFrame = do_QueryFrame(rootScrollFrame); + } + if (!scrollTarget || scrollTarget == rootScrollableFrame) { + wheelEvent->mViewPortIsOverscrolled = true; + } + wheelEvent->mOverflowDeltaX = wheelEvent->mDeltaX; + wheelEvent->mOverflowDeltaY = wheelEvent->mDeltaY; + WheelPrefs::GetInstance()->CancelApplyingUserPrefsFromOverflowDelta( + wheelEvent); + if (scrollTarget) { + DoScrollText(scrollTarget, wheelEvent); + } else { + WheelTransaction::EndTransaction(); + ScrollbarsForWheel::Inactivate(); + } + break; + } + case WheelPrefs::ACTION_HISTORY: { + // If this event doesn't cause eLegacyMouseLineOrPageScroll event or + // the direction is oblique, don't perform history back/forward. + int32_t intDelta = wheelEvent->GetPreferredIntDelta(); + if (!intDelta) { + break; + } + DoScrollHistory(intDelta); + break; + } + case WheelPrefs::ACTION_ZOOM: { + // If this event doesn't cause eLegacyMouseLineOrPageScroll event or + // the direction is oblique, don't perform zoom in/out. + int32_t intDelta = wheelEvent->GetPreferredIntDelta(); + if (!intDelta) { + break; + } + DoScrollZoom(mCurrentTarget, intDelta); + break; + } + case WheelPrefs::ACTION_NONE: + default: + bool allDeltaOverflown = false; + if (StaticPrefs::dom_event_wheel_event_groups_enabled() && + (wheelEvent->mDeltaX != 0.0 || wheelEvent->mDeltaY != 0.0)) { + if (frameToScroll) { + WheelTransaction::WillHandleDefaultAction( + wheelEvent, frameToScroll, mCurrentTarget); + } else { + WheelTransaction::EndTransaction(); + } + } + if (wheelEvent->mFlags.mHandledByAPZ) { + if (wheelEvent->mCanTriggerSwipe) { + // For events that can trigger swipes, APZ needs to know whether + // scrolling is possible in the requested direction. It does this + // by looking at the scroll overflow values on mCanTriggerSwipe + // events after they have been processed. + allDeltaOverflown = !ComputeScrollTarget( + mCurrentTarget, wheelEvent, COMPUTE_DEFAULT_ACTION_TARGET); + } + } else { + // The event was processed neither by APZ nor by us, so all of the + // delta values must be overflown delta values. + allDeltaOverflown = true; + } + + if (!allDeltaOverflown) { + break; + } + wheelEvent->mOverflowDeltaX = wheelEvent->mDeltaX; + wheelEvent->mOverflowDeltaY = wheelEvent->mDeltaY; + WheelPrefs::GetInstance()->CancelApplyingUserPrefsFromOverflowDelta( + wheelEvent); + wheelEvent->mViewPortIsOverscrolled = true; + break; + } + *aStatus = nsEventStatus_eConsumeNoDefault; + } break; + + case eGestureNotify: { + if (nsEventStatus_eConsumeNoDefault != *aStatus) { + DecideGestureEvent(aEvent->AsGestureNotifyEvent(), mCurrentTarget); + } + } break; + + case eDragEnter: + case eDragOver: { + NS_ASSERTION(aEvent->mClass == eDragEventClass, "Expected a drag event"); + + // Check if the drag is occurring inside a scrollable area. If so, scroll + // the area when the mouse is near the edges. + if (mCurrentTarget && aEvent->mMessage == eDragOver) { + nsIFrame* checkFrame = mCurrentTarget; + while (checkFrame) { + nsIScrollableFrame* scrollFrame = do_QueryFrame(checkFrame); + // Break out so only the innermost scrollframe is scrolled. + if (scrollFrame && scrollFrame->DragScroll(aEvent)) { + break; + } + checkFrame = checkFrame->GetParent(); + } + } + + nsCOMPtr<nsIDragSession> dragSession = nsContentUtils::GetDragSession(); + if (!dragSession) break; + + // Reset the flag. + dragSession->SetOnlyChromeDrop(false); + if (mPresContext) { + EnsureDocument(mPresContext); + } + bool isChromeDoc = nsContentUtils::IsChromeDoc(mDocument); + + // the initial dataTransfer is the one from the dragstart event that + // was set on the dragSession when the drag began. + RefPtr<DataTransfer> dataTransfer; + RefPtr<DataTransfer> initialDataTransfer = dragSession->GetDataTransfer(); + + WidgetDragEvent* dragEvent = aEvent->AsDragEvent(); + + // collect any changes to moz cursor settings stored in the event's + // data transfer. + UpdateDragDataTransfer(dragEvent); + + // cancelling a dragenter or dragover event means that a drop should be + // allowed, so update the dropEffect and the canDrop state to indicate + // that a drag is allowed. If the event isn't cancelled, a drop won't be + // allowed. Essentially, to allow a drop somewhere, specify the effects + // using the effectAllowed and dropEffect properties in a dragenter or + // dragover event and cancel the event. To not allow a drop somewhere, + // don't cancel the event or set the effectAllowed or dropEffect to + // "none". This way, if the event is just ignored, no drop will be + // allowed. + uint32_t dropEffect = nsIDragService::DRAGDROP_ACTION_NONE; + uint32_t action = nsIDragService::DRAGDROP_ACTION_NONE; + if (nsEventStatus_eConsumeNoDefault == *aStatus) { + // If the event has initialized its mDataTransfer, use it. + // Or the event has not been initialized its mDataTransfer, but + // it's set before dispatch because of synthesized, but without + // testing session (e.g., emulating drag from another app), use it + // coming from outside. + // XXX Perhaps, for the latter case, we need new API because we don't + // have a chance to initialize allowed effects of the session. + if (dragEvent->mDataTransfer) { + // get the dataTransfer and the dropEffect that was set on it + dataTransfer = dragEvent->mDataTransfer; + dropEffect = dataTransfer->DropEffectInt(); + } else { + // if dragEvent->mDataTransfer is null, it means that no attempt was + // made to access the dataTransfer during the event, yet the event + // was cancelled. Instead, use the initial data transfer available + // from the drag session. The drop effect would not have been + // initialized (which is done in DragEvent::GetDataTransfer), + // so set it from the drag action. We'll still want to filter it + // based on the effectAllowed below. + dataTransfer = initialDataTransfer; + + dragSession->GetDragAction(&action); + + // filter the drop effect based on the action. Use UNINITIALIZED as + // any effect is allowed. + dropEffect = nsContentUtils::FilterDropEffect( + action, nsIDragService::DRAGDROP_ACTION_UNINITIALIZED); + } + + // At this point, if the dataTransfer is null, it means that the + // drag was originally started by directly calling the drag service. + // Just assume that all effects are allowed. + uint32_t effectAllowed = nsIDragService::DRAGDROP_ACTION_UNINITIALIZED; + if (dataTransfer) { + effectAllowed = dataTransfer->EffectAllowedInt(); + } + + // set the drag action based on the drop effect and effect allowed. + // The drop effect field on the drag transfer object specifies the + // desired current drop effect. However, it cannot be used if the + // effectAllowed state doesn't include that type of action. If the + // dropEffect is "none", then the action will be 'none' so a drop will + // not be allowed. + if (effectAllowed == nsIDragService::DRAGDROP_ACTION_UNINITIALIZED || + dropEffect & effectAllowed) + action = dropEffect; + + if (action == nsIDragService::DRAGDROP_ACTION_NONE) + dropEffect = nsIDragService::DRAGDROP_ACTION_NONE; + + // inform the drag session that a drop is allowed on this node. + dragSession->SetDragAction(action); + dragSession->SetCanDrop(action != nsIDragService::DRAGDROP_ACTION_NONE); + + // For now, do this only for dragover. + // XXXsmaug dragenter needs some more work. + if (aEvent->mMessage == eDragOver && !isChromeDoc) { + // Someone has called preventDefault(), check whether is was on + // content or chrome. + dragSession->SetOnlyChromeDrop( + !dragEvent->mDefaultPreventedOnContent); + } + } else if (aEvent->mMessage == eDragOver && !isChromeDoc) { + // No one called preventDefault(), so handle drop only in chrome. + dragSession->SetOnlyChromeDrop(true); + } + if (ContentChild* child = ContentChild::GetSingleton()) { + child->SendUpdateDropEffect(action, dropEffect); + } + if (aEvent->HasBeenPostedToRemoteProcess()) { + dragSession->SetCanDrop(true); + } else if (initialDataTransfer) { + // Now set the drop effect in the initial dataTransfer. This ensures + // that we can get the desired drop effect in the drop event. For events + // dispatched to content, the content process will take care of setting + // this. + initialDataTransfer->SetDropEffectInt(dropEffect); + } + } break; + + case eDrop: { + if (aEvent->mFlags.mIsSynthesizedForTests) { + if (nsCOMPtr<nsIDragSession> dragSession = + nsContentUtils::GetDragSession()) { + MOZ_ASSERT(dragSession->IsSynthesizedForTests()); + RefPtr<WindowContext> sourceWC; + DebugOnly<nsresult> rvIgnored = + dragSession->GetSourceWindowContext(getter_AddRefs(sourceWC)); + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rvIgnored), + "nsIDragSession::GetSourceDocument() failed, but ignored"); + // If the drag source hasn't been initialized, i.e., dragstart was + // consumed by the test, the test needs to dispatch "dragend" event + // instead of the drag session. Therefore, it does not make sense + // to set drag end point in such case (you hit assersion if you do + // it). + if (sourceWC) { + CSSIntPoint dropPointInScreen = + Event::GetScreenCoords(aPresContext, aEvent, aEvent->mRefPoint) + .extract(); + dragSession->SetDragEndPointForTests(dropPointInScreen.x, + dropPointInScreen.y); + } + } + } + sLastDragOverFrame = nullptr; + ClearGlobalActiveContent(this); + break; + } + case eDragExit: + // make sure to fire the enter and exit_synth events after the + // eDragExit event, otherwise we'll clean up too early + GenerateDragDropEnterExit(presContext, aEvent->AsDragEvent()); + if (ContentChild* child = ContentChild::GetSingleton()) { + // SendUpdateDropEffect to prevent nsIDragService from waiting for + // response of forwarded dragexit event. + child->SendUpdateDropEffect(nsIDragService::DRAGDROP_ACTION_NONE, + nsIDragService::DRAGDROP_ACTION_NONE); + } + break; + + case eKeyUp: + // If space key is released, we need to inactivate the element which was + // activated by preceding space key down. + // XXX Currently, we don't store the reason of activation. Therefore, + // this may cancel what is activated by a mousedown, but it must not + // cause actual problem in web apps in the wild since it must be + // rare case that users release space key during a mouse click/drag. + if (aEvent->AsKeyboardEvent()->ShouldWorkAsSpaceKey()) { + ClearGlobalActiveContent(this); + } + break; + + case eKeyPress: { + WidgetKeyboardEvent* keyEvent = aEvent->AsKeyboardEvent(); + PostHandleKeyboardEvent(keyEvent, mCurrentTarget, *aStatus); + } break; + + case eMouseEnterIntoWidget: + if (mCurrentTarget) { + nsCOMPtr<nsIContent> targetContent; + mCurrentTarget->GetContentForEvent(aEvent, + getter_AddRefs(targetContent)); + SetContentState(targetContent, ElementState::HOVER); + } + break; + + case eMouseExitFromWidget: + PointerEventHandler::UpdateActivePointerState(aEvent->AsMouseEvent()); + break; + +#ifdef XP_MACOSX + case eMouseActivate: + if (mCurrentTarget) { + nsCOMPtr<nsIContent> targetContent; + mCurrentTarget->GetContentForEvent(aEvent, + getter_AddRefs(targetContent)); + if (!NodeAllowsClickThrough(targetContent)) { + *aStatus = nsEventStatus_eConsumeNoDefault; + } + } + break; +#endif + + default: + break; + } + + // Reset target frame to null to avoid mistargeting after reentrant event + mCurrentTarget = nullptr; + mCurrentTargetContent = nullptr; + + return ret; +} + +BrowserParent* EventStateManager::GetCrossProcessTarget() { + return IMEStateManager::GetActiveBrowserParent(); +} + +bool EventStateManager::IsTargetCrossProcess(WidgetGUIEvent* aEvent) { + // Check to see if there is a focused, editable content in chrome, + // in that case, do not forward IME events to content + Element* focusedElement = GetFocusedElement(); + if (focusedElement && focusedElement->IsEditable()) { + return false; + } + return IMEStateManager::GetActiveBrowserParent() != nullptr; +} + +void EventStateManager::NotifyDestroyPresContext(nsPresContext* aPresContext) { + RefPtr<nsPresContext> presContext = aPresContext; + if (presContext) { + IMEStateManager::OnDestroyPresContext(*presContext); + } + + // Bug 70855: Presentation is going away, possibly for a reframe. + // Reset the hover state so that if we're recreating the presentation, + // we won't have the old hover state still set in the new presentation, + // as if the new presentation is resized, a new element may be hovered. + ResetHoverState(); + + mPointersEnterLeaveHelper.Clear(); + PointerEventHandler::NotifyDestroyPresContext(presContext); +} + +void EventStateManager::ResetHoverState() { + if (mHoverContent) { + SetContentState(nullptr, ElementState::HOVER); + } +} + +void EventStateManager::SetPresContext(nsPresContext* aPresContext) { + mPresContext = aPresContext; +} + +void EventStateManager::ClearFrameRefs(nsIFrame* aFrame) { + if (aFrame && aFrame == mCurrentTarget) { + mCurrentTargetContent = aFrame->GetContent(); + } +} + +struct CursorImage { + gfx::IntPoint mHotspot; + nsCOMPtr<imgIContainer> mContainer; + ImageResolution mResolution; + bool mEarlierCursorLoading = false; +}; + +// Given the event that we're processing, and the computed cursor and hotspot, +// determine whether the custom CSS cursor should be blocked (that is, not +// honored). +// +// We will not honor it all of the following are true: +// +// * layout.cursor.block.enabled is true. +// * the size of the custom cursor is bigger than layout.cursor.block.max-size. +// * the bounds of the cursor would end up outside of the viewport of the +// top-level content document. +// +// This is done in order to prevent hijacking the cursor, see bug 1445844 and +// co. +static bool ShouldBlockCustomCursor(nsPresContext* aPresContext, + WidgetEvent* aEvent, + const CursorImage& aCursor) { + if (!StaticPrefs::layout_cursor_block_enabled()) { + return false; + } + + int32_t width = 0; + int32_t height = 0; + aCursor.mContainer->GetWidth(&width); + aCursor.mContainer->GetHeight(&height); + aCursor.mResolution.ApplyTo(width, height); + + int32_t maxSize = StaticPrefs::layout_cursor_block_max_size(); + + if (width <= maxSize && height <= maxSize) { + return false; + } + + auto input = DOMIntersectionObserver::ComputeInput(*aPresContext->Document(), + nullptr, nullptr); + + if (!input.mRootFrame) { + return false; + } + + nsPoint point = nsLayoutUtils::GetEventCoordinatesRelativeTo( + aEvent, RelativeTo{input.mRootFrame}); + + // The cursor size won't be affected by our full zoom in the parent process, + // so undo that before checking the rect. + float zoom = aPresContext->GetFullZoom(); + + // Also adjust for accessibility cursor scaling factor. + zoom /= LookAndFeel::GetFloat(LookAndFeel::FloatID::CursorScale, 1.0f); + + nsSize size(CSSPixel::ToAppUnits(width / zoom), + CSSPixel::ToAppUnits(height / zoom)); + nsPoint hotspot(CSSPixel::ToAppUnits(aCursor.mHotspot.x / zoom), + CSSPixel::ToAppUnits(aCursor.mHotspot.y / zoom)); + + const nsRect cursorRect(point - hotspot, size); + auto output = DOMIntersectionObserver::Intersect(input, cursorRect); + return !output.mIntersectionRect || + !(*output.mIntersectionRect == cursorRect); +} + +static gfx::IntPoint ComputeHotspot(imgIContainer* aContainer, + const Maybe<gfx::Point>& aHotspot) { + MOZ_ASSERT(aContainer); + + // css3-ui says to use the CSS-specified hotspot if present, + // otherwise use the intrinsic hotspot, otherwise use the top left + // corner. + if (aHotspot) { + int32_t imgWidth, imgHeight; + aContainer->GetWidth(&imgWidth); + aContainer->GetHeight(&imgHeight); + auto hotspot = gfx::IntPoint::Round(*aHotspot); + return {std::max(std::min(hotspot.x.value, imgWidth - 1), 0), + std::max(std::min(hotspot.y.value, imgHeight - 1), 0)}; + } + + gfx::IntPoint hotspot; + aContainer->GetHotspotX(&hotspot.x.value); + aContainer->GetHotspotY(&hotspot.y.value); + return hotspot; +} + +static CursorImage ComputeCustomCursor(nsPresContext* aPresContext, + WidgetEvent* aEvent, + const nsIFrame& aFrame, + const nsIFrame::Cursor& aCursor) { + if (aCursor.mAllowCustomCursor == nsIFrame::AllowCustomCursorImage::No) { + return {}; + } + const ComputedStyle& style = + aCursor.mStyle ? *aCursor.mStyle : *aFrame.Style(); + + // If we are falling back because any cursor before us is loading, let the + // consumer know. + bool loading = false; + for (const auto& image : style.StyleUI()->Cursor().images.AsSpan()) { + MOZ_ASSERT(image.image.IsImageRequestType(), + "Cursor image should only parse url() types"); + uint32_t status; + imgRequestProxy* req = image.image.GetImageRequest(); + if (!req || NS_FAILED(req->GetImageStatus(&status))) { + continue; + } + if (!(status & imgIRequest::STATUS_LOAD_COMPLETE)) { + loading = true; + continue; + } + if (status & imgIRequest::STATUS_ERROR) { + continue; + } + nsCOMPtr<imgIContainer> container; + req->GetImage(getter_AddRefs(container)); + if (!container) { + continue; + } + StyleImageOrientation orientation = + aFrame.StyleVisibility()->UsedImageOrientation(req); + container = nsLayoutUtils::OrientImage(container, orientation); + Maybe<gfx::Point> specifiedHotspot = + image.has_hotspot ? Some(gfx::Point{image.hotspot_x, image.hotspot_y}) + : Nothing(); + gfx::IntPoint hotspot = ComputeHotspot(container, specifiedHotspot); + CursorImage result{hotspot, std::move(container), + image.image.GetResolution(), loading}; + if (ShouldBlockCustomCursor(aPresContext, aEvent, result)) { + continue; + } + // This is the one we want! + return result; + } + return {{}, nullptr, {}, loading}; +} + +void EventStateManager::UpdateCursor(nsPresContext* aPresContext, + WidgetEvent* aEvent, + nsIFrame* aTargetFrame, + nsEventStatus* aStatus) { + if (aTargetFrame && IsRemoteTarget(aTargetFrame->GetContent())) { + return; + } + + auto cursor = StyleCursorKind::Default; + nsCOMPtr<imgIContainer> container; + ImageResolution resolution; + Maybe<gfx::IntPoint> hotspot; + + // If cursor is locked just use the locked one + if (mLockCursor != kInvalidCursorKind) { + cursor = mLockCursor; + } + // If not locked, look for correct cursor + else if (aTargetFrame) { + nsPoint pt = nsLayoutUtils::GetEventCoordinatesRelativeTo( + aEvent, RelativeTo{aTargetFrame}); + Maybe<nsIFrame::Cursor> framecursor = aTargetFrame->GetCursor(pt); + // Avoid setting cursor when the mouse is over a windowless plugin. + if (!framecursor) { + if (XRE_IsContentProcess()) { + mLastFrameConsumedSetCursor = true; + } + return; + } + // Make sure cursors get reset after the mouse leaves a + // windowless plugin frame. + if (mLastFrameConsumedSetCursor) { + ClearCachedWidgetCursor(aTargetFrame); + mLastFrameConsumedSetCursor = false; + } + + const CursorImage customCursor = + ComputeCustomCursor(aPresContext, aEvent, *aTargetFrame, *framecursor); + + // If the current cursor is from the same frame, and it is now + // loading some new image for the cursor, we should wait for a + // while rather than taking its fallback cursor directly. + if (customCursor.mEarlierCursorLoading && + gLastCursorSourceFrame == aTargetFrame && + TimeStamp::NowLoRes() - gLastCursorUpdateTime < + TimeDuration::FromMilliseconds(kCursorLoadingTimeout)) { + return; + } + cursor = framecursor->mCursor; + container = std::move(customCursor.mContainer); + resolution = customCursor.mResolution; + hotspot = Some(customCursor.mHotspot); + } + + if (StaticPrefs::ui_use_activity_cursor()) { + // Check whether or not to show the busy cursor + nsCOMPtr<nsIDocShell> docShell(aPresContext->GetDocShell()); + if (!docShell) return; + auto busyFlags = docShell->GetBusyFlags(); + + // Show busy cursor everywhere before page loads + // and just replace the arrow cursor after page starts loading + if (busyFlags & nsIDocShell::BUSY_FLAGS_BUSY && + (cursor == StyleCursorKind::Auto || + cursor == StyleCursorKind::Default)) { + cursor = StyleCursorKind::Progress; + container = nullptr; + } + } + + if (aTargetFrame) { + if (cursor == StyleCursorKind::Pointer && IsSelectingLink(aTargetFrame)) { + cursor = aTargetFrame->GetWritingMode().IsVertical() + ? StyleCursorKind::VerticalText + : StyleCursorKind::Text; + } + SetCursor(cursor, container, resolution, hotspot, + aTargetFrame->GetNearestWidget(), false); + gLastCursorSourceFrame = aTargetFrame; + gLastCursorUpdateTime = TimeStamp::NowLoRes(); + } + + if (mLockCursor != kInvalidCursorKind || StyleCursorKind::Auto != cursor) { + *aStatus = nsEventStatus_eConsumeDoDefault; + } +} + +void EventStateManager::ClearCachedWidgetCursor(nsIFrame* aTargetFrame) { + if (!aTargetFrame) { + return; + } + nsIWidget* aWidget = aTargetFrame->GetNearestWidget(); + if (!aWidget) { + return; + } + aWidget->ClearCachedCursor(); +} + +nsresult EventStateManager::SetCursor(StyleCursorKind aCursor, + imgIContainer* aContainer, + const ImageResolution& aResolution, + const Maybe<gfx::IntPoint>& aHotspot, + nsIWidget* aWidget, bool aLockCursor) { + EnsureDocument(mPresContext); + NS_ENSURE_TRUE(mDocument, NS_ERROR_FAILURE); + sMouseOverDocument = mDocument.get(); + + NS_ENSURE_TRUE(aWidget, NS_ERROR_FAILURE); + if (aLockCursor) { + if (StyleCursorKind::Auto != aCursor) { + mLockCursor = aCursor; + } else { + // If cursor style is set to auto we unlock the cursor again. + mLockCursor = kInvalidCursorKind; + } + } + nsCursor c; + switch (aCursor) { + case StyleCursorKind::Auto: + case StyleCursorKind::Default: + c = eCursor_standard; + break; + case StyleCursorKind::Pointer: + c = eCursor_hyperlink; + break; + case StyleCursorKind::Crosshair: + c = eCursor_crosshair; + break; + case StyleCursorKind::Move: + c = eCursor_move; + break; + case StyleCursorKind::Text: + c = eCursor_select; + break; + case StyleCursorKind::Wait: + c = eCursor_wait; + break; + case StyleCursorKind::Help: + c = eCursor_help; + break; + case StyleCursorKind::NResize: + c = eCursor_n_resize; + break; + case StyleCursorKind::SResize: + c = eCursor_s_resize; + break; + case StyleCursorKind::WResize: + c = eCursor_w_resize; + break; + case StyleCursorKind::EResize: + c = eCursor_e_resize; + break; + case StyleCursorKind::NwResize: + c = eCursor_nw_resize; + break; + case StyleCursorKind::SeResize: + c = eCursor_se_resize; + break; + case StyleCursorKind::NeResize: + c = eCursor_ne_resize; + break; + case StyleCursorKind::SwResize: + c = eCursor_sw_resize; + break; + case StyleCursorKind::Copy: // CSS3 + c = eCursor_copy; + break; + case StyleCursorKind::Alias: + c = eCursor_alias; + break; + case StyleCursorKind::ContextMenu: + c = eCursor_context_menu; + break; + case StyleCursorKind::Cell: + c = eCursor_cell; + break; + case StyleCursorKind::Grab: + c = eCursor_grab; + break; + case StyleCursorKind::Grabbing: + c = eCursor_grabbing; + break; + case StyleCursorKind::Progress: + c = eCursor_spinning; + break; + case StyleCursorKind::ZoomIn: + c = eCursor_zoom_in; + break; + case StyleCursorKind::ZoomOut: + c = eCursor_zoom_out; + break; + case StyleCursorKind::NotAllowed: + c = eCursor_not_allowed; + break; + case StyleCursorKind::ColResize: + c = eCursor_col_resize; + break; + case StyleCursorKind::RowResize: + c = eCursor_row_resize; + break; + case StyleCursorKind::NoDrop: + c = eCursor_no_drop; + break; + case StyleCursorKind::VerticalText: + c = eCursor_vertical_text; + break; + case StyleCursorKind::AllScroll: + c = eCursor_all_scroll; + break; + case StyleCursorKind::NeswResize: + c = eCursor_nesw_resize; + break; + case StyleCursorKind::NwseResize: + c = eCursor_nwse_resize; + break; + case StyleCursorKind::NsResize: + c = eCursor_ns_resize; + break; + case StyleCursorKind::EwResize: + c = eCursor_ew_resize; + break; + case StyleCursorKind::None: + c = eCursor_none; + break; + default: + MOZ_ASSERT_UNREACHABLE("Unknown cursor kind"); + c = eCursor_standard; + break; + } + + uint32_t x = aHotspot ? aHotspot->x.value : 0; + uint32_t y = aHotspot ? aHotspot->y.value : 0; + aWidget->SetCursor(nsIWidget::Cursor{c, aContainer, x, y, aResolution}); + return NS_OK; +} + +class MOZ_STACK_CLASS ESMEventCB : public EventDispatchingCallback { + public: + explicit ESMEventCB(nsIContent* aTarget) : mTarget(aTarget) {} + + MOZ_CAN_RUN_SCRIPT + void HandleEvent(EventChainPostVisitor& aVisitor) override { + if (aVisitor.mPresContext) { + nsIFrame* frame = aVisitor.mPresContext->GetPrimaryFrameFor(mTarget); + if (frame) { + frame->HandleEvent(aVisitor.mPresContext, aVisitor.mEvent->AsGUIEvent(), + &aVisitor.mEventStatus); + } + } + } + + nsCOMPtr<nsIContent> mTarget; +}; + +static UniquePtr<WidgetMouseEvent> CreateMouseOrPointerWidgetEvent( + WidgetMouseEvent* aMouseEvent, EventMessage aMessage, + EventTarget* aRelatedTarget) { + WidgetPointerEvent* sourcePointer = aMouseEvent->AsPointerEvent(); + UniquePtr<WidgetMouseEvent> newEvent; + if (sourcePointer) { + AUTO_PROFILER_LABEL("CreateMouseOrPointerWidgetEvent", OTHER); + + WidgetPointerEvent* newPointerEvent = new WidgetPointerEvent( + aMouseEvent->IsTrusted(), aMessage, aMouseEvent->mWidget); + newPointerEvent->mIsPrimary = sourcePointer->mIsPrimary; + newPointerEvent->mWidth = sourcePointer->mWidth; + newPointerEvent->mHeight = sourcePointer->mHeight; + newPointerEvent->mInputSource = sourcePointer->mInputSource; + + newEvent = WrapUnique(newPointerEvent); + } else { + newEvent = MakeUnique<WidgetMouseEvent>(aMouseEvent->IsTrusted(), aMessage, + aMouseEvent->mWidget, + WidgetMouseEvent::eReal); + } + newEvent->mRelatedTarget = aRelatedTarget; + newEvent->mRefPoint = aMouseEvent->mRefPoint; + newEvent->mModifiers = aMouseEvent->mModifiers; + newEvent->mButton = aMouseEvent->mButton; + newEvent->mButtons = aMouseEvent->mButtons; + newEvent->mPressure = aMouseEvent->mPressure; + newEvent->mInputSource = aMouseEvent->mInputSource; + newEvent->pointerId = aMouseEvent->pointerId; + + return newEvent; +} + +nsIFrame* EventStateManager::DispatchMouseOrPointerEvent( + WidgetMouseEvent* aMouseEvent, EventMessage aMessage, + nsIContent* aTargetContent, nsIContent* aRelatedContent) { + // http://dvcs.w3.org/hg/webevents/raw-file/default/mouse-lock.html#methods + // "[When the mouse is locked on an element...e]vents that require the concept + // of a mouse cursor must not be dispatched (for example: mouseover, + // mouseout). + if (PointerLockManager::IsLocked() && + (aMessage == eMouseLeave || aMessage == eMouseEnter || + aMessage == eMouseOver || aMessage == eMouseOut)) { + mCurrentTargetContent = nullptr; + nsCOMPtr<Element> pointerLockedElement = + PointerLockManager::GetLockedElement(); + if (!pointerLockedElement) { + NS_WARNING("Should have pointer locked element, but didn't."); + return nullptr; + } + return mPresContext->GetPrimaryFrameFor(pointerLockedElement); + } + + mCurrentTargetContent = nullptr; + + if (!aTargetContent) { + return nullptr; + } + + nsCOMPtr<nsIContent> targetContent = aTargetContent; + nsCOMPtr<nsIContent> relatedContent = aRelatedContent; + + UniquePtr<WidgetMouseEvent> dispatchEvent = + CreateMouseOrPointerWidgetEvent(aMouseEvent, aMessage, relatedContent); + + AutoWeakFrame previousTarget = mCurrentTarget; + mCurrentTargetContent = targetContent; + + nsIFrame* targetFrame = nullptr; + + nsEventStatus status = nsEventStatus_eIgnore; + ESMEventCB callback(targetContent); + RefPtr<nsPresContext> presContext = mPresContext; + EventDispatcher::Dispatch(targetContent, presContext, dispatchEvent.get(), + nullptr, &status, &callback); + + if (mPresContext) { + // Although the primary frame was checked in event callback, it may not be + // the same object after event dispatch and handling, so refetch it. + targetFrame = mPresContext->GetPrimaryFrameFor(targetContent); + + // If we are entering/leaving remote content, dispatch a mouse enter/exit + // event to the remote frame. + if (IsTopLevelRemoteTarget(targetContent)) { + if (aMessage == eMouseOut) { + // For remote content, send a puppet widget mouse exit event. + UniquePtr<WidgetMouseEvent> remoteEvent = + CreateMouseOrPointerWidgetEvent(aMouseEvent, eMouseExitFromWidget, + relatedContent); + remoteEvent->mExitFrom = Some(WidgetMouseEvent::ePuppet); + + // mCurrentTarget is set to the new target, so we must reset it to the + // old target and then dispatch a cross-process event. (mCurrentTarget + // will be set back below.) HandleCrossProcessEvent will query for the + // proper target via GetEventTarget which will return mCurrentTarget. + mCurrentTarget = targetFrame; + HandleCrossProcessEvent(remoteEvent.get(), &status); + } else if (aMessage == eMouseOver) { + UniquePtr<WidgetMouseEvent> remoteEvent = + CreateMouseOrPointerWidgetEvent(aMouseEvent, eMouseEnterIntoWidget, + relatedContent); + HandleCrossProcessEvent(remoteEvent.get(), &status); + } + } + } + + mCurrentTargetContent = nullptr; + mCurrentTarget = previousTarget; + + return targetFrame; +} + +static nsIContent* FindCommonAncestor(nsIContent* aNode1, nsIContent* aNode2) { + if (!aNode1 || !aNode2) { + return nullptr; + } + return nsContentUtils::GetCommonFlattenedTreeAncestor(aNode1, aNode2); +} + +class EnterLeaveDispatcher { + public: + EnterLeaveDispatcher(EventStateManager* aESM, nsIContent* aTarget, + nsIContent* aRelatedTarget, + WidgetMouseEvent* aMouseEvent, + EventMessage aEventMessage) + : mESM(aESM), mMouseEvent(aMouseEvent), mEventMessage(aEventMessage) { + nsPIDOMWindowInner* win = + aTarget ? aTarget->OwnerDoc()->GetInnerWindow() : nullptr; + if (aMouseEvent->AsPointerEvent() + ? win && win->HasPointerEnterLeaveEventListeners() + : win && win->HasMouseEnterLeaveEventListeners()) { + mRelatedTarget = + aRelatedTarget ? aRelatedTarget->FindFirstNonChromeOnlyAccessContent() + : nullptr; + nsINode* commonParent = FindCommonAncestor(aTarget, aRelatedTarget); + nsIContent* current = aTarget; + // Note, it is ok if commonParent is null! + while (current && current != commonParent) { + if (!current->ChromeOnlyAccess()) { + mTargets.AppendObject(current); + } + // mouseenter/leave is fired only on elements. + current = current->GetFlattenedTreeParent(); + } + } + } + + // TODO: Convert this to MOZ_CAN_RUN_SCRIPT (bug 1415230) + MOZ_CAN_RUN_SCRIPT_BOUNDARY void Dispatch() { + if (mEventMessage == eMouseEnter || mEventMessage == ePointerEnter) { + for (int32_t i = mTargets.Count() - 1; i >= 0; --i) { + mESM->DispatchMouseOrPointerEvent(mMouseEvent, mEventMessage, + MOZ_KnownLive(mTargets[i]), + mRelatedTarget); + } + } else { + for (int32_t i = 0; i < mTargets.Count(); ++i) { + mESM->DispatchMouseOrPointerEvent(mMouseEvent, mEventMessage, + MOZ_KnownLive(mTargets[i]), + mRelatedTarget); + } + } + } + + // Nothing overwrites anything after constructor. Please remove MOZ_KnownLive + // and MOZ_KNOWN_LIVE if anything marked as such becomes mutable. + const RefPtr<EventStateManager> mESM; + nsCOMArray<nsIContent> mTargets; + MOZ_KNOWN_LIVE nsCOMPtr<nsIContent> mRelatedTarget; + WidgetMouseEvent* mMouseEvent; + EventMessage mEventMessage; +}; + +void EventStateManager::NotifyMouseOut(WidgetMouseEvent* aMouseEvent, + nsIContent* aMovingInto) { + RefPtr<OverOutElementsWrapper> wrapper = GetWrapperByEventID(aMouseEvent); + + if (!wrapper || !wrapper->mLastOverElement) { + return; + } + // Before firing mouseout, check for recursion + if (wrapper->mLastOverElement == wrapper->mFirstOutEventElement) { + return; + } + + if (RefPtr<nsFrameLoaderOwner> flo = + do_QueryObject(wrapper->mLastOverElement)) { + if (BrowsingContext* bc = flo->GetExtantBrowsingContext()) { + if (nsIDocShell* docshell = bc->GetDocShell()) { + if (RefPtr<nsPresContext> presContext = docshell->GetPresContext()) { + EventStateManager* kidESM = presContext->EventStateManager(); + // Not moving into any element in this subdocument + kidESM->NotifyMouseOut(aMouseEvent, nullptr); + } + } + } + } + // That could have caused DOM events which could wreak havoc. Reverify + // things and be careful. + if (!wrapper->mLastOverElement) { + return; + } + + // Store the first mouseOut event we fire and don't refire mouseOut + // to that element while the first mouseOut is still ongoing. + wrapper->mFirstOutEventElement = wrapper->mLastOverElement; + + // Don't touch hover state if aMovingInto is non-null. Caller will update + // hover state itself, and we have optimizations for hover switching between + // two nearby elements both deep in the DOM tree that would be defeated by + // switching the hover state to null here. + bool isPointer = aMouseEvent->mClass == ePointerEventClass; + if (!aMovingInto && !isPointer) { + // Unset :hover + SetContentState(nullptr, ElementState::HOVER); + } + + EnterLeaveDispatcher leaveDispatcher(this, wrapper->mLastOverElement, + aMovingInto, aMouseEvent, + isPointer ? ePointerLeave : eMouseLeave); + + // Fire mouseout + nsCOMPtr<nsIContent> lastOverElement = wrapper->mLastOverElement; + DispatchMouseOrPointerEvent(aMouseEvent, isPointer ? ePointerOut : eMouseOut, + lastOverElement, aMovingInto); + leaveDispatcher.Dispatch(); + + wrapper->mLastOverFrame = nullptr; + wrapper->mLastOverElement = nullptr; + + // Turn recursion protection back off + wrapper->mFirstOutEventElement = nullptr; +} + +void EventStateManager::RecomputeMouseEnterStateForRemoteFrame( + Element& aElement) { + if (!mMouseEnterLeaveHelper || + mMouseEnterLeaveHelper->mLastOverElement != &aElement) { + return; + } + + if (BrowserParent* remote = BrowserParent::GetFrom(&aElement)) { + remote->MouseEnterIntoWidget(); + } +} + +void EventStateManager::NotifyMouseOver(WidgetMouseEvent* aMouseEvent, + nsIContent* aContent) { + NS_ASSERTION(aContent, "Mouse must be over something"); + + RefPtr<OverOutElementsWrapper> wrapper = GetWrapperByEventID(aMouseEvent); + + if (!wrapper || wrapper->mLastOverElement == aContent) return; + + // Before firing mouseover, check for recursion + if (aContent == wrapper->mFirstOverEventElement) return; + + // Check to see if we're a subdocument and if so update the parent + // document's ESM state to indicate that the mouse is over the + // content associated with our subdocument. + EnsureDocument(mPresContext); + if (Document* parentDoc = mDocument->GetInProcessParentDocument()) { + if (nsCOMPtr<nsIContent> docContent = mDocument->GetEmbedderElement()) { + if (PresShell* parentPresShell = parentDoc->GetPresShell()) { + RefPtr<EventStateManager> parentESM = + parentPresShell->GetPresContext()->EventStateManager(); + parentESM->NotifyMouseOver(aMouseEvent, docContent); + } + } + } + // Firing the DOM event in the parent document could cause all kinds + // of havoc. Reverify and take care. + if (wrapper->mLastOverElement == aContent) return; + + // Remember mLastOverElement as the related content for the + // DispatchMouseOrPointerEvent() call below, since NotifyMouseOut() resets it, + // bug 298477. + nsCOMPtr<nsIContent> lastOverElement = wrapper->mLastOverElement; + + bool isPointer = aMouseEvent->mClass == ePointerEventClass; + + EnterLeaveDispatcher enterDispatcher(this, aContent, lastOverElement, + aMouseEvent, + isPointer ? ePointerEnter : eMouseEnter); + + if (!isPointer) { + SetContentState(aContent, ElementState::HOVER); + } + + NotifyMouseOut(aMouseEvent, aContent); + + // Store the first mouseOver event we fire and don't refire mouseOver + // to that element while the first mouseOver is still ongoing. + wrapper->mFirstOverEventElement = aContent; + + // Fire mouseover + wrapper->mLastOverFrame = DispatchMouseOrPointerEvent( + aMouseEvent, isPointer ? ePointerOver : eMouseOver, aContent, + lastOverElement); + enterDispatcher.Dispatch(); + wrapper->mLastOverElement = aContent; + + // Turn recursion protection back off + wrapper->mFirstOverEventElement = nullptr; +} + +// Returns the center point of the window's client area. This is +// in widget coordinates, i.e. relative to the widget's top-left +// corner, not in screen coordinates, the same units that UIEvent:: +// refpoint is in. It may not be the exact center of the window if +// the platform requires rounding the coordinate. +static LayoutDeviceIntPoint GetWindowClientRectCenter(nsIWidget* aWidget) { + NS_ENSURE_TRUE(aWidget, LayoutDeviceIntPoint(0, 0)); + + LayoutDeviceIntRect rect = aWidget->GetClientBounds(); + LayoutDeviceIntPoint point(rect.x + rect.width / 2, rect.y + rect.height / 2); + int32_t round = aWidget->RoundsWidgetCoordinatesTo(); + point.x = point.x / round * round; + point.y = point.y / round * round; + return point - aWidget->WidgetToScreenOffset(); +} + +void EventStateManager::GeneratePointerEnterExit(EventMessage aMessage, + WidgetMouseEvent* aEvent) { + WidgetPointerEvent pointerEvent(*aEvent); + pointerEvent.mMessage = aMessage; + GenerateMouseEnterExit(&pointerEvent); +} + +/* static */ +void EventStateManager::UpdateLastRefPointOfMouseEvent( + WidgetMouseEvent* aMouseEvent) { + if (aMouseEvent->mMessage != eMouseMove && + aMouseEvent->mMessage != ePointerMove) { + return; + } + + // Mouse movement is reported on the MouseEvent.movement{X,Y} fields. + // Movement is calculated in UIEvent::GetMovementPoint() as: + // previous_mousemove_mRefPoint - current_mousemove_mRefPoint. + if (PointerLockManager::IsLocked() && aMouseEvent->mWidget) { + // The pointer is locked. If the pointer is not located at the center of + // the window, dispatch a synthetic mousemove to return the pointer there. + // Doing this between "real" pointer moves gives the impression that the + // (locked) pointer can continue moving and won't stop at the screen + // boundary. We cancel the synthetic event so that we don't end up + // dispatching the centering move event to content. + aMouseEvent->mLastRefPoint = + GetWindowClientRectCenter(aMouseEvent->mWidget); + + } else if (sLastRefPoint == kInvalidRefPoint) { + // We don't have a valid previous mousemove mRefPoint. This is either + // the first move we've encountered, or the mouse has just re-entered + // the application window. We should report (0,0) movement for this + // case, so make the current and previous mRefPoints the same. + aMouseEvent->mLastRefPoint = aMouseEvent->mRefPoint; + } else { + aMouseEvent->mLastRefPoint = sLastRefPoint; + } +} + +/* static */ +void EventStateManager::ResetPointerToWindowCenterWhilePointerLocked( + WidgetMouseEvent* aMouseEvent) { + MOZ_ASSERT(PointerLockManager::IsLocked()); + if ((aMouseEvent->mMessage != eMouseMove && + aMouseEvent->mMessage != ePointerMove) || + !aMouseEvent->mWidget) { + return; + } + + // We generate pointermove from mousemove event, so only synthesize native + // mouse move and update sSynthCenteringPoint by mousemove event. + bool updateSynthCenteringPoint = aMouseEvent->mMessage == eMouseMove; + + // The pointer is locked. If the pointer is not located at the center of + // the window, dispatch a synthetic mousemove to return the pointer there. + // Doing this between "real" pointer moves gives the impression that the + // (locked) pointer can continue moving and won't stop at the screen + // boundary. We cancel the synthetic event so that we don't end up + // dispatching the centering move event to content. + LayoutDeviceIntPoint center = GetWindowClientRectCenter(aMouseEvent->mWidget); + + if (aMouseEvent->mRefPoint != center && updateSynthCenteringPoint) { + // Mouse move doesn't finish at the center of the window. Dispatch a + // synthetic native mouse event to move the pointer back to the center + // of the window, to faciliate more movement. But first, record that + // we've dispatched a synthetic mouse movement, so we can cancel it + // in the other branch here. + sSynthCenteringPoint = center; + // XXX Once we fix XXX comments in SetPointerLock about this API, we could + // restrict that this API works only in the automation mode or in the + // pointer locked situation. + aMouseEvent->mWidget->SynthesizeNativeMouseMove( + center + aMouseEvent->mWidget->WidgetToScreenOffset(), nullptr); + } else if (aMouseEvent->mRefPoint == sSynthCenteringPoint) { + // This is the "synthetic native" event we dispatched to re-center the + // pointer. Cancel it so we don't expose the centering move to content. + aMouseEvent->StopPropagation(); + // Clear sSynthCenteringPoint so we don't cancel other events + // targeted at the center. + if (updateSynthCenteringPoint) { + sSynthCenteringPoint = kInvalidRefPoint; + } + } +} + +/* static */ +void EventStateManager::UpdateLastPointerPosition( + WidgetMouseEvent* aMouseEvent) { + if (aMouseEvent->mMessage != eMouseMove) { + return; + } + sLastRefPoint = aMouseEvent->mRefPoint; +} + +void EventStateManager::GenerateMouseEnterExit(WidgetMouseEvent* aMouseEvent) { + EnsureDocument(mPresContext); + if (!mDocument) return; + + // Hold onto old target content through the event and reset after. + nsCOMPtr<nsIContent> targetBeforeEvent = mCurrentTargetContent; + + switch (aMouseEvent->mMessage) { + case eMouseMove: + case ePointerMove: + case ePointerDown: + case ePointerGotCapture: { + // Get the target content target (mousemove target == mouseover target) + nsCOMPtr<nsIContent> targetElement = GetEventTargetContent(aMouseEvent); + if (!targetElement) { + // We're always over the document root, even if we're only + // over dead space in a page (whose frame is not associated with + // any content) or in print preview dead space + targetElement = mDocument->GetRootElement(); + } + if (targetElement) { + NotifyMouseOver(aMouseEvent, targetElement); + } + } break; + case ePointerUp: { + // Get the target content target (mousemove target == mouseover target) + nsCOMPtr<nsIContent> targetElement = GetEventTargetContent(aMouseEvent); + if (!targetElement) { + // We're always over the document root, even if we're only + // over dead space in a page (whose frame is not associated with + // any content) or in print preview dead space + targetElement = mDocument->GetRootElement(); + } + if (targetElement) { + RefPtr<OverOutElementsWrapper> helper = + GetWrapperByEventID(aMouseEvent); + if (helper) { + helper->mLastOverElement = targetElement; + } + NotifyMouseOut(aMouseEvent, nullptr); + } + } break; + case ePointerLeave: + case ePointerCancel: + case eMouseExitFromWidget: { + // This is actually the window mouse exit or pointer leave event. We're + // not moving into any new element. + + RefPtr<OverOutElementsWrapper> helper = GetWrapperByEventID(aMouseEvent); + if (helper && helper->mLastOverFrame && + nsContentUtils::GetTopLevelWidget(aMouseEvent->mWidget) != + nsContentUtils::GetTopLevelWidget( + helper->mLastOverFrame->GetNearestWidget())) { + // the Mouse/PointerOut event widget doesn't have same top widget with + // mLastOverFrame, it's a spurious event for mLastOverFrame + break; + } + + // Reset sLastRefPoint, so that we'll know not to report any + // movement the next time we re-enter the window. + sLastRefPoint = kInvalidRefPoint; + + NotifyMouseOut(aMouseEvent, nullptr); + } break; + default: + break; + } + + // reset mCurretTargetContent to what it was + mCurrentTargetContent = targetBeforeEvent; +} + +OverOutElementsWrapper* EventStateManager::GetWrapperByEventID( + WidgetMouseEvent* aEvent) { + WidgetPointerEvent* pointer = aEvent->AsPointerEvent(); + if (!pointer) { + MOZ_ASSERT(aEvent->AsMouseEvent() != nullptr); + if (!mMouseEnterLeaveHelper) { + mMouseEnterLeaveHelper = new OverOutElementsWrapper(); + } + return mMouseEnterLeaveHelper; + } + return mPointersEnterLeaveHelper.GetOrInsertNew(pointer->pointerId); +} + +/* static */ +void EventStateManager::SetPointerLock(nsIWidget* aWidget, + nsIContent* aElement) { + // Reset mouse wheel transaction + WheelTransaction::EndTransaction(); + + // Deal with DnD events + nsCOMPtr<nsIDragService> dragService = + do_GetService("@mozilla.org/widget/dragservice;1"); + + if (PointerLockManager::IsLocked()) { + MOZ_ASSERT(aWidget, "Locking pointer requires a widget"); + + // Release all pointer capture when a pointer lock is successfully applied + // on an element. + PointerEventHandler::ReleaseAllPointerCapture(); + + // Store the last known ref point so we can reposition the pointer after + // unlock. + sPreLockPoint = sLastRefPoint; + + // Fire a synthetic mouse move to ensure event state is updated. We first + // set the mouse to the center of the window, so that the mouse event + // doesn't report any movement. + // XXX Cannot we do synthesize the native mousemove in the parent process + // with calling LockNativePointer below? Then, we could make this API + // work only in the automation mode. + sLastRefPoint = GetWindowClientRectCenter(aWidget); + aWidget->SynthesizeNativeMouseMove( + sLastRefPoint + aWidget->WidgetToScreenOffset(), nullptr); + + // Suppress DnD + if (dragService) { + dragService->Suppress(); + } + + // Activate native pointer lock on platforms where it is required (Wayland) + aWidget->LockNativePointer(); + } else { + if (aWidget) { + // Deactivate native pointer lock on platforms where it is required + aWidget->UnlockNativePointer(); + } + + // Unlocking, so return pointer to the original position by firing a + // synthetic mouse event. We first reset sLastRefPoint to its + // pre-pointerlock position, so that the synthetic mouse event reports + // no movement. + sLastRefPoint = sPreLockPoint; + // Reset SynthCenteringPoint to invalid so that next time we start + // locking pointer, it has its initial value. + sSynthCenteringPoint = kInvalidRefPoint; + if (aWidget) { + // XXX Cannot we do synthesize the native mousemove in the parent process + // with calling `UnlockNativePointer` above? Then, we could make this + // API work only in the automation mode. + aWidget->SynthesizeNativeMouseMove( + sPreLockPoint + aWidget->WidgetToScreenOffset(), nullptr); + } + + // Unsuppress DnD + if (dragService) { + dragService->Unsuppress(); + } + } +} + +void EventStateManager::GenerateDragDropEnterExit(nsPresContext* aPresContext, + WidgetDragEvent* aDragEvent) { + // Hold onto old target content through the event and reset after. + nsCOMPtr<nsIContent> targetBeforeEvent = mCurrentTargetContent; + + switch (aDragEvent->mMessage) { + case eDragOver: { + // when dragging from one frame to another, events are fired in the + // order: dragexit, dragenter, dragleave + if (sLastDragOverFrame != mCurrentTarget) { + // We'll need the content, too, to check if it changed separately from + // the frames. + nsCOMPtr<nsIContent> lastContent; + nsCOMPtr<nsIContent> targetContent; + mCurrentTarget->GetContentForEvent(aDragEvent, + getter_AddRefs(targetContent)); + if (targetContent && targetContent->IsText()) { + targetContent = targetContent->GetFlattenedTreeParent(); + } + + if (sLastDragOverFrame) { + // The frame has changed but the content may not have. Check before + // dispatching to content + sLastDragOverFrame->GetContentForEvent(aDragEvent, + getter_AddRefs(lastContent)); + if (lastContent && lastContent->IsText()) { + lastContent = lastContent->GetFlattenedTreeParent(); + } + + RefPtr<nsPresContext> presContext = sLastDragOverFrame->PresContext(); + FireDragEnterOrExit(presContext, aDragEvent, eDragExit, targetContent, + lastContent, sLastDragOverFrame); + nsIContent* target = sLastDragOverFrame + ? sLastDragOverFrame.GetFrame()->GetContent() + : nullptr; + // XXXedgar, look like we need to consider fission OOP iframe, too. + if (IsTopLevelRemoteTarget(target)) { + // Dragging something and moving from web content to chrome only + // fires dragexit and dragleave to xul:browser. We have to forward + // dragexit to sLastDragOverFrame when its content is a remote + // target. We don't forward dragleave since it's generated from + // dragexit. + WidgetDragEvent remoteEvent(aDragEvent->IsTrusted(), eDragExit, + aDragEvent->mWidget); + remoteEvent.AssignDragEventData(*aDragEvent, true); + remoteEvent.mFlags.mIsSynthesizedForTests = + aDragEvent->mFlags.mIsSynthesizedForTests; + nsEventStatus remoteStatus = nsEventStatus_eIgnore; + HandleCrossProcessEvent(&remoteEvent, &remoteStatus); + } + } + + AutoWeakFrame currentTraget = mCurrentTarget; + FireDragEnterOrExit(aPresContext, aDragEvent, eDragEnter, lastContent, + targetContent, currentTraget); + + if (sLastDragOverFrame) { + RefPtr<nsPresContext> presContext = sLastDragOverFrame->PresContext(); + FireDragEnterOrExit(presContext, aDragEvent, eDragLeave, + targetContent, lastContent, sLastDragOverFrame); + } + + sLastDragOverFrame = mCurrentTarget; + } + } break; + + case eDragExit: { + // This is actually the window mouse exit event. + if (sLastDragOverFrame) { + nsCOMPtr<nsIContent> lastContent; + sLastDragOverFrame->GetContentForEvent(aDragEvent, + getter_AddRefs(lastContent)); + + RefPtr<nsPresContext> lastDragOverFramePresContext = + sLastDragOverFrame->PresContext(); + FireDragEnterOrExit(lastDragOverFramePresContext, aDragEvent, eDragExit, + nullptr, lastContent, sLastDragOverFrame); + FireDragEnterOrExit(lastDragOverFramePresContext, aDragEvent, + eDragLeave, nullptr, lastContent, + sLastDragOverFrame); + + sLastDragOverFrame = nullptr; + } + } break; + + default: + break; + } + + // reset mCurretTargetContent to what it was + mCurrentTargetContent = targetBeforeEvent; + + // Now flush all pending notifications, for better responsiveness. + FlushLayout(aPresContext); +} + +void EventStateManager::FireDragEnterOrExit(nsPresContext* aPresContext, + WidgetDragEvent* aDragEvent, + EventMessage aMessage, + nsIContent* aRelatedTarget, + nsIContent* aTargetContent, + AutoWeakFrame& aTargetFrame) { + MOZ_ASSERT(aMessage == eDragLeave || aMessage == eDragExit || + aMessage == eDragEnter); + nsEventStatus status = nsEventStatus_eIgnore; + WidgetDragEvent event(aDragEvent->IsTrusted(), aMessage, aDragEvent->mWidget); + event.AssignDragEventData(*aDragEvent, false); + event.mFlags.mIsSynthesizedForTests = + aDragEvent->mFlags.mIsSynthesizedForTests; + event.mRelatedTarget = aRelatedTarget; + if (aMessage == eDragExit && !StaticPrefs::dom_event_dragexit_enabled()) { + event.mFlags.mOnlyChromeDispatch = true; + } + + mCurrentTargetContent = aTargetContent; + + if (aTargetContent != aRelatedTarget) { + // XXX This event should still go somewhere!! + if (aTargetContent) { + EventDispatcher::Dispatch(aTargetContent, aPresContext, &event, nullptr, + &status); + } + + // adjust the drag hover if the dragenter event was cancelled or this is a + // drag exit + if (status == nsEventStatus_eConsumeNoDefault || aMessage == eDragExit) { + SetContentState((aMessage == eDragEnter) ? aTargetContent : nullptr, + ElementState::DRAGOVER); + } + + // collect any changes to moz cursor settings stored in the event's + // data transfer. + UpdateDragDataTransfer(&event); + } + + // Finally dispatch the event to the frame + if (aTargetFrame) { + aTargetFrame->HandleEvent(aPresContext, &event, &status); + } +} + +void EventStateManager::UpdateDragDataTransfer(WidgetDragEvent* dragEvent) { + NS_ASSERTION(dragEvent, "drag event is null in UpdateDragDataTransfer!"); + if (!dragEvent->mDataTransfer) { + return; + } + + nsCOMPtr<nsIDragSession> dragSession = nsContentUtils::GetDragSession(); + + if (dragSession) { + // the initial dataTransfer is the one from the dragstart event that + // was set on the dragSession when the drag began. + RefPtr<DataTransfer> initialDataTransfer = dragSession->GetDataTransfer(); + if (initialDataTransfer) { + // retrieve the current moz cursor setting and save it. + nsAutoString mozCursor; + dragEvent->mDataTransfer->GetMozCursor(mozCursor); + initialDataTransfer->SetMozCursor(mozCursor); + } + } +} + +nsresult EventStateManager::SetClickCount(WidgetMouseEvent* aEvent, + nsEventStatus* aStatus, + nsIContent* aOverrideClickTarget) { + nsCOMPtr<nsIContent> mouseContent = aOverrideClickTarget; + if (!mouseContent && mCurrentTarget) { + mCurrentTarget->GetContentForEvent(aEvent, getter_AddRefs(mouseContent)); + } + if (mouseContent && mouseContent->IsText()) { + nsINode* parent = mouseContent->GetFlattenedTreeParentNode(); + if (parent && parent->IsContent()) { + mouseContent = parent->AsContent(); + } + } + + switch (aEvent->mButton) { + case MouseButton::ePrimary: + if (aEvent->mMessage == eMouseDown) { + mLastLeftMouseDownContent = + !aEvent->mClickEventPrevented ? mouseContent : nullptr; + } else if (aEvent->mMessage == eMouseUp) { + aEvent->mClickTarget = + !aEvent->mClickEventPrevented + ? nsContentUtils::GetCommonAncestorUnderInteractiveContent( + mouseContent, mLastLeftMouseDownContent) + : nullptr; + if (aEvent->mClickTarget) { + aEvent->mClickCount = mLClickCount; + mLClickCount = 0; + } else { + aEvent->mClickCount = 0; + } + mLastLeftMouseDownContent = nullptr; + } + break; + + case MouseButton::eMiddle: + if (aEvent->mMessage == eMouseDown) { + mLastMiddleMouseDownContent = + !aEvent->mClickEventPrevented ? mouseContent : nullptr; + } else if (aEvent->mMessage == eMouseUp) { + aEvent->mClickTarget = + !aEvent->mClickEventPrevented + ? nsContentUtils::GetCommonAncestorUnderInteractiveContent( + mouseContent, mLastMiddleMouseDownContent) + : nullptr; + if (aEvent->mClickTarget) { + aEvent->mClickCount = mMClickCount; + mMClickCount = 0; + } else { + aEvent->mClickCount = 0; + } + mLastMiddleMouseDownContent = nullptr; + } + break; + + case MouseButton::eSecondary: + if (aEvent->mMessage == eMouseDown) { + mLastRightMouseDownContent = + !aEvent->mClickEventPrevented ? mouseContent : nullptr; + } else if (aEvent->mMessage == eMouseUp) { + aEvent->mClickTarget = + !aEvent->mClickEventPrevented + ? nsContentUtils::GetCommonAncestorUnderInteractiveContent( + mouseContent, mLastRightMouseDownContent) + : nullptr; + if (aEvent->mClickTarget) { + aEvent->mClickCount = mRClickCount; + mRClickCount = 0; + } else { + aEvent->mClickCount = 0; + } + mLastRightMouseDownContent = nullptr; + } + break; + } + + return NS_OK; +} + +// static +bool EventStateManager::EventCausesClickEvents( + const WidgetMouseEvent& aMouseEvent) { + if (NS_WARN_IF(aMouseEvent.mMessage != eMouseUp)) { + return false; + } + // If the mouseup event is synthesized event, we don't need to dispatch + // click events. + if (!aMouseEvent.IsReal()) { + return false; + } + // If mouse is still over same element, clickcount will be > 1. + // If it has moved it will be zero, so no click. + if (!aMouseEvent.mClickCount || !aMouseEvent.mClickTarget) { + return false; + } + // If click event was explicitly prevented, we shouldn't dispatch it. + if (aMouseEvent.mClickEventPrevented) { + return false; + } + // Check that the window isn't disabled before firing a click + // (see bug 366544). + return !(aMouseEvent.mWidget && !aMouseEvent.mWidget->IsEnabled()); +} + +nsresult EventStateManager::InitAndDispatchClickEvent( + WidgetMouseEvent* aMouseUpEvent, nsEventStatus* aStatus, + EventMessage aMessage, PresShell* aPresShell, nsIContent* aMouseUpContent, + AutoWeakFrame aCurrentTarget, bool aNoContentDispatch, + nsIContent* aOverrideClickTarget) { + MOZ_ASSERT(aMouseUpEvent); + MOZ_ASSERT(EventCausesClickEvents(*aMouseUpEvent)); + MOZ_ASSERT(aMouseUpContent || aCurrentTarget || aOverrideClickTarget); + + WidgetMouseEvent event(aMouseUpEvent->IsTrusted(), aMessage, + aMouseUpEvent->mWidget, WidgetMouseEvent::eReal); + + event.mRefPoint = aMouseUpEvent->mRefPoint; + event.mClickCount = aMouseUpEvent->mClickCount; + event.mModifiers = aMouseUpEvent->mModifiers; + event.mButtons = aMouseUpEvent->mButtons; + event.mTimeStamp = aMouseUpEvent->mTimeStamp; + event.mFlags.mOnlyChromeDispatch = + aNoContentDispatch && !aMouseUpEvent->mUseLegacyNonPrimaryDispatch; + event.mFlags.mNoContentDispatch = aNoContentDispatch; + event.mButton = aMouseUpEvent->mButton; + event.pointerId = aMouseUpEvent->pointerId; + event.mInputSource = aMouseUpEvent->mInputSource; + nsIContent* target = aMouseUpContent; + nsIFrame* targetFrame = aCurrentTarget; + if (aOverrideClickTarget) { + target = aOverrideClickTarget; + targetFrame = aOverrideClickTarget->GetPrimaryFrame(); + } + + if (!target->IsInComposedDoc()) { + return NS_OK; + } + + // Use local event status for each click event dispatching since it'll be + // cleared by EventStateManager::PreHandleEvent(). Therefore, dispatching + // an event means that previous event status will be ignored. + nsEventStatus status = nsEventStatus_eIgnore; + nsresult rv = aPresShell->HandleEventWithTarget( + &event, targetFrame, MOZ_KnownLive(target), &status); + + // Copy mMultipleActionsPrevented flag from a click event to the mouseup + // event only when it's set to true. It may be set to true if an editor has + // already handled it. This is important to avoid two or more default + // actions handled here. + aMouseUpEvent->mFlags.mMultipleActionsPrevented |= + event.mFlags.mMultipleActionsPrevented; + // If current status is nsEventStatus_eConsumeNoDefault, we don't need to + // overwrite it. + if (*aStatus == nsEventStatus_eConsumeNoDefault) { + return rv; + } + // If new status is nsEventStatus_eConsumeNoDefault or + // nsEventStatus_eConsumeDoDefault, use it. + if (status == nsEventStatus_eConsumeNoDefault || + status == nsEventStatus_eConsumeDoDefault) { + *aStatus = status; + return rv; + } + // Otherwise, keep the original status. + return rv; +} + +nsresult EventStateManager::PostHandleMouseUp( + WidgetMouseEvent* aMouseUpEvent, nsEventStatus* aStatus, + nsIContent* aOverrideClickTarget) { + MOZ_ASSERT(aMouseUpEvent); + MOZ_ASSERT(EventCausesClickEvents(*aMouseUpEvent)); + MOZ_ASSERT(aStatus); + + RefPtr<PresShell> presShell = mPresContext->GetPresShell(); + if (!presShell) { + return NS_OK; + } + + nsCOMPtr<nsIContent> clickTarget = + nsIContent::FromEventTargetOrNull(aMouseUpEvent->mClickTarget); + NS_ENSURE_STATE(clickTarget); + + // Fire click events if the event target is still available. + // Note that do not include the eMouseUp event's status since we ignore it + // for compatibility with the other browsers. + nsEventStatus status = nsEventStatus_eIgnore; + nsresult rv = DispatchClickEvents(presShell, aMouseUpEvent, &status, + clickTarget, aOverrideClickTarget); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // Do not do anything if preceding click events are consumed. + // Note that Chromium dispatches "paste" event and actually pates clipboard + // text into focused editor even if the preceding click events are consumed. + // However, this is different from our traditional behavior and does not + // conform to DOM events. If we need to keep compatibility with Chromium, + // we should change it later. + if (status == nsEventStatus_eConsumeNoDefault) { + *aStatus = nsEventStatus_eConsumeNoDefault; + return NS_OK; + } + + // Handle middle click paste if it's enabled and the mouse button is middle. + if (aMouseUpEvent->mButton != MouseButton::eMiddle || + !WidgetMouseEvent::IsMiddleClickPasteEnabled()) { + return NS_OK; + } + DebugOnly<nsresult> rvIgnored = + HandleMiddleClickPaste(presShell, aMouseUpEvent, &status, nullptr); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored), + "Failed to paste for a middle click"); + + // If new status is nsEventStatus_eConsumeNoDefault or + // nsEventStatus_eConsumeDoDefault, use it. + if (*aStatus != nsEventStatus_eConsumeNoDefault && + (status == nsEventStatus_eConsumeNoDefault || + status == nsEventStatus_eConsumeDoDefault)) { + *aStatus = status; + } + + // Don't return error even if middle mouse paste fails since we haven't + // handled it here. + return NS_OK; +} + +nsresult EventStateManager::DispatchClickEvents( + PresShell* aPresShell, WidgetMouseEvent* aMouseUpEvent, + nsEventStatus* aStatus, nsIContent* aClickTarget, + nsIContent* aOverrideClickTarget) { + MOZ_ASSERT(aPresShell); + MOZ_ASSERT(aMouseUpEvent); + MOZ_ASSERT(EventCausesClickEvents(*aMouseUpEvent)); + MOZ_ASSERT(aStatus); + MOZ_ASSERT(aClickTarget || aOverrideClickTarget); + + bool notDispatchToContents = + (aMouseUpEvent->mButton == MouseButton::eMiddle || + aMouseUpEvent->mButton == MouseButton::eSecondary); + + bool fireAuxClick = notDispatchToContents; + + AutoWeakFrame currentTarget = aClickTarget->GetPrimaryFrame(); + nsresult rv = InitAndDispatchClickEvent( + aMouseUpEvent, aStatus, eMouseClick, aPresShell, aClickTarget, + currentTarget, notDispatchToContents, aOverrideClickTarget); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // Fire auxclick event if necessary. + if (fireAuxClick && *aStatus != nsEventStatus_eConsumeNoDefault && + aClickTarget && aClickTarget->IsInComposedDoc()) { + rv = InitAndDispatchClickEvent(aMouseUpEvent, aStatus, eMouseAuxClick, + aPresShell, aClickTarget, currentTarget, + false, aOverrideClickTarget); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Failed to dispatch eMouseAuxClick"); + } + + // Fire double click event if click count is 2. + if (aMouseUpEvent->mClickCount == 2 && !fireAuxClick && aClickTarget && + aClickTarget->IsInComposedDoc()) { + rv = InitAndDispatchClickEvent(aMouseUpEvent, aStatus, eMouseDoubleClick, + aPresShell, aClickTarget, currentTarget, + notDispatchToContents, aOverrideClickTarget); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + return rv; +} + +nsresult EventStateManager::HandleMiddleClickPaste( + PresShell* aPresShell, WidgetMouseEvent* aMouseEvent, + nsEventStatus* aStatus, EditorBase* aEditorBase) { + MOZ_ASSERT(aPresShell); + MOZ_ASSERT(aMouseEvent); + MOZ_ASSERT((aMouseEvent->mMessage == eMouseAuxClick && + aMouseEvent->mButton == MouseButton::eMiddle) || + EventCausesClickEvents(*aMouseEvent)); + MOZ_ASSERT(aStatus); + MOZ_ASSERT(*aStatus != nsEventStatus_eConsumeNoDefault); + + // Even if we're called twice or more for a mouse operation, we should + // handle only once. Although mMultipleActionsPrevented may be set to + // true by different event handler in the future, we can use it for now. + if (aMouseEvent->mFlags.mMultipleActionsPrevented) { + return NS_OK; + } + aMouseEvent->mFlags.mMultipleActionsPrevented = true; + + RefPtr<Selection> selection; + if (aEditorBase) { + selection = aEditorBase->GetSelection(); + if (NS_WARN_IF(!selection)) { + return NS_ERROR_FAILURE; + } + } else { + Document* document = aPresShell->GetDocument(); + if (NS_WARN_IF(!document)) { + return NS_ERROR_FAILURE; + } + selection = nsCopySupport::GetSelectionForCopy(document); + if (NS_WARN_IF(!selection)) { + return NS_ERROR_FAILURE; + } + } + + // Don't modify selection here because we've already set caret to the point + // at "mousedown" event. + + int32_t clipboardType = nsIClipboard::kGlobalClipboard; + nsCOMPtr<nsIClipboard> clipboardService = + do_GetService("@mozilla.org/widget/clipboard;1"); + if (clipboardService && clipboardService->IsClipboardTypeSupported( + nsIClipboard::kSelectionClipboard)) { + clipboardType = nsIClipboard::kSelectionClipboard; + } + + // Fire ePaste event by ourselves since we need to dispatch "paste" event + // even if the middle click event was consumed for compatibility with + // Chromium. + if (!nsCopySupport::FireClipboardEvent(ePaste, clipboardType, aPresShell, + selection)) { + *aStatus = nsEventStatus_eConsumeNoDefault; + return NS_OK; + } + + // Although we've fired "paste" event, there is no editor to accept the + // clipboard content. + if (!aEditorBase) { + return NS_OK; + } + + // Check if the editor is still the good target to paste. + if (aEditorBase->Destroyed() || aEditorBase->IsReadonly()) { + // XXX Should we consume the event when the editor is readonly and/or + // disabled? + return NS_OK; + } + + // The selection may have been modified during reflow. Therefore, we + // should adjust event target to pass IsAcceptableInputEvent(). + const nsRange* range = selection->GetRangeAt(0); + if (!range) { + return NS_OK; + } + WidgetMouseEvent mouseEvent(*aMouseEvent); + mouseEvent.mOriginalTarget = range->GetStartContainer(); + if (NS_WARN_IF(!mouseEvent.mOriginalTarget) || + !aEditorBase->IsAcceptableInputEvent(&mouseEvent)) { + return NS_OK; + } + + // If Control key is pressed, we should paste clipboard content as + // quotation. Otherwise, paste it as is. + if (aMouseEvent->IsControl()) { + DebugOnly<nsresult> rv = aEditorBase->PasteAsQuotationAsAction( + clipboardType, EditorBase::DispatchPasteEvent::No); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Failed to paste as quotation"); + } else { + DebugOnly<nsresult> rv = aEditorBase->PasteAsAction( + clipboardType, EditorBase::DispatchPasteEvent::No); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Failed to paste"); + } + *aStatus = nsEventStatus_eConsumeNoDefault; + + return NS_OK; +} + +void EventStateManager::ConsumeInteractionData( + Record<nsString, dom::InteractionData>& aInteractions) { + OnTypingInteractionEnded(); + + aInteractions.Entries().Clear(); + auto newEntry = aInteractions.Entries().AppendElement(); + newEntry->mKey = u"Typing"_ns; + newEntry->mValue = gTypingInteraction; + gTypingInteraction = {}; +} + +nsIFrame* EventStateManager::GetEventTarget() { + PresShell* presShell; + if (mCurrentTarget || !mPresContext || + !(presShell = mPresContext->GetPresShell())) { + return mCurrentTarget; + } + + if (mCurrentTargetContent) { + mCurrentTarget = mPresContext->GetPrimaryFrameFor(mCurrentTargetContent); + if (mCurrentTarget) { + return mCurrentTarget; + } + } + + nsIFrame* frame = presShell->GetCurrentEventFrame(); + return (mCurrentTarget = frame); +} + +already_AddRefed<nsIContent> EventStateManager::GetEventTargetContent( + WidgetEvent* aEvent) { + if (aEvent && (aEvent->mMessage == eFocus || aEvent->mMessage == eBlur)) { + nsCOMPtr<nsIContent> content = GetFocusedElement(); + return content.forget(); + } + + if (mCurrentTargetContent) { + nsCOMPtr<nsIContent> content = mCurrentTargetContent; + return content.forget(); + } + + nsCOMPtr<nsIContent> content; + if (PresShell* presShell = mPresContext->GetPresShell()) { + content = presShell->GetEventTargetContent(aEvent); + } + + // Some events here may set mCurrentTarget but not set the corresponding + // event target in the PresShell. + if (!content && mCurrentTarget) { + mCurrentTarget->GetContentForEvent(aEvent, getter_AddRefs(content)); + } + + return content.forget(); +} + +static Element* GetLabelTarget(nsIContent* aPossibleLabel) { + mozilla::dom::HTMLLabelElement* label = + mozilla::dom::HTMLLabelElement::FromNode(aPossibleLabel); + if (!label) return nullptr; + + return label->GetLabeledElement(); +} + +/* static */ +inline void EventStateManager::DoStateChange(Element* aElement, + ElementState aState, + bool aAddState) { + if (aAddState) { + aElement->AddStates(aState); + } else { + aElement->RemoveStates(aState); + } +} + +/* static */ +inline void EventStateManager::DoStateChange(nsIContent* aContent, + ElementState aState, + bool aStateAdded) { + if (aContent->IsElement()) { + DoStateChange(aContent->AsElement(), aState, aStateAdded); + } +} + +/* static */ +void EventStateManager::UpdateAncestorState(nsIContent* aStartNode, + nsIContent* aStopBefore, + ElementState aState, + bool aAddState) { + for (; aStartNode && aStartNode != aStopBefore; + aStartNode = aStartNode->GetFlattenedTreeParent()) { + // We might be starting with a non-element (e.g. a text node) and + // if someone is doing something weird might be ending with a + // non-element too (e.g. a document fragment) + if (!aStartNode->IsElement()) { + continue; + } + Element* element = aStartNode->AsElement(); + DoStateChange(element, aState, aAddState); + Element* labelTarget = GetLabelTarget(element); + if (labelTarget) { + DoStateChange(labelTarget, aState, aAddState); + } + } + + if (aAddState) { + // We might be in a situation where a node was in hover both + // because it was hovered and because the label for it was + // hovered, and while we stopped hovering the node the label is + // still hovered. Or we might have had two nested labels for the + // same node, and while one is no longer hovered the other still + // is. In that situation, the label that's still hovered will be + // aStopBefore or some ancestor of it, and the call we just made + // to UpdateAncestorState with aAddState = false would have + // removed the hover state from the node. But the node should + // still be in hover state. To handle this situation we need to + // keep walking up the tree and any time we find a label mark its + // corresponding node as still in our state. + for (; aStartNode; aStartNode = aStartNode->GetFlattenedTreeParent()) { + if (!aStartNode->IsElement()) { + continue; + } + + Element* labelTarget = GetLabelTarget(aStartNode->AsElement()); + if (labelTarget && !labelTarget->State().HasState(aState)) { + DoStateChange(labelTarget, aState, true); + } + } + } +} + +// static +bool CanContentHaveActiveState(nsIContent& aContent) { + // Editable content can never become active since their default actions + // are disabled. Watch out for editable content in native anonymous + // subtrees though, as they belong to text controls. + return !aContent.IsEditable() || aContent.IsInNativeAnonymousSubtree(); +} + +bool EventStateManager::SetContentState(nsIContent* aContent, + ElementState aState) { + MOZ_ASSERT(ManagesState(aState), "Unexpected state"); + + nsCOMPtr<nsIContent> notifyContent1; + nsCOMPtr<nsIContent> notifyContent2; + bool updateAncestors; + + if (aState == ElementState::HOVER || aState == ElementState::ACTIVE) { + // Hover and active are hierarchical + updateAncestors = true; + + // check to see that this state is allowed by style. Check dragover too? + // XXX Is this even what we want? + if (mCurrentTarget && + mCurrentTarget->StyleUI()->UserInput() == StyleUserInput::None) { + return false; + } + + if (aState == ElementState::ACTIVE) { + if (aContent && !CanContentHaveActiveState(*aContent)) { + aContent = nullptr; + } + if (aContent != mActiveContent) { + notifyContent1 = aContent; + notifyContent2 = mActiveContent; + mActiveContent = aContent; + } + } else { + NS_ASSERTION(aState == ElementState::HOVER, "How did that happen?"); + nsIContent* newHover; + + if (mPresContext->IsDynamic()) { + newHover = aContent; + } else { + NS_ASSERTION(!aContent || aContent->GetComposedDoc() == + mPresContext->PresShell()->GetDocument(), + "Unexpected document"); + nsIFrame* frame = aContent ? aContent->GetPrimaryFrame() : nullptr; + if (frame && nsLayoutUtils::IsViewportScrollbarFrame(frame)) { + // The scrollbars of viewport should not ignore the hover state. + // Because they are *not* the content of the web page. + newHover = aContent; + } else { + // All contents of the web page should ignore the hover state. + newHover = nullptr; + } + } + + if (newHover != mHoverContent) { + notifyContent1 = newHover; + notifyContent2 = mHoverContent; + mHoverContent = newHover; + } + } + } else { + updateAncestors = false; + if (aState == ElementState::DRAGOVER) { + if (aContent != sDragOverContent) { + notifyContent1 = aContent; + notifyContent2 = sDragOverContent; + sDragOverContent = aContent; + } + } else if (aState == ElementState::URLTARGET) { + if (aContent != mURLTargetContent) { + notifyContent1 = aContent; + notifyContent2 = mURLTargetContent; + mURLTargetContent = aContent; + } + } + } + + // We need to keep track of which of notifyContent1 and notifyContent2 is + // getting the state set and which is getting it unset. If both are + // non-null, then notifyContent1 is having the state set and notifyContent2 + // is having it unset. But if one of them is null, we need to keep track of + // the right thing for notifyContent1 explicitly. + bool content1StateSet = true; + if (!notifyContent1) { + // This is ok because FindCommonAncestor wouldn't find anything + // anyway if notifyContent1 is null. + notifyContent1 = notifyContent2; + notifyContent2 = nullptr; + content1StateSet = false; + } + + if (notifyContent1 && mPresContext) { + EnsureDocument(mPresContext); + if (mDocument) { + nsAutoScriptBlocker scriptBlocker; + + if (updateAncestors) { + nsCOMPtr<nsIContent> commonAncestor = + FindCommonAncestor(notifyContent1, notifyContent2); + if (notifyContent2) { + // It's very important to first notify the state removal and + // then the state addition, because due to labels it's + // possible that we're removing state from some element but + // then adding it again (say because mHoverContent changed + // from a control to its label). + UpdateAncestorState(notifyContent2, commonAncestor, aState, false); + } + UpdateAncestorState(notifyContent1, commonAncestor, aState, + content1StateSet); + } else { + if (notifyContent2) { + DoStateChange(notifyContent2, aState, false); + } + DoStateChange(notifyContent1, aState, content1StateSet); + } + } + } + + return true; +} + +void EventStateManager::ResetLastOverForContent( + const uint32_t& aIdx, const RefPtr<OverOutElementsWrapper>& aElemWrapper, + nsIContent* aContent) { + if (aElemWrapper && aElemWrapper->mLastOverElement && + nsContentUtils::ContentIsFlattenedTreeDescendantOf( + aElemWrapper->mLastOverElement, aContent)) { + aElemWrapper->mLastOverElement = nullptr; + } +} + +void EventStateManager::RemoveNodeFromChainIfNeeded(ElementState aState, + nsIContent* aContentRemoved, + bool aNotify) { + MOZ_ASSERT(aState == ElementState::HOVER || aState == ElementState::ACTIVE); + if (!aContentRemoved->IsElement() || + !aContentRemoved->AsElement()->State().HasState(aState)) { + return; + } + + nsCOMPtr<nsIContent>& leaf = + aState == ElementState::HOVER ? mHoverContent : mActiveContent; + + MOZ_ASSERT(leaf); + // These two NS_ASSERTIONS below can fail for Shadow DOM sometimes, and it's + // not clear how to best handle it, see + // https://github.com/whatwg/html/issues/4795 and bug 1551621. + NS_ASSERTION( + nsContentUtils::ContentIsFlattenedTreeDescendantOf(leaf, aContentRemoved), + "Flat tree and active / hover chain got out of sync"); + + nsIContent* newLeaf = aContentRemoved->GetFlattenedTreeParent(); + MOZ_ASSERT(!newLeaf || newLeaf->IsElement()); + NS_ASSERTION(!newLeaf || newLeaf->AsElement()->State().HasState(aState), + "State got out of sync because of shadow DOM"); + if (aNotify) { + SetContentState(newLeaf, aState); + } else { + // We don't update the removed content's state here, since removing NAC + // happens from layout and we don't really want to notify at that point or + // what not. + // + // Also, NAC is not observable and NAC being removed will go away soon. + leaf = newLeaf; + } + MOZ_ASSERT(leaf == newLeaf || (aState == ElementState::ACTIVE && !leaf && + !CanContentHaveActiveState(*newLeaf))); +} + +void EventStateManager::NativeAnonymousContentRemoved(nsIContent* aContent) { + MOZ_ASSERT(aContent->IsRootOfNativeAnonymousSubtree()); + RemoveNodeFromChainIfNeeded(ElementState::HOVER, aContent, false); + RemoveNodeFromChainIfNeeded(ElementState::ACTIVE, aContent, false); + + if (mLastLeftMouseDownContent && + nsContentUtils::ContentIsFlattenedTreeDescendantOf( + mLastLeftMouseDownContent, aContent)) { + mLastLeftMouseDownContent = aContent->GetFlattenedTreeParent(); + } + + if (mLastMiddleMouseDownContent && + nsContentUtils::ContentIsFlattenedTreeDescendantOf( + mLastMiddleMouseDownContent, aContent)) { + mLastMiddleMouseDownContent = aContent->GetFlattenedTreeParent(); + } + + if (mLastRightMouseDownContent && + nsContentUtils::ContentIsFlattenedTreeDescendantOf( + mLastRightMouseDownContent, aContent)) { + mLastRightMouseDownContent = aContent->GetFlattenedTreeParent(); + } +} + +void EventStateManager::ContentRemoved(Document* aDocument, + nsIContent* aContent) { + /* + * Anchor and area elements when focused or hovered might make the UI to show + * the current link. We want to make sure that the UI gets informed when they + * are actually removed from the DOM. + */ + if (aContent->IsAnyOfHTMLElements(nsGkAtoms::a, nsGkAtoms::area) && + (aContent->AsElement()->State().HasAtLeastOneOfStates( + ElementState::FOCUS | ElementState::HOVER))) { + Element* element = aContent->AsElement(); + element->LeaveLink(element->GetPresContext(Element::eForComposedDoc)); + } + + if (aContent->IsElement()) { + if (RefPtr<nsPresContext> presContext = mPresContext) { + IMEStateManager::OnRemoveContent(*presContext, + MOZ_KnownLive(*aContent->AsElement())); + } + WheelTransaction::OnRemoveElement(aContent); + } + + // inform the focus manager that the content is being removed. If this + // content is focused, the focus will be removed without firing events. + if (RefPtr<nsFocusManager> fm = nsFocusManager::GetFocusManager()) { + fm->ContentRemoved(aDocument, aContent); + } + + RemoveNodeFromChainIfNeeded(ElementState::HOVER, aContent, true); + RemoveNodeFromChainIfNeeded(ElementState::ACTIVE, aContent, true); + + if (sDragOverContent && + sDragOverContent->OwnerDoc() == aContent->OwnerDoc() && + nsContentUtils::ContentIsFlattenedTreeDescendantOf(sDragOverContent, + aContent)) { + sDragOverContent = nullptr; + } + + PointerEventHandler::ReleaseIfCaptureByDescendant(aContent); + + // See bug 292146 for why we want to null this out + ResetLastOverForContent(0, mMouseEnterLeaveHelper, aContent); + for (const auto& entry : mPointersEnterLeaveHelper) { + ResetLastOverForContent(entry.GetKey(), entry.GetData(), aContent); + } +} + +void EventStateManager::TextControlRootWillBeRemoved( + TextControlElement& aTextControlElement) { + if (!mGestureDownInTextControl || !mGestureDownFrameOwner || + !mGestureDownFrameOwner->IsInNativeAnonymousSubtree()) { + return; + } + // If we track gesture to start drag in aTextControlElement, we should keep + // tracking it with aTextContrlElement itself for now because this may be + // caused by reframing aTextControlElement which may not be intended by the + // user. + if (&aTextControlElement == + mGestureDownFrameOwner + ->GetClosestNativeAnonymousSubtreeRootParentOrHost()) { + mGestureDownFrameOwner = &aTextControlElement; + } +} + +void EventStateManager::TextControlRootAdded( + Element& aAnonymousDivElement, TextControlElement& aTextControlElement) { + if (!mGestureDownInTextControl || + mGestureDownFrameOwner != &aTextControlElement) { + return; + } + // If we track gesture to start drag in aTextControlElement, but the frame + // owner is the text control element itself, the anonymous nodes in it are + // recreated by a reframe. If so, we should keep tracking it with the + // recreated native anonymous node. + mGestureDownFrameOwner = + aAnonymousDivElement.GetFirstChild() + ? aAnonymousDivElement.GetFirstChild() + : static_cast<nsIContent*>(&aAnonymousDivElement); +} + +bool EventStateManager::EventStatusOK(WidgetGUIEvent* aEvent) { + return !(aEvent->mMessage == eMouseDown && + aEvent->AsMouseEvent()->mButton == MouseButton::ePrimary && + !sNormalLMouseEventInProcess); +} + +//------------------------------------------- +// Access Key Registration +//------------------------------------------- +void EventStateManager::RegisterAccessKey(Element* aElement, uint32_t aKey) { + if (aElement && !mAccessKeys.Contains(aElement)) { + mAccessKeys.AppendObject(aElement); + } +} + +void EventStateManager::UnregisterAccessKey(Element* aElement, uint32_t aKey) { + if (aElement) { + mAccessKeys.RemoveObject(aElement); + } +} + +uint32_t EventStateManager::GetRegisteredAccessKey(Element* aElement) { + MOZ_ASSERT(aElement); + + if (!mAccessKeys.Contains(aElement)) { + return 0; + } + + nsAutoString accessKey; + aElement->GetAttr(kNameSpaceID_None, nsGkAtoms::accesskey, accessKey); + return accessKey.First(); +} + +void EventStateManager::EnsureDocument(nsPresContext* aPresContext) { + if (!mDocument) mDocument = aPresContext->Document(); +} + +void EventStateManager::FlushLayout(nsPresContext* aPresContext) { + MOZ_ASSERT(aPresContext, "nullptr ptr"); + if (RefPtr<PresShell> presShell = aPresContext->GetPresShell()) { + presShell->FlushPendingNotifications(FlushType::InterruptibleLayout); + } +} + +Element* EventStateManager::GetFocusedElement() { + nsFocusManager* fm = nsFocusManager::GetFocusManager(); + EnsureDocument(mPresContext); + if (!fm || !mDocument) { + return nullptr; + } + + nsCOMPtr<nsPIDOMWindowOuter> focusedWindow; + return nsFocusManager::GetFocusedDescendant( + mDocument->GetWindow(), nsFocusManager::eOnlyCurrentWindow, + getter_AddRefs(focusedWindow)); +} + +//------------------------------------------------------- +// Return true if the docshell is visible + +bool EventStateManager::IsShellVisible(nsIDocShell* aShell) { + NS_ASSERTION(aShell, "docshell is null"); + + nsCOMPtr<nsIBaseWindow> basewin = do_QueryInterface(aShell); + if (!basewin) return true; + + bool isVisible = true; + basewin->GetVisibility(&isVisible); + + // We should be doing some additional checks here so that + // we don't tab into hidden tabs of tabbrowser. -bryner + + return isVisible; +} + +nsresult EventStateManager::DoContentCommandEvent( + WidgetContentCommandEvent* aEvent) { + EnsureDocument(mPresContext); + NS_ENSURE_TRUE(mDocument, NS_ERROR_FAILURE); + nsCOMPtr<nsPIDOMWindowOuter> window(mDocument->GetWindow()); + NS_ENSURE_TRUE(window, NS_ERROR_FAILURE); + + nsCOMPtr<nsPIWindowRoot> root = window->GetTopWindowRoot(); + NS_ENSURE_TRUE(root, NS_ERROR_FAILURE); + const char* cmd; + switch (aEvent->mMessage) { + case eContentCommandCut: + cmd = "cmd_cut"; + break; + case eContentCommandCopy: + cmd = "cmd_copy"; + break; + case eContentCommandPaste: + cmd = "cmd_paste"; + break; + case eContentCommandDelete: + cmd = "cmd_delete"; + break; + case eContentCommandUndo: + cmd = "cmd_undo"; + break; + case eContentCommandRedo: + cmd = "cmd_redo"; + break; + case eContentCommandPasteTransferable: + cmd = "cmd_pasteTransferable"; + break; + case eContentCommandLookUpDictionary: + cmd = "cmd_lookUpDictionary"; + break; + default: + return NS_ERROR_NOT_IMPLEMENTED; + } + // If user tries to do something, user must try to do it in visible window. + // So, let's retrieve controller of visible window. + nsCOMPtr<nsIController> controller; + nsresult rv = + root->GetControllerForCommand(cmd, true, getter_AddRefs(controller)); + NS_ENSURE_SUCCESS(rv, rv); + if (!controller) { + // When GetControllerForCommand succeeded but there is no controller, the + // command isn't supported. + aEvent->mIsEnabled = false; + } else { + bool canDoIt; + rv = controller->IsCommandEnabled(cmd, &canDoIt); + NS_ENSURE_SUCCESS(rv, rv); + aEvent->mIsEnabled = canDoIt; + if (canDoIt && !aEvent->mOnlyEnabledCheck) { + switch (aEvent->mMessage) { + case eContentCommandPasteTransferable: { + BrowserParent* remote = BrowserParent::GetFocused(); + if (remote) { + nsCOMPtr<nsITransferable> transferable = aEvent->mTransferable; + IPCTransferableData ipcTransferableData; + nsContentUtils::TransferableToIPCTransferableData( + transferable, &ipcTransferableData, false, remote->Manager()); + bool isPrivateData = transferable->GetIsPrivateData(); + nsCOMPtr<nsIPrincipal> requestingPrincipal = + transferable->GetRequestingPrincipal(); + nsContentPolicyType contentPolicyType = + transferable->GetContentPolicyType(); + remote->SendPasteTransferable(std::move(ipcTransferableData), + isPrivateData, requestingPrincipal, + contentPolicyType); + rv = NS_OK; + } else { + nsCOMPtr<nsICommandController> commandController = + do_QueryInterface(controller); + NS_ENSURE_STATE(commandController); + + RefPtr<nsCommandParams> params = new nsCommandParams(); + rv = params->SetISupports("transferable", aEvent->mTransferable); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + rv = commandController->DoCommandWithParams(cmd, params); + } + break; + } + + case eContentCommandLookUpDictionary: { + nsCOMPtr<nsICommandController> commandController = + do_QueryInterface(controller); + if (NS_WARN_IF(!commandController)) { + return NS_ERROR_FAILURE; + } + + RefPtr<nsCommandParams> params = new nsCommandParams(); + rv = params->SetInt("x", aEvent->mRefPoint.x); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = params->SetInt("y", aEvent->mRefPoint.y); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = commandController->DoCommandWithParams(cmd, params); + break; + } + + default: + rv = controller->DoCommand(cmd); + break; + } + NS_ENSURE_SUCCESS(rv, rv); + } + } + aEvent->mSucceeded = true; + return NS_OK; +} + +nsresult EventStateManager::DoContentCommandInsertTextEvent( + WidgetContentCommandEvent* aEvent) { + MOZ_ASSERT(aEvent); + MOZ_ASSERT(aEvent->mMessage == eContentCommandInsertText); + MOZ_DIAGNOSTIC_ASSERT(aEvent->mString.isSome()); + MOZ_DIAGNOSTIC_ASSERT(!aEvent->mString.ref().IsEmpty()); + + aEvent->mIsEnabled = false; + aEvent->mSucceeded = false; + + NS_ENSURE_TRUE(mPresContext, NS_ERROR_NOT_AVAILABLE); + + if (XRE_IsParentProcess()) { + // Handle it in focused content process if there is. + if (BrowserParent* remote = BrowserParent::GetFocused()) { + remote->SendInsertText(aEvent->mString.ref()); + aEvent->mIsEnabled = true; // XXX it can be a lie... + aEvent->mSucceeded = true; + return NS_OK; + } + } + + // If there is no active editor in this process, we should treat the command + // is disabled. + RefPtr<EditorBase> activeEditor = + nsContentUtils::GetActiveEditor(mPresContext); + if (!activeEditor) { + aEvent->mSucceeded = true; + return NS_OK; + } + + nsresult rv = activeEditor->InsertTextAsAction(aEvent->mString.ref()); + aEvent->mIsEnabled = rv != NS_SUCCESS_DOM_NO_OPERATION; + aEvent->mSucceeded = NS_SUCCEEDED(rv); + return NS_OK; +} + +nsresult EventStateManager::DoContentCommandScrollEvent( + WidgetContentCommandEvent* aEvent) { + NS_ENSURE_TRUE(mPresContext, NS_ERROR_NOT_AVAILABLE); + PresShell* presShell = mPresContext->GetPresShell(); + NS_ENSURE_TRUE(presShell, NS_ERROR_NOT_AVAILABLE); + NS_ENSURE_TRUE(aEvent->mScroll.mAmount != 0, NS_ERROR_INVALID_ARG); + + ScrollUnit scrollUnit; + switch (aEvent->mScroll.mUnit) { + case WidgetContentCommandEvent::eCmdScrollUnit_Line: + scrollUnit = ScrollUnit::LINES; + break; + case WidgetContentCommandEvent::eCmdScrollUnit_Page: + scrollUnit = ScrollUnit::PAGES; + break; + case WidgetContentCommandEvent::eCmdScrollUnit_Whole: + scrollUnit = ScrollUnit::WHOLE; + break; + default: + return NS_ERROR_INVALID_ARG; + } + + aEvent->mSucceeded = true; + + nsIScrollableFrame* sf = + presShell->GetScrollableFrameToScroll(layers::EitherScrollDirection); + aEvent->mIsEnabled = + sf ? (aEvent->mScroll.mIsHorizontal ? WheelHandlingUtils::CanScrollOn( + sf, aEvent->mScroll.mAmount, 0) + : WheelHandlingUtils::CanScrollOn( + sf, 0, aEvent->mScroll.mAmount)) + : false; + + if (!aEvent->mIsEnabled || aEvent->mOnlyEnabledCheck) { + return NS_OK; + } + + nsIntPoint pt(0, 0); + if (aEvent->mScroll.mIsHorizontal) { + pt.x = aEvent->mScroll.mAmount; + } else { + pt.y = aEvent->mScroll.mAmount; + } + + // The caller may want synchronous scrolling. + sf->ScrollBy(pt, scrollUnit, ScrollMode::Instant); + return NS_OK; +} + +void EventStateManager::SetActiveManager(EventStateManager* aNewESM, + nsIContent* aContent) { + if (sActiveESM && aNewESM != sActiveESM) { + sActiveESM->SetContentState(nullptr, ElementState::ACTIVE); + } + sActiveESM = aNewESM; + if (sActiveESM && aContent) { + sActiveESM->SetContentState(aContent, ElementState::ACTIVE); + } +} + +void EventStateManager::ClearGlobalActiveContent(EventStateManager* aClearer) { + if (aClearer) { + aClearer->SetContentState(nullptr, ElementState::ACTIVE); + if (sDragOverContent) { + aClearer->SetContentState(nullptr, ElementState::DRAGOVER); + } + } + if (sActiveESM && aClearer != sActiveESM) { + sActiveESM->SetContentState(nullptr, ElementState::ACTIVE); + } + sActiveESM = nullptr; +} + +/******************************************************************/ +/* mozilla::EventStateManager::DeltaAccumulator */ +/******************************************************************/ + +void EventStateManager::DeltaAccumulator::InitLineOrPageDelta( + nsIFrame* aTargetFrame, EventStateManager* aESM, WidgetWheelEvent* aEvent) { + MOZ_ASSERT(aESM); + MOZ_ASSERT(aEvent); + + // Reset if the previous wheel event is too old. + if (!mLastTime.IsNull()) { + TimeDuration duration = TimeStamp::Now() - mLastTime; + if (duration.ToMilliseconds() > + StaticPrefs::mousewheel_transaction_timeout()) { + Reset(); + } + } + // If we have accumulated delta, we may need to reset it. + if (IsInTransaction()) { + // If wheel event type is changed, reset the values. + if (mHandlingDeltaMode != aEvent->mDeltaMode || + mIsNoLineOrPageDeltaDevice != aEvent->mIsNoLineOrPageDelta) { + Reset(); + } else { + // If the delta direction is changed, we should reset only the + // accumulated values. + if (mX && aEvent->mDeltaX && ((aEvent->mDeltaX > 0.0) != (mX > 0.0))) { + mX = mPendingScrollAmountX = 0.0; + } + if (mY && aEvent->mDeltaY && ((aEvent->mDeltaY > 0.0) != (mY > 0.0))) { + mY = mPendingScrollAmountY = 0.0; + } + } + } + + mHandlingDeltaMode = aEvent->mDeltaMode; + mIsNoLineOrPageDeltaDevice = aEvent->mIsNoLineOrPageDelta; + + { + nsIFrame* frame = aESM->ComputeScrollTarget(aTargetFrame, aEvent, + COMPUTE_DEFAULT_ACTION_TARGET); + nsPresContext* pc = + frame ? frame->PresContext() : aTargetFrame->PresContext(); + nsIScrollableFrame* scrollTarget = do_QueryFrame(frame); + aEvent->mScrollAmount = aESM->GetScrollAmount(pc, aEvent, scrollTarget); + } + + // If it's handling neither a device that does not provide line or page deltas + // nor delta values multiplied by prefs, we must not modify lineOrPageDelta + // values. + // TODO(emilio): Does this care about overridden scroll speed? + if (!mIsNoLineOrPageDeltaDevice && + !EventStateManager::WheelPrefs::GetInstance() + ->NeedToComputeLineOrPageDelta(aEvent)) { + // Set the delta values to mX and mY. They would be used when above block + // resets mX/mY/mPendingScrollAmountX/mPendingScrollAmountY if the direction + // is changed. + // NOTE: We shouldn't accumulate the delta values, it might could cause + // overflow even though it's not a realistic situation. + if (aEvent->mDeltaX) { + mX = aEvent->mDeltaX; + } + if (aEvent->mDeltaY) { + mY = aEvent->mDeltaY; + } + mLastTime = TimeStamp::Now(); + return; + } + + mX += aEvent->mDeltaX; + mY += aEvent->mDeltaY; + + if (mHandlingDeltaMode == WheelEvent_Binding::DOM_DELTA_PIXEL) { + // Records pixel delta values and init mLineOrPageDeltaX and + // mLineOrPageDeltaY for wheel events which are caused by pixel only + // devices. Ignore mouse wheel transaction for computing this. The + // lineOrPageDelta values will be used by dispatching legacy + // eMouseScrollEventClass (DOMMouseScroll) but not be used for scrolling + // of default action. The transaction should be used only for the default + // action. + auto scrollAmountInCSSPixels = + CSSIntSize::FromAppUnitsRounded(aEvent->mScrollAmount); + + aEvent->mLineOrPageDeltaX = RoundDown(mX) / scrollAmountInCSSPixels.width; + aEvent->mLineOrPageDeltaY = RoundDown(mY) / scrollAmountInCSSPixels.height; + + mX -= aEvent->mLineOrPageDeltaX * scrollAmountInCSSPixels.width; + mY -= aEvent->mLineOrPageDeltaY * scrollAmountInCSSPixels.height; + } else { + aEvent->mLineOrPageDeltaX = RoundDown(mX); + aEvent->mLineOrPageDeltaY = RoundDown(mY); + mX -= aEvent->mLineOrPageDeltaX; + mY -= aEvent->mLineOrPageDeltaY; + } + + mLastTime = TimeStamp::Now(); +} + +void EventStateManager::DeltaAccumulator::Reset() { + mX = mY = 0.0; + mPendingScrollAmountX = mPendingScrollAmountY = 0.0; + mHandlingDeltaMode = UINT32_MAX; + mIsNoLineOrPageDeltaDevice = false; +} + +nsIntPoint +EventStateManager::DeltaAccumulator::ComputeScrollAmountForDefaultAction( + WidgetWheelEvent* aEvent, const nsIntSize& aScrollAmountInDevPixels) { + MOZ_ASSERT(aEvent); + + DeltaValues acceleratedDelta = WheelTransaction::AccelerateWheelDelta(aEvent); + + nsIntPoint result(0, 0); + if (aEvent->mDeltaMode == WheelEvent_Binding::DOM_DELTA_PIXEL) { + mPendingScrollAmountX += acceleratedDelta.deltaX; + mPendingScrollAmountY += acceleratedDelta.deltaY; + } else { + mPendingScrollAmountX += + aScrollAmountInDevPixels.width * acceleratedDelta.deltaX; + mPendingScrollAmountY += + aScrollAmountInDevPixels.height * acceleratedDelta.deltaY; + } + result.x = RoundDown(mPendingScrollAmountX); + result.y = RoundDown(mPendingScrollAmountY); + mPendingScrollAmountX -= result.x; + mPendingScrollAmountY -= result.y; + + return result; +} + +/******************************************************************/ +/* mozilla::EventStateManager::WheelPrefs */ +/******************************************************************/ + +// static +EventStateManager::WheelPrefs* EventStateManager::WheelPrefs::GetInstance() { + if (!sInstance) { + sInstance = new WheelPrefs(); + } + return sInstance; +} + +// static +void EventStateManager::WheelPrefs::Shutdown() { + delete sInstance; + sInstance = nullptr; +} + +// static +void EventStateManager::WheelPrefs::OnPrefChanged(const char* aPrefName, + void* aClosure) { + // forget all prefs, it's not problem for performance. + sInstance->Reset(); + DeltaAccumulator::GetInstance()->Reset(); +} + +EventStateManager::WheelPrefs::WheelPrefs() { + Reset(); + Preferences::RegisterPrefixCallback(OnPrefChanged, "mousewheel."); +} + +EventStateManager::WheelPrefs::~WheelPrefs() { + Preferences::UnregisterPrefixCallback(OnPrefChanged, "mousewheel."); +} + +void EventStateManager::WheelPrefs::Reset() { memset(mInit, 0, sizeof(mInit)); } + +EventStateManager::WheelPrefs::Index EventStateManager::WheelPrefs::GetIndexFor( + const WidgetWheelEvent* aEvent) { + if (!aEvent) { + return INDEX_DEFAULT; + } + + Modifiers modifiers = + (aEvent->mModifiers & (MODIFIER_ALT | MODIFIER_CONTROL | MODIFIER_META | + MODIFIER_SHIFT | MODIFIER_OS)); + + switch (modifiers) { + case MODIFIER_ALT: + return INDEX_ALT; + case MODIFIER_CONTROL: + return INDEX_CONTROL; + case MODIFIER_META: + return INDEX_META; + case MODIFIER_SHIFT: + return INDEX_SHIFT; + case MODIFIER_OS: + return INDEX_OS; + default: + // If two or more modifier keys are pressed, we should use default + // settings. + return INDEX_DEFAULT; + } +} + +void EventStateManager::WheelPrefs::GetBasePrefName( + EventStateManager::WheelPrefs::Index aIndex, nsACString& aBasePrefName) { + aBasePrefName.AssignLiteral("mousewheel."); + switch (aIndex) { + case INDEX_ALT: + aBasePrefName.AppendLiteral("with_alt."); + break; + case INDEX_CONTROL: + aBasePrefName.AppendLiteral("with_control."); + break; + case INDEX_META: + aBasePrefName.AppendLiteral("with_meta."); + break; + case INDEX_SHIFT: + aBasePrefName.AppendLiteral("with_shift."); + break; + case INDEX_OS: + aBasePrefName.AppendLiteral("with_win."); + break; + case INDEX_DEFAULT: + default: + aBasePrefName.AppendLiteral("default."); + break; + } +} + +void EventStateManager::WheelPrefs::Init( + EventStateManager::WheelPrefs::Index aIndex) { + if (mInit[aIndex]) { + return; + } + mInit[aIndex] = true; + + nsAutoCString basePrefName; + GetBasePrefName(aIndex, basePrefName); + + nsAutoCString prefNameX(basePrefName); + prefNameX.AppendLiteral("delta_multiplier_x"); + mMultiplierX[aIndex] = + static_cast<double>(Preferences::GetInt(prefNameX.get(), 100)) / 100; + + nsAutoCString prefNameY(basePrefName); + prefNameY.AppendLiteral("delta_multiplier_y"); + mMultiplierY[aIndex] = + static_cast<double>(Preferences::GetInt(prefNameY.get(), 100)) / 100; + + nsAutoCString prefNameZ(basePrefName); + prefNameZ.AppendLiteral("delta_multiplier_z"); + mMultiplierZ[aIndex] = + static_cast<double>(Preferences::GetInt(prefNameZ.get(), 100)) / 100; + + nsAutoCString prefNameAction(basePrefName); + prefNameAction.AppendLiteral("action"); + int32_t action = Preferences::GetInt(prefNameAction.get(), ACTION_SCROLL); + if (action < int32_t(ACTION_NONE) || action > int32_t(ACTION_LAST)) { + NS_WARNING("Unsupported action pref value, replaced with 'Scroll'."); + action = ACTION_SCROLL; + } + mActions[aIndex] = static_cast<Action>(action); + + // Compute action values overridden by .override_x pref. + // At present, override is possible only for the x-direction + // because this pref is introduced mainly for tilt wheels. + // Note that ACTION_HORIZONTALIZED_SCROLL isn't a valid value for this pref + // because it affects only to deltaY. + prefNameAction.AppendLiteral(".override_x"); + int32_t actionOverrideX = Preferences::GetInt(prefNameAction.get(), -1); + if (actionOverrideX < -1 || actionOverrideX > int32_t(ACTION_LAST) || + actionOverrideX == ACTION_HORIZONTALIZED_SCROLL) { + NS_WARNING("Unsupported action override pref value, didn't override."); + actionOverrideX = -1; + } + mOverriddenActionsX[aIndex] = (actionOverrideX == -1) + ? static_cast<Action>(action) + : static_cast<Action>(actionOverrideX); +} + +void EventStateManager::WheelPrefs::GetMultiplierForDeltaXAndY( + const WidgetWheelEvent* aEvent, Index aIndex, double* aMultiplierForDeltaX, + double* aMultiplierForDeltaY) { + *aMultiplierForDeltaX = mMultiplierX[aIndex]; + *aMultiplierForDeltaY = mMultiplierY[aIndex]; + // If the event has been horizontalized(I.e. treated as a horizontal wheel + // scroll for a vertical wheel scroll), then we should swap mMultiplierX and + // mMultiplierY. By doing this, multipliers will still apply to the delta + // values they origianlly corresponded to. + if (aEvent->mDeltaValuesHorizontalizedForDefaultHandler && + ComputeActionFor(aEvent) == ACTION_HORIZONTALIZED_SCROLL) { + std::swap(*aMultiplierForDeltaX, *aMultiplierForDeltaY); + } +} + +void EventStateManager::WheelPrefs::ApplyUserPrefsToDelta( + WidgetWheelEvent* aEvent) { + if (aEvent->mCustomizedByUserPrefs) { + return; + } + + Index index = GetIndexFor(aEvent); + Init(index); + + double multiplierForDeltaX = 1.0, multiplierForDeltaY = 1.0; + GetMultiplierForDeltaXAndY(aEvent, index, &multiplierForDeltaX, + &multiplierForDeltaY); + aEvent->mDeltaX *= multiplierForDeltaX; + aEvent->mDeltaY *= multiplierForDeltaY; + aEvent->mDeltaZ *= mMultiplierZ[index]; + + // If the multiplier is 1.0 or -1.0, i.e., it doesn't change the absolute + // value, we should use lineOrPageDelta values which were set by widget. + // Otherwise, we need to compute them from accumulated delta values. + if (!NeedToComputeLineOrPageDelta(aEvent)) { + aEvent->mLineOrPageDeltaX *= static_cast<int32_t>(multiplierForDeltaX); + aEvent->mLineOrPageDeltaY *= static_cast<int32_t>(multiplierForDeltaY); + } else { + aEvent->mLineOrPageDeltaX = 0; + aEvent->mLineOrPageDeltaY = 0; + } + + aEvent->mCustomizedByUserPrefs = + ((mMultiplierX[index] != 1.0) || (mMultiplierY[index] != 1.0) || + (mMultiplierZ[index] != 1.0)); +} + +void EventStateManager::WheelPrefs::CancelApplyingUserPrefsFromOverflowDelta( + WidgetWheelEvent* aEvent) { + Index index = GetIndexFor(aEvent); + Init(index); + + // XXX If the multiplier pref value is negative, the scroll direction was + // changed and caused to scroll different direction. In such case, + // this method reverts the sign of overflowDelta. Does it make widget + // happy? Although, widget can know the pref applied delta values by + // referrencing the deltaX and deltaY of the event. + + double multiplierForDeltaX = 1.0, multiplierForDeltaY = 1.0; + GetMultiplierForDeltaXAndY(aEvent, index, &multiplierForDeltaX, + &multiplierForDeltaY); + if (multiplierForDeltaX) { + aEvent->mOverflowDeltaX /= multiplierForDeltaX; + } + if (multiplierForDeltaY) { + aEvent->mOverflowDeltaY /= multiplierForDeltaY; + } +} + +EventStateManager::WheelPrefs::Action +EventStateManager::WheelPrefs::ComputeActionFor( + const WidgetWheelEvent* aEvent) { + Index index = GetIndexFor(aEvent); + Init(index); + + bool deltaXPreferred = (Abs(aEvent->mDeltaX) > Abs(aEvent->mDeltaY) && + Abs(aEvent->mDeltaX) > Abs(aEvent->mDeltaZ)); + Action* actions = deltaXPreferred ? mOverriddenActionsX : mActions; + if (actions[index] == ACTION_NONE || actions[index] == ACTION_SCROLL || + actions[index] == ACTION_HORIZONTALIZED_SCROLL) { + return actions[index]; + } + + // Momentum events shouldn't run special actions. + if (aEvent->mIsMomentum) { + // Use the default action. Note that user might kill the wheel scrolling. + Init(INDEX_DEFAULT); + if (actions[INDEX_DEFAULT] == ACTION_SCROLL || + actions[INDEX_DEFAULT] == ACTION_HORIZONTALIZED_SCROLL) { + return actions[INDEX_DEFAULT]; + } + return ACTION_NONE; + } + + return actions[index]; +} + +bool EventStateManager::WheelPrefs::NeedToComputeLineOrPageDelta( + const WidgetWheelEvent* aEvent) { + Index index = GetIndexFor(aEvent); + Init(index); + + return (mMultiplierX[index] != 1.0 && mMultiplierX[index] != -1.0) || + (mMultiplierY[index] != 1.0 && mMultiplierY[index] != -1.0); +} + +void EventStateManager::WheelPrefs::GetUserPrefsForEvent( + const WidgetWheelEvent* aEvent, double* aOutMultiplierX, + double* aOutMultiplierY) { + Index index = GetIndexFor(aEvent); + Init(index); + + double multiplierForDeltaX = 1.0, multiplierForDeltaY = 1.0; + GetMultiplierForDeltaXAndY(aEvent, index, &multiplierForDeltaX, + &multiplierForDeltaY); + *aOutMultiplierX = multiplierForDeltaX; + *aOutMultiplierY = multiplierForDeltaY; +} + +// static +Maybe<layers::APZWheelAction> EventStateManager::APZWheelActionFor( + const WidgetWheelEvent* aEvent) { + if (aEvent->mMessage != eWheel) { + return Nothing(); + } + WheelPrefs::Action action = + WheelPrefs::GetInstance()->ComputeActionFor(aEvent); + switch (action) { + case WheelPrefs::ACTION_SCROLL: + case WheelPrefs::ACTION_HORIZONTALIZED_SCROLL: + return Some(layers::APZWheelAction::Scroll); + case WheelPrefs::ACTION_PINCH_ZOOM: + return Some(layers::APZWheelAction::PinchZoom); + default: + return Nothing(); + } +} + +// static +WheelDeltaAdjustmentStrategy EventStateManager::GetWheelDeltaAdjustmentStrategy( + const WidgetWheelEvent& aEvent) { + if (aEvent.mMessage != eWheel) { + return WheelDeltaAdjustmentStrategy::eNone; + } + switch (WheelPrefs::GetInstance()->ComputeActionFor(&aEvent)) { + case WheelPrefs::ACTION_SCROLL: + if (StaticPrefs::mousewheel_autodir_enabled() && 0 == aEvent.mDeltaZ) { + if (StaticPrefs::mousewheel_autodir_honourroot()) { + return WheelDeltaAdjustmentStrategy::eAutoDirWithRootHonour; + } + return WheelDeltaAdjustmentStrategy::eAutoDir; + } + return WheelDeltaAdjustmentStrategy::eNone; + case WheelPrefs::ACTION_HORIZONTALIZED_SCROLL: + return WheelDeltaAdjustmentStrategy::eHorizontalize; + default: + break; + } + return WheelDeltaAdjustmentStrategy::eNone; +} + +void EventStateManager::GetUserPrefsForWheelEvent( + const WidgetWheelEvent* aEvent, double* aOutMultiplierX, + double* aOutMultiplierY) { + WheelPrefs::GetInstance()->GetUserPrefsForEvent(aEvent, aOutMultiplierX, + aOutMultiplierY); +} + +bool EventStateManager::WheelPrefs::IsOverOnePageScrollAllowedX( + const WidgetWheelEvent* aEvent) { + Index index = GetIndexFor(aEvent); + Init(index); + return Abs(mMultiplierX[index]) >= + MIN_MULTIPLIER_VALUE_ALLOWING_OVER_ONE_PAGE_SCROLL; +} + +bool EventStateManager::WheelPrefs::IsOverOnePageScrollAllowedY( + const WidgetWheelEvent* aEvent) { + Index index = GetIndexFor(aEvent); + Init(index); + return Abs(mMultiplierY[index]) >= + MIN_MULTIPLIER_VALUE_ALLOWING_OVER_ONE_PAGE_SCROLL; +} + +} // namespace mozilla diff --git a/dom/events/EventStateManager.h b/dom/events/EventStateManager.h new file mode 100644 index 0000000000..7a43ade7a4 --- /dev/null +++ b/dom/events/EventStateManager.h @@ -0,0 +1,1256 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#ifndef mozilla_EventStateManager_h_ +#define mozilla_EventStateManager_h_ + +#include "mozilla/EventForwards.h" + +#include "nsIObserver.h" +#include "nsWeakReference.h" +#include "nsCOMPtr.h" +#include "nsCOMArray.h" +#include "nsCycleCollectionParticipant.h" +#include "nsRefPtrHashtable.h" +#include "mozilla/Attributes.h" +#include "mozilla/TimeStamp.h" +#include "mozilla/layers/APZPublicUtils.h" +#include "mozilla/dom/Record.h" +#include "Units.h" +#include "WheelHandlingHelper.h" // for WheelDeltaAdjustmentStrategy + +class nsFrameLoader; +class nsIContent; +class nsICookieJarSettings; +class nsIDocShell; +class nsIDocShellTreeItem; +class nsIFrame; +class imgIContainer; +class nsIContentViewer; +class nsIScrollableFrame; +class nsITimer; +class nsPresContext; + +namespace mozilla { + +class EditorBase; +class EnterLeaveDispatcher; +class IMEContentObserver; +class ScrollbarsForWheel; +class TextControlElement; +class WheelTransaction; + +namespace dom { +class DataTransfer; +class Document; +class Element; +class Selection; +class BrowserParent; +class RemoteDragStartData; + +} // namespace dom + +class OverOutElementsWrapper final : public nsISupports { + ~OverOutElementsWrapper(); + + public: + OverOutElementsWrapper(); + + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_CLASS(OverOutElementsWrapper) + + WeakFrame mLastOverFrame; + + nsCOMPtr<nsIContent> mLastOverElement; + + // The last element on which we fired a over event, or null if + // the last over event we fired has finished processing. + nsCOMPtr<nsIContent> mFirstOverEventElement; + + // The last element on which we fired a out event, or null if + // the last out event we fired has finished processing. + nsCOMPtr<nsIContent> mFirstOutEventElement; +}; + +class EventStateManager : public nsSupportsWeakReference, public nsIObserver { + friend class mozilla::EnterLeaveDispatcher; + friend class mozilla::ScrollbarsForWheel; + friend class mozilla::WheelTransaction; + + using ElementState = dom::ElementState; + + virtual ~EventStateManager(); + + public: + EventStateManager(); + + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_NSIOBSERVER + + nsresult Init(); + nsresult Shutdown(); + + /* The PreHandleEvent method is called before event dispatch to either + * the DOM or frames. Any processing which must not be prevented or + * cancelled should occur here. Any processing which is intended to + * be conditional based on either DOM or frame processing should occur in + * PostHandleEvent. Any centralized event processing which must occur before + * DOM or frame event handling should occur here as well. + * + * aOverrideClickTarget can be used to indicate which element should be + * used as the *up target when deciding whether to send click event. + * This is used when releasing pointer capture. Otherwise null. + */ + MOZ_CAN_RUN_SCRIPT + nsresult PreHandleEvent(nsPresContext* aPresContext, WidgetEvent* aEvent, + nsIFrame* aTargetFrame, nsIContent* aTargetContent, + nsEventStatus* aStatus, + nsIContent* aOverrideClickTarget); + + /* The PostHandleEvent method should contain all system processing which + * should occur conditionally based on DOM or frame processing. It should + * also contain any centralized event processing which must occur after + * DOM and frame processing. + */ + MOZ_CAN_RUN_SCRIPT + nsresult PostHandleEvent(nsPresContext* aPresContext, WidgetEvent* aEvent, + nsIFrame* aTargetFrame, nsEventStatus* aStatus, + nsIContent* aOverrideClickTarget); + + MOZ_CAN_RUN_SCRIPT void PostHandleKeyboardEvent( + WidgetKeyboardEvent* aKeyboardEvent, nsIFrame* aTargetFrame, + nsEventStatus& aStatus); + + /** + * DispatchLegacyMouseScrollEvents() dispatches eLegacyMouseLineOrPageScroll + * event and eLegacyMousePixelScroll event for compatibility with old Gecko. + */ + MOZ_CAN_RUN_SCRIPT_BOUNDARY void DispatchLegacyMouseScrollEvents( + nsIFrame* aTargetFrame, WidgetWheelEvent* aEvent, nsEventStatus* aStatus); + + MOZ_CAN_RUN_SCRIPT_BOUNDARY void NotifyDestroyPresContext( + nsPresContext* aPresContext); + + void ResetHoverState(); + + void SetPresContext(nsPresContext* aPresContext); + void ClearFrameRefs(nsIFrame* aFrame); + + nsIFrame* GetEventTarget(); + already_AddRefed<nsIContent> GetEventTargetContent(WidgetEvent* aEvent); + + // We manage 4 states here: ACTIVE, HOVER, DRAGOVER, URLTARGET + static bool ManagesState(ElementState aState) { + return aState == ElementState::ACTIVE || aState == ElementState::HOVER || + aState == ElementState::DRAGOVER || + aState == ElementState::URLTARGET; + } + + /** + * Notify that the given ElementState::* bit has changed for this content. + * @param aContent Content which has changed states + * @param aState Corresponding state flags such as ElementState::FOCUS + * @return Whether the content was able to change all states. Returns false + * if a resulting DOM event causes the content node passed in + * to not change states. Note, the frame for the content may + * change as a result of the content state change, because of + * frame reconstructions that may occur, but this does not + * affect the return value. + */ + bool SetContentState(nsIContent* aContent, ElementState aState); + + nsIContent* GetActiveContent() const { return mActiveContent; } + + void NativeAnonymousContentRemoved(nsIContent* aAnonContent); + MOZ_CAN_RUN_SCRIPT_BOUNDARY void ContentRemoved(dom::Document* aDocument, + nsIContent* aContent); + + /** + * Called when a native anonymous <div> element which is root element of + * text editor will be removed. + */ + void TextControlRootWillBeRemoved(TextControlElement& aTextControlElement); + + /** + * Called when a native anonymous <div> element which is root element of + * text editor is created. + */ + void TextControlRootAdded(dom::Element& aAnonymousDivElement, + TextControlElement& aTextControlElement); + + bool EventStatusOK(WidgetGUIEvent* aEvent); + + /** + * EventStateManager stores IMEContentObserver while it's observing contents. + * Following mehtods are called by IMEContentObserver when it starts to + * observe or stops observing the content. + */ + void OnStartToObserveContent(IMEContentObserver* aIMEContentObserver); + void OnStopObservingContent(IMEContentObserver* aIMEContentObserver); + + /** + * TryToFlushPendingNotificationsToIME() suggests flushing pending + * notifications to IME to IMEContentObserver. + * Doesn't do anything in child processes where flushing happens + * asynchronously. + */ + void TryToFlushPendingNotificationsToIME(); + + static bool IsKeyboardEventUserActivity(WidgetEvent* aEvent); + + /** + * Register accesskey on the given element. When accesskey is activated then + * the element will be notified via Element::PerformAccesskey() method. + * + * @param aElement the given element + * @param aKey accesskey + */ + void RegisterAccessKey(dom::Element* aElement, uint32_t aKey); + + /** + * Unregister accesskey for the given element. + * + * @param aElement the given element + * @param aKey accesskey + */ + void UnregisterAccessKey(dom::Element* aElement, uint32_t aKey); + + /** + * Get accesskey registered on the given element or 0 if there is none. + * + * @param aElement the given element (must not be null) + * @return registered accesskey + */ + uint32_t GetRegisteredAccessKey(dom::Element* aContent); + + static void GetAccessKeyLabelPrefix(dom::Element* aElement, + nsAString& aPrefix); + + /** + * HandleAccessKey() looks for access keys which matches with aEvent and + * execute when it matches with a chrome access key or some content access + * keys. + * If the event may match chrome access keys, this handles the access key + * synchronously (if there are nested ESMs, their HandleAccessKey() are + * also called recursively). + * If the event may match content access keys and focused target is a remote + * process, this does nothing for the content because when this is called, + * it should already have been handled in the remote process. + * If the event may match content access keys and focused target is not in + * remote process but there are some remote children, this will post + * HandleAccessKey messages to all remote children. + * + * @return true if there is accesskey which aEvent and + * aAccessCharCodes match with. Otherwise, false. + * I.e., when this returns true, a target is executed + * or focused. + * Note that even if this returns false, a target in + * remote process may be executed or focused + * asynchronously. + */ + bool HandleAccessKey(WidgetKeyboardEvent* aEvent, nsPresContext* aPresContext, + nsTArray<uint32_t>& aAccessCharCodes) { + return WalkESMTreeToHandleAccessKey(aEvent, aPresContext, aAccessCharCodes, + nullptr, eAccessKeyProcessingNormal, + true); + } + + /** + * CheckIfEventMatchesAccessKey() looks for access key which matches with + * aEvent in the process but won't execute it. + * + * @return true if there is accesskey which aEvent matches with + * in this process. Otherwise, false. + */ + bool CheckIfEventMatchesAccessKey(WidgetKeyboardEvent* aEvent, + nsPresContext* aPresContext); + + nsresult SetCursor(StyleCursorKind, imgIContainer*, const ImageResolution&, + const Maybe<gfx::IntPoint>& aHotspot, nsIWidget* aWidget, + bool aLockCursor); + + /** + * Checks if the current mouse over element matches the given + * Element (which has a remote frame), and if so, notifies + * the BrowserParent of the mouse enter. + * Called when we reconstruct the BrowserParent and need to + * recompute state on the new object. + */ + void RecomputeMouseEnterStateForRemoteFrame(dom::Element& aElement); + + nsPresContext* GetPresContext() { return mPresContext; } + + NS_DECL_CYCLE_COLLECTION_CLASS_AMBIGUOUS(EventStateManager, nsIObserver) + + static dom::Document* sMouseOverDocument; + + static EventStateManager* GetActiveEventStateManager() { return sActiveESM; } + + // Sets aNewESM to be the active event state manager, and + // if aContent is non-null, marks the object as active. + static void SetActiveManager(EventStateManager* aNewESM, + nsIContent* aContent); + + static bool IsRemoteTarget(nsIContent* target); + + static bool IsTopLevelRemoteTarget(nsIContent* aTarget); + + // Returns the kind of APZ action the given WidgetWheelEvent will perform. + static Maybe<layers::APZWheelAction> APZWheelActionFor( + const WidgetWheelEvent* aEvent); + + // For some kinds of scrollings, the delta values of WidgetWheelEvent are + // possbile to be adjusted. This function is used to detect such scrollings + // and returns a wheel delta adjustment strategy to use, which is corresponded + // to the kind of the scrolling. + // It returns WheelDeltaAdjustmentStrategy::eAutoDir if the current default + // action is auto-dir scrolling which honours the scrolling target(The + // comments in WheelDeltaAdjustmentStrategy describes the concept in detail). + // It returns WheelDeltaAdjustmentStrategy::eAutoDirWithRootHonour if the + // current action is auto-dir scrolling which honours the root element in the + // document where the scrolling target is(The comments in + // WheelDeltaAdjustmentStrategy describes the concept in detail). + // It returns WheelDeltaAdjustmentStrategy::eHorizontalize if the current + // default action is horizontalized scrolling. + // It returns WheelDeltaAdjustmentStrategy::eNone to mean no delta adjustment + // strategy should be used if the scrolling is just a tranditional scrolling + // whose delta values are never possible to be adjusted. + static WheelDeltaAdjustmentStrategy GetWheelDeltaAdjustmentStrategy( + const WidgetWheelEvent& aEvent); + + // Returns user-set multipliers for a wheel event. + static void GetUserPrefsForWheelEvent(const WidgetWheelEvent* aEvent, + double* aOutMultiplierX, + double* aOutMultiplierY); + + // Holds the point in screen coords that a mouse event was dispatched to, + // before we went into pointer lock mode. This is constantly updated while + // the pointer is not locked, but we don't update it while the pointer is + // locked. This is used by dom::Event::GetScreenCoords() to make mouse + // events' screen coord appear frozen at the last mouse position while + // the pointer is locked. + static CSSIntPoint sLastScreenPoint; + + // Holds the point in client coords of the last mouse event. Used by + // dom::Event::GetClientCoords() to make mouse events' client coords appear + // frozen at the last mouse position while the pointer is locked. + static CSSIntPoint sLastClientPoint; + + /** + * If the absolute values of mMultiplierX and/or mMultiplierY are equal or + * larger than this value, the computed scroll amount isn't rounded down to + * the page width or height. + */ + static constexpr double MIN_MULTIPLIER_VALUE_ALLOWING_OVER_ONE_PAGE_SCROLL = + 1000.0; + + /** + * HandleMiddleClickPaste() handles middle mouse button event as pasting + * clipboard text. Note that if aEditorBase is nullptr, this only + * dispatches ePaste event because it's necessary for some web apps which + * want to implement their own editor and supports middle click paste. + * + * @param aPresShell The PresShell for the ESM. This lifetime + * should be guaranteed by the caller. + * @param aMouseEvent The eMouseClick event which caused the + * paste. + * @param aStatus The event status of aMouseEvent. + * @param aEditorBase EditorBase which may be pasted the + * clipboard text by the middle click. + * If there is no editor for aMouseEvent, + * set nullptr. + */ + MOZ_CAN_RUN_SCRIPT + nsresult HandleMiddleClickPaste(PresShell* aPresShell, + WidgetMouseEvent* aMouseEvent, + nsEventStatus* aStatus, + EditorBase* aEditorBase); + + static void ConsumeInteractionData( + dom::Record<nsString, dom::InteractionData>& aInteractions); + + // Stop tracking a possible drag. If aClearInChildProcesses is true, send + // a notification to any child processes that are in the drag service that + // tried to start a drag. + void StopTrackingDragGesture(bool aClearInChildProcesses); + + protected: + /* + * If aTargetFrame's widget has a cached cursor value, resets the cursor + * such that the next call to SetCursor on the widget will force an update + * of the native cursor. For use in getting puppet widget to update its + * cursor between mouse exit / enter transitions. This call basically wraps + * nsIWidget ClearCachedCursor. + */ + void ClearCachedWidgetCursor(nsIFrame* aTargetFrame); + + void UpdateCursor(nsPresContext* aPresContext, WidgetEvent* aEvent, + nsIFrame* aTargetFrame, nsEventStatus* aStatus); + /** + * Turn a GUI mouse/pointer event into a mouse/pointer event targeted at the + * specified content. This returns the primary frame for the content (or null + * if it goes away during the event). + */ + MOZ_CAN_RUN_SCRIPT nsIFrame* DispatchMouseOrPointerEvent( + WidgetMouseEvent* aMouseEvent, EventMessage aMessage, + nsIContent* aTargetContent, nsIContent* aRelatedContent); + /** + * Synthesize DOM pointerover and pointerout events + */ + void GeneratePointerEnterExit(EventMessage aMessage, + WidgetMouseEvent* aEvent); + /** + * Synthesize DOM and frame mouseover and mouseout events from this + * MOUSE_MOVE or MOUSE_EXIT event. + */ + void GenerateMouseEnterExit(WidgetMouseEvent* aMouseEvent); + /** + * Tell this ESM and ESMs in parent documents that the mouse is + * over some content in this document. + */ + MOZ_CAN_RUN_SCRIPT_BOUNDARY void NotifyMouseOver( + WidgetMouseEvent* aMouseEvent, nsIContent* aContent); + /** + * Tell this ESM and ESMs in affected child documents that the mouse + * has exited this document's currently hovered content. + * TODO: Convert this to MOZ_CAN_RUN_SCRIPT (bug 1415230) + * + * @param aMouseEvent the event that triggered the mouseout + * @param aMovingInto the content node we've moved into. This is used to set + * the relatedTarget for mouseout events. Also, if it's non-null + * NotifyMouseOut will NOT change the current hover content to null; + * in that case the caller is responsible for updating hover state. + */ + MOZ_CAN_RUN_SCRIPT_BOUNDARY void NotifyMouseOut(WidgetMouseEvent* aMouseEvent, + nsIContent* aMovingInto); + MOZ_CAN_RUN_SCRIPT void GenerateDragDropEnterExit( + nsPresContext* aPresContext, WidgetDragEvent* aDragEvent); + + /** + * Return mMouseEnterLeaveHelper or relevant mPointersEnterLeaveHelper + * elements wrapper. If mPointersEnterLeaveHelper does not contain wrapper for + * pointerId it create new one + */ + OverOutElementsWrapper* GetWrapperByEventID(WidgetMouseEvent* aMouseEvent); + + /** + * Fire the dragenter and dragexit/dragleave events when the mouse moves to a + * new target. + * + * @param aRelatedTarget relatedTarget to set for the event + * @param aTargetContent target to set for the event + * @param aTargetFrame target frame for the event + */ + MOZ_CAN_RUN_SCRIPT void FireDragEnterOrExit(nsPresContext* aPresContext, + WidgetDragEvent* aDragEvent, + EventMessage aMessage, + nsIContent* aRelatedTarget, + nsIContent* aTargetContent, + AutoWeakFrame& aTargetFrame); + /** + * Update the initial drag session data transfer with any changes that occur + * on cloned data transfer objects used for events. + */ + void UpdateDragDataTransfer(WidgetDragEvent* dragEvent); + + /** + * InitAndDispatchClickEvent() dispatches a click event. + * + * @param aMouseUpEvent eMouseUp event which causes the click event. + * EventCausesClickEvents() must return true + * if this event is set to it. + * @param aStatus Returns the result of click event. + * If the status indicates consumed, the + * value won't be overwritten with + * nsEventStatus_eIgnore. + * @param aMessage Should be eMouseClick, eMouseDoubleClick or + * eMouseAuxClick. + * @param aPresShell The PresShell. + * @param aMouseUpContent The event target of aMouseUpEvent. + * @param aCurrentTarget Current target of the caller. + * @param aNoContentDispatch true if the event shouldn't be exposed to + * web contents (although will be fired on + * document and window). + * @param aOverrideClickTarget Preferred click event target. If this is + * not nullptr, aMouseUpContent and + * aCurrentTarget are ignored. + */ + MOZ_CAN_RUN_SCRIPT + static nsresult InitAndDispatchClickEvent( + WidgetMouseEvent* aMouseUpEvent, nsEventStatus* aStatus, + EventMessage aMessage, PresShell* aPresShell, nsIContent* aMouseUpContent, + AutoWeakFrame aCurrentTarget, bool aNoContentDispatch, + nsIContent* aOverrideClickTarget); + + nsresult SetClickCount(WidgetMouseEvent* aEvent, nsEventStatus* aStatus, + nsIContent* aOverrideClickTarget = nullptr); + + /** + * EventCausesClickEvents() returns true when aMouseEvent is an eMouseUp + * event and it should cause eMouseClick, eMouseDoubleClick and/or + * eMouseAuxClick events. Note that this method assumes that + * aMouseEvent.mClickCount has already been initialized with SetClickCount(). + */ + static bool EventCausesClickEvents(const WidgetMouseEvent& aMouseEvent); + + /** + * PostHandleMouseUp() handles default actions of eMouseUp event. + * + * @param aMouseUpEvent eMouseUp event which causes the click event. + * EventCausesClickEvents() must return true + * if this event is set to it. + * @param aStatus Returns the result of event status. + * If one of dispatching event is consumed or + * this does something as default action, + * returns nsEventStatus_eConsumeNoDefault. + * @param aOverrideClickTarget Preferred click event target. If nullptr, + * aMouseUpEvent target and current target + * are used. + */ + MOZ_CAN_RUN_SCRIPT + nsresult PostHandleMouseUp(WidgetMouseEvent* aMouseUpEvent, + nsEventStatus* aStatus, + nsIContent* aOverrideClickTarget); + + /** + * DispatchClickEvents() dispatches eMouseClick, eMouseDoubleClick and + * eMouseAuxClick events for aMouseUpEvent. aMouseUpEvent should cause + * click event. + * + * @param aPresShell The PresShell. + * @param aMouseUpEvent eMouseUp event which causes the click event. + * EventCausesClickEvents() must return true + * if this event is set to it. + * @param aStatus Returns the result of event status. + * If one of dispatching click event is + * consumed, returns + * nsEventStatus_eConsumeNoDefault. + * @param aMouseUpContent The event target of aMouseUpEvent. + * @param aOverrideClickTarget Preferred click event target. If this is + * not nullptr, aMouseUpContent and + * current target frame of the ESM are ignored. + */ + MOZ_CAN_RUN_SCRIPT + nsresult DispatchClickEvents(PresShell* aPresShell, + WidgetMouseEvent* aMouseUpEvent, + nsEventStatus* aStatus, + nsIContent* aMouseUpContent, + nsIContent* aOverrideClickTarget); + + void EnsureDocument(nsPresContext* aPresContext); + MOZ_CAN_RUN_SCRIPT_BOUNDARY + void FlushLayout(nsPresContext* aPresContext); + + /** + * The phases of WalkESMTreeToHandleAccessKey processing. See below. + */ + enum ProcessingAccessKeyState { + eAccessKeyProcessingNormal = 0, + eAccessKeyProcessingUp, + eAccessKeyProcessingDown + }; + + /** + * Walk EMS to look for access key and execute found access key when aExecute + * is true. + * If there is registered element for the accesskey given by the key event + * and modifier mask then call element.PerformAccesskey(), otherwise call + * WalkESMTreeToHandleAccessKey() recursively, on descendant docshells first, + * then on the ancestor (with |aBubbledFrom| set to the docshell associated + * with |this|), until something matches. + * + * @param aEvent the keyboard event triggering the acccess key + * @param aPresContext the presentation context + * @param aAccessCharCodes list of charcode candidates + * @param aBubbledFrom is used by an ancestor to avoid calling + * WalkESMTreeToHandleAccessKey() on the child the call originally + * came from, i.e. this is the child that recursively called us in + * its Up phase. The initial caller passes |nullptr| here. This is to + * avoid an infinite loop. + * @param aAccessKeyState Normal, Down or Up processing phase (see enums + * above). The initial event receiver uses 'normal', then 'down' when + * processing children and Up when recursively calling its ancestor. + * @param aExecute is true, execute an accesskey if it's found. Otherwise, + * found accesskey won't be executed. + * + * @return true if there is a target which aEvent and + * aAccessCharCodes match with in this process. + * Otherwise, false. I.e., when this returns true and + * aExecute is true, a target is executed or focused. + * Note that even if this returns false, a target in + * remote process may be executed or focused + * asynchronously. + */ + bool WalkESMTreeToHandleAccessKey(WidgetKeyboardEvent* aEvent, + nsPresContext* aPresContext, + nsTArray<uint32_t>& aAccessCharCodes, + nsIDocShellTreeItem* aBubbledFrom, + ProcessingAccessKeyState aAccessKeyState, + bool aExecute); + + /** + * Look for access key and execute found access key if aExecute is true in + * the instance. + * + * @return true if there is a target which matches with + * aAccessCharCodes and aIsTrustedEvent. Otherwise, + * false. I.e., when this returns true and aExecute + * is true, a target is executed or focused. + */ + MOZ_CAN_RUN_SCRIPT_BOUNDARY bool LookForAccessKeyAndExecute( + nsTArray<uint32_t>& aAccessCharCodes, bool aIsTrustedEvent, + bool aIsRepeat, bool aExecute); + + //--------------------------------------------- + // DocShell Focus Traversal Methods + //--------------------------------------------- + + dom::Element* GetFocusedElement(); + bool IsShellVisible(nsIDocShell* aShell); + + // These functions are for mousewheel and pixel scrolling + + class WheelPrefs { + public: + static WheelPrefs* GetInstance(); + static void Shutdown(); + + /** + * ApplyUserPrefsToDelta() overrides the wheel event's delta values with + * user prefs. + */ + void ApplyUserPrefsToDelta(WidgetWheelEvent* aEvent); + + /** + * Returns whether or not ApplyUserPrefsToDelta() would change the delta + * values of an event. + */ + void GetUserPrefsForEvent(const WidgetWheelEvent* aEvent, + double* aOutMultiplierX, double* aOutMultiplierY); + + /** + * If ApplyUserPrefsToDelta() changed the delta values with customized + * prefs, the overflowDelta values would be inflated. + * CancelApplyingUserPrefsFromOverflowDelta() cancels the inflation. + */ + void CancelApplyingUserPrefsFromOverflowDelta(WidgetWheelEvent* aEvent); + + /** + * Computes the default action for the aEvent with the prefs. + */ + enum Action : uint8_t { + ACTION_NONE = 0, + ACTION_SCROLL, + ACTION_HISTORY, + ACTION_ZOOM, + // Horizontalized scrolling means treating vertical wheel scrolling as + // horizontal scrolling during the process of its default action and + // plugins handling scrolling. Note that delta values as the event object + // in a DOM event listener won't be affected, and will be still the + // original values. For more details, refer to + // mozilla::WheelDeltaAdjustmentStrategy::eHorizontalize + ACTION_HORIZONTALIZED_SCROLL, + ACTION_PINCH_ZOOM, + ACTION_LAST = ACTION_PINCH_ZOOM, + // Following actions are used only by internal processing. So, cannot + // specified by prefs. + ACTION_SEND_TO_PLUGIN, + }; + Action ComputeActionFor(const WidgetWheelEvent* aEvent); + + /** + * NeedToComputeLineOrPageDelta() returns if the aEvent needs to be + * computed the lineOrPageDelta values. + */ + bool NeedToComputeLineOrPageDelta(const WidgetWheelEvent* aEvent); + + /** + * IsOverOnePageScrollAllowed*() checks whether wheel scroll amount should + * be rounded down to the page width/height (false) or not (true). + */ + bool IsOverOnePageScrollAllowedX(const WidgetWheelEvent* aEvent); + bool IsOverOnePageScrollAllowedY(const WidgetWheelEvent* aEvent); + + private: + WheelPrefs(); + ~WheelPrefs(); + + static void OnPrefChanged(const char* aPrefName, void* aClosure); + + enum Index { + INDEX_DEFAULT = 0, + INDEX_ALT, + INDEX_CONTROL, + INDEX_META, + INDEX_SHIFT, + INDEX_OS, + COUNT_OF_MULTIPLIERS + }; + + /** + * GetIndexFor() returns the index of the members which should be used for + * the aEvent. When only one modifier key of MODIFIER_ALT, + * MODIFIER_CONTROL, MODIFIER_META, MODIFIER_SHIFT or MODIFIER_OS is + * pressed, returns the index for the modifier. Otherwise, this return the + * default index which is used at either no modifier key is pressed or + * two or modifier keys are pressed. + */ + Index GetIndexFor(const WidgetWheelEvent* aEvent); + + /** + * GetPrefNameBase() returns the base pref name for aEvent. + * It's decided by GetModifierForPref() which modifier should be used for + * the aEvent. + * + * @param aBasePrefName The result, must be "mousewheel.with_*." or + * "mousewheel.default.". + */ + void GetBasePrefName(Index aIndex, nsACString& aBasePrefName); + + void Init(Index aIndex); + + void Reset(); + + /** + * Retrieve multiplier for aEvent->mDeltaX and aEvent->mDeltaY. + * + * Note that if the default action is ACTION_HORIZONTALIZED_SCROLL and the + * delta values have been adjusted by WheelDeltaHorizontalizer() before this + * function is called, this function will swap the X and Y multipliers. By + * doing this, multipliers will still apply to the delta values they + * originally corresponded to. + * + * @param aEvent The event which is being handled. + * @param aIndex The index of mMultiplierX and mMultiplierY. + * Should be result of GetIndexFor(aEvent). + * @param aMultiplierForDeltaX Will be set to multiplier for + * aEvent->mDeltaX. + * @param aMultiplierForDeltaY Will be set to multiplier for + * aEvent->mDeltaY. + */ + void GetMultiplierForDeltaXAndY(const WidgetWheelEvent* aEvent, + Index aIndex, double* aMultiplierForDeltaX, + double* aMultiplierForDeltaY); + + bool mInit[COUNT_OF_MULTIPLIERS]; + double mMultiplierX[COUNT_OF_MULTIPLIERS]; + double mMultiplierY[COUNT_OF_MULTIPLIERS]; + double mMultiplierZ[COUNT_OF_MULTIPLIERS]; + Action mActions[COUNT_OF_MULTIPLIERS]; + /** + * action values overridden by .override_x pref. + * If an .override_x value is -1, same as the + * corresponding mActions value. + */ + Action mOverriddenActionsX[COUNT_OF_MULTIPLIERS]; + + static WheelPrefs* sInstance; + }; + + /** + * DeltaDirection is used for specifying whether the called method should + * handle vertical delta or horizontal delta. + * This is clearer than using bool. + */ + enum DeltaDirection { DELTA_DIRECTION_X = 0, DELTA_DIRECTION_Y }; + + struct MOZ_STACK_CLASS EventState { + bool mDefaultPrevented; + bool mDefaultPreventedByContent; + + EventState() + : mDefaultPrevented(false), mDefaultPreventedByContent(false) {} + }; + + /** + * SendLineScrollEvent() dispatches a DOMMouseScroll event for the + * WidgetWheelEvent. This method shouldn't be called for non-trusted + * wheel event because it's not necessary for compatiblity. + * + * @param aTargetFrame The event target of wheel event. + * @param aEvent The original Wheel event. + * @param aState The event which should be set to the dispatching + * event. This also returns the dispatched event + * state. + * @param aDelta The delta value of the event. + * @param aDeltaDirection The X/Y direction of dispatching event. + */ + MOZ_CAN_RUN_SCRIPT void SendLineScrollEvent(nsIFrame* aTargetFrame, + WidgetWheelEvent* aEvent, + EventState& aState, + int32_t aDelta, + DeltaDirection aDeltaDirection); + + /** + * SendPixelScrollEvent() dispatches a MozMousePixelScroll event for the + * WidgetWheelEvent. This method shouldn't be called for non-trusted + * wheel event because it's not necessary for compatiblity. + * + * @param aTargetFrame The event target of wheel event. + * @param aEvent The original Wheel event. + * @param aState The event which should be set to the dispatching + * event. This also returns the dispatched event + * state. + * @param aPixelDelta The delta value of the event. + * @param aDeltaDirection The X/Y direction of dispatching event. + */ + MOZ_CAN_RUN_SCRIPT void SendPixelScrollEvent(nsIFrame* aTargetFrame, + WidgetWheelEvent* aEvent, + EventState& aState, + int32_t aPixelDelta, + DeltaDirection aDeltaDirection); + + /** + * ComputeScrollTargetAndMayAdjustWheelEvent() returns the scrollable frame + * which should be scrolled. + * + * @param aTargetFrame The event target of the wheel event. + * @param aEvent The handling mouse wheel event. + * @param aOptions The options for finding the scroll target. + * Callers should use COMPUTE_*. + * @return The scrollable frame which should be scrolled. + */ + // These flags are used in ComputeScrollTargetAndMayAdjustWheelEvent(). + // Callers should use COMPUTE_*. + enum { + PREFER_MOUSE_WHEEL_TRANSACTION = 0x00000001, + PREFER_ACTUAL_SCROLLABLE_TARGET_ALONG_X_AXIS = 0x00000002, + PREFER_ACTUAL_SCROLLABLE_TARGET_ALONG_Y_AXIS = 0x00000004, + START_FROM_PARENT = 0x00000008, + INCLUDE_PLUGIN_AS_TARGET = 0x00000010, + // Indicates the wheel scroll event being computed is an auto-dir scroll, so + // its delta may be adjusted after being computed. + MAY_BE_ADJUSTED_BY_AUTO_DIR = 0x00000020, + }; + enum ComputeScrollTargetOptions { + // At computing scroll target for legacy mouse events, we should return + // first scrollable element even when it's not scrollable to the direction. + COMPUTE_LEGACY_MOUSE_SCROLL_EVENT_TARGET = 0, + // Default action prefers the scrolled element immediately before if it's + // still under the mouse cursor. Otherwise, it prefers the nearest + // scrollable ancestor which will be scrolled actually. + COMPUTE_DEFAULT_ACTION_TARGET = + (PREFER_MOUSE_WHEEL_TRANSACTION | + PREFER_ACTUAL_SCROLLABLE_TARGET_ALONG_X_AXIS | + PREFER_ACTUAL_SCROLLABLE_TARGET_ALONG_Y_AXIS), + COMPUTE_DEFAULT_ACTION_TARGET_WITH_AUTO_DIR = + (COMPUTE_DEFAULT_ACTION_TARGET | MAY_BE_ADJUSTED_BY_AUTO_DIR), + // Look for the nearest scrollable ancestor which can be scrollable with + // aEvent. + COMPUTE_SCROLLABLE_ANCESTOR_ALONG_X_AXIS = + (PREFER_ACTUAL_SCROLLABLE_TARGET_ALONG_X_AXIS | START_FROM_PARENT), + COMPUTE_SCROLLABLE_ANCESTOR_ALONG_Y_AXIS = + (PREFER_ACTUAL_SCROLLABLE_TARGET_ALONG_Y_AXIS | START_FROM_PARENT), + COMPUTE_SCROLLABLE_ANCESTOR_ALONG_X_AXIS_WITH_AUTO_DIR = + (COMPUTE_SCROLLABLE_ANCESTOR_ALONG_X_AXIS | + MAY_BE_ADJUSTED_BY_AUTO_DIR), + COMPUTE_SCROLLABLE_ANCESTOR_ALONG_Y_AXIS_WITH_AUTO_DIR = + (COMPUTE_SCROLLABLE_ANCESTOR_ALONG_Y_AXIS | + MAY_BE_ADJUSTED_BY_AUTO_DIR), + }; + + // Compute the scroll target. + // The delta values in the wheel event may be changed if the event is for + // auto-dir scrolling. For information on auto-dir, + // @see mozilla::WheelDeltaAdjustmentStrategy + nsIFrame* ComputeScrollTargetAndMayAdjustWheelEvent( + nsIFrame* aTargetFrame, WidgetWheelEvent* aEvent, + ComputeScrollTargetOptions aOptions); + + nsIFrame* ComputeScrollTargetAndMayAdjustWheelEvent( + nsIFrame* aTargetFrame, double aDirectionX, double aDirectionY, + WidgetWheelEvent* aEvent, ComputeScrollTargetOptions aOptions); + + nsIFrame* ComputeScrollTarget(nsIFrame* aTargetFrame, + WidgetWheelEvent* aEvent, + ComputeScrollTargetOptions aOptions) { + MOZ_ASSERT(!(aOptions & MAY_BE_ADJUSTED_BY_AUTO_DIR), + "aEvent may be modified by auto-dir"); + return ComputeScrollTargetAndMayAdjustWheelEvent(aTargetFrame, aEvent, + aOptions); + } + + nsIFrame* ComputeScrollTarget(nsIFrame* aTargetFrame, double aDirectionX, + double aDirectionY, WidgetWheelEvent* aEvent, + ComputeScrollTargetOptions aOptions) { + MOZ_ASSERT(!(aOptions & MAY_BE_ADJUSTED_BY_AUTO_DIR), + "aEvent may be modified by auto-dir"); + return ComputeScrollTargetAndMayAdjustWheelEvent( + aTargetFrame, aDirectionX, aDirectionY, aEvent, aOptions); + } + + /** + * GetScrollAmount() returns the scroll amount in app uints of one line or + * one page. If the wheel event scrolls a page, returns the page width and + * height. Otherwise, returns line height for both its width and height. + * + * @param aScrollableFrame A frame which will be scrolled by the event. + * The result of + * ComputeScrollTargetAndMayAdjustWheelEvent() is + * expected for this value. + * This can be nullptr if there is no scrollable + * frame. Then, this method uses root frame's + * line height or visible area's width and height. + */ + nsSize GetScrollAmount(nsPresContext* aPresContext, WidgetWheelEvent* aEvent, + nsIScrollableFrame* aScrollableFrame); + + /** + * DoScrollText() scrolls the scrollable frame for aEvent. + */ + void DoScrollText(nsIScrollableFrame* aScrollableFrame, + WidgetWheelEvent* aEvent); + + void DoScrollHistory(int32_t direction); + void DoScrollZoom(nsIFrame* aTargetFrame, int32_t adjustment); + void ChangeZoom(bool aIncrease); + + /** + * DeltaAccumulator class manages delta values for dispatching DOMMouseScroll + * event. If wheel events are caused by pixel scroll only devices or + * the delta values are customized by prefs, this class stores the delta + * values and set lineOrPageDelta values. + */ + class DeltaAccumulator { + public: + static DeltaAccumulator* GetInstance() { + if (!sInstance) { + sInstance = new DeltaAccumulator; + } + return sInstance; + } + + static void Shutdown() { + delete sInstance; + sInstance = nullptr; + } + + bool IsInTransaction() { return mHandlingDeltaMode != UINT32_MAX; } + + /** + * InitLineOrPageDelta() stores pixel delta values of WidgetWheelEvents + * which are caused if it's needed. And if the accumulated delta becomes a + * line height, sets lineOrPageDeltaX and lineOrPageDeltaY automatically. + */ + void InitLineOrPageDelta(nsIFrame* aTargetFrame, EventStateManager* aESM, + WidgetWheelEvent* aEvent); + + /** + * Reset() resets all members. + */ + void Reset(); + + /** + * ComputeScrollAmountForDefaultAction() computes the default action's + * scroll amount in device pixels with mPendingScrollAmount*. + */ + nsIntPoint ComputeScrollAmountForDefaultAction( + WidgetWheelEvent* aEvent, const nsIntSize& aScrollAmountInDevPixels); + + private: + DeltaAccumulator() + : mX(0.0), + mY(0.0), + mPendingScrollAmountX(0.0), + mPendingScrollAmountY(0.0), + mHandlingDeltaMode(UINT32_MAX), + mIsNoLineOrPageDeltaDevice(false) {} + + double mX; + double mY; + + // When default action of a wheel event is scroll but some delta values + // are ignored because the computed amount values are not integer, the + // fractional values are saved by these members. + double mPendingScrollAmountX; + double mPendingScrollAmountY; + + TimeStamp mLastTime; + + uint32_t mHandlingDeltaMode; + bool mIsNoLineOrPageDeltaDevice; + + static DeltaAccumulator* sInstance; + }; + + // end mousewheel functions + + /* + * When a touch gesture is about to start, this function determines what + * kind of gesture interaction we will want to use, based on what is + * underneath the initial touch point. + * Currently it decides between panning (finger scrolling) or dragging + * the target element, as well as the orientation to trigger panning and + * display visual boundary feedback. The decision is stored back in aEvent. + */ + void DecideGestureEvent(WidgetGestureNotifyEvent* aEvent, + nsIFrame* targetFrame); + + // routines for the d&d gesture tracking state machine + void BeginTrackingDragGesture(nsPresContext* aPresContext, + WidgetMouseEvent* aDownEvent, + nsIFrame* aDownFrame); + + void SetGestureDownPoint(WidgetGUIEvent* aEvent); + + LayoutDeviceIntPoint GetEventRefPoint(WidgetEvent* aEvent) const; + + friend class mozilla::dom::BrowserParent; + void BeginTrackingRemoteDragGesture(nsIContent* aContent, + dom::RemoteDragStartData* aDragStartData); + + MOZ_CAN_RUN_SCRIPT + void GenerateDragGesture(nsPresContext* aPresContext, + WidgetInputEvent* aEvent); + + /** + * When starting a dnd session, UA must fire a pointercancel event and stop + * firing the subsequent pointer events. + */ + MOZ_CAN_RUN_SCRIPT + void MaybeFirePointerCancel(WidgetInputEvent* aEvent); + + /** + * Determine which node the drag should be targeted at. + * This is either the node clicked when there is a selection, or, for HTML, + * the element with a draggable property set to true. + * + * aSelectionTarget - target to check for selection + * aDataTransfer - data transfer object that will contain the data to drag + * aAllowEmptyDataTransfer - [out] set to true, if dnd operation can be + * started even if DataTransfer is empty + * aSelection - [out] set to the selection to be dragged + * aTargetNode - [out] the draggable node, or null if there isn't one + * aPrincipal - [out] set to the triggering principal of the drag, or null + * if it's from browser chrome or OS + * aCookieJarSettings - [out] set to the cookieJarSettings of the drag, or + * null if it's from browser chrome or OS. + */ + void DetermineDragTargetAndDefaultData( + nsPIDOMWindowOuter* aWindow, nsIContent* aSelectionTarget, + dom::DataTransfer* aDataTransfer, bool* aAllowEmptyDataTransfer, + dom::Selection** aSelection, + dom::RemoteDragStartData** aRemoteDragStartData, nsIContent** aTargetNode, + nsIPrincipal** aPrincipal, nsIContentSecurityPolicy** aCsp, + nsICookieJarSettings** aCookieJarSettings); + + /* + * Perform the default handling for the dragstart event and set up a + * drag for aDataTransfer if it contains any data. Returns true if a drag has + * started. + * + * aDragEvent - the dragstart event + * aDataTransfer - the data transfer that holds the data to be dragged + * aAllowEmptyDataTransfer - if true, dnd can be started even if there is no + * data to drag + * aDragTarget - the target of the drag + * aSelection - the selection to be dragged + * aData - information pertaining to a drag started in a child process + * aPrincipal - the triggering principal of the drag, or null if it's from + * browser chrome or OS + * aCookieJarSettings - the cookieJarSettings of the drag. or null if it's + * from browser chrome or OS. + */ + MOZ_CAN_RUN_SCRIPT + bool DoDefaultDragStart( + nsPresContext* aPresContext, WidgetDragEvent* aDragEvent, + dom::DataTransfer* aDataTransfer, bool aAllowEmptyDataTransfer, + nsIContent* aDragTarget, dom::Selection* aSelection, + dom::RemoteDragStartData* aDragStartData, nsIPrincipal* aPrincipal, + nsIContentSecurityPolicy* aCsp, nsICookieJarSettings* aCookieJarSettings); + + bool IsTrackingDragGesture() const { return mGestureDownContent != nullptr; } + /** + * Set the fields of aEvent to reflect the mouse position and modifier keys + * that were set when the user first pressed the mouse button (stored by + * BeginTrackingDragGesture). aEvent->mWidget must be + * mCurrentTarget->GetNearestWidget(). + */ + void FillInEventFromGestureDown(WidgetMouseEvent* aEvent); + + MOZ_CAN_RUN_SCRIPT + nsresult DoContentCommandEvent(WidgetContentCommandEvent* aEvent); + MOZ_CAN_RUN_SCRIPT + nsresult DoContentCommandInsertTextEvent(WidgetContentCommandEvent* aEvent); + nsresult DoContentCommandScrollEvent(WidgetContentCommandEvent* aEvent); + + dom::BrowserParent* GetCrossProcessTarget(); + bool IsTargetCrossProcess(WidgetGUIEvent* aEvent); + + /** + * DispatchCrossProcessEvent() try to post aEvent to target remote process. + * If you need to check if the event is posted to a remote process, you + * can use aEvent->HasBeenPostedToRemoteProcess(). + */ + void DispatchCrossProcessEvent(WidgetEvent* aEvent, + dom::BrowserParent* aRemoteTarget, + nsEventStatus* aStatus); + /** + * HandleCrossProcessEvent() may post aEvent to target remote processes. + * When it succeeded to post the event to at least one remote process, + * returns true. Otherwise, including the case not tried to dispatch to + * post the event, returns false. + * If you need to check if the event is posted to at least one remote + * process, you can use aEvent->HasBeenPostedToRemoteProcess(). + */ + bool HandleCrossProcessEvent(WidgetEvent* aEvent, nsEventStatus* aStatus); + + void ReleaseCurrentIMEContentObserver(); + + MOZ_CAN_RUN_SCRIPT void HandleQueryContentEvent( + WidgetQueryContentEvent* aEvent); + + private: + // Removes a node from the :hover / :active chain if needed, notifying if the + // node is not a NAC subtree. + // + // Only meant to be called from ContentRemoved and + // NativeAnonymousContentRemoved. + void RemoveNodeFromChainIfNeeded(ElementState aState, + nsIContent* aContentRemoved, bool aNotify); + + bool IsEventOutsideDragThreshold(WidgetInputEvent* aEvent) const; + + static inline void DoStateChange(dom::Element* aElement, ElementState aState, + bool aAddState); + static inline void DoStateChange(nsIContent* aContent, ElementState aState, + bool aAddState); + static void UpdateAncestorState(nsIContent* aStartNode, + nsIContent* aStopBefore, ElementState aState, + bool aAddState); + static void ResetLastOverForContent( + const uint32_t& aIdx, const RefPtr<OverOutElementsWrapper>& aChunk, + nsIContent* aClosure); + + /** + * Update the attribute mLastRefPoint of the mouse event. It should be + * the center of the window while the pointer is locked. + * the same value as mRefPoint while there is no known last ref point. + * the same value as the last known mRefPoint. + */ + static void UpdateLastRefPointOfMouseEvent(WidgetMouseEvent* aMouseEvent); + + static void ResetPointerToWindowCenterWhilePointerLocked( + WidgetMouseEvent* aMouseEvent); + + // Update the last known ref point to the current event's mRefPoint. + static void UpdateLastPointerPosition(WidgetMouseEvent* aMouseEvent); + + /** + * Notify target when user has been interaction with some speicific user + * gestures which are eKeyUp, eMouseUp, eTouchEnd. + */ + void NotifyTargetUserActivation(WidgetEvent* aEvent, + nsIContent* aTargetContent); + + already_AddRefed<EventStateManager> ESMFromContentOrThis( + nsIContent* aContent); + + StyleCursorKind mLockCursor; + bool mLastFrameConsumedSetCursor; + + // Last mouse event mRefPoint (the offset from the widget's origin in + // device pixels) when mouse was locked, used to restore mouse position + // after unlocking. + static LayoutDeviceIntPoint sPreLockPoint; + + // Stores the mRefPoint of the last synthetic mouse move we dispatched + // to re-center the mouse when we were pointer locked. If this is (-1,-1) it + // means we've not recently dispatched a centering event. We use this to + // detect when we receive the synth event, so we can cancel and not send it + // to content. + static LayoutDeviceIntPoint sSynthCenteringPoint; + + WeakFrame mCurrentTarget; + nsCOMPtr<nsIContent> mCurrentTargetContent; + static AutoWeakFrame sLastDragOverFrame; + + // Stores the mRefPoint (the offset from the widget's origin in device + // pixels) of the last mouse event. + static LayoutDeviceIntPoint sLastRefPoint; + + // member variables for the d&d gesture state machine + LayoutDeviceIntPoint mGestureDownPoint; // screen coordinates + // The content to use as target if we start a d&d (what we drag). + RefPtr<nsIContent> mGestureDownContent; + // The content of the frame where the mouse-down event occurred. It's the same + // as the target in most cases but not always - for example when dragging + // an <area> of an image map this is the image. (bug 289667) + nsCOMPtr<nsIContent> mGestureDownFrameOwner; + // Data associated with a drag started in a content process. + RefPtr<dom::RemoteDragStartData> mGestureDownDragStartData; + // State of keys when the original gesture-down happened + Modifiers mGestureModifiers; + uint16_t mGestureDownButtons; + + nsCOMPtr<nsIContent> mLastLeftMouseDownContent; + nsCOMPtr<nsIContent> mLastMiddleMouseDownContent; + nsCOMPtr<nsIContent> mLastRightMouseDownContent; + + nsCOMPtr<nsIContent> mActiveContent; + nsCOMPtr<nsIContent> mHoverContent; + static nsCOMPtr<nsIContent> sDragOverContent; + nsCOMPtr<nsIContent> mURLTargetContent; + + nsPresContext* mPresContext; // Not refcnted + RefPtr<dom::Document> mDocument; // Doesn't necessarily need to be owner + + RefPtr<IMEContentObserver> mIMEContentObserver; + + uint32_t mLClickCount; + uint32_t mMClickCount; + uint32_t mRClickCount; + + bool mShouldAlwaysUseLineDeltas : 1; + bool mShouldAlwaysUseLineDeltasInitialized : 1; + + bool mGestureDownInTextControl : 1; + + bool mInTouchDrag; + + bool m_haveShutdown; + + RefPtr<OverOutElementsWrapper> mMouseEnterLeaveHelper; + nsRefPtrHashtable<nsUint32HashKey, OverOutElementsWrapper> + mPointersEnterLeaveHelper; + + // Array for accesskey support + nsCOMArray<dom::Element> mAccessKeys; + + bool ShouldAlwaysUseLineDeltas(); + + public: + static nsresult UpdateUserActivityTimer(void); + + static bool sNormalLMouseEventInProcess; + static int16_t sCurrentMouseBtn; + + static EventStateManager* sActiveESM; + + static void ClearGlobalActiveContent(EventStateManager* aClearer); + + // Functions used for click hold context menus + nsCOMPtr<nsITimer> mClickHoldTimer; + void CreateClickHoldTimer(nsPresContext* aPresContext, nsIFrame* aDownFrame, + WidgetGUIEvent* aMouseDownEvent); + void KillClickHoldTimer(); + MOZ_CAN_RUN_SCRIPT_BOUNDARY void FireContextClick(); + + MOZ_CAN_RUN_SCRIPT static void SetPointerLock(nsIWidget* aWidget, + nsIContent* aElement); + static void sClickHoldCallback(nsITimer* aTimer, void* aESM); +}; + +} // namespace mozilla + +// Click and double-click events need to be handled even for content that +// has no frame. This is required for Web compatibility. +#define NS_EVENT_NEEDS_FRAME(event) \ + ((event)->mMessage != eMouseClick && \ + (event)->mMessage != eMouseDoubleClick && \ + (event)->mMessage != eMouseAuxClick) + +#endif // mozilla_EventStateManager_h_ diff --git a/dom/events/EventTarget.cpp b/dom/events/EventTarget.cpp new file mode 100644 index 0000000000..c0c4809a5c --- /dev/null +++ b/dom/events/EventTarget.cpp @@ -0,0 +1,236 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#include "mozilla/EventListenerManager.h" +#include "mozilla/dom/EventTarget.h" +#include "mozilla/dom/EventTargetBinding.h" +#include "mozilla/dom/ConstructibleEventTarget.h" +#include "mozilla/dom/Nullable.h" +#include "mozilla/dom/WindowProxyHolder.h" +#include "nsGlobalWindowInner.h" +#include "nsGlobalWindowOuter.h" +#include "nsIGlobalObject.h" +#include "nsPIDOMWindow.h" +#include "nsThreadUtils.h" + +namespace mozilla::dom { + +/* static */ +already_AddRefed<EventTarget> EventTarget::Constructor( + const GlobalObject& aGlobal, ErrorResult& aRv) { + nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports()); + if (!global) { + aRv.Throw(NS_ERROR_UNEXPECTED); + return nullptr; + } + RefPtr<EventTarget> target = new ConstructibleEventTarget(global); + return target.forget(); +} + +bool EventTarget::ComputeWantsUntrusted( + const Nullable<bool>& aWantsUntrusted, + const AddEventListenerOptionsOrBoolean* aOptions, ErrorResult& aRv) { + if (aOptions && aOptions->IsAddEventListenerOptions()) { + const auto& options = aOptions->GetAsAddEventListenerOptions(); + if (options.mWantUntrusted.WasPassed()) { + return options.mWantUntrusted.Value(); + } + } + + if (!aWantsUntrusted.IsNull()) { + return aWantsUntrusted.Value(); + } + + bool defaultWantsUntrusted = ComputeDefaultWantsUntrusted(aRv); + if (aRv.Failed()) { + return false; + } + + return defaultWantsUntrusted; +} + +void EventTarget::AddEventListener( + const nsAString& aType, EventListener* aCallback, + const AddEventListenerOptionsOrBoolean& aOptions, + const Nullable<bool>& aWantsUntrusted, ErrorResult& aRv) { + bool wantsUntrusted = ComputeWantsUntrusted(aWantsUntrusted, &aOptions, aRv); + if (aRv.Failed()) { + return; + } + + EventListenerManager* elm = GetOrCreateListenerManager(); + if (!elm) { + aRv.Throw(NS_ERROR_UNEXPECTED); + return; + } + + elm->AddEventListener(aType, aCallback, aOptions, wantsUntrusted); +} + +nsresult EventTarget::AddEventListener(const nsAString& aType, + nsIDOMEventListener* aListener, + bool aUseCapture, + const Nullable<bool>& aWantsUntrusted) { + ErrorResult rv; + bool wantsUntrusted = ComputeWantsUntrusted(aWantsUntrusted, nullptr, rv); + if (rv.Failed()) { + return rv.StealNSResult(); + } + + EventListenerManager* elm = GetOrCreateListenerManager(); + NS_ENSURE_STATE(elm); + elm->AddEventListener(aType, aListener, aUseCapture, wantsUntrusted); + return NS_OK; +} + +void EventTarget::RemoveEventListener( + const nsAString& aType, EventListener* aListener, + const EventListenerOptionsOrBoolean& aOptions, ErrorResult& aRv) { + EventListenerManager* elm = GetExistingListenerManager(); + if (elm) { + elm->RemoveEventListener(aType, aListener, aOptions); + } +} + +void EventTarget::RemoveEventListener(const nsAString& aType, + nsIDOMEventListener* aListener, + bool aUseCapture) { + EventListenerManager* elm = GetExistingListenerManager(); + if (elm) { + elm->RemoveEventListener(aType, aListener, aUseCapture); + } +} + +nsresult EventTarget::AddSystemEventListener( + const nsAString& aType, nsIDOMEventListener* aListener, bool aUseCapture, + const Nullable<bool>& aWantsUntrusted) { + ErrorResult rv; + bool wantsUntrusted = ComputeWantsUntrusted(aWantsUntrusted, nullptr, rv); + if (rv.Failed()) { + return rv.StealNSResult(); + } + + EventListenerManager* elm = GetOrCreateListenerManager(); + NS_ENSURE_STATE(elm); + + EventListenerFlags flags; + flags.mInSystemGroup = true; + flags.mCapture = aUseCapture; + flags.mAllowUntrustedEvents = wantsUntrusted; + elm->AddEventListenerByType(aListener, aType, flags); + return NS_OK; +} + +void EventTarget::RemoveSystemEventListener(const nsAString& aType, + nsIDOMEventListener* aListener, + bool aUseCapture) { + EventListenerManager* elm = GetExistingListenerManager(); + if (elm) { + EventListenerFlags flags; + flags.mInSystemGroup = true; + flags.mCapture = aUseCapture; + elm->RemoveEventListenerByType(aListener, aType, flags); + } +} + +EventHandlerNonNull* EventTarget::GetEventHandler(nsAtom* aType) { + EventListenerManager* elm = GetExistingListenerManager(); + return elm ? elm->GetEventHandler(aType) : nullptr; +} + +void EventTarget::SetEventHandler(const nsAString& aType, + EventHandlerNonNull* aHandler, + ErrorResult& aRv) { + if (!StringBeginsWith(aType, u"on"_ns)) { + aRv.Throw(NS_ERROR_INVALID_ARG); + return; + } + RefPtr<nsAtom> type = NS_Atomize(aType); + SetEventHandler(type, aHandler); +} + +void EventTarget::SetEventHandler(nsAtom* aType, + EventHandlerNonNull* aHandler) { + GetOrCreateListenerManager()->SetEventHandler(aType, aHandler); +} + +bool EventTarget::HasNonSystemGroupListenersForUntrustedKeyEvents() const { + EventListenerManager* elm = GetExistingListenerManager(); + return elm && elm->HasNonSystemGroupListenersForUntrustedKeyEvents(); +} + +bool EventTarget::HasNonPassiveNonSystemGroupListenersForUntrustedKeyEvents() + const { + EventListenerManager* elm = GetExistingListenerManager(); + return elm && + elm->HasNonPassiveNonSystemGroupListenersForUntrustedKeyEvents(); +} + +bool EventTarget::IsApzAware() const { + EventListenerManager* elm = GetExistingListenerManager(); + return elm && elm->HasApzAwareListeners(); +} + +void EventTarget::DispatchEvent(Event& aEvent) { + // The caller type doesn't really matter if we don't care about the + // return value, but let's be safe and pass NonSystem. + Unused << DispatchEvent(aEvent, CallerType::NonSystem, IgnoreErrors()); +} + +void EventTarget::DispatchEvent(Event& aEvent, ErrorResult& aRv) { + // The caller type doesn't really matter if we don't care about the + // return value, but let's be safe and pass NonSystem. + Unused << DispatchEvent(aEvent, CallerType::NonSystem, IgnoreErrors()); +} + +Nullable<WindowProxyHolder> EventTarget::GetOwnerGlobalForBindings() { + nsPIDOMWindowOuter* win = GetOwnerGlobalForBindingsInternal(); + if (!win) { + return nullptr; + } + + return WindowProxyHolder(win->GetBrowsingContext()); +} + +nsPIDOMWindowInner* EventTarget::GetAsWindowInner() { + return IsInnerWindow() ? static_cast<nsGlobalWindowInner*>(this) : nullptr; +} + +const nsPIDOMWindowInner* EventTarget::GetAsWindowInner() const { + return IsInnerWindow() ? static_cast<const nsGlobalWindowInner*>(this) + : nullptr; +} + +nsPIDOMWindowOuter* EventTarget::GetAsWindowOuter() { + return IsOuterWindow() ? static_cast<nsGlobalWindowOuter*>(this) : nullptr; +} + +const nsPIDOMWindowOuter* EventTarget::GetAsWindowOuter() const { + return IsOuterWindow() ? static_cast<const nsGlobalWindowOuter*>(this) + : nullptr; +} + +nsPIDOMWindowInner* EventTarget::AsWindowInner() { + MOZ_DIAGNOSTIC_ASSERT(IsInnerWindow()); + return static_cast<nsGlobalWindowInner*>(this); +} + +const nsPIDOMWindowInner* EventTarget::AsWindowInner() const { + MOZ_DIAGNOSTIC_ASSERT(IsInnerWindow()); + return static_cast<const nsGlobalWindowInner*>(this); +} + +nsPIDOMWindowOuter* EventTarget::AsWindowOuter() { + MOZ_DIAGNOSTIC_ASSERT(IsOuterWindow()); + return static_cast<nsGlobalWindowOuter*>(this); +} + +const nsPIDOMWindowOuter* EventTarget::AsWindowOuter() const { + MOZ_DIAGNOSTIC_ASSERT(IsOuterWindow()); + return static_cast<const nsGlobalWindowOuter*>(this); +} + +} // namespace mozilla::dom diff --git a/dom/events/EventTarget.h b/dom/events/EventTarget.h new file mode 100644 index 0000000000..ae1f94f584 --- /dev/null +++ b/dom/events/EventTarget.h @@ -0,0 +1,412 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#ifndef mozilla_dom_EventTarget_h_ +#define mozilla_dom_EventTarget_h_ + +#include "mozilla/dom/Nullable.h" +#include "nsISupports.h" +#include "nsWrapperCache.h" +#include "nsAtom.h" + +class nsIDOMEventListener; +class nsIGlobalObject; +class nsINode; +class nsPIDOMWindowInner; +class nsPIDOMWindowOuter; +class nsPIWindowRoot; + +namespace mozilla { + +class AsyncEventDispatcher; +class ErrorResult; +class EventChainPostVisitor; +class EventChainPreVisitor; +class EventChainVisitor; +class EventListenerManager; + +namespace dom { + +class AddEventListenerOptionsOrBoolean; +class Event; +class EventListener; +class EventListenerOptionsOrBoolean; +class EventHandlerNonNull; +class GlobalObject; +class WindowProxyHolder; +enum class CallerType : uint32_t; +enum class EventCallbackDebuggerNotificationType : uint8_t; + +// IID for the dom::EventTarget interface +#define NS_EVENTTARGET_IID \ + { \ + 0xde651c36, 0x0053, 0x4c67, { \ + 0xb1, 0x3d, 0x67, 0xb9, 0x40, 0xfc, 0x82, 0xe4 \ + } \ + } + +class EventTarget : public nsISupports, public nsWrapperCache { + public: + NS_DECLARE_STATIC_IID_ACCESSOR(NS_EVENTTARGET_IID) + + // WebIDL API + static already_AddRefed<EventTarget> Constructor(const GlobalObject& aGlobal, + ErrorResult& aRv); + void AddEventListener(const nsAString& aType, EventListener* aCallback, + const AddEventListenerOptionsOrBoolean& aOptions, + const Nullable<bool>& aWantsUntrusted, + ErrorResult& aRv); + void RemoveEventListener(const nsAString& aType, EventListener* aCallback, + const EventListenerOptionsOrBoolean& aOptions, + ErrorResult& aRv); + + protected: + /** + * This method allows addition of event listeners represented by + * nsIDOMEventListener, with almost the same semantics as the + * standard AddEventListener. The one difference is that it just + * has a "use capture" boolean, not an EventListenerOptions. + */ + nsresult AddEventListener(const nsAString& aType, + nsIDOMEventListener* aListener, bool aUseCapture, + const Nullable<bool>& aWantsUntrusted); + + public: + /** + * Helper methods to make the nsIDOMEventListener version of + * AddEventListener simpler to call for consumers. + */ + nsresult AddEventListener(const nsAString& aType, + nsIDOMEventListener* aListener, bool aUseCapture) { + return AddEventListener(aType, aListener, aUseCapture, Nullable<bool>()); + } + nsresult AddEventListener(const nsAString& aType, + nsIDOMEventListener* aListener, bool aUseCapture, + bool aWantsUntrusted) { + return AddEventListener(aType, aListener, aUseCapture, + Nullable<bool>(aWantsUntrusted)); + } + + /** + * This method allows the removal of event listeners represented by + * nsIDOMEventListener from the event target, with the same semantics as the + * standard RemoveEventListener. + */ + void RemoveEventListener(const nsAString& aType, + nsIDOMEventListener* aListener, bool aUseCapture); + /** + * RemoveSystemEventListener() should be used if you have used + * AddSystemEventListener(). + */ + void RemoveSystemEventListener(const nsAString& aType, + nsIDOMEventListener* aListener, + bool aUseCapture); + + /** + * Add a system event listener with the default wantsUntrusted value. + */ + nsresult AddSystemEventListener(const nsAString& aType, + nsIDOMEventListener* aListener, + bool aUseCapture) { + return AddSystemEventListener(aType, aListener, aUseCapture, + Nullable<bool>()); + } + + /** + * Add a system event listener with the given wantsUntrusted value. + */ + nsresult AddSystemEventListener(const nsAString& aType, + nsIDOMEventListener* aListener, + bool aUseCapture, bool aWantsUntrusted) { + return AddSystemEventListener(aType, aListener, aUseCapture, + Nullable<bool>(aWantsUntrusted)); + } + + virtual bool IsNode() const { return false; } + inline nsINode* GetAsNode(); + inline const nsINode* GetAsNode() const; + inline nsINode* AsNode(); + inline const nsINode* AsNode() const; + + virtual bool IsInnerWindow() const { return false; } + virtual bool IsOuterWindow() const { return false; } + virtual bool IsRootWindow() const { return false; } + nsPIDOMWindowInner* GetAsWindowInner(); + const nsPIDOMWindowInner* GetAsWindowInner() const; + nsPIDOMWindowOuter* GetAsWindowOuter(); + const nsPIDOMWindowOuter* GetAsWindowOuter() const; + inline nsPIWindowRoot* GetAsWindowRoot(); + inline const nsPIWindowRoot* GetAsWindowRoot() const; + nsPIDOMWindowInner* AsWindowInner(); + const nsPIDOMWindowInner* AsWindowInner() const; + nsPIDOMWindowOuter* AsWindowOuter(); + const nsPIDOMWindowOuter* AsWindowOuter() const; + inline nsPIWindowRoot* AsWindowRoot(); + inline const nsPIWindowRoot* AsWindowRoot() const; + + /** + * Returns the EventTarget object which should be used as the target + * of DOMEvents. + * Usually |this| is returned, but for example Window (inner windw) returns + * the WindowProxy (outer window). + */ + virtual EventTarget* GetTargetForDOMEvent() { return this; }; + + /** + * Returns the EventTarget object which should be used as the target + * of the event and when constructing event target chain. + * Usually |this| is returned, but for example WindowProxy (outer window) + * returns the Window (inner window). + */ + virtual EventTarget* GetTargetForEventTargetChain() { return this; } + + /** + * The most general DispatchEvent method. This is the one the bindings call. + */ + // TODO: Convert this to MOZ_CAN_RUN_SCRIPT (bug 1415230) + MOZ_CAN_RUN_SCRIPT_BOUNDARY virtual bool DispatchEvent(Event& aEvent, + CallerType aCallerType, + ErrorResult& aRv) = 0; + + /** + * A version of DispatchEvent you can use if you really don't care whether it + * succeeds or not and whether default is prevented or not. + */ + // TODO: Convert this to MOZ_CAN_RUN_SCRIPT (bug 1415230) + MOZ_CAN_RUN_SCRIPT_BOUNDARY void DispatchEvent(Event& aEvent); + + /** + * A version of DispatchEvent you can use if you really don't care whether + * default is prevented or not. + */ + // TODO: Convert this to MOZ_CAN_RUN_SCRIPT (bug 1415230) + MOZ_CAN_RUN_SCRIPT_BOUNDARY void DispatchEvent(Event& aEvent, + ErrorResult& aRv); + + nsIGlobalObject* GetParentObject() const { return GetOwnerGlobal(); } + + // Note, this takes the type in onfoo form! + EventHandlerNonNull* GetEventHandler(const nsAString& aType) { + RefPtr<nsAtom> type = NS_Atomize(aType); + return GetEventHandler(type); + } + + // Note, this takes the type in onfoo form! + void SetEventHandler(const nsAString& aType, EventHandlerNonNull* aHandler, + ErrorResult& rv); + + // For an event 'foo' aType will be 'onfoo'. + virtual void EventListenerAdded(nsAtom* aType) {} + + // For an event 'foo' aType will be 'onfoo'. + virtual void EventListenerRemoved(nsAtom* aType) {} + + // Returns an outer window that corresponds to the inner window this event + // target is associated with. Will return null if the inner window is not the + // current inner or if there is no window around at all. + Nullable<WindowProxyHolder> GetOwnerGlobalForBindings(); + virtual nsPIDOMWindowOuter* GetOwnerGlobalForBindingsInternal() = 0; + + // The global object this event target is associated with, if any. + // This may be an inner window or some other global object. This + // will never be an outer window. + virtual nsIGlobalObject* GetOwnerGlobal() const = 0; + + /** + * Get the event listener manager, creating it if it does not already exist. + */ + virtual EventListenerManager* GetOrCreateListenerManager() = 0; + + /** + * Get the event listener manager, returning null if it does not already + * exist. + */ + virtual EventListenerManager* GetExistingListenerManager() const = 0; + + virtual Maybe<EventCallbackDebuggerNotificationType> + GetDebuggerNotificationType() const { + return Nothing(); + } + + // Called from AsyncEventDispatcher to notify it is running. + virtual void AsyncEventRunning(AsyncEventDispatcher* aEvent) {} + + // Used by APZ to determine whether this event target has non-chrome event + // listeners for untrusted key events. + bool HasNonSystemGroupListenersForUntrustedKeyEvents() const; + + // Used by APZ to determine whether this event target has non-chrome and + // non-passive event listeners for untrusted key events. + bool HasNonPassiveNonSystemGroupListenersForUntrustedKeyEvents() const; + + virtual bool IsApzAware() const; + + /** + * Called before the capture phase of the event flow. + * This is used to create the event target chain and implementations + * should set the necessary members of EventChainPreVisitor. + * At least aVisitor.mCanHandle must be set, + * usually also aVisitor.mParentTarget if mCanHandle is true. + * mCanHandle says that this object can handle the aVisitor.mEvent event and + * the mParentTarget is the possible parent object for the event target chain. + * @see EventDispatcher.h for more documentation about aVisitor. + * + * @param aVisitor the visitor object which is used to create the + * event target chain for event dispatching. + * + * @note Only EventDispatcher should call this method. + */ + virtual void GetEventTargetParent(EventChainPreVisitor& aVisitor) = 0; + + /** + * Called before the capture phase of the event flow and after event target + * chain creation. This is used to handle things that must be executed before + * dispatching the event to DOM. + */ + virtual nsresult PreHandleEvent(EventChainVisitor& aVisitor) { return NS_OK; } + + /** + * If EventChainPreVisitor.mWantsWillHandleEvent is set true, + * called just before possible event handlers on this object will be called. + */ + virtual void WillHandleEvent(EventChainPostVisitor& aVisitor) {} + + /** + * Called after the bubble phase of the system event group. + * The default handling of the event should happen here. + * @param aVisitor the visitor object which is used during post handling. + * + * @see EventDispatcher.h for documentation about aVisitor. + * @note Only EventDispatcher should call this method. + */ + MOZ_CAN_RUN_SCRIPT + virtual nsresult PostHandleEvent(EventChainPostVisitor& aVisitor) = 0; + + protected: + EventHandlerNonNull* GetEventHandler(nsAtom* aType); + void SetEventHandler(nsAtom* aType, EventHandlerNonNull* aHandler); + + /** + * Hook for AddEventListener that allows it to compute the right + * wantsUntrusted boolean when one is not provided. If this returns failure, + * the listener will not be added. + * + * This hook will NOT be called unless aWantsUntrusted is null in + * AddEventListener. If you need to take action when event listeners are + * added, use EventListenerAdded. Especially because not all event listener + * additions go through AddEventListener! + */ + virtual bool ComputeDefaultWantsUntrusted(ErrorResult& aRv) = 0; + + /** + * A method to compute the right wantsUntrusted value for AddEventListener. + * This will call the above hook as needed. + * + * If aOptions is non-null, and it contains a value for mWantUntrusted, that + * value takes precedence over aWantsUntrusted. + */ + bool ComputeWantsUntrusted(const Nullable<bool>& aWantsUntrusted, + const AddEventListenerOptionsOrBoolean* aOptions, + ErrorResult& aRv); + + /** + * addSystemEventListener() adds an event listener of aType to the system + * group. Typically, core code should use the system group for listening to + * content (i.e., non-chrome) element's events. If core code uses + * EventTarget::AddEventListener for a content node, it means + * that the listener cannot listen to the event when web content calls + * stopPropagation() of the event. + * + * @param aType An event name you're going to handle. + * @param aListener An event listener. + * @param aUseCapture true if you want to listen the event in capturing + * phase. Otherwise, false. + * @param aWantsUntrusted true if you want to handle untrusted events. + * false if not. + * Null if you want the default behavior. + */ + nsresult AddSystemEventListener(const nsAString& aType, + nsIDOMEventListener* aListener, + bool aUseCapture, + const Nullable<bool>& aWantsUntrusted); +}; + +NS_DEFINE_STATIC_IID_ACCESSOR(EventTarget, NS_EVENTTARGET_IID) + +#define NS_IMPL_FROMEVENTTARGET_GENERIC(_class, _check, _const) \ + template <typename T> \ + static auto FromEventTarget(_const T& aEventTarget) \ + ->decltype(static_cast<_const _class*>(&aEventTarget)) { \ + return aEventTarget._check ? static_cast<_const _class*>(&aEventTarget) \ + : nullptr; \ + } \ + template <typename T> \ + static _const _class* FromEventTarget(_const T* aEventTarget) { \ + MOZ_DIAGNOSTIC_ASSERT(aEventTarget); \ + return FromEventTarget(*aEventTarget); \ + } \ + template <typename T> \ + static _const _class* FromEventTargetOrNull(_const T* aEventTarget) { \ + return aEventTarget ? FromEventTarget(*aEventTarget) : nullptr; \ + } + +#define NS_IMPL_FROMEVENTTARGET_HELPER(_class, _check) \ + NS_IMPL_FROMEVENTTARGET_GENERIC(_class, _check, ) \ + NS_IMPL_FROMEVENTTARGET_GENERIC(_class, _check, const) \ + template <typename T> \ + static _class* FromEventTarget(T&& aEventTarget) { \ + MOZ_DIAGNOSTIC_ASSERT(!!aEventTarget); \ + /* We need the double-cast in case aEventTarget is a smartptr. Those */ \ + /* can cast to superclasses of the type they're templated on, */ \ + /* but not directly to subclasses. */ \ + return aEventTarget->_check \ + ? static_cast<_class*>(static_cast<EventTarget*>(aEventTarget)) \ + : nullptr; \ + } \ + template <typename T> \ + static _class* FromEventTargetOrNull(T&& aEventTarget) { \ + return aEventTarget ? FromEventTarget(aEventTarget) : nullptr; \ + } + +// Unfortunately, nsPIDOMWindowInner and nsPIDOMWindowOuter do not inherit +// EventTarget directly, but they are public interfaces which should have +// these helper methods. Therefore, we cannot cast from EventTarget to +// the interfaces in their header file. That's the reason why we cannot use +// the zero cost casts nor decltype for the template methods which take a +// reference. +#define NS_IMPL_FROMEVENTTARGET_GENERIC_WITH_GETTER(_class, _getter, _const) \ + static _const _class* FromEventTarget( \ + _const mozilla::dom::EventTarget& aEventTarget) { \ + return aEventTarget._getter; \ + } \ + template <typename T> \ + static _const _class* FromEventTarget(_const T* aEventTarget) { \ + return aEventTarget->_getter; \ + } \ + template <typename T> \ + static _const _class* FromEventTargetOrNull(_const T* aEventTarget) { \ + return aEventTarget ? aEventTarget->_getter : nullptr; \ + } + +#define NS_IMPL_FROMEVENTTARGET_HELPER_WITH_GETTER_INNER(_class, _getter) \ + template <typename T> \ + static _class* FromEventTarget(T&& aEventTarget) { \ + return aEventTarget->_getter; \ + } \ + template <typename T> \ + static _class* FromEventTargetOrNull(T&& aEventTarget) { \ + return aEventTarget ? aEventTarget->_getter : nullptr; \ + } + +#define NS_IMPL_FROMEVENTTARGET_HELPER_WITH_GETTER(_class, _getter) \ + NS_IMPL_FROMEVENTTARGET_GENERIC_WITH_GETTER(_class, _getter, ) \ + NS_IMPL_FROMEVENTTARGET_GENERIC_WITH_GETTER(_class, _getter, const) \ + NS_IMPL_FROMEVENTTARGET_HELPER_WITH_GETTER_INNER(_class, _getter) + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_EventTarget_h_ diff --git a/dom/events/FocusEvent.cpp b/dom/events/FocusEvent.cpp new file mode 100644 index 0000000000..e00ce5ae32 --- /dev/null +++ b/dom/events/FocusEvent.cpp @@ -0,0 +1,61 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#include "mozilla/dom/FocusEvent.h" +#include "mozilla/ContentEvents.h" +#include "prtime.h" + +namespace mozilla::dom { + +FocusEvent::FocusEvent(EventTarget* aOwner, nsPresContext* aPresContext, + InternalFocusEvent* aEvent) + : UIEvent(aOwner, aPresContext, + aEvent ? aEvent : new InternalFocusEvent(false, eFocus)) { + if (aEvent) { + mEventIsInternal = false; + } else { + mEventIsInternal = true; + } +} + +already_AddRefed<EventTarget> FocusEvent::GetRelatedTarget() { + return EnsureWebAccessibleRelatedTarget( + mEvent->AsFocusEvent()->mRelatedTarget); +} + +void FocusEvent::InitFocusEvent(const nsAString& aType, bool aCanBubble, + bool aCancelable, nsGlobalWindowInner* aView, + int32_t aDetail, EventTarget* aRelatedTarget) { + MOZ_ASSERT(!mEvent->mFlags.mIsBeingDispatched); + + UIEvent::InitUIEvent(aType, aCanBubble, aCancelable, aView, aDetail); + mEvent->AsFocusEvent()->mRelatedTarget = aRelatedTarget; +} + +already_AddRefed<FocusEvent> FocusEvent::Constructor( + const GlobalObject& aGlobal, const nsAString& aType, + const FocusEventInit& aParam) { + nsCOMPtr<EventTarget> t = do_QueryInterface(aGlobal.GetAsSupports()); + RefPtr<FocusEvent> e = new FocusEvent(t, nullptr, nullptr); + bool trusted = e->Init(t); + e->InitFocusEvent(aType, aParam.mBubbles, aParam.mCancelable, aParam.mView, + aParam.mDetail, aParam.mRelatedTarget); + e->SetTrusted(trusted); + e->SetComposed(aParam.mComposed); + return e.forget(); +} + +} // namespace mozilla::dom + +using namespace mozilla; +using namespace mozilla::dom; + +already_AddRefed<FocusEvent> NS_NewDOMFocusEvent(EventTarget* aOwner, + nsPresContext* aPresContext, + InternalFocusEvent* aEvent) { + RefPtr<FocusEvent> it = new FocusEvent(aOwner, aPresContext, aEvent); + return it.forget(); +} diff --git a/dom/events/FocusEvent.h b/dom/events/FocusEvent.h new file mode 100644 index 0000000000..78c94028f1 --- /dev/null +++ b/dom/events/FocusEvent.h @@ -0,0 +1,47 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ +#ifndef mozilla_dom_FocusEvent_h_ +#define mozilla_dom_FocusEvent_h_ + +#include "mozilla/dom/FocusEventBinding.h" +#include "mozilla/dom/UIEvent.h" +#include "mozilla/EventForwards.h" + +namespace mozilla::dom { + +class FocusEvent : public UIEvent { + public: + NS_INLINE_DECL_REFCOUNTING_INHERITED(FocusEvent, UIEvent) + + virtual JSObject* WrapObjectInternal( + JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override { + return FocusEvent_Binding::Wrap(aCx, this, aGivenProto); + } + + FocusEvent(EventTarget* aOwner, nsPresContext* aPresContext, + InternalFocusEvent* aEvent); + + already_AddRefed<EventTarget> GetRelatedTarget(); + + static already_AddRefed<FocusEvent> Constructor(const GlobalObject& aGlobal, + const nsAString& aType, + const FocusEventInit& aParam); + + protected: + ~FocusEvent() = default; + + void InitFocusEvent(const nsAString& aType, bool aCanBubble, bool aCancelable, + nsGlobalWindowInner* aView, int32_t aDetail, + EventTarget* aRelatedTarget); +}; + +} // namespace mozilla::dom + +already_AddRefed<mozilla::dom::FocusEvent> NS_NewDOMFocusEvent( + mozilla::dom::EventTarget* aOwner, nsPresContext* aPresContext, + mozilla::InternalFocusEvent* aEvent); + +#endif // mozilla_dom_FocusEvent_h_ diff --git a/dom/events/GlobalKeyListener.cpp b/dom/events/GlobalKeyListener.cpp new file mode 100644 index 0000000000..5711e85d94 --- /dev/null +++ b/dom/events/GlobalKeyListener.cpp @@ -0,0 +1,751 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#include "GlobalKeyListener.h" +#include "EventTarget.h" + +#include <utility> + +#include "mozilla/EventListenerManager.h" +#include "mozilla/EventStateManager.h" +#include "mozilla/HTMLEditor.h" +#include "mozilla/KeyEventHandler.h" +#include "mozilla/NativeKeyBindingsType.h" +#include "mozilla/Preferences.h" +#include "mozilla/ShortcutKeys.h" +#include "mozilla/StaticPtr.h" +#include "mozilla/TextEvents.h" +#include "mozilla/dom/Element.h" +#include "mozilla/dom/Event.h" +#include "mozilla/dom/EventBinding.h" +#include "mozilla/dom/KeyboardEvent.h" +#include "mozilla/widget/IMEData.h" +#include "nsAtom.h" +#include "nsCOMPtr.h" +#include "nsContentUtils.h" +#include "nsFocusManager.h" +#include "nsGkAtoms.h" +#include "nsIContent.h" +#include "nsIContentInlines.h" +#include "nsIDocShell.h" +#include "nsIWidget.h" +#include "nsNetUtil.h" +#include "nsPIDOMWindow.h" + +namespace mozilla { + +using namespace mozilla::layers; + +GlobalKeyListener::GlobalKeyListener(dom::EventTarget* aTarget) + : mTarget(aTarget), mHandler(nullptr) {} + +NS_IMPL_ISUPPORTS(GlobalKeyListener, nsIDOMEventListener) + +static void BuildHandlerChain(nsIContent* aContent, KeyEventHandler** aResult) { + *aResult = nullptr; + + // Since we chain each handler onto the next handler, + // we'll enumerate them here in reverse so that when we + // walk the chain they'll come out in the original order + for (nsIContent* key = aContent->GetLastChild(); key; + key = key->GetPreviousSibling()) { + if (!key->NodeInfo()->Equals(nsGkAtoms::key, kNameSpaceID_XUL)) { + continue; + } + + dom::Element* keyElement = key->AsElement(); + // Check whether the key element has empty value at key/char attribute. + // Such element is used by localizers for alternative shortcut key + // definition on the locale. See bug 426501. + nsAutoString valKey, valCharCode, valKeyCode; + // Hopefully at least one of the attributes is set: + keyElement->GetAttr(kNameSpaceID_None, nsGkAtoms::key, valKey) || + keyElement->GetAttr(kNameSpaceID_None, nsGkAtoms::charcode, + valCharCode) || + keyElement->GetAttr(kNameSpaceID_None, nsGkAtoms::keycode, valKeyCode); + // If not, ignore this key element. + if (valKey.IsEmpty() && valCharCode.IsEmpty() && valKeyCode.IsEmpty()) { + continue; + } + + // reserved="pref" is the default for <key> elements. + ReservedKey reserved = ReservedKey_Unset; + if (keyElement->AttrValueIs(kNameSpaceID_None, nsGkAtoms::reserved, + nsGkAtoms::_true, eCaseMatters)) { + reserved = ReservedKey_True; + } else if (keyElement->AttrValueIs(kNameSpaceID_None, nsGkAtoms::reserved, + nsGkAtoms::_false, eCaseMatters)) { + reserved = ReservedKey_False; + } + + KeyEventHandler* handler = new KeyEventHandler(keyElement, reserved); + + handler->SetNextHandler(*aResult); + *aResult = handler; + } +} + +void GlobalKeyListener::WalkHandlers(dom::KeyboardEvent* aKeyEvent) { + if (aKeyEvent->DefaultPrevented()) { + return; + } + + // Don't process the event if it was not dispatched from a trusted source + if (!aKeyEvent->IsTrusted()) { + return; + } + + EnsureHandlers(); + + // skip keysets that are disabled + if (IsDisabled()) { + return; + } + + WalkHandlersInternal(aKeyEvent, true); +} + +void GlobalKeyListener::InstallKeyboardEventListenersTo( + EventListenerManager* aEventListenerManager) { + // For marking each keyboard event as if it's reserved by chrome, + // GlobalKeyListeners need to listen each keyboard events before + // web contents. + aEventListenerManager->AddEventListenerByType(this, u"keydown"_ns, + TrustedEventsAtCapture()); + aEventListenerManager->AddEventListenerByType(this, u"keyup"_ns, + TrustedEventsAtCapture()); + aEventListenerManager->AddEventListenerByType(this, u"keypress"_ns, + TrustedEventsAtCapture()); + + // For reducing the IPC cost, preventing to dispatch reserved keyboard + // events into the content process. + aEventListenerManager->AddEventListenerByType( + this, u"keydown"_ns, TrustedEventsAtSystemGroupCapture()); + aEventListenerManager->AddEventListenerByType( + this, u"keyup"_ns, TrustedEventsAtSystemGroupCapture()); + aEventListenerManager->AddEventListenerByType( + this, u"keypress"_ns, TrustedEventsAtSystemGroupCapture()); + + // Handle keyboard events in bubbling phase of the system event group. + aEventListenerManager->AddEventListenerByType( + this, u"keydown"_ns, TrustedEventsAtSystemGroupBubble()); + aEventListenerManager->AddEventListenerByType( + this, u"keyup"_ns, TrustedEventsAtSystemGroupBubble()); + aEventListenerManager->AddEventListenerByType( + this, u"keypress"_ns, TrustedEventsAtSystemGroupBubble()); + // mozaccesskeynotfound event is fired when modifiers of keypress event + // matches with modifier of content access key but it's not consumed by + // remote content. + aEventListenerManager->AddEventListenerByType( + this, u"mozaccesskeynotfound"_ns, TrustedEventsAtSystemGroupBubble()); +} + +void GlobalKeyListener::RemoveKeyboardEventListenersFrom( + EventListenerManager* aEventListenerManager) { + aEventListenerManager->RemoveEventListenerByType(this, u"keydown"_ns, + TrustedEventsAtCapture()); + aEventListenerManager->RemoveEventListenerByType(this, u"keyup"_ns, + TrustedEventsAtCapture()); + aEventListenerManager->RemoveEventListenerByType(this, u"keypress"_ns, + TrustedEventsAtCapture()); + + aEventListenerManager->RemoveEventListenerByType( + this, u"keydown"_ns, TrustedEventsAtSystemGroupCapture()); + aEventListenerManager->RemoveEventListenerByType( + this, u"keyup"_ns, TrustedEventsAtSystemGroupCapture()); + aEventListenerManager->RemoveEventListenerByType( + this, u"keypress"_ns, TrustedEventsAtSystemGroupCapture()); + + aEventListenerManager->RemoveEventListenerByType( + this, u"keydown"_ns, TrustedEventsAtSystemGroupBubble()); + aEventListenerManager->RemoveEventListenerByType( + this, u"keyup"_ns, TrustedEventsAtSystemGroupBubble()); + aEventListenerManager->RemoveEventListenerByType( + this, u"keypress"_ns, TrustedEventsAtSystemGroupBubble()); + aEventListenerManager->RemoveEventListenerByType( + this, u"mozaccesskeynotfound"_ns, TrustedEventsAtSystemGroupBubble()); +} + +NS_IMETHODIMP +GlobalKeyListener::HandleEvent(dom::Event* aEvent) { + RefPtr<dom::KeyboardEvent> keyEvent = aEvent->AsKeyboardEvent(); + NS_ENSURE_TRUE(keyEvent, NS_ERROR_INVALID_ARG); + + if (aEvent->EventPhase() == dom::Event_Binding::CAPTURING_PHASE) { + if (aEvent->WidgetEventPtr()->mFlags.mInSystemGroup) { + HandleEventOnCaptureInSystemEventGroup(keyEvent); + } else { + HandleEventOnCaptureInDefaultEventGroup(keyEvent); + } + return NS_OK; + } + + // If this event was handled by APZ then don't do the default action, and + // preventDefault to prevent any other listeners from handling the event. + if (aEvent->WidgetEventPtr()->mFlags.mHandledByAPZ) { + aEvent->PreventDefault(); + return NS_OK; + } + + WalkHandlers(keyEvent); + return NS_OK; +} + +void GlobalKeyListener::HandleEventOnCaptureInDefaultEventGroup( + dom::KeyboardEvent* aEvent) { + WidgetKeyboardEvent* widgetKeyboardEvent = + aEvent->WidgetEventPtr()->AsKeyboardEvent(); + + if (widgetKeyboardEvent->IsReservedByChrome()) { + return; + } + + bool isReserved = false; + if (HasHandlerForEvent(aEvent, &isReserved) && isReserved) { + widgetKeyboardEvent->MarkAsReservedByChrome(); + } +} + +void GlobalKeyListener::HandleEventOnCaptureInSystemEventGroup( + dom::KeyboardEvent* aEvent) { + WidgetKeyboardEvent* widgetEvent = + aEvent->WidgetEventPtr()->AsKeyboardEvent(); + + // If the event won't be sent to remote process, this listener needs to do + // nothing. Note that even if mOnlySystemGroupDispatchInContent is true, + // we need to send the event to remote process and check reply event + // before matching it with registered shortcut keys because event listeners + // in the system event group may want to handle the event before registered + // shortcut key handlers. + if (!widgetEvent->WillBeSentToRemoteProcess()) { + return; + } + + if (!HasHandlerForEvent(aEvent)) { + return; + } + + // If this event wasn't marked as IsCrossProcessForwardingStopped, + // yet, it means it wasn't processed by content. We'll not call any + // of the handlers at this moment, and will wait the reply event. + // So, stop immediate propagation in this event first, then, mark it as + // waiting reply from remote process. Finally, when this process receives + // a reply from the remote process, it should be dispatched into this + // DOM tree again. + widgetEvent->StopImmediatePropagation(); + widgetEvent->MarkAsWaitingReplyFromRemoteProcess(); +} + +// +// WalkHandlersInternal and WalkHandlersAndExecute +// +// Given a particular DOM event and a pointer to the first handler in the list, +// scan through the list to find something to handle the event. If aExecute = +// true, the handler will be executed; otherwise just return an answer telling +// if a handler for that event was found. +// +bool GlobalKeyListener::WalkHandlersInternal(dom::KeyboardEvent* aKeyEvent, + bool aExecute, + bool* aOutReservedForChrome) { + WidgetKeyboardEvent* nativeKeyboardEvent = + aKeyEvent->WidgetEventPtr()->AsKeyboardEvent(); + MOZ_ASSERT(nativeKeyboardEvent); + + AutoShortcutKeyCandidateArray shortcutKeys; + nativeKeyboardEvent->GetShortcutKeyCandidates(shortcutKeys); + + if (shortcutKeys.IsEmpty()) { + return WalkHandlersAndExecute(aKeyEvent, 0, IgnoreModifierState(), aExecute, + aOutReservedForChrome); + } + + for (unsigned long i = 0; i < shortcutKeys.Length(); ++i) { + ShortcutKeyCandidate& key = shortcutKeys[i]; + IgnoreModifierState ignoreModifierState; + ignoreModifierState.mShift = key.mIgnoreShift; + if (WalkHandlersAndExecute(aKeyEvent, key.mCharCode, ignoreModifierState, + aExecute, aOutReservedForChrome)) { + return true; + } + } + return false; +} + +bool GlobalKeyListener::WalkHandlersAndExecute( + dom::KeyboardEvent* aKeyEvent, uint32_t aCharCode, + const IgnoreModifierState& aIgnoreModifierState, bool aExecute, + bool* aOutReservedForChrome) { + if (aOutReservedForChrome) { + *aOutReservedForChrome = false; + } + + WidgetKeyboardEvent* widgetKeyboardEvent = + aKeyEvent->WidgetEventPtr()->AsKeyboardEvent(); + if (NS_WARN_IF(!widgetKeyboardEvent)) { + return false; + } + + nsAtom* eventType = + ShortcutKeys::ConvertEventToDOMEventType(widgetKeyboardEvent); + + // Try all of the handlers until we find one that matches the event. + for (KeyEventHandler* handler = mHandler; handler; + handler = handler->GetNextHandler()) { + bool stopped = aKeyEvent->IsDispatchStopped(); + if (stopped) { + // The event is finished, don't execute any more handlers + return false; + } + + if (aExecute) { + if (!handler->EventTypeEquals(eventType)) { + continue; + } + } else { + if (handler->EventTypeEquals(nsGkAtoms::keypress)) { + // If the handler is a keypress event handler, we also need to check + // if coming keydown event is a preceding event of reserved key + // combination because if default action of a keydown event is + // prevented, following keypress event won't be fired. However, if + // following keypress event is reserved, we shouldn't allow web + // contents to prevent the default of the preceding keydown event. + if (eventType != nsGkAtoms::keydown && + eventType != nsGkAtoms::keypress) { + continue; + } + } else if (!handler->EventTypeEquals(eventType)) { + // Otherwise, eventType should exactly be matched. + continue; + } + } + + // Check if the keyboard event *may* execute the handler. + if (!handler->KeyEventMatched(aKeyEvent, aCharCode, aIgnoreModifierState)) { + continue; // try the next one + } + + // Before executing this handler, check that it's not disabled, + // and that it has something to do (oncommand of the <key> or its + // <command> is non-empty). + if (!CanHandle(handler, aExecute)) { + continue; + } + + if (!aExecute) { + if (handler->EventTypeEquals(eventType)) { + if (aOutReservedForChrome) { + *aOutReservedForChrome = IsReservedKey(widgetKeyboardEvent, handler); + } + + return true; + } + + // If the command is reserved and the event is keydown, check also if + // the handler is for keypress because if following keypress event is + // reserved, we shouldn't dispatch the event into web contents. + if (eventType == nsGkAtoms::keydown && + handler->EventTypeEquals(nsGkAtoms::keypress)) { + if (IsReservedKey(widgetKeyboardEvent, handler)) { + if (aOutReservedForChrome) { + *aOutReservedForChrome = true; + } + + return true; + } + } + // Otherwise, we've not found a handler for the event yet. + continue; + } + + // This should only be assigned when aExecute is false. + MOZ_ASSERT(!aOutReservedForChrome); + + nsCOMPtr<dom::EventTarget> target = GetHandlerTarget(handler); + + // XXX Do we execute only one handler even if the handler neither stops + // propagation nor prevents default of the event? + nsresult rv = handler->ExecuteHandler(target, aKeyEvent); + if (NS_SUCCEEDED(rv)) { + return true; + } + } + +#ifdef XP_WIN + // Windows native applications ignore Windows-Logo key state when checking + // shortcut keys even if the key is pressed. Therefore, if there is no + // shortcut key which exactly matches current modifier state, we should + // retry to look for a shortcut key without the Windows-Logo key press. + if (!aIgnoreModifierState.mOS && widgetKeyboardEvent->IsOS()) { + IgnoreModifierState ignoreModifierState(aIgnoreModifierState); + ignoreModifierState.mOS = true; + return WalkHandlersAndExecute(aKeyEvent, aCharCode, ignoreModifierState, + aExecute); + } +#endif + + return false; +} + +bool GlobalKeyListener::IsReservedKey(WidgetKeyboardEvent* aKeyEvent, + KeyEventHandler* aHandler) { + ReservedKey reserved = aHandler->GetIsReserved(); + // reserved="true" means that the key is always reserved. reserved="false" + // means that the key is never reserved. Otherwise, we check site-specific + // permissions. + if (reserved == ReservedKey_False) { + return false; + } + + if (reserved != ReservedKey_True && + !nsContentUtils::ShouldBlockReservedKeys(aKeyEvent)) { + return false; + } + + // Okay, the key handler is reserved, but if the key combination is mapped to + // an edit command or a selection navigation command, we should not treat it + // as reserved since user wants to do the mapped thing(s) in editor. + if (MOZ_UNLIKELY(!aKeyEvent->IsTrusted() || !aKeyEvent->mWidget)) { + return true; + } + widget::InputContext inputContext = aKeyEvent->mWidget->GetInputContext(); + if (!inputContext.mIMEState.IsEditable()) { + return true; + } + return MOZ_UNLIKELY(!aKeyEvent->IsEditCommandsInitialized( + inputContext.GetNativeKeyBindingsType())) || + aKeyEvent + ->EditCommandsConstRef(inputContext.GetNativeKeyBindingsType()) + .IsEmpty(); +} + +bool GlobalKeyListener::HasHandlerForEvent(dom::KeyboardEvent* aEvent, + bool* aOutReservedForChrome) { + WidgetKeyboardEvent* widgetKeyboardEvent = + aEvent->WidgetEventPtr()->AsKeyboardEvent(); + if (NS_WARN_IF(!widgetKeyboardEvent) || !widgetKeyboardEvent->IsTrusted()) { + return false; + } + + EnsureHandlers(); + + if (IsDisabled()) { + return false; + } + + return WalkHandlersInternal(aEvent, false, aOutReservedForChrome); +} + +// +// AttachGlobalKeyHandler +// +// Creates a new key handler and prepares to listen to key events on the given +// event receiver (either a document or an content node). If the receiver is +// content, then extra work needs to be done to hook it up to the document (XXX +// WHY??) +// +void XULKeySetGlobalKeyListener::AttachKeyHandler( + dom::Element* aElementTarget) { + // Only attach if we're really in a document + nsCOMPtr<dom::Document> doc = aElementTarget->GetUncomposedDoc(); + if (!doc) { + return; + } + + EventListenerManager* manager = doc->GetOrCreateListenerManager(); + if (!manager) { + return; + } + + // the listener already exists, so skip this + if (aElementTarget->GetProperty(nsGkAtoms::listener)) { + return; + } + + // Create the key handler + RefPtr<XULKeySetGlobalKeyListener> handler = + new XULKeySetGlobalKeyListener(aElementTarget, doc); + + handler->InstallKeyboardEventListenersTo(manager); + + aElementTarget->SetProperty(nsGkAtoms::listener, handler.forget().take(), + nsPropertyTable::SupportsDtorFunc, true); +} + +// +// DetachGlobalKeyHandler +// +// Removes a key handler added by AttachKeyHandler. +// +void XULKeySetGlobalKeyListener::DetachKeyHandler( + dom::Element* aElementTarget) { + // Only attach if we're really in a document + nsCOMPtr<dom::Document> doc = aElementTarget->GetUncomposedDoc(); + if (!doc) { + return; + } + + EventListenerManager* manager = doc->GetOrCreateListenerManager(); + if (!manager) { + return; + } + + nsIDOMEventListener* handler = static_cast<nsIDOMEventListener*>( + aElementTarget->GetProperty(nsGkAtoms::listener)); + if (!handler) { + return; + } + + static_cast<XULKeySetGlobalKeyListener*>(handler) + ->RemoveKeyboardEventListenersFrom(manager); + aElementTarget->RemoveProperty(nsGkAtoms::listener); +} + +XULKeySetGlobalKeyListener::XULKeySetGlobalKeyListener( + dom::Element* aElement, dom::EventTarget* aTarget) + : GlobalKeyListener(aTarget) { + mWeakPtrForElement = do_GetWeakReference(aElement); +} + +dom::Element* XULKeySetGlobalKeyListener::GetElement(bool* aIsDisabled) const { + RefPtr<dom::Element> element = do_QueryReferent(mWeakPtrForElement); + if (element && aIsDisabled) { + *aIsDisabled = element->AttrValueIs(kNameSpaceID_None, nsGkAtoms::disabled, + nsGkAtoms::_true, eCaseMatters); + } + return element.get(); +} + +XULKeySetGlobalKeyListener::~XULKeySetGlobalKeyListener() { + if (mWeakPtrForElement) { + delete mHandler; + } +} + +void XULKeySetGlobalKeyListener::EnsureHandlers() { + if (mHandler) { + return; + } + + dom::Element* element = GetElement(); + if (!element) { + return; + } + + BuildHandlerChain(element, &mHandler); +} + +bool XULKeySetGlobalKeyListener::IsDisabled() const { + bool isDisabled; + dom::Element* element = GetElement(&isDisabled); + return element && isDisabled; +} + +bool XULKeySetGlobalKeyListener::GetElementForHandler( + KeyEventHandler* aHandler, dom::Element** aElementForHandler) const { + MOZ_ASSERT(aElementForHandler); + *aElementForHandler = nullptr; + + RefPtr<dom::Element> keyElement = aHandler->GetHandlerElement(); + if (!keyElement) { + // This should only be the case where the <key> element that generated the + // handler has been destroyed. Not sure why we return true here... + return true; + } + + nsCOMPtr<dom::Element> chromeHandlerElement = GetElement(); + if (!chromeHandlerElement) { + NS_WARNING_ASSERTION(keyElement->IsInUncomposedDoc(), "uncomposed"); + keyElement.swap(*aElementForHandler); + return true; + } + + // We are in a XUL doc. Obtain our command attribute. + nsAutoString command; + keyElement->GetAttr(kNameSpaceID_None, nsGkAtoms::command, command); + if (command.IsEmpty()) { + // There is no command element associated with the key element. + NS_WARNING_ASSERTION(keyElement->IsInUncomposedDoc(), "uncomposed"); + keyElement.swap(*aElementForHandler); + return true; + } + + // XXX Shouldn't we check this earlier? + dom::Document* doc = keyElement->GetUncomposedDoc(); + if (NS_WARN_IF(!doc)) { + return false; + } + + nsCOMPtr<dom::Element> commandElement = doc->GetElementById(command); + if (!commandElement) { + NS_ERROR( + "A XUL <key> is observing a command that doesn't exist. " + "Unable to execute key binding!"); + return false; + } + + commandElement.swap(*aElementForHandler); + return true; +} + +bool XULKeySetGlobalKeyListener::IsExecutableElement( + dom::Element* aElement) const { + if (!aElement) { + return false; + } + + nsAutoString value; + aElement->GetAttr(nsGkAtoms::disabled, value); + if (value.EqualsLiteral("true")) { + return false; + } + + // Internal keys are defined as <key> elements so that the menu label + // and disabled state can be updated properly, but the command is executed + // by some other means. This will typically be because the key is defined + // as a shortcut defined in ShortcutKeyDefinitions.cpp instead, or on Mac, + // some special system defined keys. + return !aElement->AttrValueIs(kNameSpaceID_None, nsGkAtoms::internal, + nsGkAtoms::_true, eCaseMatters); +} + +already_AddRefed<dom::EventTarget> XULKeySetGlobalKeyListener::GetHandlerTarget( + KeyEventHandler* aHandler) { + nsCOMPtr<dom::Element> commandElement; + if (!GetElementForHandler(aHandler, getter_AddRefs(commandElement))) { + return nullptr; + } + + return commandElement.forget(); +} + +bool XULKeySetGlobalKeyListener::CanHandle(KeyEventHandler* aHandler, + bool aWillExecute) const { + // If the <key> element itself is disabled, ignore it. + if (aHandler->KeyElementIsDisabled()) { + return false; + } + + nsCOMPtr<dom::Element> commandElement; + if (!GetElementForHandler(aHandler, getter_AddRefs(commandElement))) { + return false; + } + + // The only case where commandElement can be null here is where the <key> + // element for the handler is already destroyed. I'm not sure why we continue + // in this case. + if (!commandElement) { + return true; + } + + // If we're not actually going to execute here bypass the execution check. + return !aWillExecute || IsExecutableElement(commandElement); +} + +/* static */ +layers::KeyboardMap RootWindowGlobalKeyListener::CollectKeyboardShortcuts() { + KeyEventHandler* handlers = ShortcutKeys::GetHandlers(HandlerType::eBrowser); + + // Convert the handlers into keyboard shortcuts, using an AutoTArray with + // the maximum amount of shortcuts used on any platform to minimize + // allocations + AutoTArray<KeyboardShortcut, 48> shortcuts; + + // Append keyboard shortcuts for hardcoded actions like tab + KeyboardShortcut::AppendHardcodedShortcuts(shortcuts); + + for (KeyEventHandler* handler = handlers; handler; + handler = handler->GetNextHandler()) { + KeyboardShortcut shortcut; + if (handler->TryConvertToKeyboardShortcut(&shortcut)) { + shortcuts.AppendElement(shortcut); + } + } + + return layers::KeyboardMap(std::move(shortcuts)); +} + +// +// AttachGlobalKeyHandler +// +// Creates a new key handler and prepares to listen to key events on the given +// event receiver (either a document or an content node). If the receiver is +// content, then extra work needs to be done to hook it up to the document (XXX +// WHY??) +// +void RootWindowGlobalKeyListener::AttachKeyHandler(dom::EventTarget* aTarget) { + EventListenerManager* manager = aTarget->GetOrCreateListenerManager(); + if (!manager) { + return; + } + + // Create the key handler + RefPtr<RootWindowGlobalKeyListener> handler = + new RootWindowGlobalKeyListener(aTarget); + + // This registers handler with the manager so the manager will keep handler + // alive past this point. + handler->InstallKeyboardEventListenersTo(manager); +} + +RootWindowGlobalKeyListener::RootWindowGlobalKeyListener( + dom::EventTarget* aTarget) + : GlobalKeyListener(aTarget) {} + +/* static */ +bool RootWindowGlobalKeyListener::IsHTMLEditorFocused() { + nsFocusManager* fm = nsFocusManager::GetFocusManager(); + if (!fm) { + return false; + } + + nsCOMPtr<mozIDOMWindowProxy> focusedWindow; + fm->GetFocusedWindow(getter_AddRefs(focusedWindow)); + if (!focusedWindow) { + return false; + } + + auto* piwin = nsPIDOMWindowOuter::From(focusedWindow); + nsIDocShell* docShell = piwin->GetDocShell(); + if (!docShell) { + return false; + } + + HTMLEditor* htmlEditor = docShell->GetHTMLEditor(); + if (!htmlEditor) { + return false; + } + + if (htmlEditor->IsInDesignMode()) { + // Don't need to perform any checks in designMode documents. + return true; + } + + nsINode* focusedNode = fm->GetFocusedElement(); + if (focusedNode && focusedNode->IsElement()) { + // If there is a focused element, make sure it's in the active editing host. + // Note that ComputeEditingHost finds the current editing host based on + // the document's selection. Even though the document selection is usually + // collapsed to where the focus is, but the page may modify the selection + // without our knowledge, in which case this check will do something useful. + dom::Element* editingHost = htmlEditor->ComputeEditingHost(); + if (!editingHost) { + return false; + } + return focusedNode->IsInclusiveDescendantOf(editingHost); + } + + return false; +} + +void RootWindowGlobalKeyListener::EnsureHandlers() { + if (IsHTMLEditorFocused()) { + mHandler = ShortcutKeys::GetHandlers(HandlerType::eEditor); + } else { + mHandler = ShortcutKeys::GetHandlers(HandlerType::eBrowser); + } +} + +} // namespace mozilla diff --git a/dom/events/GlobalKeyListener.h b/dom/events/GlobalKeyListener.h new file mode 100644 index 0000000000..78a00efe53 --- /dev/null +++ b/dom/events/GlobalKeyListener.h @@ -0,0 +1,186 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#ifndef mozilla_GlobalKeyListener_h_ +#define mozilla_GlobalKeyListener_h_ + +#include "mozilla/EventForwards.h" +#include "mozilla/layers/KeyboardMap.h" +#include "nsIDOMEventListener.h" +#include "nsIWeakReferenceUtils.h" + +class nsAtom; + +namespace mozilla { +class EventListenerManager; +class WidgetKeyboardEvent; +struct IgnoreModifierState; + +namespace layers { +class KeyboardMap; +} + +namespace dom { +class Element; +class EventTarget; +class KeyboardEvent; +} // namespace dom + +class KeyEventHandler; + +/** + * A generic listener for key events. + * + * Maintains a list of shortcut handlers and is registered as a listener for DOM + * key events from a target. Responsible for executing the appropriate handler + * when a keyboard event is received. + */ +class GlobalKeyListener : public nsIDOMEventListener { + public: + explicit GlobalKeyListener(dom::EventTarget* aTarget); + + void InstallKeyboardEventListenersTo( + EventListenerManager* aEventListenerManager); + void RemoveKeyboardEventListenersFrom( + EventListenerManager* aEventListenerManager); + + NS_DECL_ISUPPORTS + NS_DECL_NSIDOMEVENTLISTENER + + protected: + virtual ~GlobalKeyListener() = default; + + MOZ_CAN_RUN_SCRIPT + void WalkHandlers(dom::KeyboardEvent* aKeyEvent); + + // walk the handlers, looking for one to handle the event + MOZ_CAN_RUN_SCRIPT + bool WalkHandlersInternal(dom::KeyboardEvent* aKeyEvent, bool aExecute, + bool* aOutReservedForChrome = nullptr); + + // walk the handlers for aEvent, aCharCode and aIgnoreModifierState. Execute + // it if aExecute = true. + MOZ_CAN_RUN_SCRIPT + bool WalkHandlersAndExecute(dom::KeyboardEvent* aKeyEvent, uint32_t aCharCode, + const IgnoreModifierState& aIgnoreModifierState, + bool aExecute, + bool* aOutReservedForChrome = nullptr); + + // HandleEvent function for the capturing phase in the default event group. + MOZ_CAN_RUN_SCRIPT + void HandleEventOnCaptureInDefaultEventGroup(dom::KeyboardEvent* aEvent); + // HandleEvent function for the capturing phase in the system event group. + MOZ_CAN_RUN_SCRIPT + void HandleEventOnCaptureInSystemEventGroup(dom::KeyboardEvent* aEvent); + + // Check if any handler would handle the given event. Optionally returns + // whether the command handler for the event is marked with the "reserved" + // attribute. + MOZ_CAN_RUN_SCRIPT + bool HasHandlerForEvent(dom::KeyboardEvent* aEvent, + bool* aOutReservedForChrome = nullptr); + + // Returns true if the key would be reserved for the given handler. A reserved + // key is not sent to a content process or single-process equivalent. + bool IsReservedKey(WidgetKeyboardEvent* aKeyEvent, KeyEventHandler* aHandler); + + // lazily load the handlers. Overridden to handle being attached + // to a particular element rather than the document + virtual void EnsureHandlers() = 0; + + virtual bool CanHandle(KeyEventHandler* aHandler, bool aWillExecute) const { + return true; + } + + virtual bool IsDisabled() const { return false; } + + virtual already_AddRefed<dom::EventTarget> GetHandlerTarget( + KeyEventHandler* aHandler) { + return do_AddRef(mTarget); + } + + dom::EventTarget* mTarget; // weak ref; + + KeyEventHandler* mHandler; // Linked list of event handlers. +}; + +/** + * A listener for shortcut keys defined in XUL keyset elements. + * + * Listens for keyboard events from the document object and triggers the + * appropriate XUL key elements. + */ +class XULKeySetGlobalKeyListener final : public GlobalKeyListener { + public: + explicit XULKeySetGlobalKeyListener(dom::Element* aElement, + dom::EventTarget* aTarget); + + static void AttachKeyHandler(dom::Element* aElementTarget); + static void DetachKeyHandler(dom::Element* aElementTarget); + + protected: + virtual ~XULKeySetGlobalKeyListener(); + + // Returns the element which was passed as a parameter to the constructor, + // unless the element has been removed from the document. Optionally returns + // whether the disabled attribute is set on the element (assuming the element + // is non-null). + dom::Element* GetElement(bool* aIsDisabled = nullptr) const; + + virtual void EnsureHandlers() override; + + virtual bool CanHandle(KeyEventHandler* aHandler, + bool aWillExecute) const override; + virtual bool IsDisabled() const override; + virtual already_AddRefed<dom::EventTarget> GetHandlerTarget( + KeyEventHandler* aHandler) override; + + /** + * GetElementForHandler() retrieves an element for the handler. The element + * may be a command element or a key element. + * + * @param aHandler The handler. + * @param aElementForHandler Must not be nullptr. The element is returned to + * this. + * @return true if the handler is valid. Otherwise, false. + */ + bool GetElementForHandler(KeyEventHandler* aHandler, + dom::Element** aElementForHandler) const; + + /** + * IsExecutableElement() returns true if aElement is executable. + * Otherwise, false. aElement should be a command element or a key element. + */ + bool IsExecutableElement(dom::Element* aElement) const; + + // Using weak pointer to the DOM Element. + nsWeakPtr mWeakPtrForElement; +}; + +/** + * Listens for built-in shortcut keys. + * + * Listens to DOM keyboard events from the window or text input and runs the + * built-in shortcuts (see dom/events/keyevents) as necessary. + */ +class RootWindowGlobalKeyListener final : public GlobalKeyListener { + public: + explicit RootWindowGlobalKeyListener(dom::EventTarget* aTarget); + + static void AttachKeyHandler(dom::EventTarget* aTarget); + + static layers::KeyboardMap CollectKeyboardShortcuts(); + + protected: + // Is an HTML editable element focused + static bool IsHTMLEditorFocused(); + + virtual void EnsureHandlers() override; +}; + +} // namespace mozilla + +#endif diff --git a/dom/events/IMEContentObserver.cpp b/dom/events/IMEContentObserver.cpp new file mode 100644 index 0000000000..0a63a8b841 --- /dev/null +++ b/dom/events/IMEContentObserver.cpp @@ -0,0 +1,2173 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#include "mozilla/Logging.h" + +#include "ContentEventHandler.h" +#include "IMEContentObserver.h" +#include "mozilla/AsyncEventDispatcher.h" +#include "mozilla/AutoRestore.h" +#include "mozilla/EventStateManager.h" +#include "mozilla/IMEStateManager.h" +#include "mozilla/MouseEvents.h" +#include "mozilla/PresShell.h" +#include "mozilla/TextComposition.h" +#include "mozilla/TextEvents.h" +#include "mozilla/dom/Element.h" +#include "mozilla/dom/Selection.h" +#include "nsContentUtils.h" +#include "nsGkAtoms.h" +#include "nsAtom.h" +#include "nsDocShell.h" +#include "nsIContent.h" +#include "mozilla/dom/Document.h" +#include "nsIFrame.h" +#include "nsINode.h" +#include "nsISelectionController.h" +#include "nsISupports.h" +#include "nsIWeakReferenceUtils.h" +#include "nsIWidget.h" +#include "nsPresContext.h" +#include "nsRange.h" +#include "nsRefreshDriver.h" +#include "WritingModes.h" + +namespace mozilla { + +using NodePosition = ContentEventHandler::NodePosition; +using NodePositionBefore = ContentEventHandler::NodePositionBefore; + +using namespace dom; +using namespace widget; + +LazyLogModule sIMECOLog("IMEContentObserver"); + +static const char* ToChar(bool aBool) { return aBool ? "true" : "false"; } + +// This method determines the node to use for the point before the current node. +// If you have the following aContent and aContainer, and want to represent the +// following point for `NodePosition` or `RangeBoundary`: +// +// <parent> {node} {node} | {node} </parent> +// ^ ^ ^ +// aContainer point aContent +// +// This function will shift `aContent` to the left into the format which +// `NodePosition` and `RangeBoundary` use: +// +// <parent> {node} {node} | {node} </parent> +// ^ ^ ^ +// aContainer result point +static nsIContent* PointBefore(nsINode* aContainer, nsIContent* aContent) { + if (aContent) { + return aContent->GetPreviousSibling(); + } + return aContainer->GetLastChild(); +} + +/****************************************************************************** + * mozilla::IMEContentObserver + ******************************************************************************/ + +NS_IMPL_CYCLE_COLLECTION_CLASS(IMEContentObserver) + +// Note that we don't need to add mFirstAddedContainer nor +// mLastAddedContainer to cycle collection because they are non-null only +// during short time and shouldn't be touched while they are non-null. + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(IMEContentObserver) + nsAutoScriptBlocker scriptBlocker; + + tmp->NotifyIMEOfBlur(); + tmp->UnregisterObservers(); + + NS_IMPL_CYCLE_COLLECTION_UNLINK(mSelection) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mRootElement) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mEditableNode) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mDocShell) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mEditorBase) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mDocumentObserver) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mEndOfAddedTextCache.mContainerNode) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mStartOfRemovingTextRangeCache.mContainerNode) + NS_IMPL_CYCLE_COLLECTION_UNLINK_WEAK_REFERENCE + + tmp->mIMENotificationRequests = nullptr; + tmp->mESM = nullptr; +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(IMEContentObserver) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mWidget) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mFocusedWidget) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSelection) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mRootElement) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mEditableNode) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDocShell) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mEditorBase) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDocumentObserver) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mEndOfAddedTextCache.mContainerNode) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE( + mStartOfRemovingTextRangeCache.mContainerNode) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(IMEContentObserver) + NS_INTERFACE_MAP_ENTRY(nsIMutationObserver) + NS_INTERFACE_MAP_ENTRY(nsIReflowObserver) + NS_INTERFACE_MAP_ENTRY(nsIScrollObserver) + NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIReflowObserver) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTING_ADDREF(IMEContentObserver) +NS_IMPL_CYCLE_COLLECTING_RELEASE(IMEContentObserver) + +IMEContentObserver::IMEContentObserver() + : mESM(nullptr), + mIMENotificationRequests(nullptr), + mSendingNotification(NOTIFY_IME_OF_NOTHING), + mIsObserving(false), + mIMEHasFocus(false), + mNeedsToNotifyIMEOfFocusSet(false), + mNeedsToNotifyIMEOfTextChange(false), + mNeedsToNotifyIMEOfSelectionChange(false), + mNeedsToNotifyIMEOfPositionChange(false), + mNeedsToNotifyIMEOfCompositionEventHandled(false), + mIsHandlingQueryContentEvent(false) { +#ifdef DEBUG + mTextChangeData.Test(); +#endif +} + +void IMEContentObserver::Init(nsIWidget& aWidget, nsPresContext& aPresContext, + Element* aElement, EditorBase& aEditorBase) { + State state = GetState(); + if (NS_WARN_IF(state == eState_Observing)) { + return; // Nothing to do. + } + + bool firstInitialization = state != eState_StoppedObserving; + if (!firstInitialization) { + // If this is now trying to initialize with new contents, all observers + // should be registered again for simpler implementation. + UnregisterObservers(); + Clear(); + } + + mESM = aPresContext.EventStateManager(); + mESM->OnStartToObserveContent(this); + + mWidget = &aWidget; + mIMENotificationRequests = &mWidget->IMENotificationRequestsRef(); + + if (!InitWithEditor(aPresContext, aElement, aEditorBase)) { + MOZ_LOG(sIMECOLog, LogLevel::Error, + ("0x%p Init() FAILED, due to InitWithEditor() " + "failure", + this)); + Clear(); + return; + } + + if (firstInitialization) { + // Now, try to send NOTIFY_IME_OF_FOCUS to IME via the widget. + MaybeNotifyIMEOfFocusSet(); + // When this is called first time, IME has not received NOTIFY_IME_OF_FOCUS + // yet since NOTIFY_IME_OF_FOCUS will be sent to widget asynchronously. + // So, we need to do nothing here. After NOTIFY_IME_OF_FOCUS has been + // sent, OnIMEReceivedFocus() will be called and content, selection and/or + // position changes will be observed + return; + } + + // When this is called after editor reframing (i.e., the root editable node + // is also recreated), IME has usually received NOTIFY_IME_OF_FOCUS. In this + // case, we need to restart to observe content, selection and/or position + // changes in new root editable node. + ObserveEditableNode(); + + if (!NeedsToNotifyIMEOfSomething()) { + return; + } + + // Some change events may wait to notify IME because this was being + // initialized. It is the time to flush them. + FlushMergeableNotifications(); +} + +void IMEContentObserver::OnIMEReceivedFocus() { + // While Init() notifies IME of focus, pending layout may be flushed + // because the notification may cause querying content. Then, recursive + // call of Init() with the latest content may occur. In such case, we + // shouldn't keep first initialization which notified IME of focus. + if (GetState() != eState_Initializing) { + MOZ_LOG(sIMECOLog, LogLevel::Warning, + ("0x%p OnIMEReceivedFocus(), " + "but the state is not \"initializing\", so does nothing", + this)); + return; + } + + // NOTIFY_IME_OF_FOCUS might cause recreating IMEContentObserver + // instance via IMEStateManager::UpdateIMEState(). So, this + // instance might already have been destroyed, check it. + if (!mRootElement) { + MOZ_LOG(sIMECOLog, LogLevel::Warning, + ("0x%p OnIMEReceivedFocus(), " + "but mRootElement has already been cleared, so does nothing", + this)); + return; + } + + // Start to observe which is needed by IME when IME actually has focus. + ObserveEditableNode(); + + if (!NeedsToNotifyIMEOfSomething()) { + return; + } + + // Some change events may wait to notify IME because this was being + // initialized. It is the time to flush them. + FlushMergeableNotifications(); +} + +bool IMEContentObserver::InitWithEditor(nsPresContext& aPresContext, + Element* aElement, + EditorBase& aEditorBase) { + mEditableNode = IMEStateManager::GetRootEditableNode(aPresContext, aElement); + if (NS_WARN_IF(!mEditableNode)) { + return false; + } + + mEditorBase = &aEditorBase; + + RefPtr<PresShell> presShell = aPresContext.GetPresShell(); + + // get selection and root content + nsCOMPtr<nsISelectionController> selCon; + if (mEditableNode->IsContent()) { + nsIFrame* frame = mEditableNode->AsContent()->GetPrimaryFrame(); + if (NS_WARN_IF(!frame)) { + return false; + } + + frame->GetSelectionController(&aPresContext, getter_AddRefs(selCon)); + } else { + // mEditableNode is a document + selCon = presShell; + } + + if (NS_WARN_IF(!selCon)) { + return false; + } + + mSelection = selCon->GetSelection(nsISelectionController::SELECTION_NORMAL); + if (NS_WARN_IF(!mSelection)) { + return false; + } + + if (mEditorBase->IsTextEditor()) { + mRootElement = mEditorBase->GetRoot(); // The anonymous <div> + MOZ_ASSERT(mRootElement); + MOZ_ASSERT(mRootElement->GetFirstChild()); + if (auto* text = Text::FromNodeOrNull( + mRootElement ? mRootElement->GetFirstChild() : nullptr)) { + mTextControlValueLength = ContentEventHandler::GetNativeTextLength(*text); + } + } else if (const nsRange* selRange = mSelection->GetRangeAt(0)) { + if (NS_WARN_IF(!selRange->GetStartContainer())) { + return false; + } + + nsCOMPtr<nsINode> startContainer = selRange->GetStartContainer(); + mRootElement = Element::FromNodeOrNull( + startContainer->GetSelectionRootContent(presShell)); + } else { + nsCOMPtr<nsINode> editableNode = mEditableNode; + mRootElement = Element::FromNodeOrNull( + editableNode->GetSelectionRootContent(presShell)); + } + if (!mRootElement && mEditableNode->IsDocument()) { + // The document node is editable, but there are no contents, this document + // is not editable. + return false; + } + + if (NS_WARN_IF(!mRootElement)) { + return false; + } + + mDocShell = aPresContext.GetDocShell(); + if (NS_WARN_IF(!mDocShell)) { + return false; + } + + mDocumentObserver = new DocumentObserver(*this); + + return true; +} + +void IMEContentObserver::Clear() { + mEditorBase = nullptr; + mSelection = nullptr; + mEditableNode = nullptr; + mRootElement = nullptr; + mDocShell = nullptr; + // Should be safe to clear mDocumentObserver here even though it grabs + // this instance in most cases because this is called by Init() or Destroy(). + // The callers of Init() grab this instance with local RefPtr. + // The caller of Destroy() also grabs this instance with local RefPtr. + // So, this won't cause refcount of this instance become 0. + mDocumentObserver = nullptr; +} + +void IMEContentObserver::ObserveEditableNode() { + MOZ_RELEASE_ASSERT(mSelection); + MOZ_RELEASE_ASSERT(mRootElement); + MOZ_RELEASE_ASSERT(GetState() != eState_Observing); + + // If this is called before sending NOTIFY_IME_OF_FOCUS (it's possible when + // the editor is reframed before sending NOTIFY_IME_OF_FOCUS asynchronously), + // the notification requests of mWidget may be different from after the widget + // receives NOTIFY_IME_OF_FOCUS. So, this should be called again by + // OnIMEReceivedFocus() which is called after sending NOTIFY_IME_OF_FOCUS. + if (!mIMEHasFocus) { + MOZ_ASSERT(!mWidget || mNeedsToNotifyIMEOfFocusSet || + mSendingNotification == NOTIFY_IME_OF_FOCUS, + "Wow, OnIMEReceivedFocus() won't be called?"); + return; + } + + mIsObserving = true; + if (mEditorBase) { + mEditorBase->SetIMEContentObserver(this); + } + + mRootElement->AddMutationObserver(this); + // If it's in a document (should be so), we can use document observer to + // reduce redundant computation of text change offsets. + Document* doc = mRootElement->GetComposedDoc(); + if (doc) { + RefPtr<DocumentObserver> documentObserver = mDocumentObserver; + documentObserver->Observe(doc); + } + + if (mDocShell) { + // Add scroll position listener and reflow observer to detect position + // and size changes + mDocShell->AddWeakScrollObserver(this); + mDocShell->AddWeakReflowObserver(this); + } +} + +void IMEContentObserver::NotifyIMEOfBlur() { + // Prevent any notifications to be sent IME. + nsCOMPtr<nsIWidget> widget; + mWidget.swap(widget); + mIMENotificationRequests = nullptr; + + // If we hasn't been set focus, we shouldn't send blur notification to IME. + if (!mIMEHasFocus) { + return; + } + + // mWidget must have been non-nullptr if IME has focus. + MOZ_RELEASE_ASSERT(widget); + + RefPtr<IMEContentObserver> kungFuDeathGrip(this); + + MOZ_LOG(sIMECOLog, LogLevel::Info, + ("0x%p NotifyIMEOfBlur(), sending NOTIFY_IME_OF_BLUR", this)); + + // For now, we need to send blur notification in any condition because + // we don't have any simple ways to send blur notification asynchronously. + // After this call, Destroy() or Unlink() will stop observing the content + // and forget everything. Therefore, if it's not safe to send notification + // when script blocker is unlocked, we cannot send blur notification after + // that and before next focus notification. + // Anyway, as far as we know, IME doesn't try to query content when it loses + // focus. So, this may not cause any problem. + mIMEHasFocus = false; + IMEStateManager::NotifyIME(IMENotification(NOTIFY_IME_OF_BLUR), widget); + + MOZ_LOG(sIMECOLog, LogLevel::Debug, + ("0x%p NotifyIMEOfBlur(), sent NOTIFY_IME_OF_BLUR", this)); +} + +void IMEContentObserver::UnregisterObservers() { + if (!mIsObserving) { + return; + } + mIsObserving = false; + + if (mEditorBase) { + mEditorBase->SetIMEContentObserver(nullptr); + } + + if (mSelection) { + mSelectionData.Clear(); + mFocusedWidget = nullptr; + } + + if (mRootElement) { + mRootElement->RemoveMutationObserver(this); + } + + if (mDocumentObserver) { + RefPtr<DocumentObserver> documentObserver = mDocumentObserver; + documentObserver->StopObserving(); + } + + if (mDocShell) { + mDocShell->RemoveWeakScrollObserver(this); + mDocShell->RemoveWeakReflowObserver(this); + } +} + +nsPresContext* IMEContentObserver::GetPresContext() const { + return mESM ? mESM->GetPresContext() : nullptr; +} + +void IMEContentObserver::Destroy() { + // WARNING: When you change this method, you have to check Unlink() too. + + // Note that don't send any notifications later from here. I.e., notify + // IMEStateManager of the blur synchronously because IMEStateManager needs to + // stop notifying the main process if this is requested by the main process. + NotifyIMEOfBlur(); + UnregisterObservers(); + Clear(); + + mWidget = nullptr; + mIMENotificationRequests = nullptr; + + if (mESM) { + mESM->OnStopObservingContent(this); + mESM = nullptr; + } +} + +bool IMEContentObserver::Destroyed() const { return !mWidget; } + +void IMEContentObserver::DisconnectFromEventStateManager() { mESM = nullptr; } + +bool IMEContentObserver::MaybeReinitialize(nsIWidget& aWidget, + nsPresContext& aPresContext, + Element* aElement, + EditorBase& aEditorBase) { + if (!IsObservingContent(aPresContext, aElement)) { + return false; + } + + if (GetState() == eState_StoppedObserving) { + Init(aWidget, aPresContext, aElement, aEditorBase); + } + return IsManaging(aPresContext, aElement); +} + +bool IMEContentObserver::IsManaging(const nsPresContext& aPresContext, + const Element* aElement) const { + return GetState() == eState_Observing && + IsObservingContent(aPresContext, aElement); +} + +bool IMEContentObserver::IsBeingInitializedFor( + const nsPresContext& aPresContext, const Element* aElement) const { + return GetState() == eState_Initializing && + IsObservingContent(aPresContext, aElement); +} + +bool IMEContentObserver::IsManaging( + const TextComposition& aTextComposition) const { + if (GetState() != eState_Observing) { + return false; + } + nsPresContext* const presContext = aTextComposition.GetPresContext(); + if (NS_WARN_IF(!presContext)) { + return false; + } + if (presContext != GetPresContext()) { + return false; // observing different document + } + return IsObservingContent( + *presContext, + Element::FromNodeOrNull(aTextComposition.GetEventTargetNode())); +} + +IMEContentObserver::State IMEContentObserver::GetState() const { + if (!mSelection || !mRootElement || !mEditableNode) { + return eState_NotObserving; // failed to initialize or finalized. + } + if (!mRootElement->IsInComposedDoc()) { + // the focused editor has already been reframed. + return eState_StoppedObserving; + } + return mIsObserving ? eState_Observing : eState_Initializing; +} + +bool IMEContentObserver::IsObservingContent(const nsPresContext& aPresContext, + const Element* aElement) const { + return mEditableNode == + IMEStateManager::GetRootEditableNode(aPresContext, aElement); +} + +bool IMEContentObserver::IsEditorHandlingEventForComposition() const { + if (!mWidget) { + return false; + } + RefPtr<TextComposition> composition = + IMEStateManager::GetTextCompositionFor(mWidget); + if (!composition) { + return false; + } + return composition->IsEditorHandlingEvent(); +} + +bool IMEContentObserver::IsEditorComposing() const { + // Note that don't use TextComposition here. The important thing is, + // whether the editor already started to handle composition because + // web contents can change selection, text content and/or something from + // compositionstart event listener which is run before EditorBase handles it. + if (NS_WARN_IF(!mEditorBase)) { + return false; + } + return mEditorBase->IsIMEComposing(); +} + +nsresult IMEContentObserver::GetSelectionAndRoot(Selection** aSelection, + Element** aRootElement) const { + if (!mEditableNode || !mSelection) { + return NS_ERROR_NOT_AVAILABLE; + } + + NS_ASSERTION(mSelection && mRootElement, "uninitialized content observer"); + NS_ADDREF(*aSelection = mSelection); + NS_ADDREF(*aRootElement = mRootElement); + return NS_OK; +} + +void IMEContentObserver::OnSelectionChange(Selection& aSelection) { + if (!mIsObserving) { + return; + } + + if (mWidget) { + bool causedByComposition = IsEditorHandlingEventForComposition(); + bool causedBySelectionEvent = TextComposition::IsHandlingSelectionEvent(); + bool duringComposition = IsEditorComposing(); + MaybeNotifyIMEOfSelectionChange(causedByComposition, causedBySelectionEvent, + duringComposition); + } +} + +void IMEContentObserver::ScrollPositionChanged() { + if (!NeedsPositionChangeNotification()) { + return; + } + + MaybeNotifyIMEOfPositionChange(); +} + +NS_IMETHODIMP +IMEContentObserver::Reflow(DOMHighResTimeStamp aStart, + DOMHighResTimeStamp aEnd) { + if (!NeedsPositionChangeNotification()) { + return NS_OK; + } + + MaybeNotifyIMEOfPositionChange(); + return NS_OK; +} + +NS_IMETHODIMP +IMEContentObserver::ReflowInterruptible(DOMHighResTimeStamp aStart, + DOMHighResTimeStamp aEnd) { + if (!NeedsPositionChangeNotification()) { + return NS_OK; + } + + MaybeNotifyIMEOfPositionChange(); + return NS_OK; +} + +nsresult IMEContentObserver::HandleQueryContentEvent( + WidgetQueryContentEvent* aEvent) { + // If the instance has normal selection cache and the query event queries + // normal selection's range, it should use the cached selection which was + // sent to the widget. However, if this instance has already received new + // selection change notification but hasn't updated the cache yet (i.e., + // not sending selection change notification to IME, don't use the cached + // value. Note that don't update selection cache here since if you update + // selection cache here, IMENotificationSender won't notify IME of selection + // change because it looks like that the selection isn't actually changed. + const bool isSelectionCacheAvailable = aEvent->mUseNativeLineBreak && + mSelectionData.IsInitialized() && + !mNeedsToNotifyIMEOfSelectionChange; + if (isSelectionCacheAvailable && aEvent->mMessage == eQuerySelectedText && + aEvent->mInput.mSelectionType == SelectionType::eNormal) { + aEvent->EmplaceReply(); + if (mSelectionData.HasRange()) { + aEvent->mReply->mOffsetAndData.emplace(mSelectionData.mOffset, + mSelectionData.String(), + OffsetAndDataFor::SelectedString); + aEvent->mReply->mReversed = mSelectionData.mReversed; + } + aEvent->mReply->mContentsRoot = mRootElement; + aEvent->mReply->mWritingMode = mSelectionData.GetWritingMode(); + // The selection cache in IMEContentObserver must always have been in + // an editing host (or an editable annoymous <div> element). Therefore, + // we set mIsEditableContent to true here even though it's already been + // blurred or changed its editable state but the selection cache has not + // been invalidated yet. + aEvent->mReply->mIsEditableContent = true; + MOZ_LOG(sIMECOLog, LogLevel::Debug, + ("0x%p HandleQueryContentEvent(aEvent={ " + "mMessage=%s, mReply=%s })", + this, ToChar(aEvent->mMessage), ToString(aEvent->mReply).c_str())); + return NS_OK; + } + + MOZ_LOG(sIMECOLog, LogLevel::Info, + ("0x%p HandleQueryContentEvent(aEvent={ mMessage=%s })", this, + ToChar(aEvent->mMessage))); + + // If we can make the event's input offset absolute with TextComposition or + // mSelection, we should set it here for reducing the cost of computing + // selection start offset. If ContentEventHandler receives a + // WidgetQueryContentEvent whose input offset is relative to insertion point, + // it computes current selection start offset (this may be expensive) and + // make the offset absolute value itself. + // Note that calling MakeOffsetAbsolute() makes the event a query event with + // absolute offset. So, ContentEventHandler doesn't pay any additional cost + // after calling MakeOffsetAbsolute() here. + if (aEvent->mInput.mRelativeToInsertionPoint && + aEvent->mInput.IsValidEventMessage(aEvent->mMessage)) { + RefPtr<TextComposition> composition = + IMEStateManager::GetTextCompositionFor(aEvent->mWidget); + if (composition) { + uint32_t compositionStart = composition->NativeOffsetOfStartComposition(); + if (NS_WARN_IF(!aEvent->mInput.MakeOffsetAbsolute(compositionStart))) { + return NS_ERROR_FAILURE; + } + } else if (isSelectionCacheAvailable && mSelectionData.HasRange()) { + const uint32_t selectionStart = mSelectionData.mOffset; + if (NS_WARN_IF(!aEvent->mInput.MakeOffsetAbsolute(selectionStart))) { + return NS_ERROR_FAILURE; + } + } + } + + AutoRestore<bool> handling(mIsHandlingQueryContentEvent); + mIsHandlingQueryContentEvent = true; + ContentEventHandler handler(GetPresContext()); + nsresult rv = handler.HandleQueryContentEvent(aEvent); + if (NS_WARN_IF(Destroyed())) { + // If this has already destroyed during querying the content, the query + // is outdated even if it's succeeded. So, make the query fail. + aEvent->mReply.reset(); + MOZ_LOG(sIMECOLog, LogLevel::Warning, + ("0x%p HandleQueryContentEvent(), WARNING, " + "IMEContentObserver has been destroyed during the query, " + "making the query fail", + this)); + return rv; + } + + if (aEvent->Succeeded() && + NS_WARN_IF(aEvent->mReply->mContentsRoot != mRootElement)) { + // Focus has changed unexpectedly, so make the query fail. + aEvent->mReply.reset(); + } + return rv; +} + +nsresult IMEContentObserver::MaybeHandleSelectionEvent( + nsPresContext* aPresContext, WidgetSelectionEvent* aEvent) { + MOZ_ASSERT(aEvent); + MOZ_ASSERT(aEvent->mMessage == eSetSelection); + NS_ASSERTION(!mNeedsToNotifyIMEOfSelectionChange, + "Selection cache has not been updated yet"); + + MOZ_LOG( + sIMECOLog, LogLevel::Debug, + ("0x%p MaybeHandleSelectionEvent(aEvent={ " + "mMessage=%s, mOffset=%u, mLength=%u, mReversed=%s, " + "mExpandToClusterBoundary=%s, mUseNativeLineBreak=%s }), " + "mSelectionData=%s", + this, ToChar(aEvent->mMessage), aEvent->mOffset, aEvent->mLength, + ToChar(aEvent->mReversed), ToChar(aEvent->mExpandToClusterBoundary), + ToChar(aEvent->mUseNativeLineBreak), ToString(mSelectionData).c_str())); + + // When we have Selection cache, and the caller wants to set same selection + // range, we shouldn't try to compute same range because it may be impossible + // if the range boundary is around element boundaries which won't be + // serialized with line breaks like close tags of inline elements. In that + // case, inserting new text at different point may be different from intention + // of users or web apps which set current selection. + // FIXME: We cache only selection data computed with native line breaker + // lengths. Perhaps, we should improve the struct to have both data of + // offset and length. E.g., adding line break counts for both offset and + // length. + if (!mNeedsToNotifyIMEOfSelectionChange && aEvent->mUseNativeLineBreak && + mSelectionData.IsInitialized() && mSelectionData.HasRange() && + mSelectionData.StartOffset() == aEvent->mOffset && + mSelectionData.Length() == aEvent->mLength) { + if (RefPtr<Selection> selection = mSelection) { + selection->ScrollIntoView(nsISelectionController::SELECTION_FOCUS_REGION, + ScrollAxis(), ScrollAxis(), 0); + } + aEvent->mSucceeded = true; + return NS_OK; + } + + ContentEventHandler handler(aPresContext); + return handler.OnSelectionEvent(aEvent); +} + +bool IMEContentObserver::OnMouseButtonEvent(nsPresContext& aPresContext, + WidgetMouseEvent& aMouseEvent) { + if (!mIMENotificationRequests || + !mIMENotificationRequests->WantMouseButtonEventOnChar()) { + return false; + } + if (!aMouseEvent.IsTrusted() || aMouseEvent.DefaultPrevented() || + !aMouseEvent.mWidget) { + return false; + } + // Now, we need to notify only mouse down and mouse up event. + switch (aMouseEvent.mMessage) { + case eMouseUp: + case eMouseDown: + break; + default: + return false; + } + if (NS_WARN_IF(!mWidget) || NS_WARN_IF(mWidget->Destroyed())) { + return false; + } + + WidgetQueryContentEvent queryCharAtPointEvent(true, eQueryCharacterAtPoint, + aMouseEvent.mWidget); + queryCharAtPointEvent.mRefPoint = aMouseEvent.mRefPoint; + ContentEventHandler handler(&aPresContext); + handler.OnQueryCharacterAtPoint(&queryCharAtPointEvent); + if (NS_WARN_IF(queryCharAtPointEvent.Failed()) || + queryCharAtPointEvent.DidNotFindChar()) { + return false; + } + + // The widget might be destroyed during querying the content since it + // causes flushing layout. + if (!mWidget || NS_WARN_IF(mWidget->Destroyed())) { + return false; + } + + // The result character rect is relative to the top level widget. + // We should notify it with offset in the widget. + nsIWidget* topLevelWidget = mWidget->GetTopLevelWidget(); + if (topLevelWidget && topLevelWidget != mWidget) { + queryCharAtPointEvent.mReply->mRect.MoveBy( + topLevelWidget->WidgetToScreenOffset() - + mWidget->WidgetToScreenOffset()); + } + // The refPt is relative to its widget. + // We should notify it with offset in the widget. + if (aMouseEvent.mWidget != mWidget) { + queryCharAtPointEvent.mRefPoint += + aMouseEvent.mWidget->WidgetToScreenOffset() - + mWidget->WidgetToScreenOffset(); + } + + IMENotification notification(NOTIFY_IME_OF_MOUSE_BUTTON_EVENT); + notification.mMouseButtonEventData.mEventMessage = aMouseEvent.mMessage; + notification.mMouseButtonEventData.mOffset = + queryCharAtPointEvent.mReply->StartOffset(); + notification.mMouseButtonEventData.mCursorPos = + queryCharAtPointEvent.mRefPoint; + notification.mMouseButtonEventData.mCharRect = + queryCharAtPointEvent.mReply->mRect; + notification.mMouseButtonEventData.mButton = aMouseEvent.mButton; + notification.mMouseButtonEventData.mButtons = aMouseEvent.mButtons; + notification.mMouseButtonEventData.mModifiers = aMouseEvent.mModifiers; + + nsresult rv = IMEStateManager::NotifyIME(notification, mWidget); + if (NS_WARN_IF(NS_FAILED(rv))) { + return false; + } + + bool consumed = (rv == NS_SUCCESS_EVENT_CONSUMED); + if (consumed) { + aMouseEvent.PreventDefault(); + } + return consumed; +} + +void IMEContentObserver::CharacterDataWillChange( + nsIContent* aContent, const CharacterDataChangeInfo& aInfo) { + if (!aContent->IsText()) { + return; // Ignore if it's a comment node or something other invisible data + // node. + } + MOZ_ASSERT(mPreCharacterDataChangeLength < 0, + "CharacterDataChanged() should've reset " + "mPreCharacterDataChangeLength"); + + if (!NeedsTextChangeNotification() || + !nsContentUtils::IsInSameAnonymousTree(mRootElement, aContent)) { + return; + } + + mEndOfAddedTextCache.Clear(); + mStartOfRemovingTextRangeCache.Clear(); + + // Although we don't assume this change occurs while this is storing + // the range of added consecutive nodes, if it actually happens, we need to + // flush them since this change may occur before or in the range. So, it's + // safe to flush pending computation of mTextChangeData before handling this. + MaybeNotifyIMEOfAddedTextDuringDocumentChange(); + + mPreCharacterDataChangeLength = ContentEventHandler::GetNativeTextLength( + *aContent->AsText(), aInfo.mChangeStart, aInfo.mChangeEnd); + MOZ_ASSERT( + mPreCharacterDataChangeLength >= aInfo.mChangeEnd - aInfo.mChangeStart, + "The computed length must be same as or larger than XP length"); +} + +void IMEContentObserver::CharacterDataChanged( + nsIContent* aContent, const CharacterDataChangeInfo& aInfo) { + if (!aContent->IsText()) { + return; // Ignore if it's a comment node or something other invisible data + // node. + } + + // Let TextComposition have a change to update composition string range in + // the text node if the change is caused by the web apps. + if (mWidget && !IsEditorHandlingEventForComposition()) { + if (RefPtr<TextComposition> composition = + IMEStateManager::GetTextCompositionFor(mWidget)) { + composition->OnCharacterDataChanged(*aContent->AsText(), aInfo); + } + } + + if (!NeedsTextChangeNotification() || + !nsContentUtils::IsInSameAnonymousTree(mRootElement, aContent)) { + return; + } + + mEndOfAddedTextCache.Clear(); + mStartOfRemovingTextRangeCache.Clear(); + MOZ_ASSERT( + !HasAddedNodesDuringDocumentChange(), + "The stored range should be flushed before actually the data is changed"); + + int64_t removedLength = mPreCharacterDataChangeLength; + mPreCharacterDataChangeLength = -1; + + MOZ_ASSERT(removedLength >= 0, + "mPreCharacterDataChangeLength should've been set by " + "CharacterDataWillChange()"); + + uint32_t offset = 0; + // get offsets of change and fire notification + nsresult rv = ContentEventHandler::GetFlatTextLengthInRange( + NodePosition(mRootElement, 0u), + NodePosition(aContent, aInfo.mChangeStart), mRootElement, &offset, + LINE_BREAK_TYPE_NATIVE); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + uint32_t newLength = ContentEventHandler::GetNativeTextLength( + *aContent->AsText(), aInfo.mChangeStart, + aInfo.mChangeStart + aInfo.mReplaceLength); + + uint32_t oldEnd = offset + static_cast<uint32_t>(removedLength); + uint32_t newEnd = offset + newLength; + + TextChangeData data(offset, oldEnd, newEnd, + IsEditorHandlingEventForComposition(), + IsEditorComposing()); + MaybeNotifyIMEOfTextChange(data); +} + +void IMEContentObserver::NotifyContentAdded(nsINode* aContainer, + nsIContent* aFirstContent, + nsIContent* aLastContent) { + if (!NeedsTextChangeNotification() || + !nsContentUtils::IsInSameAnonymousTree(mRootElement, aFirstContent)) { + return; + } + + MOZ_ASSERT_IF(aFirstContent, aFirstContent->GetParentNode() == aContainer); + MOZ_ASSERT_IF(aLastContent, aLastContent->GetParentNode() == aContainer); + + mStartOfRemovingTextRangeCache.Clear(); + + // If it's in a document change, nodes are added consecutively. Therefore, + // if we cache the first node and the last node, we need to compute the + // range once. + // FYI: This is not true if the change caused by an operation in the editor. + if (IsInDocumentChange()) { + // Now, mEndOfAddedTextCache may be invalid if node is added before + // the last node in mEndOfAddedTextCache. Clear it. + mEndOfAddedTextCache.Clear(); + if (!HasAddedNodesDuringDocumentChange()) { + mFirstAddedContainer = mLastAddedContainer = aContainer; + mFirstAddedContent = aFirstContent; + mLastAddedContent = aLastContent; + MOZ_LOG(sIMECOLog, LogLevel::Debug, + ("0x%p NotifyContentAdded(), starts to store consecutive added " + "nodes", + this)); + return; + } + // If first node being added is not next node of the last node, + // notify IME of the previous range first, then, restart to cache the + // range. + if (NS_WARN_IF(!IsNextNodeOfLastAddedNode(aContainer, aFirstContent))) { + // Flush the old range first. + MaybeNotifyIMEOfAddedTextDuringDocumentChange(); + mFirstAddedContainer = aContainer; + mFirstAddedContent = aFirstContent; + MOZ_LOG(sIMECOLog, LogLevel::Debug, + ("0x%p NotifyContentAdded(), starts to store consecutive added " + "nodes", + this)); + } + mLastAddedContainer = aContainer; + mLastAddedContent = aLastContent; + return; + } + MOZ_ASSERT(!HasAddedNodesDuringDocumentChange(), + "The cache should be cleared when document change finished"); + + uint32_t offset = 0; + nsresult rv = NS_OK; + if (!mEndOfAddedTextCache.Match(aContainer, + aFirstContent->GetPreviousSibling())) { + mEndOfAddedTextCache.Clear(); + rv = ContentEventHandler::GetFlatTextLengthInRange( + NodePosition(mRootElement, 0u), + NodePositionBefore(aContainer, PointBefore(aContainer, aFirstContent)), + mRootElement, &offset, LINE_BREAK_TYPE_NATIVE); + if (NS_WARN_IF(NS_FAILED((rv)))) { + return; + } + } else { + offset = mEndOfAddedTextCache.mFlatTextLength; + } + + // get offset at the end of the last added node + uint32_t addingLength = 0; + rv = ContentEventHandler::GetFlatTextLengthInRange( + NodePositionBefore(aContainer, PointBefore(aContainer, aFirstContent)), + NodePosition(aContainer, aLastContent), mRootElement, &addingLength, + LINE_BREAK_TYPE_NATIVE); + if (NS_WARN_IF(NS_FAILED((rv)))) { + mEndOfAddedTextCache.Clear(); + return; + } + + // If multiple lines are being inserted in an HTML editor, next call of + // NotifyContentAdded() is for adding next node. Therefore, caching the text + // length can skip to compute the text length before the adding node and + // before of it. + mEndOfAddedTextCache.Cache(aContainer, aLastContent, offset + addingLength); + + if (!addingLength) { + return; + } + + TextChangeData data(offset, offset, offset + addingLength, + IsEditorHandlingEventForComposition(), + IsEditorComposing()); + MaybeNotifyIMEOfTextChange(data); +} + +void IMEContentObserver::ContentAppended(nsIContent* aFirstNewContent) { + nsIContent* parent = aFirstNewContent->GetParent(); + MOZ_ASSERT(parent); + NotifyContentAdded(parent, aFirstNewContent, parent->GetLastChild()); +} + +void IMEContentObserver::ContentInserted(nsIContent* aChild) { + MOZ_ASSERT(aChild); + NotifyContentAdded(aChild->GetParentNode(), aChild, aChild); +} + +void IMEContentObserver::ContentRemoved(nsIContent* aChild, + nsIContent* aPreviousSibling) { + if (!NeedsTextChangeNotification() || + !nsContentUtils::IsInSameAnonymousTree(mRootElement, aChild)) { + return; + } + + mEndOfAddedTextCache.Clear(); + MaybeNotifyIMEOfAddedTextDuringDocumentChange(); + + nsINode* containerNode = aChild->GetParentNode(); + MOZ_ASSERT(containerNode); + + uint32_t offset = 0; + nsresult rv = NS_OK; + if (!mStartOfRemovingTextRangeCache.Match(containerNode, aPreviousSibling)) { + // At removing a child node of aContainer, we need the line break caused + // by open tag of aContainer. Be careful when aPreviousSibling is nullptr. + + rv = ContentEventHandler::GetFlatTextLengthInRange( + NodePosition(mRootElement, 0u), + NodePosition(containerNode, aPreviousSibling), mRootElement, &offset, + LINE_BREAK_TYPE_NATIVE); + if (NS_WARN_IF(NS_FAILED(rv))) { + mStartOfRemovingTextRangeCache.Clear(); + return; + } + mStartOfRemovingTextRangeCache.Cache(containerNode, aPreviousSibling, + offset); + } else { + offset = mStartOfRemovingTextRangeCache.mFlatTextLength; + } + + // get offset at the end of the deleted node + uint32_t textLength = 0; + if (const Text* textNode = Text::FromNode(aChild)) { + textLength = ContentEventHandler::GetNativeTextLength(*textNode); + } else { + nsresult rv = ContentEventHandler::GetFlatTextLengthInRange( + NodePositionBefore(aChild, 0u), + NodePosition(aChild, aChild->GetChildCount()), mRootElement, + &textLength, LINE_BREAK_TYPE_NATIVE, true); + if (NS_WARN_IF(NS_FAILED(rv))) { + mStartOfRemovingTextRangeCache.Clear(); + return; + } + } + + if (!textLength) { + return; + } + + TextChangeData data(offset, offset + textLength, offset, + IsEditorHandlingEventForComposition(), + IsEditorComposing()); + MaybeNotifyIMEOfTextChange(data); +} + +void IMEContentObserver::ClearAddedNodesDuringDocumentChange() { + mFirstAddedContainer = mLastAddedContainer = nullptr; + mFirstAddedContent = mLastAddedContent = nullptr; + MOZ_LOG(sIMECOLog, LogLevel::Debug, + ("0x%p ClearAddedNodesDuringDocumentChange(), finished storing " + "consecutive nodes", + this)); +} + +bool IMEContentObserver::IsNextNodeOfLastAddedNode(nsINode* aParent, + nsIContent* aChild) const { + MOZ_ASSERT(aParent); + MOZ_ASSERT(aChild && aChild->GetParentNode() == aParent); + MOZ_ASSERT(mRootElement); + MOZ_ASSERT(HasAddedNodesDuringDocumentChange()); + + // If the parent node isn't changed, we can check that mLastAddedContent has + // aChild as its next sibling. + if (aParent == mLastAddedContainer) { + if (NS_WARN_IF(mLastAddedContent->GetNextSibling() != aChild)) { + return false; + } + return true; + } + + // If the parent node is changed, that means that the recorded last added node + // shouldn't have a sibling. + if (NS_WARN_IF(mLastAddedContent->GetNextSibling())) { + return false; + } + + // If the node is aParent is a descendant of mLastAddedContainer, + // aChild should be the first child in the new container. + if (mLastAddedContainer == aParent->GetParent()) { + if (NS_WARN_IF(aChild->GetPreviousSibling())) { + return false; + } + return true; + } + + // Otherwise, we need to check it even with slow path. + nsIContent* nextContentOfLastAddedContent = + mLastAddedContent->GetNextNode(mRootElement->GetParentNode()); + if (NS_WARN_IF(!nextContentOfLastAddedContent)) { + return false; + } + if (NS_WARN_IF(nextContentOfLastAddedContent != aChild)) { + return false; + } + return true; +} + +void IMEContentObserver::MaybeNotifyIMEOfAddedTextDuringDocumentChange() { + if (!HasAddedNodesDuringDocumentChange()) { + return; + } + + MOZ_LOG(sIMECOLog, LogLevel::Debug, + ("0x%p " + "IMEContentObserver::MaybeNotifyIMEOfAddedTextDuringDocumentChange()" + ", flushing stored consecutive nodes", + this)); + + // Notify IME of text change which is caused by added nodes now. + + // First, compute offset of start of first added node from start of the + // editor. + uint32_t offset; + nsresult rv = ContentEventHandler::GetFlatTextLengthInRange( + NodePosition(mRootElement, 0u), + NodePosition(mFirstAddedContainer, + PointBefore(mFirstAddedContainer, mFirstAddedContent)), + mRootElement, &offset, LINE_BREAK_TYPE_NATIVE); + if (NS_WARN_IF(NS_FAILED(rv))) { + ClearAddedNodesDuringDocumentChange(); + return; + } + + // Next, compute the text length of added nodes. + uint32_t length; + rv = ContentEventHandler::GetFlatTextLengthInRange( + NodePosition(mFirstAddedContainer, + PointBefore(mFirstAddedContainer, mFirstAddedContent)), + NodePosition(mLastAddedContainer, mLastAddedContent), mRootElement, + &length, LINE_BREAK_TYPE_NATIVE); + if (NS_WARN_IF(NS_FAILED(rv))) { + ClearAddedNodesDuringDocumentChange(); + return; + } + + // Finally, try to notify IME of the range. + TextChangeData data(offset, offset, offset + length, + IsEditorHandlingEventForComposition(), + IsEditorComposing()); + MaybeNotifyIMEOfTextChange(data); + ClearAddedNodesDuringDocumentChange(); +} + +void IMEContentObserver::OnTextControlValueChangedWhileNotObservable( + const nsAString& aNewValue) { + MOZ_ASSERT(mEditorBase); + MOZ_ASSERT(mEditorBase->IsTextEditor()); + uint32_t newLength = ContentEventHandler::GetNativeTextLength(aNewValue); + TextChangeData data(0, mTextControlValueLength, newLength, false, false); + MaybeNotifyIMEOfTextChange(data); +} + +void IMEContentObserver::BeginDocumentUpdate() { + MOZ_LOG(sIMECOLog, LogLevel::Debug, + ("0x%p BeginDocumentUpdate(), HasAddedNodesDuringDocumentChange()=%s", + this, ToChar(HasAddedNodesDuringDocumentChange()))); + + // If we're not in a nested document update, this will return early, + // otherwise, it will handle flusing any changes currently pending before + // entering a nested document update. + MaybeNotifyIMEOfAddedTextDuringDocumentChange(); +} + +void IMEContentObserver::EndDocumentUpdate() { + MOZ_LOG(sIMECOLog, LogLevel::Debug, + ("0x%p EndDocumentUpdate(), HasAddedNodesDuringDocumentChange()=%s", + this, ToChar(HasAddedNodesDuringDocumentChange()))); + + MaybeNotifyIMEOfAddedTextDuringDocumentChange(); +} + +void IMEContentObserver::SuppressNotifyingIME() { + mSuppressNotifications++; + + MOZ_LOG(sIMECOLog, LogLevel::Debug, + ("0x%p SuppressNotifyingIME(), mSuppressNotifications=%u", this, + mSuppressNotifications)); +} + +void IMEContentObserver::UnsuppressNotifyingIME() { + MOZ_LOG(sIMECOLog, LogLevel::Debug, + ("0x%p UnsuppressNotifyingIME(), mSuppressNotifications=%u", this, + mSuppressNotifications)); + + if (!mSuppressNotifications || --mSuppressNotifications) { + return; + } + FlushMergeableNotifications(); +} + +void IMEContentObserver::OnEditActionHandled() { + MOZ_LOG(sIMECOLog, LogLevel::Debug, ("0x%p EditAction()", this)); + + mEndOfAddedTextCache.Clear(); + mStartOfRemovingTextRangeCache.Clear(); + FlushMergeableNotifications(); +} + +void IMEContentObserver::BeforeEditAction() { + MOZ_LOG(sIMECOLog, LogLevel::Debug, ("0x%p BeforeEditAction()", this)); + + mEndOfAddedTextCache.Clear(); + mStartOfRemovingTextRangeCache.Clear(); +} + +void IMEContentObserver::CancelEditAction() { + MOZ_LOG(sIMECOLog, LogLevel::Debug, ("0x%p CancelEditAction()", this)); + + mEndOfAddedTextCache.Clear(); + mStartOfRemovingTextRangeCache.Clear(); + FlushMergeableNotifications(); +} + +void IMEContentObserver::PostFocusSetNotification() { + MOZ_LOG(sIMECOLog, LogLevel::Debug, + ("0x%p PostFocusSetNotification()", this)); + + mNeedsToNotifyIMEOfFocusSet = true; +} + +void IMEContentObserver::PostTextChangeNotification() { + MOZ_LOG(sIMECOLog, LogLevel::Debug, + ("0x%p PostTextChangeNotification(mTextChangeData=%s)", this, + ToString(mTextChangeData).c_str())); + + MOZ_ASSERT(mTextChangeData.IsValid(), + "mTextChangeData must have text change data"); + mNeedsToNotifyIMEOfTextChange = true; + // Even if the observer hasn't received selection change, selection in the + // flat text may have already been changed. For example, when previous `<p>` + // element of another `<p>` element which contains caret is removed by a DOM + // mutation, selection change event won't be fired, but selection start offset + // should be decreased by the length of removed `<p>` element. + // In such case, HandleQueryContentEvent shouldn't use the selection cache + // anymore. Therefore, we also need to post selection change notification + // too. eQuerySelectedText event may be dispatched at sending a text change + // notification. + mNeedsToNotifyIMEOfSelectionChange = true; +} + +void IMEContentObserver::PostSelectionChangeNotification() { + MOZ_LOG(sIMECOLog, LogLevel::Debug, + ("0x%p PostSelectionChangeNotification(), mSelectionData={ " + "mCausedByComposition=%s, mCausedBySelectionEvent=%s }", + this, ToChar(mSelectionData.mCausedByComposition), + ToChar(mSelectionData.mCausedBySelectionEvent))); + + mNeedsToNotifyIMEOfSelectionChange = true; +} + +void IMEContentObserver::MaybeNotifyIMEOfFocusSet() { + MOZ_LOG(sIMECOLog, LogLevel::Debug, + ("0x%p MaybeNotifyIMEOfFocusSet()", this)); + + PostFocusSetNotification(); + FlushMergeableNotifications(); +} + +void IMEContentObserver::MaybeNotifyIMEOfTextChange( + const TextChangeDataBase& aTextChangeData) { + MOZ_LOG(sIMECOLog, LogLevel::Debug, + ("0x%p MaybeNotifyIMEOfTextChange(aTextChangeData=%s)", this, + ToString(aTextChangeData).c_str())); + + if (mEditorBase && mEditorBase->IsTextEditor()) { + MOZ_DIAGNOSTIC_ASSERT(static_cast<int64_t>(mTextControlValueLength) + + aTextChangeData.Difference() >= + 0); + mTextControlValueLength += aTextChangeData.Difference(); + } + + mTextChangeData += aTextChangeData; + PostTextChangeNotification(); + FlushMergeableNotifications(); +} + +void IMEContentObserver::CancelNotifyingIMEOfTextChange() { + MOZ_LOG(sIMECOLog, LogLevel::Debug, + ("0x%p CancelNotifyingIMEOfTextChange()", this)); + mTextChangeData.Clear(); + mNeedsToNotifyIMEOfTextChange = false; +} + +void IMEContentObserver::MaybeNotifyIMEOfSelectionChange( + bool aCausedByComposition, bool aCausedBySelectionEvent, + bool aOccurredDuringComposition) { + MOZ_LOG( + sIMECOLog, LogLevel::Debug, + ("0x%p MaybeNotifyIMEOfSelectionChange(aCausedByComposition=%s, " + "aCausedBySelectionEvent=%s, aOccurredDuringComposition)", + this, ToChar(aCausedByComposition), ToChar(aCausedBySelectionEvent))); + + mSelectionData.AssignReason(aCausedByComposition, aCausedBySelectionEvent, + aOccurredDuringComposition); + PostSelectionChangeNotification(); + FlushMergeableNotifications(); +} + +void IMEContentObserver::MaybeNotifyIMEOfPositionChange() { + MOZ_LOG(sIMECOLog, LogLevel::Verbose, + ("0x%p MaybeNotifyIMEOfPositionChange()", this)); + // If reflow is caused by ContentEventHandler during PositionChangeEvent + // sending NOTIFY_IME_OF_POSITION_CHANGE, we don't need to notify IME of it + // again since ContentEventHandler returns the result including this reflow's + // result. + if (mIsHandlingQueryContentEvent && + mSendingNotification == NOTIFY_IME_OF_POSITION_CHANGE) { + MOZ_LOG(sIMECOLog, LogLevel::Verbose, + ("0x%p MaybeNotifyIMEOfPositionChange(), ignored since caused by " + "ContentEventHandler during sending NOTIFY_IME_OF_POSITION_CHANGE", + this)); + return; + } + PostPositionChangeNotification(); + FlushMergeableNotifications(); +} + +void IMEContentObserver::CancelNotifyingIMEOfPositionChange() { + MOZ_LOG(sIMECOLog, LogLevel::Debug, + ("0x%p CancelNotifyIMEOfPositionChange()", this)); + mNeedsToNotifyIMEOfPositionChange = false; +} + +void IMEContentObserver::MaybeNotifyCompositionEventHandled() { + MOZ_LOG(sIMECOLog, LogLevel::Debug, + ("0x%p MaybeNotifyCompositionEventHandled()", this)); + + PostCompositionEventHandledNotification(); + FlushMergeableNotifications(); +} + +bool IMEContentObserver::UpdateSelectionCache(bool aRequireFlush /* = true */) { + MOZ_ASSERT(IsSafeToNotifyIME()); + + mSelectionData.ClearSelectionData(); + + // XXX Cannot we cache some information for reducing the cost to compute + // selection offset and writing mode? + WidgetQueryContentEvent querySelectedTextEvent(true, eQuerySelectedText, + mWidget); + querySelectedTextEvent.mNeedsToFlushLayout = aRequireFlush; + ContentEventHandler handler(GetPresContext()); + handler.OnQuerySelectedText(&querySelectedTextEvent); + if (NS_WARN_IF(querySelectedTextEvent.Failed()) || + NS_WARN_IF(querySelectedTextEvent.mReply->mContentsRoot != + mRootElement)) { + return false; + } + + mFocusedWidget = querySelectedTextEvent.mReply->mFocusedWidget; + mSelectionData.Assign(querySelectedTextEvent); + + // WARNING: Don't set the reason of selection change here because it should be + // set the reason at sending the notification. + + MOZ_LOG(sIMECOLog, LogLevel::Debug, + ("0x%p UpdateSelectionCache(), mSelectionData=%s", this, + ToString(mSelectionData).c_str())); + + return true; +} + +void IMEContentObserver::PostPositionChangeNotification() { + MOZ_LOG(sIMECOLog, LogLevel::Debug, + ("0x%p PostPositionChangeNotification()", this)); + + mNeedsToNotifyIMEOfPositionChange = true; +} + +void IMEContentObserver::PostCompositionEventHandledNotification() { + MOZ_LOG(sIMECOLog, LogLevel::Debug, + ("0x%p PostCompositionEventHandledNotification()", this)); + + mNeedsToNotifyIMEOfCompositionEventHandled = true; +} + +bool IMEContentObserver::IsReflowLocked() const { + nsPresContext* presContext = GetPresContext(); + if (NS_WARN_IF(!presContext)) { + return false; + } + PresShell* presShell = presContext->GetPresShell(); + if (NS_WARN_IF(!presShell)) { + return false; + } + // During reflow, we shouldn't notify IME because IME may query content + // synchronously. Then, it causes ContentEventHandler will try to flush + // pending notifications during reflow. + return presShell->IsReflowLocked(); +} + +bool IMEContentObserver::IsSafeToNotifyIME() const { + // If this is already detached from the widget, this doesn't need to notify + // anything. + if (!mWidget) { + MOZ_LOG(sIMECOLog, LogLevel::Debug, + ("0x%p IsSafeToNotifyIME(), it's not safe because of no widget", + this)); + return false; + } + + // Don't notify IME of anything if it's not good time to do it. + if (mSuppressNotifications) { + MOZ_LOG(sIMECOLog, LogLevel::Debug, + ("0x%p IsSafeToNotifyIME(), it's not safe because of no widget", + this)); + return false; + } + + if (!mESM || NS_WARN_IF(!GetPresContext())) { + MOZ_LOG(sIMECOLog, LogLevel::Debug, + ("0x%p IsSafeToNotifyIME(), it's not safe because of no " + "EventStateManager and/or PresContext", + this)); + return false; + } + + // If it's in reflow, we should wait to finish the reflow. + // FYI: This should be called again from Reflow() or ReflowInterruptible(). + if (IsReflowLocked()) { + MOZ_LOG( + sIMECOLog, LogLevel::Debug, + ("0x%p IsSafeToNotifyIME(), it's not safe because of reflow locked", + this)); + return false; + } + + // If we're in handling an edit action, this method will be called later. + if (mEditorBase && mEditorBase->IsInEditSubAction()) { + MOZ_LOG(sIMECOLog, LogLevel::Debug, + ("0x%p IsSafeToNotifyIME(), it's not safe because of focused " + "editor handling somethings", + this)); + return false; + } + + return true; +} + +void IMEContentObserver::FlushMergeableNotifications() { + if (!IsSafeToNotifyIME()) { + // So, if this is already called, this should do nothing. + MOZ_LOG(sIMECOLog, LogLevel::Warning, + ("0x%p FlushMergeableNotifications(), Warning, do nothing due to " + "unsafe to notify IME", + this)); + return; + } + + // Notifying something may cause nested call of this method. For example, + // when somebody notified one of the notifications may dispatch query content + // event. Then, it causes flushing layout which may cause another layout + // change notification. + + if (mQueuedSender) { + // So, if this is already called, this should do nothing. + MOZ_LOG(sIMECOLog, LogLevel::Warning, + ("0x%p FlushMergeableNotifications(), Warning, do nothing due to " + "already flushing pending notifications", + this)); + return; + } + + // If text change notification and/or position change notification becomes + // unnecessary, let's cancel them. + if (mNeedsToNotifyIMEOfTextChange && !NeedsTextChangeNotification()) { + CancelNotifyingIMEOfTextChange(); + } + if (mNeedsToNotifyIMEOfPositionChange && !NeedsPositionChangeNotification()) { + CancelNotifyingIMEOfPositionChange(); + } + + if (!NeedsToNotifyIMEOfSomething()) { + MOZ_LOG(sIMECOLog, LogLevel::Warning, + ("0x%p FlushMergeableNotifications(), Warning, due to no pending " + "notifications", + this)); + return; + } + + // NOTE: Reset each pending flag because sending notification may cause + // another change. + + MOZ_LOG( + sIMECOLog, LogLevel::Info, + ("0x%p FlushMergeableNotifications(), creating IMENotificationSender...", + this)); + + // If contents in selection range is modified, the selection range still + // has removed node from the tree. In such case, ContentIterator won't + // work well. Therefore, we shouldn't use AddScriptRunnder() here since + // it may kick runnable event immediately after DOM tree is changed but + // the selection range isn't modified yet. + mQueuedSender = new IMENotificationSender(this); + mQueuedSender->Dispatch(mDocShell); + MOZ_LOG(sIMECOLog, LogLevel::Debug, + ("0x%p FlushMergeableNotifications(), finished", this)); +} + +void IMEContentObserver::TryToFlushPendingNotifications(bool aAllowAsync) { + // If a sender instance is sending notifications, we shouldn't try to create + // a new sender again because the sender will recreate by itself if there are + // new pending notifications. + if (mSendingNotification != NOTIFY_IME_OF_NOTHING) { + return; + } + + // When the caller allows to put off notifying IME, we can wait the next + // call of this method or to run the queued sender. + if (mQueuedSender && XRE_IsContentProcess() && aAllowAsync) { + return; + } + + if (!mQueuedSender) { + // If it was not safe to dispatch notifications when the pending + // notifications are posted, this may not have IMENotificationSender + // instance because it couldn't dispatch it, e.g., when an edit sub-action + // is being handled in the editor, we shouldn't do it even if it's safe to + // run script. Therefore, we need to create the sender instance here in the + // case. + if (!NeedsToNotifyIMEOfSomething()) { + return; + } + mQueuedSender = new IMENotificationSender(this); + } + + MOZ_LOG(sIMECOLog, LogLevel::Debug, + ("0x%p TryToFlushPendingNotifications(), performing queued " + "IMENotificationSender forcibly", + this)); + RefPtr<IMENotificationSender> queuedSender = mQueuedSender; + queuedSender->Run(); +} + +/****************************************************************************** + * mozilla::IMEContentObserver::AChangeEvent + ******************************************************************************/ + +bool IMEContentObserver::AChangeEvent::CanNotifyIME( + ChangeEventType aChangeEventType) const { + RefPtr<IMEContentObserver> observer = GetObserver(); + if (NS_WARN_IF(!observer)) { + return false; + } + + const LogLevel debugOrVerbose = + aChangeEventType == ChangeEventType::eChangeEventType_Position + ? LogLevel::Verbose + : LogLevel::Debug; + + if (aChangeEventType == eChangeEventType_CompositionEventHandled) { + if (observer->mWidget) { + return true; + } + MOZ_LOG(sIMECOLog, debugOrVerbose, + ("0x%p AChangeEvent::CanNotifyIME(), Cannot notify IME of " + "composition event handled because of no widget", + this)); + return false; + } + State state = observer->GetState(); + // If it's not initialized, we should do nothing. + if (state == eState_NotObserving) { + MOZ_LOG(sIMECOLog, debugOrVerbose, + ("0x%p AChangeEvent::CanNotifyIME(), Cannot notify IME because " + "of not observing", + this)); + return false; + } + // If setting focus, just check the state. + if (aChangeEventType == eChangeEventType_Focus) { + if (!observer->mIMEHasFocus) { + return true; + } + MOZ_LOG(sIMECOLog, debugOrVerbose, + ("0x%p AChangeEvent::CanNotifyIME(), Cannot notify IME of focus " + "change because of already focused", + this)); + NS_WARNING("IME already has focus"); + return false; + } + // If we've not notified IME of focus yet, we shouldn't notify anything. + if (!observer->mIMEHasFocus) { + MOZ_LOG(sIMECOLog, debugOrVerbose, + ("0x%p AChangeEvent::CanNotifyIME(), Cannot notify IME because " + "of not focused", + this)); + return false; + } + + // If IME has focus, IMEContentObserver must hold the widget. + MOZ_ASSERT(observer->mWidget); + + return true; +} + +bool IMEContentObserver::AChangeEvent::IsSafeToNotifyIME( + ChangeEventType aChangeEventType) const { + const LogLevel warningOrVerbose = + aChangeEventType == ChangeEventType::eChangeEventType_Position + ? LogLevel::Verbose + : LogLevel::Warning; + + if (NS_WARN_IF(!nsContentUtils::IsSafeToRunScript())) { + MOZ_LOG(sIMECOLog, warningOrVerbose, + ("0x%p AChangeEvent::IsSafeToNotifyIME(), Warning, Cannot notify " + "IME because of not safe to run script", + this)); + return false; + } + + RefPtr<IMEContentObserver> observer = GetObserver(); + if (!observer) { + MOZ_LOG(sIMECOLog, warningOrVerbose, + ("0x%p AChangeEvent::IsSafeToNotifyIME(), Warning, Cannot notify " + "IME because of no observer", + this)); + return false; + } + + // While we're sending a notification, we shouldn't send another notification + // recursively. + if (observer->mSendingNotification != NOTIFY_IME_OF_NOTHING) { + MOZ_LOG(sIMECOLog, warningOrVerbose, + ("0x%p AChangeEvent::IsSafeToNotifyIME(), Warning, Cannot notify " + "IME because of the observer sending another notification", + this)); + return false; + } + State state = observer->GetState(); + if (aChangeEventType == eChangeEventType_Focus) { + if (NS_WARN_IF(state != eState_Initializing && state != eState_Observing)) { + MOZ_LOG(sIMECOLog, warningOrVerbose, + ("0x%p AChangeEvent::IsSafeToNotifyIME(), Warning, Cannot " + "notify IME of focus because of not observing", + this)); + return false; + } + } else if (aChangeEventType == eChangeEventType_CompositionEventHandled) { + // It doesn't need to check the observing status. + } else if (state != eState_Observing) { + MOZ_LOG(sIMECOLog, warningOrVerbose, + ("0x%p AChangeEvent::IsSafeToNotifyIME(), Warning, Cannot notify " + "IME because of not observing", + this)); + return false; + } + return observer->IsSafeToNotifyIME(); +} + +/****************************************************************************** + * mozilla::IMEContentObserver::IMENotificationSender + ******************************************************************************/ + +void IMEContentObserver::IMENotificationSender::Dispatch( + nsIDocShell* aDocShell) { + if (XRE_IsContentProcess() && aDocShell) { + RefPtr<nsPresContext> presContext = aDocShell->GetPresContext(); + if (presContext) { + nsRefreshDriver* refreshDriver = presContext->RefreshDriver(); + if (refreshDriver) { + refreshDriver->AddEarlyRunner(this); + return; + } + } + } + + nsIScriptGlobalObject* globalObject = + aDocShell ? aDocShell->GetScriptGlobalObject() : nullptr; + if (globalObject) { + RefPtr<IMENotificationSender> queuedSender = this; + globalObject->Dispatch(TaskCategory::Other, queuedSender.forget()); + } else { + NS_DispatchToCurrentThread(this); + } +} + +NS_IMETHODIMP +IMEContentObserver::IMENotificationSender::Run() { + if (NS_WARN_IF(mIsRunning)) { + MOZ_LOG( + sIMECOLog, LogLevel::Error, + ("0x%p IMENotificationSender::Run(), FAILED, due to called recursively", + this)); + return NS_OK; + } + + RefPtr<IMEContentObserver> observer = GetObserver(); + if (!observer) { + return NS_OK; + } + + AutoRestore<bool> running(mIsRunning); + mIsRunning = true; + + // This instance was already performed forcibly. + if (observer->mQueuedSender != this) { + return NS_OK; + } + + // NOTE: Reset each pending flag because sending notification may cause + // another change. + + if (observer->mNeedsToNotifyIMEOfFocusSet) { + observer->mNeedsToNotifyIMEOfFocusSet = false; + SendFocusSet(); + observer->mQueuedSender = nullptr; + // If it's not safe to notify IME of focus, SendFocusSet() sets + // mNeedsToNotifyIMEOfFocusSet true again. For guaranteeing to send the + // focus notification later, we should put a new sender into the queue but + // this case must be rare. Note that if mIMEContentObserver is already + // destroyed, mNeedsToNotifyIMEOfFocusSet is never set true again. + if (observer->mNeedsToNotifyIMEOfFocusSet) { + MOZ_ASSERT(!observer->mIMEHasFocus); + MOZ_LOG(sIMECOLog, LogLevel::Debug, + ("0x%p IMENotificationSender::Run(), posting " + "IMENotificationSender to current thread", + this)); + observer->mQueuedSender = new IMENotificationSender(observer); + observer->mQueuedSender->Dispatch(observer->mDocShell); + return NS_OK; + } + // This is the first notification to IME. So, we don't need to notify + // anymore since IME starts to query content after it gets focus. + observer->ClearPendingNotifications(); + return NS_OK; + } + + if (observer->mNeedsToNotifyIMEOfTextChange) { + observer->mNeedsToNotifyIMEOfTextChange = false; + SendTextChange(); + } + + // If a text change notification causes another text change again, we should + // notify IME of that before sending a selection change notification. + if (!observer->mNeedsToNotifyIMEOfTextChange) { + // Be aware, PuppetWidget depends on the order of this. A selection change + // notification should not be sent before a text change notification because + // PuppetWidget shouldn't query new text content every selection change. + if (observer->mNeedsToNotifyIMEOfSelectionChange) { + observer->mNeedsToNotifyIMEOfSelectionChange = false; + SendSelectionChange(); + } + } + + // If a text change notification causes another text change again or a + // selection change notification causes either a text change or another + // selection change, we should notify IME of those before sending a position + // change notification. + if (!observer->mNeedsToNotifyIMEOfTextChange && + !observer->mNeedsToNotifyIMEOfSelectionChange) { + if (observer->mNeedsToNotifyIMEOfPositionChange) { + observer->mNeedsToNotifyIMEOfPositionChange = false; + SendPositionChange(); + } + } + + // Composition event handled notification should be sent after all the + // other notifications because this notifies widget of finishing all pending + // events are handled completely. + if (!observer->mNeedsToNotifyIMEOfTextChange && + !observer->mNeedsToNotifyIMEOfSelectionChange && + !observer->mNeedsToNotifyIMEOfPositionChange) { + if (observer->mNeedsToNotifyIMEOfCompositionEventHandled) { + observer->mNeedsToNotifyIMEOfCompositionEventHandled = false; + SendCompositionEventHandled(); + } + } + + observer->mQueuedSender = nullptr; + + // If notifications caused some new change, we should notify them now. + if (observer->NeedsToNotifyIMEOfSomething()) { + if (observer->GetState() == eState_StoppedObserving) { + MOZ_LOG(sIMECOLog, LogLevel::Debug, + ("0x%p IMENotificationSender::Run(), waiting " + "IMENotificationSender to be reinitialized", + this)); + } else { + MOZ_LOG(sIMECOLog, LogLevel::Debug, + ("0x%p IMENotificationSender::Run(), posting " + "IMENotificationSender to current thread", + this)); + observer->mQueuedSender = new IMENotificationSender(observer); + observer->mQueuedSender->Dispatch(observer->mDocShell); + } + } + return NS_OK; +} + +void IMEContentObserver::IMENotificationSender::SendFocusSet() { + RefPtr<IMEContentObserver> observer = GetObserver(); + if (!observer) { + return; + } + + if (!CanNotifyIME(eChangeEventType_Focus)) { + // If IMEContentObserver has already gone, we don't need to notify IME of + // focus. + MOZ_LOG(sIMECOLog, LogLevel::Warning, + ("0x%p IMENotificationSender::SendFocusSet(), Warning, does not " + "send notification due to impossible to notify IME of focus", + this)); + observer->ClearPendingNotifications(); + return; + } + + if (!IsSafeToNotifyIME(eChangeEventType_Focus)) { + MOZ_LOG( + sIMECOLog, LogLevel::Warning, + ("0x%p IMENotificationSender::SendFocusSet(), Warning, does not send " + "notification due to unsafe, retrying to send NOTIFY_IME_OF_FOCUS...", + this)); + observer->PostFocusSetNotification(); + return; + } + + observer->mIMEHasFocus = true; + // Initialize selection cache with the first selection data. +#ifdef XP_MACOSX + // We need to flush layout only on macOS because character coordinates are + // cached by cocoa with this call, but we don't have a way to update them + // after that. Therefore, we need the latest layout information right now. + observer->UpdateSelectionCache(true); +#else + // We avoid flushing for focus in the general case. + observer->UpdateSelectionCache(false); +#endif // #ifdef XP_MACOSX #else + MOZ_LOG(sIMECOLog, LogLevel::Info, + ("0x%p IMENotificationSender::SendFocusSet(), sending " + "NOTIFY_IME_OF_FOCUS...", + this)); + + MOZ_RELEASE_ASSERT(observer->mSendingNotification == NOTIFY_IME_OF_NOTHING); + observer->mSendingNotification = NOTIFY_IME_OF_FOCUS; + IMEStateManager::NotifyIME(IMENotification(NOTIFY_IME_OF_FOCUS), + observer->mWidget); + observer->mSendingNotification = NOTIFY_IME_OF_NOTHING; + + // IMENotificationRequests referred by ObserveEditableNode() may be different + // before or after widget receives NOTIFY_IME_OF_FOCUS. Therefore, we need + // to guarantee to call ObserveEditableNode() after sending + // NOTIFY_IME_OF_FOCUS. + observer->OnIMEReceivedFocus(); + + MOZ_LOG( + sIMECOLog, LogLevel::Debug, + ("0x%p IMENotificationSender::SendFocusSet(), sent NOTIFY_IME_OF_FOCUS", + this)); +} + +void IMEContentObserver::IMENotificationSender::SendSelectionChange() { + RefPtr<IMEContentObserver> observer = GetObserver(); + if (!observer) { + return; + } + + if (!CanNotifyIME(eChangeEventType_Selection)) { + MOZ_LOG(sIMECOLog, LogLevel::Warning, + ("0x%p IMENotificationSender::SendSelectionChange(), Warning, " + "does not send notification due to impossible to notify IME of " + "selection change", + this)); + return; + } + + if (!IsSafeToNotifyIME(eChangeEventType_Selection)) { + MOZ_LOG(sIMECOLog, LogLevel::Warning, + ("0x%p IMENotificationSender::SendSelectionChange(), Warning, " + "does not send notification due to unsafe, retrying to send " + "NOTIFY_IME_OF_SELECTION_CHANGE...", + this)); + observer->PostSelectionChangeNotification(); + return; + } + + SelectionChangeData lastSelChangeData = observer->mSelectionData; + if (NS_WARN_IF(!observer->UpdateSelectionCache())) { + MOZ_LOG(sIMECOLog, LogLevel::Error, + ("0x%p IMENotificationSender::SendSelectionChange(), FAILED, due " + "to UpdateSelectionCache() failure", + this)); + return; + } + + // The state may be changed since querying content causes flushing layout. + if (!CanNotifyIME(eChangeEventType_Selection)) { + MOZ_LOG(sIMECOLog, LogLevel::Error, + ("0x%p IMENotificationSender::SendSelectionChange(), FAILED, due " + "to flushing layout having changed something", + this)); + return; + } + + // If the selection isn't changed actually, we shouldn't notify IME of + // selection change. + SelectionChangeData& newSelChangeData = observer->mSelectionData; + if (lastSelChangeData.IsInitialized() && + lastSelChangeData.EqualsRangeAndDirectionAndWritingMode( + newSelChangeData)) { + MOZ_LOG( + sIMECOLog, LogLevel::Debug, + ("0x%p IMENotificationSender::SendSelectionChange(), not notifying IME " + "of NOTIFY_IME_OF_SELECTION_CHANGE due to not changed actually", + this)); + return; + } + + MOZ_LOG(sIMECOLog, LogLevel::Info, + ("0x%p IMENotificationSender::SendSelectionChange(), sending " + "NOTIFY_IME_OF_SELECTION_CHANGE... newSelChangeData=%s", + this, ToString(newSelChangeData).c_str())); + + IMENotification notification(NOTIFY_IME_OF_SELECTION_CHANGE); + notification.SetData(observer->mSelectionData); + + MOZ_RELEASE_ASSERT(observer->mSendingNotification == NOTIFY_IME_OF_NOTHING); + observer->mSendingNotification = NOTIFY_IME_OF_SELECTION_CHANGE; + IMEStateManager::NotifyIME(notification, observer->mWidget); + observer->mSendingNotification = NOTIFY_IME_OF_NOTHING; + + MOZ_LOG(sIMECOLog, LogLevel::Debug, + ("0x%p IMENotificationSender::SendSelectionChange(), sent " + "NOTIFY_IME_OF_SELECTION_CHANGE", + this)); +} + +void IMEContentObserver::IMENotificationSender::SendTextChange() { + RefPtr<IMEContentObserver> observer = GetObserver(); + if (!observer) { + return; + } + + if (!CanNotifyIME(eChangeEventType_Text)) { + MOZ_LOG( + sIMECOLog, LogLevel::Warning, + ("0x%p IMENotificationSender::SendTextChange(), Warning, does not " + "send notification due to impossible to notify IME of text change", + this)); + return; + } + + if (!IsSafeToNotifyIME(eChangeEventType_Text)) { + MOZ_LOG(sIMECOLog, LogLevel::Warning, + ("0x%p IMENotificationSender::SendTextChange(), Warning, does " + "not send notification due to unsafe, retrying to send " + "NOTIFY_IME_OF_TEXT_CHANGE...", + this)); + observer->PostTextChangeNotification(); + return; + } + + // If text change notification is unnecessary anymore, just cancel it. + if (!observer->NeedsTextChangeNotification()) { + MOZ_LOG(sIMECOLog, LogLevel::Warning, + ("0x%p IMENotificationSender::SendTextChange(), Warning, " + "canceling sending NOTIFY_IME_OF_TEXT_CHANGE", + this)); + observer->CancelNotifyingIMEOfTextChange(); + return; + } + + MOZ_LOG(sIMECOLog, LogLevel::Info, + ("0x%p IMENotificationSender::SendTextChange(), sending " + "NOTIFY_IME_OF_TEXT_CHANGE... mIMEContentObserver={ " + "mTextChangeData=%s }", + this, ToString(observer->mTextChangeData).c_str())); + + IMENotification notification(NOTIFY_IME_OF_TEXT_CHANGE); + notification.SetData(observer->mTextChangeData); + observer->mTextChangeData.Clear(); + + MOZ_RELEASE_ASSERT(observer->mSendingNotification == NOTIFY_IME_OF_NOTHING); + observer->mSendingNotification = NOTIFY_IME_OF_TEXT_CHANGE; + IMEStateManager::NotifyIME(notification, observer->mWidget); + observer->mSendingNotification = NOTIFY_IME_OF_NOTHING; + + MOZ_LOG(sIMECOLog, LogLevel::Debug, + ("0x%p IMENotificationSender::SendTextChange(), sent " + "NOTIFY_IME_OF_TEXT_CHANGE", + this)); +} + +void IMEContentObserver::IMENotificationSender::SendPositionChange() { + RefPtr<IMEContentObserver> observer = GetObserver(); + if (!observer) { + return; + } + + if (!CanNotifyIME(eChangeEventType_Position)) { + MOZ_LOG(sIMECOLog, LogLevel::Verbose, + ("0x%p IMENotificationSender::SendPositionChange(), Warning, " + "does not send notification due to impossible to notify IME of " + "position change", + this)); + return; + } + + if (!IsSafeToNotifyIME(eChangeEventType_Position)) { + MOZ_LOG(sIMECOLog, LogLevel::Verbose, + ("0x%p IMENotificationSender::SendPositionChange(), Warning, " + "does not send notification due to unsafe, retrying to send " + "NOTIFY_IME_OF_POSITION_CHANGE...", + this)); + observer->PostPositionChangeNotification(); + return; + } + + // If position change notification is unnecessary anymore, just cancel it. + if (!observer->NeedsPositionChangeNotification()) { + MOZ_LOG(sIMECOLog, LogLevel::Verbose, + ("0x%p IMENotificationSender::SendPositionChange(), Warning, " + "canceling sending NOTIFY_IME_OF_POSITION_CHANGE", + this)); + observer->CancelNotifyingIMEOfPositionChange(); + return; + } + + MOZ_LOG(sIMECOLog, LogLevel::Info, + ("0x%p IMENotificationSender::SendPositionChange(), sending " + "NOTIFY_IME_OF_POSITION_CHANGE...", + this)); + + MOZ_RELEASE_ASSERT(observer->mSendingNotification == NOTIFY_IME_OF_NOTHING); + observer->mSendingNotification = NOTIFY_IME_OF_POSITION_CHANGE; + IMEStateManager::NotifyIME(IMENotification(NOTIFY_IME_OF_POSITION_CHANGE), + observer->mWidget); + observer->mSendingNotification = NOTIFY_IME_OF_NOTHING; + + MOZ_LOG(sIMECOLog, LogLevel::Debug, + ("0x%p IMENotificationSender::SendPositionChange(), sent " + "NOTIFY_IME_OF_POSITION_CHANGE", + this)); +} + +void IMEContentObserver::IMENotificationSender::SendCompositionEventHandled() { + RefPtr<IMEContentObserver> observer = GetObserver(); + if (!observer) { + return; + } + + if (!CanNotifyIME(eChangeEventType_CompositionEventHandled)) { + MOZ_LOG(sIMECOLog, LogLevel::Warning, + ("0x%p IMENotificationSender::SendCompositionEventHandled(), " + "Warning, does not send notification due to impossible to notify " + "IME of composition event handled", + this)); + return; + } + + if (!IsSafeToNotifyIME(eChangeEventType_CompositionEventHandled)) { + MOZ_LOG(sIMECOLog, LogLevel::Warning, + ("0x%p IMENotificationSender::SendCompositionEventHandled(), " + "Warning, does not send notification due to unsafe, retrying to " + "send NOTIFY_IME_OF_POSITION_CHANGE...", + this)); + observer->PostCompositionEventHandledNotification(); + return; + } + + MOZ_LOG(sIMECOLog, LogLevel::Info, + ("0x%p IMENotificationSender::SendCompositionEventHandled(), sending " + "NOTIFY_IME_OF_COMPOSITION_EVENT_HANDLED...", + this)); + + MOZ_RELEASE_ASSERT(observer->mSendingNotification == NOTIFY_IME_OF_NOTHING); + observer->mSendingNotification = NOTIFY_IME_OF_COMPOSITION_EVENT_HANDLED; + IMEStateManager::NotifyIME( + IMENotification(NOTIFY_IME_OF_COMPOSITION_EVENT_HANDLED), + observer->mWidget); + observer->mSendingNotification = NOTIFY_IME_OF_NOTHING; + + MOZ_LOG(sIMECOLog, LogLevel::Debug, + ("0x%p IMENotificationSender::SendCompositionEventHandled(), sent " + "NOTIFY_IME_OF_COMPOSITION_EVENT_HANDLED", + this)); +} + +/****************************************************************************** + * mozilla::IMEContentObserver::DocumentObservingHelper + ******************************************************************************/ + +NS_IMPL_CYCLE_COLLECTION_CLASS(IMEContentObserver::DocumentObserver) + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(IMEContentObserver::DocumentObserver) + // StopObserving() releases mIMEContentObserver and mDocument. + tmp->StopObserving(); +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(IMEContentObserver::DocumentObserver) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mIMEContentObserver) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDocument) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(IMEContentObserver::DocumentObserver) + NS_INTERFACE_MAP_ENTRY(nsIDocumentObserver) + NS_INTERFACE_MAP_ENTRY(nsIMutationObserver) + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTING_ADDREF(IMEContentObserver::DocumentObserver) +NS_IMPL_CYCLE_COLLECTING_RELEASE(IMEContentObserver::DocumentObserver) + +void IMEContentObserver::DocumentObserver::Observe(Document* aDocument) { + MOZ_ASSERT(aDocument); + + // Guarantee that aDocument won't be destroyed during a call of + // StopObserving(). + RefPtr<Document> newDocument = aDocument; + + StopObserving(); + + mDocument = std::move(newDocument); + mDocument->AddObserver(this); +} + +void IMEContentObserver::DocumentObserver::StopObserving() { + if (!IsObserving()) { + return; + } + + // Grab IMEContentObserver which could be destroyed during method calls. + RefPtr<IMEContentObserver> observer = std::move(mIMEContentObserver); + + // Stop observing the document first. + RefPtr<Document> document = std::move(mDocument); + document->RemoveObserver(this); + + // Notify IMEContentObserver of ending of document updates if this already + // notified it of beginning of document updates. + for (; IsUpdating(); --mDocumentUpdating) { + // FYI: IsUpdating() returns true until mDocumentUpdating becomes 0. + // However, IsObserving() returns false now because mDocument was + // already cleared above. Therefore, this method won't be called + // recursively. + observer->EndDocumentUpdate(); + } +} + +void IMEContentObserver::DocumentObserver::Destroy() { + StopObserving(); + mIMEContentObserver = nullptr; +} + +void IMEContentObserver::DocumentObserver::BeginUpdate(Document* aDocument) { + if (NS_WARN_IF(Destroyed()) || NS_WARN_IF(!IsObserving())) { + return; + } + mDocumentUpdating++; + mIMEContentObserver->BeginDocumentUpdate(); +} + +void IMEContentObserver::DocumentObserver::EndUpdate(Document* aDocument) { + if (NS_WARN_IF(Destroyed()) || NS_WARN_IF(!IsObserving()) || + NS_WARN_IF(!IsUpdating())) { + return; + } + mDocumentUpdating--; + mIMEContentObserver->EndDocumentUpdate(); +} + +} // namespace mozilla diff --git a/dom/events/IMEContentObserver.h b/dom/events/IMEContentObserver.h new file mode 100644 index 0000000000..83822168b9 --- /dev/null +++ b/dom/events/IMEContentObserver.h @@ -0,0 +1,538 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#ifndef mozilla_IMEContentObserver_h +#define mozilla_IMEContentObserver_h + +#include "mozilla/Attributes.h" +#include "mozilla/EditorBase.h" +#include "mozilla/dom/Element.h" +#include "mozilla/dom/Selection.h" +#include "nsCOMPtr.h" +#include "nsCycleCollectionParticipant.h" +#include "nsIDocShell.h" // XXX Why does only this need to be included here? +#include "nsIReflowObserver.h" +#include "nsIScrollObserver.h" +#include "nsIWidget.h" +#include "nsStubDocumentObserver.h" +#include "nsStubMutationObserver.h" +#include "nsThreadUtils.h" +#include "nsWeakReference.h" + +class nsIContent; +class nsINode; +class nsPresContext; + +namespace mozilla { + +class EventStateManager; +class TextComposition; + +namespace dom { +class Selection; +} // namespace dom + +// IMEContentObserver notifies widget of any text and selection changes +// in the currently focused editor +class IMEContentObserver final : public nsStubMutationObserver, + public nsIReflowObserver, + public nsIScrollObserver, + public nsSupportsWeakReference { + public: + using SelectionChangeData = widget::IMENotification::SelectionChangeData; + using TextChangeData = widget::IMENotification::TextChangeData; + using TextChangeDataBase = widget::IMENotification::TextChangeDataBase; + using IMENotificationRequests = widget::IMENotificationRequests; + using IMEMessage = widget::IMEMessage; + + IMEContentObserver(); + + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_CLASS_AMBIGUOUS(IMEContentObserver, + nsIReflowObserver) + NS_DECL_NSIMUTATIONOBSERVER_CHARACTERDATAWILLCHANGE + NS_DECL_NSIMUTATIONOBSERVER_CHARACTERDATACHANGED + NS_DECL_NSIMUTATIONOBSERVER_CONTENTAPPENDED + NS_DECL_NSIMUTATIONOBSERVER_CONTENTINSERTED + NS_DECL_NSIMUTATIONOBSERVER_CONTENTREMOVED + NS_DECL_NSIREFLOWOBSERVER + + // nsIScrollObserver + virtual void ScrollPositionChanged() override; + + /** + * OnSelectionChange() is called when selection is changed in the editor. + */ + void OnSelectionChange(dom::Selection& aSelection); + + MOZ_CAN_RUN_SCRIPT bool OnMouseButtonEvent(nsPresContext& aPresContext, + WidgetMouseEvent& aMouseEvent); + + MOZ_CAN_RUN_SCRIPT nsresult + HandleQueryContentEvent(WidgetQueryContentEvent* aEvent); + + /** + * Handle eSetSelection event if and only if aEvent changes selection offset + * or length. Doing nothing when selection range is same is important to + * honer users' intention or web app's intention because ContentEventHandler + * does not support to put range boundaries to arbitrary side of element + * boundaries. E.g., `<b>bold[]</b> normal` vs. `<b>bold</b>[] normal`. + * Note that this compares given range with selection cache which has been + * notified IME via widget. Therefore, the caller needs to guarantee that + * pending notifications should've been flushed. If you test this, you need + * to wait 2 animation frames before sending eSetSelection event. + */ + MOZ_CAN_RUN_SCRIPT nsresult MaybeHandleSelectionEvent( + nsPresContext* aPresContext, WidgetSelectionEvent* aEvent); + + /** + * Init() initializes the instance, i.e., retrieving necessary objects and + * starts to observe something. + * Be aware, callers of this method need to guarantee that the instance + * won't be released during calling this. + * + * @param aWidget The widget which can access native IME. + * @param aPresContext The PresContext which has aContent. + * @param aElement An editable element or nullptr if this will observe + * design mode document. + * @param aEditorBase The editor which is associated with aContent. + */ + MOZ_CAN_RUN_SCRIPT void Init(nsIWidget& aWidget, nsPresContext& aPresContext, + dom::Element* aElement, EditorBase& aEditorBase); + + /** + * Destroy() finalizes the instance, i.e., stops observing contents and + * clearing the members. + * Be aware, callers of this method need to guarantee that the instance + * won't be released during calling this. + */ + void Destroy(); + + /** + * Returns false if the instance refers some objects and observing them. + * Otherwise, true. + */ + bool Destroyed() const; + + /** + * IMEContentObserver is stored by EventStateManager during observing. + * DisconnectFromEventStateManager() is called when EventStateManager stops + * storing the instance. + */ + void DisconnectFromEventStateManager(); + + /** + * MaybeReinitialize() tries to restart to observe the editor's root node. + * This is useful when the editor is reframed and all children are replaced + * with new node instances. + * Be aware, callers of this method need to guarantee that the instance + * won't be released during calling this. + * + * @return Returns true if the instance is managing the content. + * Otherwise, false. + */ + MOZ_CAN_RUN_SCRIPT bool MaybeReinitialize(nsIWidget& aWidget, + nsPresContext& aPresContext, + dom::Element* aElement, + EditorBase& aEditorBase); + + bool IsManaging(const nsPresContext& aPresContext, + const dom::Element* aElement) const; + bool IsBeingInitializedFor(const nsPresContext& aPresContext, + const dom::Element* aElement) const; + bool IsManaging(const TextComposition& aTextComposition) const; + bool WasInitializedWith(const EditorBase& aEditorBase) const { + return mEditorBase == &aEditorBase; + } + bool IsEditorHandlingEventForComposition() const; + bool KeepAliveDuringDeactive() const { + return mIMENotificationRequests && + mIMENotificationRequests->WantDuringDeactive(); + } + nsIWidget* GetWidget() const { return mWidget; } + void SuppressNotifyingIME(); + void UnsuppressNotifyingIME(); + nsPresContext* GetPresContext() const; + nsresult GetSelectionAndRoot(dom::Selection** aSelection, + dom::Element** aRootElement) const; + + /** + * TryToFlushPendingNotifications() should be called when pending events + * should be flushed. This tries to run the queued IMENotificationSender. + * Doesn't do anything in child processes where flushing happens + * asynchronously unless aAllowAsync is false. + */ + void TryToFlushPendingNotifications(bool aAllowAsync); + + /** + * MaybeNotifyCompositionEventHandled() posts composition event handled + * notification into the pseudo queue. + */ + void MaybeNotifyCompositionEventHandled(); + + /** + * Following methods are called when the editor: + * - an edit action handled. + * - before handling an edit action. + * - canceled handling an edit action after calling BeforeEditAction(). + */ + void OnEditActionHandled(); + void BeforeEditAction(); + void CancelEditAction(); + + /** + * Called when text control value is changed while this is not observing + * mRootElement. This is typically there is no frame for the editor (i.e., + * no proper anonymous <div> element for the editor yet) or the TextEditor + * has not been created (i.e., IMEStateManager has not been reinitialized + * this instance with new anonymous <div> element yet). + */ + void OnTextControlValueChangedWhileNotObservable(const nsAString& aNewValue); + + dom::Element* GetObservingElement() const { + return mIsObserving ? mRootElement.get() : nullptr; + } + + private: + ~IMEContentObserver() = default; + + enum State { + eState_NotObserving, + eState_Initializing, + eState_StoppedObserving, + eState_Observing + }; + State GetState() const; + MOZ_CAN_RUN_SCRIPT bool InitWithEditor(nsPresContext& aPresContext, + dom::Element* aElement, + EditorBase& aEditorBase); + void OnIMEReceivedFocus(); + void Clear(); + [[nodiscard]] bool IsObservingContent(const nsPresContext& aPresContext, + const dom::Element* aElement) const; + [[nodiscard]] bool IsReflowLocked() const; + [[nodiscard]] bool IsSafeToNotifyIME() const; + [[nodiscard]] bool IsEditorComposing() const; + + // Following methods are called by DocumentObserver when + // beginning to update the contents and ending updating the contents. + void BeginDocumentUpdate(); + void EndDocumentUpdate(); + + // Following methods manages added nodes during a document change. + + /** + * MaybeNotifyIMEOfAddedTextDuringDocumentChange() may send text change + * notification caused by the nodes added between mFirstAddedContent in + * mFirstAddedContainer and mLastAddedContent in + * mLastAddedContainer and forgets the range. + */ + void MaybeNotifyIMEOfAddedTextDuringDocumentChange(); + + /** + * IsInDocumentChange() returns true while the DOM tree is being modified + * with mozAutoDocUpdate. E.g., it's being modified by setting innerHTML or + * insertAdjacentHTML(). This returns false when user types something in + * the focused editor editor. + */ + bool IsInDocumentChange() const { + return mDocumentObserver && mDocumentObserver->IsUpdating(); + } + + /** + * Forget the range of added nodes during a document change. + */ + void ClearAddedNodesDuringDocumentChange(); + + /** + * HasAddedNodesDuringDocumentChange() returns true when this stores range + * of nodes which were added into the DOM tree during a document change but + * have not been sent to IME. Note that this should always return false when + * IsInDocumentChange() returns false. + */ + bool HasAddedNodesDuringDocumentChange() const { + return mFirstAddedContainer && mLastAddedContainer; + } + + /** + * Returns true if the passed-in node in aParent is the next node of + * mLastAddedContent in pre-order tree traversal of the DOM. + */ + bool IsNextNodeOfLastAddedNode(nsINode* aParent, nsIContent* aChild) const; + + void PostFocusSetNotification(); + void MaybeNotifyIMEOfFocusSet(); + void PostTextChangeNotification(); + void MaybeNotifyIMEOfTextChange(const TextChangeDataBase& aTextChangeData); + void CancelNotifyingIMEOfTextChange(); + void PostSelectionChangeNotification(); + void MaybeNotifyIMEOfSelectionChange(bool aCausedByComposition, + bool aCausedBySelectionEvent, + bool aOccurredDuringComposition); + void PostPositionChangeNotification(); + void MaybeNotifyIMEOfPositionChange(); + void CancelNotifyingIMEOfPositionChange(); + void PostCompositionEventHandledNotification(); + + void NotifyContentAdded(nsINode* aContainer, nsIContent* aFirstContent, + nsIContent* aLastContent); + void ObserveEditableNode(); + /** + * NotifyIMEOfBlur() notifies IME of blur. + */ + void NotifyIMEOfBlur(); + /** + * UnregisterObservers() unregisters all listeners and observers. + */ + void UnregisterObservers(); + void FlushMergeableNotifications(); + bool NeedsTextChangeNotification() const { + return mIMENotificationRequests && + mIMENotificationRequests->WantTextChange(); + } + bool NeedsPositionChangeNotification() const { + return mIMENotificationRequests && + mIMENotificationRequests->WantPositionChanged(); + } + void ClearPendingNotifications() { + mNeedsToNotifyIMEOfFocusSet = false; + mNeedsToNotifyIMEOfTextChange = false; + mNeedsToNotifyIMEOfSelectionChange = false; + mNeedsToNotifyIMEOfPositionChange = false; + mNeedsToNotifyIMEOfCompositionEventHandled = false; + mTextChangeData.Clear(); + } + bool NeedsToNotifyIMEOfSomething() const { + return mNeedsToNotifyIMEOfFocusSet || mNeedsToNotifyIMEOfTextChange || + mNeedsToNotifyIMEOfSelectionChange || + mNeedsToNotifyIMEOfPositionChange || + mNeedsToNotifyIMEOfCompositionEventHandled; + } + + /** + * UpdateSelectionCache() updates mSelectionData with the latest selection. + * This should be called only when IsSafeToNotifyIME() returns true. + */ + MOZ_CAN_RUN_SCRIPT bool UpdateSelectionCache(bool aRequireFlush = true); + + nsCOMPtr<nsIWidget> mWidget; + // mFocusedWidget has the editor observed by the instance. E.g., if the + // focused editor is in XUL panel, this should be the widget of the panel. + // On the other hand, mWidget is its parent which handles IME. + nsCOMPtr<nsIWidget> mFocusedWidget; + RefPtr<dom::Selection> mSelection; + RefPtr<dom::Element> mRootElement; + nsCOMPtr<nsINode> mEditableNode; + nsCOMPtr<nsIDocShell> mDocShell; + RefPtr<EditorBase> mEditorBase; + + /** + * Helper classes to notify IME. + */ + + class AChangeEvent : public Runnable { + protected: + enum ChangeEventType { + eChangeEventType_Focus, + eChangeEventType_Selection, + eChangeEventType_Text, + eChangeEventType_Position, + eChangeEventType_CompositionEventHandled + }; + + explicit AChangeEvent(const char* aName, + IMEContentObserver* aIMEContentObserver) + : Runnable(aName), + mIMEContentObserver(do_GetWeakReference( + static_cast<nsIReflowObserver*>(aIMEContentObserver))) { + MOZ_ASSERT(aIMEContentObserver); + } + + already_AddRefed<IMEContentObserver> GetObserver() const { + nsCOMPtr<nsIReflowObserver> observer = + do_QueryReferent(mIMEContentObserver); + return observer.forget().downcast<IMEContentObserver>(); + } + + nsWeakPtr mIMEContentObserver; + + /** + * CanNotifyIME() checks if mIMEContentObserver can and should notify IME. + */ + bool CanNotifyIME(ChangeEventType aChangeEventType) const; + + /** + * IsSafeToNotifyIME() checks if it's safe to noitify IME. + */ + bool IsSafeToNotifyIME(ChangeEventType aChangeEventType) const; + }; + + class IMENotificationSender : public AChangeEvent { + public: + explicit IMENotificationSender(IMEContentObserver* aIMEContentObserver) + : AChangeEvent("IMENotificationSender", aIMEContentObserver), + mIsRunning(false) {} + MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHOD Run() override; + + void Dispatch(nsIDocShell* aDocShell); + + private: + MOZ_CAN_RUN_SCRIPT void SendFocusSet(); + MOZ_CAN_RUN_SCRIPT void SendSelectionChange(); + void SendTextChange(); + void SendPositionChange(); + void SendCompositionEventHandled(); + + bool mIsRunning; + }; + + // mQueuedSender is, it was put into the event queue but not run yet. + RefPtr<IMENotificationSender> mQueuedSender; + + /** + * IMEContentObserver is a mutation observer of mRootContent. However, + * it needs to know the beginning of content changes and end of it too for + * reducing redundant computation of text offset with ContentEventHandler. + * Therefore, it needs helper class to listen only them since if + * both mutations were observed by IMEContentObserver directly, each + * methods need to check if the changing node is in mRootContent but it's + * too expensive. + */ + class DocumentObserver final : public nsStubDocumentObserver { + public: + explicit DocumentObserver(IMEContentObserver& aIMEContentObserver) + : mIMEContentObserver(&aIMEContentObserver), mDocumentUpdating(0) { + SetEnabledCallbacks(nsIMutationObserver::kBeginUpdate | + nsIMutationObserver::kEndUpdate); + } + + NS_DECL_CYCLE_COLLECTION_CLASS(DocumentObserver) + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_NSIDOCUMENTOBSERVER_BEGINUPDATE + NS_DECL_NSIDOCUMENTOBSERVER_ENDUPDATE + + void Observe(dom::Document*); + void StopObserving(); + void Destroy(); + + bool Destroyed() const { return !mIMEContentObserver; } + bool IsObserving() const { return mDocument != nullptr; } + bool IsUpdating() const { return mDocumentUpdating != 0; } + + private: + DocumentObserver() = delete; + virtual ~DocumentObserver() { Destroy(); } + + RefPtr<IMEContentObserver> mIMEContentObserver; + RefPtr<dom::Document> mDocument; + uint32_t mDocumentUpdating; + }; + RefPtr<DocumentObserver> mDocumentObserver; + + /** + * FlatTextCache stores flat text length from start of the content to + * mNodeOffset of mContainerNode. + */ + struct FlatTextCache { + // mContainerNode and mNode represent a point in DOM tree. E.g., + // if mContainerNode is a div element, mNode is a child. + nsCOMPtr<nsINode> mContainerNode; + // mNode points to the last child which participates in the current + // mFlatTextLength. If mNode is null, then that means that the end point for + // mFlatTextLength is immediately before the first child of mContainerNode. + nsCOMPtr<nsINode> mNode; + // Length of flat text generated from contents between the start of content + // and a child node whose index is mNodeOffset of mContainerNode. + uint32_t mFlatTextLength; + + FlatTextCache() : mFlatTextLength(0) {} + + void Clear() { + mContainerNode = nullptr; + mNode = nullptr; + mFlatTextLength = 0; + } + + void Cache(nsINode* aContainer, nsINode* aNode, uint32_t aFlatTextLength) { + MOZ_ASSERT(aContainer, "aContainer must not be null"); + MOZ_ASSERT(!aNode || aNode->GetParentNode() == aContainer, + "aNode must be either null or a child of aContainer"); + mContainerNode = aContainer; + mNode = aNode; + mFlatTextLength = aFlatTextLength; + } + + bool Match(nsINode* aContainer, nsINode* aNode) const { + return aContainer == mContainerNode && aNode == mNode; + } + }; + // mEndOfAddedTextCache caches text length from the start of content to + // the end of the last added content only while an edit action is being + // handled by the editor and no other mutation (e.g., removing node) + // occur. + FlatTextCache mEndOfAddedTextCache; + // mStartOfRemovingTextRangeCache caches text length from the start of content + // to the start of the last removed content only while an edit action is being + // handled by the editor and no other mutation (e.g., adding node) occur. + FlatTextCache mStartOfRemovingTextRangeCache; + + // mFirstAddedContainer is parent node of first added node in current + // document change. So, this is not nullptr only when a node was added + // during a document change and the change has not been included into + // mTextChangeData yet. + // Note that this shouldn't be in cycle collection since this is not nullptr + // only during a document change. + nsCOMPtr<nsINode> mFirstAddedContainer; + // mLastAddedContainer is parent node of last added node in current + // document change. So, this is not nullptr only when a node was added + // during a document change and the change has not been included into + // mTextChangeData yet. + // Note that this shouldn't be in cycle collection since this is not nullptr + // only during a document change. + nsCOMPtr<nsINode> mLastAddedContainer; + + // mFirstAddedContent is the first node added in mFirstAddedContainer. + nsCOMPtr<nsIContent> mFirstAddedContent; + // mLastAddedContent is the last node added in mLastAddedContainer; + nsCOMPtr<nsIContent> mLastAddedContent; + + TextChangeData mTextChangeData; + + // mSelectionData is the last selection data which was notified. The + // selection information is modified by UpdateSelectionCache(). The reason + // of the selection change is modified by MaybeNotifyIMEOfSelectionChange(). + SelectionChangeData mSelectionData; + + EventStateManager* mESM; + + const IMENotificationRequests* mIMENotificationRequests; + int64_t mPreCharacterDataChangeLength = -1; + uint32_t mSuppressNotifications = 0; + + // If the observing editor is a text control's one, this is set to the value + // length. + uint32_t mTextControlValueLength = 0; + + // mSendingNotification is a notification which is now sending from + // IMENotificationSender. When the value is NOTIFY_IME_OF_NOTHING, it's + // not sending any notification. + IMEMessage mSendingNotification; + + bool mIsObserving; + bool mIMEHasFocus; + bool mNeedsToNotifyIMEOfFocusSet; + bool mNeedsToNotifyIMEOfTextChange; + bool mNeedsToNotifyIMEOfSelectionChange; + bool mNeedsToNotifyIMEOfPositionChange; + bool mNeedsToNotifyIMEOfCompositionEventHandled; + // mIsHandlingQueryContentEvent is true when IMEContentObserver is handling + // WidgetQueryContentEvent with ContentEventHandler. + bool mIsHandlingQueryContentEvent; +}; + +} // namespace mozilla + +#endif // mozilla_IMEContentObserver_h diff --git a/dom/events/IMEStateManager.cpp b/dom/events/IMEStateManager.cpp new file mode 100644 index 0000000000..2477f74a9c --- /dev/null +++ b/dom/events/IMEStateManager.cpp @@ -0,0 +1,2363 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#include "IMEStateManager.h" + +#include "mozilla/Logging.h" + +#include "mozilla/Attributes.h" +#include "mozilla/AutoRestore.h" +#include "mozilla/EditorBase.h" +#include "mozilla/EventListenerManager.h" +#include "mozilla/EventStateManager.h" +#include "mozilla/HTMLEditor.h" +#include "mozilla/MouseEvents.h" +#include "mozilla/PresShell.h" +#include "mozilla/StaticPrefs_dom.h" +#include "mozilla/StaticPrefs_intl.h" +#include "mozilla/TextComposition.h" +#include "mozilla/TextEvents.h" +#include "mozilla/ToString.h" +#include "mozilla/Unused.h" +#include "mozilla/dom/BrowserBridgeChild.h" +#include "mozilla/dom/BrowserParent.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/Element.h" +#include "mozilla/dom/HTMLFormElement.h" +#include "mozilla/dom/HTMLTextAreaElement.h" +#include "mozilla/dom/MouseEventBinding.h" +#include "mozilla/dom/UserActivation.h" + +#include "HTMLInputElement.h" +#include "IMEContentObserver.h" + +#include "nsCOMPtr.h" +#include "nsContentUtils.h" +#include "nsFocusManager.h" +#include "nsIContent.h" +#include "nsIContentInlines.h" +#include "nsIFormControl.h" +#include "nsINode.h" +#include "nsISupports.h" +#include "nsIURI.h" +#include "nsIURIMutator.h" +#include "nsPresContext.h" + +namespace mozilla { + +using namespace dom; +using namespace widget; + +/** + * When a method is called, log its arguments and/or related static variables + * with LogLevel::Info. However, if it puts too many logs like + * OnDestroyPresContext(), should long only when the method actually does + * something. In this case, the log should start with "<method name>". + * + * When a method quits due to unexpected situation, log the reason with + * LogLevel::Error. In this case, the log should start with + * "<method name>(), FAILED". The indent makes the log look easier. + * + * When a method does something only in some situations and it may be important + * for debug, log the information with LogLevel::Debug. In this case, the log + * should start with " <method name>(),". + */ +LazyLogModule sISMLog("IMEStateManager"); + +static const char* GetBoolName(bool aBool) { return aBool ? "true" : "false"; } + +StaticRefPtr<Element> IMEStateManager::sFocusedElement; +StaticRefPtr<nsPresContext> IMEStateManager::sFocusedPresContext; +nsIWidget* IMEStateManager::sTextInputHandlingWidget = nullptr; +nsIWidget* IMEStateManager::sFocusedIMEWidget = nullptr; +StaticRefPtr<BrowserParent> IMEStateManager::sFocusedIMEBrowserParent; +nsIWidget* IMEStateManager::sActiveInputContextWidget = nullptr; +StaticRefPtr<IMEContentObserver> IMEStateManager::sActiveIMEContentObserver; +TextCompositionArray* IMEStateManager::sTextCompositions = nullptr; +InputContext::Origin IMEStateManager::sOrigin = InputContext::ORIGIN_MAIN; +InputContext IMEStateManager::sActiveChildInputContext; +bool IMEStateManager::sInstalledMenuKeyboardListener = false; +bool IMEStateManager::sIsGettingNewIMEState = false; +bool IMEStateManager::sCleaningUpForStoppingIMEStateManagement = false; +bool IMEStateManager::sIsActive = false; +Maybe<IMEStateManager::PendingFocusedBrowserSwitchingData> + IMEStateManager::sPendingFocusedBrowserSwitchingData; + +// static +void IMEStateManager::Init() { + sOrigin = XRE_IsParentProcess() ? InputContext::ORIGIN_MAIN + : InputContext::ORIGIN_CONTENT; + ResetActiveChildInputContext(); +} + +// static +void IMEStateManager::Shutdown() { + MOZ_LOG( + sISMLog, LogLevel::Info, + ("Shutdown(), sTextCompositions=0x%p, sTextCompositions->Length()=%zu, " + "sPendingFocusedBrowserSwitchingData.isSome()=%s", + sTextCompositions, sTextCompositions ? sTextCompositions->Length() : 0, + GetBoolName(sPendingFocusedBrowserSwitchingData.isSome()))); + MOZ_LOG(sISMLog, LogLevel::Debug, + (" Shutdown(), sFocusedElement=0x%p, sFocusedPresContext=0x%p, " + "sTextInputHandlingWidget=0x%p, sFocusedIMEWidget=0x%p, " + "sFocusedIMEBrowserParent=0x%p, sActiveInputContextWidget=0x%p, " + "sActiveIMEContentObserver=0x%p", + sFocusedElement.get(), sFocusedPresContext.get(), + sTextInputHandlingWidget, sFocusedIMEWidget, + sFocusedIMEBrowserParent.get(), sActiveInputContextWidget, + sActiveIMEContentObserver.get())); + + sPendingFocusedBrowserSwitchingData.reset(); + MOZ_ASSERT(!sTextCompositions || !sTextCompositions->Length()); + delete sTextCompositions; + sTextCompositions = nullptr; + // All string instances in the global space need to be empty after XPCOM + // shutdown. + sActiveChildInputContext.ShutDown(); +} + +// static +void IMEStateManager::OnFocusMovedBetweenBrowsers(BrowserParent* aBlur, + BrowserParent* aFocus) { + MOZ_ASSERT(aBlur != aFocus); + MOZ_ASSERT(XRE_IsParentProcess()); + + if (sPendingFocusedBrowserSwitchingData.isSome()) { + MOZ_ASSERT(aBlur == + sPendingFocusedBrowserSwitchingData.ref().mBrowserParentFocused); + // If focus is not changed between browsers actually, we need to do + // nothing here. Let's cancel handling what this method does. + if (sPendingFocusedBrowserSwitchingData.ref().mBrowserParentBlurred == + aFocus) { + sPendingFocusedBrowserSwitchingData.reset(); + MOZ_LOG(sISMLog, LogLevel::Info, + (" OnFocusMovedBetweenBrowsers(), canceled all pending focus " + "moves between browsers")); + return; + } + aBlur = sPendingFocusedBrowserSwitchingData.ref().mBrowserParentBlurred; + sPendingFocusedBrowserSwitchingData.ref().mBrowserParentFocused = aFocus; + MOZ_ASSERT(aBlur != aFocus); + } + + // If application was inactive, but is now activated, and the last focused + // this is called by BrowserParent::UnsetTopLevelWebFocusAll() from + // nsFocusManager::WindowRaised(). If a content has focus in a remote + // process and it has composition, it may get focus back later and the + // composition shouldn't be commited now. Therefore, we should put off to + // handle this until getting another call of this method or a call of + //`OnFocusChangeInternal()`. + if (aBlur && !aFocus && !sIsActive && sTextInputHandlingWidget && + sTextCompositions && + sTextCompositions->GetCompositionFor(sTextInputHandlingWidget)) { + if (sPendingFocusedBrowserSwitchingData.isNothing()) { + sPendingFocusedBrowserSwitchingData.emplace(aBlur, aFocus); + } + MOZ_LOG(sISMLog, LogLevel::Debug, + (" OnFocusMovedBetweenBrowsers(), put off to handle it until " + "next OnFocusChangeInternal() call")); + return; + } + sPendingFocusedBrowserSwitchingData.reset(); + + const nsCOMPtr<nsIWidget> oldWidget = sTextInputHandlingWidget; + // In the chrome-process case, we'll get sTextInputHandlingWidget from a + // PresShell later. + sTextInputHandlingWidget = + aFocus ? nsCOMPtr<nsIWidget>(aFocus->GetTextInputHandlingWidget()).get() + : nullptr; + if (oldWidget && sTextCompositions) { + RefPtr<TextComposition> composition = + sTextCompositions->GetCompositionFor(oldWidget); + if (composition) { + MOZ_LOG( + sISMLog, LogLevel::Debug, + (" OnFocusMovedBetweenBrowsers(), requesting to commit " + "composition to " + "the (previous) focused widget (would request=%s)", + GetBoolName( + !oldWidget->IMENotificationRequestsRef().WantDuringDeactive()))); + NotifyIME(REQUEST_TO_COMMIT_COMPOSITION, oldWidget, + composition->GetBrowserParent()); + } + } + + // The manager check is to avoid telling the content process to stop + // IME state management after focus has already moved there between + // two same-process-hosted out-of-process iframes. + if (aBlur && (!aFocus || (aBlur->Manager() != aFocus->Manager()))) { + MOZ_LOG(sISMLog, LogLevel::Debug, + (" OnFocusMovedBetweenBrowsers(), notifying previous " + "focused child process of parent process or another child process " + "getting focus")); + aBlur->StopIMEStateManagement(); + } + + if (sActiveIMEContentObserver) { + DestroyIMEContentObserver(); + } + + if (sFocusedIMEWidget) { + // sFocusedIMEBrowserParent can be null, if IME focus hasn't been + // taken before BrowserParent blur. + // aBlur can be null when keyboard focus moves not actually + // between tabs but an open menu is involved. + MOZ_ASSERT(!sFocusedIMEBrowserParent || !aBlur || + (sFocusedIMEBrowserParent == aBlur)); + MOZ_LOG(sISMLog, LogLevel::Debug, + (" OnFocusMovedBetweenBrowsers(), notifying IME of blur")); + NotifyIME(NOTIFY_IME_OF_BLUR, sFocusedIMEWidget, sFocusedIMEBrowserParent); + + MOZ_ASSERT(!sFocusedIMEBrowserParent); + MOZ_ASSERT(!sFocusedIMEWidget); + + } else { + MOZ_ASSERT(!sFocusedIMEBrowserParent); + } + + // We deliberately don't null out sFocusedElement or sFocusedPresContext here. + // When focus is in remote content, as far as layout in the chrome process is + // concerned, the corresponding content is the top-level XUL browser. Changes + // among out-of-process iframes don't change that, so dropping the pointer to + // the XUL browser upon such a change would break IME handling. +} + +// static +void IMEStateManager::WidgetDestroyed(nsIWidget* aWidget) { + MOZ_LOG(sISMLog, LogLevel::Debug, + ("WidgetDestroyed(aWidget=0x%p), sFocusedIMEWidget=0x%p, " + "sActiveInputContextWidget=0x%p, sFocusedIMEBrowserParent=0x%p", + aWidget, sFocusedIMEWidget, sActiveInputContextWidget, + sFocusedIMEBrowserParent.get())); + if (sTextInputHandlingWidget == aWidget) { + sTextInputHandlingWidget = nullptr; + } + if (sFocusedIMEWidget == aWidget) { + if (sFocusedIMEBrowserParent) { + OnFocusMovedBetweenBrowsers(sFocusedIMEBrowserParent, nullptr); + MOZ_ASSERT(!sFocusedIMEBrowserParent); + } + sFocusedIMEWidget = nullptr; + } + if (sActiveInputContextWidget == aWidget) { + sActiveInputContextWidget = nullptr; + } +} + +// static +void IMEStateManager::WidgetOnQuit(nsIWidget* aWidget) { + if (sFocusedIMEWidget == aWidget) { + MOZ_LOG( + sISMLog, LogLevel::Debug, + ("WidgetOnQuit(aWidget=0x%p (available %s)), sFocusedIMEWidget=0x%p", + aWidget, GetBoolName(aWidget && !aWidget->Destroyed()), + sFocusedIMEWidget)); + // Notify IME of blur (which is done by IMEContentObserver::Destroy + // automatically) when the widget still has IME focus before forgetting the + // focused widget because the focused widget is required to clean up native + // IME handler with sending blur notification. Fortunately, the widget + // has not been destroyed yet here since some methods to sending blur + // notification won't work with destroyed widget. + IMEStateManager::DestroyIMEContentObserver(); + // Finally, clean up the widget and related objects for avoiding to leak. + IMEStateManager::WidgetDestroyed(aWidget); + } +} + +// static +void IMEStateManager::StopIMEStateManagement() { + MOZ_ASSERT(XRE_IsContentProcess()); + MOZ_LOG(sISMLog, LogLevel::Info, ("StopIMEStateManagement()")); + + // NOTE: Don't set input context from here since this has already lost + // the rights to change input context. + + // The requestee of this method in the main process must destroy its + // active IMEContentObserver for making existing composition end and + // make it be possible to start new composition in new focused process. + // Therefore, we shouldn't notify the main process of any changes which + // occurred after here. + AutoRestore<bool> restoreStoppingIMEStateManagementState( + sCleaningUpForStoppingIMEStateManagement); + sCleaningUpForStoppingIMEStateManagement = true; + + if (sTextCompositions && sFocusedPresContext) { + NotifyIME(REQUEST_TO_COMMIT_COMPOSITION, sFocusedPresContext, nullptr); + } + sActiveInputContextWidget = nullptr; + sFocusedPresContext = nullptr; + sFocusedElement = nullptr; + sIsActive = false; + DestroyIMEContentObserver(); +} + +// static +void IMEStateManager::MaybeStartOffsetUpdatedInChild(nsIWidget* aWidget, + uint32_t aStartOffset) { + if (NS_WARN_IF(!sTextCompositions)) { + MOZ_LOG(sISMLog, LogLevel::Warning, + ("MaybeStartOffsetUpdatedInChild(aWidget=0x%p, aStartOffset=%u), " + "called when there is no composition", + aWidget, aStartOffset)); + return; + } + + RefPtr<TextComposition> composition = GetTextCompositionFor(aWidget); + if (NS_WARN_IF(!composition)) { + MOZ_LOG(sISMLog, LogLevel::Warning, + ("MaybeStartOffsetUpdatedInChild(aWidget=0x%p, aStartOffset=%u), " + "called when there is no composition", + aWidget, aStartOffset)); + return; + } + + if (composition->NativeOffsetOfStartComposition() == aStartOffset) { + return; + } + + MOZ_LOG( + sISMLog, LogLevel::Info, + ("MaybeStartOffsetUpdatedInChild(aWidget=0x%p, aStartOffset=%u), " + "old offset=%u", + aWidget, aStartOffset, composition->NativeOffsetOfStartComposition())); + composition->OnStartOffsetUpdatedInChild(aStartOffset); +} + +// static +nsresult IMEStateManager::OnDestroyPresContext(nsPresContext& aPresContext) { + // First, if there is a composition in the aPresContext, clean up it. + if (sTextCompositions) { + TextCompositionArray::index_type i = + sTextCompositions->IndexOf(&aPresContext); + if (i != TextCompositionArray::NoIndex) { + MOZ_LOG(sISMLog, LogLevel::Debug, + (" OnDestroyPresContext(), " + "removing TextComposition instance from the array (index=%zu)", + i)); + // there should be only one composition per presContext object. + sTextCompositions->ElementAt(i)->Destroy(); + sTextCompositions->RemoveElementAt(i); + if (sTextCompositions->IndexOf(&aPresContext) != + TextCompositionArray::NoIndex) { + MOZ_LOG(sISMLog, LogLevel::Error, + (" OnDestroyPresContext(), FAILED to remove " + "TextComposition instance from the array")); + MOZ_CRASH("Failed to remove TextComposition instance from the array"); + } + } + } + + if (&aPresContext != sFocusedPresContext) { + return NS_OK; + } + + MOZ_LOG( + sISMLog, LogLevel::Info, + ("OnDestroyPresContext(aPresContext=0x%p), " + "sFocusedPresContext=0x%p, sFocusedElement=0x%p, sTextCompositions=0x%p", + &aPresContext, sFocusedPresContext.get(), sFocusedElement.get(), + sTextCompositions)); + + DestroyIMEContentObserver(); + + if (sTextInputHandlingWidget) { + IMEState newState = GetNewIMEState(*sFocusedPresContext, nullptr); + InputContextAction action(InputContextAction::CAUSE_UNKNOWN, + InputContextAction::LOST_FOCUS); + InputContext::Origin origin = + BrowserParent::GetFocused() ? InputContext::ORIGIN_CONTENT : sOrigin; + OwningNonNull<nsIWidget> textInputHandlingWidget = + *sTextInputHandlingWidget; + SetIMEState(newState, nullptr, nullptr, textInputHandlingWidget, action, + origin); + } + sTextInputHandlingWidget = nullptr; + sFocusedElement = nullptr; + sFocusedPresContext = nullptr; + return NS_OK; +} + +// static +nsresult IMEStateManager::OnRemoveContent(nsPresContext& aPresContext, + Element& aElement) { + // First, if there is a composition in the aElement, clean up it. + if (sTextCompositions) { + const RefPtr<TextComposition> compositionInContent = + sTextCompositions->GetCompositionInContent(&aPresContext, &aElement); + + if (compositionInContent) { + MOZ_LOG(sISMLog, LogLevel::Debug, + (" OnRemoveContent(), " + "composition is in the content")); + + // Try resetting the native IME state. Be aware, typically, this method + // is called during the content being removed. Then, the native + // composition events which are caused by following APIs are ignored due + // to unsafe to run script (in PresShell::HandleEvent()). + nsresult rv = + compositionInContent->NotifyIME(REQUEST_TO_CANCEL_COMPOSITION); + if (NS_FAILED(rv)) { + compositionInContent->NotifyIME(REQUEST_TO_COMMIT_COMPOSITION); + } + } + } + + if (!sFocusedPresContext || !sFocusedElement || + !sFocusedElement->IsInclusiveDescendantOf(&aElement)) { + return NS_OK; + } + MOZ_ASSERT(sFocusedPresContext == &aPresContext); + + MOZ_LOG( + sISMLog, LogLevel::Info, + ("OnRemoveContent(aPresContext=0x%p, aElement=0x%p), " + "sFocusedPresContext=0x%p, sFocusedElement=0x%p, sTextCompositions=0x%p", + &aPresContext, &aElement, sFocusedPresContext.get(), + sFocusedElement.get(), sTextCompositions)); + + DestroyIMEContentObserver(); + + // FYI: Don't clear sTextInputHandlingWidget and sFocusedPresContext because + // the window/document keeps having focus. + sFocusedElement = nullptr; + + // Current IME transaction should commit + if (!sTextInputHandlingWidget) { + return NS_OK; + } + + IMEState newState = GetNewIMEState(*sFocusedPresContext, nullptr); + InputContextAction action(InputContextAction::CAUSE_UNKNOWN, + InputContextAction::LOST_FOCUS); + InputContext::Origin origin = + BrowserParent::GetFocused() ? InputContext::ORIGIN_CONTENT : sOrigin; + OwningNonNull<nsIWidget> textInputHandlingWidget = *sTextInputHandlingWidget; + SetIMEState(newState, &aPresContext, nullptr, textInputHandlingWidget, action, + origin); + if (sFocusedPresContext != &aPresContext || sFocusedElement) { + return NS_OK; // Some body must have set focus + } + + if (IsIMEObserverNeeded(newState)) { + if (RefPtr<HTMLEditor> htmlEditor = + nsContentUtils::GetHTMLEditor(&aPresContext)) { + CreateIMEContentObserver(*htmlEditor, nullptr); + } + } + + return NS_OK; +} + +// static +bool IMEStateManager::CanHandleWith(const nsPresContext* aPresContext) { + return aPresContext && aPresContext->GetPresShell() && + !aPresContext->PresShell()->IsDestroying(); +} + +// static +nsresult IMEStateManager::OnChangeFocus(nsPresContext* aPresContext, + Element* aElement, + InputContextAction::Cause aCause) { + MOZ_LOG(sISMLog, LogLevel::Info, + ("OnChangeFocus(aPresContext=0x%p, aElement=0x%p, aCause=%s)", + aPresContext, aElement, ToString(aCause).c_str())); + + InputContextAction action(aCause); + return OnChangeFocusInternal(aPresContext, aElement, action); +} + +// static +nsresult IMEStateManager::OnChangeFocusInternal(nsPresContext* aPresContext, + Element* aElement, + InputContextAction aAction) { + NS_ASSERTION(!aElement || aElement->GetPresContext( + Element::PresContextFor::eForComposedDoc) == + aPresContext, + "aPresContext does not match with one of aElement"); + + bool remoteHasFocus = EventStateManager::IsRemoteTarget(aElement); + // If we've handled focused content, we were inactive but now active, + // a remote process has focus, and setting focus to same content in the main + // process, it means that we're restoring focus without changing DOM focus + // both in the main process and the remote process. + const bool restoringContextForRemoteContent = + XRE_IsParentProcess() && remoteHasFocus && !sIsActive && aPresContext && + sFocusedPresContext && sFocusedElement && + sFocusedPresContext.get() == aPresContext && + sFocusedElement.get() == aElement && + aAction.mFocusChange != InputContextAction::MENU_GOT_PSEUDO_FOCUS; + + MOZ_LOG( + sISMLog, LogLevel::Info, + ("OnChangeFocusInternal(aPresContext=0x%p (available: %s), " + "aElement=0x%p (remote: %s), aAction={ mCause=%s, " + "mFocusChange=%s }), sFocusedPresContext=0x%p (available: %s), " + "sFocusedElement=0x%p, sTextInputHandlingWidget=0x%p (available: %s), " + "BrowserParent::GetFocused()=0x%p, sActiveIMEContentObserver=0x%p, " + "sInstalledMenuKeyboardListener=%s, sIsActive=%s, " + "restoringContextForRemoteContent=%s", + aPresContext, GetBoolName(CanHandleWith(aPresContext)), aElement, + GetBoolName(remoteHasFocus), ToString(aAction.mCause).c_str(), + ToString(aAction.mFocusChange).c_str(), sFocusedPresContext.get(), + GetBoolName(CanHandleWith(sFocusedPresContext)), sFocusedElement.get(), + sTextInputHandlingWidget, + GetBoolName(sTextInputHandlingWidget && + !sTextInputHandlingWidget->Destroyed()), + BrowserParent::GetFocused(), sActiveIMEContentObserver.get(), + GetBoolName(sInstalledMenuKeyboardListener), GetBoolName(sIsActive), + GetBoolName(restoringContextForRemoteContent))); + + sIsActive = !!aPresContext; + if (sPendingFocusedBrowserSwitchingData.isSome()) { + MOZ_ASSERT(XRE_IsParentProcess()); + RefPtr<Element> focusedElement = sFocusedElement; + RefPtr<nsPresContext> focusedPresContext = sFocusedPresContext; + RefPtr<BrowserParent> browserParentBlurred = + sPendingFocusedBrowserSwitchingData.ref().mBrowserParentBlurred; + RefPtr<BrowserParent> browserParentFocused = + sPendingFocusedBrowserSwitchingData.ref().mBrowserParentFocused; + OnFocusMovedBetweenBrowsers(browserParentBlurred, browserParentFocused); + // If another call of this method happens during the + // OnFocusMovedBetweenBrowsers call, we shouldn't take back focus to + // the old one. + if (focusedElement != sFocusedElement.get() || + focusedPresContext != sFocusedPresContext.get()) { + MOZ_LOG(sISMLog, LogLevel::Debug, + (" OnChangeFocusInternal(aPresContext=0x%p, aElement=0x%p) " + "stoped handling it because the focused content was changed to " + "sFocusedPresContext=0x%p, sFocusedElement=0x%p by another call", + aPresContext, aElement, sFocusedPresContext.get(), + sFocusedElement.get())); + return NS_OK; + } + } + + // If new aPresShell has been destroyed, this should handle the focus change + // as nobody is getting focus. + MOZ_ASSERT_IF(!aPresContext, !aElement); + if (NS_WARN_IF(aPresContext && !CanHandleWith(aPresContext))) { + MOZ_LOG(sISMLog, LogLevel::Warning, + (" OnChangeFocusInternal(), called with destroyed PresShell, " + "handling this call as nobody getting focus")); + aPresContext = nullptr; + aElement = nullptr; + } else if (!aPresContext) { + aElement = nullptr; + } + + const nsCOMPtr<nsIWidget> oldWidget = sTextInputHandlingWidget; + const nsCOMPtr<nsIWidget> newWidget = + aPresContext ? aPresContext->GetTextInputHandlingWidget() : nullptr; + const bool focusActuallyChanging = + (sFocusedElement != aElement || sFocusedPresContext != aPresContext || + oldWidget != newWidget || + (remoteHasFocus && !restoringContextForRemoteContent && + (aAction.mFocusChange != InputContextAction::MENU_GOT_PSEUDO_FOCUS))); + + // If old widget has composition, we may need to commit composition since + // a native IME context is shared on all editors on some widgets or all + // widgets (it depends on platforms). + if (oldWidget && focusActuallyChanging && sTextCompositions) { + RefPtr<TextComposition> composition = + sTextCompositions->GetCompositionFor(oldWidget); + if (composition) { + // However, don't commit the composition if we're being inactivated + // but the composition should be kept even during deactive. + // Note that oldWidget and sFocusedIMEWidget may be different here (in + // such case, sFocusedIMEWidget is perhaps nullptr). For example, IME + // may receive only blur notification but still has composition. + // We need to clean up only the oldWidget's composition state here. + if (aPresContext || + !oldWidget->IMENotificationRequestsRef().WantDuringDeactive()) { + MOZ_LOG( + sISMLog, LogLevel::Info, + (" OnChangeFocusInternal(), requesting to commit composition to " + "the (previous) focused widget")); + NotifyIME(REQUEST_TO_COMMIT_COMPOSITION, oldWidget, + composition->GetBrowserParent()); + } + } + } + + if (sActiveIMEContentObserver) { + MOZ_ASSERT(!remoteHasFocus || XRE_IsContentProcess(), + "IMEContentObserver should have been destroyed by " + "OnFocusMovedBetweenBrowsers."); + if (!aPresContext) { + if (!sActiveIMEContentObserver->KeepAliveDuringDeactive()) { + DestroyIMEContentObserver(); + } + } + // Otherwise, i.e., new focused content is in this process, let's check + // whether the new focused content is already being managed by the + // active IME content observer. + else if (!sActiveIMEContentObserver->IsManaging(*aPresContext, aElement)) { + DestroyIMEContentObserver(); + } + } + + if (!aPresContext) { + MOZ_LOG(sISMLog, LogLevel::Debug, + (" OnChangeFocusInternal(), no nsPresContext is being activated")); + return NS_OK; + } + + if (NS_WARN_IF(!newWidget)) { + MOZ_LOG(sISMLog, LogLevel::Error, + (" OnChangeFocusInternal(), FAILED due to no widget to manage its " + "IME state")); + return NS_OK; + } + + // Update the cached widget since root view of the presContext may be + // changed to different view. + sTextInputHandlingWidget = newWidget; + + // If a child process has focus, we should disable IME state until the child + // process actually gets focus because if user types keys before that they + // are handled by IME. + IMEState newState = remoteHasFocus ? IMEState(IMEEnabled::Disabled) + : GetNewIMEState(*aPresContext, aElement); + bool setIMEState = true; + + if (remoteHasFocus && XRE_IsParentProcess()) { + if (aAction.mFocusChange == InputContextAction::MENU_GOT_PSEUDO_FOCUS) { + // If menu keyboard listener is installed, we need to disable IME now. + setIMEState = true; + } else if (aAction.mFocusChange == + InputContextAction::MENU_LOST_PSEUDO_FOCUS) { + // If menu keyboard listener is uninstalled, we need to restore + // input context which was set by the remote process. However, if + // the remote process hasn't been set input context yet, we need to + // wait next SetInputContextForChildProcess() call. + if (HasActiveChildSetInputContext()) { + setIMEState = true; + newState = sActiveChildInputContext.mIMEState; + } else { + setIMEState = false; + } + } else if (focusActuallyChanging) { + InputContext context = newWidget->GetInputContext(); + if (context.mIMEState.mEnabled == IMEEnabled::Disabled && + context.mOrigin == InputContext::ORIGIN_CONTENT) { + setIMEState = false; + MOZ_LOG(sISMLog, LogLevel::Debug, + (" OnChangeFocusInternal(), doesn't set IME state because " + "focused element (or document) is in a child process and the " + "IME state is already disabled by a remote process")); + } else { + // When new remote process gets focus, we should forget input context + // coming from old focused remote process. + ResetActiveChildInputContext(); + MOZ_LOG(sISMLog, LogLevel::Debug, + (" OnChangeFocusInternal(), will disable IME until new " + "focused element (or document) in the child process will get " + "focus actually")); + } + } else if (newWidget->GetInputContext().mOrigin != + InputContext::ORIGIN_MAIN) { + // When focus is NOT changed actually, we shouldn't set IME state if + // current input context was set by a remote process since that means + // that the window is being activated and the child process may have + // composition. Then, we shouldn't commit the composition with making + // IME state disabled. + setIMEState = false; + MOZ_LOG( + sISMLog, LogLevel::Debug, + (" OnChangeFocusInternal(), doesn't set IME state because focused " + "element (or document) is already in the child process")); + } + } else { + // When this process gets focus, we should forget input context coming + // from remote process. + ResetActiveChildInputContext(); + } + + if (setIMEState) { + if (!focusActuallyChanging) { + // actual focus isn't changing, but if IME enabled state is changing, + // we should do it. + InputContext context = newWidget->GetInputContext(); + if (context.mIMEState.mEnabled == newState.mEnabled) { + MOZ_LOG(sISMLog, LogLevel::Debug, + (" OnChangeFocusInternal(), neither focus nor IME state is " + "changing")); + return NS_OK; + } + aAction.mFocusChange = InputContextAction::FOCUS_NOT_CHANGED; + + // Even if focus isn't changing actually, we should commit current + // composition here since the IME state is changing. + if (sFocusedPresContext && oldWidget && !focusActuallyChanging) { + NotifyIME(REQUEST_TO_COMMIT_COMPOSITION, oldWidget, + sFocusedIMEBrowserParent); + } + } else if (aAction.mFocusChange == InputContextAction::FOCUS_NOT_CHANGED) { + // If aElement isn't null or aElement is null but editable, somebody gets + // focus. + bool gotFocus = aElement || (newState.mEnabled == IMEEnabled::Enabled); + aAction.mFocusChange = gotFocus ? InputContextAction::GOT_FOCUS + : InputContextAction::LOST_FOCUS; + } + + if (remoteHasFocus && HasActiveChildSetInputContext() && + aAction.mFocusChange == InputContextAction::MENU_LOST_PSEUDO_FOCUS) { + // Restore the input context in the active remote process when + // menu keyboard listener is uninstalled and active remote tab has + // focus. + SetInputContext(*newWidget, sActiveChildInputContext, aAction); + } else { + // Update IME state for new focus widget + SetIMEState(newState, aPresContext, aElement, *newWidget, aAction, + remoteHasFocus ? InputContext::ORIGIN_CONTENT : sOrigin); + } + } + + sFocusedPresContext = aPresContext; + sFocusedElement = aElement; + + // Don't call CreateIMEContentObserver() here because it will be called from + // the focus event handler of focused editor. + + MOZ_LOG(sISMLog, LogLevel::Debug, + (" OnChangeFocusInternal(), modified IME state for " + "sFocusedPresContext=0x%p, sFocusedElement=0x%p", + sFocusedPresContext.get(), sFocusedElement.get())); + + return NS_OK; +} + +// static +void IMEStateManager::OnInstalledMenuKeyboardListener(bool aInstalling) { + MOZ_LOG( + sISMLog, LogLevel::Info, + ("OnInstalledMenuKeyboardListener(aInstalling=%s), " + "sInstalledMenuKeyboardListener=%s, BrowserParent::GetFocused()=0x%p, " + "sActiveChildInputContext=%s", + GetBoolName(aInstalling), GetBoolName(sInstalledMenuKeyboardListener), + BrowserParent::GetFocused(), + ToString(sActiveChildInputContext).c_str())); + + sInstalledMenuKeyboardListener = aInstalling; + + InputContextAction action(InputContextAction::CAUSE_UNKNOWN, + aInstalling + ? InputContextAction::MENU_GOT_PSEUDO_FOCUS + : InputContextAction::MENU_LOST_PSEUDO_FOCUS); + RefPtr<nsPresContext> focusedPresContext = sFocusedPresContext; + RefPtr<Element> focusedElement = sFocusedElement; + OnChangeFocusInternal(focusedPresContext, focusedElement, action); +} + +// static +bool IMEStateManager::OnMouseButtonEventInEditor( + nsPresContext& aPresContext, Element* aElement, + WidgetMouseEvent& aMouseEvent) { + MOZ_LOG(sISMLog, LogLevel::Info, + ("OnMouseButtonEventInEditor(aPresContext=0x%p (available: %s), " + "aElement=0x%p, aMouseEvent=0x%p), sFocusedPresContext=0x%p, " + "sFocusedElement=0x%p", + &aPresContext, GetBoolName(CanHandleWith(&aPresContext)), aElement, + &aMouseEvent, sFocusedPresContext.get(), sFocusedElement.get())); + + if (sFocusedPresContext != &aPresContext || sFocusedElement != aElement) { + MOZ_LOG(sISMLog, LogLevel::Debug, + (" OnMouseButtonEventInEditor(), " + "the mouse event isn't fired on the editor managed by ISM")); + return false; + } + + if (!sActiveIMEContentObserver) { + MOZ_LOG(sISMLog, LogLevel::Debug, + (" OnMouseButtonEventInEditor(), " + "there is no active IMEContentObserver")); + return false; + } + + if (!sActiveIMEContentObserver->IsManaging(aPresContext, aElement)) { + MOZ_LOG(sISMLog, LogLevel::Debug, + (" OnMouseButtonEventInEditor(), " + "the active IMEContentObserver isn't managing the editor")); + return false; + } + + OwningNonNull<IMEContentObserver> observer = *sActiveIMEContentObserver; + bool consumed = observer->OnMouseButtonEvent(aPresContext, aMouseEvent); + MOZ_LOG(sISMLog, LogLevel::Info, + (" OnMouseButtonEventInEditor(), " + "mouse event (mMessage=%s, mButton=%d) is %s", + ToChar(aMouseEvent.mMessage), aMouseEvent.mButton, + consumed ? "consumed" : "not consumed")); + return consumed; +} + +// static +void IMEStateManager::OnClickInEditor(nsPresContext& aPresContext, + Element* aElement, + const WidgetMouseEvent& aMouseEvent) { + MOZ_LOG(sISMLog, LogLevel::Info, + ("OnClickInEditor(aPresContext=0x%p (available: %s), aElement=0x%p, " + "aMouseEvent=0x%p), sFocusedPresContext=0x%p, sFocusedElement=0x%p, " + "sTextInputHandlingWidget=0x%p (available: %s)", + &aPresContext, GetBoolName(CanHandleWith(&aPresContext)), aElement, + &aMouseEvent, sFocusedPresContext.get(), sFocusedElement.get(), + sTextInputHandlingWidget, + GetBoolName(sTextInputHandlingWidget && + !sTextInputHandlingWidget->Destroyed()))); + + if (sFocusedPresContext != &aPresContext || sFocusedElement != aElement || + NS_WARN_IF(!sFocusedPresContext) || + NS_WARN_IF(!sTextInputHandlingWidget) || + NS_WARN_IF(sTextInputHandlingWidget->Destroyed())) { + MOZ_LOG(sISMLog, LogLevel::Debug, + (" OnClickInEditor(), " + "the mouse event isn't fired on the editor managed by ISM")); + return; + } + + const OwningNonNull<nsIWidget> textInputHandlingWidget = + *sTextInputHandlingWidget; + +#ifdef DEBUG + { + nsCOMPtr<nsIWidget> currentTextInputHandlingWidget = + sFocusedPresContext->GetTextInputHandlingWidget(); + MOZ_ASSERT(!currentTextInputHandlingWidget || + currentTextInputHandlingWidget == textInputHandlingWidget); + } +#endif // DEBUG + + if (!aMouseEvent.IsTrusted()) { + MOZ_LOG(sISMLog, LogLevel::Debug, + (" OnClickInEditor(), " + "the mouse event isn't a trusted event")); + return; // ignore untrusted event. + } + + if (aMouseEvent.mButton) { + MOZ_LOG(sISMLog, LogLevel::Debug, + (" OnClickInEditor(), " + "the mouse event isn't a left mouse button event")); + return; // not a left click event. + } + + if (aMouseEvent.mClickCount != 1) { + MOZ_LOG(sISMLog, LogLevel::Debug, + (" OnClickInEditor(), " + "the mouse event isn't a single click event")); + return; // should notify only first click event. + } + + MOZ_ASSERT_IF(aElement, !EventStateManager::IsRemoteTarget(aElement)); + InputContextAction::Cause cause = + aMouseEvent.mInputSource == MouseEvent_Binding::MOZ_SOURCE_TOUCH + ? InputContextAction::CAUSE_TOUCH + : InputContextAction::CAUSE_MOUSE; + + InputContextAction action(cause, InputContextAction::FOCUS_NOT_CHANGED); + IMEState newState = GetNewIMEState(aPresContext, aElement); + // If the state is not editable, there should be no active IMEContentObserver. + // However, if this click sets focus to the editor, IMEContentObserver may + // have not been created yet. Instead, if there is active IMEContentObserver, + // it should be editable. + MOZ_ASSERT_IF(!newState.IsEditable(), !sActiveIMEContentObserver); + MOZ_ASSERT_IF(sActiveIMEContentObserver, newState.IsEditable()); + SetIMEState(newState, &aPresContext, aElement, textInputHandlingWidget, + action, sOrigin); +} + +// static +Element* IMEStateManager::GetFocusedElement() { return sFocusedElement; } + +// static +bool IMEStateManager::IsFocusedElement(const nsPresContext& aPresContext, + const Element* aFocusedElement) { + if (!sFocusedPresContext || &aPresContext != sFocusedPresContext) { + return false; + } + + if (sFocusedElement == aFocusedElement) { + return true; + } + + // If sFocusedElement is not nullptr, but aFocusedElement is nullptr, it does + // not have focus from point of view of IMEStateManager. + if (sFocusedElement) { + return false; + } + + // If the caller does not think that nobody has focus, but we know there is + // a focused content, the caller must be called with wrong content. + if (!aFocusedElement) { + return false; + } + + // If the aFocusedElement is in design mode, sFocusedElement may be nullptr. + if (aFocusedElement->IsInDesignMode()) { + MOZ_ASSERT(&aPresContext == sFocusedPresContext && !sFocusedElement); + return true; + } + + // Otherwise, only when aFocusedElement is the root element, it can have + // focus, but IMEStateManager::OnChangeFocus is called with nullptr for + // aFocusedElement if it was not editable. + // XXX In this case, should the caller update sFocusedElement? + return aFocusedElement->IsEditable() && sFocusedPresContext->Document() && + sFocusedPresContext->Document()->GetRootElement() == aFocusedElement; +} + +// static +void IMEStateManager::OnFocusInEditor(nsPresContext& aPresContext, + Element* aElement, + EditorBase& aEditorBase) { + MOZ_LOG(sISMLog, LogLevel::Info, + ("OnFocusInEditor(aPresContext=0x%p (available: %s), aElement=0x%p, " + "aEditorBase=0x%p), sFocusedPresContext=0x%p, sFocusedElement=0x%p, " + "sActiveIMEContentObserver=0x%p", + &aPresContext, GetBoolName(CanHandleWith(&aPresContext)), aElement, + &aEditorBase, sFocusedPresContext.get(), sFocusedElement.get(), + sActiveIMEContentObserver.get())); + + if (!IsFocusedElement(aPresContext, aElement)) { + MOZ_LOG(sISMLog, LogLevel::Debug, + (" OnFocusInEditor(), " + "an editor not managed by ISM gets focus")); + return; + } + MOZ_ASSERT(sTextInputHandlingWidget); + + // If the IMEContentObserver instance isn't managing the editor actually, + // we need to recreate the instance. + if (sActiveIMEContentObserver) { + if (sActiveIMEContentObserver->IsManaging(aPresContext, aElement)) { + MOZ_LOG( + sISMLog, LogLevel::Debug, + (" OnFocusInEditor(), " + "the editor is already being managed by sActiveIMEContentObserver")); + return; + } + // If the IMEContentObserver has not finished initializing itself yet, + // we don't need to recreate it because the following + // TryToFlushPendingNotifications call must make it initialized. + const nsCOMPtr<nsIWidget> textInputHandlingWidget = + sTextInputHandlingWidget; + if (!sActiveIMEContentObserver->IsBeingInitializedFor(aPresContext, + aElement)) { + DestroyIMEContentObserver(); + } + if (NS_WARN_IF(!IsFocusedElement(aPresContext, aElement)) || + NS_WARN_IF(!sTextInputHandlingWidget) || + NS_WARN_IF(sTextInputHandlingWidget != textInputHandlingWidget)) { + MOZ_LOG(sISMLog, LogLevel::Error, + (" OnFocusInEditor(), detected unexpected focus change with " + "re-initializing active IMEContentObserver")); + return; + } + } + + if (!sActiveIMEContentObserver && sTextInputHandlingWidget && + IsIMEObserverNeeded( + sTextInputHandlingWidget->GetInputContext().mIMEState)) { + CreateIMEContentObserver(aEditorBase, aElement); + if (sActiveIMEContentObserver) { + MOZ_LOG(sISMLog, LogLevel::Debug, + (" OnFocusInEditor(), new IMEContentObserver is created (0x%p)", + sActiveIMEContentObserver.get())); + } + } + + if (sActiveIMEContentObserver) { + sActiveIMEContentObserver->TryToFlushPendingNotifications(false); + MOZ_LOG(sISMLog, LogLevel::Debug, + (" OnFocusInEditor(), trying to send pending notifications in " + "the active IMEContentObserver (0x%p)...", + sActiveIMEContentObserver.get())); + } +} + +// static +void IMEStateManager::OnEditorInitialized(EditorBase& aEditorBase) { + if (!sActiveIMEContentObserver || + !sActiveIMEContentObserver->WasInitializedWith(aEditorBase)) { + return; + } + + MOZ_LOG(sISMLog, LogLevel::Info, + ("OnEditorInitialized(aEditorBase=0x%p)", &aEditorBase)); + + sActiveIMEContentObserver->UnsuppressNotifyingIME(); +} + +// static +void IMEStateManager::OnEditorDestroying(EditorBase& aEditorBase) { + if (!sActiveIMEContentObserver || + !sActiveIMEContentObserver->WasInitializedWith(aEditorBase)) { + return; + } + + MOZ_LOG(sISMLog, LogLevel::Info, + ("OnEditorDestroying(aEditorBase=0x%p)", &aEditorBase)); + + // The IMEContentObserver shouldn't notify IME of anything until reframing + // is finished. + sActiveIMEContentObserver->SuppressNotifyingIME(); +} + +void IMEStateManager::OnReFocus(nsPresContext& aPresContext, + Element& aElement) { + MOZ_LOG(sISMLog, LogLevel::Info, + ("OnReFocus(aPresContext=0x%p (available: %s), aElement=0x%p), " + "sActiveIMEContentObserver=0x%p, aElement=0x%p", + &aPresContext, GetBoolName(CanHandleWith(&aPresContext)), &aElement, + sActiveIMEContentObserver.get(), sFocusedElement.get())); + + if (NS_WARN_IF(!sTextInputHandlingWidget) || + NS_WARN_IF(sTextInputHandlingWidget->Destroyed())) { + return; + } + + if (!sActiveIMEContentObserver || + !sActiveIMEContentObserver->IsManaging(aPresContext, &aElement)) { + MOZ_LOG(sISMLog, LogLevel::Debug, + (" OnReFocus(), there is no valid IMEContentObserver, so we don't " + "manage this. Ignore this")); + return; + } + + MOZ_ASSERT(&aElement == sFocusedElement.get()); + + if (!UserActivation::IsHandlingUserInput() || + UserActivation::IsHandlingKeyboardInput()) { + return; + } + + const OwningNonNull<nsIWidget> textInputHandlingWidget = + *sTextInputHandlingWidget; + + // Don't update IME state during composition. + if (sTextCompositions) { + if (const TextComposition* composition = + sTextCompositions->GetCompositionFor(textInputHandlingWidget)) { + if (composition->IsComposing()) { + return; + } + } + } + + InputContextAction action(InputContextAction::CAUSE_UNKNOWN, + InputContextAction::FOCUS_NOT_CHANGED); + IMEState newState = GetNewIMEState(aPresContext, &aElement); + MOZ_ASSERT(newState.IsEditable()); + SetIMEState(newState, &aPresContext, &aElement, textInputHandlingWidget, + action, sOrigin); +} + +// static +void IMEStateManager::MaybeOnEditableStateDisabled(nsPresContext& aPresContext, + dom::Element* aElement) { + MOZ_LOG( + sISMLog, LogLevel::Info, + ("MaybeOnEditableStateDisabled(aPresContext=0x%p, aElement=0x%p), " + "sFocusedPresContext=0x%p (available: %s), " + "sFocusedElement=0x%p, sTextInputHandlingWidget=0x%p (available: %s), " + "sActiveIMEContentObserver=0x%p, sIsGettingNewIMEState=%s", + &aPresContext, aElement, sFocusedPresContext.get(), + GetBoolName(CanHandleWith(sFocusedPresContext)), sFocusedElement.get(), + sTextInputHandlingWidget, + GetBoolName(sTextInputHandlingWidget && + !sTextInputHandlingWidget->Destroyed()), + sActiveIMEContentObserver.get(), GetBoolName(sIsGettingNewIMEState))); + + if (sIsGettingNewIMEState) { + MOZ_LOG(sISMLog, LogLevel::Debug, + (" MaybeOnEditableStateDisabled(), " + "does nothing because of called while getting new IME state")); + return; + } + + if (&aPresContext != sFocusedPresContext || aElement != sFocusedElement) { + MOZ_LOG(sISMLog, LogLevel::Debug, + (" MaybeOnEditableStateDisabled(), " + "does nothing because of another element already has focus")); + return; + } + + if (NS_WARN_IF(!sTextInputHandlingWidget) || + NS_WARN_IF(sTextInputHandlingWidget->Destroyed())) { + MOZ_LOG(sISMLog, LogLevel::Error, + (" MaybeOnEditableStateDisabled(), FAILED due to " + "the widget for the managing the nsPresContext has gone")); + return; + } + + const OwningNonNull<nsIWidget> textInputHandlingWidget = + *sTextInputHandlingWidget; + +#ifdef DEBUG + { + nsCOMPtr<nsIWidget> currentTextInputHandlingWidget = + aPresContext.GetTextInputHandlingWidget(); + MOZ_ASSERT(!currentTextInputHandlingWidget || + currentTextInputHandlingWidget == textInputHandlingWidget); + } +#endif // DEBUG + + const IMEState newIMEState = GetNewIMEState(aPresContext, aElement); + // If IME state becomes editable, HTMLEditor should also be initialized with + // same path as it gets focus. Therefore, IMEStateManager::UpdateIMEState + // should be called by HTMLEditor instead. + if (newIMEState.IsEditable()) { + MOZ_LOG(sISMLog, LogLevel::Debug, + (" MaybeOnEditableStateDisabled(), " + "does nothing because IME state does not become disabled")); + return; + } + + // Otherwise, disable IME on the widget and destroy active IMEContentObserver + // if there is. + const InputContext inputContext = textInputHandlingWidget->GetInputContext(); + if (inputContext.mIMEState.mEnabled == newIMEState.mEnabled) { + MOZ_LOG(sISMLog, LogLevel::Debug, + (" MaybeOnEditableStateDisabled(), " + "does nothing because IME state is not changed")); + return; + } + + if (sActiveIMEContentObserver) { + DestroyIMEContentObserver(); + } + + InputContextAction action(InputContextAction::CAUSE_UNKNOWN, + InputContextAction::FOCUS_NOT_CHANGED); + SetIMEState(newIMEState, &aPresContext, aElement, textInputHandlingWidget, + action, sOrigin); +} + +// static +void IMEStateManager::UpdateIMEState(const IMEState& aNewIMEState, + Element* aElement, EditorBase& aEditorBase, + const UpdateIMEStateOptions& aOptions) { + MOZ_LOG( + sISMLog, LogLevel::Info, + ("UpdateIMEState(aNewIMEState=%s, aElement=0x%p, aEditorBase=0x%p, " + "aOptions=0x%0x), sFocusedPresContext=0x%p (available: %s), " + "sFocusedElement=0x%p, sTextInputHandlingWidget=0x%p (available: %s), " + "sActiveIMEContentObserver=0x%p, sIsGettingNewIMEState=%s", + ToString(aNewIMEState).c_str(), aElement, &aEditorBase, + aOptions.serialize(), sFocusedPresContext.get(), + GetBoolName(CanHandleWith(sFocusedPresContext)), sFocusedElement.get(), + sTextInputHandlingWidget, + GetBoolName(sTextInputHandlingWidget && + !sTextInputHandlingWidget->Destroyed()), + sActiveIMEContentObserver.get(), GetBoolName(sIsGettingNewIMEState))); + + if (sIsGettingNewIMEState) { + MOZ_LOG(sISMLog, LogLevel::Debug, + (" UpdateIMEState(), " + "does nothing because of called while getting new IME state")); + return; + } + + RefPtr<PresShell> presShell(aEditorBase.GetPresShell()); + if (NS_WARN_IF(!presShell)) { + MOZ_LOG(sISMLog, LogLevel::Error, + (" UpdateIMEState(), FAILED due to " + "editor doesn't have PresShell")); + return; + } + + const RefPtr<nsPresContext> presContext = + aElement + ? aElement->GetPresContext(Element::PresContextFor::eForComposedDoc) + : aEditorBase.GetPresContext(); + if (NS_WARN_IF(!presContext)) { + MOZ_LOG(sISMLog, LogLevel::Error, + (" UpdateIMEState(), FAILED due to " + "editor doesn't have PresContext")); + return; + } + + // IMEStateManager::UpdateIMEState() should be called after + // IMEStateManager::OnChangeFocus() is called for setting focus to aElement + // and aEditorBase. However, when aEditorBase is an HTMLEditor, this may be + // called by nsIEditor::PostCreate() before IMEStateManager::OnChangeFocus(). + // Similarly, when aEditorBase is a TextEditor, this may be called by + // nsIEditor::SetFlags(). In such cases, this method should do nothing + // because input context should be updated when + // IMEStateManager::OnChangeFocus() is called later. + if (sFocusedPresContext != presContext) { + MOZ_LOG(sISMLog, LogLevel::Warning, + (" UpdateIMEState(), does nothing due to " + "the editor hasn't managed by IMEStateManager yet")); + return; + } + + // If IMEStateManager doesn't manage any document, this cannot update IME + // state of any widget. + if (NS_WARN_IF(!sFocusedPresContext)) { + MOZ_LOG(sISMLog, LogLevel::Error, + (" UpdateIMEState(), FAILED due to " + "no managing nsPresContext")); + return; + } + + if (NS_WARN_IF(!sTextInputHandlingWidget) || + NS_WARN_IF(sTextInputHandlingWidget->Destroyed())) { + MOZ_LOG(sISMLog, LogLevel::Error, + (" UpdateIMEState(), FAILED due to " + "the widget for the managing nsPresContext has gone")); + return; + } + + const OwningNonNull<nsIWidget> textInputHandlingWidget = + *sTextInputHandlingWidget; + +#ifdef DEBUG + { + nsCOMPtr<nsIWidget> currentTextInputHandlingWidget = + sFocusedPresContext->GetTextInputHandlingWidget(); + MOZ_ASSERT(!currentTextInputHandlingWidget || + currentTextInputHandlingWidget == textInputHandlingWidget); + } +#endif // DEBUG + + // TODO: Investigate if we could put off to initialize IMEContentObserver + // later because a lot of callers need to be marked as + // MOZ_CAN_RUN_SCRIPT otherwise. + + // Even if there is active IMEContentObserver, it may not be observing the + // editor with current editable root content due to reframed. In such case, + // We should try to reinitialize the IMEContentObserver. + if (sActiveIMEContentObserver && IsIMEObserverNeeded(aNewIMEState)) { + MOZ_LOG(sISMLog, LogLevel::Debug, + (" UpdateIMEState(), try to reinitialize the " + "active IMEContentObserver")); + OwningNonNull<IMEContentObserver> contentObserver = + *sActiveIMEContentObserver; + OwningNonNull<nsPresContext> presContext = *sFocusedPresContext; + if (!contentObserver->MaybeReinitialize( + textInputHandlingWidget, presContext, aElement, aEditorBase)) { + MOZ_LOG(sISMLog, LogLevel::Error, + (" UpdateIMEState(), failed to reinitialize the " + "active IMEContentObserver")); + } + if (NS_WARN_IF(textInputHandlingWidget->Destroyed())) { + MOZ_LOG(sISMLog, LogLevel::Error, + (" UpdateIMEState(), widget has gone during re-initializing the " + "active IMEContentObserver")); + return; + } + } + + // If there is no active IMEContentObserver or it isn't observing the + // editor correctly, we should recreate it. + const bool createTextStateManager = + IsIMEObserverNeeded(aNewIMEState) && + (!sActiveIMEContentObserver || + !sActiveIMEContentObserver->IsManaging(*sFocusedPresContext, aElement)); + // If we're recreating new IMEContentObserver or new state does not need + // IMEContentObserver, destroy the active IMEContentObserver. + const bool destroyTextStateManager = + sActiveIMEContentObserver && + (createTextStateManager || !IsIMEObserverNeeded(aNewIMEState)); + + const bool updateIMEState = + aOptions.contains(UpdateIMEStateOption::ForceUpdate) || + (textInputHandlingWidget->GetInputContext().mIMEState.mEnabled != + aNewIMEState.mEnabled); + if (NS_WARN_IF(textInputHandlingWidget->Destroyed())) { + MOZ_LOG( + sISMLog, LogLevel::Error, + (" UpdateIMEState(), widget has gone during getting input context")); + return; + } + + if (updateIMEState && + !aOptions.contains(UpdateIMEStateOption::DontCommitComposition)) { + // commit current composition before modifying IME state. + NotifyIME(REQUEST_TO_COMMIT_COMPOSITION, textInputHandlingWidget, + sFocusedIMEBrowserParent); + if (NS_WARN_IF(textInputHandlingWidget->Destroyed())) { + MOZ_LOG(sISMLog, LogLevel::Error, + (" UpdateIMEState(), widget has gone during committing " + "composition")); + return; + } + // FIXME: If committing composition changes IME state recursively, we should + // not keep updating IME state here. However, how can we manage it? + // Is a generation of the state is required? + } + + if (destroyTextStateManager) { + DestroyIMEContentObserver(); + if (NS_WARN_IF(textInputHandlingWidget->Destroyed()) || + NS_WARN_IF(sTextInputHandlingWidget != textInputHandlingWidget)) { + MOZ_LOG(sISMLog, LogLevel::Error, + (" UpdateIMEState(), has set input context, but the widget is " + "not focused")); + return; + } + } + + if (updateIMEState) { + InputContextAction action(InputContextAction::CAUSE_UNKNOWN, + InputContextAction::FOCUS_NOT_CHANGED); + RefPtr<nsPresContext> editorPresContext = aEditorBase.GetPresContext(); + if (NS_WARN_IF(!editorPresContext)) { + MOZ_LOG(sISMLog, LogLevel::Error, + (" UpdateIMEState(), nsPresContext for editor has already been " + "lost")); + return; + } + SetIMEState(aNewIMEState, editorPresContext, aElement, + textInputHandlingWidget, action, sOrigin); + if (NS_WARN_IF(textInputHandlingWidget->Destroyed()) || + NS_WARN_IF(sTextInputHandlingWidget != textInputHandlingWidget)) { + MOZ_LOG(sISMLog, LogLevel::Error, + (" UpdateIMEState(), has set input context, but the widget is " + "not focused")); + return; + } + if (NS_WARN_IF( + sTextInputHandlingWidget->GetInputContext().mIMEState.mEnabled != + aNewIMEState.mEnabled)) { + MOZ_LOG(sISMLog, LogLevel::Error, + (" UpdateIMEState(), has set input context, but IME enabled " + "state was overridden by somebody else")); + return; + } + } + + NS_ASSERTION(IsFocusedElement(*presContext, aElement), + "aElement does not match with sFocusedElement"); + + if (createTextStateManager) { + if (!sActiveIMEContentObserver && sFocusedPresContext && + sTextInputHandlingWidget) { + // XXX In this case, it might not be enough safe to notify IME of + // anything. So, don't try to flush pending notifications of + // IMEContentObserver here. + CreateIMEContentObserver(aEditorBase, aElement); + } else { + MOZ_LOG(sISMLog, LogLevel::Error, + (" UpdateIMEState(), wanted to create IMEContentObserver, but " + "lost focus")); + } + } +} + +// static +IMEState IMEStateManager::GetNewIMEState(const nsPresContext& aPresContext, + Element* aElement) { + MOZ_LOG( + sISMLog, LogLevel::Info, + ("GetNewIMEState(aPresContext=0x%p, aElement=0x%p), " + "sInstalledMenuKeyboardListener=%s", + &aPresContext, aElement, GetBoolName(sInstalledMenuKeyboardListener))); + + if (!CanHandleWith(&aPresContext)) { + MOZ_LOG(sISMLog, LogLevel::Debug, + (" GetNewIMEState() returns IMEEnabled::Disabled because " + "the nsPresContext has been destroyed")); + return IMEState(IMEEnabled::Disabled); + } + + // On Printing or Print Preview, we don't need IME. + if (aPresContext.Type() == nsPresContext::eContext_PrintPreview || + aPresContext.Type() == nsPresContext::eContext_Print) { + MOZ_LOG(sISMLog, LogLevel::Debug, + (" GetNewIMEState() returns IMEEnabled::Disabled because " + "the nsPresContext is for print or print preview")); + return IMEState(IMEEnabled::Disabled); + } + + if (sInstalledMenuKeyboardListener) { + MOZ_LOG(sISMLog, LogLevel::Debug, + (" GetNewIMEState() returns IMEEnabled::Disabled because " + "menu keyboard listener was installed")); + return IMEState(IMEEnabled::Disabled); + } + + if (!aElement) { + // Even if there are no focused content, the focused document might be + // editable, such case is design mode. + if (aPresContext.Document() && aPresContext.Document()->IsInDesignMode()) { + MOZ_LOG(sISMLog, LogLevel::Debug, + (" GetNewIMEState() returns IMEEnabled::Enabled because " + "design mode editor has focus")); + return IMEState(IMEEnabled::Enabled); + } + MOZ_LOG(sISMLog, LogLevel::Debug, + (" GetNewIMEState() returns IMEEnabled::Disabled because " + "no content has focus")); + return IMEState(IMEEnabled::Disabled); + } + + // If aElement is in designMode, aElement should be the root node of the + // document. + if (aElement && aElement->IsInDesignMode()) { + MOZ_LOG(sISMLog, LogLevel::Debug, + (" GetNewIMEState() returns IMEEnabled::Enabled because " + "a content node in design mode editor has focus")); + return IMEState(IMEEnabled::Enabled); + } + + // nsIContent::GetDesiredIMEState() may cause a call of UpdateIMEState() + // from EditorBase::PostCreate() because GetDesiredIMEState() needs to + // retrieve an editor instance for the element if it's editable element. + // For avoiding such nested IME state updates, we should set + // sIsGettingNewIMEState here and UpdateIMEState() should check it. + GettingNewIMEStateBlocker blocker; + + IMEState newIMEState = aElement->GetDesiredIMEState(); + MOZ_LOG(sISMLog, LogLevel::Debug, + (" GetNewIMEState() returns %s", ToString(newIMEState).c_str())); + return newIMEState; +} + +// static +void IMEStateManager::ResetActiveChildInputContext() { + sActiveChildInputContext.mIMEState.mEnabled = IMEEnabled::Unknown; +} + +// static +bool IMEStateManager::HasActiveChildSetInputContext() { + return sActiveChildInputContext.mIMEState.mEnabled != IMEEnabled::Unknown; +} + +// static +void IMEStateManager::SetInputContextForChildProcess( + BrowserParent* aBrowserParent, const InputContext& aInputContext, + const InputContextAction& aAction) { + MOZ_LOG( + sISMLog, LogLevel::Info, + ("SetInputContextForChildProcess(aBrowserParent=0x%p, " + "aInputContext=%s , aAction={ mCause=%s, mAction=%s }), " + "sFocusedPresContext=0x%p (available: %s), " + "sTextInputHandlingWidget=0x%p (available: %s), " + "BrowserParent::GetFocused()=0x%p, sInstalledMenuKeyboardListener=%s", + aBrowserParent, ToString(aInputContext).c_str(), + ToString(aAction.mCause).c_str(), ToString(aAction.mFocusChange).c_str(), + sFocusedPresContext.get(), + GetBoolName(CanHandleWith(sFocusedPresContext)), + sTextInputHandlingWidget, + GetBoolName(sTextInputHandlingWidget && + !sTextInputHandlingWidget->Destroyed()), + BrowserParent::GetFocused(), + GetBoolName(sInstalledMenuKeyboardListener))); + + if (aBrowserParent != BrowserParent::GetFocused()) { + MOZ_LOG(sISMLog, LogLevel::Error, + (" SetInputContextForChildProcess(), FAILED, " + "because non-focused tab parent tries to set input context")); + return; + } + + if (NS_WARN_IF(!CanHandleWith(sFocusedPresContext))) { + MOZ_LOG(sISMLog, LogLevel::Error, + (" SetInputContextForChildProcess(), FAILED, " + "due to no focused presContext")); + return; + } + + if (NS_WARN_IF(!sTextInputHandlingWidget) || + NS_WARN_IF(sTextInputHandlingWidget->Destroyed())) { + MOZ_LOG(sISMLog, LogLevel::Error, + (" SetInputContextForChildProcess(), FAILED, " + "due to the widget for the nsPresContext has gone")); + return; + } + + const OwningNonNull<nsIWidget> textInputHandlingWidget = + *sTextInputHandlingWidget; +#ifdef DEBUG + { + nsCOMPtr<nsIWidget> currentTextInputHandlingWidget = + sFocusedPresContext->GetTextInputHandlingWidget(); + MOZ_ASSERT(!currentTextInputHandlingWidget || + currentTextInputHandlingWidget == textInputHandlingWidget); + } +#endif // DEBUG + MOZ_ASSERT(aInputContext.mOrigin == InputContext::ORIGIN_CONTENT); + + sActiveChildInputContext = aInputContext; + MOZ_ASSERT(HasActiveChildSetInputContext()); + + // If input context is changed in remote process while menu keyboard listener + // is installed, this process shouldn't set input context now. When it's + // uninstalled, input context should be restored from + // sActiveChildInputContext. + if (sInstalledMenuKeyboardListener) { + MOZ_LOG(sISMLog, LogLevel::Info, + (" SetInputContextForChildProcess(), waiting to set input context " + "until menu keyboard listener is uninstalled")); + return; + } + + SetInputContext(textInputHandlingWidget, aInputContext, aAction); +} + +MOZ_CAN_RUN_SCRIPT static bool IsNextFocusableElementTextControl( + const Element* aInputContent) { + RefPtr<nsFocusManager> fm = nsFocusManager::GetFocusManager(); + if (MOZ_UNLIKELY(!fm)) { + return false; + } + nsCOMPtr<nsIContent> nextContent; + const RefPtr<Element> inputContent = const_cast<Element*>(aInputContent); + const nsCOMPtr<nsPIDOMWindowOuter> outerWindow = + aInputContent->OwnerDoc()->GetWindow(); + nsresult rv = fm->DetermineElementToMoveFocus( + outerWindow, inputContent, nsIFocusManager::MOVEFOCUS_FORWARD, true, + false, getter_AddRefs(nextContent)); + if (NS_WARN_IF(NS_FAILED(rv)) || !nextContent) { + return false; + } + nextContent = nextContent->FindFirstNonChromeOnlyAccessContent(); + nsCOMPtr<nsIFormControl> nextControl = do_QueryInterface(nextContent); + if (!nextControl || !nextControl->IsTextControl(false)) { + return false; + } + + // XXX We don't consider next element is date/time control yet. + nsGenericHTMLElement* nextElement = + nsGenericHTMLElement::FromNodeOrNull(nextContent); + if (!nextElement) { + return false; + } + bool focusable = false; + nextElement->IsHTMLFocusable(false, &focusable, nullptr); + if (!focusable) { + return false; + } + + // Check readonly attribute. + if (nextElement->IsHTMLElement(nsGkAtoms::textarea)) { + HTMLTextAreaElement* textAreaElement = + HTMLTextAreaElement::FromNodeOrNull(nextElement); + return !textAreaElement->ReadOnly(); + } + + // If neither textarea nor input, what element type? + MOZ_DIAGNOSTIC_ASSERT(nextElement->IsHTMLElement(nsGkAtoms::input)); + + HTMLInputElement* inputElement = + HTMLInputElement::FromNodeOrNull(nextElement); + return !inputElement->ReadOnly(); +} + +static void GetInputType(const IMEState& aState, const nsIContent& aContent, + nsAString& aInputType) { + // NOTE: If you change here, you may need to update + // widget::InputContext::GatNativeKeyBindings too. + if (aContent.IsHTMLElement(nsGkAtoms::input)) { + const HTMLInputElement* inputElement = + HTMLInputElement::FromNode(&aContent); + if (inputElement->HasBeenTypePassword() && aState.IsEditable()) { + aInputType.AssignLiteral("password"); + } else { + inputElement->GetType(aInputType); + } + } else if (aContent.IsHTMLElement(nsGkAtoms::textarea)) { + aInputType.Assign(nsGkAtoms::textarea->GetUTF16String()); + } +} + +MOZ_CAN_RUN_SCRIPT static void GetActionHint(const IMEState& aState, + const nsIContent& aContent, + nsAString& aActionHint) { + MOZ_ASSERT(aContent.IsHTMLElement()); + + if (aState.IsEditable() && StaticPrefs::dom_forms_enterkeyhint()) { + nsGenericHTMLElement::FromNode(&aContent)->GetEnterKeyHint(aActionHint); + + // If enterkeyhint is set, we don't infer action hint. + if (!aActionHint.IsEmpty()) { + return; + } + } + + if (!aContent.IsAnyOfHTMLElements(nsGkAtoms::input, nsGkAtoms::textarea)) { + return; + } + + // XXX This is old compatibility, but we might be able to remove this. + aContent.AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::moz_action_hint, + aActionHint); + + if (!aActionHint.IsEmpty()) { + ToLowerCase(aActionHint); + return; + } + + // Get the input content corresponding to the focused node, + // which may be an anonymous child of the input content. + MOZ_ASSERT(&aContent == aContent.FindFirstNonChromeOnlyAccessContent()); + const HTMLInputElement* inputElement = HTMLInputElement::FromNode(aContent); + if (!inputElement) { + return; + } + + // If we don't have an action hint and + // return won't submit the form, use "maybenext". + bool willSubmit = false; + bool isLastElement = false; + HTMLFormElement* formElement = inputElement->GetForm(); + // is this a form and does it have a default submit element? + if (formElement) { + if (formElement->IsLastActiveElement(inputElement)) { + isLastElement = true; + } + + if (formElement->GetDefaultSubmitElement()) { + willSubmit = true; + // is this an html form... + } else { + // ... and does it only have a single text input element ? + if (!formElement->ImplicitSubmissionIsDisabled() || + // ... or is this the last non-disabled element? + isLastElement) { + willSubmit = true; + } + } + } + + if (!isLastElement && formElement) { + // If next tabbable content in form is text control, hint should be "next" + // even there is submit in form. + // MOZ_KnownLive(inputElement) because it's an alias of aContent. + if (IsNextFocusableElementTextControl(MOZ_KnownLive(inputElement))) { + // This is focusable text control + // XXX What good hint for read only field? + aActionHint.AssignLiteral("maybenext"); + return; + } + } + + if (!willSubmit) { + return; + } + + if (inputElement->ControlType() == FormControlType::InputSearch) { + aActionHint.AssignLiteral("search"); + return; + } + + aActionHint.AssignLiteral("go"); +} + +static void GetInputMode(const IMEState& aState, const nsIContent& aContent, + nsAString& aInputMode) { + if (aState.IsEditable() && + (StaticPrefs::dom_forms_inputmode() || + nsContentUtils::IsChromeDoc(aContent.OwnerDoc()))) { + aContent.AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::inputmode, + aInputMode); + if (aContent.IsHTMLElement(nsGkAtoms::input) && + aInputMode.EqualsLiteral("mozAwesomebar")) { + if (!nsContentUtils::IsChromeDoc(aContent.OwnerDoc())) { + // mozAwesomebar should be allowed only in chrome + aInputMode.Truncate(); + } + } else { + ToLowerCase(aInputMode); + } + } +} + +static void GetAutocapitalize(const IMEState& aState, const Element& aElement, + const InputContext& aInputContext, + nsAString& aAutocapitalize) { + if (aElement.IsHTMLElement() && aState.IsEditable() && + StaticPrefs::dom_forms_autocapitalize() && + aInputContext.IsAutocapitalizeSupported()) { + nsGenericHTMLElement::FromNode(&aElement)->GetAutocapitalize( + aAutocapitalize); + } +} + +// static +void IMEStateManager::SetIMEState(const IMEState& aState, + const nsPresContext* aPresContext, + Element* aElement, nsIWidget& aWidget, + InputContextAction aAction, + InputContext::Origin aOrigin) { + MOZ_LOG(sISMLog, LogLevel::Info, + ("SetIMEState(aState=%s, nsPresContext=0x%p, aElement=0x%p " + "(BrowserParent=0x%p), aWidget=0x%p, aAction={ mCause=%s, " + "mFocusChange=%s }, aOrigin=%s)", + ToString(aState).c_str(), aPresContext, aElement, + BrowserParent::GetFrom(aElement), &aWidget, + ToString(aAction.mCause).c_str(), + ToString(aAction.mFocusChange).c_str(), ToChar(aOrigin))); + + InputContext context; + context.mIMEState = aState; + if (aPresContext) { + if (nsIURI* uri = aPresContext->Document()->GetDocumentURI()) { + // We don't need to and should not expose special URLs such as: + // about: Any apps like IME should work normally and constantly in any + // default pages such as about:blank, about:home, etc in either + // the main process or a content process. + // blob: This may contain big data. If we copy it to the main process, + // it may make the heap dirty which makes the process slower. + // chrome: Same as about, any apps should work normally and constantly in + // any chrome documents. + // data: Any native apps in the environment shouldn't change the behavior + // with the data URL's content and it may contain too big data. + // file: The file path may contain private things and we shouldn't let + // other apps like IME know which one is touched by the user because + // malicious text services may like files which are explicitly used + // by the user better. + if (uri->SchemeIs("http") || uri->SchemeIs("https")) { + // Note that we don't need to expose UserPass, Query and Reference to + // IME since they may contain sensitive data, but non-malicious text + // services must not require these data. + nsCOMPtr<nsIURI> exposableURL; + if (NS_SUCCEEDED(NS_MutateURI(uri) + .SetQuery(""_ns) + .SetRef(""_ns) + .SetUserPass(""_ns) + .Finalize(exposableURL))) { + context.mURI = std::move(exposableURL); + } + } + } + } + context.mOrigin = aOrigin; + + context.mHasHandledUserInput = + aPresContext && aPresContext->PresShell()->HasHandledUserInput(); + + context.mInPrivateBrowsing = + aPresContext && + nsContentUtils::IsInPrivateBrowsing(aPresContext->Document()); + + const RefPtr<Element> focusedElement = + aElement ? Element::FromNodeOrNull( + aElement->FindFirstNonChromeOnlyAccessContent()) + : nullptr; + + if (focusedElement && focusedElement->IsHTMLElement()) { + GetInputType(aState, *focusedElement, context.mHTMLInputType); + GetActionHint(aState, *focusedElement, context.mActionHint); + GetInputMode(aState, *focusedElement, context.mHTMLInputMode); + GetAutocapitalize(aState, *focusedElement, context, + context.mAutocapitalize); + } + + if (aAction.mCause == InputContextAction::CAUSE_UNKNOWN && + nsContentUtils::LegacyIsCallerChromeOrNativeCode()) { + aAction.mCause = InputContextAction::CAUSE_UNKNOWN_CHROME; + } + + if ((aAction.mCause == InputContextAction::CAUSE_UNKNOWN || + aAction.mCause == InputContextAction::CAUSE_UNKNOWN_CHROME) && + UserActivation::IsHandlingUserInput()) { + aAction.mCause = + UserActivation::IsHandlingKeyboardInput() + ? InputContextAction::CAUSE_UNKNOWN_DURING_KEYBOARD_INPUT + : InputContextAction::CAUSE_UNKNOWN_DURING_NON_KEYBOARD_INPUT; + } + + SetInputContext(aWidget, context, aAction); +} + +// static +void IMEStateManager::SetInputContext(nsIWidget& aWidget, + const InputContext& aInputContext, + const InputContextAction& aAction) { + MOZ_LOG( + sISMLog, LogLevel::Info, + ("SetInputContext(aWidget=0x%p, aInputContext=%s, " + "aAction={ mCause=%s, mAction=%s }), BrowserParent::GetFocused()=0x%p", + &aWidget, ToString(aInputContext).c_str(), + ToString(aAction.mCause).c_str(), ToString(aAction.mFocusChange).c_str(), + BrowserParent::GetFocused())); + + OwningNonNull<nsIWidget> widget = aWidget; + widget->SetInputContext(aInputContext, aAction); + sActiveInputContextWidget = widget; +} + +// static +void IMEStateManager::EnsureTextCompositionArray() { + if (sTextCompositions) { + return; + } + sTextCompositions = new TextCompositionArray(); +} + +// static +void IMEStateManager::DispatchCompositionEvent( + nsINode* aEventTargetNode, nsPresContext* aPresContext, + BrowserParent* aBrowserParent, WidgetCompositionEvent* aCompositionEvent, + nsEventStatus* aStatus, EventDispatchingCallback* aCallBack, + bool aIsSynthesized) { + MOZ_LOG( + sISMLog, LogLevel::Info, + ("DispatchCompositionEvent(aNode=0x%p, " + "aPresContext=0x%p, aCompositionEvent={ mMessage=%s, " + "mNativeIMEContext={ mRawNativeIMEContext=0x%" PRIXPTR ", " + "mOriginProcessID=0x%" PRIX64 " }, mWidget(0x%p)={ " + "GetNativeIMEContext()={ mRawNativeIMEContext=0x%" PRIXPTR ", " + "mOriginProcessID=0x%" PRIX64 " }, Destroyed()=%s }, " + "mFlags={ mIsTrusted=%s, mPropagationStopped=%s } }, " + "aIsSynthesized=%s), browserParent=%p", + aEventTargetNode, aPresContext, ToChar(aCompositionEvent->mMessage), + aCompositionEvent->mNativeIMEContext.mRawNativeIMEContext, + aCompositionEvent->mNativeIMEContext.mOriginProcessID, + aCompositionEvent->mWidget.get(), + aCompositionEvent->mWidget->GetNativeIMEContext().mRawNativeIMEContext, + aCompositionEvent->mWidget->GetNativeIMEContext().mOriginProcessID, + GetBoolName(aCompositionEvent->mWidget->Destroyed()), + GetBoolName(aCompositionEvent->mFlags.mIsTrusted), + GetBoolName(aCompositionEvent->mFlags.mPropagationStopped), + GetBoolName(aIsSynthesized), aBrowserParent)); + + if (NS_WARN_IF(!aCompositionEvent->IsTrusted()) || + NS_WARN_IF(aCompositionEvent->PropagationStopped())) { + return; + } + + MOZ_ASSERT(aCompositionEvent->mMessage != eCompositionUpdate, + "compositionupdate event shouldn't be dispatched manually"); + + EnsureTextCompositionArray(); + + RefPtr<TextComposition> composition = + sTextCompositions->GetCompositionFor(aCompositionEvent); + if (!composition) { + // If synthesized event comes after delayed native composition events + // for request of commit or cancel, we should ignore it. + if (NS_WARN_IF(aIsSynthesized)) { + return; + } + MOZ_LOG(sISMLog, LogLevel::Debug, + (" DispatchCompositionEvent(), " + "adding new TextComposition to the array")); + MOZ_ASSERT(aCompositionEvent->mMessage == eCompositionStart); + composition = new TextComposition(aPresContext, aEventTargetNode, + aBrowserParent, aCompositionEvent); + sTextCompositions->AppendElement(composition); + } +#ifdef DEBUG + else { + MOZ_ASSERT(aCompositionEvent->mMessage != eCompositionStart); + } +#endif // #ifdef DEBUG + + // Dispatch the event on composing target. + composition->DispatchCompositionEvent(aCompositionEvent, aStatus, aCallBack, + aIsSynthesized); + + // WARNING: the |composition| might have been destroyed already. + + // Remove the ended composition from the array. + // NOTE: When TextComposition is synthesizing compositionend event for + // emulating a commit, the instance shouldn't be removed from the array + // because IME may perform it later. Then, we need to ignore the + // following commit events in TextComposition::DispatchEvent(). + // However, if commit or cancel for a request is performed synchronously + // during not safe to dispatch events, PresShell must have discarded + // compositionend event. Then, the synthesized compositionend event is + // the last event for the composition. In this case, we need to + // destroy the TextComposition with synthesized compositionend event. + if ((!aIsSynthesized || + composition->WasNativeCompositionEndEventDiscarded()) && + aCompositionEvent->CausesDOMCompositionEndEvent()) { + TextCompositionArray::index_type i = + sTextCompositions->IndexOf(aCompositionEvent->mWidget); + if (i != TextCompositionArray::NoIndex) { + MOZ_LOG( + sISMLog, LogLevel::Debug, + (" DispatchCompositionEvent(), " + "removing TextComposition from the array since NS_COMPOSTION_END " + "was dispatched")); + sTextCompositions->ElementAt(i)->Destroy(); + sTextCompositions->RemoveElementAt(i); + } + } +} + +// static +IMEContentObserver* IMEStateManager::GetActiveContentObserver() { + return sActiveIMEContentObserver; +} + +// static +nsIContent* IMEStateManager::GetRootContent(nsPresContext* aPresContext) { + Document* doc = aPresContext->Document(); + if (NS_WARN_IF(!doc)) { + return nullptr; + } + return doc->GetRootElement(); +} + +// static +void IMEStateManager::HandleSelectionEvent( + nsPresContext* aPresContext, nsIContent* aEventTargetContent, + WidgetSelectionEvent* aSelectionEvent) { + RefPtr<BrowserParent> browserParent = GetActiveBrowserParent(); + if (!browserParent) { + browserParent = BrowserParent::GetFrom(aEventTargetContent + ? aEventTargetContent + : GetRootContent(aPresContext)); + } + + MOZ_LOG( + sISMLog, LogLevel::Info, + ("HandleSelectionEvent(aPresContext=0x%p, " + "aEventTargetContent=0x%p, aSelectionEvent={ mMessage=%s, " + "mFlags={ mIsTrusted=%s } }), browserParent=%p", + aPresContext, aEventTargetContent, ToChar(aSelectionEvent->mMessage), + GetBoolName(aSelectionEvent->mFlags.mIsTrusted), browserParent.get())); + + if (!aSelectionEvent->IsTrusted()) { + return; + } + + RefPtr<TextComposition> composition = + sTextCompositions + ? sTextCompositions->GetCompositionFor(aSelectionEvent->mWidget) + : nullptr; + if (composition) { + // When there is a composition, TextComposition should guarantee that the + // selection event will be handled in same target as composition events. + composition->HandleSelectionEvent(aSelectionEvent); + } else { + // When there is no composition, the selection event should be handled + // in the aPresContext or browserParent. + TextComposition::HandleSelectionEvent(aPresContext, browserParent, + aSelectionEvent); + } +} + +// static +void IMEStateManager::OnCompositionEventDiscarded( + WidgetCompositionEvent* aCompositionEvent) { + // Note that this method is never called for synthesized events for emulating + // commit or cancel composition. + + MOZ_LOG( + sISMLog, LogLevel::Info, + ("OnCompositionEventDiscarded(aCompositionEvent={ " + "mMessage=%s, mNativeIMEContext={ mRawNativeIMEContext=0x%" PRIXPTR ", " + "mOriginProcessID=0x%" PRIX64 " }, mWidget(0x%p)={ " + "GetNativeIMEContext()={ mRawNativeIMEContext=0x%" PRIXPTR ", " + "mOriginProcessID=0x%" PRIX64 " }, Destroyed()=%s }, " + "mFlags={ mIsTrusted=%s } })", + ToChar(aCompositionEvent->mMessage), + aCompositionEvent->mNativeIMEContext.mRawNativeIMEContext, + aCompositionEvent->mNativeIMEContext.mOriginProcessID, + aCompositionEvent->mWidget.get(), + aCompositionEvent->mWidget->GetNativeIMEContext().mRawNativeIMEContext, + aCompositionEvent->mWidget->GetNativeIMEContext().mOriginProcessID, + GetBoolName(aCompositionEvent->mWidget->Destroyed()), + GetBoolName(aCompositionEvent->mFlags.mIsTrusted))); + + if (!aCompositionEvent->IsTrusted()) { + return; + } + + // Ignore compositionstart for now because sTextCompositions may not have + // been created yet. + if (aCompositionEvent->mMessage == eCompositionStart) { + return; + } + + RefPtr<TextComposition> composition = + sTextCompositions->GetCompositionFor(aCompositionEvent->mWidget); + if (!composition) { + // If the PresShell has been being destroyed during composition, + // a TextComposition instance for the composition was already removed from + // the array and destroyed in OnDestroyPresContext(). Therefore, we may + // fail to retrieve a TextComposition instance here. + MOZ_LOG(sISMLog, LogLevel::Info, + (" OnCompositionEventDiscarded(), " + "TextComposition instance for the widget has already gone")); + return; + } + composition->OnCompositionEventDiscarded(aCompositionEvent); +} + +// static +nsresult IMEStateManager::NotifyIME(IMEMessage aMessage, nsIWidget* aWidget, + BrowserParent* aBrowserParent) { + return IMEStateManager::NotifyIME(IMENotification(aMessage), aWidget, + aBrowserParent); +} + +// static +nsresult IMEStateManager::NotifyIME(const IMENotification& aNotification, + nsIWidget* aWidget, + BrowserParent* aBrowserParent) { + MOZ_LOG(sISMLog, LogLevel::Info, + ("NotifyIME(aNotification={ mMessage=%s }, " + "aWidget=0x%p, aBrowserParent=0x%p), sFocusedIMEWidget=0x%p, " + "BrowserParent::GetFocused()=0x%p, sFocusedIMEBrowserParent=0x%p, " + "aBrowserParent == BrowserParent::GetFocused()=%s, " + "aBrowserParent == sFocusedIMEBrowserParent=%s, " + "CanSendNotificationToWidget()=%s", + ToChar(aNotification.mMessage), aWidget, aBrowserParent, + sFocusedIMEWidget, BrowserParent::GetFocused(), + sFocusedIMEBrowserParent.get(), + GetBoolName(aBrowserParent == BrowserParent::GetFocused()), + GetBoolName(aBrowserParent == sFocusedIMEBrowserParent), + GetBoolName(CanSendNotificationToWidget()))); + + if (NS_WARN_IF(!aWidget)) { + MOZ_LOG(sISMLog, LogLevel::Error, + (" NotifyIME(), FAILED due to no widget")); + return NS_ERROR_INVALID_ARG; + } + + switch (aNotification.mMessage) { + case NOTIFY_IME_OF_FOCUS: { + MOZ_ASSERT(CanSendNotificationToWidget()); + + // If focus notification comes from a remote browser which already lost + // focus, we shouldn't accept the focus notification. Then, following + // notifications from the process will be ignored. + if (aBrowserParent != BrowserParent::GetFocused()) { + MOZ_LOG(sISMLog, LogLevel::Warning, + (" NotifyIME(), WARNING, the received focus notification is " + "ignored, because its associated BrowserParent did not match" + "the focused BrowserParent.")); + return NS_OK; + } + // If IME focus is already set, first blur the currently-focused + // IME widget + if (sFocusedIMEWidget) { + // XXX Why don't we first request the previously-focused IME + // widget to commit the composition? + MOZ_ASSERT( + sFocusedIMEBrowserParent || aBrowserParent, + "This case shouldn't be caused by focus move in this process"); + MOZ_LOG(sISMLog, LogLevel::Warning, + (" NotifyIME(), WARNING, received focus notification with ") + "non-null sFocusedIMEWidget. How come " + "OnFocusMovedBetweenBrowsers did not blur it already?"); + nsCOMPtr<nsIWidget> focusedIMEWidget(sFocusedIMEWidget); + sFocusedIMEWidget = nullptr; + sFocusedIMEBrowserParent = nullptr; + focusedIMEWidget->NotifyIME(IMENotification(NOTIFY_IME_OF_BLUR)); + } +#ifdef DEBUG + if (aBrowserParent) { + nsCOMPtr<nsIWidget> browserParentWidget = + aBrowserParent->GetTextInputHandlingWidget(); + MOZ_ASSERT(browserParentWidget == aWidget); + } +#endif + sFocusedIMEBrowserParent = aBrowserParent; + sFocusedIMEWidget = aWidget; + nsCOMPtr<nsIWidget> widget(aWidget); + MOZ_LOG( + sISMLog, LogLevel::Info, + (" NotifyIME(), about to call widget->NotifyIME() for IME focus")); + return widget->NotifyIME(aNotification); + } + case NOTIFY_IME_OF_BLUR: { + if (aBrowserParent != sFocusedIMEBrowserParent) { + MOZ_LOG(sISMLog, LogLevel::Warning, + (" NotifyIME(), WARNING, the received blur notification is " + "ignored " + "because it's not from current focused IME browser")); + return NS_OK; + } + if (!sFocusedIMEWidget) { + MOZ_LOG( + sISMLog, LogLevel::Error, + (" NotifyIME(), WARNING, received blur notification but there is " + "no focused IME widget")); + return NS_OK; + } + if (NS_WARN_IF(sFocusedIMEWidget != aWidget)) { + MOZ_LOG(sISMLog, LogLevel::Warning, + (" NotifyIME(), WARNING, the received blur notification is " + "ignored " + "because it's not for current focused IME widget")); + return NS_OK; + } + nsCOMPtr<nsIWidget> focusedIMEWidget(sFocusedIMEWidget); + sFocusedIMEWidget = nullptr; + sFocusedIMEBrowserParent = nullptr; + return CanSendNotificationToWidget() + ? focusedIMEWidget->NotifyIME( + IMENotification(NOTIFY_IME_OF_BLUR)) + : NS_OK; + } + case NOTIFY_IME_OF_SELECTION_CHANGE: + case NOTIFY_IME_OF_TEXT_CHANGE: + case NOTIFY_IME_OF_POSITION_CHANGE: + case NOTIFY_IME_OF_MOUSE_BUTTON_EVENT: + case NOTIFY_IME_OF_COMPOSITION_EVENT_HANDLED: { + if (aBrowserParent != sFocusedIMEBrowserParent) { + MOZ_LOG( + sISMLog, LogLevel::Warning, + (" NotifyIME(), WARNING, the received content change notification " + "is ignored because it's not from current focused IME browser")); + return NS_OK; + } + if (!sFocusedIMEWidget) { + MOZ_LOG( + sISMLog, LogLevel::Warning, + (" NotifyIME(), WARNING, the received content change notification " + "is ignored because there is no focused IME widget")); + return NS_OK; + } + if (NS_WARN_IF(sFocusedIMEWidget != aWidget)) { + MOZ_LOG( + sISMLog, LogLevel::Warning, + (" NotifyIME(), WARNING, the received content change notification " + "is ignored because it's not for current focused IME widget")); + return NS_OK; + } + if (!CanSendNotificationToWidget()) { + return NS_OK; + } + nsCOMPtr<nsIWidget> widget(aWidget); + return widget->NotifyIME(aNotification); + } + default: + // Other notifications should be sent only when there is composition. + // So, we need to handle the others below. + break; + } + + if (!sTextCompositions) { + MOZ_LOG(sISMLog, LogLevel::Info, + (" NotifyIME(), the request to IME is ignored because " + "there have been no compositions yet")); + return NS_OK; + } + + RefPtr<TextComposition> composition = + sTextCompositions->GetCompositionFor(aWidget); + if (!composition) { + MOZ_LOG(sISMLog, LogLevel::Info, + (" NotifyIME(), the request to IME is ignored because " + "there is no active composition")); + return NS_OK; + } + + if (aBrowserParent != composition->GetBrowserParent()) { + MOZ_LOG( + sISMLog, LogLevel::Warning, + (" NotifyIME(), WARNING, the request to IME is ignored because " + "it does not come from the remote browser which has the composition " + "on aWidget")); + return NS_OK; + } + + switch (aNotification.mMessage) { + case REQUEST_TO_COMMIT_COMPOSITION: + return composition->RequestToCommit(aWidget, false); + case REQUEST_TO_CANCEL_COMPOSITION: + return composition->RequestToCommit(aWidget, true); + default: + MOZ_CRASH("Unsupported notification"); + } + MOZ_CRASH( + "Failed to handle the notification for non-synthesized composition"); + return NS_ERROR_FAILURE; +} + +// static +nsresult IMEStateManager::NotifyIME(IMEMessage aMessage, + nsPresContext* aPresContext, + BrowserParent* aBrowserParent) { + MOZ_LOG(sISMLog, LogLevel::Info, + ("NotifyIME(aMessage=%s, aPresContext=0x%p, aBrowserParent=0x%p)", + ToChar(aMessage), aPresContext, aBrowserParent)); + + if (NS_WARN_IF(!CanHandleWith(aPresContext))) { + return NS_ERROR_INVALID_ARG; + } + + nsCOMPtr<nsIWidget> widget = aPresContext->GetTextInputHandlingWidget(); + if (NS_WARN_IF(!widget)) { + MOZ_LOG(sISMLog, LogLevel::Error, + (" NotifyIME(), FAILED due to no widget for the " + "nsPresContext")); + return NS_ERROR_NOT_AVAILABLE; + } + return NotifyIME(aMessage, widget, aBrowserParent); +} + +// static +bool IMEStateManager::IsEditable(nsINode* node) { + if (node->IsEditable()) { + return true; + } + // |node| might be readwrite (for example, a text control) + if (node->IsElement() && + node->AsElement()->State().HasState(ElementState::READWRITE)) { + return true; + } + return false; +} + +// static +nsINode* IMEStateManager::GetRootEditableNode(const nsPresContext& aPresContext, + const Element* aElement) { + if (aElement) { + // If the focused content is in design mode, return is composed document + // because aElement may be in UA widget shadow tree. + if (aElement->IsInDesignMode()) { + return aElement->GetComposedDoc(); + } + + nsINode* candidateRootNode = const_cast<Element*>(aElement); + for (nsINode* node = candidateRootNode; node && IsEditable(node); + node = node->GetParentNode()) { + // If the node has independent selection like <input type="text"> or + // <textarea>, the node should be the root editable node for aElement. + // FYI: <select> element also has independent selection but IsEditable() + // returns false. + // XXX: If somebody adds new editable element which has independent + // selection but doesn't own editor, we'll need more checks here. + // XXX: If aElement is not in native anonymous subtree, checking + // independent selection must be wrong, see bug 1731005. + if (node->IsContent() && node->AsContent()->HasIndependentSelection()) { + return node; + } + candidateRootNode = node; + } + return candidateRootNode; + } + + return aPresContext.Document() && aPresContext.Document()->IsInDesignMode() + ? aPresContext.Document() + : nullptr; +} + +// static +bool IMEStateManager::IsIMEObserverNeeded(const IMEState& aState) { + return aState.IsEditable(); +} + +// static +void IMEStateManager::DestroyIMEContentObserver() { + if (!sActiveIMEContentObserver) { + MOZ_LOG(sISMLog, LogLevel::Verbose, + ("DestroyIMEContentObserver() does nothing")); + return; + } + + MOZ_LOG(sISMLog, LogLevel::Info, + ("DestroyIMEContentObserver(), destroying " + "the active IMEContentObserver...")); + RefPtr<IMEContentObserver> tsm = sActiveIMEContentObserver.get(); + sActiveIMEContentObserver = nullptr; + tsm->Destroy(); +} + +// static +void IMEStateManager::CreateIMEContentObserver(EditorBase& aEditorBase, + Element* aFocusedElement) { + MOZ_ASSERT(!sActiveIMEContentObserver); + MOZ_ASSERT(sTextInputHandlingWidget); + MOZ_ASSERT(sFocusedPresContext); + MOZ_ASSERT(IsIMEObserverNeeded( + sTextInputHandlingWidget->GetInputContext().mIMEState)); + + MOZ_LOG(sISMLog, LogLevel::Info, + ("CreateIMEContentObserver(aEditorBase=0x%p, aFocusedElement=0x%p), " + "sFocusedPresContext=0x%p, sFocusedElement=0x%p, " + "sTextInputHandlingWidget=0x%p (available: %s), " + "sActiveIMEContentObserver=0x%p, " + "sActiveIMEContentObserver->IsManaging(sFocusedPresContext, " + "sFocusedElement)=%s", + &aEditorBase, aFocusedElement, sFocusedPresContext.get(), + sFocusedElement.get(), sTextInputHandlingWidget, + GetBoolName(sTextInputHandlingWidget && + !sTextInputHandlingWidget->Destroyed()), + sActiveIMEContentObserver.get(), + GetBoolName(sActiveIMEContentObserver && sFocusedPresContext && + sActiveIMEContentObserver->IsManaging( + *sFocusedPresContext, sFocusedElement)))); + + if (NS_WARN_IF(sTextInputHandlingWidget->Destroyed())) { + MOZ_LOG(sISMLog, LogLevel::Error, + (" CreateIMEContentObserver(), FAILED due to " + "the widget for the nsPresContext has gone")); + return; + } + + const OwningNonNull<nsIWidget> textInputHandlingWidget = + *sTextInputHandlingWidget; + +#ifdef DEBUG + { + nsCOMPtr<nsIWidget> currentTextInputHandlingWidget = + sFocusedPresContext->GetTextInputHandlingWidget(); + MOZ_ASSERT(currentTextInputHandlingWidget == textInputHandlingWidget); + } +#endif // DEBUG + + MOZ_LOG(sISMLog, LogLevel::Debug, + (" CreateIMEContentObserver() is creating an " + "IMEContentObserver instance...")); + sActiveIMEContentObserver = new IMEContentObserver(); + + // IMEContentObserver::Init() might create another IMEContentObserver + // instance. So, sActiveIMEContentObserver would be replaced with new one. + // We should hold the current instance here. + OwningNonNull<IMEContentObserver> activeIMEContentObserver = + *sActiveIMEContentObserver; + OwningNonNull<nsPresContext> focusedPresContext = *sFocusedPresContext; + RefPtr<Element> focusedElement = aFocusedElement; + activeIMEContentObserver->Init(textInputHandlingWidget, focusedPresContext, + focusedElement, aEditorBase); +} + +// static +nsresult IMEStateManager::GetFocusSelectionAndRootElement( + Selection** aSelection, Element** aRootElement) { + if (!sActiveIMEContentObserver) { + return NS_ERROR_NOT_AVAILABLE; + } + return sActiveIMEContentObserver->GetSelectionAndRoot(aSelection, + aRootElement); +} + +// static +already_AddRefed<TextComposition> IMEStateManager::GetTextCompositionFor( + nsIWidget* aWidget) { + if (!sTextCompositions) { + return nullptr; + } + RefPtr<TextComposition> textComposition = + sTextCompositions->GetCompositionFor(aWidget); + return textComposition.forget(); +} + +// static +already_AddRefed<TextComposition> IMEStateManager::GetTextCompositionFor( + const WidgetCompositionEvent* aCompositionEvent) { + if (!sTextCompositions) { + return nullptr; + } + RefPtr<TextComposition> textComposition = + sTextCompositions->GetCompositionFor(aCompositionEvent); + return textComposition.forget(); +} + +// static +already_AddRefed<TextComposition> IMEStateManager::GetTextCompositionFor( + nsPresContext* aPresContext) { + if (!sTextCompositions) { + return nullptr; + } + RefPtr<TextComposition> textComposition = + sTextCompositions->GetCompositionFor(aPresContext); + return textComposition.forget(); +} + +} // namespace mozilla diff --git a/dom/events/IMEStateManager.h b/dom/events/IMEStateManager.h new file mode 100644 index 0000000000..1320fef8cc --- /dev/null +++ b/dom/events/IMEStateManager.h @@ -0,0 +1,494 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#ifndef mozilla_IMEStateManager_h_ +#define mozilla_IMEStateManager_h_ + +#include "mozilla/EventForwards.h" +#include "mozilla/Maybe.h" +#include "mozilla/StaticPtr.h" +#include "mozilla/dom/BrowserParent.h" +#include "nsIWidget.h" + +class nsIContent; +class nsINode; +class nsPresContext; + +namespace mozilla { + +class EditorBase; +class EventDispatchingCallback; +class IMEContentObserver; +class TextCompositionArray; +class TextComposition; + +namespace dom { +class Element; +class Selection; +} // namespace dom + +/** + * IMEStateManager manages InputContext (e.g., active editor type, IME enabled + * state and IME open state) of nsIWidget instances, manages IMEContentObserver + * and provides useful API for IME. + */ + +class IMEStateManager { + typedef dom::BrowserParent BrowserParent; + typedef widget::IMEMessage IMEMessage; + typedef widget::IMENotification IMENotification; + typedef widget::IMEState IMEState; + typedef widget::InputContext InputContext; + typedef widget::InputContextAction InputContextAction; + + public: + static void Init(); + static void Shutdown(); + + /** + * GetActiveBrowserParent() returns a pointer to a BrowserParent instance + * which is managed by the focused content (sFocusedElement). If the focused + * content isn't managing another process, this returns nullptr. + */ + static BrowserParent* GetActiveBrowserParent() { + // If menu has pseudo focus, we should ignore active child process. + if (sInstalledMenuKeyboardListener) { + return nullptr; + } + // If we know focused browser parent, use it for making any events related + // to composition go to same content process. + if (sFocusedIMEBrowserParent) { + return sFocusedIMEBrowserParent; + } + return BrowserParent::GetFocused(); + } + + /** + * DoesBrowserParentHaveIMEFocus() returns true when aBrowserParent has IME + * focus, i.e., the BrowserParent sent "focus" notification but not yet sends + * "blur". Note that this doesn't check if the remote processes are same + * because if another BrowserParent has focus, committing composition causes + * firing composition events in different BrowserParent. (Anyway, such case + * shouldn't occur.) + */ + static bool DoesBrowserParentHaveIMEFocus( + const BrowserParent* aBrowserParent) { + MOZ_ASSERT(aBrowserParent); + return sFocusedIMEBrowserParent == aBrowserParent; + } + + /** + * If CanSendNotificationToWidget() returns false (it should occur + * only in a content process), we shouldn't notify the widget of + * any focused editor changes since the content process was blurred. + * Also, even if content process, widget has native text event dispatcher such + * as Android, it still notify it. + */ + static bool CanSendNotificationToWidget() { +#ifdef MOZ_WIDGET_ANDROID + return true; +#else + return !sCleaningUpForStoppingIMEStateManagement; +#endif + } + + /** + * Focus moved between browsers from aBlur to aFocus. (nullptr means the + * chrome process.) + */ + static void OnFocusMovedBetweenBrowsers(BrowserParent* aBlur, + BrowserParent* aFocus); + + /** + * Called when aWidget is being deleted. + */ + static void WidgetDestroyed(nsIWidget* aWidget); + + /** + * Called when a widget exists when the app is quitting + */ + static void WidgetOnQuit(nsIWidget* aWidget); + + /** + * GetWidgetForActiveInputContext() returns a widget which IMEStateManager + * is managing input context with. If a widget instance needs to cache + * the last input context for nsIWidget::GetInputContext() or something, + * it should check if its cache is valid with this method before using it + * because if this method returns another instance, it means that + * IMEStateManager may have already changed shared input context via the + * widget. + */ + static nsIWidget* GetWidgetForActiveInputContext() { + return sActiveInputContextWidget; + } + + /** + * SetIMEContextForChildProcess() is called when aBrowserParent receives + * SetInputContext() from the remote process. + */ + static void SetInputContextForChildProcess(BrowserParent* aBrowserParent, + const InputContext& aInputContext, + const InputContextAction& aAction); + + /** + * StopIMEStateManagement() is called when the process should stop managing + * IME state. + */ + static void StopIMEStateManagement(); + + /** + * MaybeStartOffsetUpdatedInChild() is called when composition start offset + * is maybe updated in the child process. I.e., even if it's not updated, + * this is called and never called if the composition is in this process. + * @param aWidget The widget whose native IME context has the + * composition. + * @param aStartOffset New composition start offset with native + * linebreaks. + */ + static void MaybeStartOffsetUpdatedInChild(nsIWidget* aWidget, + uint32_t aStartOffset); + + MOZ_CAN_RUN_SCRIPT static nsresult OnDestroyPresContext( + nsPresContext& aPresContext); + MOZ_CAN_RUN_SCRIPT static nsresult OnRemoveContent( + nsPresContext& aPresContext, dom::Element& aElement); + /** + * OnChangeFocus() should be called when focused content is changed or + * IME enabled state is changed. If nobody has focus, set both aPresContext + * and aContent nullptr. E.g., all windows are deactivated. Otherwise, + * set focused element (even if it won't receive `focus`event) and + * corresponding nsPresContext for it. Then, IMEStateManager can avoid + * handling delayed notifications from the others with verifying the + * focused element. + */ + MOZ_CAN_RUN_SCRIPT static nsresult OnChangeFocus( + nsPresContext* aPresContext, dom::Element* aElement, + InputContextAction::Cause aCause); + + /** + * OnInstalledMenuKeyboardListener() is called when menu keyboard listener + * is installed or uninstalled in the process. So, even if menu keyboard + * listener was installed in chrome process, this won't be called in content + * processes. + * + * @param aInstalling true if menu keyboard listener is installed. + * Otherwise, i.e., menu keyboard listener is + * uninstalled, false. + */ + MOZ_CAN_RUN_SCRIPT static void OnInstalledMenuKeyboardListener( + bool aInstalling); + + // These two methods manage focus and selection/text observers. + // They are separate from OnChangeFocus above because this offers finer + // control compared to having the two methods incorporated into OnChangeFocus + + // Get the focused editor's selection and root + static nsresult GetFocusSelectionAndRootElement(dom::Selection** aSel, + dom::Element** aRootElement); + // This method updates the current IME state. However, if the enabled state + // isn't changed by the new state, this method does nothing. + // Note that this method changes the IME state of the active element in the + // widget. So, the caller must have focus. + // XXX Changing this to MOZ_CAN_RUN_SCRIPT requires too many callers to be + // marked too. Probably, we should initialize IMEContentObserver + // asynchronously. + enum class UpdateIMEStateOption { + ForceUpdate, + DontCommitComposition, + }; + using UpdateIMEStateOptions = EnumSet<UpdateIMEStateOption, uint32_t>; + MOZ_CAN_RUN_SCRIPT static void UpdateIMEState( + const IMEState& aNewIMEState, dom::Element* aElement, + EditorBase& aEditorBase, const UpdateIMEStateOptions& aOptions = {}); + + // This method is called when user operates mouse button in focused editor + // and before the editor handles it. + // Returns true if IME consumes the event. Otherwise, false. + MOZ_CAN_RUN_SCRIPT static bool OnMouseButtonEventInEditor( + nsPresContext& aPresContext, dom::Element* aElement, + WidgetMouseEvent& aMouseEvent); + + // This method is called when user clicked in an editor. + // aElement must be: + // If the editor is for <input> or <textarea>, the element. + // If the editor is for contenteditable, the active editinghost. + // If the editor is for designMode, nullptr. + MOZ_CAN_RUN_SCRIPT static void OnClickInEditor( + nsPresContext& aPresContext, dom::Element* aElement, + const WidgetMouseEvent& aMouseEvent); + + // This method is called when editor actually gets focus. + // aContent must be: + // If the editor is for <input> or <textarea>, the element. + // If the editor is for contenteditable, the active editinghost. + // If the editor is for designMode, nullptr. + static void OnFocusInEditor(nsPresContext& aPresContext, + dom::Element* aElement, EditorBase& aEditorBase); + + // This method is called when the editor is initialized. + static void OnEditorInitialized(EditorBase& aEditorBase); + + // This method is called when the editor is (might be temporarily) being + // destroyed. + static void OnEditorDestroying(EditorBase& aEditorBase); + + // This method is called when focus is set to same content again. + MOZ_CAN_RUN_SCRIPT static void OnReFocus(nsPresContext& aPresContext, + dom::Element& aElement); + + // This method is called when designMode is set to "off" or an editing host + // becomes not editable due to removing `contenteditable` attribute or setting + // it to "false". + MOZ_CAN_RUN_SCRIPT static void MaybeOnEditableStateDisabled( + nsPresContext& aPresContext, dom::Element* aElement); + + /** + * All composition events must be dispatched via DispatchCompositionEvent() + * for storing the composition target and ensuring a set of composition + * events must be fired the stored target. If the stored composition event + * target is destroying, this removes the stored composition automatically. + */ + MOZ_CAN_RUN_SCRIPT static void DispatchCompositionEvent( + nsINode* aEventTargetNode, nsPresContext* aPresContext, + BrowserParent* aBrowserParent, WidgetCompositionEvent* aCompositionEvent, + nsEventStatus* aStatus, EventDispatchingCallback* aCallBack, + bool aIsSynthesized = false); + + /** + * All selection events must be handled via HandleSelectionEvent() + * because they must be handled by same target as composition events when + * there is a composition. + */ + MOZ_CAN_RUN_SCRIPT + static void HandleSelectionEvent(nsPresContext* aPresContext, + nsIContent* aEventTargetContent, + WidgetSelectionEvent* aSelectionEvent); + + /** + * This is called when PresShell ignores a composition event due to not safe + * to dispatch events. + */ + static void OnCompositionEventDiscarded( + WidgetCompositionEvent* aCompositionEvent); + + /** + * Get TextComposition from widget. + */ + static already_AddRefed<TextComposition> GetTextCompositionFor( + nsIWidget* aWidget); + + /** + * Returns TextComposition instance for the event. + */ + static already_AddRefed<TextComposition> GetTextCompositionFor( + const WidgetCompositionEvent* aCompositionEvent); + + /** + * Returns TextComposition instance for the pres context. + * Be aware, even if another pres context which shares native IME context with + * specified pres context has composition, this returns nullptr. + */ + static already_AddRefed<TextComposition> GetTextCompositionFor( + nsPresContext* aPresContext); + + /** + * Send a notification to IME. It depends on the IME or platform spec what + * will occur (or not occur). + */ + static nsresult NotifyIME(const IMENotification& aNotification, + nsIWidget* aWidget, + BrowserParent* aBrowserParent = nullptr); + static nsresult NotifyIME(IMEMessage aMessage, nsIWidget* aWidget, + BrowserParent* aBrowserParent = nullptr); + static nsresult NotifyIME(IMEMessage aMessage, nsPresContext* aPresContext, + BrowserParent* aBrowserParent = nullptr); + + static nsINode* GetRootEditableNode(const nsPresContext& aPresContext, + const dom::Element* aElement); + + /** + * Returns active IMEContentObserver but may be nullptr if focused content + * isn't editable or focus in a remote process. + */ + static IMEContentObserver* GetActiveContentObserver(); + + /** + * Return focused element which was notified by a OnChangeFocus() call. + */ + static dom::Element* GetFocusedElement(); + + protected: + MOZ_CAN_RUN_SCRIPT static nsresult OnChangeFocusInternal( + nsPresContext* aPresContext, dom::Element* aElement, + InputContextAction aAction); + MOZ_CAN_RUN_SCRIPT static void SetIMEState(const IMEState& aState, + const nsPresContext* aPresContext, + dom::Element* aElement, + nsIWidget& aWidget, + InputContextAction aAction, + InputContext::Origin aOrigin); + static void SetInputContext(nsIWidget& aWidget, + const InputContext& aInputContext, + const InputContextAction& aAction); + static IMEState GetNewIMEState(const nsPresContext& aPresContext, + dom::Element* aElement); + + static void EnsureTextCompositionArray(); + + // XXX Changing this to MOZ_CAN_RUN_SCRIPT requires too many callers to be + // marked too. Probably, we should initialize IMEContentObserver + // asynchronously. + MOZ_CAN_RUN_SCRIPT_BOUNDARY static void CreateIMEContentObserver( + EditorBase& aEditorBase, dom::Element* aFocusedElement); + + /** + * Check whether the content matches or does not match with focus information + * which is previously notified via OnChangeFocus(); + */ + [[nodiscard]] static bool IsFocusedElement( + const nsPresContext& aPresContext, const dom::Element* aFocusedElement); + + static void DestroyIMEContentObserver(); + + [[nodiscard]] static bool IsEditable(nsINode* node); + + [[nodiscard]] static bool IsIMEObserverNeeded(const IMEState& aState); + + [[nodiscard]] static nsIContent* GetRootContent(nsPresContext* aPresContext); + + /** + * CanHandleWith() returns false if it's destroyed. + */ + [[nodiscard]] static bool CanHandleWith(const nsPresContext* aPresContext); + + /** + * ResetActiveChildInputContext() resets sActiveChildInputContext. + * So, HasActiveChildSetInputContext() will return false until a remote + * process gets focus and set input context. + */ + static void ResetActiveChildInputContext(); + + /** + * HasActiveChildSetInputContext() returns true if a remote tab has focus + * and it has already set input context. Otherwise, returns false. + */ + static bool HasActiveChildSetInputContext(); + + // sFocusedElement and sFocusedPresContext are the focused content and + // PresContext. If a document has focus but there is no focused element, + // sFocusedElement may be nullptr. + static StaticRefPtr<dom::Element> sFocusedElement; + static StaticRefPtr<nsPresContext> sFocusedPresContext; + // sTextInputHandlingWidget is cache for the result of + // sFocusedPresContext->GetTextInputHandlingWidget(). Even after + // sFocusedPresContext has gone, we need to clean up some IME state on the + // widget if the widget is available. + // Note that this is cleared when the widget is being destroyed. + static nsIWidget* sTextInputHandlingWidget; + // sFocusedIMEBrowserParent is the tab parent, which send "focus" notification + // to sFocusedIMEWidget (and didn't yet sent "blur" notification). + // Note that this is cleared when the widget is being destroyed. + static nsIWidget* sFocusedIMEWidget; + static StaticRefPtr<BrowserParent> sFocusedIMEBrowserParent; + // sActiveInputContextWidget is the last widget whose SetInputContext() is + // called. This is important to reduce sync IPC cost with parent process. + // If IMEStateManager set input context to different widget, PuppetWidget can + // return cached input context safely. + // Note that this is cleared when the widget is being destroyed. + static nsIWidget* sActiveInputContextWidget; + // sActiveIMEContentObserver points to the currently active + // IMEContentObserver. This is null if there is no focused editor. + static StaticRefPtr<IMEContentObserver> sActiveIMEContentObserver; + + // All active compositions in the process are stored by this array. + // When you get an item of this array and use it, please be careful. + // The instances in this array can be destroyed automatically if you do + // something to cause committing or canceling the composition. + static TextCompositionArray* sTextCompositions; + + // Origin type of current process. + static InputContext::Origin sOrigin; + + // sActiveChildInputContext is valid only when BrowserParent::GetFocused() is + // not nullptr. This stores last information of input context in the remote + // process of BrowserParent::GetFocused(). I.e., they are set when + // SetInputContextForChildProcess() is called. This is necessary for + // restoring IME state when menu keyboard listener is uninstalled. + static InputContext sActiveChildInputContext; + + // sInstalledMenuKeyboardListener is true if menu keyboard listener is + // installed in the process. + static bool sInstalledMenuKeyboardListener; + + static bool sIsGettingNewIMEState; + static bool sCheckForIMEUnawareWebApps; + + // Set to true only if this is an instance in a content process and + // only while `IMEStateManager::StopIMEStateManagement()`. + static bool sCleaningUpForStoppingIMEStateManagement; + + // Set to true when: + // - In the main process, a window belonging to this app is active in the + // desktop. + // - In a content process, the process has focus. + // + // This is updated by `OnChangeFocusInternal()` is called in the main + // process. Therefore, this indicates the active state which + // `IMEStateManager` notified the focus change, there is timelag from + // the `nsFocusManager`'s status update. This allows that all methods + // to handle something specially when they are called while the process + // is being activated or inactivated. E.g., `OnFocusMovedBetweenBrowsers()` + // is called twice before `OnChangeFocusInternal()` when the main process + // becomes active. In this case, it wants to wait a following call of + // `OnChangeFocusInternal()` to keep active composition. See also below. + static bool sIsActive; + + // While the application is being activated, `OnFocusMovedBetweenBrowsers()` + // are called twice before `OnChangeFocusInternal()`. First time, aBlur is + // the last focused `BrowserParent` at deactivating and aFocus is always + // `nullptr`. Then, it'll be called again with actually focused + // `BrowserParent` when a content in a remote process has focus. If we need + // to keep active composition while all windows are deactivated, we shouldn't + // commit it at the first call since usually, the second call's aFocus + // and the first call's aBlur are same `BrowserParent`. For solving this + // issue, we need to merge the given `BrowserParent`s of multiple calls of + // `OnFocusMovedBetweenBrowsers()`. The following struct is the data for + // calling `OnFocusMovedBetweenBrowsers()` later from + // `OnChangeFocusInternal()`. Note that focus can be moved even while the + // main process is not active because JS can change focus. In such case, + // composition is committed at that time. Therefore, this is required only + // when the main process is activated and there is a composition in a remote + // process. + struct PendingFocusedBrowserSwitchingData final { + RefPtr<BrowserParent> mBrowserParentBlurred; + RefPtr<BrowserParent> mBrowserParentFocused; + + PendingFocusedBrowserSwitchingData() = delete; + explicit PendingFocusedBrowserSwitchingData(BrowserParent* aBlur, + BrowserParent* aFocus) + : mBrowserParentBlurred(aBlur), mBrowserParentFocused(aFocus) {} + }; + static Maybe<PendingFocusedBrowserSwitchingData> + sPendingFocusedBrowserSwitchingData; + + class MOZ_STACK_CLASS GettingNewIMEStateBlocker final { + public: + GettingNewIMEStateBlocker() + : mOldValue(IMEStateManager::sIsGettingNewIMEState) { + IMEStateManager::sIsGettingNewIMEState = true; + } + ~GettingNewIMEStateBlocker() { + IMEStateManager::sIsGettingNewIMEState = mOldValue; + } + + private: + bool mOldValue; + }; +}; + +} // namespace mozilla + +#endif // mozilla_IMEStateManager_h_ diff --git a/dom/events/ImageCaptureError.cpp b/dom/events/ImageCaptureError.cpp new file mode 100644 index 0000000000..87cfad39ce --- /dev/null +++ b/dom/events/ImageCaptureError.cpp @@ -0,0 +1,39 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#include "mozilla/dom/ImageCaptureError.h" +#include "mozilla/dom/ImageCaptureErrorEventBinding.h" + +namespace mozilla::dom { + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(ImageCaptureError, mParent) +NS_IMPL_CYCLE_COLLECTING_ADDREF(ImageCaptureError) +NS_IMPL_CYCLE_COLLECTING_RELEASE(ImageCaptureError) +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ImageCaptureError) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +ImageCaptureError::ImageCaptureError(nsISupports* aParent, uint16_t aCode, + const nsAString& aMessage) + : mParent(aParent), mMessage(aMessage), mCode(aCode) {} + +ImageCaptureError::~ImageCaptureError() = default; + +nsISupports* ImageCaptureError::GetParentObject() const { return mParent; } + +JSObject* ImageCaptureError::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return ImageCaptureError_Binding::Wrap(aCx, this, aGivenProto); +} + +uint16_t ImageCaptureError::Code() const { return mCode; } + +void ImageCaptureError::GetMessage(nsAString& retval) const { + retval = mMessage; +} + +} // namespace mozilla::dom diff --git a/dom/events/ImageCaptureError.h b/dom/events/ImageCaptureError.h new file mode 100644 index 0000000000..9c56c3f004 --- /dev/null +++ b/dom/events/ImageCaptureError.h @@ -0,0 +1,56 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#ifndef mozilla_dom_ImageCaptureError_h +#define mozilla_dom_ImageCaptureError_h + +#include "mozilla/Attributes.h" +#include "nsCOMPtr.h" +#include "nsString.h" +#include "nsWrapperCache.h" + +namespace mozilla::dom { + +/** + * This is the implementation of ImageCaptureError on W3C specification + * https://dvcs.w3.org/hg/dap/raw-file/default/media-stream-capture/ImageCapture.html#idl-def-ImageCaptureError. + * This object should be generated by ImageCapture object only. + */ +class ImageCaptureError final : public nsISupports, public nsWrapperCache { + public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(ImageCaptureError) + + ImageCaptureError(nsISupports* aParent, uint16_t aCode, + const nsAString& aMessage); + + nsISupports* GetParentObject() const; + + virtual JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + uint16_t Code() const; + + enum { + FRAME_GRAB_ERROR = 1, + SETTINGS_ERROR = 2, + PHOTO_ERROR = 3, + ERROR_UNKNOWN = 4, + }; + + void GetMessage(nsAString& retval) const; + + private: + ~ImageCaptureError(); + + nsCOMPtr<nsISupports> mParent; + nsString mMessage; + uint16_t mCode; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_ImageCaptureError_h diff --git a/dom/events/InputEvent.cpp b/dom/events/InputEvent.cpp new file mode 100644 index 0000000000..45754b1d82 --- /dev/null +++ b/dom/events/InputEvent.cpp @@ -0,0 +1,118 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#include "mozilla/dom/InputEvent.h" +#include "mozilla/TextEvents.h" +#include "mozilla/StaticPrefs_dom.h" +#include "prtime.h" + +namespace mozilla::dom { + +InputEvent::InputEvent(EventTarget* aOwner, nsPresContext* aPresContext, + InternalEditorInputEvent* aEvent) + : UIEvent(aOwner, aPresContext, + aEvent + ? aEvent + : new InternalEditorInputEvent(false, eVoidEvent, nullptr)) { + NS_ASSERTION(mEvent->mClass == eEditorInputEventClass, "event type mismatch"); + + if (aEvent) { + mEventIsInternal = false; + } else { + mEventIsInternal = true; + } +} + +void InputEvent::GetData(nsAString& aData, CallerType aCallerType) { + InternalEditorInputEvent* editorInputEvent = mEvent->AsEditorInputEvent(); + MOZ_ASSERT(editorInputEvent); + // If clipboard event is disabled, user may not want to leak clipboard + // information via DOM events. If so, we should return empty string instead. + if (mEvent->IsTrusted() && aCallerType != CallerType::System && + !StaticPrefs::dom_event_clipboardevents_enabled() && + ExposesClipboardDataOrDataTransfer(editorInputEvent->mInputType)) { + aData = editorInputEvent->mData.IsVoid() ? VoidString() : u""_ns; + return; + } + aData = editorInputEvent->mData; +} + +already_AddRefed<DataTransfer> InputEvent::GetDataTransfer( + CallerType aCallerType) { + InternalEditorInputEvent* editorInputEvent = mEvent->AsEditorInputEvent(); + MOZ_ASSERT(editorInputEvent); + // If clipboard event is disabled, user may not want to leak clipboard + // information via DOM events. If so, we should return DataTransfer which + // has empty string instead. The reason why we make it have empty string is, + // web apps may not expect that InputEvent.dataTransfer returns empty and + // non-null DataTransfer instance. + if (mEvent->IsTrusted() && aCallerType != CallerType::System && + !StaticPrefs::dom_event_clipboardevents_enabled() && + ExposesClipboardDataOrDataTransfer(editorInputEvent->mInputType)) { + if (!editorInputEvent->mDataTransfer) { + return nullptr; + } + return do_AddRef( + new DataTransfer(editorInputEvent->mDataTransfer->GetParentObject(), + editorInputEvent->mMessage, u""_ns)); + } + return do_AddRef(editorInputEvent->mDataTransfer); +} + +void InputEvent::GetInputType(nsAString& aInputType) { + InternalEditorInputEvent* editorInputEvent = mEvent->AsEditorInputEvent(); + MOZ_ASSERT(editorInputEvent); + if (editorInputEvent->mInputType == EditorInputType::eUnknown) { + aInputType = mInputTypeValue; + } else { + editorInputEvent->GetDOMInputTypeName(aInputType); + } +} + +void InputEvent::GetTargetRanges(nsTArray<RefPtr<StaticRange>>& aTargetRanges) { + MOZ_ASSERT(aTargetRanges.IsEmpty()); + MOZ_ASSERT(mEvent->AsEditorInputEvent()); + aTargetRanges.AppendElements(mEvent->AsEditorInputEvent()->mTargetRanges); +} + +bool InputEvent::IsComposing() { + return mEvent->AsEditorInputEvent()->mIsComposing; +} + +already_AddRefed<InputEvent> InputEvent::Constructor( + const GlobalObject& aGlobal, const nsAString& aType, + const InputEventInit& aParam) { + nsCOMPtr<EventTarget> t = do_QueryInterface(aGlobal.GetAsSupports()); + RefPtr<InputEvent> e = new InputEvent(t, nullptr, nullptr); + bool trusted = e->Init(t); + e->InitUIEvent(aType, aParam.mBubbles, aParam.mCancelable, aParam.mView, + aParam.mDetail); + InternalEditorInputEvent* internalEvent = e->mEvent->AsEditorInputEvent(); + internalEvent->mInputType = + InternalEditorInputEvent::GetEditorInputType(aParam.mInputType); + if (internalEvent->mInputType == EditorInputType::eUnknown) { + e->mInputTypeValue = aParam.mInputType; + } + internalEvent->mData = aParam.mData; + internalEvent->mDataTransfer = aParam.mDataTransfer; + internalEvent->mTargetRanges = aParam.mTargetRanges; + internalEvent->mIsComposing = aParam.mIsComposing; + e->SetTrusted(trusted); + e->SetComposed(aParam.mComposed); + return e.forget(); +} + +} // namespace mozilla::dom + +using namespace mozilla; +using namespace mozilla::dom; + +already_AddRefed<InputEvent> NS_NewDOMInputEvent( + EventTarget* aOwner, nsPresContext* aPresContext, + InternalEditorInputEvent* aEvent) { + RefPtr<InputEvent> it = new InputEvent(aOwner, aPresContext, aEvent); + return it.forget(); +} diff --git a/dom/events/InputEvent.h b/dom/events/InputEvent.h new file mode 100644 index 0000000000..11c7472d9e --- /dev/null +++ b/dom/events/InputEvent.h @@ -0,0 +1,58 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#ifndef mozilla_dom_InputEvent_h_ +#define mozilla_dom_InputEvent_h_ + +#include "mozilla/dom/UIEvent.h" + +#include "mozilla/dom/InputEventBinding.h" +#include "mozilla/dom/StaticRange.h" +#include "mozilla/EventForwards.h" +#include "nsTArray.h" + +namespace mozilla::dom { + +class DataTransfer; + +class InputEvent : public UIEvent { + public: + InputEvent(EventTarget* aOwner, nsPresContext* aPresContext, + InternalEditorInputEvent* aEvent); + + NS_INLINE_DECL_REFCOUNTING_INHERITED(InputEvent, UIEvent) + + static already_AddRefed<InputEvent> Constructor(const GlobalObject& aGlobal, + const nsAString& aType, + const InputEventInit& aParam); + + virtual JSObject* WrapObjectInternal( + JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override { + return InputEvent_Binding::Wrap(aCx, this, aGivenProto); + } + + void GetInputType(nsAString& aInputType); + void GetData(nsAString& aData, CallerType aCallerType = CallerType::System); + already_AddRefed<DataTransfer> GetDataTransfer( + CallerType aCallerType = CallerType::System); + void GetTargetRanges(nsTArray<RefPtr<StaticRange>>& aTargetRanges); + bool IsComposing(); + + protected: + ~InputEvent() = default; + + // mInputTypeValue stores inputType attribute value if the instance is + // created by script and not initialized with known inputType value. + nsString mInputTypeValue; +}; + +} // namespace mozilla::dom + +already_AddRefed<mozilla::dom::InputEvent> NS_NewDOMInputEvent( + mozilla::dom::EventTarget* aOwner, nsPresContext* aPresContext, + mozilla::InternalEditorInputEvent* aEvent); + +#endif // mozilla_dom_InputEvent_h_ diff --git a/dom/events/InputEventOptions.h b/dom/events/InputEventOptions.h new file mode 100644 index 0000000000..94096d759f --- /dev/null +++ b/dom/events/InputEventOptions.h @@ -0,0 +1,75 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#ifndef mozilla_InputEventOptions_h +#define mozilla_InputEventOptions_h + +#include "mozilla/Attributes.h" +#include "mozilla/RefPtr.h" +#include "mozilla/TextEvents.h" +#include "mozilla/dom/DataTransfer.h" +#include "mozilla/dom/StaticRange.h" +#include "nsString.h" +#include "nsTArray.h" + +namespace mozilla { + +/** + * InputEventOptions is used by nsContentUtils::DispatchInputEvent() to specify + * some attributes of InputEvent. It would be nice if this was in + * nsContentUtils.h, however, it needs to include StaticRange.h for declaring + * this. That would cause unnecessary dependency and makes incremental build + * slower when you touch StaticRange.h even though most nsContentUtils.h users + * don't use it. Therefore, this struct is declared in separated header file + * here. + */ +struct MOZ_STACK_CLASS InputEventOptions final { + enum class NeverCancelable { + No, + Yes, + }; + InputEventOptions() : mDataTransfer(nullptr), mNeverCancelable(false) {} + explicit InputEventOptions(const InputEventOptions& aOther) = delete; + InputEventOptions(InputEventOptions&& aOther) = default; + explicit InputEventOptions(const nsAString& aData, + NeverCancelable aNeverCancelable) + : mData(aData), + mDataTransfer(nullptr), + mNeverCancelable(aNeverCancelable == NeverCancelable::Yes) {} + explicit InputEventOptions(dom::DataTransfer* aDataTransfer, + NeverCancelable aNeverCancelable) + : mDataTransfer(aDataTransfer), + mNeverCancelable(aNeverCancelable == NeverCancelable::Yes) { + MOZ_ASSERT(mDataTransfer); + MOZ_ASSERT(mDataTransfer->IsReadOnly()); + } + InputEventOptions(const nsAString& aData, + OwningNonNullStaticRangeArray&& aTargetRanges, + NeverCancelable aNeverCancelable) + : mData(aData), + mDataTransfer(nullptr), + mTargetRanges(std::move(aTargetRanges)), + mNeverCancelable(aNeverCancelable == NeverCancelable::Yes) {} + InputEventOptions(dom::DataTransfer* aDataTransfer, + OwningNonNullStaticRangeArray&& aTargetRanges, + NeverCancelable aNeverCancelable) + : mDataTransfer(aDataTransfer), + mTargetRanges(std::move(aTargetRanges)), + mNeverCancelable(aNeverCancelable == NeverCancelable::Yes) { + MOZ_ASSERT(mDataTransfer); + MOZ_ASSERT(mDataTransfer->IsReadOnly()); + } + + nsString mData; + dom::DataTransfer* mDataTransfer; + OwningNonNullStaticRangeArray mTargetRanges; + // If this is set to true, dispatching event won't be cancelable. + bool mNeverCancelable; +}; + +} // namespace mozilla + +#endif // #ifndef mozilla_InputEventOptions_h diff --git a/dom/events/InputTypeList.h b/dom/events/InputTypeList.h new file mode 100644 index 0000000000..ac5cee2554 --- /dev/null +++ b/dom/events/InputTypeList.h @@ -0,0 +1,73 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +/** + * This header file defines all inputType values which are used for DOM + * InputEvent.inputType. + * You must define NS_DEFINE_INPUTTYPE macro before including this. + * + * It must have two arguments, (aCPPName, aDOMName) + * aCPPName is usable name for a part of C++ constants. + * aDOMName is the actual value declared by the specs: + * Level 1: + * https://rawgit.com/w3c/input-events/v1/index.html#interface-InputEvent-Attributes + * Level 2: + * https://w3c.github.io/input-events/index.html#interface-InputEvent-Attributes + */ + +NS_DEFINE_INPUTTYPE(InsertText, "insertText") +NS_DEFINE_INPUTTYPE(InsertReplacementText, "insertReplacementText") +NS_DEFINE_INPUTTYPE(InsertLineBreak, "insertLineBreak") +NS_DEFINE_INPUTTYPE(InsertParagraph, "insertParagraph") +NS_DEFINE_INPUTTYPE(InsertOrderedList, "insertOrderedList") +NS_DEFINE_INPUTTYPE(InsertUnorderedList, "insertUnorderedList") +NS_DEFINE_INPUTTYPE(InsertHorizontalRule, "insertHorizontalRule") +NS_DEFINE_INPUTTYPE(InsertFromYank, "insertFromYank") +NS_DEFINE_INPUTTYPE(InsertFromDrop, "insertFromDrop") +NS_DEFINE_INPUTTYPE(InsertFromPaste, "insertFromPaste") +NS_DEFINE_INPUTTYPE(InsertFromPasteAsQuotation, "insertFromPasteAsQuotation") +NS_DEFINE_INPUTTYPE(InsertTranspose, "insertTranspose") +NS_DEFINE_INPUTTYPE(InsertCompositionText, "insertCompositionText") +NS_DEFINE_INPUTTYPE(InsertFromComposition, + "insertFromComposition") // Level 2 +NS_DEFINE_INPUTTYPE(InsertLink, "insertLink") +NS_DEFINE_INPUTTYPE(DeleteByComposition, + "deleteByComposition") // Level 2 +NS_DEFINE_INPUTTYPE(DeleteCompositionText, + "deleteCompositionText") // Level 2 +NS_DEFINE_INPUTTYPE(DeleteWordBackward, "deleteWordBackward") +NS_DEFINE_INPUTTYPE(DeleteWordForward, "deleteWordForward") +NS_DEFINE_INPUTTYPE(DeleteSoftLineBackward, "deleteSoftLineBackward") +NS_DEFINE_INPUTTYPE(DeleteSoftLineForward, "deleteSoftLineForward") +NS_DEFINE_INPUTTYPE(DeleteEntireSoftLine, "deleteEntireSoftLine") +NS_DEFINE_INPUTTYPE(DeleteHardLineBackward, "deleteHardLineBackward") +NS_DEFINE_INPUTTYPE(DeleteHardLineForward, "deleteHardLineForward") +NS_DEFINE_INPUTTYPE(DeleteByDrag, "deleteByDrag") +NS_DEFINE_INPUTTYPE(DeleteByCut, "deleteByCut") +NS_DEFINE_INPUTTYPE(DeleteContent, "deleteContent") +NS_DEFINE_INPUTTYPE(DeleteContentBackward, "deleteContentBackward") +NS_DEFINE_INPUTTYPE(DeleteContentForward, "deleteContentForward") +NS_DEFINE_INPUTTYPE(HistoryUndo, "historyUndo") +NS_DEFINE_INPUTTYPE(HistoryRedo, "historyRedo") +NS_DEFINE_INPUTTYPE(FormatBold, "formatBold") +NS_DEFINE_INPUTTYPE(FormatItalic, "formatItalic") +NS_DEFINE_INPUTTYPE(FormatUnderline, "formatUnderline") +NS_DEFINE_INPUTTYPE(FormatStrikeThrough, "formatStrikeThrough") +NS_DEFINE_INPUTTYPE(FormatSuperscript, "formatSuperscript") +NS_DEFINE_INPUTTYPE(FormatSubscript, "formatSubscript") +NS_DEFINE_INPUTTYPE(FormatJustifyFull, "formatJustifyFull") +NS_DEFINE_INPUTTYPE(FormatJustifyCenter, "formatJustifyCenter") +NS_DEFINE_INPUTTYPE(FormatJustifyRight, "formatJustifyRight") +NS_DEFINE_INPUTTYPE(FormatJustifyLeft, "formatJustifyLeft") +NS_DEFINE_INPUTTYPE(FormatIndent, "formatIndent") +NS_DEFINE_INPUTTYPE(FormatOutdent, "formatOutdent") +NS_DEFINE_INPUTTYPE(FormatRemove, "formatRemove") +NS_DEFINE_INPUTTYPE(FormatSetBlockTextDirection, "formatSetBlockTextDirection") +NS_DEFINE_INPUTTYPE(FormatSetInlineTextDirection, + "formatSetInlineTextDirection") +NS_DEFINE_INPUTTYPE(FormatBackColor, "formatBackColor") +NS_DEFINE_INPUTTYPE(FormatFontColor, "formatFontColor") +NS_DEFINE_INPUTTYPE(FormatFontName, "formatFontName") diff --git a/dom/events/InternalMutationEvent.h b/dom/events/InternalMutationEvent.h new file mode 100644 index 0000000000..41a20677e4 --- /dev/null +++ b/dom/events/InternalMutationEvent.h @@ -0,0 +1,66 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#ifndef mozilla_MutationEvent_h__ +#define mozilla_MutationEvent_h__ + +#include "mozilla/BasicEvents.h" +#include "nsCOMPtr.h" +#include "nsAtom.h" +#include "nsINode.h" + +namespace mozilla { + +class InternalMutationEvent : public WidgetEvent { + public: + virtual InternalMutationEvent* AsMutationEvent() override { return this; } + + InternalMutationEvent(bool aIsTrusted, EventMessage aMessage) + : WidgetEvent(aIsTrusted, aMessage, eMutationEventClass), mAttrChange(0) { + mFlags.mCancelable = false; + } + + virtual WidgetEvent* Duplicate() const override { + MOZ_ASSERT(mClass == eMutationEventClass, + "Duplicate() must be overridden by sub class"); + InternalMutationEvent* result = new InternalMutationEvent(false, mMessage); + result->AssignMutationEventData(*this, true); + result->mFlags = mFlags; + return result; + } + + nsCOMPtr<nsINode> mRelatedNode; + RefPtr<nsAtom> mAttrName; + RefPtr<nsAtom> mPrevAttrValue; + RefPtr<nsAtom> mNewAttrValue; + unsigned short mAttrChange; + + void AssignMutationEventData(const InternalMutationEvent& aEvent, + bool aCopyTargets) { + AssignEventData(aEvent, aCopyTargets); + + mRelatedNode = aEvent.mRelatedNode; + mAttrName = aEvent.mAttrName; + mPrevAttrValue = aEvent.mPrevAttrValue; + mNewAttrValue = aEvent.mNewAttrValue; + mAttrChange = aEvent.mAttrChange; + } +}; + +// Bits are actually checked to optimize mutation event firing. +// That's why I don't number from 0x00. The first event should +// always be 0x01. +#define NS_EVENT_BITS_MUTATION_SUBTREEMODIFIED 0x01 +#define NS_EVENT_BITS_MUTATION_NODEINSERTED 0x02 +#define NS_EVENT_BITS_MUTATION_NODEREMOVED 0x04 +#define NS_EVENT_BITS_MUTATION_NODEREMOVEDFROMDOCUMENT 0x08 +#define NS_EVENT_BITS_MUTATION_NODEINSERTEDINTODOCUMENT 0x10 +#define NS_EVENT_BITS_MUTATION_ATTRMODIFIED 0x20 +#define NS_EVENT_BITS_MUTATION_CHARACTERDATAMODIFIED 0x40 + +} // namespace mozilla + +#endif // mozilla_MutationEvent_h__ diff --git a/dom/events/JSEventHandler.cpp b/dom/events/JSEventHandler.cpp new file mode 100644 index 0000000000..8304ec6582 --- /dev/null +++ b/dom/events/JSEventHandler.cpp @@ -0,0 +1,229 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ +#include "nsJSUtils.h" +#include "nsString.h" +#include "nsIScriptContext.h" +#include "nsIScriptGlobalObject.h" +#include "nsVariant.h" +#include "nsGkAtoms.h" +#include "xpcpublic.h" +#include "nsJSEnvironment.h" +#include "nsDOMJSUtils.h" +#include "mozilla/ContentEvents.h" +#include "mozilla/CycleCollectedJSContext.h" +#include "mozilla/HoldDropJSObjects.h" +#include "mozilla/JSEventHandler.h" +#include "mozilla/Likely.h" +#include "mozilla/dom/BeforeUnloadEvent.h" +#include "mozilla/dom/ErrorEvent.h" +#include "mozilla/dom/WorkerPrivate.h" + +namespace mozilla { + +using namespace dom; + +JSEventHandler::JSEventHandler(EventTarget* aTarget, nsAtom* aType, + const TypedEventHandler& aTypedHandler) + : mTarget(aTarget), mEventName(aType), mTypedHandler(aTypedHandler) { + // Note, we call HoldJSObjects to get CanSkip called before CC. + HoldJSObjects(this); +} + +JSEventHandler::~JSEventHandler() { + NS_ASSERTION(!mTarget, "Should have called Disconnect()!"); + DropJSObjects(this); +} + +NS_IMPL_CYCLE_COLLECTION_CLASS(JSEventHandler) + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(JSEventHandler) + tmp->mTypedHandler.ForgetHandler(); +NS_IMPL_CYCLE_COLLECTION_UNLINK_END +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INTERNAL(JSEventHandler) + if (MOZ_UNLIKELY(cb.WantDebugInfo()) && tmp->mEventName) { + nsAutoCString name; + name.AppendLiteral("JSEventHandler handlerName="); + name.Append( + NS_ConvertUTF16toUTF8(nsDependentAtomString(tmp->mEventName)).get()); + cb.DescribeRefCountedNode(tmp->mRefCnt.get(), name.get()); + } else { + NS_IMPL_CYCLE_COLLECTION_DESCRIBE(JSEventHandler, tmp->mRefCnt.get()) + } + NS_IMPL_CYCLE_COLLECTION_TRAVERSE_RAWPTR(mTypedHandler.Ptr()) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_BEGIN(JSEventHandler) + if (tmp->IsBlackForCC()) { + return true; + } + // If we have a target, it is the one which has tmp as onfoo handler. + if (tmp->mTarget) { + nsXPCOMCycleCollectionParticipant* cp = nullptr; + CallQueryInterface(tmp->mTarget, &cp); + nsISupports* canonical = nullptr; + tmp->mTarget->QueryInterface(NS_GET_IID(nsCycleCollectionISupports), + reinterpret_cast<void**>(&canonical)); + // Usually CanSkip ends up unmarking the event listeners of mTarget, + // so tmp may become black. + if (cp && canonical && cp->CanSkip(canonical, true)) { + return tmp->IsBlackForCC(); + } + } +NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_END + +NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_IN_CC_BEGIN(JSEventHandler) + return tmp->IsBlackForCC(); +NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_IN_CC_END + +NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_THIS_BEGIN(JSEventHandler) + return tmp->IsBlackForCC(); +NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_THIS_END + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(JSEventHandler) + NS_INTERFACE_MAP_ENTRY(nsIDOMEventListener) + NS_INTERFACE_MAP_ENTRY(nsISupports) + NS_INTERFACE_MAP_ENTRY(JSEventHandler) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTING_ADDREF(JSEventHandler) +NS_IMPL_CYCLE_COLLECTING_RELEASE(JSEventHandler) + +bool JSEventHandler::IsBlackForCC() { + // We can claim to be black if all the things we reference are + // effectively black already. + return !mTypedHandler.HasEventHandler() || + mTypedHandler.Ptr()->IsBlackForCC(); +} + +nsresult JSEventHandler::HandleEvent(Event* aEvent) { + nsCOMPtr<EventTarget> target = mTarget; + if (!target || !mTypedHandler.HasEventHandler() || + !GetTypedEventHandler().Ptr()->CallbackPreserveColor()) { + return NS_ERROR_FAILURE; + } + + bool isMainThread = aEvent->IsMainThreadEvent(); + bool isChromeHandler = + isMainThread + ? nsContentUtils::ObjectPrincipal( + GetTypedEventHandler().Ptr()->CallbackGlobalOrNull()) == + nsContentUtils::GetSystemPrincipal() + : mozilla::dom::IsCurrentThreadRunningChromeWorker(); + + if (mTypedHandler.Type() == TypedEventHandler::eOnError) { + MOZ_ASSERT_IF(mEventName, mEventName == nsGkAtoms::onerror); + + nsString errorMsg, file; + EventOrString msgOrEvent; + Optional<nsAString> fileName; + Optional<uint32_t> lineNumber; + Optional<uint32_t> columnNumber; + Optional<JS::Handle<JS::Value>> error; + + NS_ENSURE_TRUE(aEvent, NS_ERROR_UNEXPECTED); + ErrorEvent* scriptEvent = aEvent->AsErrorEvent(); + if (scriptEvent) { + scriptEvent->GetMessage(errorMsg); + msgOrEvent.SetAsString().ShareOrDependUpon(errorMsg); + + scriptEvent->GetFilename(file); + fileName = &file; + + lineNumber.Construct(); + lineNumber.Value() = scriptEvent->Lineno(); + + columnNumber.Construct(); + columnNumber.Value() = scriptEvent->Colno(); + + error.Construct(RootingCx()); + scriptEvent->GetError(&error.Value()); + } else { + msgOrEvent.SetAsEvent() = aEvent; + } + + RefPtr<OnErrorEventHandlerNonNull> handler = + mTypedHandler.OnErrorEventHandler(); + ErrorResult rv; + JS::Rooted<JS::Value> retval(RootingCx()); + handler->Call(target, msgOrEvent, fileName, lineNumber, columnNumber, error, + &retval, rv); + if (rv.Failed()) { + return rv.StealNSResult(); + } + + if (retval.isBoolean() && retval.toBoolean() == bool(scriptEvent)) { + aEvent->PreventDefaultInternal(isChromeHandler); + } + return NS_OK; + } + + if (mTypedHandler.Type() == TypedEventHandler::eOnBeforeUnload) { + MOZ_ASSERT(mEventName == nsGkAtoms::onbeforeunload); + + RefPtr<OnBeforeUnloadEventHandlerNonNull> handler = + mTypedHandler.OnBeforeUnloadEventHandler(); + ErrorResult rv; + nsString retval; + handler->Call(target, *aEvent, retval, rv); + if (rv.Failed()) { + return rv.StealNSResult(); + } + + BeforeUnloadEvent* beforeUnload = aEvent->AsBeforeUnloadEvent(); + NS_ENSURE_STATE(beforeUnload); + + if (!DOMStringIsNull(retval)) { + aEvent->PreventDefaultInternal(isChromeHandler); + + nsAutoString text; + beforeUnload->GetReturnValue(text); + + // Set the text in the beforeUnload event as long as it wasn't + // already set (through event.returnValue, which takes + // precedence over a value returned from a JS function in IE) + if (text.IsEmpty()) { + beforeUnload->SetReturnValue(retval); + } + } + + return NS_OK; + } + + MOZ_ASSERT(mTypedHandler.Type() == TypedEventHandler::eNormal); + ErrorResult rv; + RefPtr<EventHandlerNonNull> handler = mTypedHandler.NormalEventHandler(); + JS::Rooted<JS::Value> retval(RootingCx()); + handler->Call(target, *aEvent, &retval, rv); + if (rv.Failed()) { + return rv.StealNSResult(); + } + + // If the handler returned false, then prevent default. + if (retval.isBoolean() && !retval.toBoolean()) { + aEvent->PreventDefaultInternal(isChromeHandler); + } + + return NS_OK; +} + +} // namespace mozilla + +using namespace mozilla; + +/* + * Factory functions + */ + +nsresult NS_NewJSEventHandler(mozilla::dom::EventTarget* aTarget, + nsAtom* aEventType, + const TypedEventHandler& aTypedHandler, + JSEventHandler** aReturn) { + NS_ENSURE_ARG(aEventType || !NS_IsMainThread()); + JSEventHandler* it = new JSEventHandler(aTarget, aEventType, aTypedHandler); + NS_ADDREF(*aReturn = it); + + return NS_OK; +} diff --git a/dom/events/JSEventHandler.h b/dom/events/JSEventHandler.h new file mode 100644 index 0000000000..60a3a259e3 --- /dev/null +++ b/dom/events/JSEventHandler.h @@ -0,0 +1,231 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#ifndef mozilla_JSEventHandler_h_ +#define mozilla_JSEventHandler_h_ + +#include "mozilla/Attributes.h" +#include "mozilla/MemoryReporting.h" +#include "mozilla/dom/EventHandlerBinding.h" +#include "nsCOMPtr.h" +#include "nsCycleCollectionParticipant.h" +#include "nsAtom.h" +#include "nsIDOMEventListener.h" +#include "nsIScriptContext.h" + +namespace mozilla { +namespace dom { +class EventTarget; +} // namespace dom + +class TypedEventHandler { + public: + enum HandlerType { + eUnset = 0, + eNormal = 0x1, + eOnError = 0x2, + eOnBeforeUnload = 0x3, + eTypeBits = 0x3 + }; + + TypedEventHandler() : mBits(0) {} + + explicit TypedEventHandler(dom::EventHandlerNonNull* aHandler) : mBits(0) { + Assign(aHandler, eNormal); + } + + explicit TypedEventHandler(dom::OnErrorEventHandlerNonNull* aHandler) + : mBits(0) { + Assign(aHandler, eOnError); + } + + explicit TypedEventHandler(dom::OnBeforeUnloadEventHandlerNonNull* aHandler) + : mBits(0) { + Assign(aHandler, eOnBeforeUnload); + } + + TypedEventHandler(const TypedEventHandler& aOther) { + if (aOther.HasEventHandler()) { + // Have to make sure we take our own ref + Assign(aOther.Ptr(), aOther.Type()); + } else { + mBits = 0; + } + } + + ~TypedEventHandler() { ReleaseHandler(); } + + HandlerType Type() const { return HandlerType(mBits & eTypeBits); } + + bool HasEventHandler() const { return !!Ptr(); } + + void SetHandler(const TypedEventHandler& aHandler) { + if (aHandler.HasEventHandler()) { + ReleaseHandler(); + Assign(aHandler.Ptr(), aHandler.Type()); + } else { + ForgetHandler(); + } + } + + dom::EventHandlerNonNull* NormalEventHandler() const { + MOZ_ASSERT(Type() == eNormal && Ptr()); + return reinterpret_cast<dom::EventHandlerNonNull*>(Ptr()); + } + + void SetHandler(dom::EventHandlerNonNull* aHandler) { + ReleaseHandler(); + Assign(aHandler, eNormal); + } + + dom::OnBeforeUnloadEventHandlerNonNull* OnBeforeUnloadEventHandler() const { + MOZ_ASSERT(Type() == eOnBeforeUnload); + return reinterpret_cast<dom::OnBeforeUnloadEventHandlerNonNull*>(Ptr()); + } + + void SetHandler(dom::OnBeforeUnloadEventHandlerNonNull* aHandler) { + ReleaseHandler(); + Assign(aHandler, eOnBeforeUnload); + } + + dom::OnErrorEventHandlerNonNull* OnErrorEventHandler() const { + MOZ_ASSERT(Type() == eOnError); + return reinterpret_cast<dom::OnErrorEventHandlerNonNull*>(Ptr()); + } + + void SetHandler(dom::OnErrorEventHandlerNonNull* aHandler) { + ReleaseHandler(); + Assign(aHandler, eOnError); + } + + dom::CallbackFunction* Ptr() const { + // Have to cast eTypeBits so we don't have to worry about + // promotion issues after the bitflip. + return reinterpret_cast<dom::CallbackFunction*>(mBits & + ~uintptr_t(eTypeBits)); + } + + void ForgetHandler() { + ReleaseHandler(); + mBits = 0; + } + + bool operator==(const TypedEventHandler& aOther) const { + return Ptr() && aOther.Ptr() && + Ptr()->CallbackPreserveColor() == + aOther.Ptr()->CallbackPreserveColor(); + } + + private: + void operator=(const TypedEventHandler&) = delete; + + void ReleaseHandler() { + nsISupports* ptr = Ptr(); + NS_IF_RELEASE(ptr); + } + + void Assign(nsISupports* aHandler, HandlerType aType) { + MOZ_ASSERT(aHandler, "Must have handler"); + NS_ADDREF(aHandler); + mBits = uintptr_t(aHandler) | uintptr_t(aType); + } + + uintptr_t mBits; +}; + +/** + * Implemented by script event listeners. Used to retrieve the script object + * corresponding to the event target and the handler itself. + * + * Note, mTarget is a raw pointer and the owner of the JSEventHandler object + * is expected to call Disconnect()! + */ + +#define NS_JSEVENTHANDLER_IID \ + { \ + 0x4f486881, 0x1956, 0x4079, { \ + 0x8c, 0xa0, 0xf3, 0xbd, 0x60, 0x5c, 0xc2, 0x79 \ + } \ + } + +class JSEventHandler : public nsIDOMEventListener { + public: + NS_DECLARE_STATIC_IID_ACCESSOR(NS_JSEVENTHANDLER_IID) + + JSEventHandler(dom::EventTarget* aTarget, nsAtom* aType, + const TypedEventHandler& aTypedHandler); + + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + + // nsIDOMEventListener interface + NS_DECL_NSIDOMEVENTLISTENER + + void Disconnect() { mTarget = nullptr; } + + const TypedEventHandler& GetTypedEventHandler() const { + return mTypedHandler; + } + + void ForgetHandler() { mTypedHandler.ForgetHandler(); } + + nsAtom* EventName() const { return mEventName; } + + // Set a handler for this event listener. The handler must already + // be bound to the right target. + void SetHandler(const TypedEventHandler& aTypedHandler) { + mTypedHandler.SetHandler(aTypedHandler); + } + void SetHandler(dom::EventHandlerNonNull* aHandler) { + mTypedHandler.SetHandler(aHandler); + } + void SetHandler(dom::OnBeforeUnloadEventHandlerNonNull* aHandler) { + mTypedHandler.SetHandler(aHandler); + } + void SetHandler(dom::OnErrorEventHandlerNonNull* aHandler) { + mTypedHandler.SetHandler(aHandler); + } + + size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const { + return 0; + + // Measurement of the following members may be added later if DMD finds it + // is worthwhile: + // - mTarget + // + // The following members are not measured: + // - mTypedHandler: may be shared with others + // - mEventName: shared with others + } + + size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) { + return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf); + } + + NS_DECL_CYCLE_COLLECTION_SKIPPABLE_CLASS(JSEventHandler) + + bool IsBlackForCC(); + + protected: + virtual ~JSEventHandler(); + + dom::EventTarget* mTarget; + RefPtr<nsAtom> mEventName; + TypedEventHandler mTypedHandler; +}; + +NS_DEFINE_STATIC_IID_ACCESSOR(JSEventHandler, NS_JSEVENTHANDLER_IID) + +} // namespace mozilla + +/** + * Factory function. aHandler must already be bound to aTarget. + * aContext is allowed to be null if aHandler is already set up. + */ +nsresult NS_NewJSEventHandler(mozilla::dom::EventTarget* aTarget, nsAtom* aType, + const mozilla::TypedEventHandler& aTypedHandler, + mozilla::JSEventHandler** aReturn); + +#endif // mozilla_JSEventHandler_h_ diff --git a/dom/events/KeyEventHandler.cpp b/dom/events/KeyEventHandler.cpp new file mode 100644 index 0000000000..4fdf3f7107 --- /dev/null +++ b/dom/events/KeyEventHandler.cpp @@ -0,0 +1,741 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#include "mozilla/ArrayUtils.h" + +#include "nsCOMPtr.h" +#include "nsQueryObject.h" +#include "KeyEventHandler.h" +#include "nsContentUtils.h" +#include "nsGlobalWindow.h" +#include "nsGlobalWindowCommands.h" +#include "nsIContent.h" +#include "nsAtom.h" +#include "nsNameSpaceManager.h" +#include "mozilla/dom/Document.h" +#include "nsIController.h" +#include "nsIControllers.h" +#include "nsXULElement.h" +#include "nsFocusManager.h" +#include "nsIFormControl.h" +#include "nsPIDOMWindow.h" +#include "nsPIWindowRoot.h" +#include "nsIScriptError.h" +#include "nsIWeakReferenceUtils.h" +#include "nsString.h" +#include "nsReadableUtils.h" +#include "nsGkAtoms.h" +#include "nsDOMCID.h" +#include "nsUnicharUtils.h" +#include "nsCRT.h" +#include "nsJSUtils.h" +#include "mozilla/BasicEvents.h" +#include "mozilla/LookAndFeel.h" +#include "mozilla/JSEventHandler.h" +#include "mozilla/Preferences.h" +#include "mozilla/TextEvents.h" +#include "mozilla/dom/Element.h" +#include "mozilla/dom/Event.h" +#include "mozilla/dom/EventHandlerBinding.h" +#include "mozilla/dom/HTMLInputElement.h" +#include "mozilla/dom/HTMLTextAreaElement.h" +#include "mozilla/dom/KeyboardEvent.h" +#include "mozilla/dom/KeyboardEventBinding.h" +#include "mozilla/dom/ScriptSettings.h" +#include "mozilla/layers/KeyboardMap.h" +#include "xpcpublic.h" + +namespace mozilla { + +using namespace mozilla::layers; + +uint32_t KeyEventHandler::gRefCnt = 0; + +const int32_t KeyEventHandler::cShift = (1 << 0); +const int32_t KeyEventHandler::cAlt = (1 << 1); +const int32_t KeyEventHandler::cControl = (1 << 2); +const int32_t KeyEventHandler::cMeta = (1 << 3); +const int32_t KeyEventHandler::cOS = (1 << 4); + +const int32_t KeyEventHandler::cShiftMask = (1 << 5); +const int32_t KeyEventHandler::cAltMask = (1 << 6); +const int32_t KeyEventHandler::cControlMask = (1 << 7); +const int32_t KeyEventHandler::cMetaMask = (1 << 8); +const int32_t KeyEventHandler::cOSMask = (1 << 9); + +const int32_t KeyEventHandler::cAllModifiers = + cShiftMask | cAltMask | cControlMask | cMetaMask | cOSMask; + +KeyEventHandler::KeyEventHandler(dom::Element* aHandlerElement, + ReservedKey aReserved) + : mHandlerElement(nullptr), + mIsXULKey(true), + mReserved(aReserved), + mNextHandler(nullptr) { + Init(); + + // Make sure our prototype is initialized. + ConstructPrototype(aHandlerElement); +} + +KeyEventHandler::KeyEventHandler(ShortcutKeyData* aKeyData) + : mCommand(nullptr), + mIsXULKey(false), + mReserved(ReservedKey_False), + mNextHandler(nullptr) { + Init(); + + ConstructPrototype(nullptr, aKeyData->event, aKeyData->command, + aKeyData->keycode, aKeyData->key, aKeyData->modifiers); +} + +KeyEventHandler::~KeyEventHandler() { + --gRefCnt; + if (mIsXULKey) { + NS_IF_RELEASE(mHandlerElement); + } else if (mCommand) { + free(mCommand); + } + + // We own the next handler in the chain, so delete it now. + NS_CONTENT_DELETE_LIST_MEMBER(KeyEventHandler, this, mNextHandler); +} + +void KeyEventHandler::GetCommand(nsAString& aCommand) const { + MOZ_ASSERT(aCommand.IsEmpty()); + if (mIsXULKey) { + MOZ_ASSERT_UNREACHABLE("Not yet implemented"); + return; + } + if (mCommand) { + aCommand.Assign(mCommand); + } +} + +bool KeyEventHandler::TryConvertToKeyboardShortcut( + KeyboardShortcut* aOut) const { + // Convert the event type + KeyboardInput::KeyboardEventType eventType; + + if (mEventName == nsGkAtoms::keydown) { + eventType = KeyboardInput::KEY_DOWN; + } else if (mEventName == nsGkAtoms::keypress) { + eventType = KeyboardInput::KEY_PRESS; + } else if (mEventName == nsGkAtoms::keyup) { + eventType = KeyboardInput::KEY_UP; + } else { + return false; + } + + // Convert the modifiers + Modifiers modifiersMask = GetModifiersMask(); + Modifiers modifiers = GetModifiers(); + + // Mask away any bits that won't be compared + modifiers &= modifiersMask; + + // Convert the keyCode or charCode + uint32_t keyCode; + uint32_t charCode; + + if (mMisc) { + keyCode = 0; + charCode = static_cast<uint32_t>(mDetail); + } else { + keyCode = static_cast<uint32_t>(mDetail); + charCode = 0; + } + + NS_LossyConvertUTF16toASCII commandText(mCommand); + KeyboardScrollAction action; + if (!nsGlobalWindowCommands::FindScrollCommand(commandText.get(), &action)) { + // This action doesn't represent a scroll so we need to create a dispatch + // to content keyboard shortcut so APZ handles this command correctly + *aOut = KeyboardShortcut(eventType, keyCode, charCode, modifiers, + modifiersMask); + return true; + } + + // This prototype is a command which represents a scroll action, so create + // a keyboard shortcut to handle it + *aOut = KeyboardShortcut(eventType, keyCode, charCode, modifiers, + modifiersMask, action); + return true; +} + +bool KeyEventHandler::KeyElementIsDisabled() const { + RefPtr<dom::Element> keyElement = GetHandlerElement(); + return keyElement && + keyElement->AttrValueIs(kNameSpaceID_None, nsGkAtoms::disabled, + nsGkAtoms::_true, eCaseMatters); +} + +already_AddRefed<dom::Element> KeyEventHandler::GetHandlerElement() const { + if (mIsXULKey) { + nsCOMPtr<dom::Element> element = do_QueryReferent(mHandlerElement); + return element.forget(); + } + + return nullptr; +} + +nsresult KeyEventHandler::ExecuteHandler(dom::EventTarget* aTarget, + dom::Event* aEvent) { + // In both cases the union should be defined. + if (!mHandlerElement) { + return NS_ERROR_FAILURE; + } + + // XUL handlers and commands shouldn't be triggered by non-trusted + // events. + if (!aEvent->IsTrusted()) { + return NS_OK; + } + + if (mIsXULKey) { + return DispatchXULKeyCommand(aEvent); + } + + return DispatchXBLCommand(aTarget, aEvent); +} + +nsresult KeyEventHandler::DispatchXBLCommand(dom::EventTarget* aTarget, + dom::Event* aEvent) { + // This is a special-case optimization to make command handling fast. + // It isn't really a part of XBL, but it helps speed things up. + + if (aEvent) { + // See if preventDefault has been set. If so, don't execute. + if (aEvent->DefaultPrevented()) { + return NS_OK; + } + bool dispatchStopped = aEvent->IsDispatchStopped(); + if (dispatchStopped) { + return NS_OK; + } + } + + // Instead of executing JS, let's get the controller for the bound + // element and call doCommand on it. + nsCOMPtr<nsIController> controller; + + nsCOMPtr<nsPIDOMWindowOuter> privateWindow; + nsCOMPtr<nsPIWindowRoot> windowRoot = + nsPIWindowRoot::FromEventTargetOrNull(aTarget); + if (windowRoot) { + privateWindow = windowRoot->GetWindow(); + } else { + privateWindow = nsPIDOMWindowOuter::FromEventTargetOrNull(aTarget); + if (!privateWindow) { + nsCOMPtr<dom::Document> doc; + // XXXbz sXBL/XBL2 issue -- this should be the "scope doc" or + // something... whatever we use when wrapping DOM nodes + // normally. It's not clear that the owner doc is the right + // thing. + if (nsIContent* content = nsIContent::FromEventTargetOrNull(aTarget)) { + doc = content->OwnerDoc(); + } + + if (!doc) { + if (nsINode* node = nsINode::FromEventTargetOrNull(aTarget)) { + if (node->IsDocument()) { + doc = node->AsDocument(); + } + } + } + + if (!doc) { + return NS_ERROR_FAILURE; + } + + privateWindow = doc->GetWindow(); + if (!privateWindow) { + return NS_ERROR_FAILURE; + } + } + + windowRoot = privateWindow->GetTopWindowRoot(); + } + + NS_LossyConvertUTF16toASCII command(mCommand); + if (windowRoot) { + // If user tries to do something, user must try to do it in visible window. + // So, let's retrieve controller of visible window. + windowRoot->GetControllerForCommand(command.get(), true, + getter_AddRefs(controller)); + } else { + controller = + GetController(aTarget); // We're attached to the receiver possibly. + } + + // We are the default action for this command. + // Stop any other default action from executing. + aEvent->PreventDefault(); + + if (mEventName == nsGkAtoms::keypress && + mDetail == dom::KeyboardEvent_Binding::DOM_VK_SPACE && mMisc == 1) { + // get the focused element so that we can pageDown only at + // certain times. + + nsCOMPtr<nsPIDOMWindowOuter> windowToCheck; + if (windowRoot) { + windowToCheck = windowRoot->GetWindow(); + } else { + windowToCheck = privateWindow->GetPrivateRoot(); + } + + nsCOMPtr<nsIContent> focusedContent; + if (windowToCheck) { + nsCOMPtr<nsPIDOMWindowOuter> focusedWindow; + focusedContent = nsFocusManager::GetFocusedDescendant( + windowToCheck, nsFocusManager::eIncludeAllDescendants, + getter_AddRefs(focusedWindow)); + } + + // If the focus is in an editable region, don't scroll. + if (focusedContent && focusedContent->IsEditable()) { + return NS_OK; + } + + // If the focus is in a form control, don't scroll. + for (nsIContent* c = focusedContent; c; c = c->GetParent()) { + nsCOMPtr<nsIFormControl> formControl = do_QueryInterface(c); + if (formControl) { + return NS_OK; + } + } + } + + if (controller) { + controller->DoCommand(command.get()); + } + + return NS_OK; +} + +nsresult KeyEventHandler::DispatchXULKeyCommand(dom::Event* aEvent) { + nsCOMPtr<dom::Element> handlerElement = GetHandlerElement(); + NS_ENSURE_STATE(handlerElement); + if (handlerElement->AttrValueIs(kNameSpaceID_None, nsGkAtoms::disabled, + nsGkAtoms::_true, eCaseMatters)) { + // Don't dispatch command events for disabled keys. + return NS_OK; + } + + aEvent->PreventDefault(); + + // Copy the modifiers from the key event. + RefPtr<dom::KeyboardEvent> domKeyboardEvent = aEvent->AsKeyboardEvent(); + if (!domKeyboardEvent) { + NS_ERROR("Trying to execute a key handler for a non-key event!"); + return NS_ERROR_FAILURE; + } + + // XXX We should use mozilla::Modifiers for supporting all modifiers. + + bool isAlt = domKeyboardEvent->AltKey(); + bool isControl = domKeyboardEvent->CtrlKey(); + bool isShift = domKeyboardEvent->ShiftKey(); + bool isMeta = domKeyboardEvent->MetaKey(); + + nsContentUtils::DispatchXULCommand(handlerElement, true, nullptr, nullptr, + isControl, isAlt, isShift, isMeta); + return NS_OK; +} + +Modifiers KeyEventHandler::GetModifiers() const { + Modifiers modifiers = 0; + + if (mKeyMask & cMeta) { + modifiers |= MODIFIER_META; + } + if (mKeyMask & cOS) { + modifiers |= MODIFIER_OS; + } + if (mKeyMask & cShift) { + modifiers |= MODIFIER_SHIFT; + } + if (mKeyMask & cAlt) { + modifiers |= MODIFIER_ALT; + } + if (mKeyMask & cControl) { + modifiers |= MODIFIER_CONTROL; + } + + return modifiers; +} + +Modifiers KeyEventHandler::GetModifiersMask() const { + Modifiers modifiersMask = 0; + + if (mKeyMask & cMetaMask) { + modifiersMask |= MODIFIER_META; + } + if (mKeyMask & cOSMask) { + modifiersMask |= MODIFIER_OS; + } + if (mKeyMask & cShiftMask) { + modifiersMask |= MODIFIER_SHIFT; + } + if (mKeyMask & cAltMask) { + modifiersMask |= MODIFIER_ALT; + } + if (mKeyMask & cControlMask) { + modifiersMask |= MODIFIER_CONTROL; + } + + return modifiersMask; +} + +already_AddRefed<nsIController> KeyEventHandler::GetController( + dom::EventTarget* aTarget) { + if (!aTarget) { + return nullptr; + } + + // XXX Fix this so there's a generic interface that describes controllers, + // This code should have no special knowledge of what objects might have + // controllers. + nsCOMPtr<nsIControllers> controllers; + + if (nsIContent* targetContent = nsIContent::FromEventTarget(aTarget)) { + RefPtr<nsXULElement> xulElement = nsXULElement::FromNode(targetContent); + if (xulElement) { + controllers = xulElement->GetControllers(IgnoreErrors()); + } + + if (!controllers) { + dom::HTMLTextAreaElement* htmlTextArea = + dom::HTMLTextAreaElement::FromNode(targetContent); + if (htmlTextArea) { + htmlTextArea->GetControllers(getter_AddRefs(controllers)); + } + } + + if (!controllers) { + dom::HTMLInputElement* htmlInputElement = + dom::HTMLInputElement::FromNode(targetContent); + if (htmlInputElement) { + htmlInputElement->GetControllers(getter_AddRefs(controllers)); + } + } + } + + if (!controllers) { + if (nsCOMPtr<nsPIDOMWindowOuter> domWindow = + nsPIDOMWindowOuter::FromEventTarget(aTarget)) { + domWindow->GetControllers(getter_AddRefs(controllers)); + } + } + + // Return the first controller. + // XXX This code should be checking the command name and using supportscommand + // and iscommandenabled. + nsCOMPtr<nsIController> controller; + if (controllers) { + controllers->GetControllerAt(0, getter_AddRefs(controller)); + } + + return controller.forget(); +} + +bool KeyEventHandler::KeyEventMatched( + dom::KeyboardEvent* aDomKeyboardEvent, uint32_t aCharCode, + const IgnoreModifierState& aIgnoreModifierState) { + if (mDetail != -1) { + // Get the keycode or charcode of the key event. + uint32_t code; + + if (mMisc) { + if (aCharCode) { + code = aCharCode; + } else { + code = aDomKeyboardEvent->CharCode(); + } + if (IS_IN_BMP(code)) { + code = ToLowerCase(char16_t(code)); + } + } else { + code = aDomKeyboardEvent->KeyCode(); + } + + if (code != static_cast<uint32_t>(mDetail)) { + return false; + } + } + + return ModifiersMatchMask(aDomKeyboardEvent, aIgnoreModifierState); +} + +struct keyCodeData { + const char* str; + uint16_t strlength; + uint16_t keycode; +}; + +// All of these must be uppercase, since the function below does +// case-insensitive comparison by converting to uppercase. +// XXX: be sure to check this periodically for new symbol additions! +static const keyCodeData gKeyCodes[] = { + +#define NS_DEFINE_VK(aDOMKeyName, aDOMKeyCode) \ + {#aDOMKeyName, sizeof(#aDOMKeyName) - 1, aDOMKeyCode}, +#include "mozilla/VirtualKeyCodeList.h" +#undef NS_DEFINE_VK + + {nullptr, 0, 0}}; + +int32_t KeyEventHandler::GetMatchingKeyCode(const nsAString& aKeyName) { + nsAutoCString keyName; + LossyCopyUTF16toASCII(aKeyName, keyName); + ToUpperCase(keyName); // We want case-insensitive comparison with data + // stored as uppercase. + + uint32_t keyNameLength = keyName.Length(); + const char* keyNameStr = keyName.get(); + for (unsigned long i = 0; i < ArrayLength(gKeyCodes) - 1; ++i) { + if (keyNameLength == gKeyCodes[i].strlength && + !nsCRT::strcmp(gKeyCodes[i].str, keyNameStr)) { + return gKeyCodes[i].keycode; + } + } + + return 0; +} + +int32_t KeyEventHandler::KeyToMask(uint32_t key) { + switch (key) { + case dom::KeyboardEvent_Binding::DOM_VK_META: + return cMeta | cMetaMask; + + case dom::KeyboardEvent_Binding::DOM_VK_WIN: + return cOS | cOSMask; + + case dom::KeyboardEvent_Binding::DOM_VK_ALT: + return cAlt | cAltMask; + + case dom::KeyboardEvent_Binding::DOM_VK_CONTROL: + default: + return cControl | cControlMask; + } +} + +// static +int32_t KeyEventHandler::AccelKeyMask() { + switch (WidgetInputEvent::AccelModifier()) { + case MODIFIER_ALT: + return KeyToMask(dom::KeyboardEvent_Binding::DOM_VK_ALT); + case MODIFIER_CONTROL: + return KeyToMask(dom::KeyboardEvent_Binding::DOM_VK_CONTROL); + case MODIFIER_META: + return KeyToMask(dom::KeyboardEvent_Binding::DOM_VK_META); + case MODIFIER_OS: + return KeyToMask(dom::KeyboardEvent_Binding::DOM_VK_WIN); + default: + MOZ_CRASH("Handle the new result of WidgetInputEvent::AccelModifier()"); + return 0; + } +} + +void KeyEventHandler::GetEventType(nsAString& aEvent) { + nsCOMPtr<dom::Element> handlerElement = GetHandlerElement(); + if (!handlerElement) { + aEvent.Truncate(); + return; + } + handlerElement->GetAttr(kNameSpaceID_None, nsGkAtoms::event, aEvent); + + if (aEvent.IsEmpty() && mIsXULKey) { + // If no type is specified for a XUL <key> element, let's assume that we're + // "keypress". + aEvent.AssignLiteral("keypress"); + } +} + +void KeyEventHandler::ConstructPrototype(dom::Element* aKeyElement, + const char16_t* aEvent, + const char16_t* aCommand, + const char16_t* aKeyCode, + const char16_t* aCharCode, + const char16_t* aModifiers) { + mDetail = -1; + mMisc = 0; + mKeyMask = 0; + nsAutoString modifiers; + + if (mIsXULKey) { + nsWeakPtr weak = do_GetWeakReference(aKeyElement); + if (!weak) { + return; + } + weak.swap(mHandlerElement); + + nsAutoString event; + GetEventType(event); + if (event.IsEmpty()) { + return; + } + mEventName = NS_Atomize(event); + + aKeyElement->GetAttr(kNameSpaceID_None, nsGkAtoms::modifiers, modifiers); + } else { + mCommand = ToNewUnicode(nsDependentString(aCommand)); + mEventName = NS_Atomize(aEvent); + modifiers = aModifiers; + } + + BuildModifiers(modifiers); + + nsAutoString key(aCharCode); + if (key.IsEmpty()) { + if (mIsXULKey) { + aKeyElement->GetAttr(kNameSpaceID_None, nsGkAtoms::key, key); + if (key.IsEmpty()) { + aKeyElement->GetAttr(kNameSpaceID_None, nsGkAtoms::charcode, key); + } + } + } + + if (!key.IsEmpty()) { + if (mKeyMask == 0) { + mKeyMask = cAllModifiers; + } + ToLowerCase(key); + + // We have a charcode. + mMisc = 1; + mDetail = key[0]; + const uint8_t GTK2Modifiers = cShift | cControl | cShiftMask | cControlMask; + if (mIsXULKey && (mKeyMask & GTK2Modifiers) == GTK2Modifiers && + modifiers.First() != char16_t(',') && + (mDetail == 'u' || mDetail == 'U')) { + ReportKeyConflict(key.get(), modifiers.get(), aKeyElement, + "GTK2Conflict2"); + } + const uint8_t WinModifiers = cControl | cAlt | cControlMask | cAltMask; + if (mIsXULKey && (mKeyMask & WinModifiers) == WinModifiers && + modifiers.First() != char16_t(',') && + (('A' <= mDetail && mDetail <= 'Z') || + ('a' <= mDetail && mDetail <= 'z'))) { + ReportKeyConflict(key.get(), modifiers.get(), aKeyElement, + "WinConflict2"); + } + } else { + key.Assign(aKeyCode); + if (mIsXULKey) { + aKeyElement->GetAttr(kNameSpaceID_None, nsGkAtoms::keycode, key); + } + + if (!key.IsEmpty()) { + if (mKeyMask == 0) { + mKeyMask = cAllModifiers; + } + mDetail = GetMatchingKeyCode(key); + } + } +} + +void KeyEventHandler::BuildModifiers(nsAString& aModifiers) { + if (!aModifiers.IsEmpty()) { + mKeyMask = cAllModifiers; + char* str = ToNewCString(aModifiers); + char* newStr; + char* token = nsCRT::strtok(str, ", \t", &newStr); + while (token != nullptr) { + if (strcmp(token, "shift") == 0) { + mKeyMask |= cShift | cShiftMask; + } else if (strcmp(token, "alt") == 0) { + mKeyMask |= cAlt | cAltMask; + } else if (strcmp(token, "meta") == 0) { + mKeyMask |= cMeta | cMetaMask; + } else if (strcmp(token, "os") == 0) { + mKeyMask |= cOS | cOSMask; + } else if (strcmp(token, "control") == 0) { + mKeyMask |= cControl | cControlMask; + } else if (strcmp(token, "accel") == 0) { + mKeyMask |= AccelKeyMask(); + } else if (strcmp(token, "access") == 0) { + mKeyMask |= KeyToMask(LookAndFeel::GetMenuAccessKey()); + } else if (strcmp(token, "any") == 0) { + mKeyMask &= ~(mKeyMask << 5); + } + + token = nsCRT::strtok(newStr, ", \t", &newStr); + } + + free(str); + } +} + +void KeyEventHandler::ReportKeyConflict(const char16_t* aKey, + const char16_t* aModifiers, + dom::Element* aKeyElement, + const char* aMessageName) { + nsCOMPtr<dom::Document> doc = aKeyElement->OwnerDoc(); + + nsAutoString id; + aKeyElement->GetAttr(kNameSpaceID_None, nsGkAtoms::id, id); + AutoTArray<nsString, 3> params; + params.AppendElement(aKey); + params.AppendElement(aModifiers); + params.AppendElement(id); + nsContentUtils::ReportToConsole(nsIScriptError::warningFlag, + "Key dom::Event Handler"_ns, doc, + nsContentUtils::eDOM_PROPERTIES, aMessageName, + params, nullptr, u""_ns, 0); +} + +bool KeyEventHandler::ModifiersMatchMask( + dom::UIEvent* aEvent, const IgnoreModifierState& aIgnoreModifierState) { + WidgetInputEvent* inputEvent = aEvent->WidgetEventPtr()->AsInputEvent(); + NS_ENSURE_TRUE(inputEvent, false); + + if (mKeyMask & cMetaMask) { + if (inputEvent->IsMeta() != ((mKeyMask & cMeta) != 0)) { + return false; + } + } + + if ((mKeyMask & cOSMask) && !aIgnoreModifierState.mOS) { + if (inputEvent->IsOS() != ((mKeyMask & cOS) != 0)) { + return false; + } + } + + if (mKeyMask & cShiftMask && !aIgnoreModifierState.mShift) { + if (inputEvent->IsShift() != ((mKeyMask & cShift) != 0)) { + return false; + } + } + + if (mKeyMask & cAltMask) { + if (inputEvent->IsAlt() != ((mKeyMask & cAlt) != 0)) { + return false; + } + } + + if (mKeyMask & cControlMask) { + if (inputEvent->IsControl() != ((mKeyMask & cControl) != 0)) { + return false; + } + } + + return true; +} + +size_t KeyEventHandler::SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const { + size_t n = 0; + for (const KeyEventHandler* handler = this; handler; + handler = handler->mNextHandler) { + n += aMallocSizeOf(handler); + if (!mIsXULKey) { + n += aMallocSizeOf(handler->mCommand); + } + } + return n; +} + +} // namespace mozilla diff --git a/dom/events/KeyEventHandler.h b/dom/events/KeyEventHandler.h new file mode 100644 index 0000000000..5d444cf322 --- /dev/null +++ b/dom/events/KeyEventHandler.h @@ -0,0 +1,176 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#ifndef mozilla_KeyEventHandler_h_ +#define mozilla_KeyEventHandler_h_ + +#include "mozilla/EventForwards.h" +#include "mozilla/MemoryReporting.h" +#include "nsAtom.h" +#include "nsString.h" +#include "nsCOMPtr.h" +#include "nsIController.h" +#include "nsIWeakReference.h" +#include "nsCycleCollectionParticipant.h" +#include "js/TypeDecls.h" +#include "mozilla/ShortcutKeys.h" + +namespace mozilla { + +namespace layers { +class KeyboardShortcut; +} // namespace layers + +struct IgnoreModifierState; + +namespace dom { +class Event; +class UIEvent; +class Element; +class EventTarget; +class KeyboardEvent; +class Element; +} // namespace dom + +// Values of the reserved attribute. When unset, the default value depends on +// the permissions.default.shortcuts preference. +enum ReservedKey : uint8_t { + ReservedKey_False = 0, + ReservedKey_True = 1, + ReservedKey_Unset = 2, +}; + +class KeyEventHandler final { + public: + // This constructor is used only by XUL key handlers (e.g., <key>) + explicit KeyEventHandler(dom::Element* aHandlerElement, + ReservedKey aReserved); + + // This constructor is used for keyboard handlers for browser, editor, input + // and textarea elements. + explicit KeyEventHandler(ShortcutKeyData* aKeyData); + + ~KeyEventHandler(); + + /** + * Try and convert this XBL handler into an APZ KeyboardShortcut for handling + * key events on the compositor thread. This only works for XBL handlers that + * represent scroll commands. + * + * @param aOut the converted KeyboardShortcut, must be non null + * @return whether the handler was converted into a KeyboardShortcut + */ + bool TryConvertToKeyboardShortcut(layers::KeyboardShortcut* aOut) const; + + bool EventTypeEquals(nsAtom* aEventType) const { + return mEventName == aEventType; + } + + // if aCharCode is not zero, it is used instead of the charCode of + // aKeyEventHandler. + bool KeyEventMatched(dom::KeyboardEvent* aDomKeyboardEvent, + uint32_t aCharCode, + const IgnoreModifierState& aIgnoreModifierState); + + /** + * Check whether the handler element is disabled. Note that this requires + * a QI to getting GetHandlerELement(). Therefore, this should not be used + * first in multiple checks. + */ + bool KeyElementIsDisabled() const; + + already_AddRefed<dom::Element> GetHandlerElement() const; + + ReservedKey GetIsReserved() { return mReserved; } + + KeyEventHandler* GetNextHandler() { return mNextHandler; } + void SetNextHandler(KeyEventHandler* aHandler) { mNextHandler = aHandler; } + + MOZ_CAN_RUN_SCRIPT + nsresult ExecuteHandler(dom::EventTarget* aTarget, dom::Event* aEvent); + + size_t SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const; + + void GetCommand(nsAString& aCommand) const; + + public: + static uint32_t gRefCnt; + + protected: + void Init() { ++gRefCnt; } + + already_AddRefed<nsIController> GetController(dom::EventTarget* aTarget); + + inline int32_t GetMatchingKeyCode(const nsAString& aKeyName); + void ConstructPrototype(dom::Element* aKeyElement, + const char16_t* aEvent = nullptr, + const char16_t* aCommand = nullptr, + const char16_t* aKeyCode = nullptr, + const char16_t* aCharCode = nullptr, + const char16_t* aModifiers = nullptr); + void BuildModifiers(nsAString& aModifiers); + + void ReportKeyConflict(const char16_t* aKey, const char16_t* aModifiers, + dom::Element* aKeyElement, const char* aMessageName); + void GetEventType(nsAString& aEvent); + bool ModifiersMatchMask(dom::UIEvent* aEvent, + const IgnoreModifierState& aIgnoreModifierState); + MOZ_CAN_RUN_SCRIPT + nsresult DispatchXBLCommand(dom::EventTarget* aTarget, dom::Event* aEvent); + MOZ_CAN_RUN_SCRIPT + nsresult DispatchXULKeyCommand(dom::Event* aEvent); + + Modifiers GetModifiers() const; + Modifiers GetModifiersMask() const; + + static int32_t KeyToMask(uint32_t aKey); + static int32_t AccelKeyMask(); + + static const int32_t cShift; + static const int32_t cAlt; + static const int32_t cControl; + static const int32_t cMeta; + static const int32_t cOS; + + static const int32_t cShiftMask; + static const int32_t cAltMask; + static const int32_t cControlMask; + static const int32_t cMetaMask; + static const int32_t cOSMask; + + static const int32_t cAllModifiers; + + protected: + union { + nsIWeakReference* + mHandlerElement; // For XUL <key> element handlers. [STRONG] + char16_t* mCommand; // For built-in shortcuts the command to execute. + }; + + // The following four values make up 32 bits. + bool mIsXULKey; // This handler is either for a XUL <key> element or it is + // a command dispatcher. + uint8_t mMisc; // Miscellaneous extra information. For key events, + // stores whether or not we're a key code or char code. + // For mouse events, stores the clickCount. + + ReservedKey mReserved; // <key> is reserved for chrome. Not used by handlers. + + int32_t mKeyMask; // Which modifier keys this event handler expects to have + // down in order to be matched. + + // The primary filter information for mouse/key events. + int32_t mDetail; // For key events, contains a charcode or keycode. For + // mouse events, stores the button info. + + // Prototype handlers are chained. We own the next handler in the chain. + KeyEventHandler* mNextHandler; + RefPtr<nsAtom> mEventName; // The type of the event, e.g., "keypress" +}; + +} // namespace mozilla + +#endif diff --git a/dom/events/KeyNameList.h b/dom/events/KeyNameList.h new file mode 100644 index 0000000000..d35b99061f --- /dev/null +++ b/dom/events/KeyNameList.h @@ -0,0 +1,438 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +/** + * This header file defines all DOM key name which are used for DOM + * KeyboardEvent.key. + * You must define NS_DEFINE_KEYNAME macro before including this. + * + * It must have two arguments, (aCPPName, aDOMKeyName) + * aCPPName is usable name for a part of C++ constants. + * aDOMKeyName is the actual value. + */ + +#define DEFINE_KEYNAME_INTERNAL(aCPPName, aDOMKeyName) \ + NS_DEFINE_KEYNAME(aCPPName, aDOMKeyName) + +#define DEFINE_KEYNAME_WITH_SAME_NAME(aName) \ + DEFINE_KEYNAME_INTERNAL(aName, #aName) + +/****************************************************************************** + * Special Key Values + *****************************************************************************/ +DEFINE_KEYNAME_WITH_SAME_NAME(Unidentified) + +/****************************************************************************** + * Our Internal Key Values (must have "Moz" prefix) + *****************************************************************************/ +DEFINE_KEYNAME_INTERNAL(PrintableKey, "MozPrintableKey") +DEFINE_KEYNAME_INTERNAL(SoftLeft, "MozSoftLeft") +DEFINE_KEYNAME_INTERNAL(SoftRight, "MozSoftRight") + +/****************************************************************************** + * Modifier Keys + *****************************************************************************/ +DEFINE_KEYNAME_WITH_SAME_NAME(Alt) +DEFINE_KEYNAME_WITH_SAME_NAME(AltGraph) +DEFINE_KEYNAME_WITH_SAME_NAME(CapsLock) +DEFINE_KEYNAME_WITH_SAME_NAME(Control) +DEFINE_KEYNAME_WITH_SAME_NAME(Fn) +DEFINE_KEYNAME_WITH_SAME_NAME(FnLock) +DEFINE_KEYNAME_WITH_SAME_NAME(Hyper) +DEFINE_KEYNAME_WITH_SAME_NAME(Meta) +DEFINE_KEYNAME_WITH_SAME_NAME(NumLock) +DEFINE_KEYNAME_WITH_SAME_NAME(OS) // Dropped from the latest draft, bug 1232918 +DEFINE_KEYNAME_WITH_SAME_NAME(ScrollLock) +DEFINE_KEYNAME_WITH_SAME_NAME(Shift) +DEFINE_KEYNAME_WITH_SAME_NAME(Super) +DEFINE_KEYNAME_WITH_SAME_NAME(Symbol) +DEFINE_KEYNAME_WITH_SAME_NAME(SymbolLock) + +/****************************************************************************** + * Whitespace Keys + *****************************************************************************/ +DEFINE_KEYNAME_WITH_SAME_NAME(Enter) +DEFINE_KEYNAME_WITH_SAME_NAME(Tab) + +/****************************************************************************** + * Navigation Keys + *****************************************************************************/ +DEFINE_KEYNAME_WITH_SAME_NAME(ArrowDown) +DEFINE_KEYNAME_WITH_SAME_NAME(ArrowLeft) +DEFINE_KEYNAME_WITH_SAME_NAME(ArrowRight) +DEFINE_KEYNAME_WITH_SAME_NAME(ArrowUp) +DEFINE_KEYNAME_WITH_SAME_NAME(End) +DEFINE_KEYNAME_WITH_SAME_NAME(Home) +DEFINE_KEYNAME_WITH_SAME_NAME(PageDown) +DEFINE_KEYNAME_WITH_SAME_NAME(PageUp) + +/****************************************************************************** + * Editing Keys + *****************************************************************************/ +DEFINE_KEYNAME_WITH_SAME_NAME(Backspace) +DEFINE_KEYNAME_WITH_SAME_NAME(Clear) +DEFINE_KEYNAME_WITH_SAME_NAME(Copy) +DEFINE_KEYNAME_WITH_SAME_NAME(CrSel) +DEFINE_KEYNAME_WITH_SAME_NAME(Cut) +DEFINE_KEYNAME_WITH_SAME_NAME(Delete) +DEFINE_KEYNAME_WITH_SAME_NAME(EraseEof) +DEFINE_KEYNAME_WITH_SAME_NAME(ExSel) +DEFINE_KEYNAME_WITH_SAME_NAME(Insert) +DEFINE_KEYNAME_WITH_SAME_NAME(Paste) +DEFINE_KEYNAME_WITH_SAME_NAME(Redo) +DEFINE_KEYNAME_WITH_SAME_NAME(Undo) + +/****************************************************************************** + * UI Keys + *****************************************************************************/ +DEFINE_KEYNAME_WITH_SAME_NAME(Accept) +DEFINE_KEYNAME_WITH_SAME_NAME(Again) +DEFINE_KEYNAME_WITH_SAME_NAME(Attn) +DEFINE_KEYNAME_WITH_SAME_NAME(Cancel) +DEFINE_KEYNAME_WITH_SAME_NAME(ContextMenu) +DEFINE_KEYNAME_WITH_SAME_NAME(Escape) +DEFINE_KEYNAME_WITH_SAME_NAME(Execute) +DEFINE_KEYNAME_WITH_SAME_NAME(Find) +DEFINE_KEYNAME_WITH_SAME_NAME(Help) +DEFINE_KEYNAME_WITH_SAME_NAME(Pause) +DEFINE_KEYNAME_WITH_SAME_NAME(Play) +DEFINE_KEYNAME_WITH_SAME_NAME(Props) +DEFINE_KEYNAME_WITH_SAME_NAME(Select) +DEFINE_KEYNAME_WITH_SAME_NAME(ZoomIn) +DEFINE_KEYNAME_WITH_SAME_NAME(ZoomOut) + +/****************************************************************************** + * Device Keys + *****************************************************************************/ +DEFINE_KEYNAME_WITH_SAME_NAME(BrightnessDown) +DEFINE_KEYNAME_WITH_SAME_NAME(BrightnessUp) +DEFINE_KEYNAME_WITH_SAME_NAME(Eject) +DEFINE_KEYNAME_WITH_SAME_NAME(LogOff) +DEFINE_KEYNAME_WITH_SAME_NAME(Power) +DEFINE_KEYNAME_WITH_SAME_NAME(PowerOff) +DEFINE_KEYNAME_WITH_SAME_NAME(PrintScreen) +DEFINE_KEYNAME_WITH_SAME_NAME(Hibernate) +DEFINE_KEYNAME_WITH_SAME_NAME(Standby) +DEFINE_KEYNAME_WITH_SAME_NAME(WakeUp) + +/****************************************************************************** + * IME and Composition Keys + *****************************************************************************/ +DEFINE_KEYNAME_WITH_SAME_NAME(AllCandidates) +DEFINE_KEYNAME_WITH_SAME_NAME(Alphanumeric) +DEFINE_KEYNAME_WITH_SAME_NAME(CodeInput) +DEFINE_KEYNAME_WITH_SAME_NAME(Compose) +DEFINE_KEYNAME_WITH_SAME_NAME(Convert) +DEFINE_KEYNAME_WITH_SAME_NAME(Dead) +DEFINE_KEYNAME_WITH_SAME_NAME(FinalMode) +DEFINE_KEYNAME_WITH_SAME_NAME(GroupFirst) +DEFINE_KEYNAME_WITH_SAME_NAME(GroupLast) +DEFINE_KEYNAME_WITH_SAME_NAME(GroupNext) +DEFINE_KEYNAME_WITH_SAME_NAME(GroupPrevious) +DEFINE_KEYNAME_WITH_SAME_NAME(ModeChange) +DEFINE_KEYNAME_WITH_SAME_NAME(NextCandidate) +DEFINE_KEYNAME_WITH_SAME_NAME(NonConvert) +DEFINE_KEYNAME_WITH_SAME_NAME(PreviousCandidate) +DEFINE_KEYNAME_WITH_SAME_NAME(Process) +DEFINE_KEYNAME_WITH_SAME_NAME(SingleCandidate) + +/****************************************************************************** + * Keys specific to Korean keyboards + *****************************************************************************/ +DEFINE_KEYNAME_WITH_SAME_NAME(HangulMode) +DEFINE_KEYNAME_WITH_SAME_NAME(HanjaMode) +DEFINE_KEYNAME_WITH_SAME_NAME(JunjaMode) + +/****************************************************************************** + * Keys specific to Japanese keyboards + *****************************************************************************/ +DEFINE_KEYNAME_WITH_SAME_NAME(Eisu) +DEFINE_KEYNAME_WITH_SAME_NAME(Hankaku) +DEFINE_KEYNAME_WITH_SAME_NAME(Hiragana) +DEFINE_KEYNAME_WITH_SAME_NAME(HiraganaKatakana) +DEFINE_KEYNAME_WITH_SAME_NAME(KanaMode) +DEFINE_KEYNAME_WITH_SAME_NAME(KanjiMode) +DEFINE_KEYNAME_WITH_SAME_NAME(Katakana) +DEFINE_KEYNAME_WITH_SAME_NAME(Romaji) +DEFINE_KEYNAME_WITH_SAME_NAME(Zenkaku) +DEFINE_KEYNAME_WITH_SAME_NAME(ZenkakuHankaku) + +/****************************************************************************** + * General-Purpose Function Keys + *****************************************************************************/ +DEFINE_KEYNAME_WITH_SAME_NAME(F1) +DEFINE_KEYNAME_WITH_SAME_NAME(F2) +DEFINE_KEYNAME_WITH_SAME_NAME(F3) +DEFINE_KEYNAME_WITH_SAME_NAME(F4) +DEFINE_KEYNAME_WITH_SAME_NAME(F5) +DEFINE_KEYNAME_WITH_SAME_NAME(F6) +DEFINE_KEYNAME_WITH_SAME_NAME(F7) +DEFINE_KEYNAME_WITH_SAME_NAME(F8) +DEFINE_KEYNAME_WITH_SAME_NAME(F9) +DEFINE_KEYNAME_WITH_SAME_NAME(F10) +DEFINE_KEYNAME_WITH_SAME_NAME(F11) +DEFINE_KEYNAME_WITH_SAME_NAME(F12) +DEFINE_KEYNAME_WITH_SAME_NAME(F13) +DEFINE_KEYNAME_WITH_SAME_NAME(F14) +DEFINE_KEYNAME_WITH_SAME_NAME(F15) +DEFINE_KEYNAME_WITH_SAME_NAME(F16) +DEFINE_KEYNAME_WITH_SAME_NAME(F17) +DEFINE_KEYNAME_WITH_SAME_NAME(F18) +DEFINE_KEYNAME_WITH_SAME_NAME(F19) +DEFINE_KEYNAME_WITH_SAME_NAME(F20) +DEFINE_KEYNAME_WITH_SAME_NAME(F21) +DEFINE_KEYNAME_WITH_SAME_NAME(F22) +DEFINE_KEYNAME_WITH_SAME_NAME(F23) +DEFINE_KEYNAME_WITH_SAME_NAME(F24) +DEFINE_KEYNAME_WITH_SAME_NAME(F25) +DEFINE_KEYNAME_WITH_SAME_NAME(F26) +DEFINE_KEYNAME_WITH_SAME_NAME(F27) +DEFINE_KEYNAME_WITH_SAME_NAME(F28) +DEFINE_KEYNAME_WITH_SAME_NAME(F29) +DEFINE_KEYNAME_WITH_SAME_NAME(F30) +DEFINE_KEYNAME_WITH_SAME_NAME(F31) +DEFINE_KEYNAME_WITH_SAME_NAME(F32) +DEFINE_KEYNAME_WITH_SAME_NAME(F33) +DEFINE_KEYNAME_WITH_SAME_NAME(F34) +DEFINE_KEYNAME_WITH_SAME_NAME(F35) +DEFINE_KEYNAME_WITH_SAME_NAME(Soft1) +DEFINE_KEYNAME_WITH_SAME_NAME(Soft2) +DEFINE_KEYNAME_WITH_SAME_NAME(Soft3) +DEFINE_KEYNAME_WITH_SAME_NAME(Soft4) + +/****************************************************************************** + * Multimedia Keys + *****************************************************************************/ +DEFINE_KEYNAME_WITH_SAME_NAME(ChannelDown) +DEFINE_KEYNAME_WITH_SAME_NAME(ChannelUp) +DEFINE_KEYNAME_WITH_SAME_NAME(Close) +DEFINE_KEYNAME_WITH_SAME_NAME(MailForward) +DEFINE_KEYNAME_WITH_SAME_NAME(MailReply) +DEFINE_KEYNAME_WITH_SAME_NAME(MailSend) +DEFINE_KEYNAME_WITH_SAME_NAME(MediaFastForward) +DEFINE_KEYNAME_WITH_SAME_NAME(MediaPause) +DEFINE_KEYNAME_WITH_SAME_NAME(MediaPlay) +DEFINE_KEYNAME_WITH_SAME_NAME(MediaPlayPause) +DEFINE_KEYNAME_WITH_SAME_NAME(MediaRecord) +DEFINE_KEYNAME_WITH_SAME_NAME(MediaRewind) +DEFINE_KEYNAME_WITH_SAME_NAME(MediaStop) +DEFINE_KEYNAME_WITH_SAME_NAME(MediaTrackNext) +DEFINE_KEYNAME_WITH_SAME_NAME(MediaTrackPrevious) +DEFINE_KEYNAME_WITH_SAME_NAME(New) +DEFINE_KEYNAME_WITH_SAME_NAME(Open) +DEFINE_KEYNAME_WITH_SAME_NAME(Print) +DEFINE_KEYNAME_WITH_SAME_NAME(Save) +DEFINE_KEYNAME_WITH_SAME_NAME(SpellCheck) + +/****************************************************************************** + * Multimedia Numpad Keys + *****************************************************************************/ +DEFINE_KEYNAME_WITH_SAME_NAME(Key11) +DEFINE_KEYNAME_WITH_SAME_NAME(Key12) + +/****************************************************************************** + * Audio Keys + *****************************************************************************/ +DEFINE_KEYNAME_WITH_SAME_NAME(AudioBalanceLeft) +DEFINE_KEYNAME_WITH_SAME_NAME(AudioBalanceRight) +DEFINE_KEYNAME_WITH_SAME_NAME(AudioBassBoostDown) +DEFINE_KEYNAME_WITH_SAME_NAME(AudioBassBoostToggle) +DEFINE_KEYNAME_WITH_SAME_NAME(AudioBassBoostUp) +DEFINE_KEYNAME_WITH_SAME_NAME(AudioFaderFront) +DEFINE_KEYNAME_WITH_SAME_NAME(AudioFaderRear) +DEFINE_KEYNAME_WITH_SAME_NAME(AudioSurroundModeNext) +DEFINE_KEYNAME_WITH_SAME_NAME(AudioTrebleDown) +DEFINE_KEYNAME_WITH_SAME_NAME(AudioTrebleUp) +DEFINE_KEYNAME_WITH_SAME_NAME(AudioVolumeDown) +DEFINE_KEYNAME_WITH_SAME_NAME(AudioVolumeUp) +DEFINE_KEYNAME_WITH_SAME_NAME(AudioVolumeMute) + +DEFINE_KEYNAME_WITH_SAME_NAME(MicrophoneToggle) +DEFINE_KEYNAME_WITH_SAME_NAME(MicrophoneVolumeDown) +DEFINE_KEYNAME_WITH_SAME_NAME(MicrophoneVolumeUp) +DEFINE_KEYNAME_WITH_SAME_NAME(MicrophoneVolumeMute) + +/****************************************************************************** + * Speech Keys + *****************************************************************************/ +DEFINE_KEYNAME_WITH_SAME_NAME(SpeechCorrectionList) +DEFINE_KEYNAME_WITH_SAME_NAME(SpeechInputToggle) + +/****************************************************************************** + * Application Keys + *****************************************************************************/ +DEFINE_KEYNAME_WITH_SAME_NAME(LaunchCalculator) +DEFINE_KEYNAME_WITH_SAME_NAME(LaunchCalendar) +DEFINE_KEYNAME_WITH_SAME_NAME(LaunchContacts) +DEFINE_KEYNAME_WITH_SAME_NAME(LaunchMail) +DEFINE_KEYNAME_WITH_SAME_NAME(LaunchMediaPlayer) +DEFINE_KEYNAME_WITH_SAME_NAME(LaunchMusicPlayer) +DEFINE_KEYNAME_WITH_SAME_NAME(LaunchMyComputer) +DEFINE_KEYNAME_WITH_SAME_NAME(LaunchPhone) +DEFINE_KEYNAME_WITH_SAME_NAME(LaunchScreenSaver) +DEFINE_KEYNAME_WITH_SAME_NAME(LaunchSpreadsheet) +DEFINE_KEYNAME_WITH_SAME_NAME(LaunchWebBrowser) +DEFINE_KEYNAME_WITH_SAME_NAME(LaunchWebCam) +DEFINE_KEYNAME_WITH_SAME_NAME(LaunchWordProcessor) + +DEFINE_KEYNAME_WITH_SAME_NAME(LaunchApplication1) +DEFINE_KEYNAME_WITH_SAME_NAME(LaunchApplication2) +DEFINE_KEYNAME_WITH_SAME_NAME(LaunchApplication3) +DEFINE_KEYNAME_WITH_SAME_NAME(LaunchApplication4) +DEFINE_KEYNAME_WITH_SAME_NAME(LaunchApplication5) +DEFINE_KEYNAME_WITH_SAME_NAME(LaunchApplication6) +DEFINE_KEYNAME_WITH_SAME_NAME(LaunchApplication7) +DEFINE_KEYNAME_WITH_SAME_NAME(LaunchApplication8) +DEFINE_KEYNAME_WITH_SAME_NAME(LaunchApplication9) +DEFINE_KEYNAME_WITH_SAME_NAME(LaunchApplication10) +DEFINE_KEYNAME_WITH_SAME_NAME(LaunchApplication11) +DEFINE_KEYNAME_WITH_SAME_NAME(LaunchApplication12) +DEFINE_KEYNAME_WITH_SAME_NAME(LaunchApplication13) +DEFINE_KEYNAME_WITH_SAME_NAME(LaunchApplication14) +DEFINE_KEYNAME_WITH_SAME_NAME(LaunchApplication15) +DEFINE_KEYNAME_WITH_SAME_NAME(LaunchApplication16) +DEFINE_KEYNAME_WITH_SAME_NAME(LaunchApplication17) +DEFINE_KEYNAME_WITH_SAME_NAME(LaunchApplication18) + +/****************************************************************************** + * Browser Keys + *****************************************************************************/ +DEFINE_KEYNAME_WITH_SAME_NAME(BrowserBack) +DEFINE_KEYNAME_WITH_SAME_NAME(BrowserFavorites) +DEFINE_KEYNAME_WITH_SAME_NAME(BrowserForward) +DEFINE_KEYNAME_WITH_SAME_NAME(BrowserHome) +DEFINE_KEYNAME_WITH_SAME_NAME(BrowserRefresh) +DEFINE_KEYNAME_WITH_SAME_NAME(BrowserSearch) +DEFINE_KEYNAME_WITH_SAME_NAME(BrowserStop) + +/****************************************************************************** + * Mobile Phone Keys + *****************************************************************************/ +DEFINE_KEYNAME_WITH_SAME_NAME(AppSwitch) +DEFINE_KEYNAME_WITH_SAME_NAME(Call) +DEFINE_KEYNAME_WITH_SAME_NAME(Camera) +DEFINE_KEYNAME_WITH_SAME_NAME(CameraFocus) +DEFINE_KEYNAME_WITH_SAME_NAME(EndCall) +DEFINE_KEYNAME_WITH_SAME_NAME(GoBack) +DEFINE_KEYNAME_WITH_SAME_NAME(GoHome) +DEFINE_KEYNAME_WITH_SAME_NAME(HeadsetHook) +DEFINE_KEYNAME_WITH_SAME_NAME(LastNumberRedial) +DEFINE_KEYNAME_WITH_SAME_NAME(Notification) +DEFINE_KEYNAME_WITH_SAME_NAME(MannerMode) +DEFINE_KEYNAME_WITH_SAME_NAME(VoiceDial) + +/****************************************************************************** + * TV Keys + *****************************************************************************/ +DEFINE_KEYNAME_WITH_SAME_NAME(TV) +DEFINE_KEYNAME_WITH_SAME_NAME(TV3DMode) +DEFINE_KEYNAME_WITH_SAME_NAME(TVAntennaCable) +DEFINE_KEYNAME_WITH_SAME_NAME(TVAudioDescription) +DEFINE_KEYNAME_WITH_SAME_NAME(TVAudioDescriptionMixDown) +DEFINE_KEYNAME_WITH_SAME_NAME(TVAudioDescriptionMixUp) +DEFINE_KEYNAME_WITH_SAME_NAME(TVContentsMenu) +DEFINE_KEYNAME_WITH_SAME_NAME(TVDataService) +DEFINE_KEYNAME_WITH_SAME_NAME(TVInput) +DEFINE_KEYNAME_WITH_SAME_NAME(TVInputComponent1) +DEFINE_KEYNAME_WITH_SAME_NAME(TVInputComponent2) +DEFINE_KEYNAME_WITH_SAME_NAME(TVInputComposite1) +DEFINE_KEYNAME_WITH_SAME_NAME(TVInputComposite2) +DEFINE_KEYNAME_WITH_SAME_NAME(TVInputHDMI1) +DEFINE_KEYNAME_WITH_SAME_NAME(TVInputHDMI2) +DEFINE_KEYNAME_WITH_SAME_NAME(TVInputHDMI3) +DEFINE_KEYNAME_WITH_SAME_NAME(TVInputHDMI4) +DEFINE_KEYNAME_WITH_SAME_NAME(TVInputVGA1) +DEFINE_KEYNAME_WITH_SAME_NAME(TVMediaContext) +DEFINE_KEYNAME_WITH_SAME_NAME(TVNetwork) +DEFINE_KEYNAME_WITH_SAME_NAME(TVNumberEntry) +DEFINE_KEYNAME_WITH_SAME_NAME(TVPower) +DEFINE_KEYNAME_WITH_SAME_NAME(TVRadioService) +DEFINE_KEYNAME_WITH_SAME_NAME(TVSatellite) +DEFINE_KEYNAME_WITH_SAME_NAME(TVSatelliteBS) +DEFINE_KEYNAME_WITH_SAME_NAME(TVSatelliteCS) +DEFINE_KEYNAME_WITH_SAME_NAME(TVSatelliteToggle) +DEFINE_KEYNAME_WITH_SAME_NAME(TVTerrestrialAnalog) +DEFINE_KEYNAME_WITH_SAME_NAME(TVTerrestrialDigital) +DEFINE_KEYNAME_WITH_SAME_NAME(TVTimer) + +/****************************************************************************** + * Media Controller Keys + *****************************************************************************/ +DEFINE_KEYNAME_WITH_SAME_NAME(AVRInput) +DEFINE_KEYNAME_WITH_SAME_NAME(AVRPower) +DEFINE_KEYNAME_WITH_SAME_NAME(ColorF0Red) +DEFINE_KEYNAME_WITH_SAME_NAME(ColorF1Green) +DEFINE_KEYNAME_WITH_SAME_NAME(ColorF2Yellow) +DEFINE_KEYNAME_WITH_SAME_NAME(ColorF3Blue) +DEFINE_KEYNAME_WITH_SAME_NAME(ColorF4Grey) +DEFINE_KEYNAME_WITH_SAME_NAME(ColorF5Brown) +DEFINE_KEYNAME_WITH_SAME_NAME(ClosedCaptionToggle) +DEFINE_KEYNAME_WITH_SAME_NAME(Dimmer) +DEFINE_KEYNAME_WITH_SAME_NAME(DisplaySwap) +DEFINE_KEYNAME_WITH_SAME_NAME(DVR) +DEFINE_KEYNAME_WITH_SAME_NAME(Exit) +DEFINE_KEYNAME_WITH_SAME_NAME(FavoriteClear0) +DEFINE_KEYNAME_WITH_SAME_NAME(FavoriteClear1) +DEFINE_KEYNAME_WITH_SAME_NAME(FavoriteClear2) +DEFINE_KEYNAME_WITH_SAME_NAME(FavoriteClear3) +DEFINE_KEYNAME_WITH_SAME_NAME(FavoriteRecall0) +DEFINE_KEYNAME_WITH_SAME_NAME(FavoriteRecall1) +DEFINE_KEYNAME_WITH_SAME_NAME(FavoriteRecall2) +DEFINE_KEYNAME_WITH_SAME_NAME(FavoriteRecall3) +DEFINE_KEYNAME_WITH_SAME_NAME(FavoriteStore0) +DEFINE_KEYNAME_WITH_SAME_NAME(FavoriteStore1) +DEFINE_KEYNAME_WITH_SAME_NAME(FavoriteStore2) +DEFINE_KEYNAME_WITH_SAME_NAME(FavoriteStore3) +DEFINE_KEYNAME_WITH_SAME_NAME(Guide) +DEFINE_KEYNAME_WITH_SAME_NAME(GuideNextDay) +DEFINE_KEYNAME_WITH_SAME_NAME(GuidePreviousDay) +DEFINE_KEYNAME_WITH_SAME_NAME(Info) +DEFINE_KEYNAME_WITH_SAME_NAME(InstantReplay) +DEFINE_KEYNAME_WITH_SAME_NAME(Link) +DEFINE_KEYNAME_WITH_SAME_NAME(ListProgram) +DEFINE_KEYNAME_WITH_SAME_NAME(LiveContent) +DEFINE_KEYNAME_WITH_SAME_NAME(Lock) +DEFINE_KEYNAME_WITH_SAME_NAME(MediaApps) +DEFINE_KEYNAME_WITH_SAME_NAME(MediaAudioTrack) +DEFINE_KEYNAME_WITH_SAME_NAME(MediaLast) +DEFINE_KEYNAME_WITH_SAME_NAME(MediaSkipBackward) +DEFINE_KEYNAME_WITH_SAME_NAME(MediaSkipForward) +DEFINE_KEYNAME_WITH_SAME_NAME(MediaStepBackward) +DEFINE_KEYNAME_WITH_SAME_NAME(MediaStepForward) +DEFINE_KEYNAME_WITH_SAME_NAME(MediaTopMenu) +DEFINE_KEYNAME_WITH_SAME_NAME(NavigateIn) +DEFINE_KEYNAME_WITH_SAME_NAME(NavigateNext) +DEFINE_KEYNAME_WITH_SAME_NAME(NavigateOut) +DEFINE_KEYNAME_WITH_SAME_NAME(NavigatePrevious) +DEFINE_KEYNAME_WITH_SAME_NAME(NextFavoriteChannel) +DEFINE_KEYNAME_WITH_SAME_NAME(NextUserProfile) +DEFINE_KEYNAME_WITH_SAME_NAME(OnDemand) +DEFINE_KEYNAME_WITH_SAME_NAME(Pairing) +DEFINE_KEYNAME_WITH_SAME_NAME(PinPDown) +DEFINE_KEYNAME_WITH_SAME_NAME(PinPMove) +DEFINE_KEYNAME_WITH_SAME_NAME(PinPToggle) +DEFINE_KEYNAME_WITH_SAME_NAME(PinPUp) +DEFINE_KEYNAME_WITH_SAME_NAME(PlaySpeedDown) +DEFINE_KEYNAME_WITH_SAME_NAME(PlaySpeedReset) +DEFINE_KEYNAME_WITH_SAME_NAME(PlaySpeedUp) +DEFINE_KEYNAME_WITH_SAME_NAME(RandomToggle) +DEFINE_KEYNAME_WITH_SAME_NAME(RcLowBattery) +DEFINE_KEYNAME_WITH_SAME_NAME(RecordSpeedNext) +DEFINE_KEYNAME_WITH_SAME_NAME(RfBypass) +DEFINE_KEYNAME_WITH_SAME_NAME(ScanChannelsToggle) +DEFINE_KEYNAME_WITH_SAME_NAME(ScreenModeNext) +DEFINE_KEYNAME_WITH_SAME_NAME(Settings) +DEFINE_KEYNAME_WITH_SAME_NAME(SplitScreenToggle) +DEFINE_KEYNAME_WITH_SAME_NAME(STBInput) +DEFINE_KEYNAME_WITH_SAME_NAME(STBPower) +DEFINE_KEYNAME_WITH_SAME_NAME(Subtitle) +DEFINE_KEYNAME_WITH_SAME_NAME(Teletext) +DEFINE_KEYNAME_WITH_SAME_NAME(VideoModeNext) +DEFINE_KEYNAME_WITH_SAME_NAME(Wink) +DEFINE_KEYNAME_WITH_SAME_NAME(ZoomToggle) + +#undef DEFINE_KEYNAME_WITH_SAME_NAME +#undef DEFINE_KEYNAME_INTERNAL diff --git a/dom/events/KeyboardEvent.cpp b/dom/events/KeyboardEvent.cpp new file mode 100644 index 0000000000..e186f71451 --- /dev/null +++ b/dom/events/KeyboardEvent.cpp @@ -0,0 +1,424 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#include "mozilla/dom/KeyboardEvent.h" + +#include "mozilla/BasicEvents.h" +#include "mozilla/StaticPrefs_dom.h" +#include "mozilla/TextEvents.h" +#include "mozilla/dom/Document.h" +#include "mozilla/LookAndFeel.h" +#include "nsContentUtils.h" +#include "nsIPrincipal.h" +#include "nsRFPService.h" +#include "prtime.h" + +namespace mozilla::dom { + +KeyboardEvent::KeyboardEvent(EventTarget* aOwner, nsPresContext* aPresContext, + WidgetKeyboardEvent* aEvent) + : UIEvent(aOwner, aPresContext, + aEvent ? aEvent + : new WidgetKeyboardEvent(false, eVoidEvent, nullptr)), + mInitializedByJS(false), + mInitializedByCtor(false), + mInitializedWhichValue(0) { + if (aEvent) { + mEventIsInternal = false; + } else { + mEventIsInternal = true; + mEvent->AsKeyboardEvent()->mKeyNameIndex = KEY_NAME_INDEX_USE_STRING; + } +} + +bool KeyboardEvent::IsMenuAccessKeyPressed() const { + Modifiers mask = LookAndFeel::GetMenuAccessKeyModifiers(); + Modifiers modifiers = GetModifiersForMenuAccessKey(); + return mask != MODIFIER_SHIFT && (modifiers & mask) && + (modifiers & ~(mask | MODIFIER_SHIFT)) == 0; +} + +static constexpr Modifiers kPossibleModifiersForAccessKey = + MODIFIER_SHIFT | MODIFIER_CONTROL | MODIFIER_ALT | MODIFIER_META | + MODIFIER_OS; + +Modifiers KeyboardEvent::GetModifiersForMenuAccessKey() const { + const WidgetInputEvent* inputEvent = WidgetEventPtr()->AsInputEvent(); + MOZ_ASSERT(inputEvent); + return inputEvent->mModifiers & kPossibleModifiersForAccessKey; +} + +bool KeyboardEvent::AltKey(CallerType aCallerType) { + bool altState = mEvent->AsKeyboardEvent()->IsAlt(); + + if (!ShouldResistFingerprinting(aCallerType)) { + return altState; + } + + // We need to give a spoofed state for Alt key since it could be used as a + // modifier key in certain keyboard layout. For example, the '@' key for + // German keyboard for MAC is Alt+L. + return GetSpoofedModifierStates(Modifier::MODIFIER_ALT, altState); +} + +bool KeyboardEvent::CtrlKey(CallerType aCallerType) { + // We don't spoof this key when privacy.resistFingerprinting + // is enabled, because it is often used for command key + // combinations in web apps. + return mEvent->AsKeyboardEvent()->IsControl(); +} + +bool KeyboardEvent::ShiftKey(CallerType aCallerType) { + bool shiftState = mEvent->AsKeyboardEvent()->IsShift(); + + if (!ShouldResistFingerprinting(aCallerType)) { + return shiftState; + } + + return GetSpoofedModifierStates(Modifier::MODIFIER_SHIFT, shiftState); +} + +bool KeyboardEvent::MetaKey() { + // We don't spoof this key when privacy.resistFingerprinting + // is enabled, because it is often used for command key + // combinations in web apps. + return mEvent->AsKeyboardEvent()->IsMeta(); +} + +bool KeyboardEvent::Repeat() { return mEvent->AsKeyboardEvent()->mIsRepeat; } + +bool KeyboardEvent::IsComposing() { + return mEvent->AsKeyboardEvent()->mIsComposing; +} + +void KeyboardEvent::GetKey(nsAString& aKeyName) const { + mEvent->AsKeyboardEvent()->GetDOMKeyName(aKeyName); +} + +void KeyboardEvent::GetCode(nsAString& aCodeName, CallerType aCallerType) { + if (!ShouldResistFingerprinting(aCallerType)) { + mEvent->AsKeyboardEvent()->GetDOMCodeName(aCodeName); + return; + } + + // When fingerprinting resistance is enabled, we will give a spoofed code + // according to the content-language of the document. + nsCOMPtr<Document> doc = GetDocument(); + + nsRFPService::GetSpoofedCode(doc, mEvent->AsKeyboardEvent(), aCodeName); +} + +void KeyboardEvent::GetInitDict(KeyboardEventInit& aParam) { + GetKey(aParam.mKey); + GetCode(aParam.mCode); + aParam.mLocation = Location(); + aParam.mRepeat = Repeat(); + aParam.mIsComposing = IsComposing(); + + // legacy attributes + aParam.mKeyCode = KeyCode(); + aParam.mCharCode = CharCode(); + aParam.mWhich = Which(); + + // modifiers from EventModifierInit + aParam.mCtrlKey = CtrlKey(); + aParam.mShiftKey = ShiftKey(); + aParam.mAltKey = AltKey(); + aParam.mMetaKey = MetaKey(); + + WidgetKeyboardEvent* internalEvent = mEvent->AsKeyboardEvent(); + aParam.mModifierAltGraph = internalEvent->IsAltGraph(); + aParam.mModifierCapsLock = internalEvent->IsCapsLocked(); + aParam.mModifierFn = internalEvent->IsFn(); + aParam.mModifierFnLock = internalEvent->IsFnLocked(); + aParam.mModifierNumLock = internalEvent->IsNumLocked(); + aParam.mModifierOS = internalEvent->IsOS(); + aParam.mModifierScrollLock = internalEvent->IsScrollLocked(); + aParam.mModifierSymbol = internalEvent->IsSymbol(); + aParam.mModifierSymbolLock = internalEvent->IsSymbolLocked(); + + // EventInit + aParam.mBubbles = internalEvent->mFlags.mBubbles; + aParam.mCancelable = internalEvent->mFlags.mCancelable; +} + +bool KeyboardEvent::ShouldUseSameValueForCharCodeAndKeyCode( + const WidgetKeyboardEvent& aWidgetKeyboardEvent, + CallerType aCallerType) const { + // - If this event is initialized by JS, we don't need to return same value + // for keyCode and charCode since they can be initialized separately. + // - If this is not a keypress event, we shouldn't return same value for + // keyCode and charCode. + // - If we need to return legacy keyCode and charCode values for the web + // app due to in the blacklist. + // - If this event is referred by default handler, i.e., the caller is + // system or this event is now in the system group, we don't need to use + // hack for web-compat. + if (mInitializedByJS || aWidgetKeyboardEvent.mMessage != eKeyPress || + aWidgetKeyboardEvent.mUseLegacyKeyCodeAndCharCodeValues || + aCallerType == CallerType::System || + aWidgetKeyboardEvent.mFlags.mInSystemGroup) { + return false; + } + + MOZ_ASSERT(aCallerType == CallerType::NonSystem); + + return StaticPrefs:: + dom_keyboardevent_keypress_set_keycode_and_charcode_to_same_value(); +} + +uint32_t KeyboardEvent::CharCode(CallerType aCallerType) { + WidgetKeyboardEvent* widgetKeyboardEvent = mEvent->AsKeyboardEvent(); + if (mInitializedByJS) { + // If this is initialized by Ctor, we should return the initialized value. + if (mInitializedByCtor) { + return widgetKeyboardEvent->mCharCode; + } + // Otherwise, i.e., initialized by InitKey*Event(), we should return the + // initialized value only when eKeyPress or eAccessKeyNotFound event. + // Although this is odd, but our traditional behavior. + return widgetKeyboardEvent->mMessage == eKeyPress || + widgetKeyboardEvent->mMessage == eAccessKeyNotFound + ? widgetKeyboardEvent->mCharCode + : 0; + } + + // If the key is a function key, we should return the result of KeyCode() + // even from CharCode(). Otherwise, i.e., the key may be a printable + // key or actually a printable key, we should return the given charCode + // value. + + if (widgetKeyboardEvent->mKeyNameIndex != KEY_NAME_INDEX_USE_STRING && + ShouldUseSameValueForCharCodeAndKeyCode(*widgetKeyboardEvent, + aCallerType)) { + return ComputeTraditionalKeyCode(*widgetKeyboardEvent, aCallerType); + } + + return widgetKeyboardEvent->mCharCode; +} + +uint32_t KeyboardEvent::KeyCode(CallerType aCallerType) { + WidgetKeyboardEvent* widgetKeyboardEvent = mEvent->AsKeyboardEvent(); + if (mInitializedByJS) { + // If this is initialized by Ctor, we should return the initialized value. + if (mInitializedByCtor) { + return widgetKeyboardEvent->mKeyCode; + } + // Otherwise, i.e., initialized by InitKey*Event(), we should return the + // initialized value only when the event message is a valid keyboard event + // message. Although this is odd, but our traditional behavior. + // NOTE: The fix of bug 1222285 changed the behavior temporarily if + // spoofing is enabled. However, the behavior does not make sense + // since if the event is generated by JS, the behavior shouldn't + // be changed by whether spoofing is enabled or not. Therefore, + // we take back the original behavior. + return widgetKeyboardEvent->HasKeyEventMessage() + ? widgetKeyboardEvent->mKeyCode + : 0; + } + + // If the key is not a function key, i.e., the key may be a printable key + // or a function key mapped as a printable key, we should use charCode value + // for keyCode value if this is a "keypress" event. + + if (widgetKeyboardEvent->mKeyNameIndex == KEY_NAME_INDEX_USE_STRING && + ShouldUseSameValueForCharCodeAndKeyCode(*widgetKeyboardEvent, + aCallerType)) { + return widgetKeyboardEvent->mCharCode; + } + + return ComputeTraditionalKeyCode(*widgetKeyboardEvent, aCallerType); +} + +uint32_t KeyboardEvent::ComputeTraditionalKeyCode( + WidgetKeyboardEvent& aKeyboardEvent, CallerType aCallerType) { + if (!ShouldResistFingerprinting(aCallerType)) { + return aKeyboardEvent.mKeyCode; + } + + // In Netscape style (i.e., traditional behavior of Gecko), the keyCode + // should be zero if the char code is given. + if ((mEvent->mMessage == eKeyPress || + mEvent->mMessage == eAccessKeyNotFound) && + aKeyboardEvent.mCharCode) { + return 0; + } + + // When fingerprinting resistance is enabled, we will give a spoofed keyCode + // according to the content-language of the document. + nsCOMPtr<Document> doc = GetDocument(); + uint32_t spoofedKeyCode; + + if (nsRFPService::GetSpoofedKeyCode(doc, &aKeyboardEvent, spoofedKeyCode)) { + return spoofedKeyCode; + } + + return 0; +} + +uint32_t KeyboardEvent::Which(CallerType aCallerType) { + // If this event is initialized with ctor, which can have independent value. + if (mInitializedByCtor) { + return mInitializedWhichValue; + } + + switch (mEvent->mMessage) { + case eKeyDown: + case eKeyUp: + return KeyCode(aCallerType); + case eKeyPress: + // Special case for 4xp bug 62878. Try to make value of which + // more closely mirror the values that 4.x gave for RETURN and BACKSPACE + { + uint32_t keyCode = mEvent->AsKeyboardEvent()->mKeyCode; + if (keyCode == NS_VK_RETURN || keyCode == NS_VK_BACK) { + return keyCode; + } + return CharCode(); + } + default: + break; + } + + return 0; +} + +uint32_t KeyboardEvent::Location() { + return mEvent->AsKeyboardEvent()->mLocation; +} + +// static +already_AddRefed<KeyboardEvent> KeyboardEvent::ConstructorJS( + const GlobalObject& aGlobal, const nsAString& aType, + const KeyboardEventInit& aParam) { + nsCOMPtr<EventTarget> target = do_QueryInterface(aGlobal.GetAsSupports()); + RefPtr<KeyboardEvent> newEvent = new KeyboardEvent(target, nullptr, nullptr); + newEvent->InitWithKeyboardEventInit(target, aType, aParam); + + return newEvent.forget(); +} + +void KeyboardEvent::InitWithKeyboardEventInit(EventTarget* aOwner, + const nsAString& aType, + const KeyboardEventInit& aParam) { + bool trusted = Init(aOwner); + InitKeyEventJS(aType, aParam.mBubbles, aParam.mCancelable, aParam.mView, + false, false, false, false, aParam.mKeyCode, aParam.mCharCode); + InitModifiers(aParam); + SetTrusted(trusted); + mDetail = aParam.mDetail; + mInitializedByJS = true; + mInitializedByCtor = true; + mInitializedWhichValue = aParam.mWhich; + + WidgetKeyboardEvent* internalEvent = mEvent->AsKeyboardEvent(); + internalEvent->mLocation = aParam.mLocation; + internalEvent->mIsRepeat = aParam.mRepeat; + internalEvent->mIsComposing = aParam.mIsComposing; + internalEvent->mKeyNameIndex = + WidgetKeyboardEvent::GetKeyNameIndex(aParam.mKey); + if (internalEvent->mKeyNameIndex == KEY_NAME_INDEX_USE_STRING) { + internalEvent->mKeyValue = aParam.mKey; + } + internalEvent->mCodeNameIndex = + WidgetKeyboardEvent::GetCodeNameIndex(aParam.mCode); + if (internalEvent->mCodeNameIndex == CODE_NAME_INDEX_USE_STRING) { + internalEvent->mCodeValue = aParam.mCode; + } +} + +// static +bool KeyboardEvent::IsInitKeyEventAvailable(JSContext* aCx, JSObject*) { + if (StaticPrefs::dom_keyboardevent_init_key_event_enabled()) { + return true; + } + if (!StaticPrefs::dom_keyboardevent_init_key_event_enabled_in_addons()) { + return false; + } + nsIPrincipal* principal = nsContentUtils::SubjectPrincipal(aCx); + return principal && principal->GetIsAddonOrExpandedAddonPrincipal(); +} + +void KeyboardEvent::InitKeyEventJS(const nsAString& aType, bool aCanBubble, + bool aCancelable, nsGlobalWindowInner* aView, + bool aCtrlKey, bool aAltKey, bool aShiftKey, + bool aMetaKey, uint32_t aKeyCode, + uint32_t aCharCode) { + NS_ENSURE_TRUE_VOID(!mEvent->mFlags.mIsBeingDispatched); + mInitializedByJS = true; + mInitializedByCtor = false; + + UIEvent::InitUIEvent(aType, aCanBubble, aCancelable, aView, 0); + + WidgetKeyboardEvent* keyEvent = mEvent->AsKeyboardEvent(); + keyEvent->InitBasicModifiers(aCtrlKey, aAltKey, aShiftKey, aMetaKey); + keyEvent->mKeyCode = aKeyCode; + keyEvent->mCharCode = aCharCode; +} + +void KeyboardEvent::InitKeyboardEventJS( + const nsAString& aType, bool aCanBubble, bool aCancelable, + nsGlobalWindowInner* aView, const nsAString& aKey, uint32_t aLocation, + bool aCtrlKey, bool aAltKey, bool aShiftKey, bool aMetaKey) { + NS_ENSURE_TRUE_VOID(!mEvent->mFlags.mIsBeingDispatched); + mInitializedByJS = true; + mInitializedByCtor = false; + + UIEvent::InitUIEvent(aType, aCanBubble, aCancelable, aView, 0); + + WidgetKeyboardEvent* keyEvent = mEvent->AsKeyboardEvent(); + keyEvent->InitBasicModifiers(aCtrlKey, aAltKey, aShiftKey, aMetaKey); + keyEvent->mLocation = aLocation; + keyEvent->mKeyNameIndex = KEY_NAME_INDEX_USE_STRING; + keyEvent->mKeyValue = aKey; +} + +bool KeyboardEvent::ShouldResistFingerprinting(CallerType aCallerType) { + // There are five situations we don't need to spoof this keyboard event. + // 1. The pref privcy.resistFingerprinting' is false, we fast return here. + // 2. This event is initialized by scripts. + // 3. This event is from Numpad. + // 4. This event is in the system group. + // 5. The caller type is system. + if (!nsContentUtils::ShouldResistFingerprinting("Efficiency Check", + RFPTarget::KeyboardEvents) || + mInitializedByJS || aCallerType == CallerType::System || + mEvent->mFlags.mInSystemGroup || + mEvent->AsKeyboardEvent()->mLocation == + KeyboardEvent_Binding::DOM_KEY_LOCATION_NUMPAD) { + return false; + } + + nsCOMPtr<Document> doc = GetDocument(); + // We've checked the pref above, so use true as fallback if doc is null. + return doc ? doc->ShouldResistFingerprinting(RFPTarget::KeyboardEvents) + : true; +} + +bool KeyboardEvent::GetSpoofedModifierStates(const Modifiers aModifierKey, + const bool aRawModifierState) { + bool spoofedState; + nsCOMPtr<Document> doc = GetDocument(); + + if (nsRFPService::GetSpoofedModifierStates(doc, mEvent->AsKeyboardEvent(), + aModifierKey, spoofedState)) { + return spoofedState; + } + + return aRawModifierState; +} + +} // namespace mozilla::dom + +using namespace mozilla; +using namespace mozilla::dom; + +already_AddRefed<KeyboardEvent> NS_NewDOMKeyboardEvent( + EventTarget* aOwner, nsPresContext* aPresContext, + WidgetKeyboardEvent* aEvent) { + RefPtr<KeyboardEvent> it = new KeyboardEvent(aOwner, aPresContext, aEvent); + return it.forget(); +} diff --git a/dom/events/KeyboardEvent.h b/dom/events/KeyboardEvent.h new file mode 100644 index 0000000000..a43c6caaef --- /dev/null +++ b/dom/events/KeyboardEvent.h @@ -0,0 +1,135 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#ifndef mozilla_dom_KeyboardEvent_h_ +#define mozilla_dom_KeyboardEvent_h_ + +#include "mozilla/dom/UIEvent.h" +#include "mozilla/dom/KeyboardEventBinding.h" +#include "mozilla/EventForwards.h" + +namespace mozilla::dom { + +class KeyboardEvent : public UIEvent { + public: + KeyboardEvent(EventTarget* aOwner, nsPresContext* aPresContext, + WidgetKeyboardEvent* aEvent); + + NS_INLINE_DECL_REFCOUNTING_INHERITED(KeyboardEvent, UIEvent) + + virtual KeyboardEvent* AsKeyboardEvent() override { return this; } + + static already_AddRefed<KeyboardEvent> ConstructorJS( + const GlobalObject& aGlobal, const nsAString& aType, + const KeyboardEventInit& aParam); + + virtual JSObject* WrapObjectInternal( + JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override { + return KeyboardEvent_Binding::Wrap(aCx, this, aGivenProto); + } + + bool AltKey(CallerType aCallerType = CallerType::System); + bool CtrlKey(CallerType aCallerType = CallerType::System); + bool ShiftKey(CallerType aCallerType = CallerType::System); + bool MetaKey(); + + // Returns true if the modifier state of the event matches the modifier state + // of access key. + bool IsMenuAccessKeyPressed() const; + Modifiers GetModifiersForMenuAccessKey() const; + + bool GetModifierState(const nsAString& aKey, + CallerType aCallerType = CallerType::System) { + bool modifierState = GetModifierStateInternal(aKey); + + if (!ShouldResistFingerprinting(aCallerType)) { + return modifierState; + } + + Modifiers modifier = WidgetInputEvent::GetModifier(aKey); + return GetSpoofedModifierStates(modifier, modifierState); + } + + bool Repeat(); + bool IsComposing(); + void GetKey(nsAString& aKey) const; + uint32_t CharCode(CallerType aCallerType = CallerType::System); + uint32_t KeyCode(CallerType aCallerType = CallerType::System); + virtual uint32_t Which(CallerType aCallerType = CallerType::System) override; + uint32_t Location(); + + void GetCode(nsAString& aCode, CallerType aCallerType = CallerType::System); + void GetInitDict(KeyboardEventInit& aParam); + + void InitKeyEventJS(const nsAString& aType, bool aCanBubble, bool aCancelable, + nsGlobalWindowInner* aView, bool aCtrlKey, bool aAltKey, + bool aShiftKey, bool aMetaKey, uint32_t aKeyCode, + uint32_t aCharCode); + static bool IsInitKeyEventAvailable(JSContext*, JSObject*); + + void InitKeyboardEventJS(const nsAString& aType, bool aCanBubble, + bool aCancelable, nsGlobalWindowInner* aView, + const nsAString& aKey, uint32_t aLocation, + bool aCtrlKey, bool aAltKey, bool aShiftKey, + bool aMetaKey); + + protected: + ~KeyboardEvent() = default; + + void InitWithKeyboardEventInit(EventTarget* aOwner, const nsAString& aType, + const KeyboardEventInit& aParam); + + private: + // True, if the instance is initialized by JS. + bool mInitializedByJS; + // True, if the instance is initialized by Ctor. + bool mInitializedByCtor; + + // If the instance is created with Constructor(), which may have independent + // value. mInitializedWhichValue stores it. I.e., this is invalid when + // mInitializedByCtor is false. + uint32_t mInitializedWhichValue; + + // This method returns the boolean to indicate whether spoofing keyboard + // event for fingerprinting resistance. It will return true when pref + // 'privacy.resistFingerprinting' is true and the event target is content. + // Otherwise, it will return false. + bool ShouldResistFingerprinting(CallerType aCallerType); + + // This method returns the spoofed modifier state of the given modifier key + // for fingerprinting resistance. + bool GetSpoofedModifierStates(const Modifiers aModifierKey, + const bool aRawModifierState); + + /** + * ComputeTraditionalKeyCode() computes traditional keyCode value. I.e., + * returns 0 if this event should return non-zero from CharCode(). + * In spite of the name containing "traditional", this computes spoof + * keyCode value if user wants it. + * + * @param aKeyboardEvent Should be |*mEvent->AsKeyboardEvent()|. + * @param aCallerType Set caller type of KeyCode() or CharCode(). + * @return If traditional charCode value is 0, returns + * the raw keyCode value or spoof keyCode value. + * Otherwise, 0. + */ + uint32_t ComputeTraditionalKeyCode(WidgetKeyboardEvent& aKeyboardEvent, + CallerType aCallerType); + /** + * ShouldUseSameValueForCharCodeAndKeyCode() returns true if KeyCode() and + * CharCode() should return same value. + */ + bool ShouldUseSameValueForCharCodeAndKeyCode( + const WidgetKeyboardEvent& aKeyboardEvent, CallerType aCallerType) const; +}; + +} // namespace mozilla::dom + +already_AddRefed<mozilla::dom::KeyboardEvent> NS_NewDOMKeyboardEvent( + mozilla::dom::EventTarget* aOwner, nsPresContext* aPresContext, + mozilla::WidgetKeyboardEvent* aEvent); + +#endif // mozilla_dom_KeyboardEvent_h_ diff --git a/dom/events/MessageEvent.cpp b/dom/events/MessageEvent.cpp new file mode 100644 index 0000000000..92e59a6348 --- /dev/null +++ b/dom/events/MessageEvent.cpp @@ -0,0 +1,162 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#include "mozilla/dom/MessageEvent.h" +#include "mozilla/dom/BrowsingContext.h" +#include "mozilla/dom/MessageEventBinding.h" +#include "mozilla/dom/MessagePort.h" +#include "mozilla/dom/MessagePortBinding.h" +#include "mozilla/dom/ServiceWorker.h" + +#include "mozilla/HoldDropJSObjects.h" +#include "jsapi.h" + +namespace mozilla::dom { + +NS_IMPL_CYCLE_COLLECTION_CLASS(MessageEvent) + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(MessageEvent, Event) + tmp->mData.setUndefined(); + NS_IMPL_CYCLE_COLLECTION_UNLINK(mWindowSource) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mPortSource) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mServiceWorkerSource) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mPorts) +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(MessageEvent, Event) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mWindowSource) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPortSource) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mServiceWorkerSource) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPorts) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN_INHERITED(MessageEvent, Event) + NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mData) +NS_IMPL_CYCLE_COLLECTION_TRACE_END + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(MessageEvent) +NS_INTERFACE_MAP_END_INHERITING(Event) + +NS_IMPL_ADDREF_INHERITED(MessageEvent, Event) +NS_IMPL_RELEASE_INHERITED(MessageEvent, Event) + +MessageEvent::MessageEvent(EventTarget* aOwner, nsPresContext* aPresContext, + WidgetEvent* aEvent) + : Event(aOwner, aPresContext, aEvent), mData(JS::UndefinedValue()) {} + +MessageEvent::~MessageEvent() { DropJSObjects(this); } + +JSObject* MessageEvent::WrapObjectInternal(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return mozilla::dom::MessageEvent_Binding::Wrap(aCx, this, aGivenProto); +} + +void MessageEvent::GetData(JSContext* aCx, JS::MutableHandle<JS::Value> aData, + ErrorResult& aRv) { + aData.set(mData); + if (!JS_WrapValue(aCx, aData)) { + aRv.Throw(NS_ERROR_FAILURE); + } +} + +void MessageEvent::GetOrigin(nsAString& aOrigin) const { aOrigin = mOrigin; } + +void MessageEvent::GetLastEventId(nsAString& aLastEventId) const { + aLastEventId = mLastEventId; +} + +void MessageEvent::GetSource( + Nullable<OwningWindowProxyOrMessagePortOrServiceWorker>& aValue) const { + if (mWindowSource) { + aValue.SetValue().SetAsWindowProxy() = mWindowSource; + } else if (mPortSource) { + aValue.SetValue().SetAsMessagePort() = mPortSource; + } else if (mServiceWorkerSource) { + aValue.SetValue().SetAsServiceWorker() = mServiceWorkerSource; + } +} + +/* static */ +already_AddRefed<MessageEvent> MessageEvent::Constructor( + const GlobalObject& aGlobal, const nsAString& aType, + const MessageEventInit& aParam) { + nsCOMPtr<EventTarget> t = do_QueryInterface(aGlobal.GetAsSupports()); + return Constructor(t, aType, aParam); +} + +/* static */ +already_AddRefed<MessageEvent> MessageEvent::Constructor( + EventTarget* aEventTarget, const nsAString& aType, + const MessageEventInit& aParam) { + RefPtr<MessageEvent> event = new MessageEvent(aEventTarget, nullptr, nullptr); + + event->InitEvent(aType, aParam.mBubbles, aParam.mCancelable); + bool trusted = event->Init(aEventTarget); + event->SetTrusted(trusted); + + event->mData = aParam.mData; + + mozilla::HoldJSObjects(event.get()); + + event->mOrigin = aParam.mOrigin; + event->mLastEventId = aParam.mLastEventId; + + if (!aParam.mSource.IsNull()) { + if (aParam.mSource.Value().IsWindowProxy()) { + event->mWindowSource = aParam.mSource.Value().GetAsWindowProxy().get(); + } else if (aParam.mSource.Value().IsMessagePort()) { + event->mPortSource = aParam.mSource.Value().GetAsMessagePort(); + } else { + event->mServiceWorkerSource = aParam.mSource.Value().GetAsServiceWorker(); + } + + MOZ_ASSERT(event->mWindowSource || event->mPortSource || + event->mServiceWorkerSource); + } + + event->mPorts.AppendElements(aParam.mPorts); + + return event.forget(); +} + +void MessageEvent::InitMessageEvent( + JSContext* aCx, const nsAString& aType, mozilla::CanBubble aCanBubble, + mozilla::Cancelable aCancelable, JS::Handle<JS::Value> aData, + const nsAString& aOrigin, const nsAString& aLastEventId, + const Nullable<WindowProxyOrMessagePortOrServiceWorker>& aSource, + const Sequence<OwningNonNull<MessagePort>>& aPorts) { + NS_ENSURE_TRUE_VOID(!mEvent->mFlags.mIsBeingDispatched); + + Event::InitEvent(aType, aCanBubble, aCancelable); + mData = aData; + mozilla::HoldJSObjects(this); + mOrigin = aOrigin; + mLastEventId = aLastEventId; + + mWindowSource = nullptr; + mPortSource = nullptr; + mServiceWorkerSource = nullptr; + + if (!aSource.IsNull()) { + if (aSource.Value().IsWindowProxy()) { + mWindowSource = aSource.Value().GetAsWindowProxy().get(); + } else if (aSource.Value().IsMessagePort()) { + mPortSource = &aSource.Value().GetAsMessagePort(); + } else { + mServiceWorkerSource = &aSource.Value().GetAsServiceWorker(); + } + } + + mPorts.Clear(); + mPorts.AppendElements(aPorts); + MessageEvent_Binding::ClearCachedPortsValue(this); +} + +void MessageEvent::GetPorts(nsTArray<RefPtr<MessagePort>>& aPorts) { + aPorts = mPorts.Clone(); +} + +} // namespace mozilla::dom diff --git a/dom/events/MessageEvent.h b/dom/events/MessageEvent.h new file mode 100644 index 0000000000..e6afd33998 --- /dev/null +++ b/dom/events/MessageEvent.h @@ -0,0 +1,102 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#ifndef mozilla_dom_MessageEvent_h_ +#define mozilla_dom_MessageEvent_h_ + +#include "js/RootingAPI.h" +#include "js/Value.h" +#include "mozilla/AlreadyAddRefed.h" +#include "mozilla/Assertions.h" +#include "mozilla/BasicEvents.h" +#include "mozilla/RefPtr.h" +#include "mozilla/dom/Event.h" +#include "nsCycleCollectionParticipant.h" +#include "nsISupports.h" +#include "nsStringFwd.h" +#include "nsTArray.h" + +namespace mozilla::dom { + +class BrowsingContext; +struct MessageEventInit; +class MessagePort; +class OwningWindowProxyOrMessagePortOrServiceWorker; +class ServiceWorker; +class WindowProxyOrMessagePortOrServiceWorker; + +/** + * Implements the MessageEvent event, used for cross-document messaging and + * server-sent events. + * + * See http://www.whatwg.org/specs/web-apps/current-work/#messageevent for + * further details. + */ +class MessageEvent final : public Event { + public: + MessageEvent(EventTarget* aOwner, nsPresContext* aPresContext, + WidgetEvent* aEvent); + + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS_INHERITED(MessageEvent, Event) + + JSObject* WrapObjectInternal(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + MessageEvent* AsMessageEvent() override { return this; } + + void GetData(JSContext* aCx, JS::MutableHandle<JS::Value> aData, + ErrorResult& aRv); + void GetOrigin(nsAString&) const; + void GetLastEventId(nsAString&) const; + void GetSource( + Nullable<OwningWindowProxyOrMessagePortOrServiceWorker>& aValue) const; + + void GetPorts(nsTArray<RefPtr<MessagePort>>& aPorts); + + static already_AddRefed<MessageEvent> Constructor( + const GlobalObject& aGlobal, const nsAString& aType, + const MessageEventInit& aEventInit); + + static already_AddRefed<MessageEvent> Constructor( + EventTarget* aEventTarget, const nsAString& aType, + const MessageEventInit& aEventInit); + + void InitMessageEvent( + JSContext* aCx, const nsAString& aType, bool aCanBubble, bool aCancelable, + JS::Handle<JS::Value> aData, const nsAString& aOrigin, + const nsAString& aLastEventId, + const Nullable<WindowProxyOrMessagePortOrServiceWorker>& aSource, + const Sequence<OwningNonNull<MessagePort>>& aPorts) { + InitMessageEvent(aCx, aType, aCanBubble ? CanBubble::eYes : CanBubble::eNo, + aCancelable ? Cancelable::eYes : Cancelable::eNo, aData, + aOrigin, aLastEventId, aSource, aPorts); + } + + void InitMessageEvent( + JSContext* aCx, const nsAString& aType, mozilla::CanBubble, + mozilla::Cancelable, JS::Handle<JS::Value> aData, + const nsAString& aOrigin, const nsAString& aLastEventId, + const Nullable<WindowProxyOrMessagePortOrServiceWorker>& aSource, + const Sequence<OwningNonNull<MessagePort>>& aPorts); + + protected: + ~MessageEvent(); + + private: + JS::Heap<JS::Value> mData; + nsString mOrigin; + nsString mLastEventId; + RefPtr<BrowsingContext> mWindowSource; + RefPtr<MessagePort> mPortSource; + RefPtr<ServiceWorker> mServiceWorkerSource; + + nsTArray<RefPtr<MessagePort>> mPorts; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_MessageEvent_h_ diff --git a/dom/events/MouseEvent.cpp b/dom/events/MouseEvent.cpp new file mode 100644 index 0000000000..069e5531b7 --- /dev/null +++ b/dom/events/MouseEvent.cpp @@ -0,0 +1,320 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#include "mozilla/dom/MouseEvent.h" +#include "mozilla/MouseEvents.h" +#include "nsContentUtils.h" +#include "nsIContent.h" +#include "nsIScreenManager.h" +#include "prtime.h" + +namespace mozilla::dom { + +MouseEvent::MouseEvent(EventTarget* aOwner, nsPresContext* aPresContext, + WidgetMouseEventBase* aEvent) + : UIEvent(aOwner, aPresContext, + aEvent ? aEvent + : new WidgetMouseEvent(false, eVoidEvent, nullptr, + WidgetMouseEvent::eReal)) { + // There's no way to make this class' ctor allocate an WidgetMouseScrollEvent. + // It's not that important, though, since a scroll event is not a real + // DOM event. + + WidgetMouseEvent* mouseEvent = mEvent->AsMouseEvent(); + if (aEvent) { + mEventIsInternal = false; + } else { + mEventIsInternal = true; + mEvent->mRefPoint = LayoutDeviceIntPoint(0, 0); + mouseEvent->mInputSource = MouseEvent_Binding::MOZ_SOURCE_UNKNOWN; + } + + if (mouseEvent) { + MOZ_ASSERT(mouseEvent->mReason != WidgetMouseEvent::eSynthesized, + "Don't dispatch DOM events from synthesized mouse events"); + mDetail = mouseEvent->mClickCount; + } +} + +void MouseEvent::InitMouseEvent(const nsAString& aType, bool aCanBubble, + bool aCancelable, nsGlobalWindowInner* aView, + int32_t aDetail, int32_t aScreenX, + int32_t aScreenY, int32_t aClientX, + int32_t aClientY, bool aCtrlKey, bool aAltKey, + bool aShiftKey, bool aMetaKey, uint16_t aButton, + EventTarget* aRelatedTarget) { + NS_ENSURE_TRUE_VOID(!mEvent->mFlags.mIsBeingDispatched); + + UIEvent::InitUIEvent(aType, aCanBubble, aCancelable, aView, aDetail); + + switch (mEvent->mClass) { + case eMouseEventClass: + case eMouseScrollEventClass: + case eWheelEventClass: + case eDragEventClass: + case ePointerEventClass: + case eSimpleGestureEventClass: { + WidgetMouseEventBase* mouseEventBase = mEvent->AsMouseEventBase(); + mouseEventBase->mRelatedTarget = aRelatedTarget; + mouseEventBase->mButton = aButton; + mouseEventBase->InitBasicModifiers(aCtrlKey, aAltKey, aShiftKey, + aMetaKey); + mDefaultClientPoint.x = aClientX; + mDefaultClientPoint.y = aClientY; + mouseEventBase->mRefPoint.x = aScreenX; + mouseEventBase->mRefPoint.y = aScreenY; + + WidgetMouseEvent* mouseEvent = mEvent->AsMouseEvent(); + if (mouseEvent) { + mouseEvent->mClickCount = aDetail; + } + break; + } + default: + break; + } +} + +void MouseEvent::InitMouseEvent(const nsAString& aType, bool aCanBubble, + bool aCancelable, nsGlobalWindowInner* aView, + int32_t aDetail, int32_t aScreenX, + int32_t aScreenY, int32_t aClientX, + int32_t aClientY, int16_t aButton, + EventTarget* aRelatedTarget, + const nsAString& aModifiersList) { + NS_ENSURE_TRUE_VOID(!mEvent->mFlags.mIsBeingDispatched); + + Modifiers modifiers = ComputeModifierState(aModifiersList); + + InitMouseEvent( + aType, aCanBubble, aCancelable, aView, aDetail, aScreenX, aScreenY, + aClientX, aClientY, (modifiers & MODIFIER_CONTROL) != 0, + (modifiers & MODIFIER_ALT) != 0, (modifiers & MODIFIER_SHIFT) != 0, + (modifiers & MODIFIER_META) != 0, aButton, aRelatedTarget); + + switch (mEvent->mClass) { + case eMouseEventClass: + case eMouseScrollEventClass: + case eWheelEventClass: + case eDragEventClass: + case ePointerEventClass: + case eSimpleGestureEventClass: + mEvent->AsInputEvent()->mModifiers = modifiers; + return; + default: + MOZ_CRASH("There is no space to store the modifiers"); + } +} + +void MouseEvent::InitializeExtraMouseEventDictionaryMembers( + const MouseEventInit& aParam) { + InitModifiers(aParam); + mEvent->AsMouseEventBase()->mButtons = aParam.mButtons; + mMovementPoint.x = aParam.mMovementX; + mMovementPoint.y = aParam.mMovementY; +} + +already_AddRefed<MouseEvent> MouseEvent::Constructor( + const GlobalObject& aGlobal, const nsAString& aType, + const MouseEventInit& aParam) { + nsCOMPtr<EventTarget> t = do_QueryInterface(aGlobal.GetAsSupports()); + RefPtr<MouseEvent> e = new MouseEvent(t, nullptr, nullptr); + bool trusted = e->Init(t); + e->InitMouseEvent(aType, aParam.mBubbles, aParam.mCancelable, aParam.mView, + aParam.mDetail, aParam.mScreenX, aParam.mScreenY, + aParam.mClientX, aParam.mClientY, aParam.mCtrlKey, + aParam.mAltKey, aParam.mShiftKey, aParam.mMetaKey, + aParam.mButton, aParam.mRelatedTarget); + e->InitializeExtraMouseEventDictionaryMembers(aParam); + e->SetTrusted(trusted); + e->SetComposed(aParam.mComposed); + return e.forget(); +} + +void MouseEvent::InitNSMouseEvent(const nsAString& aType, bool aCanBubble, + bool aCancelable, nsGlobalWindowInner* aView, + int32_t aDetail, int32_t aScreenX, + int32_t aScreenY, int32_t aClientX, + int32_t aClientY, bool aCtrlKey, bool aAltKey, + bool aShiftKey, bool aMetaKey, + uint16_t aButton, EventTarget* aRelatedTarget, + float aPressure, uint16_t aInputSource) { + NS_ENSURE_TRUE_VOID(!mEvent->mFlags.mIsBeingDispatched); + + MouseEvent::InitMouseEvent(aType, aCanBubble, aCancelable, aView, aDetail, + aScreenX, aScreenY, aClientX, aClientY, aCtrlKey, + aAltKey, aShiftKey, aMetaKey, aButton, + aRelatedTarget); + + WidgetMouseEventBase* mouseEventBase = mEvent->AsMouseEventBase(); + mouseEventBase->mPressure = aPressure; + mouseEventBase->mInputSource = aInputSource; +} + +void MouseEvent::PreventClickEvent() { + if (WidgetMouseEvent* mouseEvent = mEvent->AsMouseEvent()) { + mouseEvent->mClickEventPrevented = true; + } +} + +bool MouseEvent::ClickEventPrevented() { + if (WidgetMouseEvent* mouseEvent = mEvent->AsMouseEvent()) { + return mouseEvent->mClickEventPrevented; + } + return false; +} + +int16_t MouseEvent::Button() { + switch (mEvent->mClass) { + case eMouseEventClass: + case eMouseScrollEventClass: + case eWheelEventClass: + case eDragEventClass: + case ePointerEventClass: + case eSimpleGestureEventClass: + return mEvent->AsMouseEventBase()->mButton; + default: + NS_WARNING("Tried to get mouse mButton for non-mouse event!"); + return MouseButton::ePrimary; + } +} + +uint16_t MouseEvent::Buttons() { + switch (mEvent->mClass) { + case eMouseEventClass: + case eMouseScrollEventClass: + case eWheelEventClass: + case eDragEventClass: + case ePointerEventClass: + case eSimpleGestureEventClass: + return mEvent->AsMouseEventBase()->mButtons; + default: + MOZ_CRASH("Tried to get mouse buttons for non-mouse event!"); + } +} + +already_AddRefed<EventTarget> MouseEvent::GetRelatedTarget() { + nsCOMPtr<EventTarget> relatedTarget; + switch (mEvent->mClass) { + case eMouseEventClass: + case eMouseScrollEventClass: + case eWheelEventClass: + case eDragEventClass: + case ePointerEventClass: + case eSimpleGestureEventClass: + relatedTarget = mEvent->AsMouseEventBase()->mRelatedTarget; + break; + default: + break; + } + + return EnsureWebAccessibleRelatedTarget(relatedTarget); +} + +void MouseEvent::GetRegion(nsAString& aRegion) { SetDOMStringToNull(aRegion); } + +CSSIntPoint MouseEvent::ScreenPoint(CallerType aCallerType) const { + if (mEvent->mFlags.mIsPositionless) { + return {}; + } + + if (nsContentUtils::ShouldResistFingerprinting(aCallerType, GetParentObject(), + RFPTarget::Unknown)) { + // Sanitize to something sort of like client cooords, but not quite + // (defaulting to (0,0) instead of our pre-specified client coords). + return Event::GetClientCoords(mPresContext, mEvent, mEvent->mRefPoint, + CSSIntPoint(0, 0)); + } + + return Event::GetScreenCoords(mPresContext, mEvent, mEvent->mRefPoint) + .extract(); +} + +LayoutDeviceIntPoint MouseEvent::ScreenPointLayoutDevicePix() const { + const CSSIntPoint point = ScreenPoint(CallerType::System); + auto scale = mPresContext ? mPresContext->CSSToDevPixelScale() + : CSSToLayoutDeviceScale(); + return LayoutDeviceIntPoint::Round(point * scale); +} + +DesktopIntPoint MouseEvent::ScreenPointDesktopPix() const { + const CSSIntPoint point = ScreenPoint(CallerType::System); + auto scale = + mPresContext + ? mPresContext->CSSToDevPixelScale() / + mPresContext->DeviceContext()->GetDesktopToDeviceScale() + : CSSToDesktopScale(); + return DesktopIntPoint::Round(point * scale); +} + +already_AddRefed<nsIScreen> MouseEvent::GetScreen() { + nsCOMPtr<nsIScreenManager> screenMgr = + do_GetService("@mozilla.org/gfx/screenmanager;1"); + if (!screenMgr) { + return nullptr; + } + return screenMgr->ScreenForRect( + DesktopIntRect(ScreenPointDesktopPix(), DesktopIntSize(1, 1))); +} + +CSSIntPoint MouseEvent::PagePoint() const { + if (mEvent->mFlags.mIsPositionless) { + return {}; + } + + if (mPrivateDataDuplicated) { + return mPagePoint; + } + + return Event::GetPageCoords(mPresContext, mEvent, mEvent->mRefPoint, + mDefaultClientPoint); +} + +CSSIntPoint MouseEvent::ClientPoint() const { + if (mEvent->mFlags.mIsPositionless) { + return {}; + } + + return Event::GetClientCoords(mPresContext, mEvent, mEvent->mRefPoint, + mDefaultClientPoint); +} + +CSSIntPoint MouseEvent::OffsetPoint() const { + if (mEvent->mFlags.mIsPositionless) { + return {}; + } + + return Event::GetOffsetCoords(mPresContext, mEvent, mEvent->mRefPoint, + mDefaultClientPoint); +} + +bool MouseEvent::AltKey() { return mEvent->AsInputEvent()->IsAlt(); } + +bool MouseEvent::CtrlKey() { return mEvent->AsInputEvent()->IsControl(); } + +bool MouseEvent::ShiftKey() { return mEvent->AsInputEvent()->IsShift(); } + +bool MouseEvent::MetaKey() { return mEvent->AsInputEvent()->IsMeta(); } + +float MouseEvent::MozPressure() const { + return mEvent->AsMouseEventBase()->mPressure; +} + +uint16_t MouseEvent::MozInputSource() const { + return mEvent->AsMouseEventBase()->mInputSource; +} + +} // namespace mozilla::dom + +using namespace mozilla; +using namespace mozilla::dom; + +already_AddRefed<MouseEvent> NS_NewDOMMouseEvent(EventTarget* aOwner, + nsPresContext* aPresContext, + WidgetMouseEvent* aEvent) { + RefPtr<MouseEvent> it = new MouseEvent(aOwner, aPresContext, aEvent); + return it.forget(); +} diff --git a/dom/events/MouseEvent.h b/dom/events/MouseEvent.h new file mode 100644 index 0000000000..7d1b403ab0 --- /dev/null +++ b/dom/events/MouseEvent.h @@ -0,0 +1,115 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#ifndef mozilla_dom_MouseEvent_h_ +#define mozilla_dom_MouseEvent_h_ + +#include "mozilla/dom/BindingDeclarations.h" +#include "mozilla/dom/UIEvent.h" +#include "mozilla/dom/MouseEventBinding.h" +#include "mozilla/EventForwards.h" + +namespace mozilla::dom { + +class MouseEvent : public UIEvent { + public: + MouseEvent(EventTarget* aOwner, nsPresContext* aPresContext, + WidgetMouseEventBase* aEvent); + + NS_INLINE_DECL_REFCOUNTING_INHERITED(MouseEvent, UIEvent) + + virtual JSObject* WrapObjectInternal( + JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override { + return MouseEvent_Binding::Wrap(aCx, this, aGivenProto); + } + + virtual MouseEvent* AsMouseEvent() override { return this; } + + // Web IDL binding methods + virtual uint32_t Which(CallerType aCallerType) override { + return Button() + 1; + } + + already_AddRefed<nsIScreen> GetScreen(); + + // In CSS coords. + CSSIntPoint ScreenPoint(CallerType) const; + int32_t ScreenX(CallerType aCallerType) const { + return ScreenPoint(aCallerType).x; + } + int32_t ScreenY(CallerType aCallerType) const { + return ScreenPoint(aCallerType).y; + } + LayoutDeviceIntPoint ScreenPointLayoutDevicePix() const; + DesktopIntPoint ScreenPointDesktopPix() const; + + CSSIntPoint PagePoint() const; + int32_t PageX() const { return PagePoint().x; } + int32_t PageY() const { return PagePoint().y; } + + CSSIntPoint ClientPoint() const; + int32_t ClientX() const { return ClientPoint().x; } + int32_t ClientY() const { return ClientPoint().y; } + + CSSIntPoint OffsetPoint() const; + int32_t OffsetX() const { return OffsetPoint().x; } + int32_t OffsetY() const { return OffsetPoint().y; } + + bool CtrlKey(); + bool ShiftKey(); + bool AltKey(); + bool MetaKey(); + int16_t Button(); + uint16_t Buttons(); + already_AddRefed<EventTarget> GetRelatedTarget(); + void GetRegion(nsAString& aRegion); + void InitMouseEvent(const nsAString& aType, bool aCanBubble, bool aCancelable, + nsGlobalWindowInner* aView, int32_t aDetail, + int32_t aScreenX, int32_t aScreenY, int32_t aClientX, + int32_t aClientY, bool aCtrlKey, bool aAltKey, + bool aShiftKey, bool aMetaKey, uint16_t aButton, + EventTarget* aRelatedTarget); + + void InitializeExtraMouseEventDictionaryMembers(const MouseEventInit& aParam); + + bool GetModifierState(const nsAString& aKeyArg) { + return GetModifierStateInternal(aKeyArg); + } + static already_AddRefed<MouseEvent> Constructor(const GlobalObject& aGlobal, + const nsAString& aType, + const MouseEventInit& aParam); + int32_t MovementX() { return GetMovementPoint().x; } + int32_t MovementY() { return GetMovementPoint().y; } + float MozPressure() const; + uint16_t MozInputSource() const; + void InitNSMouseEvent(const nsAString& aType, bool aCanBubble, + bool aCancelable, nsGlobalWindowInner* aView, + int32_t aDetail, int32_t aScreenX, int32_t aScreenY, + int32_t aClientX, int32_t aClientY, bool aCtrlKey, + bool aAltKey, bool aShiftKey, bool aMetaKey, + uint16_t aButton, EventTarget* aRelatedTarget, + float aPressure, uint16_t aInputSource); + void PreventClickEvent(); + bool ClickEventPrevented(); + + protected: + ~MouseEvent() = default; + + void InitMouseEvent(const nsAString& aType, bool aCanBubble, bool aCancelable, + nsGlobalWindowInner* aView, int32_t aDetail, + int32_t aScreenX, int32_t aScreenY, int32_t aClientX, + int32_t aClientY, int16_t aButton, + EventTarget* aRelatedTarget, + const nsAString& aModifiersList); +}; + +} // namespace mozilla::dom + +already_AddRefed<mozilla::dom::MouseEvent> NS_NewDOMMouseEvent( + mozilla::dom::EventTarget* aOwner, nsPresContext* aPresContext, + mozilla::WidgetMouseEvent* aEvent); + +#endif // mozilla_dom_MouseEvent_h_ diff --git a/dom/events/MouseScrollEvent.cpp b/dom/events/MouseScrollEvent.cpp new file mode 100644 index 0000000000..58bc0b2dbb --- /dev/null +++ b/dom/events/MouseScrollEvent.cpp @@ -0,0 +1,66 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#include "mozilla/dom/MouseScrollEvent.h" +#include "mozilla/dom/MouseEventBinding.h" +#include "mozilla/MouseEvents.h" +#include "prtime.h" + +namespace mozilla::dom { + +MouseScrollEvent::MouseScrollEvent(EventTarget* aOwner, + nsPresContext* aPresContext, + WidgetMouseScrollEvent* aEvent) + : MouseEvent(aOwner, aPresContext, + aEvent + ? aEvent + : new WidgetMouseScrollEvent(false, eVoidEvent, nullptr)) { + if (aEvent) { + mEventIsInternal = false; + } else { + mEventIsInternal = true; + mEvent->mRefPoint = LayoutDeviceIntPoint(0, 0); + static_cast<WidgetMouseEventBase*>(mEvent)->mInputSource = + MouseEvent_Binding::MOZ_SOURCE_UNKNOWN; + } + + mDetail = mEvent->AsMouseScrollEvent()->mDelta; +} + +void MouseScrollEvent::InitMouseScrollEvent( + const nsAString& aType, bool aCanBubble, bool aCancelable, + nsGlobalWindowInner* aView, int32_t aDetail, int32_t aScreenX, + int32_t aScreenY, int32_t aClientX, int32_t aClientY, bool aCtrlKey, + bool aAltKey, bool aShiftKey, bool aMetaKey, uint16_t aButton, + EventTarget* aRelatedTarget, int32_t aAxis) { + NS_ENSURE_TRUE_VOID(!mEvent->mFlags.mIsBeingDispatched); + + MouseEvent::InitMouseEvent(aType, aCanBubble, aCancelable, aView, aDetail, + aScreenX, aScreenY, aClientX, aClientY, aCtrlKey, + aAltKey, aShiftKey, aMetaKey, aButton, + aRelatedTarget); + mEvent->AsMouseScrollEvent()->mIsHorizontal = + (aAxis == MouseScrollEvent_Binding::HORIZONTAL_AXIS); +} + +int32_t MouseScrollEvent::Axis() { + return mEvent->AsMouseScrollEvent()->mIsHorizontal + ? MouseScrollEvent_Binding::HORIZONTAL_AXIS + : MouseScrollEvent_Binding::VERTICAL_AXIS; +} + +} // namespace mozilla::dom + +using namespace mozilla; +using namespace dom; + +already_AddRefed<MouseScrollEvent> NS_NewDOMMouseScrollEvent( + EventTarget* aOwner, nsPresContext* aPresContext, + WidgetMouseScrollEvent* aEvent) { + RefPtr<MouseScrollEvent> it = + new MouseScrollEvent(aOwner, aPresContext, aEvent); + return it.forget(); +} diff --git a/dom/events/MouseScrollEvent.h b/dom/events/MouseScrollEvent.h new file mode 100644 index 0000000000..69be88691a --- /dev/null +++ b/dom/events/MouseScrollEvent.h @@ -0,0 +1,47 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#ifndef mozilla_dom_MouseScrollEvent_h_ +#define mozilla_dom_MouseScrollEvent_h_ + +#include "mozilla/dom/MouseEvent.h" +#include "mozilla/dom/MouseScrollEventBinding.h" + +namespace mozilla::dom { + +class MouseScrollEvent : public MouseEvent { + public: + MouseScrollEvent(EventTarget* aOwner, nsPresContext* aPresContext, + WidgetMouseScrollEvent* aEvent); + + NS_INLINE_DECL_REFCOUNTING_INHERITED(MouseScrollEvent, MouseEvent) + + virtual JSObject* WrapObjectInternal( + JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override { + return MouseScrollEvent_Binding::Wrap(aCx, this, aGivenProto); + } + + int32_t Axis(); + + void InitMouseScrollEvent(const nsAString& aType, bool aCanBubble, + bool aCancelable, nsGlobalWindowInner* aView, + int32_t aDetail, int32_t aScreenX, int32_t aScreenY, + int32_t aClientX, int32_t aClientY, bool aCtrlKey, + bool aAltKey, bool aShiftKey, bool aMetaKey, + uint16_t aButton, EventTarget* aRelatedTarget, + int32_t aAxis); + + protected: + ~MouseScrollEvent() = default; +}; + +} // namespace mozilla::dom + +already_AddRefed<mozilla::dom::MouseScrollEvent> NS_NewDOMMouseScrollEvent( + mozilla::dom::EventTarget* aOwner, nsPresContext* aPresContext, + mozilla::WidgetMouseScrollEvent* aEvent); + +#endif // mozilla_dom_MouseScrollEvent_h_ diff --git a/dom/events/MutationEvent.cpp b/dom/events/MutationEvent.cpp new file mode 100644 index 0000000000..f1e91c10a5 --- /dev/null +++ b/dom/events/MutationEvent.cpp @@ -0,0 +1,75 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#include "nsCOMPtr.h" +#include "mozilla/dom/MutationEvent.h" +#include "mozilla/InternalMutationEvent.h" + +class nsPresContext; + +namespace mozilla::dom { + +MutationEvent::MutationEvent(EventTarget* aOwner, nsPresContext* aPresContext, + InternalMutationEvent* aEvent) + : Event(aOwner, aPresContext, + aEvent ? aEvent : new InternalMutationEvent(false, eVoidEvent)) { + mEventIsInternal = (aEvent == nullptr); +} + +nsINode* MutationEvent::GetRelatedNode() { + return mEvent->AsMutationEvent()->mRelatedNode; +} + +void MutationEvent::GetPrevValue(nsAString& aPrevValue) const { + InternalMutationEvent* mutation = mEvent->AsMutationEvent(); + if (mutation->mPrevAttrValue) mutation->mPrevAttrValue->ToString(aPrevValue); +} + +void MutationEvent::GetNewValue(nsAString& aNewValue) const { + InternalMutationEvent* mutation = mEvent->AsMutationEvent(); + if (mutation->mNewAttrValue) mutation->mNewAttrValue->ToString(aNewValue); +} + +void MutationEvent::GetAttrName(nsAString& aAttrName) const { + InternalMutationEvent* mutation = mEvent->AsMutationEvent(); + if (mutation->mAttrName) mutation->mAttrName->ToString(aAttrName); +} + +uint16_t MutationEvent::AttrChange() { + return mEvent->AsMutationEvent()->mAttrChange; +} + +void MutationEvent::InitMutationEvent(const nsAString& aType, bool aCanBubble, + bool aCancelable, nsINode* aRelatedNode, + const nsAString& aPrevValue, + const nsAString& aNewValue, + const nsAString& aAttrName, + uint16_t& aAttrChange, ErrorResult& aRv) { + NS_ENSURE_TRUE_VOID(!mEvent->mFlags.mIsBeingDispatched); + + Event::InitEvent(aType, aCanBubble, aCancelable); + + InternalMutationEvent* mutation = mEvent->AsMutationEvent(); + mutation->mRelatedNode = aRelatedNode; + if (!aPrevValue.IsEmpty()) mutation->mPrevAttrValue = NS_Atomize(aPrevValue); + if (!aNewValue.IsEmpty()) mutation->mNewAttrValue = NS_Atomize(aNewValue); + if (!aAttrName.IsEmpty()) { + mutation->mAttrName = NS_Atomize(aAttrName); + } + mutation->mAttrChange = aAttrChange; +} + +} // namespace mozilla::dom + +using namespace mozilla; +using namespace mozilla::dom; + +already_AddRefed<MutationEvent> NS_NewDOMMutationEvent( + EventTarget* aOwner, nsPresContext* aPresContext, + InternalMutationEvent* aEvent) { + RefPtr<MutationEvent> it = new MutationEvent(aOwner, aPresContext, aEvent); + return it.forget(); +} diff --git a/dom/events/MutationEvent.h b/dom/events/MutationEvent.h new file mode 100644 index 0000000000..5751715038 --- /dev/null +++ b/dom/events/MutationEvent.h @@ -0,0 +1,53 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#ifndef mozilla_dom_MutationEvent_h_ +#define mozilla_dom_MutationEvent_h_ + +#include "mozilla/EventForwards.h" +#include "mozilla/dom/Event.h" +#include "mozilla/dom/MutationEventBinding.h" +#include "nsINode.h" + +namespace mozilla::dom { + +class MutationEvent : public Event { + public: + MutationEvent(EventTarget* aOwner, nsPresContext* aPresContext, + InternalMutationEvent* aEvent); + + NS_INLINE_DECL_REFCOUNTING_INHERITED(MutationEvent, Event) + + virtual JSObject* WrapObjectInternal( + JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override { + return MutationEvent_Binding::Wrap(aCx, this, aGivenProto); + } + + void GetPrevValue(nsAString& aPrevValue) const; + void GetNewValue(nsAString& aNewValue) const; + void GetAttrName(nsAString& aAttrName) const; + + nsINode* GetRelatedNode(); + + uint16_t AttrChange(); + + void InitMutationEvent(const nsAString& aType, bool aCanBubble, + bool aCancelable, nsINode* aRelatedNode, + const nsAString& aPrevValue, + const nsAString& aNewValue, const nsAString& aAttrName, + uint16_t& aAttrChange, ErrorResult& aRv); + + protected: + ~MutationEvent() = default; +}; + +} // namespace mozilla::dom + +already_AddRefed<mozilla::dom::MutationEvent> NS_NewDOMMutationEvent( + mozilla::dom::EventTarget* aOwner, nsPresContext* aPresContext, + mozilla::InternalMutationEvent* aEvent); + +#endif // mozilla_dom_MutationEvent_h_ diff --git a/dom/events/NotifyPaintEvent.cpp b/dom/events/NotifyPaintEvent.cpp new file mode 100644 index 0000000000..6bbcd736a4 --- /dev/null +++ b/dom/events/NotifyPaintEvent.cpp @@ -0,0 +1,133 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#include "base/basictypes.h" +#include "ipc/IPCMessageUtils.h" +#include "mozilla/dom/DOMRect.h" +#include "mozilla/dom/NotifyPaintEvent.h" +#include "mozilla/dom/PaintRequest.h" +#include "mozilla/GfxMessageUtils.h" +#include "nsContentUtils.h" + +namespace mozilla::dom { + +NotifyPaintEvent::NotifyPaintEvent( + EventTarget* aOwner, nsPresContext* aPresContext, WidgetEvent* aEvent, + EventMessage aEventMessage, nsTArray<nsRect>* aInvalidateRequests, + uint64_t aTransactionId, DOMHighResTimeStamp aTimeStamp) + : Event(aOwner, aPresContext, aEvent) { + if (mEvent) { + mEvent->mMessage = aEventMessage; + } + if (aInvalidateRequests) { + mInvalidateRequests.SwapElements(*aInvalidateRequests); + } + + mTransactionId = aTransactionId; + mTimeStamp = aTimeStamp; +} + +nsRegion NotifyPaintEvent::GetRegion(SystemCallerGuarantee) { + nsRegion r; + for (uint32_t i = 0; i < mInvalidateRequests.Length(); ++i) { + r.Or(r, mInvalidateRequests[i]); + r.SimplifyOutward(10); + } + return r; +} + +already_AddRefed<DOMRect> NotifyPaintEvent::BoundingClientRect( + SystemCallerGuarantee aGuarantee) { + RefPtr<DOMRect> rect = new DOMRect(ToSupports(this)); + + if (mPresContext) { + rect->SetLayoutRect(GetRegion(aGuarantee).GetBounds()); + } + + return rect.forget(); +} + +already_AddRefed<DOMRectList> NotifyPaintEvent::ClientRects( + SystemCallerGuarantee aGuarantee) { + nsISupports* parent = ToSupports(this); + RefPtr<DOMRectList> rectList = new DOMRectList(parent); + + nsRegion r = GetRegion(aGuarantee); + for (auto iter = r.RectIter(); !iter.Done(); iter.Next()) { + RefPtr<DOMRect> rect = new DOMRect(parent); + rect->SetLayoutRect(iter.Get()); + rectList->Append(rect); + } + + return rectList.forget(); +} + +already_AddRefed<PaintRequestList> NotifyPaintEvent::PaintRequests( + SystemCallerGuarantee) { + Event* parent = this; + RefPtr<PaintRequestList> requests = new PaintRequestList(parent); + + for (uint32_t i = 0; i < mInvalidateRequests.Length(); ++i) { + RefPtr<PaintRequest> r = new PaintRequest(parent); + r->SetRequest(mInvalidateRequests[i]); + requests->Append(r); + } + + return requests.forget(); +} + +void NotifyPaintEvent::Serialize(IPC::MessageWriter* aWriter, + bool aSerializeInterfaceType) { + if (aSerializeInterfaceType) { + IPC::WriteParam(aWriter, u"notifypaintevent"_ns); + } + + Event::Serialize(aWriter, false); + + uint32_t length = mInvalidateRequests.Length(); + IPC::WriteParam(aWriter, length); + for (uint32_t i = 0; i < length; ++i) { + IPC::WriteParam(aWriter, mInvalidateRequests[i]); + } +} + +bool NotifyPaintEvent::Deserialize(IPC::MessageReader* aReader) { + NS_ENSURE_TRUE(Event::Deserialize(aReader), false); + + uint32_t length = 0; + NS_ENSURE_TRUE(IPC::ReadParam(aReader, &length), false); + mInvalidateRequests.SetCapacity(length); + for (uint32_t i = 0; i < length; ++i) { + nsRect req; + NS_ENSURE_TRUE(IPC::ReadParam(aReader, &req), false); + mInvalidateRequests.AppendElement(req); + } + + return true; +} + +uint64_t NotifyPaintEvent::TransactionId(SystemCallerGuarantee) { + return mTransactionId; +} + +DOMHighResTimeStamp NotifyPaintEvent::PaintTimeStamp(SystemCallerGuarantee) { + return mTimeStamp; +} + +} // namespace mozilla::dom + +using namespace mozilla; +using namespace mozilla::dom; + +already_AddRefed<NotifyPaintEvent> NS_NewDOMNotifyPaintEvent( + EventTarget* aOwner, nsPresContext* aPresContext, WidgetEvent* aEvent, + EventMessage aEventMessage, nsTArray<nsRect>* aInvalidateRequests, + uint64_t aTransactionId, DOMHighResTimeStamp aTimeStamp) { + RefPtr<NotifyPaintEvent> it = + new NotifyPaintEvent(aOwner, aPresContext, aEvent, aEventMessage, + aInvalidateRequests, aTransactionId, aTimeStamp); + return it.forget(); +} diff --git a/dom/events/NotifyPaintEvent.h b/dom/events/NotifyPaintEvent.h new file mode 100644 index 0000000000..1da6578e81 --- /dev/null +++ b/dom/events/NotifyPaintEvent.h @@ -0,0 +1,73 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#ifndef mozilla_dom_NotifyPaintEvent_h_ +#define mozilla_dom_NotifyPaintEvent_h_ + +#include "mozilla/Attributes.h" +#include "mozilla/dom/Event.h" +#include "mozilla/dom/BindingDeclarations.h" +#include "mozilla/dom/NotifyPaintEventBinding.h" +#include "nsDOMNavigationTiming.h" +#include "nsPresContext.h" + +namespace mozilla::dom { + +class DOMRect; +class DOMRectList; +class PaintRequestList; + +class NotifyPaintEvent : public Event { + public: + NotifyPaintEvent(EventTarget* aOwner, nsPresContext* aPresContext, + WidgetEvent* aEvent, EventMessage aEventMessage, + nsTArray<nsRect>* aInvalidateRequests, + uint64_t aTransactionId, DOMHighResTimeStamp aTimeStamp); + + NS_INLINE_DECL_REFCOUNTING_INHERITED(NotifyPaintEvent, Event) + + void Serialize(IPC::MessageWriter* aWriter, + bool aSerializeInterfaceType) override; + bool Deserialize(IPC::MessageReader* aReader) override; + + virtual JSObject* WrapObjectInternal( + JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override { + return NotifyPaintEvent_Binding::Wrap(aCx, this, aGivenProto); + } + + already_AddRefed<DOMRectList> ClientRects(SystemCallerGuarantee aGuarantee); + + already_AddRefed<DOMRect> BoundingClientRect( + SystemCallerGuarantee aGuarantee); + + already_AddRefed<PaintRequestList> PaintRequests(SystemCallerGuarantee); + + uint64_t TransactionId(SystemCallerGuarantee); + + DOMHighResTimeStamp PaintTimeStamp(SystemCallerGuarantee); + + protected: + ~NotifyPaintEvent() = default; + + private: + nsRegion GetRegion(SystemCallerGuarantee); + + nsTArray<nsRect> mInvalidateRequests; + uint64_t mTransactionId; + DOMHighResTimeStamp mTimeStamp; +}; + +} // namespace mozilla::dom + +// This empties aInvalidateRequests. +already_AddRefed<mozilla::dom::NotifyPaintEvent> NS_NewDOMNotifyPaintEvent( + mozilla::dom::EventTarget* aOwner, nsPresContext* aPresContext, + mozilla::WidgetEvent* aEvent, + mozilla::EventMessage aEventMessage = mozilla::eVoidEvent, + nsTArray<nsRect>* aInvalidateRequests = nullptr, + uint64_t aTransactionId = 0, DOMHighResTimeStamp aTimeStamp = 0); + +#endif // mozilla_dom_NotifyPaintEvent_h_ diff --git a/dom/events/PaintRequest.cpp b/dom/events/PaintRequest.cpp new file mode 100644 index 0000000000..0cb4fbd940 --- /dev/null +++ b/dom/events/PaintRequest.cpp @@ -0,0 +1,60 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#include "mozilla/dom/PaintRequest.h" + +#include "mozilla/dom/PaintRequestBinding.h" +#include "mozilla/dom/PaintRequestListBinding.h" +#include "mozilla/dom/DOMRect.h" + +namespace mozilla::dom { + +/****************************************************************************** + * mozilla::dom::PaintRequest + *****************************************************************************/ + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(PaintRequest, mParent) + +NS_INTERFACE_TABLE_HEAD(PaintRequest) + NS_WRAPPERCACHE_INTERFACE_TABLE_ENTRY + NS_INTERFACE_TABLE_TO_MAP_SEGUE_CYCLE_COLLECTION(PaintRequest) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTING_ADDREF(PaintRequest) +NS_IMPL_CYCLE_COLLECTING_RELEASE(PaintRequest) + +/* virtual */ +JSObject* PaintRequest::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return PaintRequest_Binding::Wrap(aCx, this, aGivenProto); +} + +already_AddRefed<DOMRect> PaintRequest::ClientRect() { + RefPtr<DOMRect> clientRect = new DOMRect(mParent); + clientRect->SetLayoutRect(mRequest); + return clientRect.forget(); +} + +/****************************************************************************** + * mozilla::dom::PaintRequestList + *****************************************************************************/ + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(PaintRequestList, mParent) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(PaintRequestList) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTING_ADDREF(PaintRequestList) +NS_IMPL_CYCLE_COLLECTING_RELEASE(PaintRequestList) + +JSObject* PaintRequestList::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return PaintRequestList_Binding::Wrap(aCx, this, aGivenProto); +} + +} // namespace mozilla::dom diff --git a/dom/events/PaintRequest.h b/dom/events/PaintRequest.h new file mode 100644 index 0000000000..72a1e69283 --- /dev/null +++ b/dom/events/PaintRequest.h @@ -0,0 +1,77 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#ifndef mozilla_dom_PaintRequest_h_ +#define mozilla_dom_PaintRequest_h_ + +#include "nsPresContext.h" +#include "mozilla/Attributes.h" +#include "mozilla/RefPtr.h" +#include "mozilla/dom/Event.h" +#include "nsWrapperCache.h" + +namespace mozilla::dom { + +class DOMRect; + +class PaintRequest final : public nsISupports, public nsWrapperCache { + public: + explicit PaintRequest(Event* aParent) : mParent(aParent) {} + + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(PaintRequest) + + virtual JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + Event* GetParentObject() const { return mParent; } + + already_AddRefed<DOMRect> ClientRect(); + void GetReason(nsAString& aResult) const { aResult.AssignLiteral("repaint"); } + + void SetRequest(const nsRect& aRequest) { mRequest = aRequest; } + + private: + ~PaintRequest() = default; + + RefPtr<Event> mParent; + nsRect mRequest; +}; + +class PaintRequestList final : public nsISupports, public nsWrapperCache { + public: + explicit PaintRequestList(Event* aParent) : mParent(aParent) {} + + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(PaintRequestList) + + virtual JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + nsISupports* GetParentObject() { return mParent; } + + void Append(PaintRequest* aElement) { mArray.AppendElement(aElement); } + + uint32_t Length() { return mArray.Length(); } + + PaintRequest* Item(uint32_t aIndex) { return mArray.SafeElementAt(aIndex); } + PaintRequest* IndexedGetter(uint32_t aIndex, bool& aFound) { + aFound = aIndex < mArray.Length(); + if (!aFound) { + return nullptr; + } + return mArray.ElementAt(aIndex); + } + + private: + ~PaintRequestList() = default; + + nsTArray<RefPtr<PaintRequest> > mArray; + RefPtr<Event> mParent; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_PaintRequest_h_ diff --git a/dom/events/PendingFullscreenEvent.h b/dom/events/PendingFullscreenEvent.h new file mode 100644 index 0000000000..afd5e73b83 --- /dev/null +++ b/dom/events/PendingFullscreenEvent.h @@ -0,0 +1,71 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#ifndef mozilla_PendingFullscreenEvent_h_ +#define mozilla_PendingFullscreenEvent_h_ + +#include "mozilla/dom/Document.h" +#include "nsContentUtils.h" + +namespace mozilla { + +namespace dom { +class Document; +} + +enum class FullscreenEventType { + Change, + Error, +}; + +/* + * Class for dispatching a fullscreen event. It should be queued and + * invoked as part of "run the fullscreen steps" algorithm. + */ +class PendingFullscreenEvent { + public: + PendingFullscreenEvent(FullscreenEventType aType, dom::Document* aDocument, + nsINode* aTarget) + : mDocument(aDocument), mTarget(aTarget), mType(aType) { + MOZ_ASSERT(aDocument); + MOZ_ASSERT(aTarget); + } + + dom::Document* Document() const { return mDocument; } + + void Dispatch() { +#ifdef DEBUG + MOZ_ASSERT(!mDispatched); + mDispatched = true; +#endif + nsString name; + switch (mType) { + case FullscreenEventType::Change: + name = u"fullscreenchange"_ns; + break; + case FullscreenEventType::Error: + name = u"fullscreenerror"_ns; + break; + } + nsINode* target = mTarget->GetComposedDoc() == mDocument ? mTarget.get() + : mDocument.get(); + Unused << nsContentUtils::DispatchTrustedEvent( + mDocument, target, name, CanBubble::eYes, Cancelable::eNo, + Composed::eYes); + } + + private: + RefPtr<dom::Document> mDocument; + nsCOMPtr<nsINode> mTarget; + FullscreenEventType mType; +#ifdef DEBUG + bool mDispatched = false; +#endif +}; + +} // namespace mozilla + +#endif // mozilla_PendingFullscreenEvent_h_ diff --git a/dom/events/PhysicalKeyCodeNameList.h b/dom/events/PhysicalKeyCodeNameList.h new file mode 100644 index 0000000000..d83af423d9 --- /dev/null +++ b/dom/events/PhysicalKeyCodeNameList.h @@ -0,0 +1,235 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +/** + * This header file defines all DOM code name which are used for DOM + * KeyboardEvent.code. + * You must define NS_DEFINE_PHYSICAL_KEY_CODE_NAME macro before including this. + * + * It must have two arguments, (aCPPName, aDOMCodeName) + * aCPPName is usable name for a part of C++ constants. + * aDOMCodeName is the actual value. + */ + +#define NS_DEFINE_PHYSICAL_KEY_CODE_NAME_INTERNAL(aCPPName, aDOMCodeName) \ + NS_DEFINE_PHYSICAL_KEY_CODE_NAME(aCPPName, aDOMCodeName) + +#define DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(aName) \ + NS_DEFINE_PHYSICAL_KEY_CODE_NAME_INTERNAL(aName, #aName) + +// Unknown key +NS_DEFINE_PHYSICAL_KEY_CODE_NAME_INTERNAL(UNKNOWN, "") + +// Writing system keys +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(Backquote) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(Backslash) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(Backspace) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(BracketLeft) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(BracketRight) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(Comma) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(Digit0) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(Digit1) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(Digit2) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(Digit3) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(Digit4) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(Digit5) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(Digit6) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(Digit7) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(Digit8) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(Digit9) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(Equal) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(IntlBackslash) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(IntlHash) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(IntlRo) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(IntlYen) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(KeyA) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(KeyB) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(KeyC) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(KeyD) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(KeyE) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(KeyF) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(KeyG) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(KeyH) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(KeyI) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(KeyJ) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(KeyK) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(KeyL) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(KeyM) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(KeyN) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(KeyO) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(KeyP) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(KeyQ) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(KeyR) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(KeyS) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(KeyT) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(KeyU) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(KeyV) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(KeyW) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(KeyX) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(KeyY) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(KeyZ) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(Minus) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(Period) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(Quote) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(Semicolon) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(Slash) + +// Functional keys +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(AltLeft) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(AltRight) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(CapsLock) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(ContextMenu) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(ControlLeft) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(ControlRight) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(Enter) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(OSLeft) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(OSRight) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(ShiftLeft) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(ShiftRight) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(Space) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(Tab) + +// IME keys +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(Convert) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(KanaMode) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(Lang1) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(Lang2) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(Lang3) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(Lang4) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(Lang5) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(NonConvert) + +// Control pad section +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(Delete) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(End) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(Help) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(Home) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(Insert) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(PageDown) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(PageUp) + +// Arrow pad section +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(ArrowDown) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(ArrowLeft) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(ArrowRight) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(ArrowUp) + +// Numpad section +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(NumLock) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(Numpad0) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(Numpad1) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(Numpad2) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(Numpad3) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(Numpad4) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(Numpad5) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(Numpad6) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(Numpad7) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(Numpad8) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(Numpad9) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(NumpadAdd) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(NumpadBackspace) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(NumpadClear) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(NumpadClearEntry) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(NumpadComma) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(NumpadDecimal) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(NumpadDivide) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(NumpadEnter) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(NumpadEqual) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(NumpadMemoryAdd) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(NumpadMemoryClear) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(NumpadMemoryRecall) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(NumpadMemoryStore) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(NumpadMemorySubtract) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(NumpadMultiply) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(NumpadParenLeft) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(NumpadParenRight) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(NumpadSubtract) + +// Function section +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(Escape) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(F1) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(F2) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(F3) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(F4) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(F5) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(F6) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(F7) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(F8) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(F9) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(F10) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(F11) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(F12) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(F13) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(F14) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(F15) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(F16) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(F17) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(F18) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(F19) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(F20) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(F21) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(F22) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(F23) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(F24) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(Fn) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(FnLock) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(PrintScreen) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(ScrollLock) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(Pause) + +// Media keys +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(BrowserBack) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(BrowserFavorites) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(BrowserForward) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(BrowserHome) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(BrowserRefresh) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(BrowserSearch) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(BrowserStop) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(Eject) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(LaunchApp1) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(LaunchApp2) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(LaunchMail) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(MediaPlayPause) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(MediaSelect) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(MediaStop) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(MediaTrackNext) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(MediaTrackPrevious) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(Power) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(Sleep) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(VolumeDown) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(VolumeMute) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(VolumeUp) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(WakeUp) + +// Legacy Keys and Non-Standard Keys + +// Legacy modifier keys +// DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(Hyper) +// DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(Super) +// DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(Turbo) + +// Legacy process control keys +// DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(Abort) +// DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(Resume) +// DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(Suspend) + +// Legacy editing keys +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(Again) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(Copy) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(Cut) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(Find) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(Open) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(Paste) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(Props) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(Select) +DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(Undo) + +// International keyboards +// DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(Hiragana) +// DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME(Katakana) + +#undef DEFINE_PHYSICAL_KEY_CODE_NAME_WITH_SAME_NAME +#undef NS_DEFINE_PHYSICAL_KEY_CODE_NAME_INTERNAL diff --git a/dom/events/PointerEvent.cpp b/dom/events/PointerEvent.cpp new file mode 100644 index 0000000000..3acae988db --- /dev/null +++ b/dom/events/PointerEvent.cpp @@ -0,0 +1,298 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. + * + * Portions Copyright 2013 Microsoft Open Technologies, Inc. */ + +#include "mozilla/dom/MouseEventBinding.h" +#include "mozilla/dom/PointerEvent.h" +#include "mozilla/dom/PointerEventBinding.h" +#include "mozilla/dom/PointerEventHandler.h" +#include "mozilla/MouseEvents.h" +#include "nsContentUtils.h" +#include "prtime.h" + +namespace mozilla::dom { + +PointerEvent::PointerEvent(EventTarget* aOwner, nsPresContext* aPresContext, + WidgetPointerEvent* aEvent) + : MouseEvent(aOwner, aPresContext, + aEvent ? aEvent + : new WidgetPointerEvent(false, eVoidEvent, nullptr)) { + NS_ASSERTION(mEvent->mClass == ePointerEventClass, + "event type mismatch ePointerEventClass"); + + WidgetMouseEvent* mouseEvent = mEvent->AsMouseEvent(); + if (aEvent) { + mEventIsInternal = false; + } else { + mEventIsInternal = true; + mEvent->mRefPoint = LayoutDeviceIntPoint(0, 0); + mouseEvent->mInputSource = MouseEvent_Binding::MOZ_SOURCE_UNKNOWN; + } + // 5.2 Pointer Event types, for all pointer events, |detail| attribute SHOULD + // be 0. + mDetail = 0; +} + +JSObject* PointerEvent::WrapObjectInternal(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return PointerEvent_Binding::Wrap(aCx, this, aGivenProto); +} + +static uint16_t ConvertStringToPointerType(const nsAString& aPointerTypeArg) { + if (aPointerTypeArg.EqualsLiteral("mouse")) { + return MouseEvent_Binding::MOZ_SOURCE_MOUSE; + } + if (aPointerTypeArg.EqualsLiteral("pen")) { + return MouseEvent_Binding::MOZ_SOURCE_PEN; + } + if (aPointerTypeArg.EqualsLiteral("touch")) { + return MouseEvent_Binding::MOZ_SOURCE_TOUCH; + } + + return MouseEvent_Binding::MOZ_SOURCE_UNKNOWN; +} + +void ConvertPointerTypeToString(uint16_t aPointerTypeSrc, + nsAString& aPointerTypeDest) { + switch (aPointerTypeSrc) { + case MouseEvent_Binding::MOZ_SOURCE_MOUSE: + aPointerTypeDest.AssignLiteral("mouse"); + break; + case MouseEvent_Binding::MOZ_SOURCE_PEN: + aPointerTypeDest.AssignLiteral("pen"); + break; + case MouseEvent_Binding::MOZ_SOURCE_TOUCH: + aPointerTypeDest.AssignLiteral("touch"); + break; + default: + aPointerTypeDest.Truncate(); + break; + } +} + +// static +already_AddRefed<PointerEvent> PointerEvent::Constructor( + EventTarget* aOwner, const nsAString& aType, + const PointerEventInit& aParam) { + RefPtr<PointerEvent> e = new PointerEvent(aOwner, nullptr, nullptr); + bool trusted = e->Init(aOwner); + + e->InitMouseEvent(aType, aParam.mBubbles, aParam.mCancelable, aParam.mView, + aParam.mDetail, aParam.mScreenX, aParam.mScreenY, + aParam.mClientX, aParam.mClientY, false, false, false, + false, aParam.mButton, aParam.mRelatedTarget); + e->InitializeExtraMouseEventDictionaryMembers(aParam); + e->mPointerType = Some(aParam.mPointerType); + + WidgetPointerEvent* widgetEvent = e->mEvent->AsPointerEvent(); + widgetEvent->pointerId = aParam.mPointerId; + widgetEvent->mWidth = aParam.mWidth; + widgetEvent->mHeight = aParam.mHeight; + widgetEvent->mPressure = aParam.mPressure; + widgetEvent->tangentialPressure = aParam.mTangentialPressure; + widgetEvent->tiltX = aParam.mTiltX; + widgetEvent->tiltY = aParam.mTiltY; + widgetEvent->twist = aParam.mTwist; + widgetEvent->mInputSource = ConvertStringToPointerType(aParam.mPointerType); + widgetEvent->mIsPrimary = aParam.mIsPrimary; + widgetEvent->mButtons = aParam.mButtons; + + if (!aParam.mCoalescedEvents.IsEmpty()) { + e->mCoalescedEvents.AppendElements(aParam.mCoalescedEvents); + } + if (!aParam.mPredictedEvents.IsEmpty()) { + e->mPredictedEvents.AppendElements(aParam.mPredictedEvents); + } + e->SetTrusted(trusted); + e->SetComposed(aParam.mComposed); + return e.forget(); +} + +// static +already_AddRefed<PointerEvent> PointerEvent::Constructor( + const GlobalObject& aGlobal, const nsAString& aType, + const PointerEventInit& aParam) { + nsCOMPtr<EventTarget> owner = do_QueryInterface(aGlobal.GetAsSupports()); + return Constructor(owner, aType, aParam); +} + +NS_IMPL_CYCLE_COLLECTION_CLASS(PointerEvent) + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(PointerEvent, MouseEvent) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mCoalescedEvents) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mPredictedEvents) +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(PointerEvent, MouseEvent) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mCoalescedEvents) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPredictedEvents) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(PointerEvent) +NS_INTERFACE_MAP_END_INHERITING(MouseEvent) + +NS_IMPL_ADDREF_INHERITED(PointerEvent, MouseEvent) +NS_IMPL_RELEASE_INHERITED(PointerEvent, MouseEvent) + +void PointerEvent::GetPointerType(nsAString& aPointerType) { + if (mPointerType.isSome()) { + aPointerType = mPointerType.value(); + return; + } + + if (ShouldResistFingerprinting()) { + aPointerType.AssignLiteral("mouse"); + return; + } + + ConvertPointerTypeToString(mEvent->AsPointerEvent()->mInputSource, + aPointerType); +} + +int32_t PointerEvent::PointerId() { + return ShouldResistFingerprinting() + ? PointerEventHandler::GetSpoofedPointerIdForRFP() + : mEvent->AsPointerEvent()->pointerId; +} + +int32_t PointerEvent::Width() { + return ShouldResistFingerprinting() ? 1 : mEvent->AsPointerEvent()->mWidth; +} + +int32_t PointerEvent::Height() { + return ShouldResistFingerprinting() ? 1 : mEvent->AsPointerEvent()->mHeight; +} + +float PointerEvent::Pressure() { + if (mEvent->mMessage == ePointerUp || !ShouldResistFingerprinting()) { + return mEvent->AsPointerEvent()->mPressure; + } + + // According to [1], we should use 0.5 when it is in active buttons state and + // 0 otherwise for devices that don't support pressure. And a pointerup event + // always reports 0, so we don't need to spoof that. + // + // [1] https://www.w3.org/TR/pointerevents/#dom-pointerevent-pressure + float spoofedPressure = 0.0; + if (mEvent->AsPointerEvent()->mButtons) { + spoofedPressure = 0.5; + } + + return spoofedPressure; +} + +float PointerEvent::TangentialPressure() { + return ShouldResistFingerprinting() + ? 0 + : mEvent->AsPointerEvent()->tangentialPressure; +} + +int32_t PointerEvent::TiltX() { + return ShouldResistFingerprinting() ? 0 : mEvent->AsPointerEvent()->tiltX; +} + +int32_t PointerEvent::TiltY() { + return ShouldResistFingerprinting() ? 0 : mEvent->AsPointerEvent()->tiltY; +} + +int32_t PointerEvent::Twist() { + return ShouldResistFingerprinting() ? 0 : mEvent->AsPointerEvent()->twist; +} + +bool PointerEvent::IsPrimary() { return mEvent->AsPointerEvent()->mIsPrimary; } + +void PointerEvent::GetCoalescedEvents( + nsTArray<RefPtr<PointerEvent>>& aPointerEvents) { + WidgetPointerEvent* widgetEvent = mEvent->AsPointerEvent(); + if (mCoalescedEvents.IsEmpty() && widgetEvent && + widgetEvent->mCoalescedWidgetEvents && + !widgetEvent->mCoalescedWidgetEvents->mEvents.IsEmpty()) { + nsCOMPtr<EventTarget> owner = do_QueryInterface(mOwner); + for (WidgetPointerEvent& event : + widgetEvent->mCoalescedWidgetEvents->mEvents) { + RefPtr<PointerEvent> domEvent = + NS_NewDOMPointerEvent(owner, nullptr, &event); + + // The dom event is derived from an OS generated widget event. Setup + // mWidget and mPresContext since they are necessary to calculate + // offsetX / offsetY. + domEvent->mEvent->AsGUIEvent()->mWidget = widgetEvent->mWidget; + domEvent->mPresContext = mPresContext; + + // The coalesced widget mouse events shouldn't have been dispatched. + MOZ_ASSERT(!domEvent->mEvent->mTarget); + // The event target should be the same as the dispatched event's target. + domEvent->mEvent->mTarget = mEvent->mTarget; + + // JS could hold reference to dom events. We have to ask dom event to + // duplicate its private data to avoid the widget event is destroyed. + domEvent->DuplicatePrivateData(); + + // Setup mPresContext again after DuplicatePrivateData since it clears + // mPresContext. + domEvent->mPresContext = mPresContext; + mCoalescedEvents.AppendElement(domEvent); + } + } + if (mEvent->IsTrusted() && mEvent->mTarget) { + for (RefPtr<PointerEvent>& pointerEvent : mCoalescedEvents) { + // Only set event target when it's null. + if (!pointerEvent->mEvent->mTarget) { + pointerEvent->mEvent->mTarget = mEvent->mTarget; + } + } + } + aPointerEvents.AppendElements(mCoalescedEvents); +} + +void PointerEvent::GetPredictedEvents( + nsTArray<RefPtr<PointerEvent>>& aPointerEvents) { + // XXX Add support for native predicted events, bug 1550461 + if (mEvent->IsTrusted() && mEvent->mTarget) { + for (RefPtr<PointerEvent>& pointerEvent : mPredictedEvents) { + // Only set event target when it's null. + if (!pointerEvent->mEvent->mTarget) { + pointerEvent->mEvent->mTarget = mEvent->mTarget; + } + } + } + aPointerEvents.AppendElements(mPredictedEvents); +} + +bool PointerEvent::ShouldResistFingerprinting() { + // There are three simple situations we don't need to spoof this pointer + // event. + // 1. The pref privcy.resistFingerprinting' is false, we fast return here + // since we don't need to do any QI of following codes. + // 2. This event is generated by scripts. + // 3. This event is a mouse pointer event. + // We don't need to check for the system group since pointer events won't be + // dispatched to the system group. + if (!nsContentUtils::ShouldResistFingerprinting("Efficiency Check", + RFPTarget::PointerEvents) || + !mEvent->IsTrusted() || + mEvent->AsPointerEvent()->mInputSource == + MouseEvent_Binding::MOZ_SOURCE_MOUSE) { + return false; + } + + // Pref is checked above, so use true as fallback. + nsCOMPtr<Document> doc = GetDocument(); + return doc ? doc->ShouldResistFingerprinting(RFPTarget::PointerEvents) : true; +} + +} // namespace mozilla::dom + +using namespace mozilla; +using namespace mozilla::dom; + +already_AddRefed<PointerEvent> NS_NewDOMPointerEvent( + EventTarget* aOwner, nsPresContext* aPresContext, + WidgetPointerEvent* aEvent) { + RefPtr<PointerEvent> it = new PointerEvent(aOwner, aPresContext, aEvent); + return it.forget(); +} diff --git a/dom/events/PointerEvent.h b/dom/events/PointerEvent.h new file mode 100644 index 0000000000..652259bcd0 --- /dev/null +++ b/dom/events/PointerEvent.h @@ -0,0 +1,78 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. + * + * Portions Copyright 2013 Microsoft Open Technologies, Inc. */ + +#ifndef mozilla_dom_PointerEvent_h_ +#define mozilla_dom_PointerEvent_h_ + +#include "mozilla/dom/MouseEvent.h" +#include "mozilla/dom/PointerEventBinding.h" +#include "mozilla/Maybe.h" + +class nsPresContext; + +namespace mozilla::dom { + +struct PointerEventInit; + +class PointerEvent : public MouseEvent { + public: + PointerEvent(EventTarget* aOwner, nsPresContext* aPresContext, + WidgetPointerEvent* aEvent); + + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(PointerEvent, MouseEvent) + + virtual JSObject* WrapObjectInternal( + JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override; + + static already_AddRefed<PointerEvent> Constructor( + const GlobalObject& aGlobal, const nsAString& aType, + const PointerEventInit& aParam); + + static already_AddRefed<PointerEvent> Constructor( + EventTarget* aOwner, const nsAString& aType, + const PointerEventInit& aParam); + + int32_t PointerId(); + int32_t Width(); + int32_t Height(); + float Pressure(); + float TangentialPressure(); + int32_t TiltX(); + int32_t TiltY(); + int32_t Twist(); + bool IsPrimary(); + void GetPointerType(nsAString& aPointerType); + void GetCoalescedEvents(nsTArray<RefPtr<PointerEvent>>& aPointerEvents); + void GetPredictedEvents(nsTArray<RefPtr<PointerEvent>>& aPointerEvents); + + protected: + ~PointerEvent() = default; + + private: + // This method returns the boolean to indicate whether spoofing pointer + // event for fingerprinting resistance. + bool ShouldResistFingerprinting(); + + nsTArray<RefPtr<PointerEvent>> mCoalescedEvents; + nsTArray<RefPtr<PointerEvent>> mPredictedEvents; + + // This is used to store the pointerType assigned from constructor. + Maybe<nsString> mPointerType; +}; + +void ConvertPointerTypeToString(uint16_t aPointerTypeSrc, + nsAString& aPointerTypeDest); + +} // namespace mozilla::dom + +already_AddRefed<mozilla::dom::PointerEvent> NS_NewDOMPointerEvent( + mozilla::dom::EventTarget* aOwner, nsPresContext* aPresContext, + mozilla::WidgetPointerEvent* aEvent); + +#endif // mozilla_dom_PointerEvent_h_ diff --git a/dom/events/PointerEventHandler.cpp b/dom/events/PointerEventHandler.cpp new file mode 100644 index 0000000000..73e4be7155 --- /dev/null +++ b/dom/events/PointerEventHandler.cpp @@ -0,0 +1,800 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#include "PointerEventHandler.h" +#include "nsIContentInlines.h" +#include "nsIFrame.h" +#include "PointerEvent.h" +#include "PointerLockManager.h" +#include "nsRFPService.h" +#include "mozilla/PresShell.h" +#include "mozilla/StaticPrefs_dom.h" +#include "mozilla/dom/BrowserChild.h" +#include "mozilla/dom/BrowserParent.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/MouseEventBinding.h" + +namespace mozilla { + +using namespace dom; + +Maybe<int32_t> PointerEventHandler::sSpoofedPointerId; + +// Keeps a map between pointerId and element that currently capturing pointer +// with such pointerId. If pointerId is absent in this map then nobody is +// capturing it. Additionally keep information about pending capturing content. +static nsClassHashtable<nsUint32HashKey, PointerCaptureInfo>* + sPointerCaptureList; + +// Keeps information about pointers such as pointerId, activeState, pointerType, +// primaryState +static nsClassHashtable<nsUint32HashKey, PointerInfo>* sActivePointersIds; + +// Keeps track of which BrowserParent requested pointer capture for a pointer +// id. +static nsTHashMap<nsUint32HashKey, BrowserParent*>* + sPointerCaptureRemoteTargetTable = nullptr; + +/* static */ +void PointerEventHandler::InitializeStatics() { + MOZ_ASSERT(!sPointerCaptureList, "InitializeStatics called multiple times!"); + sPointerCaptureList = + new nsClassHashtable<nsUint32HashKey, PointerCaptureInfo>; + sActivePointersIds = new nsClassHashtable<nsUint32HashKey, PointerInfo>; + if (XRE_IsParentProcess()) { + sPointerCaptureRemoteTargetTable = + new nsTHashMap<nsUint32HashKey, BrowserParent*>; + } +} + +/* static */ +void PointerEventHandler::ReleaseStatics() { + MOZ_ASSERT(sPointerCaptureList, "ReleaseStatics called without Initialize!"); + delete sPointerCaptureList; + sPointerCaptureList = nullptr; + delete sActivePointersIds; + sActivePointersIds = nullptr; + if (sPointerCaptureRemoteTargetTable) { + MOZ_ASSERT(XRE_IsParentProcess()); + delete sPointerCaptureRemoteTargetTable; + sPointerCaptureRemoteTargetTable = nullptr; + } +} + +/* static */ +bool PointerEventHandler::IsPointerEventImplicitCaptureForTouchEnabled() { + return StaticPrefs::dom_w3c_pointer_events_implicit_capture(); +} + +/* static */ +void PointerEventHandler::UpdateActivePointerState(WidgetMouseEvent* aEvent, + nsIContent* aTargetContent) { + if (!aEvent) { + return; + } + switch (aEvent->mMessage) { + case eMouseEnterIntoWidget: + // In this case we have to know information about available mouse pointers + sActivePointersIds->InsertOrUpdate( + aEvent->pointerId, + MakeUnique<PointerInfo>(false, aEvent->mInputSource, true, nullptr)); + + MaybeCacheSpoofedPointerID(aEvent->mInputSource, aEvent->pointerId); + break; + case ePointerDown: + // In this case we switch pointer to active state + if (WidgetPointerEvent* pointerEvent = aEvent->AsPointerEvent()) { + // XXXedgar, test could possibly synthesize a mousedown event on a + // coordinate outside the browser window and cause aTargetContent to be + // nullptr, not sure if this also happens on real usage. + sActivePointersIds->InsertOrUpdate( + pointerEvent->pointerId, + MakeUnique<PointerInfo>( + true, pointerEvent->mInputSource, pointerEvent->mIsPrimary, + aTargetContent ? aTargetContent->OwnerDoc() : nullptr)); + MaybeCacheSpoofedPointerID(pointerEvent->mInputSource, + pointerEvent->pointerId); + } + break; + case ePointerCancel: + // pointercancel means a pointer is unlikely to continue to produce + // pointer events. In that case, we should turn off active state or remove + // the pointer from active pointers. + case ePointerUp: + // In this case we remove information about pointer or turn off active + // state + if (WidgetPointerEvent* pointerEvent = aEvent->AsPointerEvent()) { + if (pointerEvent->mInputSource != + MouseEvent_Binding::MOZ_SOURCE_TOUCH) { + sActivePointersIds->InsertOrUpdate( + pointerEvent->pointerId, + MakeUnique<PointerInfo>(false, pointerEvent->mInputSource, + pointerEvent->mIsPrimary, nullptr)); + } else { + sActivePointersIds->Remove(pointerEvent->pointerId); + } + } + break; + case eMouseExitFromWidget: + // In this case we have to remove information about disappeared mouse + // pointers + sActivePointersIds->Remove(aEvent->pointerId); + break; + default: + MOZ_ASSERT_UNREACHABLE("event has invalid type"); + break; + } +} + +/* static */ +void PointerEventHandler::RequestPointerCaptureById(uint32_t aPointerId, + Element* aElement) { + SetPointerCaptureById(aPointerId, aElement); + + if (BrowserChild* browserChild = + BrowserChild::GetFrom(aElement->OwnerDoc()->GetDocShell())) { + browserChild->SendRequestPointerCapture( + aPointerId, + [aPointerId](bool aSuccess) { + if (!aSuccess) { + PointerEventHandler::ReleasePointerCaptureById(aPointerId); + } + }, + [](mozilla::ipc::ResponseRejectReason) {}); + } +} + +/* static */ +void PointerEventHandler::SetPointerCaptureById(uint32_t aPointerId, + Element* aElement) { + MOZ_ASSERT(aElement); + sPointerCaptureList->WithEntryHandle(aPointerId, [&](auto&& entry) { + if (entry) { + entry.Data()->mPendingElement = aElement; + } else { + entry.Insert(MakeUnique<PointerCaptureInfo>(aElement)); + } + }); +} + +/* static */ +PointerCaptureInfo* PointerEventHandler::GetPointerCaptureInfo( + uint32_t aPointerId) { + PointerCaptureInfo* pointerCaptureInfo = nullptr; + sPointerCaptureList->Get(aPointerId, &pointerCaptureInfo); + return pointerCaptureInfo; +} + +/* static */ +void PointerEventHandler::ReleasePointerCaptureById(uint32_t aPointerId) { + PointerCaptureInfo* pointerCaptureInfo = GetPointerCaptureInfo(aPointerId); + if (pointerCaptureInfo) { + if (Element* pendingElement = pointerCaptureInfo->mPendingElement) { + if (BrowserChild* browserChild = BrowserChild::GetFrom( + pendingElement->OwnerDoc()->GetDocShell())) { + browserChild->SendReleasePointerCapture(aPointerId); + } + } + pointerCaptureInfo->mPendingElement = nullptr; + } +} + +/* static */ +void PointerEventHandler::ReleaseAllPointerCapture() { + for (const auto& entry : *sPointerCaptureList) { + PointerCaptureInfo* data = entry.GetWeak(); + if (data && data->mPendingElement) { + ReleasePointerCaptureById(entry.GetKey()); + } + } +} + +/* static */ +bool PointerEventHandler::SetPointerCaptureRemoteTarget( + uint32_t aPointerId, dom::BrowserParent* aBrowserParent) { + MOZ_ASSERT(XRE_IsParentProcess()); + MOZ_ASSERT(sPointerCaptureRemoteTargetTable); + MOZ_ASSERT(aBrowserParent); + + if (PointerLockManager::GetLockedRemoteTarget()) { + return false; + } + + BrowserParent* currentRemoteTarget = + PointerEventHandler::GetPointerCapturingRemoteTarget(aPointerId); + if (currentRemoteTarget && currentRemoteTarget != aBrowserParent) { + return false; + } + + sPointerCaptureRemoteTargetTable->InsertOrUpdate(aPointerId, aBrowserParent); + return true; +} + +/* static */ +void PointerEventHandler::ReleasePointerCaptureRemoteTarget( + BrowserParent* aBrowserParent) { + MOZ_ASSERT(XRE_IsParentProcess()); + MOZ_ASSERT(sPointerCaptureRemoteTargetTable); + MOZ_ASSERT(aBrowserParent); + + sPointerCaptureRemoteTargetTable->RemoveIf([aBrowserParent]( + const auto& iter) { + BrowserParent* browserParent = iter.Data(); + MOZ_ASSERT(browserParent, "Null BrowserParent in pointer captured table?"); + + return aBrowserParent == browserParent; + }); +} + +/* static */ +void PointerEventHandler::ReleasePointerCaptureRemoteTarget( + uint32_t aPointerId) { + MOZ_ASSERT(XRE_IsParentProcess()); + MOZ_ASSERT(sPointerCaptureRemoteTargetTable); + + sPointerCaptureRemoteTargetTable->Remove(aPointerId); +} + +/* static */ +BrowserParent* PointerEventHandler::GetPointerCapturingRemoteTarget( + uint32_t aPointerId) { + MOZ_ASSERT(XRE_IsParentProcess()); + MOZ_ASSERT(sPointerCaptureRemoteTargetTable); + + return sPointerCaptureRemoteTargetTable->Get(aPointerId); +} + +/* static */ +void PointerEventHandler::ReleaseAllPointerCaptureRemoteTarget() { + MOZ_ASSERT(XRE_IsParentProcess()); + MOZ_ASSERT(sPointerCaptureRemoteTargetTable); + + for (auto iter = sPointerCaptureRemoteTargetTable->Iter(); !iter.Done(); + iter.Next()) { + BrowserParent* browserParent = iter.Data(); + MOZ_ASSERT(browserParent, "Null BrowserParent in pointer captured table?"); + + Unused << browserParent->SendReleaseAllPointerCapture(); + iter.Remove(); + } +} + +/* static */ +const PointerInfo* PointerEventHandler::GetPointerInfo(uint32_t aPointerId) { + return sActivePointersIds->Get(aPointerId); +} + +/* static */ +void PointerEventHandler::MaybeProcessPointerCapture(WidgetGUIEvent* aEvent) { + switch (aEvent->mClass) { + case eMouseEventClass: + ProcessPointerCaptureForMouse(aEvent->AsMouseEvent()); + break; + case eTouchEventClass: + ProcessPointerCaptureForTouch(aEvent->AsTouchEvent()); + break; + default: + break; + } +} + +/* static */ +void PointerEventHandler::ProcessPointerCaptureForMouse( + WidgetMouseEvent* aEvent) { + if (!ShouldGeneratePointerEventFromMouse(aEvent)) { + return; + } + + PointerCaptureInfo* info = GetPointerCaptureInfo(aEvent->pointerId); + if (!info || info->mPendingElement == info->mOverrideElement) { + return; + } + WidgetPointerEvent localEvent(*aEvent); + InitPointerEventFromMouse(&localEvent, aEvent, eVoidEvent); + CheckPointerCaptureState(&localEvent); +} + +/* static */ +void PointerEventHandler::ProcessPointerCaptureForTouch( + WidgetTouchEvent* aEvent) { + if (!ShouldGeneratePointerEventFromTouch(aEvent)) { + return; + } + + for (uint32_t i = 0; i < aEvent->mTouches.Length(); ++i) { + Touch* touch = aEvent->mTouches[i]; + if (!TouchManager::ShouldConvertTouchToPointer(touch, aEvent)) { + continue; + } + PointerCaptureInfo* info = GetPointerCaptureInfo(touch->Identifier()); + if (!info || info->mPendingElement == info->mOverrideElement) { + continue; + } + WidgetPointerEvent event(aEvent->IsTrusted(), eVoidEvent, aEvent->mWidget); + InitPointerEventFromTouch(event, *aEvent, *touch, i == 0); + CheckPointerCaptureState(&event); + } +} + +/* static */ +void PointerEventHandler::CheckPointerCaptureState(WidgetPointerEvent* aEvent) { + // Handle pending pointer capture before any pointer events except + // gotpointercapture / lostpointercapture. + if (!aEvent) { + return; + } + MOZ_ASSERT(aEvent->mClass == ePointerEventClass); + + PointerCaptureInfo* captureInfo = GetPointerCaptureInfo(aEvent->pointerId); + + // When fingerprinting resistance is enabled, we need to map other pointer + // ids into the spoofed one. We don't have to do the mapping if the capture + // info exists for the non-spoofed pointer id because of we won't allow + // content to set pointer capture other than the spoofed one. Thus, it must be + // from chrome if the capture info exists in this case. And we don't have to + // do anything if the pointer id is the same as the spoofed one. + if (nsContentUtils::ShouldResistFingerprinting("Efficiency Check", + RFPTarget::PointerEvents) && + aEvent->pointerId != (uint32_t)GetSpoofedPointerIdForRFP() && + !captureInfo) { + PointerCaptureInfo* spoofedCaptureInfo = + GetPointerCaptureInfo(GetSpoofedPointerIdForRFP()); + + // We need to check the target element's document should resist + // fingerprinting. If not, we don't need to send a capture event + // since the capture info of the original pointer id doesn't exist + // in this case. + if (!spoofedCaptureInfo || !spoofedCaptureInfo->mPendingElement || + !spoofedCaptureInfo->mPendingElement->OwnerDoc() + ->ShouldResistFingerprinting(RFPTarget::PointerEvents)) { + return; + } + + captureInfo = spoofedCaptureInfo; + } + + if (!captureInfo || + captureInfo->mPendingElement == captureInfo->mOverrideElement) { + return; + } + + RefPtr<Element> overrideElement = captureInfo->mOverrideElement; + RefPtr<Element> pendingElement = captureInfo->mPendingElement; + + // Update captureInfo before dispatching event since sPointerCaptureList may + // be changed in the pointer event listener. + captureInfo->mOverrideElement = captureInfo->mPendingElement; + if (captureInfo->Empty()) { + sPointerCaptureList->Remove(aEvent->pointerId); + } + + if (overrideElement) { + DispatchGotOrLostPointerCaptureEvent(/* aIsGotCapture */ false, aEvent, + overrideElement); + } + if (pendingElement) { + DispatchGotOrLostPointerCaptureEvent(/* aIsGotCapture */ true, aEvent, + pendingElement); + } +} + +/* static */ +void PointerEventHandler::ImplicitlyCapturePointer(nsIFrame* aFrame, + WidgetEvent* aEvent) { + MOZ_ASSERT(aEvent->mMessage == ePointerDown); + if (!aFrame || !IsPointerEventImplicitCaptureForTouchEnabled()) { + return; + } + WidgetPointerEvent* pointerEvent = aEvent->AsPointerEvent(); + NS_WARNING_ASSERTION(pointerEvent, + "Call ImplicitlyCapturePointer with non-pointer event"); + if (!pointerEvent->mFromTouchEvent) { + // We only implicitly capture the pointer for touch device. + return; + } + nsCOMPtr<nsIContent> target; + aFrame->GetContentForEvent(aEvent, getter_AddRefs(target)); + while (target && !target->IsElement()) { + target = target->GetParent(); + } + if (NS_WARN_IF(!target)) { + return; + } + RequestPointerCaptureById(pointerEvent->pointerId, target->AsElement()); +} + +/* static */ +void PointerEventHandler::ImplicitlyReleasePointerCapture(WidgetEvent* aEvent) { + MOZ_ASSERT(aEvent); + if (aEvent->mMessage != ePointerUp && aEvent->mMessage != ePointerCancel) { + return; + } + WidgetPointerEvent* pointerEvent = aEvent->AsPointerEvent(); + ReleasePointerCaptureById(pointerEvent->pointerId); + CheckPointerCaptureState(pointerEvent); +} + +/* static */ +Element* PointerEventHandler::GetPointerCapturingElement(uint32_t aPointerId) { + PointerCaptureInfo* pointerCaptureInfo = GetPointerCaptureInfo(aPointerId); + if (pointerCaptureInfo) { + return pointerCaptureInfo->mOverrideElement; + } + return nullptr; +} + +/* static */ +Element* PointerEventHandler::GetPointerCapturingElement( + WidgetGUIEvent* aEvent) { + if ((aEvent->mClass != ePointerEventClass && + aEvent->mClass != eMouseEventClass) || + aEvent->mMessage == ePointerDown || aEvent->mMessage == eMouseDown) { + // Pointer capture should only be applied to all pointer events and mouse + // events except ePointerDown and eMouseDown; + return nullptr; + } + + WidgetMouseEvent* mouseEvent = aEvent->AsMouseEvent(); + if (!mouseEvent) { + return nullptr; + } + return GetPointerCapturingElement(mouseEvent->pointerId); +} + +/* static */ +void PointerEventHandler::ReleaseIfCaptureByDescendant(nsIContent* aContent) { + // We should check that aChild does not contain pointer capturing elements. + // If it does we should release the pointer capture for the elements. + for (const auto& entry : *sPointerCaptureList) { + PointerCaptureInfo* data = entry.GetWeak(); + if (data && data->mPendingElement && + data->mPendingElement->IsInclusiveDescendantOf(aContent)) { + ReleasePointerCaptureById(entry.GetKey()); + } + } +} + +/* static */ +void PointerEventHandler::PreHandlePointerEventsPreventDefault( + WidgetPointerEvent* aPointerEvent, WidgetGUIEvent* aMouseOrTouchEvent) { + if (!aPointerEvent->mIsPrimary || aPointerEvent->mMessage == ePointerDown) { + return; + } + PointerInfo* pointerInfo = nullptr; + if (!sActivePointersIds->Get(aPointerEvent->pointerId, &pointerInfo) || + !pointerInfo) { + // The PointerInfo for active pointer should be added for normal cases. But + // in some cases, we may receive mouse events before adding PointerInfo in + // sActivePointersIds. (e.g. receive mousemove before + // eMouseEnterIntoWidget). In these cases, we could ignore them because they + // are not the events between a DefaultPrevented pointerdown and the + // corresponding pointerup. + return; + } + if (!pointerInfo->mPreventMouseEventByContent) { + return; + } + aMouseOrTouchEvent->PreventDefault(false); + aMouseOrTouchEvent->mFlags.mOnlyChromeDispatch = true; + if (aPointerEvent->mMessage == ePointerUp) { + pointerInfo->mPreventMouseEventByContent = false; + } +} + +/* static */ +void PointerEventHandler::PostHandlePointerEventsPreventDefault( + WidgetPointerEvent* aPointerEvent, WidgetGUIEvent* aMouseOrTouchEvent) { + if (!aPointerEvent->mIsPrimary || aPointerEvent->mMessage != ePointerDown || + !aPointerEvent->DefaultPreventedByContent()) { + return; + } + PointerInfo* pointerInfo = nullptr; + if (!sActivePointersIds->Get(aPointerEvent->pointerId, &pointerInfo) || + !pointerInfo) { + // We already added the PointerInfo for active pointer when + // PresShell::HandleEvent handling pointerdown event. +#ifdef DEBUG + MOZ_CRASH("Got ePointerDown w/o active pointer info!!"); +#endif // #ifdef DEBUG + return; + } + // PreventDefault only applied for active pointers. + if (!pointerInfo->mActiveState) { + return; + } + aMouseOrTouchEvent->PreventDefault(false); + aMouseOrTouchEvent->mFlags.mOnlyChromeDispatch = true; + pointerInfo->mPreventMouseEventByContent = true; +} + +/* static */ +void PointerEventHandler::InitPointerEventFromMouse( + WidgetPointerEvent* aPointerEvent, WidgetMouseEvent* aMouseEvent, + EventMessage aMessage) { + MOZ_ASSERT(aPointerEvent); + MOZ_ASSERT(aMouseEvent); + aPointerEvent->pointerId = aMouseEvent->pointerId; + aPointerEvent->mInputSource = aMouseEvent->mInputSource; + aPointerEvent->mMessage = aMessage; + aPointerEvent->mButton = aMouseEvent->mMessage == eMouseMove + ? MouseButton::eNotPressed + : aMouseEvent->mButton; + + aPointerEvent->mButtons = aMouseEvent->mButtons; + aPointerEvent->mPressure = + aPointerEvent->mButtons + ? aMouseEvent->mPressure ? aMouseEvent->mPressure : 0.5f + : 0.0f; +} + +/* static */ +void PointerEventHandler::InitPointerEventFromTouch( + WidgetPointerEvent& aPointerEvent, const WidgetTouchEvent& aTouchEvent, + const mozilla::dom::Touch& aTouch, bool aIsPrimary) { + // Use mButton/mButtons only when mButton got a value (from pen input) + int16_t button = aTouchEvent.mMessage == eTouchMove ? MouseButton::eNotPressed + : aTouchEvent.mButton != MouseButton::eNotPressed + ? aTouchEvent.mButton + : MouseButton::ePrimary; + int16_t buttons = aTouchEvent.mMessage == eTouchEnd + ? MouseButtonsFlag::eNoButtons + : aTouchEvent.mButton != MouseButton::eNotPressed + ? aTouchEvent.mButtons + : MouseButtonsFlag::ePrimaryFlag; + + aPointerEvent.mIsPrimary = aIsPrimary; + aPointerEvent.pointerId = aTouch.Identifier(); + aPointerEvent.mRefPoint = aTouch.mRefPoint; + aPointerEvent.mModifiers = aTouchEvent.mModifiers; + aPointerEvent.mWidth = aTouch.RadiusX(CallerType::System); + aPointerEvent.mHeight = aTouch.RadiusY(CallerType::System); + aPointerEvent.tiltX = aTouch.tiltX; + aPointerEvent.tiltY = aTouch.tiltY; + aPointerEvent.twist = aTouch.twist; + aPointerEvent.mTimeStamp = aTouchEvent.mTimeStamp; + aPointerEvent.mFlags = aTouchEvent.mFlags; + aPointerEvent.mButton = button; + aPointerEvent.mButtons = buttons; + aPointerEvent.mInputSource = aTouchEvent.mInputSource; + aPointerEvent.mFromTouchEvent = true; + aPointerEvent.mPressure = aTouch.mForce; +} + +/* static */ +void PointerEventHandler::DispatchPointerFromMouseOrTouch( + PresShell* aShell, nsIFrame* aFrame, nsIContent* aContent, + WidgetGUIEvent* aEvent, bool aDontRetargetEvents, nsEventStatus* aStatus, + nsIContent** aTargetContent) { + MOZ_ASSERT(aFrame || aContent); + MOZ_ASSERT(aEvent); + + EventMessage pointerMessage = eVoidEvent; + if (aEvent->mClass == eMouseEventClass) { + WidgetMouseEvent* mouseEvent = aEvent->AsMouseEvent(); + // Don't dispatch pointer events caused by a mouse when simulating touch + // devices in RDM. + Document* doc = aShell->GetDocument(); + if (!doc) { + return; + } + + BrowsingContext* bc = doc->GetBrowsingContext(); + if (bc && bc->TouchEventsOverride() == TouchEventsOverride::Enabled && + bc->InRDMPane()) { + return; + } + + // 1. If it is not mouse then it is likely will come as touch event + // 2. We don't synthesize pointer events for those events that are not + // dispatched to DOM. + if (!mouseEvent->convertToPointer || + !aEvent->IsAllowedToDispatchDOMEvent()) { + return; + } + + switch (mouseEvent->mMessage) { + case eMouseMove: + pointerMessage = ePointerMove; + break; + case eMouseUp: + pointerMessage = mouseEvent->mButtons ? ePointerMove : ePointerUp; + break; + case eMouseDown: + pointerMessage = + mouseEvent->mButtons & ~nsContentUtils::GetButtonsFlagForButton( + mouseEvent->mButton) + ? ePointerMove + : ePointerDown; + break; + default: + return; + } + + WidgetPointerEvent event(*mouseEvent); + InitPointerEventFromMouse(&event, mouseEvent, pointerMessage); + event.convertToPointer = mouseEvent->convertToPointer = false; + RefPtr<PresShell> shell(aShell); + if (!aFrame) { + shell = PresShell::GetShellForEventTarget(nullptr, aContent); + if (!shell) { + return; + } + } + PreHandlePointerEventsPreventDefault(&event, aEvent); + // Dispatch pointer event to the same target which is found by the + // corresponding mouse event. + shell->HandleEventWithTarget(&event, aFrame, aContent, aStatus, true, + aTargetContent); + PostHandlePointerEventsPreventDefault(&event, aEvent); + } else if (aEvent->mClass == eTouchEventClass) { + WidgetTouchEvent* touchEvent = aEvent->AsTouchEvent(); + // loop over all touches and dispatch pointer events on each touch + // copy the event + switch (touchEvent->mMessage) { + case eTouchMove: + pointerMessage = ePointerMove; + break; + case eTouchEnd: + pointerMessage = ePointerUp; + break; + case eTouchStart: + pointerMessage = ePointerDown; + break; + case eTouchCancel: + case eTouchPointerCancel: + pointerMessage = ePointerCancel; + break; + default: + return; + } + + RefPtr<PresShell> shell(aShell); + for (uint32_t i = 0; i < touchEvent->mTouches.Length(); ++i) { + Touch* touch = touchEvent->mTouches[i]; + if (!TouchManager::ShouldConvertTouchToPointer(touch, touchEvent)) { + continue; + } + + WidgetPointerEvent event(touchEvent->IsTrusted(), pointerMessage, + touchEvent->mWidget); + + InitPointerEventFromTouch(event, *touchEvent, *touch, i == 0); + event.convertToPointer = touch->convertToPointer = false; + event.mCoalescedWidgetEvents = touch->mCoalescedWidgetEvents; + if (aEvent->mMessage == eTouchStart) { + // We already did hit test for touchstart in PresShell. We should + // dispatch pointerdown to the same target as touchstart. + nsCOMPtr<nsIContent> content = + nsIContent::FromEventTargetOrNull(touch->mTarget); + if (!content) { + continue; + } + + nsIFrame* frame = content->GetPrimaryFrame(); + shell = PresShell::GetShellForEventTarget(frame, content); + if (!shell) { + continue; + } + + PreHandlePointerEventsPreventDefault(&event, aEvent); + shell->HandleEventWithTarget(&event, frame, content, aStatus, true, + nullptr); + PostHandlePointerEventsPreventDefault(&event, aEvent); + } else { + // We didn't hit test for other touch events. Spec doesn't mention that + // all pointer events should be dispatched to the same target as their + // corresponding touch events. Call PresShell::HandleEvent so that we do + // hit test for pointer events. + PreHandlePointerEventsPreventDefault(&event, aEvent); + shell->HandleEvent(aFrame, &event, aDontRetargetEvents, aStatus); + PostHandlePointerEventsPreventDefault(&event, aEvent); + } + } + } +} + +/* static */ +void PointerEventHandler::NotifyDestroyPresContext( + nsPresContext* aPresContext) { + // Clean up pointer capture info + for (auto iter = sPointerCaptureList->Iter(); !iter.Done(); iter.Next()) { + PointerCaptureInfo* data = iter.UserData(); + MOZ_ASSERT(data, "how could we have a null PointerCaptureInfo here?"); + if (data->mPendingElement && + data->mPendingElement->GetPresContext(Element::eForComposedDoc) == + aPresContext) { + data->mPendingElement = nullptr; + } + if (data->mOverrideElement && + data->mOverrideElement->GetPresContext(Element::eForComposedDoc) == + aPresContext) { + data->mOverrideElement = nullptr; + } + if (data->Empty()) { + iter.Remove(); + } + } +} + +bool PointerEventHandler::IsDragAndDropEnabled(WidgetMouseEvent& aEvent) { +#ifdef XP_WIN + if (StaticPrefs::dom_w3c_pointer_events_dispatch_by_pointer_messages()) { + // WM_POINTER does not support drag and drop, see bug 1692277 + return (aEvent.mInputSource != dom::MouseEvent_Binding::MOZ_SOURCE_PEN && + aEvent.mReason != WidgetMouseEvent::eSynthesized); // bug 1692151 + } +#endif + return true; +} + +/* static */ +uint16_t PointerEventHandler::GetPointerType(uint32_t aPointerId) { + PointerInfo* pointerInfo = nullptr; + if (sActivePointersIds->Get(aPointerId, &pointerInfo) && pointerInfo) { + return pointerInfo->mPointerType; + } + return MouseEvent_Binding::MOZ_SOURCE_UNKNOWN; +} + +/* static */ +bool PointerEventHandler::GetPointerPrimaryState(uint32_t aPointerId) { + PointerInfo* pointerInfo = nullptr; + if (sActivePointersIds->Get(aPointerId, &pointerInfo) && pointerInfo) { + return pointerInfo->mPrimaryState; + } + return false; +} + +/* static */ +void PointerEventHandler::DispatchGotOrLostPointerCaptureEvent( + bool aIsGotCapture, const WidgetPointerEvent* aPointerEvent, + Element* aCaptureTarget) { + Document* targetDoc = aCaptureTarget->OwnerDoc(); + RefPtr<PresShell> presShell = targetDoc->GetPresShell(); + if (NS_WARN_IF(!presShell || presShell->IsDestroying())) { + return; + } + + if (!aIsGotCapture && !aCaptureTarget->IsInComposedDoc()) { + // If the capturing element was removed from the DOM tree, fire + // ePointerLostCapture at the document. + PointerEventInit init; + init.mPointerId = aPointerEvent->pointerId; + init.mBubbles = true; + init.mComposed = true; + ConvertPointerTypeToString(aPointerEvent->mInputSource, init.mPointerType); + init.mIsPrimary = aPointerEvent->mIsPrimary; + RefPtr<PointerEvent> event; + event = PointerEvent::Constructor(aCaptureTarget, u"lostpointercapture"_ns, + init); + targetDoc->DispatchEvent(*event); + return; + } + nsEventStatus status = nsEventStatus_eIgnore; + WidgetPointerEvent localEvent( + aPointerEvent->IsTrusted(), + aIsGotCapture ? ePointerGotCapture : ePointerLostCapture, + aPointerEvent->mWidget); + + localEvent.AssignPointerEventData(*aPointerEvent, true); + DebugOnly<nsresult> rv = presShell->HandleEventWithTarget( + &localEvent, aCaptureTarget->GetPrimaryFrame(), aCaptureTarget, &status); + + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "DispatchGotOrLostPointerCaptureEvent failed"); +} + +/* static */ +void PointerEventHandler::MaybeCacheSpoofedPointerID(uint16_t aInputSource, + uint32_t aPointerId) { + if (sSpoofedPointerId.isSome() || aInputSource != SPOOFED_POINTER_INTERFACE) { + return; + } + + sSpoofedPointerId.emplace(aPointerId); +} + +} // namespace mozilla diff --git a/dom/events/PointerEventHandler.h b/dom/events/PointerEventHandler.h new file mode 100644 index 0000000000..8e7aed1717 --- /dev/null +++ b/dom/events/PointerEventHandler.h @@ -0,0 +1,237 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#ifndef mozilla_PointerEventHandler_h +#define mozilla_PointerEventHandler_h + +#include "mozilla/EventForwards.h" +#include "mozilla/MouseEvents.h" +#include "mozilla/TouchEvents.h" +#include "mozilla/WeakPtr.h" + +// XXX Avoid including this here by moving function bodies to the cpp file +#include "mozilla/dom/Document.h" +#include "mozilla/dom/Element.h" + +class nsIFrame; +class nsIContent; +class nsPresContext; + +namespace mozilla { + +class PresShell; + +namespace dom { +class BrowserParent; +class Document; +class Element; +}; // namespace dom + +class PointerCaptureInfo final { + public: + RefPtr<dom::Element> mPendingElement; + RefPtr<dom::Element> mOverrideElement; + + explicit PointerCaptureInfo(dom::Element* aPendingElement) + : mPendingElement(aPendingElement) { + MOZ_COUNT_CTOR(PointerCaptureInfo); + } + + MOZ_COUNTED_DTOR(PointerCaptureInfo) + + bool Empty() { return !(mPendingElement || mOverrideElement); } +}; + +class PointerInfo final { + public: + uint16_t mPointerType; + bool mActiveState; + bool mPrimaryState; + bool mPreventMouseEventByContent; + WeakPtr<dom::Document> mActiveDocument; + explicit PointerInfo(bool aActiveState, uint16_t aPointerType, + bool aPrimaryState, dom::Document* aActiveDocument) + : mPointerType(aPointerType), + mActiveState(aActiveState), + mPrimaryState(aPrimaryState), + mPreventMouseEventByContent(false), + mActiveDocument(aActiveDocument) {} +}; + +class PointerEventHandler final { + public: + // Called in nsLayoutStatics::Initialize/Shutdown to initialize pointer event + // related static variables. + static void InitializeStatics(); + static void ReleaseStatics(); + + // Return the preference value of implicit capture. + static bool IsPointerEventImplicitCaptureForTouchEnabled(); + + // Called in ESM::PreHandleEvent to update current active pointers in a hash + // table. + static void UpdateActivePointerState(WidgetMouseEvent* aEvent, + nsIContent* aTargetContent = nullptr); + + // Request/release pointer capture of the specified pointer by the element. + static void RequestPointerCaptureById(uint32_t aPointerId, + dom::Element* aElement); + static void ReleasePointerCaptureById(uint32_t aPointerId); + static void ReleaseAllPointerCapture(); + + // Set/release pointer capture of the specified pointer by the remote target. + // Should only be called in parent process. + static bool SetPointerCaptureRemoteTarget(uint32_t aPointerId, + dom::BrowserParent* aBrowserParent); + static void ReleasePointerCaptureRemoteTarget( + dom::BrowserParent* aBrowserParent); + static void ReleasePointerCaptureRemoteTarget(uint32_t aPointerId); + static void ReleaseAllPointerCaptureRemoteTarget(); + + // Get the pointer capturing remote target of the specified pointer. + static dom::BrowserParent* GetPointerCapturingRemoteTarget( + uint32_t aPointerId); + + // Get the pointer captured info of the specified pointer. + static PointerCaptureInfo* GetPointerCaptureInfo(uint32_t aPointerId); + + // Return the PointerInfo if the pointer with aPointerId is situated in device + // , nullptr otherwise. + static const PointerInfo* GetPointerInfo(uint32_t aPointerId); + + // CheckPointerCaptureState checks cases, when got/lostpointercapture events + // should be fired. + MOZ_CAN_RUN_SCRIPT + static void MaybeProcessPointerCapture(WidgetGUIEvent* aEvent); + MOZ_CAN_RUN_SCRIPT + static void ProcessPointerCaptureForMouse(WidgetMouseEvent* aEvent); + MOZ_CAN_RUN_SCRIPT + static void ProcessPointerCaptureForTouch(WidgetTouchEvent* aEvent); + MOZ_CAN_RUN_SCRIPT + static void CheckPointerCaptureState(WidgetPointerEvent* aEvent); + + // Implicitly get and release capture of current pointer for touch. + static void ImplicitlyCapturePointer(nsIFrame* aFrame, WidgetEvent* aEvent); + MOZ_CAN_RUN_SCRIPT + static void ImplicitlyReleasePointerCapture(WidgetEvent* aEvent); + + /** + * GetPointerCapturingContent returns a target element which captures the + * pointer. It's applied to mouse or pointer event (except mousedown and + * pointerdown). When capturing, return the element. Otherwise, nullptr. + * + * @param aEvent A mouse event or pointer event which may be + * captured. + * + * @return Target element for aEvent. + */ + static dom::Element* GetPointerCapturingElement(WidgetGUIEvent* aEvent); + + static dom::Element* GetPointerCapturingElement(uint32_t aPointerId); + + // Release pointer capture if captured by the specified content or it's + // descendant. This is called to handle the case that the pointer capturing + // content or it's parent is removed from the document. + static void ReleaseIfCaptureByDescendant(nsIContent* aContent); + + /* + * This function handles the case when content had called preventDefault on + * the active pointer. In that case we have to prevent firing subsequent mouse + * to content. We check the flag PointerInfo::mPreventMouseEventByContent and + * call PreventDefault(false) to stop default behaviors and stop firing mouse + * events to content and chrome. + * + * note: mouse transition events are excluded + * note: we have to clean mPreventMouseEventByContent on pointerup for those + * devices support hover + * note: we don't suppress firing mouse events to chrome and system group + * handlers because they may implement default behaviors + */ + static void PreHandlePointerEventsPreventDefault( + WidgetPointerEvent* aPointerEvent, WidgetGUIEvent* aMouseOrTouchEvent); + + /* + * This function handles the preventDefault behavior of pointerdown. When user + * preventDefault on pointerdown, We have to mark the active pointer to + * prevent sebsequent mouse events (except mouse transition events) and + * default behaviors. + * + * We add mPreventMouseEventByContent flag in PointerInfo to represent the + * active pointer won't firing compatible mouse events. It's set to true when + * content preventDefault on pointerdown + */ + static void PostHandlePointerEventsPreventDefault( + WidgetPointerEvent* aPointerEvent, WidgetGUIEvent* aMouseOrTouchEvent); + + MOZ_CAN_RUN_SCRIPT + static void DispatchPointerFromMouseOrTouch( + PresShell* aShell, nsIFrame* aFrame, nsIContent* aContent, + WidgetGUIEvent* aEvent, bool aDontRetargetEvents, nsEventStatus* aStatus, + nsIContent** aTargetContent); + + static void InitPointerEventFromMouse(WidgetPointerEvent* aPointerEvent, + WidgetMouseEvent* aMouseEvent, + EventMessage aMessage); + + static void InitPointerEventFromTouch(WidgetPointerEvent& aPointerEvent, + const WidgetTouchEvent& aTouchEvent, + const mozilla::dom::Touch& aTouch, + bool aIsPrimary); + + static bool ShouldGeneratePointerEventFromMouse(WidgetGUIEvent* aEvent) { + return aEvent->mMessage == eMouseDown || aEvent->mMessage == eMouseUp || + aEvent->mMessage == eMouseMove || + aEvent->mMessage == eMouseExitFromWidget; + } + + static bool ShouldGeneratePointerEventFromTouch(WidgetGUIEvent* aEvent) { + return aEvent->mMessage == eTouchStart || aEvent->mMessage == eTouchMove || + aEvent->mMessage == eTouchEnd || aEvent->mMessage == eTouchCancel || + aEvent->mMessage == eTouchPointerCancel; + } + + static MOZ_ALWAYS_INLINE int32_t GetSpoofedPointerIdForRFP() { + return sSpoofedPointerId.valueOr(0); + } + + static void NotifyDestroyPresContext(nsPresContext* aPresContext); + + static bool IsDragAndDropEnabled(WidgetMouseEvent& aEvent); + + private: + // Set pointer capture of the specified pointer by the element. + static void SetPointerCaptureById(uint32_t aPointerId, + dom::Element* aElement); + + // GetPointerType returns pointer type like mouse, pen or touch for pointer + // event with pointerId. The return value must be one of + // MouseEvent_Binding::MOZ_SOURCE_* + static uint16_t GetPointerType(uint32_t aPointerId); + + // GetPointerPrimaryState returns state of attribute isPrimary for pointer + // event with pointerId + static bool GetPointerPrimaryState(uint32_t aPointerId); + + MOZ_CAN_RUN_SCRIPT + static void DispatchGotOrLostPointerCaptureEvent( + bool aIsGotCapture, const WidgetPointerEvent* aPointerEvent, + dom::Element* aCaptureTarget); + + // The cached spoofed pointer ID for fingerprinting resistance. We will use a + // mouse pointer id for desktop. For mobile, we should use the touch pointer + // id as the spoofed one, and this work will be addressed in Bug 1492775. + static Maybe<int32_t> sSpoofedPointerId; + + // A helper function to cache the pointer id of the spoofed interface, we + // would only cache the pointer id once. After that, we would always stick to + // that pointer id for fingerprinting resistance. + static void MaybeCacheSpoofedPointerID(uint16_t aInputSource, + uint32_t aPointerId); +}; + +} // namespace mozilla + +#endif // mozilla_PointerEventHandler_h diff --git a/dom/events/RemoteDragStartData.cpp b/dom/events/RemoteDragStartData.cpp new file mode 100644 index 0000000000..69d7692bf8 --- /dev/null +++ b/dom/events/RemoteDragStartData.cpp @@ -0,0 +1,83 @@ +/* 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/. */ + +#include "nsContentAreaDragDrop.h" +#include "RemoteDragStartData.h" +#include "nsContentUtils.h" +#include "nsICookieJarSettings.h" +#include "nsVariant.h" +#include "mozilla/dom/BlobImpl.h" +#include "mozilla/dom/BrowserParent.h" +#include "mozilla/dom/IPCBlobUtils.h" +#include "mozilla/dom/DOMTypes.h" +#include "mozilla/ipc/ProtocolUtils.h" + +using namespace mozilla::ipc; + +namespace mozilla::dom { + +RemoteDragStartData::~RemoteDragStartData() = default; + +RemoteDragStartData::RemoteDragStartData( + BrowserParent* aBrowserParent, + nsTArray<IPCTransferableData>&& aTransferableData, + const LayoutDeviceIntRect& aRect, nsIPrincipal* aPrincipal, + nsIContentSecurityPolicy* aCsp, nsICookieJarSettings* aCookieJarSettings, + WindowContext* aSourceWindowContext, WindowContext* aSourceTopWindowContext) + : mBrowserParent(aBrowserParent), + mTransferableData(std::move(aTransferableData)), + mRect(aRect), + mPrincipal(aPrincipal), + mCsp(aCsp), + mCookieJarSettings(aCookieJarSettings), + mSourceWindowContext(aSourceWindowContext), + mSourceTopWindowContext(aSourceTopWindowContext) {} + +void RemoteDragStartData::AddInitialDnDDataTo( + DataTransfer* aDataTransfer, nsIPrincipal** aPrincipal, + nsIContentSecurityPolicy** aCsp, + nsICookieJarSettings** aCookieJarSettings) { + NS_IF_ADDREF(*aPrincipal = mPrincipal); + NS_IF_ADDREF(*aCsp = mCsp); + NS_IF_ADDREF(*aCookieJarSettings = mCookieJarSettings); + + for (uint32_t i = 0; i < mTransferableData.Length(); ++i) { + nsTArray<IPCTransferableDataItem>& itemArray = mTransferableData[i].items(); + for (auto& item : itemArray) { + if (!nsContentUtils::IPCTransferableDataItemHasKnownFlavor(item)) { + NS_WARNING( + "Ignoring unknown flavor in " + "RemoteDragStartData::AddInitialDnDDataTo"); + continue; + } + + RefPtr<nsVariantCC> variant = new nsVariantCC(); + // Special case kFilePromiseMime so that we get the right + // nsIFlavorDataProvider for it. + if (item.flavor().EqualsLiteral(kFilePromiseMime)) { + RefPtr<nsISupports> flavorDataProvider = + new nsContentAreaDragDropDataProvider(); + variant->SetAsISupports(flavorDataProvider); + } else { + nsresult rv = + nsContentUtils::IPCTransferableDataItemToVariant(item, variant); + if (NS_FAILED(rv)) { + continue; + } + } + + // We set aHidden to false, as we don't need to worry about hiding data + // from content in the parent process where there is no content. + aDataTransfer->SetDataWithPrincipalFromOtherProcess( + NS_ConvertUTF8toUTF16(item.flavor()), variant, i, mPrincipal, + /* aHidden = */ false); + } + } + + // Clear things that are no longer needed. + mTransferableData.Clear(); + mPrincipal = nullptr; +} + +} // namespace mozilla::dom diff --git a/dom/events/RemoteDragStartData.h b/dom/events/RemoteDragStartData.h new file mode 100644 index 0000000000..0d6a78222c --- /dev/null +++ b/dom/events/RemoteDragStartData.h @@ -0,0 +1,74 @@ +/* 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/. */ + +#ifndef mozilla_dom_RemoteDragStartData_h +#define mozilla_dom_RemoteDragStartData_h + +#include "nsCOMPtr.h" +#include "nsRect.h" +#include "mozilla/dom/DataTransfer.h" +#include "mozilla/dom/DOMTypes.h" +#include "mozilla/gfx/DataSurfaceHelpers.h" + +class nsICookieJarSettings; + +namespace mozilla::dom { + +class IPCDataTransferItem; +class BrowserParent; + +/** + * This class is used to hold information about a drag + * when a drag begins in a content process. + */ +class RemoteDragStartData { + public: + NS_INLINE_DECL_REFCOUNTING(RemoteDragStartData) + + RemoteDragStartData(BrowserParent* aBrowserParent, + nsTArray<IPCTransferableData>&& aTransferableData, + const LayoutDeviceIntRect& aRect, + nsIPrincipal* aPrincipal, nsIContentSecurityPolicy* aCsp, + nsICookieJarSettings* aCookieJarSettings, + WindowContext* aSourceWindowContext, + WindowContext* aSourceTopWindowContext); + + void SetVisualization( + already_AddRefed<gfx::DataSourceSurface> aVisualization) { + mVisualization = aVisualization; + } + + // Get the drag image and rectangle, clearing it from this + // RemoteDragStartData in the process. + already_AddRefed<mozilla::gfx::SourceSurface> TakeVisualization( + LayoutDeviceIntRect* aRect) { + *aRect = mRect; + return mVisualization.forget(); + } + + void AddInitialDnDDataTo(DataTransfer* aDataTransfer, + nsIPrincipal** aPrincipal, + nsIContentSecurityPolicy** aCsp, + nsICookieJarSettings** aCookieJarSettings); + + WindowContext* GetSourceWindowContext() { return mSourceWindowContext; } + WindowContext* GetSourceTopWindowContext() { return mSourceTopWindowContext; } + + private: + virtual ~RemoteDragStartData(); + + RefPtr<BrowserParent> mBrowserParent; + nsTArray<IPCTransferableData> mTransferableData; + const LayoutDeviceIntRect mRect; + nsCOMPtr<nsIPrincipal> mPrincipal; + nsCOMPtr<nsIContentSecurityPolicy> mCsp; + nsCOMPtr<nsICookieJarSettings> mCookieJarSettings; + RefPtr<WindowContext> mSourceWindowContext; + RefPtr<WindowContext> mSourceTopWindowContext; + RefPtr<mozilla::gfx::SourceSurface> mVisualization; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_RemoteDragStartData_h diff --git a/dom/events/ScrollAreaEvent.cpp b/dom/events/ScrollAreaEvent.cpp new file mode 100644 index 0000000000..4cc7bc61e7 --- /dev/null +++ b/dom/events/ScrollAreaEvent.cpp @@ -0,0 +1,80 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#include "base/basictypes.h" +#include "ipc/IPCMessageUtils.h" +#include "ipc/IPCMessageUtilsSpecializations.h" +#include "mozilla/dom/DOMRect.h" +#include "mozilla/dom/ScrollAreaEvent.h" +#include "mozilla/ContentEvents.h" + +namespace mozilla::dom { + +ScrollAreaEvent::ScrollAreaEvent(EventTarget* aOwner, + nsPresContext* aPresContext, + InternalScrollAreaEvent* aEvent) + : UIEvent(aOwner, aPresContext, aEvent), mClientArea(new DOMRect(nullptr)) { + mClientArea->SetLayoutRect(aEvent ? aEvent->mArea : nsRect()); +} + +NS_IMPL_CYCLE_COLLECTION_INHERITED(ScrollAreaEvent, UIEvent, mClientArea) + +NS_IMPL_ADDREF_INHERITED(ScrollAreaEvent, UIEvent) +NS_IMPL_RELEASE_INHERITED(ScrollAreaEvent, UIEvent) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ScrollAreaEvent) +NS_INTERFACE_MAP_END_INHERITING(UIEvent) + +void ScrollAreaEvent::InitScrollAreaEvent(const nsAString& aEventType, + bool aCanBubble, bool aCancelable, + nsGlobalWindowInner* aView, + int32_t aDetail, float aX, float aY, + float aWidth, float aHeight) { + NS_ENSURE_TRUE_VOID(!mEvent->mFlags.mIsBeingDispatched); + + UIEvent::InitUIEvent(aEventType, aCanBubble, aCancelable, aView, aDetail); + mClientArea->SetRect(aX, aY, aWidth, aHeight); +} + +void ScrollAreaEvent::Serialize(IPC::MessageWriter* aWriter, + bool aSerializeInterfaceType) { + if (aSerializeInterfaceType) { + IPC::WriteParam(aWriter, u"scrollareaevent"_ns); + } + + Event::Serialize(aWriter, false); + + IPC::WriteParam(aWriter, X()); + IPC::WriteParam(aWriter, Y()); + IPC::WriteParam(aWriter, Width()); + IPC::WriteParam(aWriter, Height()); +} + +bool ScrollAreaEvent::Deserialize(IPC::MessageReader* aReader) { + NS_ENSURE_TRUE(Event::Deserialize(aReader), false); + + float x, y, width, height; + NS_ENSURE_TRUE(IPC::ReadParam(aReader, &x), false); + NS_ENSURE_TRUE(IPC::ReadParam(aReader, &y), false); + NS_ENSURE_TRUE(IPC::ReadParam(aReader, &width), false); + NS_ENSURE_TRUE(IPC::ReadParam(aReader, &height), false); + mClientArea->SetRect(x, y, width, height); + + return true; +} + +} // namespace mozilla::dom + +using namespace mozilla; +using namespace mozilla::dom; + +already_AddRefed<ScrollAreaEvent> NS_NewDOMScrollAreaEvent( + EventTarget* aOwner, nsPresContext* aPresContext, + InternalScrollAreaEvent* aEvent) { + RefPtr<ScrollAreaEvent> ev = + new ScrollAreaEvent(aOwner, aPresContext, aEvent); + return ev.forget(); +} diff --git a/dom/events/ScrollAreaEvent.h b/dom/events/ScrollAreaEvent.h new file mode 100644 index 0000000000..4ba96ef9f4 --- /dev/null +++ b/dom/events/ScrollAreaEvent.h @@ -0,0 +1,60 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#ifndef mozilla_dom_ScrollAreaEvent_h_ +#define mozilla_dom_ScrollAreaEvent_h_ + +#include "mozilla/dom/DOMRect.h" +#include "mozilla/dom/ScrollAreaEventBinding.h" +#include "mozilla/dom/UIEvent.h" +#include "mozilla/Attributes.h" +#include "mozilla/EventForwards.h" + +namespace mozilla::dom { + +class ScrollAreaEvent : public UIEvent { + public: + ScrollAreaEvent(EventTarget* aOwner, nsPresContext* aPresContext, + InternalScrollAreaEvent* aEvent); + + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(ScrollAreaEvent, UIEvent) + + void Serialize(IPC::MessageWriter* aWriter, + bool aSerializeInterfaceType) override; + bool Deserialize(IPC::MessageReader* aReader) override; + + virtual JSObject* WrapObjectInternal( + JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override { + return ScrollAreaEvent_Binding::Wrap(aCx, this, aGivenProto); + } + + float X() const { return mClientArea->Left(); } + + float Y() const { return mClientArea->Top(); } + + float Width() const { return mClientArea->Width(); } + + float Height() const { return mClientArea->Height(); } + + void InitScrollAreaEvent(const nsAString& aType, bool aCanBubble, + bool aCancelable, nsGlobalWindowInner* aView, + int32_t aDetail, float aX, float aY, float aWidth, + float aHeight); + + protected: + ~ScrollAreaEvent() = default; + + RefPtr<DOMRect> mClientArea; +}; + +} // namespace mozilla::dom + +already_AddRefed<mozilla::dom::ScrollAreaEvent> NS_NewDOMScrollAreaEvent( + mozilla::dom::EventTarget* aOwner, nsPresContext* aPresContext, + mozilla::InternalScrollAreaEvent* aEvent); + +#endif // mozilla_dom_ScrollAreaEvent_h_ diff --git a/dom/events/ShortcutKeyDefinitions.cpp b/dom/events/ShortcutKeyDefinitions.cpp new file mode 100644 index 0000000000..3f34f45240 --- /dev/null +++ b/dom/events/ShortcutKeyDefinitions.cpp @@ -0,0 +1,780 @@ +/* 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/. */ + +#include "ShortcutKeys.h" + +#if !defined(XP_WIN) && !defined(MOZ_WIDGET_COCOA) && \ + !defined(MOZ_WIDGET_GTK) && !defined(MOZ_WIDGET_ANDROID) +# define USE_EMACS_KEY_BINDINGS +#endif + +/** + * This file defines shortcut keys for <input>, <textarea>, page navigation + * and HTML editor. You must write each mapping in one line and append inline + * comment on which platform it's mapped since this style helps you to looking + * for the defintion with searchfox, etc. + * Be aware, the commands defined in this file may not work because editor or + * other keyboard event listeners may handle before. + * Note: When you change key mappings, you need to change + * `test/gtest/TestShortcutKeyDefinitions.cpp` too. + * + * The latest version of the original files are: + * + * Windows: + * https://searchfox.org/mozilla-central/rev/fd853f4aea89186efdb368e759a71b7a90c2b89c/dom/events/win/ShortcutKeyDefinitions.cpp + * macOS: + * https://searchfox.org/mozilla-central/rev/fd853f4aea89186efdb368e759a71b7a90c2b89c/dom/events/mac/ShortcutKeyDefinitions.cpp + * Linux: + * https://searchfox.org/mozilla-central/rev/fd853f4aea89186efdb368e759a71b7a90c2b89c/dom/events/unix/ShortcutKeyDefinitions.cpp + * Android: + * https://searchfox.org/mozilla-central/rev/fd853f4aea89186efdb368e759a71b7a90c2b89c/dom/events/android/ShortcutKeyDefinitions.cpp + * Emacs: + * https://searchfox.org/mozilla-central/rev/fd853f4aea89186efdb368e759a71b7a90c2b89c/dom/events/emacs/ShortcutKeyDefinitions.cpp + * + * And common definitions except macOS: + * https://searchfox.org/mozilla-central/rev/fd853f4aea89186efdb368e759a71b7a90c2b89c/dom/events/ShortcutKeyDefinitionsForInputCommon.h + * https://searchfox.org/mozilla-central/rev/fd853f4aea89186efdb368e759a71b7a90c2b89c/dom/events/ShortcutKeyDefinitionsForTextAreaCommon.h + * https://searchfox.org/mozilla-central/rev/fd853f4aea89186efdb368e759a71b7a90c2b89c/dom/events/ShortcutKeyDefinitionsForEditorCommon.h + * + * And common definitions for page navigation on all platforms: + * https://searchfox.org/mozilla-central/rev/fd853f4aea89186efdb368e759a71b7a90c2b89c/dom/events/ShortcutKeyDefinitionsForBrowserCommon.h + * + * If you don't see shortcut key definitions here, but you see shortcut keys + * work on Linux or macOS, it probably comes from NativeKeyBindings under + * widget. + */ + +namespace mozilla { + +ShortcutKeyData ShortcutKeys::sInputHandlers[] = { +// clang-format off + /************************************************************************** + * Arrow keys to move caret in <input>. + **************************************************************************/ +#if defined(XP_WIN) || defined(MOZ_WIDGET_GTK) || \ + defined(MOZ_WIDGET_ANDROID) || defined(USE_EMACS_KEY_BINDINGS) + {u"keypress", u"VK_LEFT", nullptr, nullptr, u"cmd_moveLeft"}, // Win, Linux, Android, Emacs + {u"keypress", u"VK_RIGHT", nullptr, nullptr, u"cmd_moveRight"}, // Win, Linux, Android, Emacs + {u"keypress", u"VK_UP", nullptr, nullptr, u"cmd_moveUp"}, // Win, Linux, Android, Emacs + {u"keypress", u"VK_DOWN", nullptr, nullptr, u"cmd_moveDown"}, // Win, Linux, Android, Emacs +#endif // Except MOZ_WIDGET_COCOA + + /************************************************************************** + * Arrow keys to select a char/line in <input>. + **************************************************************************/ +#if defined(XP_WIN) || defined(MOZ_WIDGET_GTK) || \ + defined(MOZ_WIDGET_ANDROID) || defined(USE_EMACS_KEY_BINDINGS) + {u"keypress", u"VK_LEFT", nullptr, u"shift", u"cmd_selectLeft"}, // Win, Linux, Android, Emacs + {u"keypress", u"VK_RIGHT", nullptr, u"shift", u"cmd_selectRight"}, // Win, Linux, Android, Emacs + {u"keypress", u"VK_UP", nullptr, u"shift", u"cmd_selectUp"}, // Win, Linux, Android, Emacs + {u"keypress", u"VK_DOWN", nullptr, u"shift", u"cmd_selectDown"}, // Win, Linux, Android, Emacs +#endif // Except MOZ_WIDGET_COCOA + + /************************************************************************** + * Arrow keys per word in <input>. + **************************************************************************/ +#if defined(MOZ_WIDGET_ANDROID) || defined(USE_EMACS_KEY_BINDINGS) + {u"keypress", u"VK_LEFT", nullptr, u"control", u"cmd_wordPrevious"}, // Android, Emacs + {u"keypress", u"VK_RIGHT", nullptr, u"control", u"cmd_wordNext"}, // Android, Emacs + {u"keypress", u"VK_LEFT", nullptr, u"shift,control", u"cmd_selectWordPrevious"}, // Android, Emacs + {u"keypress", u"VK_RIGHT", nullptr, u"shift,control", u"cmd_selectWordNext"}, // Android, Emacs +#endif // MOZ_WIDGET_ANDROID || USE_EMACS_KEY_BINDINGS +#if defined(XP_WIN) + {u"keypress", u"VK_LEFT", nullptr, u"control", u"cmd_moveLeft2"}, // Win + {u"keypress", u"VK_RIGHT", nullptr, u"control", u"cmd_moveRight2"}, // Win + {u"keypress", u"VK_LEFT", nullptr, u"shift,control", u"cmd_selectLeft2"}, // Win + {u"keypress", u"VK_RIGHT", nullptr, u"shift,control", u"cmd_selectRight2"}, // Win +#endif // XP_WIN + + /************************************************************************** + * Arrow keys per block in <input>. + **************************************************************************/ +#if defined(XP_WIN) + {u"keypress", u"VK_UP", nullptr, u"control", u"cmd_moveUp2"}, // Win + {u"keypress", u"VK_DOWN", nullptr, u"control", u"cmd_moveDown2"}, // Win + {u"keypress", u"VK_UP", nullptr, u"shift,control", u"cmd_selectUp2"}, // Win + {u"keypress", u"VK_DOWN", nullptr, u"shift,control", u"cmd_selectDown2"}, // Win +#endif // XP_WIN + + /************************************************************************** + * Arrow keys to begin/end of a line in <input>. + **************************************************************************/ +#if defined(MOZ_WIDGET_ANDROID) + {u"keypress", u"VK_LEFT", nullptr, u"alt", u"cmd_beginLine"}, // Android + {u"keypress", u"VK_RIGHT", nullptr, u"alt", u"cmd_endLine"}, // Android + {u"keypress", u"VK_LEFT", nullptr, u"shift,alt", u"cmd_selectBeginLine"}, // Android + {u"keypress", u"VK_RIGHT", nullptr, u"shift,alt", u"cmd_selectEndLine"}, // Android +#endif // MOZ_WIDGET_ANDROID + + /************************************************************************** + * Home/End keys in <input>. + **************************************************************************/ +#if defined(XP_WIN) || defined(MOZ_WIDGET_ANDROID) ||\ + defined(USE_EMACS_KEY_BINDINGS) + {u"keypress", u"VK_HOME", nullptr, nullptr, u"cmd_beginLine"}, // Win, Android, Emacs + {u"keypress", u"VK_END", nullptr, nullptr, u"cmd_endLine"}, // Win, Android, Emacs + {u"keypress", u"VK_HOME", nullptr, u"shift", u"cmd_selectBeginLine"}, // Win, Android, Emacs + {u"keypress", u"VK_END", nullptr, u"shift", u"cmd_selectEndLine"}, // Win, Android, Emacs +#endif // XP_WIN || MOZ_WIDGET_ANDROID || USE_EMACS_KEY_BINDINGS +#if defined(USE_EMACS_KEY_BINDINGS) + {u"keypress", u"VK_HOME", nullptr, u"control", u"cmd_beginLine"}, // Emacs + {u"keypress", u"VK_END", nullptr, u"control", u"cmd_endLine"}, // Emacs + {u"keypress", u"VK_HOME", nullptr, u"control,shift", u"cmd_selectBeginLine"}, // Emacs + {u"keypress", u"VK_END", nullptr, u"control,shift", u"cmd_selectEndLine"}, // Emacs +#endif // USE_EMACS_KEY_BINDINGS +#if defined(XP_WIN) + {u"keypress", u"VK_HOME", nullptr, u"control", u"cmd_moveTop"}, // Win + {u"keypress", u"VK_END", nullptr, u"control", u"cmd_moveBottom"}, // Win + {u"keypress", u"VK_HOME", nullptr, u"shift,control", u"cmd_selectTop"}, // Win + {u"keypress", u"VK_END", nullptr, u"shift,control", u"cmd_selectBottom"}, // Win +#endif // XP_WIN + + /************************************************************************** + * Insert key in <input>. + **************************************************************************/ +#if defined(XP_WIN) || defined(USE_EMACS_KEY_BINDINGS) + {u"keypress", u"VK_INSERT", nullptr, u"control", u"cmd_copy"}, // Win, Emacs + {u"keypress", u"VK_INSERT", nullptr, u"shift", u"cmd_paste"}, // Win, Emacs +#endif // XP_WIN || USE_EMACS_KEY_BINDINGS + + /************************************************************************** + * Delete key in <input>. + **************************************************************************/ +#if defined(XP_WIN) || defined(USE_EMACS_KEY_BINDINGS) + {u"keypress", u"VK_DELETE", nullptr, u"shift", u"cmd_cutOrDelete"}, // Win, Emacs +#endif // XP_WIN || USE_EMACS_KEY_BINDINGS +#if defined(USE_EMACS_KEY_BINDINGS) + {u"keypress", u"VK_DELETE", nullptr, u"control", u"cmd_copyOrDelete"}, // Emacs +#endif // USE_EMACS_KEY_BINDINGS +#if defined(XP_WIN) || defined(MOZ_WIDGET_ANDROID) + {u"keypress", u"VK_DELETE", nullptr, u"control", u"cmd_deleteWordForward"}, // Win, Android +#endif // XP_WIN +#if defined(MOZ_WIDGET_ANDROID) + {u"keypress", u"VK_DELETE", nullptr, u"alt", u"cmd_deleteToEndOfLine"}, // Android +#endif // MOZ_WIDGET_ANDROID + + /************************************************************************** + * Backspace key in <input>. + **************************************************************************/ +#if defined(XP_WIN) || defined(MOZ_WIDGET_ANDROID) ||\ + defined(USE_EMACS_KEY_BINDINGS) + {u"keypress", u"VK_BACK", nullptr, u"control", u"cmd_deleteWordBackward"}, // Win, Android, Emacs +#endif // XP_WIN || MOZ_WIDGET_ANDROID || USE_EMACS_KEY_BINDINGS +#if defined(MOZ_WIDGET_ANDROID) + {u"keypress", u"VK_BACK", nullptr, u"alt", u"cmd_deleteToBeginningOfLine"}, // Android +#endif // MOZ_WIDGET_ANDROID +#if defined(XP_WIN) + {u"keypress", u"VK_BACK", nullptr, u"alt", u"cmd_undo"}, // Win + {u"keypress", u"VK_BACK", nullptr, u"alt,shift", u"cmd_redo"}, // Win +#endif // XP_WIN + + /************************************************************************** + * Common editor commands in <input>. + **************************************************************************/ + {u"keypress", nullptr, u"c", u"accel", u"cmd_copy"}, // Win, macOS, Linux, Android, Emacs + {u"keypress", nullptr, u"x", u"accel", u"cmd_cut"}, // Win, macOS, Linux, Android, Emacs + {u"keypress", nullptr, u"v", u"accel", u"cmd_paste"}, // Win, macOS, Linux, Android, Emacs + {u"keypress", nullptr, u"z", u"accel", u"cmd_undo"}, // Win, macOS, Linux, Android, Emacs + {u"keypress", nullptr, u"z", u"accel,shift", u"cmd_redo"}, // Win, macOS, Linux, Android, Emacs + + {u"keypress", nullptr, u"v", u"accel,shift", u"cmd_paste"}, // Win, macOS, Linux, Android, Emacs +// Mac uses Option+Shift+Command+V for Paste and Match Style +#if defined(MOZ_WIDGET_COCOA) + {u"keypress", nullptr, u"v", u"accel,alt,shift", u"cmd_paste"}, // macOS +#endif // MOZ_WIDGET_COCOA + +#if defined(XP_WIN) || defined(MOZ_WIDGET_GTK) ||\ + defined(USE_EMACS_KEY_BINDINGS) + {u"keypress", nullptr, u"y", u"accel", u"cmd_redo"}, // Win, Linux, Emacs +#endif // XP_WIN || MOZ_WIDGET_GTK || USE_EMACS_KEY_BINDINGS + +#if defined(XP_WIN) || defined(MOZ_WIDGET_COCOA) || defined(MOZ_WIDGET_GTK) ||\ + defined(MOZ_WIDGET_ANDROID) + {u"keypress", nullptr, u"a", u"accel", u"cmd_selectAll"}, // Win, macOS, Linux, Android +#endif // XP_WIN || MOZ_WIDGET_COCOA || MOZ_WIDGET_GTK || MOZ_WIDGET_ANDROID +#if defined(USE_EMACS_KEY_BINDINGS) + {u"keypress", nullptr, u"a", u"alt", u"cmd_selectAll"}, // Emacs +#endif // USE_EMACS_KEY_BINDINGS + + /************************************************************************** + * Emacs specific shortcut keys in <input>. + **************************************************************************/ +#if defined(USE_EMACS_KEY_BINDINGS) + {u"keypress", nullptr, u"a", u"control", u"cmd_beginLine"}, // Emacs + {u"keypress", nullptr, u"e", u"control", u"cmd_endLine"}, // Emacs + {u"keypress", nullptr, u"b", u"control", u"cmd_charPrevious"}, // Emacs + {u"keypress", nullptr, u"f", u"control", u"cmd_charNext"}, // Emacs + {u"keypress", nullptr, u"h", u"control", u"cmd_deleteCharBackward"}, // Emacs + {u"keypress", nullptr, u"d", u"control", u"cmd_deleteCharForward"}, // Emacs + {u"keypress", nullptr, u"w", u"control", u"cmd_deleteWordBackward"}, // Emacs + {u"keypress", nullptr, u"u", u"control", u"cmd_deleteToBeginningOfLine"}, // Emacs + {u"keypress", nullptr, u"k", u"control", u"cmd_deleteToEndOfLine"}, // Emacs +#endif // USE_EMACS_KEY_BINDINGS + // clang-format on + + {nullptr, nullptr, nullptr, nullptr, nullptr}}; + +ShortcutKeyData ShortcutKeys::sTextAreaHandlers[] = { +// clang-format off + /************************************************************************** + * Arrow keys to move caret in <textarea>. + **************************************************************************/ +#if defined(XP_WIN) || defined(MOZ_WIDGET_GTK) || \ + defined(MOZ_WIDGET_ANDROID) || defined(USE_EMACS_KEY_BINDINGS) + {u"keypress", u"VK_LEFT", nullptr, nullptr, u"cmd_moveLeft"}, // Win, Linux, Android, Emacs + {u"keypress", u"VK_RIGHT", nullptr, nullptr, u"cmd_moveRight"}, // Win, Linux, Android, Emacs + {u"keypress", u"VK_UP", nullptr, nullptr, u"cmd_moveUp"}, // Win, Linux, Android, Emacs + {u"keypress", u"VK_DOWN", nullptr, nullptr, u"cmd_moveDown"}, // Win, Linux, Android, Emacs +#endif // Except MOZ_WIDGET_COCOA + + /************************************************************************** + * Arrow keys to select a char/line in <textarea>. + **************************************************************************/ +#if defined(XP_WIN) || defined(MOZ_WIDGET_GTK) || \ + defined(MOZ_WIDGET_ANDROID) || defined(USE_EMACS_KEY_BINDINGS) + {u"keypress", u"VK_LEFT", nullptr, u"shift", u"cmd_selectLeft"}, // Win, Linux, Android, Emacs + {u"keypress", u"VK_RIGHT", nullptr, u"shift", u"cmd_selectRight"}, // Win, Linux, Android, Emacs + {u"keypress", u"VK_UP", nullptr, u"shift", u"cmd_selectUp"}, // Win, Linux, Android, Emacs + {u"keypress", u"VK_DOWN", nullptr, u"shift", u"cmd_selectDown"}, // Win, Linux, Android, Emacs +#endif // Except MOZ_WIDGET_COCOA + + /************************************************************************** + * Arrow keys per word in <textarea>. + **************************************************************************/ +#if defined(MOZ_WIDGET_ANDROID) || defined(USE_EMACS_KEY_BINDINGS) + {u"keypress", u"VK_LEFT", nullptr, u"control", u"cmd_wordPrevious"}, // Android, Emacs + {u"keypress", u"VK_RIGHT", nullptr, u"control", u"cmd_wordNext"}, // Android, Emacs + {u"keypress", u"VK_LEFT", nullptr, u"shift,control", u"cmd_selectWordPrevious"}, // Android, Emacs + {u"keypress", u"VK_RIGHT", nullptr, u"shift,control", u"cmd_selectWordNext"}, // Android, Emacs +#endif // MOZ_WIDGET_ANDROID || USE_EMACS_KEY_BINDINGS +#if defined(XP_WIN) + {u"keypress", u"VK_LEFT", nullptr, u"control", u"cmd_moveLeft2"}, // Win + {u"keypress", u"VK_RIGHT", nullptr, u"control", u"cmd_moveRight2"}, // Win + {u"keypress", u"VK_LEFT", nullptr, u"shift,control", u"cmd_selectLeft2"}, // Win + {u"keypress", u"VK_RIGHT", nullptr, u"shift,control", u"cmd_selectRight2"}, // Win +#endif // XP_WIN + + /************************************************************************** + * Arrow keys per block in <textarea>. + **************************************************************************/ +#if defined(MOZ_WIDGET_ANDROID) + {u"keypress", u"VK_UP", nullptr, u"alt", u"cmd_moveTop"}, // Android + {u"keypress", u"VK_DOWN", nullptr, u"alt", u"cmd_moveBottom"}, // Android + {u"keypress", u"VK_UP", nullptr, u"shift,alt", u"cmd_selectTop"}, // Android + {u"keypress", u"VK_DOWN", nullptr, u"shift,alt", u"cmd_selectBottom"}, // Android +#endif // MOZ_WIDGET_ANDROID +#if defined(XP_WIN) + {u"keypress", u"VK_UP", nullptr, u"control", u"cmd_moveUp2"}, // Win + {u"keypress", u"VK_DOWN", nullptr, u"control", u"cmd_moveDown2"}, // Win + {u"keypress", u"VK_UP", nullptr, u"shift,control", u"cmd_selectUp2"}, // Win + {u"keypress", u"VK_DOWN", nullptr, u"shift,control", u"cmd_selectDown2"}, // Win +#endif // XP_WIN + + /************************************************************************** + * Arrow keys to begin/end of a line in <textarea>. + **************************************************************************/ +#if defined(MOZ_WIDGET_ANDROID) + {u"keypress", u"VK_LEFT", nullptr, u"alt", u"cmd_beginLine"}, // Android + {u"keypress", u"VK_RIGHT", nullptr, u"alt", u"cmd_endLine"}, // Android + {u"keypress", u"VK_LEFT", nullptr, u"shift,alt", u"cmd_selectBeginLine"}, // Android + {u"keypress", u"VK_RIGHT", nullptr, u"shift,alt", u"cmd_selectEndLine"}, // Android +#endif // MOZ_WIDGET_ANDROID + + /************************************************************************** + * PageUp/PageDown keys in <textarea>. + **************************************************************************/ +#if defined(XP_WIN) || defined(MOZ_WIDGET_ANDROID) ||\ + defined(USE_EMACS_KEY_BINDINGS) + {u"keypress", u"VK_PAGE_UP", nullptr, nullptr, u"cmd_movePageUp"}, // Win, Android, Emacs + {u"keypress", u"VK_PAGE_DOWN", nullptr, nullptr, u"cmd_movePageDown"}, // Win, Android, Emacs + {u"keypress", u"VK_PAGE_UP", nullptr, u"shift", u"cmd_selectPageUp"}, // Win, Android, Emacs + {u"keypress", u"VK_PAGE_DOWN", nullptr, u"shift", u"cmd_selectPageDown"}, // Win, Android, Emacs +#endif // XP_WIN || MOZ_WIDGET_ANDROID || USE_EMACS_KEY_BINDINGS +#if defined(MOZ_WIDGET_ANDROID) + {u"keypress", u"VK_PAGE_UP", nullptr, u"alt", u"cmd_moveTop"}, // Android + {u"keypress", u"VK_PAGE_DOWN", nullptr, u"alt", u"cmd_moveBottom"}, // Android + {u"keypress", u"VK_PAGE_UP", nullptr, u"shift,alt", u"cmd_selectTop"}, // Android + {u"keypress", u"VK_PAGE_DOWN", nullptr, u"shift,alt", u"cmd_selectBottom"}, // Android +#endif // MOZ_WIDGET_ANDROID + + /************************************************************************** + * Home/End keys in <textarea>. + **************************************************************************/ +#if defined(XP_WIN) || defined(MOZ_WIDGET_ANDROID) ||\ + defined(USE_EMACS_KEY_BINDINGS) + {u"keypress", u"VK_HOME", nullptr, nullptr, u"cmd_beginLine"}, // Win, Android, Emacs + {u"keypress", u"VK_END", nullptr, nullptr, u"cmd_endLine"}, // Win, Android, Emacs + {u"keypress", u"VK_HOME", nullptr, u"shift", u"cmd_selectBeginLine"}, // Win, Android, Emacs + {u"keypress", u"VK_END", nullptr, u"shift", u"cmd_selectEndLine"}, // Win, Android, Emacs + {u"keypress", u"VK_HOME", nullptr, u"control", u"cmd_moveTop"}, // Win, Android, Emacs + {u"keypress", u"VK_END", nullptr, u"control", u"cmd_moveBottom"}, // Win, Android, Emacs + {u"keypress", u"VK_HOME", nullptr, u"shift,control", u"cmd_selectTop"}, // Win, Android, Emacs + {u"keypress", u"VK_END", nullptr, u"shift,control", u"cmd_selectBottom"}, // Win, Android, Emacs +#endif // XP_WIN || MOZ_WIDGET_ANDROID || USE_EMACS_KEY_BINDINGS + + /************************************************************************** + * Insert key in <textarea>. + **************************************************************************/ +#if defined(XP_WIN) || defined(USE_EMACS_KEY_BINDINGS) + {u"keypress", u"VK_INSERT", nullptr, u"control", u"cmd_copy"}, // Win, Emacs + {u"keypress", u"VK_INSERT", nullptr, u"shift", u"cmd_paste"}, // Win, Emacs +#endif // XP_WIN || USE_EMACS_KEY_BINDINGS + + {u"keypress", nullptr, u"v", u"accel,shift", u"cmd_paste"}, // Win, macOS, Linux, Android, Emacs +// Mac uses Option+Shift+Command+V for Paste and Match Style +#if defined(MOZ_WIDGET_COCOA) + {u"keypress", nullptr, u"v", u"accel,alt,shift", u"cmd_paste"}, // macOS +#endif // MOZ_WIDGET_COCOA + + /************************************************************************** + * Delete key in <textarea>. + **************************************************************************/ +#if defined(XP_WIN) || defined(USE_EMACS_KEY_BINDINGS) + {u"keypress", u"VK_DELETE", nullptr, u"shift", u"cmd_cutOrDelete"}, // Win, Emacs +#endif // XP_WIN || USE_EMACS_KEY_BINDINGS +#if defined(USE_EMACS_KEY_BINDINGS) + {u"keypress", u"VK_DELETE", nullptr, u"control", u"cmd_copyOrDelete"}, // Emacs +#endif // USE_EMACS_KEY_BINDINGS +#if defined(XP_WIN) || defined(MOZ_WIDGET_ANDROID) + {u"keypress", u"VK_DELETE", nullptr, u"control", u"cmd_deleteWordForward"}, // Win, Android +#endif // XP_WIN || MOZ_WIDGET_ANDROID +#if defined(MOZ_WIDGET_ANDROID) + {u"keypress", u"VK_DELETE", nullptr, u"alt", u"cmd_deleteToEndOfLine"}, // Android +#endif // MOZ_WIDGET_ANDROID + + /************************************************************************** + * Backspace key in <textarea>. + **************************************************************************/ +#if defined(XP_WIN) || defined(MOZ_WIDGET_ANDROID) ||\ + defined(USE_EMACS_KEY_BINDINGS) + {u"keypress", u"VK_BACK", nullptr, u"control", u"cmd_deleteWordBackward"}, // Win, Android, Emacs +#endif // XP_WIN || MOZ_WIDGET_ANDROID || USE_EMACS_KEY_BINDINGS +#if defined(MOZ_WIDGET_ANDROID) + {u"keypress", u"VK_BACK", nullptr, u"alt", u"cmd_deleteToBeginningOfLine"}, // Android +#endif // MOZ_WIDGET_ANDROID +#if defined(XP_WIN) + {u"keypress", u"VK_BACK", nullptr, u"alt", u"cmd_undo"}, // Win + {u"keypress", u"VK_BACK", nullptr, u"alt,shift", u"cmd_redo"}, // Win +#endif // XP_WIN + + /************************************************************************** + * Common editor commands in <textarea>. + **************************************************************************/ + {u"keypress", nullptr, u"c", u"accel", u"cmd_copy"}, // Win, macOS, Linux, Android, Emacs + {u"keypress", nullptr, u"x", u"accel", u"cmd_cut"}, // Win, macOS, Linux, Android, Emacs + {u"keypress", nullptr, u"v", u"accel", u"cmd_paste"}, // Win, macOS, Linux, Android, Emacs + {u"keypress", nullptr, u"z", u"accel", u"cmd_undo"}, // Win, macOS, Linux, Android, Emacs + {u"keypress", nullptr, u"z", u"accel,shift", u"cmd_redo"}, // Win, macOS, Linux, Android, Emacs + +#if defined(XP_WIN) || defined(MOZ_WIDGET_GTK) ||\ + defined(USE_EMACS_KEY_BINDINGS) + {u"keypress", nullptr, u"y", u"accel", u"cmd_redo"}, // Win, Linux, Emacs +#endif // XP_WIN || MOZ_WIDGET_GTK || USE_EMACS_KEY_BINDINGS + +#if defined(XP_WIN) || defined(MOZ_WIDGET_COCOA) || defined(MOZ_WIDGET_GTK) ||\ + defined(MOZ_WIDGET_ANDROID) + {u"keypress", nullptr, u"a", u"accel", u"cmd_selectAll"}, // Win, macOS, Linux, Android +#endif // XP_WIN || MOZ_WIDGET_COCOA || MOZ_WIDGET_GTK || MOZ_WIDGET_ANDROID +#if defined(USE_EMACS_KEY_BINDINGS) + {u"keypress", nullptr, u"a", u"alt", u"cmd_selectAll"}, // Emacs +#endif // USE_EMACS_KEY_BINDINGS + + /************************************************************************** + * Emacs specific shortcut keys in <textarea>. + **************************************************************************/ +#if defined(USE_EMACS_KEY_BINDINGS) + {u"keypress", nullptr, u"a", u"control", u"cmd_beginLine"}, // Emacs + {u"keypress", nullptr, u"e", u"control", u"cmd_endLine"}, // Emacs + {u"keypress", nullptr, u"b", u"control", u"cmd_charPrevious"}, // Emacs + {u"keypress", nullptr, u"f", u"control", u"cmd_charNext"}, // Emacs + {u"keypress", nullptr, u"h", u"control", u"cmd_deleteCharBackward"}, // Emacs + {u"keypress", nullptr, u"d", u"control", u"cmd_deleteCharForward"}, // Emacs + {u"keypress", nullptr, u"w", u"control", u"cmd_deleteWordBackward"}, // Emacs + {u"keypress", nullptr, u"u", u"control", u"cmd_deleteToBeginningOfLine"}, // Emacs + {u"keypress", nullptr, u"k", u"control", u"cmd_deleteToEndOfLine"}, // Emacs + {u"keypress", nullptr, u"n", u"control", u"cmd_lineNext"}, // Emacs + {u"keypress", nullptr, u"p", u"control", u"cmd_linePrevious"}, // Emacs +#endif // USE_EMACS_KEY_BINDINGS + // clang-format on + + {nullptr, nullptr, nullptr, nullptr, nullptr}}; + +ShortcutKeyData ShortcutKeys::sBrowserHandlers[] = { + // clang-format off + /************************************************************************** + * Arrow keys to move caret in non-editable element. + **************************************************************************/ + {u"keypress", u"VK_LEFT", nullptr, nullptr, u"cmd_moveLeft"}, // Win, macOS, Linux, Android, Emacs + {u"keypress", u"VK_RIGHT", nullptr, nullptr, u"cmd_moveRight"}, // Win, macOS, Linux, Android, Emacs + {u"keypress", u"VK_UP", nullptr, nullptr, u"cmd_moveUp"}, // Win, macOS, Linux, Android, Emacs + {u"keypress", u"VK_DOWN", nullptr, nullptr, u"cmd_moveDown"}, // Win, macOS, Linux, Android, Emacs + + /************************************************************************** + * Arrow keys to select a char/line in non-editable element. + **************************************************************************/ +#if defined(XP_WIN) || defined(MOZ_WIDGET_COCOA) || defined(MOZ_WIDGET_GTK) + {u"keypress", u"VK_LEFT", nullptr, u"shift", u"cmd_selectLeft"}, // Win, macOS, Linux + {u"keypress", u"VK_RIGHT", nullptr, u"shift", u"cmd_selectRight"}, // Win, macOS, Linux + {u"keypress", u"VK_UP", nullptr, u"shift", u"cmd_selectUp"}, // Win, macOS, Linux + {u"keypress", u"VK_DOWN", nullptr, u"shift", u"cmd_selectDown"}, // Win, macOS, Linux +#endif // XP_WIN || MOZ_WIDGET_COCOA || MOZ_WIDGET_GTK +#if defined(MOZ_WIDGET_ANDROID) || defined(USE_EMACS_KEY_BINDINGS) + {u"keypress", u"VK_LEFT", nullptr, u"shift", u"cmd_selectCharPrevious"}, // Android, Emacs + {u"keypress", u"VK_RIGHT", nullptr, u"shift", u"cmd_selectCharNext"}, // Android, Emacs + {u"keypress", u"VK_UP", nullptr, u"shift", u"cmd_selectLinePrevious"}, // Android, Emacs + {u"keypress", u"VK_DOWN", nullptr, u"shift", u"cmd_selectLineNext"}, // Android, Emacs +#endif // MOZ_WIDGET_ANDROID || USE_EMACS_KEY_BINDINGS + + /************************************************************************** + * Arrow keys per word in non-editable element. + **************************************************************************/ +#if defined(MOZ_WIDGET_ANDROID) || defined(USE_EMACS_KEY_BINDINGS) + {u"keypress", u"VK_LEFT", nullptr, u"control", u"cmd_wordPrevious"}, // Android, Emacs + {u"keypress", u"VK_RIGHT", nullptr, u"control", u"cmd_wordNext"}, // Android, Emacs + {u"keypress", u"VK_LEFT", nullptr, u"control,shift", u"cmd_selectWordPrevious"}, // Android, Emacs + {u"keypress", u"VK_RIGHT", nullptr, u"control,shift", u"cmd_selectWordNext"}, // Android, Emacs +#endif // MOZ_WIDGET_ANDROID || USE_EMACS_KEY_BINDINGS +#if defined(XP_WIN) || defined(MOZ_WIDGET_GTK) + {u"keypress", u"VK_LEFT", nullptr, u"control", u"cmd_moveLeft2"}, // Win, Linux + {u"keypress", u"VK_RIGHT", nullptr, u"control", u"cmd_moveRight2"}, // Win, Linux + {u"keypress", u"VK_LEFT", nullptr, u"control,shift", u"cmd_selectLeft2"}, // Win, Linux + {u"keypress", u"VK_RIGHT", nullptr, u"control,shift", u"cmd_selectRight2"}, // Win, Linux +#endif // XP_WIN || MOZ_WIDGET_GTK +#if defined(MOZ_WIDGET_COCOA) + {u"keypress", u"VK_LEFT", nullptr, u"alt", u"cmd_moveLeft2"}, // macOS + {u"keypress", u"VK_RIGHT", nullptr, u"alt", u"cmd_moveRight2"}, // macOS + {u"keypress", u"VK_LEFT", nullptr, u"alt,shift", u"cmd_selectLeft2"}, // macOS + {u"keypress", u"VK_RIGHT", nullptr, u"alt,shift", u"cmd_selectRight2"}, // macOS +#endif // MOZ_WIDGET_COCOA + + /************************************************************************** + * Arrow keys per block in non-editable element. + **************************************************************************/ +#if defined(XP_WIN) || defined(MOZ_WIDGET_GTK) + {u"keypress", u"VK_UP", nullptr, u"control", u"cmd_moveUp2"}, // Win, Linux + {u"keypress", u"VK_DOWN", nullptr, u"control", u"cmd_moveDown2"}, // Win, Linux + {u"keypress", u"VK_UP", nullptr, u"control,shift", u"cmd_selectUp2"}, // Win, Linux + {u"keypress", u"VK_DOWN", nullptr, u"control,shift", u"cmd_selectDown2"}, // Win, Linux +#endif // XP_WIN || MOZ_WIDGET_GTK +#if defined(MOZ_WIDGET_COCOA) + {u"keypress", u"VK_UP", nullptr, u"accel", u"cmd_moveUp2"}, // macOS + {u"keypress", u"VK_DOWN", nullptr, u"accel", u"cmd_moveDown2"}, // macOS + {u"keypress", u"VK_UP", nullptr, u"alt,shift", u"cmd_selectUp2"}, // macOS + {u"keypress", u"VK_DOWN", nullptr, u"alt,shift", u"cmd_selectDown2"}, // macOS +#endif // MOZ_WIDGET_COCOA +#if defined(MOZ_WIDGET_ANDROID) + {u"keypress", u"VK_UP", nullptr, u"alt", u"cmd_moveTop"}, // Android + {u"keypress", u"VK_DOWN", nullptr, u"alt", u"cmd_moveBottom"}, // Android + {u"keypress", u"VK_UP", nullptr, u"shift,alt", u"cmd_selectTop"}, // Android + {u"keypress", u"VK_DOWN", nullptr, u"shift,alt", u"cmd_selectBottom"}, // Android +#endif // MOZ_WIDGET_ANDROID + + /************************************************************************** + * Arrow keys to begin/end of a line in non-editable element. + **************************************************************************/ +#if defined(MOZ_WIDGET_ANDROID) + {u"keypress", u"VK_LEFT", nullptr, u"alt", u"cmd_beginLine"}, // Android + {u"keypress", u"VK_RIGHT", nullptr, u"alt", u"cmd_endLine"}, // Android + {u"keypress", u"VK_LEFT", nullptr, u"shift,alt", u"cmd_selectBeginLine"}, // Android + {u"keypress", u"VK_RIGHT", nullptr, u"shift,alt", u"cmd_selectEndLine"}, // Android +#endif // MOZ_WIDGET_ANDROID + + /************************************************************************** + * PageUp/PageDown keys in non-editable element. + **************************************************************************/ +#if defined(MOZ_WIDGET_COCOA) + {u"keypress", u"VK_PAGE_UP", nullptr, nullptr, u"cmd_scrollPageUp"}, // macOS + {u"keypress", u"VK_PAGE_DOWN", nullptr, nullptr, u"cmd_scrollPageDown"}, // macOS +#endif // MOZ_WIDGET_COCOA +#if defined(XP_WIN) || defined(MOZ_WIDGET_GTK) ||\ + defined(MOZ_WIDGET_ANDROID) || defined(USE_EMACS_KEY_BINDINGS) + {u"keypress", u"VK_PAGE_UP", nullptr, nullptr, u"cmd_movePageUp"}, // Win, Linux, Android, Emacs + {u"keypress", u"VK_PAGE_DOWN", nullptr, nullptr, u"cmd_movePageDown"}, // Win, Linux, Android, Emacs + {u"keypress", u"VK_PAGE_UP", nullptr, u"shift", u"cmd_selectPageUp"}, // Win, Linux, Android, Emacs + {u"keypress", u"VK_PAGE_DOWN", nullptr, u"shift", u"cmd_selectPageDown"}, // Win, Linux, Android, Emacs +#endif // XP_WIN || MOZ_WIDGET_GTK || MOZ_WIDGET_ANDROID || USE_EMACS_KEY_BINDINGS +#if defined(MOZ_WIDGET_ANDROID) + {u"keypress", u"VK_PAGE_UP", nullptr, u"alt", u"cmd_moveTop"}, // Android + {u"keypress", u"VK_PAGE_DOWN", nullptr, u"alt", u"cmd_moveBottom"}, // Android + {u"keypress", u"VK_PAGE_UP", nullptr, u"shift,alt", u"cmd_selectTop"}, // Android + {u"keypress", u"VK_PAGE_DOWN", nullptr, u"shift,alt", u"cmd_selectBottom"}, // Android +#endif // MOZ_WIDGET_ANDROID + + /************************************************************************** + * Home/End keys in non-editable element. + **************************************************************************/ +#if defined(MOZ_WIDGET_COCOA) + {u"keypress", u"VK_HOME", nullptr, nullptr, u"cmd_scrollTop"}, // macOS + {u"keypress", u"VK_END", nullptr, nullptr, u"cmd_scrollBottom"}, // macOS +#endif // MOZ_WIDGET_COCOA +#if defined(XP_WIN) || defined(MOZ_WIDGET_GTK) ||\ + defined(MOZ_WIDGET_ANDROID) || defined(USE_EMACS_KEY_BINDINGS) + {u"keypress", u"VK_HOME", nullptr, nullptr, u"cmd_beginLine"}, // Win, Linux, Android, Emacs + {u"keypress", u"VK_END", nullptr, nullptr, u"cmd_endLine"}, // Win, Linux, Android, Emacs + {u"keypress", u"VK_HOME", nullptr, u"shift", u"cmd_selectBeginLine"}, // Win, Linux, Android, Emacs + {u"keypress", u"VK_END", nullptr, u"shift", u"cmd_selectEndLine"}, // Win, Linux, Android, Emacs + {u"keypress", u"VK_HOME", nullptr, u"control", u"cmd_moveTop"}, // Win, Linux, Android, Emacs + {u"keypress", u"VK_END", nullptr, u"control", u"cmd_moveBottom"}, // Win, Linux, Android, Emacs + {u"keypress", u"VK_HOME", nullptr, u"shift,control", u"cmd_selectTop"}, // Win, Linux, Android, Emacs + {u"keypress", u"VK_END", nullptr, u"shift,control", u"cmd_selectBottom"}, // Win, Linux, Android, Emacs +#endif // XP_WIN || MOZ_WIDGET_GTK || MOZ_WIDGET_ANDROID || USE_EMACS_KEY_BINDINGS + + /************************************************************************** + * Insert key in non-editable element. + **************************************************************************/ +#if defined(XP_WIN) || defined(MOZ_WIDGET_GTK) ||\ + defined(USE_EMACS_KEY_BINDINGS) + {u"keypress", u"VK_INSERT", nullptr, u"control", u"cmd_copy"}, // Win, Linux, Emacs +#endif // XP_WIN || MOZ_WIDGET_GTK || USE_EMACS_KEY_BINDINGS + + /************************************************************************** + * Delete key in non-editable element. + **************************************************************************/ +#if defined(XP_WIN) || defined(MOZ_WIDGET_GTK) ||\ + defined(USE_EMACS_KEY_BINDINGS) + {u"keypress", u"VK_DELETE", nullptr, u"shift", u"cmd_cut"}, // Win, Linux, Emacs +#endif // XP_WIN || MOZ_WIDGET_GTK || USE_EMACS_KEY_BINDINGS +#if defined(MOZ_WIDGET_GTK) || defined(USE_EMACS_KEY_BINDINGS) + {u"keypress", u"VK_DELETE", nullptr, u"control", u"cmd_copy"}, // Linux, Emacs +#endif // MOZ_WIDGET_GTK || USE_EMACS_KEY_BINDINGS +#if defined(XP_WIN) || defined(MOZ_WIDGET_ANDROID) + {u"keypress", u"VK_DELETE", nullptr, u"control", u"cmd_deleteWordForward"}, // Win, Android +#endif // XP_WIN || MOZ_WIDGET_ANDROID +#if defined(MOZ_WIDGET_ANDROID) + {u"keypress", u"VK_DELETE", nullptr, u"alt", u"cmd_deleteToEndOfLine"}, // Android +#endif // MOZ_WIDGET_ANDROID + + /************************************************************************** + * Backspace key in non-editable element. + **************************************************************************/ +#if defined(MOZ_WIDGET_ANDROID) + {u"keypress", u"VK_BACK", nullptr, u"alt", u"cmd_deleteToBeginningOfLine"}, // Android + {u"keypress", u"VK_BACK", nullptr, u"control", u"cmd_deleteWordBackward"}, // Android +#endif // MOZ_WIDGET_ANDROID + + /************************************************************************** + * Common editor commands in non-editable element. + **************************************************************************/ + {u"keypress", nullptr, u"c", u"accel", u"cmd_copy"}, // Win, macOS, Linux, Android, Emacs + {u"keypress", nullptr, u"x", u"accel", u"cmd_cut"}, // Win, macOS, Linux, Android, Emacs + {u"keypress", nullptr, u"v", u"accel", u"cmd_paste"}, // Win, macOS, Linux, Android, Emacs + {u"keypress", nullptr, u"v", u"accel,shift", u"cmd_pasteNoFormatting"}, // Win, macOS, Linux, Android, Emacs + {u"keypress", nullptr, u"z", u"accel", u"cmd_undo"}, // Win, macOS, Linux, Android, Emacs + {u"keypress", nullptr, u"z", u"accel,shift", u"cmd_redo"}, // Win, macOS, Linux, Android, Emacs + +// Mac uses Option+Shift+Command+V for Paste and Match Style +#if defined(MOZ_WIDGET_COCOA) + {u"keypress", nullptr, u"v", u"accel,alt,shift", u"cmd_pasteNoFormatting"}, // macOS +#endif // MOZ_WIDGET_COCOA + +#if defined(XP_WIN) + {u"keypress", nullptr, u"y", u"accel", u"cmd_redo"}, // Win +#endif // XP_WIN + + {u"keypress", nullptr, u"a", u"accel", u"cmd_selectAll"}, // Win, macOS, Linux, Android, Emacs +#if defined(USE_EMACS_KEY_BINDINGS) + {u"keypress", nullptr, u"a", u"alt", u"cmd_selectAll"}, // Emacs +#endif // USE_EMACS_KEY_BINDINGS + + /************************************************************************** + * Space key in non-editable element. + **************************************************************************/ + {u"keypress", nullptr, u" ", nullptr, u"cmd_scrollPageDown"}, // Win, macOS, Linux, Android, Emacs + {u"keypress", nullptr, u" ", u"shift", u"cmd_scrollPageUp"}, // Win, macOS, Linux, Android, Emacs + + + {nullptr, nullptr, nullptr, nullptr, nullptr}}; + +ShortcutKeyData ShortcutKeys::sEditorHandlers[] = { +// clang-format off + /************************************************************************** + * Arrow keys to move caret in HTMLEditor. + **************************************************************************/ +#if defined(XP_WIN) || defined(MOZ_WIDGET_GTK) || \ + defined(MOZ_WIDGET_ANDROID) || defined(USE_EMACS_KEY_BINDINGS) + {u"keypress", u"VK_LEFT", nullptr, nullptr, u"cmd_moveLeft"}, // Win, Linux, Android, Emacs + {u"keypress", u"VK_RIGHT", nullptr, nullptr, u"cmd_moveRight"}, // Win, Linux, Android, Emacs + {u"keypress", u"VK_UP", nullptr, nullptr, u"cmd_moveUp"}, // Win, Linux, Android, Emacs + {u"keypress", u"VK_DOWN", nullptr, nullptr, u"cmd_moveDown"}, // Win, Linux, Android, Emacs +#endif // Except MOZ_WIDGET_COCOA + + /************************************************************************** + * Arrow keys to select a char/line in HTMLEditor. + **************************************************************************/ +#if defined(XP_WIN) || defined(MOZ_WIDGET_GTK) || \ + defined(MOZ_WIDGET_ANDROID) || defined(USE_EMACS_KEY_BINDINGS) + {u"keypress", u"VK_LEFT", nullptr, u"shift", u"cmd_selectLeft"}, // Win, Linux, Android, Emacs + {u"keypress", u"VK_RIGHT", nullptr, u"shift", u"cmd_selectRight"}, // Win, Linux, Android, Emacs + {u"keypress", u"VK_UP", nullptr, u"shift", u"cmd_selectUp"}, // Win, Linux, Android, Emacs + {u"keypress", u"VK_DOWN", nullptr, u"shift", u"cmd_selectDown"}, // Win, Linux, Android, Emacs +#endif // Except MOZ_WIDGET_COCOA + + /************************************************************************** + * Arrow keys per word in HTMLEditor. + **************************************************************************/ +#if defined(MOZ_WIDGET_ANDROID) || defined(USE_EMACS_KEY_BINDINGS) + {u"keypress", u"VK_LEFT", nullptr, u"control", u"cmd_wordPrevious"}, // Android, Emacs + {u"keypress", u"VK_RIGHT", nullptr, u"control", u"cmd_wordNext"}, // Android, Emacs + {u"keypress", u"VK_LEFT", nullptr, u"shift,control", u"cmd_selectWordPrevious"}, // Android, Emacs + {u"keypress", u"VK_RIGHT", nullptr, u"shift,control", u"cmd_selectWordNext"}, // Android, Emacs +#endif // MOZ_WIDGET_ANDROID || USE_EMACS_KEY_BINDINGS +#if defined(XP_WIN) + {u"keypress", u"VK_LEFT", nullptr, u"accel", u"cmd_moveLeft2"}, // Win + {u"keypress", u"VK_RIGHT", nullptr, u"accel", u"cmd_moveRight2"}, // Win + {u"keypress", u"VK_LEFT", nullptr, u"shift,accel", u"cmd_selectLeft2"}, // Win + {u"keypress", u"VK_RIGHT", nullptr, u"shift,accel", u"cmd_selectRight2"}, // Win +#endif // XP_WIN + + /************************************************************************** + * Arrow keys per block in HTMLEditor. + **************************************************************************/ +#if defined(MOZ_WIDGET_ANDROID) + {u"keypress", u"VK_UP", nullptr, u"alt", u"cmd_moveTop"}, // Android + {u"keypress", u"VK_DOWN", nullptr, u"alt", u"cmd_moveBottom"}, // Android + {u"keypress", u"VK_UP", nullptr, u"shift,alt", u"cmd_selectTop"}, // Android + {u"keypress", u"VK_DOWN", nullptr, u"shift,alt", u"cmd_selectBottom"}, // Android +#endif // MOZ_WIDGET_ANDROID +#if defined(XP_WIN) + {u"keypress", u"VK_UP", nullptr, u"accel", u"cmd_moveUp2"}, // Win + {u"keypress", u"VK_DOWN", nullptr, u"accel", u"cmd_moveDown2"}, // Win + {u"keypress", u"VK_UP", nullptr, u"shift,accel", u"cmd_selectUp2"}, // Win + {u"keypress", u"VK_DOWN", nullptr, u"shift,accel", u"cmd_selectDown2"}, // Win +#endif // XP_WIN + + /************************************************************************** + * Arrow keys to begin/end of a line in HTMLEditor. + **************************************************************************/ +#if defined(MOZ_WIDGET_ANDROID) + {u"keypress", u"VK_LEFT", nullptr, u"alt", u"cmd_beginLine"}, // Android + {u"keypress", u"VK_RIGHT", nullptr, u"alt", u"cmd_endLine"}, // Android + {u"keypress", u"VK_LEFT", nullptr, u"shift,alt", u"cmd_selectBeginLine"}, // Android + {u"keypress", u"VK_RIGHT", nullptr, u"shift,alt", u"cmd_selectEndLine"}, // Android +#endif // MOZ_WIDGET_ANDROID + + /************************************************************************** + * PageUp/PageDown keys in HTMLEditor. + **************************************************************************/ +#if defined(XP_WIN) || defined(MOZ_WIDGET_ANDROID) ||\ + defined(USE_EMACS_KEY_BINDINGS) + {u"keypress", u"VK_PAGE_UP", nullptr, nullptr, u"cmd_movePageUp"}, // Win, Android, Emacs + {u"keypress", u"VK_PAGE_DOWN", nullptr, nullptr, u"cmd_movePageDown"}, // Win, Android, Emacs + {u"keypress", u"VK_PAGE_UP", nullptr, u"shift", u"cmd_selectPageUp"}, // Win, Android, Emacs + {u"keypress", u"VK_PAGE_DOWN", nullptr, u"shift", u"cmd_selectPageDown"}, // Win, Android, Emacs +#endif // XP_WIN || MOZ_WIDGET_ANDROID || USE_EMACS_KEY_BINDINGS +#if defined(MOZ_WIDGET_ANDROID) + {u"keypress", u"VK_PAGE_UP", nullptr, u"alt", u"cmd_moveTop"}, // Android + {u"keypress", u"VK_PAGE_DOWN", nullptr, u"alt", u"cmd_moveBottom"}, // Android + {u"keypress", u"VK_PAGE_UP", nullptr, u"shift,alt", u"cmd_selectTop"}, // Android + {u"keypress", u"VK_PAGE_DOWN", nullptr, u"shift,alt", u"cmd_selectBottom"}, // Android +#endif // MOZ_WIDGET_ANDROID + + /************************************************************************** + * Home/End keys in HTMLEditor. + **************************************************************************/ +#if defined(XP_WIN) || defined(MOZ_WIDGET_ANDROID) ||\ + defined(USE_EMACS_KEY_BINDINGS) + {u"keypress", u"VK_HOME", nullptr, nullptr, u"cmd_beginLine"}, // Win, Android, Emacs + {u"keypress", u"VK_END", nullptr, nullptr, u"cmd_endLine"}, // Win, Android, Emacs + {u"keypress", u"VK_HOME", nullptr, u"shift", u"cmd_selectBeginLine"}, // Win, Android, Emacs + {u"keypress", u"VK_END", nullptr, u"shift", u"cmd_selectEndLine"}, // Win, Android, Emacs + {u"keypress", u"VK_HOME", nullptr, u"control", u"cmd_moveTop"}, // Win, Android, Emacs + {u"keypress", u"VK_END", nullptr, u"control", u"cmd_moveBottom"}, // Win, Android, Emacs + {u"keypress", u"VK_HOME", nullptr, u"shift,control", u"cmd_selectTop"}, // Win, Android, Emacs + {u"keypress", u"VK_END", nullptr, u"shift,control", u"cmd_selectBottom"}, // Win, Android, Emacs +#endif // XP_WIN || MOZ_WIDGET_ANDROID || USE_EMACS_KEY_BINDINGS + + /************************************************************************** + * Insert key in HTMLEditor. + **************************************************************************/ +#if defined(XP_WIN) || defined(USE_EMACS_KEY_BINDINGS) + {u"keypress", u"VK_INSERT", nullptr, u"control", u"cmd_copy"}, // Win, Emacs + {u"keypress", u"VK_INSERT", nullptr, u"shift", u"cmd_paste"}, // Win, Emacs +#endif // XP_WIN || USE_EMACS_KEY_BINDINGS + + /************************************************************************** + * Delete key in HTMLEditor. + **************************************************************************/ +#if defined(XP_WIN) || defined(USE_EMACS_KEY_BINDINGS) + {u"keypress", u"VK_DELETE", nullptr, u"shift", u"cmd_cutOrDelete"}, // Win, Emacs +#endif // XP_WIN || USE_EMACS_KEY_BINDINGS +#if defined(USE_EMACS_KEY_BINDINGS) + {u"keypress", u"VK_DELETE", nullptr, u"control", u"cmd_copyOrDelete"}, // Emacs +#endif // USE_EMACS_KEY_BINDINGS +#if defined(XP_WIN) || defined(MOZ_WIDGET_ANDROID) + {u"keypress", u"VK_DELETE", nullptr, u"control", u"cmd_deleteWordForward"}, // Win, Android +#endif // XP_WIN || MOZ_WIDGET_ANDROID +#if defined(MOZ_WIDGET_ANDROID) + {u"keypress", u"VK_DELETE", nullptr, u"alt", u"cmd_deleteToEndOfLine"}, // Android +#endif // MOZ_WIDGET_ANDROID + + /************************************************************************** + * Backspace key in HTMLEditor. + **************************************************************************/ +#if defined(XP_WIN) || defined(MOZ_WIDGET_ANDROID) || defined(USE_EMACS_KEY_BINDINGS) + {u"keypress", u"VK_BACK", nullptr, u"control", u"cmd_deleteWordBackward"}, // Win, Android, Emacs +#endif // XP_WIN || MOZ_WIDGET_ANDROID || USE_EMACS_KEY_BINDINGS +#if defined(MOZ_WIDGET_ANDROID) + {u"keypress", u"VK_BACK", nullptr, u"alt", u"cmd_deleteToBeginningOfLine"}, // Android +#endif // MOZ_WIDGET_ANDROID +#if defined(XP_WIN) + {u"keypress", u"VK_BACK", nullptr, u"alt", u"cmd_undo"}, // Win + {u"keypress", u"VK_BACK", nullptr, u"alt,shift", u"cmd_redo"}, // Win +#endif // XP_WIN + + /************************************************************************** + * Common editor commands in HTMLEditor. + **************************************************************************/ + {u"keypress", nullptr, u"c", u"accel", u"cmd_copy"}, // Win, macOS, Linux, Android, Emacs + {u"keypress", nullptr, u"x", u"accel", u"cmd_cut"}, // Win, macOS, Linux, Android, Emacs + {u"keypress", nullptr, u"v", u"accel", u"cmd_paste"}, // Win, macOS, Linux, Android, Emacs + {u"keypress", nullptr, u"v", u"accel,shift", u"cmd_pasteNoFormatting"}, // Win, macOS, Linux, Android, Emacs + {u"keypress", nullptr, u"z", u"accel", u"cmd_undo"}, // Win, macOS, Linux, Android, Emacs + {u"keypress", nullptr, u"z", u"accel,shift", u"cmd_redo"}, // Win, macOS, Linux, Android, Emacs + +// Mac uses Option+Shift+Command+V for Paste and Match Style +#if defined(MOZ_WIDGET_COCOA) + {u"keypress", nullptr, u"v", u"accel,alt,shift", u"cmd_pasteNoFormatting"}, // macOS +#endif // MOZ_WIDGET_COCOA + +#if defined(XP_WIN) || defined(MOZ_WIDGET_GTK) ||\ + defined(USE_EMACS_KEY_BINDINGS) + {u"keypress", nullptr, u"y", u"accel", u"cmd_redo"}, // Emacs +#endif // XP_WIN || MOZ_WIDGET_GTK || USE_EMACS_KEY_BINDINGS + +#if defined(XP_WIN) || defined(MOZ_WIDGET_COCOA) || defined(MOZ_WIDGET_GTK) ||\ + defined(MOZ_WIDGET_ANDROID) + {u"keypress", nullptr, u"a", u"accel", u"cmd_selectAll"}, // Win, macOS, Linux, Android +#endif // XP_WIN || MOZ_WIDGET_COCOA || MOZ_WIDGET_GTK || MOZ_WIDGET_ANDROID +#if defined(USE_EMACS_KEY_BINDINGS) + {u"keypress", nullptr, u"a", u"alt", u"cmd_selectAll"}, // Emacs +#endif // USE_EMACS_KEY_BINDINGS + + /************************************************************************** + * Space key in HTMLEditor. + **************************************************************************/ + {u"keypress", nullptr, u" ", nullptr, u"cmd_scrollPageDown"}, // Win, macOS, Linux, Android, Emacs + {u"keypress", nullptr, u" ", u"shift", u"cmd_scrollPageUp"}, // Win, macOS, Linux, Android, Emacs + + /************************************************************************** + * Emacs specific shortcut keys in HTMLEditor. + **************************************************************************/ +#if defined(USE_EMACS_KEY_BINDINGS) + {u"keypress", nullptr, u"h", u"control", u"cmd_deleteCharBackward"}, // Emacs + {u"keypress", nullptr, u"d", u"control", u"cmd_deleteCharForward"}, // Emacs + {u"keypress", nullptr, u"k", u"control", u"cmd_deleteToEndOfLine"}, // Emacs + {u"keypress", nullptr, u"u", u"control", u"cmd_deleteToBeginningOfLine"}, // Emacs + {u"keypress", nullptr, u"a", u"control", u"cmd_beginLine"}, // Emacs + {u"keypress", nullptr, u"e", u"control", u"cmd_endLine"}, // Emacs + {u"keypress", nullptr, u"b", u"control", u"cmd_charPrevious"}, // Emacs + {u"keypress", nullptr, u"f", u"control", u"cmd_charNext"}, // Emacs + {u"keypress", nullptr, u"p", u"control", u"cmd_linePrevious"}, // Emacs + {u"keypress", nullptr, u"n", u"control", u"cmd_lineNext"}, // Emacs +#endif // USE_EMACS_KEY_BINDINGS + // clang-format on + + {nullptr, nullptr, nullptr, nullptr, nullptr}}; + +} // namespace mozilla + +#undef USE_EMACS_KEY_BINDINGS diff --git a/dom/events/ShortcutKeys.cpp b/dom/events/ShortcutKeys.cpp new file mode 100644 index 0000000000..20b9d039d1 --- /dev/null +++ b/dom/events/ShortcutKeys.cpp @@ -0,0 +1,120 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#include "ShortcutKeys.h" +#include "mozilla/KeyEventHandler.h" +#include "nsContentUtils.h" +#include "nsAtom.h" +#include "mozilla/TextEvents.h" + +namespace mozilla { + +NS_IMPL_ISUPPORTS(ShortcutKeys, nsIObserver); + +StaticRefPtr<ShortcutKeys> ShortcutKeys::sInstance; + +ShortcutKeys::ShortcutKeys() + : mBrowserHandlers(nullptr), + mEditorHandlers(nullptr), + mInputHandlers(nullptr), + mTextAreaHandlers(nullptr) { + MOZ_ASSERT(!sInstance, "Attempt to instantiate a second ShortcutKeys."); + nsContentUtils::RegisterShutdownObserver(this); +} + +ShortcutKeys::~ShortcutKeys() { + delete mBrowserHandlers; + delete mEditorHandlers; + delete mInputHandlers; + delete mTextAreaHandlers; +} + +nsresult ShortcutKeys::Observe(nsISupports* aSubject, const char* aTopic, + const char16_t* aData) { + // Clear our strong reference so we can clean up. + ShortcutKeys::Shutdown(); + return NS_OK; +} + +void ShortcutKeys::Shutdown() { sInstance = nullptr; } + +/* static */ +KeyEventHandler* ShortcutKeys::GetHandlers(HandlerType aType) { + if (!sInstance) { + sInstance = new ShortcutKeys(); + } + + return sInstance->EnsureHandlers(aType); +} + +/* static */ +nsAtom* ShortcutKeys::ConvertEventToDOMEventType( + const WidgetKeyboardEvent* aWidgetKeyboardEvent) { + switch (aWidgetKeyboardEvent->mMessage) { + case eKeyDown: + return nsGkAtoms::keydown; + case eKeyUp: + return nsGkAtoms::keyup; + // eAccessKeyNotFound event is always created from eKeyPress event and + // the original eKeyPress event has stopped its propagation before + // dispatched into the DOM tree in this process and not matched with remote + // content's access keys. So, we should treat it as an eKeyPress event and + // execute a command if it's registered as a shortcut key. + case eKeyPress: + case eAccessKeyNotFound: + return nsGkAtoms::keypress; + default: + MOZ_ASSERT_UNREACHABLE( + "All event messages relating to shortcut keys should be handled"); + return nullptr; + } +} + +KeyEventHandler* ShortcutKeys::EnsureHandlers(HandlerType aType) { + ShortcutKeyData* keyData; + KeyEventHandler** cache; + + switch (aType) { + case HandlerType::eBrowser: + keyData = &sBrowserHandlers[0]; + cache = &mBrowserHandlers; + break; + case HandlerType::eEditor: + keyData = &sEditorHandlers[0]; + cache = &mEditorHandlers; + break; + case HandlerType::eInput: + keyData = &sInputHandlers[0]; + cache = &mInputHandlers; + break; + case HandlerType::eTextArea: + keyData = &sTextAreaHandlers[0]; + cache = &mTextAreaHandlers; + break; + default: + MOZ_ASSERT(false, "Unknown handler type requested."); + } + + if (*cache) { + return *cache; + } + + KeyEventHandler* lastHandler = nullptr; + while (keyData->event) { + KeyEventHandler* handler = new KeyEventHandler(keyData); + if (lastHandler) { + lastHandler->SetNextHandler(handler); + } else { + *cache = handler; + } + lastHandler = handler; + keyData++; + } + + return *cache; +} + +} // namespace mozilla diff --git a/dom/events/ShortcutKeys.h b/dom/events/ShortcutKeys.h new file mode 100644 index 0000000000..51d844bd07 --- /dev/null +++ b/dom/events/ShortcutKeys.h @@ -0,0 +1,70 @@ +/* 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/. */ + +#ifndef mozilla_ShortcutKeys_h +#define mozilla_ShortcutKeys_h + +#include "nsIObserver.h" + +class nsAtom; + +namespace mozilla { +class KeyEventHandler; +class WidgetKeyboardEvent; + +struct ShortcutKeyData { + const char16_t* event; + const char16_t* keycode; + const char16_t* key; + const char16_t* modifiers; + const char16_t* command; +}; + +enum class HandlerType { + eInput, + eTextArea, + eBrowser, + eEditor, +}; + +class ShortcutKeys : public nsIObserver { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIOBSERVER + + // Returns a pointer to the first handler for the given type. + static KeyEventHandler* GetHandlers(HandlerType aType); + + // Gets the event type for a widget keyboard event. + static nsAtom* ConvertEventToDOMEventType( + const WidgetKeyboardEvent* aWidgetKeyboardEvent); + + static void Shutdown(); + + protected: + ShortcutKeys(); + virtual ~ShortcutKeys(); + + // Returns a pointer to the first handler for the given type. + KeyEventHandler* EnsureHandlers(HandlerType aType); + + // Maintains a strong reference to the only instance. + static StaticRefPtr<ShortcutKeys> sInstance; + + // Shortcut keys for different elements. + static ShortcutKeyData sBrowserHandlers[]; + static ShortcutKeyData sEditorHandlers[]; + static ShortcutKeyData sInputHandlers[]; + static ShortcutKeyData sTextAreaHandlers[]; + + // Cached event handlers generated from the above data. + KeyEventHandler* mBrowserHandlers; + KeyEventHandler* mEditorHandlers; + KeyEventHandler* mInputHandlers; + KeyEventHandler* mTextAreaHandlers; +}; + +} // namespace mozilla + +#endif // #ifndef mozilla_ShortcutKeys_h diff --git a/dom/events/SimpleGestureEvent.cpp b/dom/events/SimpleGestureEvent.cpp new file mode 100644 index 0000000000..3395cb7dbb --- /dev/null +++ b/dom/events/SimpleGestureEvent.cpp @@ -0,0 +1,86 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#include "mozilla/dom/MouseEventBinding.h" +#include "mozilla/dom/SimpleGestureEvent.h" +#include "mozilla/TouchEvents.h" +#include "prtime.h" + +namespace mozilla::dom { + +SimpleGestureEvent::SimpleGestureEvent(EventTarget* aOwner, + nsPresContext* aPresContext, + WidgetSimpleGestureEvent* aEvent) + : MouseEvent( + aOwner, aPresContext, + aEvent ? aEvent + : new WidgetSimpleGestureEvent(false, eVoidEvent, nullptr)) { + NS_ASSERTION(mEvent->mClass == eSimpleGestureEventClass, + "event type mismatch"); + + if (aEvent) { + mEventIsInternal = false; + } else { + mEventIsInternal = true; + mEvent->mRefPoint = LayoutDeviceIntPoint(0, 0); + static_cast<WidgetMouseEventBase*>(mEvent)->mInputSource = + MouseEvent_Binding::MOZ_SOURCE_UNKNOWN; + } +} + +uint32_t SimpleGestureEvent::AllowedDirections() const { + return mEvent->AsSimpleGestureEvent()->mAllowedDirections; +} + +void SimpleGestureEvent::SetAllowedDirections(uint32_t aAllowedDirections) { + mEvent->AsSimpleGestureEvent()->mAllowedDirections = aAllowedDirections; +} + +uint32_t SimpleGestureEvent::Direction() const { + return mEvent->AsSimpleGestureEvent()->mDirection; +} + +double SimpleGestureEvent::Delta() const { + return mEvent->AsSimpleGestureEvent()->mDelta; +} + +uint32_t SimpleGestureEvent::ClickCount() const { + return mEvent->AsSimpleGestureEvent()->mClickCount; +} + +void SimpleGestureEvent::InitSimpleGestureEvent( + const nsAString& aTypeArg, bool aCanBubbleArg, bool aCancelableArg, + nsGlobalWindowInner* aViewArg, int32_t aDetailArg, int32_t aScreenX, + int32_t aScreenY, int32_t aClientX, int32_t aClientY, bool aCtrlKeyArg, + bool aAltKeyArg, bool aShiftKeyArg, bool aMetaKeyArg, uint16_t aButton, + EventTarget* aRelatedTarget, uint32_t aAllowedDirectionsArg, + uint32_t aDirectionArg, double aDeltaArg, uint32_t aClickCountArg) { + NS_ENSURE_TRUE_VOID(!mEvent->mFlags.mIsBeingDispatched); + + MouseEvent::InitMouseEvent(aTypeArg, aCanBubbleArg, aCancelableArg, aViewArg, + aDetailArg, aScreenX, aScreenY, aClientX, aClientY, + aCtrlKeyArg, aAltKeyArg, aShiftKeyArg, aMetaKeyArg, + aButton, aRelatedTarget); + + WidgetSimpleGestureEvent* simpleGestureEvent = mEvent->AsSimpleGestureEvent(); + simpleGestureEvent->mAllowedDirections = aAllowedDirectionsArg; + simpleGestureEvent->mDirection = aDirectionArg; + simpleGestureEvent->mDelta = aDeltaArg; + simpleGestureEvent->mClickCount = aClickCountArg; +} + +} // namespace mozilla::dom + +using namespace mozilla; +using namespace mozilla::dom; + +already_AddRefed<SimpleGestureEvent> NS_NewDOMSimpleGestureEvent( + EventTarget* aOwner, nsPresContext* aPresContext, + WidgetSimpleGestureEvent* aEvent) { + RefPtr<SimpleGestureEvent> it = + new SimpleGestureEvent(aOwner, aPresContext, aEvent); + return it.forget(); +} diff --git a/dom/events/SimpleGestureEvent.h b/dom/events/SimpleGestureEvent.h new file mode 100644 index 0000000000..8b3cb99702 --- /dev/null +++ b/dom/events/SimpleGestureEvent.h @@ -0,0 +1,56 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#ifndef mozilla_dom_SimpleGestureEvent_h_ +#define mozilla_dom_SimpleGestureEvent_h_ + +#include "mozilla/dom/MouseEvent.h" +#include "mozilla/dom/SimpleGestureEventBinding.h" +#include "mozilla/EventForwards.h" + +class nsPresContext; + +namespace mozilla::dom { + +class SimpleGestureEvent : public MouseEvent { + public: + SimpleGestureEvent(EventTarget* aOwner, nsPresContext* aPresContext, + WidgetSimpleGestureEvent* aEvent); + + NS_INLINE_DECL_REFCOUNTING_INHERITED(SimpleGestureEvent, MouseEvent) + + virtual JSObject* WrapObjectInternal( + JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override { + return SimpleGestureEvent_Binding::Wrap(aCx, this, aGivenProto); + } + + uint32_t AllowedDirections() const; + void SetAllowedDirections(uint32_t aAllowedDirections); + uint32_t Direction() const; + double Delta() const; + uint32_t ClickCount() const; + + void InitSimpleGestureEvent(const nsAString& aType, bool aCanBubble, + bool aCancelable, nsGlobalWindowInner* aView, + int32_t aDetail, int32_t aScreenX, + int32_t aScreenY, int32_t aClientX, + int32_t aClientY, bool aCtrlKey, bool aAltKey, + bool aShiftKey, bool aMetaKey, uint16_t aButton, + EventTarget* aRelatedTarget, + uint32_t aAllowedDirections, uint32_t aDirection, + double aDelta, uint32_t aClickCount); + + protected: + ~SimpleGestureEvent() = default; +}; + +} // namespace mozilla::dom + +already_AddRefed<mozilla::dom::SimpleGestureEvent> NS_NewDOMSimpleGestureEvent( + mozilla::dom::EventTarget* aOwner, nsPresContext* aPresContext, + mozilla::WidgetSimpleGestureEvent* aEvent); + +#endif // mozilla_dom_SimpleGestureEvent_h_ diff --git a/dom/events/SpeechRecognitionError.cpp b/dom/events/SpeechRecognitionError.cpp new file mode 100644 index 0000000000..c01c2efd92 --- /dev/null +++ b/dom/events/SpeechRecognitionError.cpp @@ -0,0 +1,46 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#include "SpeechRecognitionError.h" + +namespace mozilla::dom { + +SpeechRecognitionError::SpeechRecognitionError( + mozilla::dom::EventTarget* aOwner, nsPresContext* aPresContext, + WidgetEvent* aEvent) + : Event(aOwner, aPresContext, aEvent), mError() {} + +SpeechRecognitionError::~SpeechRecognitionError() = default; + +already_AddRefed<SpeechRecognitionError> SpeechRecognitionError::Constructor( + const GlobalObject& aGlobal, const nsAString& aType, + const SpeechRecognitionErrorInit& aParam) { + nsCOMPtr<mozilla::dom::EventTarget> t = + do_QueryInterface(aGlobal.GetAsSupports()); + RefPtr<SpeechRecognitionError> e = + new SpeechRecognitionError(t, nullptr, nullptr); + bool trusted = e->Init(t); + e->InitSpeechRecognitionError(aType, aParam.mBubbles, aParam.mCancelable, + aParam.mError, + NS_ConvertUTF16toUTF8(aParam.mMessage)); + e->SetTrusted(trusted); + e->SetComposed(aParam.mComposed); + return e.forget(); +} + +void SpeechRecognitionError::GetMessage(nsAString& aString) { + CopyUTF8toUTF16(mMessage, aString); +} + +void SpeechRecognitionError::InitSpeechRecognitionError( + const nsAString& aType, bool aCanBubble, bool aCancelable, + SpeechRecognitionErrorCode aError, const nsACString& aMessage) { + Event::InitEvent(aType, aCanBubble, aCancelable); + mError = aError; + mMessage = aMessage; +} + +} // namespace mozilla::dom diff --git a/dom/events/SpeechRecognitionError.h b/dom/events/SpeechRecognitionError.h new file mode 100644 index 0000000000..dbd5401f74 --- /dev/null +++ b/dom/events/SpeechRecognitionError.h @@ -0,0 +1,48 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#ifndef SpeechRecognitionError_h__ +#define SpeechRecognitionError_h__ + +#include "mozilla/dom/Event.h" +#include "mozilla/dom/SpeechRecognitionErrorBinding.h" + +namespace mozilla::dom { + +class SpeechRecognitionError : public Event { + public: + SpeechRecognitionError(mozilla::dom::EventTarget* aOwner, + nsPresContext* aPresContext, WidgetEvent* aEvent); + virtual ~SpeechRecognitionError(); + + static already_AddRefed<SpeechRecognitionError> Constructor( + const GlobalObject& aGlobal, const nsAString& aType, + const SpeechRecognitionErrorInit& aParam); + + virtual JSObject* WrapObjectInternal( + JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override { + return mozilla::dom::SpeechRecognitionError_Binding::Wrap(aCx, this, + aGivenProto); + } + + void GetMessage(nsAString& aString); + + SpeechRecognitionErrorCode Error() { return mError; } + // aMessage should be valid UTF-8, but invalid UTF-8 byte sequences are + // replaced with the REPLACEMENT CHARACTER on conversion to UTF-16. + void InitSpeechRecognitionError(const nsAString& aType, bool aCanBubble, + bool aCancelable, + SpeechRecognitionErrorCode aError, + const nsACString& aMessage); + + protected: + SpeechRecognitionErrorCode mError; + nsCString mMessage; +}; + +} // namespace mozilla::dom + +#endif // SpeechRecognitionError_h__ diff --git a/dom/events/StorageEvent.cpp b/dom/events/StorageEvent.cpp new file mode 100644 index 0000000000..6854400293 --- /dev/null +++ b/dom/events/StorageEvent.cpp @@ -0,0 +1,84 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#include "mozilla/dom/StorageEvent.h" +#include "mozilla/dom/Storage.h" +#include "mozilla/dom/StorageEventBinding.h" + +namespace mozilla::dom { + +NS_IMPL_CYCLE_COLLECTION_CLASS(StorageEvent) + +NS_IMPL_ADDREF_INHERITED(StorageEvent, Event) +NS_IMPL_RELEASE_INHERITED(StorageEvent, Event) + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(StorageEvent, Event) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mStorageArea) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN_INHERITED(StorageEvent, Event) +NS_IMPL_CYCLE_COLLECTION_TRACE_END + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(StorageEvent, Event) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mStorageArea) +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(StorageEvent) +NS_INTERFACE_MAP_END_INHERITING(Event) + +StorageEvent::StorageEvent(EventTarget* aOwner) + : Event(aOwner, nullptr, nullptr) {} + +StorageEvent::~StorageEvent() = default; + +StorageEvent* StorageEvent::AsStorageEvent() { return this; } + +JSObject* StorageEvent::WrapObjectInternal(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return StorageEvent_Binding::Wrap(aCx, this, aGivenProto); +} + +already_AddRefed<StorageEvent> StorageEvent::Constructor( + EventTarget* aOwner, const nsAString& aType, + const StorageEventInit& aEventInitDict) { + RefPtr<StorageEvent> e = new StorageEvent(aOwner); + + bool trusted = e->Init(aOwner); + e->InitEvent(aType, aEventInitDict.mBubbles, aEventInitDict.mCancelable); + e->mKey = aEventInitDict.mKey; + e->mOldValue = aEventInitDict.mOldValue; + e->mNewValue = aEventInitDict.mNewValue; + e->mUrl = aEventInitDict.mUrl; + e->mStorageArea = aEventInitDict.mStorageArea; + e->SetTrusted(trusted); + e->SetComposed(aEventInitDict.mComposed); + return e.forget(); +} + +already_AddRefed<StorageEvent> StorageEvent::Constructor( + const GlobalObject& aGlobal, const nsAString& aType, + const StorageEventInit& aEventInitDict) { + nsCOMPtr<EventTarget> owner = do_QueryInterface(aGlobal.GetAsSupports()); + return Constructor(owner, aType, aEventInitDict); +} + +void StorageEvent::InitStorageEvent(const nsAString& aType, bool aCanBubble, + bool aCancelable, const nsAString& aKey, + const nsAString& aOldValue, + const nsAString& aNewValue, + const nsAString& aURL, + Storage* aStorageArea) { + NS_ENSURE_TRUE_VOID(!mEvent->mFlags.mIsBeingDispatched); + + InitEvent(aType, aCanBubble, aCancelable); + mKey = aKey; + mOldValue = aOldValue; + mNewValue = aNewValue; + mUrl = aURL; + mStorageArea = aStorageArea; +} + +} // namespace mozilla::dom diff --git a/dom/events/StorageEvent.h b/dom/events/StorageEvent.h new file mode 100644 index 0000000000..255a4a8c27 --- /dev/null +++ b/dom/events/StorageEvent.h @@ -0,0 +1,85 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#ifndef mozilla_dom_StorageEvent_h +#define mozilla_dom_StorageEvent_h + +#include "js/RootingAPI.h" +#include "mozilla/AlreadyAddRefed.h" +#include "mozilla/Assertions.h" +#include "mozilla/RefPtr.h" +#include "mozilla/dom/Event.h" +#include "nsCOMPtr.h" +#include "nsCycleCollectionParticipant.h" +#include "nsIPrincipal.h" +#include "nsISupports.h" +#include "nsStringFwd.h" + +class nsIPrincipal; + +namespace mozilla::dom { + +class Storage; +struct StorageEventInit; + +class StorageEvent : public Event { + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS_INHERITED(StorageEvent, Event) + + explicit StorageEvent(EventTarget* aOwner); + + protected: + virtual ~StorageEvent(); + + nsString mKey; + nsString mOldValue; + nsString mNewValue; + nsString mUrl; + RefPtr<Storage> mStorageArea; + nsCOMPtr<nsIPrincipal> mPrincipal; + + public: + virtual StorageEvent* AsStorageEvent(); + + virtual JSObject* WrapObjectInternal( + JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override; + + static already_AddRefed<StorageEvent> Constructor( + EventTarget* aOwner, const nsAString& aType, + const StorageEventInit& aEventInitDict); + + static already_AddRefed<StorageEvent> Constructor( + const GlobalObject& aGlobal, const nsAString& aType, + const StorageEventInit& aEventInitDict); + + void InitStorageEvent(const nsAString& aType, bool aCanBubble, + bool aCancelable, const nsAString& aKey, + const nsAString& aOldValue, const nsAString& aNewValue, + const nsAString& aURL, Storage* aStorageArea); + + void GetKey(nsString& aRetVal) const { aRetVal = mKey; } + + void GetOldValue(nsString& aRetVal) const { aRetVal = mOldValue; } + + void GetNewValue(nsString& aRetVal) const { aRetVal = mNewValue; } + + void GetUrl(nsString& aRetVal) const { aRetVal = mUrl; } + + Storage* GetStorageArea() const { return mStorageArea; } + + // Non WebIDL methods + void SetPrincipal(nsIPrincipal* aPrincipal) { + MOZ_ASSERT(!mPrincipal); + mPrincipal = aPrincipal; + } + + nsIPrincipal* GetPrincipal() const { return mPrincipal; } +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_StorageEvent_h diff --git a/dom/events/TextClause.cpp b/dom/events/TextClause.cpp new file mode 100644 index 0000000000..45154d2434 --- /dev/null +++ b/dom/events/TextClause.cpp @@ -0,0 +1,45 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#include "mozilla/dom/TextClause.h" +#include "mozilla/dom/TextClauseBinding.h" +#include "mozilla/TextEvents.h" + +namespace mozilla::dom { + +// Only needed for refcounted objects. +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_0(TextClause) +NS_IMPL_CYCLE_COLLECTING_ADDREF(TextClause) +NS_IMPL_CYCLE_COLLECTING_RELEASE(TextClause) +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(TextClause) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +TextClause::TextClause(nsPIDOMWindowInner* aOwner, const TextRange& aRange, + const TextRange* aTargetRange) + : mOwner(aOwner), mIsTargetClause(false) { + MOZ_ASSERT(aOwner); + mStartOffset = aRange.mStartOffset; + mEndOffset = aRange.mEndOffset; + if (aRange.IsClause()) { + mIsCaret = false; + if (aTargetRange && aTargetRange->mStartOffset == mStartOffset) { + mIsTargetClause = true; + } + } else { + mIsCaret = true; + } +} + +JSObject* TextClause::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return TextClause_Binding::Wrap(aCx, this, aGivenProto); +} + +TextClause::~TextClause() = default; + +} // namespace mozilla::dom diff --git a/dom/events/TextClause.h b/dom/events/TextClause.h new file mode 100644 index 0000000000..18a3c89392 --- /dev/null +++ b/dom/events/TextClause.h @@ -0,0 +1,53 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#ifndef mozilla_dom_TextClause_h +#define mozilla_dom_TextClause_h + +#include "js/TypeDecls.h" +#include "mozilla/Attributes.h" +#include "mozilla/dom/BindingDeclarations.h" +#include "nsCycleCollectionParticipant.h" +#include "nsPIDOMWindow.h" +#include "nsWrapperCache.h" + +namespace mozilla::dom { + +class TextClause final : public nsISupports, public nsWrapperCache { + public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(TextClause) + + nsPIDOMWindowInner* GetParentObject() const { return mOwner; } + + TextClause(nsPIDOMWindowInner* aWindow, const TextRange& aRange, + const TextRange* targetRange); + + virtual JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + inline uint32_t StartOffset() const { return mStartOffset; } + + inline uint32_t EndOffset() const { return mEndOffset; } + + inline bool IsCaret() const { return mIsCaret; } + + inline bool IsTargetClause() const { return mIsTargetClause; } + + private: + ~TextClause(); + nsCOMPtr<nsPIDOMWindowInner> mOwner; + + // Following members, please take look at widget/TextRange.h. + uint32_t mStartOffset; + uint32_t mEndOffset; + bool mIsCaret; + bool mIsTargetClause; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_TextClause_h diff --git a/dom/events/TextComposition.cpp b/dom/events/TextComposition.cpp new file mode 100644 index 0000000000..68cd63d078 --- /dev/null +++ b/dom/events/TextComposition.cpp @@ -0,0 +1,1045 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#include "ContentEventHandler.h" +#include "IMEContentObserver.h" +#include "IMEStateManager.h" +#include "nsContentUtils.h" +#include "nsIContent.h" +#include "nsIMutationObserver.h" +#include "nsPresContext.h" +#include "mozilla/AutoRestore.h" +#include "mozilla/EditorBase.h" +#include "mozilla/EventDispatcher.h" +#include "mozilla/IMEStateManager.h" +#include "mozilla/IntegerRange.h" +#include "mozilla/MiscEvents.h" +#include "mozilla/PresShell.h" +#include "mozilla/RangeBoundary.h" +#include "mozilla/StaticPrefs_dom.h" +#include "mozilla/StaticPrefs_intl.h" +#include "mozilla/TextComposition.h" +#include "mozilla/TextEvents.h" +#include "mozilla/Unused.h" +#include "mozilla/dom/BrowserParent.h" + +#ifdef XP_MACOSX +// Some defiens will be conflict with OSX SDK +# define TextRange _TextRange +# define TextRangeArray _TextRangeArray +# define Comment _Comment +#endif + +#ifdef XP_MACOSX +# undef TextRange +# undef TextRangeArray +# undef Comment +#endif + +using namespace mozilla::widget; + +namespace mozilla { + +#define IDEOGRAPHIC_SPACE (u"\x3000"_ns) + +/****************************************************************************** + * TextComposition + ******************************************************************************/ + +bool TextComposition::sHandlingSelectionEvent = false; + +TextComposition::TextComposition(nsPresContext* aPresContext, nsINode* aNode, + BrowserParent* aBrowserParent, + WidgetCompositionEvent* aCompositionEvent) + : mPresContext(aPresContext), + mNode(aNode), + mBrowserParent(aBrowserParent), + mNativeContext(aCompositionEvent->mNativeIMEContext), + mCompositionStartOffset(0), + mTargetClauseOffsetInComposition(0), + mCompositionStartOffsetInTextNode(UINT32_MAX), + mCompositionLengthInTextNode(UINT32_MAX), + mIsSynthesizedForTests(aCompositionEvent->mFlags.mIsSynthesizedForTests), + mIsComposing(false), + mIsEditorHandlingEvent(false), + mIsRequestingCommit(false), + mIsRequestingCancel(false), + mRequestedToCommitOrCancel(false), + mHasDispatchedDOMTextEvent(false), + mHasReceivedCommitEvent(false), + mWasNativeCompositionEndEventDiscarded(false), + mAllowControlCharacters( + StaticPrefs::dom_compositionevent_allow_control_characters()), + mWasCompositionStringEmpty(true) { + MOZ_ASSERT(aCompositionEvent->mNativeIMEContext.IsValid()); +} + +void TextComposition::Destroy() { + mPresContext = nullptr; + mNode = nullptr; + mBrowserParent = nullptr; + mContainerTextNode = nullptr; + mCompositionStartOffsetInTextNode = UINT32_MAX; + mCompositionLengthInTextNode = UINT32_MAX; + // TODO: If the editor is still alive and this is held by it, we should tell + // this being destroyed for cleaning up the stuff. +} + +void TextComposition::OnCharacterDataChanged( + Text& aText, const CharacterDataChangeInfo& aInfo) { + if (mContainerTextNode != &aText || + mCompositionStartOffsetInTextNode == UINT32_MAX || + mCompositionLengthInTextNode == UINT32_MAX) { + return; + } + + // Ignore changes after composition string. + if (aInfo.mChangeStart >= + mCompositionStartOffsetInTextNode + mCompositionLengthInTextNode) { + return; + } + + // If the change ends before the composition string, we need only to adjust + // the start offset. + if (aInfo.mChangeEnd <= mCompositionStartOffsetInTextNode) { + MOZ_ASSERT(aInfo.LengthOfRemovedText() <= + mCompositionStartOffsetInTextNode); + mCompositionStartOffsetInTextNode -= aInfo.LengthOfRemovedText(); + mCompositionStartOffsetInTextNode += aInfo.mReplaceLength; + return; + } + + // If this is caused by a splitting text node, the composition string + // may be split out to the new right node. In the case, + // CompositionTransaction::DoTransaction handles it with warking the + // following text nodes. Therefore, we should NOT shrink the composing + // range for avoind breaking the fix of bug 1310912. Although the handling + // looks buggy so that we need to move the handling into here later. + if (aInfo.mDetails && + aInfo.mDetails->mType == CharacterDataChangeInfo::Details::eSplit) { + return; + } + + // If the change removes/replaces the last character of the composition + // string, we should shrink the composition range before the change start. + // Then, the replace string will be never updated by coming composition + // updates. + if (aInfo.mChangeEnd >= + mCompositionStartOffsetInTextNode + mCompositionLengthInTextNode) { + // If deleting the first character of the composition string, collapse IME + // selection temporarily. Updating composition string will insert new + // composition string there. + if (aInfo.mChangeStart <= mCompositionStartOffsetInTextNode) { + mCompositionStartOffsetInTextNode = aInfo.mChangeStart; + mCompositionLengthInTextNode = 0u; + return; + } + // If some characters in the composition still stay, composition range + // should be shrunken. + MOZ_ASSERT(aInfo.mChangeStart > mCompositionStartOffsetInTextNode); + mCompositionLengthInTextNode = + aInfo.mChangeStart - mCompositionStartOffsetInTextNode; + return; + } + + // If removed range starts in the composition string, we need only adjust + // the length to make composition range contain the replace string. + if (aInfo.mChangeStart >= mCompositionStartOffsetInTextNode) { + MOZ_ASSERT(aInfo.LengthOfRemovedText() <= mCompositionLengthInTextNode); + mCompositionLengthInTextNode -= aInfo.LengthOfRemovedText(); + mCompositionLengthInTextNode += aInfo.mReplaceLength; + return; + } + + // If preceding characers of the composition string is also removed, new + // composition start will be there and new composition ends at current + // position. + const uint32_t removedLengthInCompositionString = + aInfo.mChangeEnd - mCompositionStartOffsetInTextNode; + mCompositionStartOffsetInTextNode = aInfo.mChangeStart; + mCompositionLengthInTextNode -= removedLengthInCompositionString; + mCompositionLengthInTextNode += aInfo.mReplaceLength; +} + +bool TextComposition::IsValidStateForComposition(nsIWidget* aWidget) const { + return !Destroyed() && aWidget && !aWidget->Destroyed() && + mPresContext->GetPresShell() && + !mPresContext->PresShell()->IsDestroying(); +} + +bool TextComposition::MaybeDispatchCompositionUpdate( + const WidgetCompositionEvent* aCompositionEvent) { + MOZ_RELEASE_ASSERT(!mBrowserParent); + + if (!IsValidStateForComposition(aCompositionEvent->mWidget)) { + return false; + } + + // Note that we don't need to dispatch eCompositionUpdate event even if + // mHasDispatchedDOMTextEvent is false and eCompositionCommit event is + // dispatched with empty string immediately after eCompositionStart + // because composition string has never been changed from empty string to + // non-empty string in such composition even if selected string was not + // empty string (mLastData isn't set to selected text when this receives + // eCompositionStart). + if (mLastData == aCompositionEvent->mData) { + return true; + } + CloneAndDispatchAs(aCompositionEvent, eCompositionUpdate); + return IsValidStateForComposition(aCompositionEvent->mWidget); +} + +BaseEventFlags TextComposition::CloneAndDispatchAs( + const WidgetCompositionEvent* aCompositionEvent, EventMessage aMessage, + nsEventStatus* aStatus, EventDispatchingCallback* aCallBack) { + MOZ_RELEASE_ASSERT(!mBrowserParent); + + MOZ_ASSERT(IsValidStateForComposition(aCompositionEvent->mWidget), + "Should be called only when it's safe to dispatch an event"); + + WidgetCompositionEvent compositionEvent(aCompositionEvent->IsTrusted(), + aMessage, aCompositionEvent->mWidget); + compositionEvent.mTimeStamp = aCompositionEvent->mTimeStamp; + compositionEvent.mData = aCompositionEvent->mData; + compositionEvent.mNativeIMEContext = aCompositionEvent->mNativeIMEContext; + compositionEvent.mOriginalMessage = aCompositionEvent->mMessage; + compositionEvent.mFlags.mIsSynthesizedForTests = + aCompositionEvent->mFlags.mIsSynthesizedForTests; + + nsEventStatus dummyStatus = nsEventStatus_eConsumeNoDefault; + nsEventStatus* status = aStatus ? aStatus : &dummyStatus; + if (aMessage == eCompositionUpdate) { + mLastData = compositionEvent.mData; + mLastRanges = aCompositionEvent->mRanges; + } + + DispatchEvent(&compositionEvent, status, aCallBack, aCompositionEvent); + return compositionEvent.mFlags; +} + +void TextComposition::DispatchEvent( + WidgetCompositionEvent* aDispatchEvent, nsEventStatus* aStatus, + EventDispatchingCallback* aCallBack, + const WidgetCompositionEvent* aOriginalEvent) { + if (aDispatchEvent->mMessage == eCompositionChange) { + aDispatchEvent->mFlags.mOnlySystemGroupDispatchInContent = true; + } + RefPtr<nsINode> node = mNode; + RefPtr<nsPresContext> presContext = mPresContext; + EventDispatcher::Dispatch(node, presContext, aDispatchEvent, nullptr, aStatus, + aCallBack); + + OnCompositionEventDispatched(aDispatchEvent); +} + +void TextComposition::OnCompositionEventDiscarded( + WidgetCompositionEvent* aCompositionEvent) { + // Note that this method is never called for synthesized events for emulating + // commit or cancel composition. + + MOZ_ASSERT(aCompositionEvent->IsTrusted(), + "Shouldn't be called with untrusted event"); + + if (mBrowserParent) { + // The composition event should be discarded in the child process too. + Unused << mBrowserParent->SendCompositionEvent(*aCompositionEvent); + } + + // XXX If composition events are discarded, should we dispatch them with + // runnable event? However, even if we do so, it might make native IME + // confused due to async modification. Especially when native IME is + // TSF. + if (!aCompositionEvent->CausesDOMCompositionEndEvent()) { + return; + } + + mWasNativeCompositionEndEventDiscarded = true; +} + +static inline bool IsControlChar(uint32_t aCharCode) { + return aCharCode < ' ' || aCharCode == 0x7F; +} + +static size_t FindFirstControlCharacter(const nsAString& aStr) { + const char16_t* sourceBegin = aStr.BeginReading(); + const char16_t* sourceEnd = aStr.EndReading(); + + for (const char16_t* source = sourceBegin; source < sourceEnd; ++source) { + if (*source != '\t' && IsControlChar(*source)) { + return source - sourceBegin; + } + } + + return -1; +} + +static void RemoveControlCharactersFrom(nsAString& aStr, + TextRangeArray* aRanges) { + size_t firstControlCharOffset = FindFirstControlCharacter(aStr); + if (firstControlCharOffset == (size_t)-1) { + return; + } + + nsAutoString copy(aStr); + const char16_t* sourceBegin = copy.BeginReading(); + const char16_t* sourceEnd = copy.EndReading(); + + char16_t* dest = aStr.BeginWriting(); + if (NS_WARN_IF(!dest)) { + return; + } + + char16_t* curDest = dest + firstControlCharOffset; + size_t i = firstControlCharOffset; + for (const char16_t* source = sourceBegin + firstControlCharOffset; + source < sourceEnd; ++source) { + if (*source == '\t' || *source == '\n' || !IsControlChar(*source)) { + *curDest = *source; + ++curDest; + ++i; + } else if (aRanges) { + aRanges->RemoveCharacter(i); + } + } + + aStr.SetLength(curDest - dest); +} + +nsString TextComposition::CommitStringIfCommittedAsIs() const { + nsString result(mLastData); + if (!mAllowControlCharacters) { + RemoveControlCharactersFrom(result, nullptr); + } + if (StaticPrefs::intl_ime_remove_placeholder_character_at_commit() && + mLastData == IDEOGRAPHIC_SPACE) { + return EmptyString(); + } + return result; +} + +void TextComposition::DispatchCompositionEvent( + WidgetCompositionEvent* aCompositionEvent, nsEventStatus* aStatus, + EventDispatchingCallback* aCallBack, bool aIsSynthesized) { + mWasCompositionStringEmpty = mString.IsEmpty(); + + if (aCompositionEvent->IsFollowedByCompositionEnd()) { + mHasReceivedCommitEvent = true; + } + + // If this instance has requested to commit or cancel composition but + // is not synthesizing commit event, that means that the IME commits or + // cancels the composition asynchronously. Typically, iBus behaves so. + // Then, synthesized events which were dispatched immediately after + // the request has already committed our editor's composition string and + // told it to web apps. Therefore, we should ignore the delayed events. + if (mRequestedToCommitOrCancel && !aIsSynthesized) { + *aStatus = nsEventStatus_eConsumeNoDefault; + return; + } + + // If the content is a container of BrowserParent, composition should be in + // the remote process. + if (mBrowserParent) { + Unused << mBrowserParent->SendCompositionEvent(*aCompositionEvent); + aCompositionEvent->StopPropagation(); + if (aCompositionEvent->CausesDOMTextEvent()) { + mLastData = aCompositionEvent->mData; + mLastRanges = aCompositionEvent->mRanges; + // Although, the composition event hasn't been actually handled yet, + // emulate an editor to be handling the composition event. + EditorWillHandleCompositionChangeEvent(aCompositionEvent); + EditorDidHandleCompositionChangeEvent(); + } + return; + } + + if (!mAllowControlCharacters) { + RemoveControlCharactersFrom(aCompositionEvent->mData, + aCompositionEvent->mRanges); + } + if (aCompositionEvent->mMessage == eCompositionCommitAsIs) { + NS_ASSERTION(!aCompositionEvent->mRanges, + "mRanges of eCompositionCommitAsIs should be null"); + aCompositionEvent->mRanges = nullptr; + NS_ASSERTION(aCompositionEvent->mData.IsEmpty(), + "mData of eCompositionCommitAsIs should be empty string"); + if (StaticPrefs::intl_ime_remove_placeholder_character_at_commit() && + mLastData == IDEOGRAPHIC_SPACE) { + // If the last data is an ideographic space (FullWidth space), it might be + // a placeholder character of some Chinese IME. So, committing with + // this data might not be expected by users. Let's use empty string. + aCompositionEvent->mData.Truncate(); + } else { + aCompositionEvent->mData = mLastData; + } + } else if (aCompositionEvent->mMessage == eCompositionCommit) { + NS_ASSERTION(!aCompositionEvent->mRanges, + "mRanges of eCompositionCommit should be null"); + aCompositionEvent->mRanges = nullptr; + } + + if (!IsValidStateForComposition(aCompositionEvent->mWidget)) { + *aStatus = nsEventStatus_eConsumeNoDefault; + return; + } + + // IME may commit composition with empty string for a commit request or + // with non-empty string for a cancel request. We should prevent such + // unexpected result. E.g., web apps may be confused if they implement + // autocomplete which attempts to commit composition forcibly when the user + // selects one of suggestions but composition string is cleared by IME. + // Note that most Chinese IMEs don't expose actual composition string to us. + // They typically tell us an IDEOGRAPHIC SPACE or empty string as composition + // string. Therefore, we should hack it only when: + // 1. committing string is empty string at requesting commit but the last + // data isn't IDEOGRAPHIC SPACE. + // 2. non-empty string is committed at requesting cancel. + if (!aIsSynthesized && (mIsRequestingCommit || mIsRequestingCancel)) { + nsString* committingData = nullptr; + switch (aCompositionEvent->mMessage) { + case eCompositionEnd: + case eCompositionChange: + case eCompositionCommitAsIs: + case eCompositionCommit: + committingData = &aCompositionEvent->mData; + break; + default: + NS_WARNING( + "Unexpected event comes during committing or " + "canceling composition"); + break; + } + if (committingData) { + if (mIsRequestingCommit && committingData->IsEmpty() && + mLastData != IDEOGRAPHIC_SPACE) { + committingData->Assign(mLastData); + } else if (mIsRequestingCancel && !committingData->IsEmpty()) { + committingData->Truncate(); + } + } + } + + bool dispatchEvent = true; + bool dispatchDOMTextEvent = aCompositionEvent->CausesDOMTextEvent(); + + // When mIsComposing is false but the committing string is different from + // the last data (E.g., previous eCompositionChange event made the + // composition string empty or didn't have clause information), we don't + // need to dispatch redundant DOM text event. (But note that we need to + // dispatch eCompositionChange event if we have not dispatched + // eCompositionChange event yet and commit string replaces selected string + // with empty string since selected string hasn't been replaced with empty + // string yet.) + if (dispatchDOMTextEvent && + aCompositionEvent->mMessage != eCompositionChange && !mIsComposing && + mHasDispatchedDOMTextEvent && mLastData == aCompositionEvent->mData) { + dispatchEvent = dispatchDOMTextEvent = false; + } + + // widget may dispatch redundant eCompositionChange event + // which modifies neither composition string, clauses nor caret + // position. In such case, we shouldn't dispatch DOM events. + if (dispatchDOMTextEvent && + aCompositionEvent->mMessage == eCompositionChange && + mLastData == aCompositionEvent->mData && mRanges && + aCompositionEvent->mRanges && + mRanges->Equals(*aCompositionEvent->mRanges)) { + dispatchEvent = dispatchDOMTextEvent = false; + } + + if (dispatchDOMTextEvent) { + if (!MaybeDispatchCompositionUpdate(aCompositionEvent)) { + return; + } + } + + if (dispatchEvent) { + // If the composition event should cause a DOM text event, we should + // overwrite the event message as eCompositionChange because due to + // the limitation of mapping between event messages and DOM event types, + // we cannot map multiple event messages to a DOM event type. + if (dispatchDOMTextEvent && + aCompositionEvent->mMessage != eCompositionChange) { + mHasDispatchedDOMTextEvent = true; + aCompositionEvent->mFlags = CloneAndDispatchAs( + aCompositionEvent, eCompositionChange, aStatus, aCallBack); + } else { + if (aCompositionEvent->mMessage == eCompositionChange) { + mHasDispatchedDOMTextEvent = true; + } + DispatchEvent(aCompositionEvent, aStatus, aCallBack); + } + } else { + *aStatus = nsEventStatus_eConsumeNoDefault; + } + + if (!IsValidStateForComposition(aCompositionEvent->mWidget)) { + return; + } + + // Emulate editor behavior of compositionchange event (DOM text event) handler + // if no editor handles composition events. + if (dispatchDOMTextEvent && !HasEditor()) { + EditorWillHandleCompositionChangeEvent(aCompositionEvent); + EditorDidHandleCompositionChangeEvent(); + } + + if (aCompositionEvent->CausesDOMCompositionEndEvent()) { + // Dispatch a compositionend event if it's necessary. + if (aCompositionEvent->mMessage != eCompositionEnd) { + CloneAndDispatchAs(aCompositionEvent, eCompositionEnd); + } + MOZ_ASSERT(!mIsComposing, "Why is the editor still composing?"); + MOZ_ASSERT(!HasEditor(), "Why does the editor still keep to hold this?"); + } + + MaybeNotifyIMEOfCompositionEventHandled(aCompositionEvent); +} + +// static +void TextComposition::HandleSelectionEvent( + nsPresContext* aPresContext, BrowserParent* aBrowserParent, + WidgetSelectionEvent* aSelectionEvent) { + // If the content is a container of BrowserParent, composition should be in + // the remote process. + if (aBrowserParent) { + Unused << aBrowserParent->SendSelectionEvent(*aSelectionEvent); + aSelectionEvent->StopPropagation(); + return; + } + + AutoRestore<bool> saveHandlingSelectionEvent(sHandlingSelectionEvent); + sHandlingSelectionEvent = true; + + if (RefPtr<IMEContentObserver> contentObserver = + IMEStateManager::GetActiveContentObserver()) { + contentObserver->MaybeHandleSelectionEvent(aPresContext, aSelectionEvent); + return; + } + + ContentEventHandler handler(aPresContext); + // XXX During setting selection, a selection listener may change selection + // again. In such case, sHandlingSelectionEvent doesn't indicate if + // the selection change is caused by a selection event. However, it + // must be non-realistic scenario. + handler.OnSelectionEvent(aSelectionEvent); +} + +uint32_t TextComposition::GetSelectionStartOffset() { + nsCOMPtr<nsIWidget> widget = mPresContext->GetRootWidget(); + WidgetQueryContentEvent querySelectedTextEvent(true, eQuerySelectedText, + widget); + // Due to a bug of widget, mRanges may not be nullptr even though composition + // string is empty. So, we need to check it here for avoiding to return + // odd start offset. + if (!mLastData.IsEmpty() && mRanges && mRanges->HasClauses()) { + querySelectedTextEvent.InitForQuerySelectedText( + ToSelectionType(mRanges->GetFirstClause()->mRangeType)); + } else { + NS_WARNING_ASSERTION( + !mLastData.IsEmpty() || !mRanges || !mRanges->HasClauses(), + "Shouldn't have empty clause info when composition string is empty"); + querySelectedTextEvent.InitForQuerySelectedText(SelectionType::eNormal); + } + + // The editor which has this composition is observed by active + // IMEContentObserver, we can use the cache of it. + RefPtr<IMEContentObserver> contentObserver = + IMEStateManager::GetActiveContentObserver(); + bool doQuerySelection = true; + if (contentObserver) { + if (contentObserver->IsManaging(*this)) { + doQuerySelection = false; + contentObserver->HandleQueryContentEvent(&querySelectedTextEvent); + } + // If another editor already has focus, we cannot retrieve selection + // in the editor which has this composition... + else if (NS_WARN_IF(contentObserver->GetPresContext() == mPresContext)) { + return 0; // XXX Is this okay? + } + } + + // Otherwise, using slow path (i.e., compute every time with + // ContentEventHandler) + if (doQuerySelection) { + ContentEventHandler handler(mPresContext); + handler.HandleQueryContentEvent(&querySelectedTextEvent); + } + + if (NS_WARN_IF(querySelectedTextEvent.DidNotFindSelection())) { + return 0; // XXX Is this okay? + } + return querySelectedTextEvent.mReply->AnchorOffset(); +} + +void TextComposition::OnCompositionEventDispatched( + const WidgetCompositionEvent* aCompositionEvent) { + MOZ_RELEASE_ASSERT(!mBrowserParent); + + if (!IsValidStateForComposition(aCompositionEvent->mWidget)) { + return; + } + + // Every composition event may cause changing composition start offset, + // especially when there is no composition string. Therefore, we need to + // update mCompositionStartOffset with the latest offset. + + MOZ_ASSERT(aCompositionEvent->mMessage != eCompositionStart || + mWasCompositionStringEmpty, + "mWasCompositionStringEmpty should be true if the dispatched " + "event is eCompositionStart"); + + if (mWasCompositionStringEmpty && + !aCompositionEvent->CausesDOMCompositionEndEvent()) { + // If there was no composition string, current selection start may be the + // offset for inserting composition string. + // Update composition start offset with current selection start. + mCompositionStartOffset = GetSelectionStartOffset(); + mTargetClauseOffsetInComposition = 0; + } + + if (aCompositionEvent->CausesDOMTextEvent()) { + mTargetClauseOffsetInComposition = aCompositionEvent->TargetClauseOffset(); + } +} + +void TextComposition::OnStartOffsetUpdatedInChild(uint32_t aStartOffset) { + mCompositionStartOffset = aStartOffset; +} + +void TextComposition::MaybeNotifyIMEOfCompositionEventHandled( + const WidgetCompositionEvent* aCompositionEvent) { + if (aCompositionEvent->mMessage != eCompositionStart && + !aCompositionEvent->CausesDOMTextEvent()) { + return; + } + + RefPtr<IMEContentObserver> contentObserver = + IMEStateManager::GetActiveContentObserver(); + // When IMEContentObserver is managing the editor which has this composition, + // composition event handled notification should be sent after the observer + // notifies all pending notifications. Therefore, we should use it. + // XXX If IMEContentObserver suddenly loses focus after here and notifying + // widget of pending notifications, we won't notify widget of composition + // event handled. Although, this is a bug but it should be okay since + // destroying IMEContentObserver notifies IME of blur. So, native IME + // handler can treat it as this notification too. + if (contentObserver && contentObserver->IsManaging(*this)) { + contentObserver->MaybeNotifyCompositionEventHandled(); + return; + } + // Otherwise, e.g., this composition is in non-active window, we should + // notify widget directly. + NotifyIME(NOTIFY_IME_OF_COMPOSITION_EVENT_HANDLED); +} + +void TextComposition::DispatchCompositionEventRunnable( + EventMessage aEventMessage, const nsAString& aData, + bool aIsSynthesizingCommit) { + nsContentUtils::AddScriptRunner(new CompositionEventDispatcher( + this, mNode, aEventMessage, aData, aIsSynthesizingCommit)); +} + +nsresult TextComposition::RequestToCommit(nsIWidget* aWidget, bool aDiscard) { + // If this composition is already requested to be committed or canceled, + // or has already finished in IME, we don't need to request it again because + // request from this instance shouldn't cause committing nor canceling current + // composition in IME, and even if the first request failed, new request + // won't success, probably. And we shouldn't synthesize events for + // committing or canceling composition twice or more times. + if (!CanRequsetIMEToCommitOrCancelComposition()) { + return NS_OK; + } + + RefPtr<TextComposition> kungFuDeathGrip(this); + const nsAutoString lastData(mLastData); + + if (IMEStateManager::CanSendNotificationToWidget()) { + AutoRestore<bool> saveRequestingCancel(mIsRequestingCancel); + AutoRestore<bool> saveRequestingCommit(mIsRequestingCommit); + if (aDiscard) { + mIsRequestingCancel = true; + mIsRequestingCommit = false; + } else { + mIsRequestingCancel = false; + mIsRequestingCommit = true; + } + // FYI: CompositionEvents caused by a call of NotifyIME() may be + // discarded by PresShell if it's not safe to dispatch the event. + nsresult rv = aWidget->NotifyIME( + IMENotification(aDiscard ? REQUEST_TO_CANCEL_COMPOSITION + : REQUEST_TO_COMMIT_COMPOSITION)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + mRequestedToCommitOrCancel = true; + + // If the request is performed synchronously, this must be already destroyed. + if (Destroyed()) { + return NS_OK; + } + + // Otherwise, synthesize the commit in content. + nsAutoString data(aDiscard ? EmptyString() : lastData); + if (data == mLastData) { + DispatchCompositionEventRunnable(eCompositionCommitAsIs, u""_ns, true); + } else { + DispatchCompositionEventRunnable(eCompositionCommit, data, true); + } + return NS_OK; +} + +nsresult TextComposition::NotifyIME(IMEMessage aMessage) { + NS_ENSURE_TRUE(mPresContext, NS_ERROR_NOT_AVAILABLE); + return IMEStateManager::NotifyIME(aMessage, mPresContext, mBrowserParent); +} + +void TextComposition::EditorWillHandleCompositionChangeEvent( + const WidgetCompositionEvent* aCompositionChangeEvent) { + mIsComposing = aCompositionChangeEvent->IsComposing(); + mRanges = aCompositionChangeEvent->mRanges; + mIsEditorHandlingEvent = true; + + MOZ_ASSERT( + mLastData == aCompositionChangeEvent->mData, + "The text of a compositionchange event must be same as previous data " + "attribute value of the latest compositionupdate event"); +} + +void TextComposition::OnEditorDestroyed() { + MOZ_RELEASE_ASSERT(!mBrowserParent); + + MOZ_ASSERT(!mIsEditorHandlingEvent, + "The editor should have stopped listening events"); + nsCOMPtr<nsIWidget> widget = GetWidget(); + if (NS_WARN_IF(!widget)) { + // XXX If this could happen, how do we notify IME of destroying the editor? + return; + } + + // Try to cancel the composition. + RequestToCommit(widget, true); +} + +void TextComposition::EditorDidHandleCompositionChangeEvent() { + mString = mLastData; + mIsEditorHandlingEvent = false; +} + +void TextComposition::StartHandlingComposition(EditorBase* aEditorBase) { + MOZ_RELEASE_ASSERT(!mBrowserParent); + + MOZ_ASSERT(!HasEditor(), "There is a handling editor already"); + mEditorBaseWeak = do_GetWeakReference(static_cast<nsIEditor*>(aEditorBase)); +} + +void TextComposition::EndHandlingComposition(EditorBase* aEditorBase) { + MOZ_RELEASE_ASSERT(!mBrowserParent); + +#ifdef DEBUG + RefPtr<EditorBase> editorBase = GetEditorBase(); + MOZ_ASSERT(!editorBase || editorBase == aEditorBase, + "Another editor handled the composition?"); +#endif // #ifdef DEBUG + mEditorBaseWeak = nullptr; +} + +already_AddRefed<EditorBase> TextComposition::GetEditorBase() const { + nsCOMPtr<nsIEditor> editor = do_QueryReferent(mEditorBaseWeak); + RefPtr<EditorBase> editorBase = static_cast<EditorBase*>(editor.get()); + return editorBase.forget(); +} + +bool TextComposition::HasEditor() const { + return mEditorBaseWeak && mEditorBaseWeak->IsAlive(); +} + +RawRangeBoundary TextComposition::FirstIMESelectionStartRef() const { + RefPtr<EditorBase> editorBase = GetEditorBase(); + if (!editorBase) { + return RawRangeBoundary(); + } + + nsISelectionController* selectionController = + editorBase->GetSelectionController(); + if (NS_WARN_IF(!selectionController)) { + return RawRangeBoundary(); + } + + const nsRange* firstRange = nullptr; + static const SelectionType kIMESelectionTypes[] = { + SelectionType::eIMERawClause, SelectionType::eIMESelectedRawClause, + SelectionType::eIMEConvertedClause, SelectionType::eIMESelectedClause}; + for (auto selectionType : kIMESelectionTypes) { + dom::Selection* selection = + selectionController->GetSelection(ToRawSelectionType(selectionType)); + if (!selection) { + continue; + } + const uint32_t rangeCount = selection->RangeCount(); + for (const uint32_t i : IntegerRange(rangeCount)) { + MOZ_ASSERT(selection->RangeCount() == rangeCount); + const nsRange* range = selection->GetRangeAt(i); + MOZ_ASSERT(range); + if (MOZ_UNLIKELY(NS_WARN_IF(!range)) || + MOZ_UNLIKELY(NS_WARN_IF(!range->GetStartContainer()))) { + continue; + } + if (!firstRange) { + firstRange = range; + continue; + } + // In most cases, all composition string should be in same text node. + if (firstRange->GetStartContainer() == range->GetStartContainer()) { + if (firstRange->StartOffset() > range->StartOffset()) { + firstRange = range; + } + continue; + } + // However, if web apps have inserted different nodes in composition + // string, composition string may span 2 or more nodes. + if (firstRange->GetStartContainer()->GetNextSibling() == + range->GetStartContainer()) { + // Fast path for some known applications like Google Keep. + firstRange = range; + continue; + } + // Unfortunately, really slow path. + // The ranges should always have a common ancestor, hence, be comparable. + if (*nsContentUtils::ComparePoints(range->StartRef(), + firstRange->StartRef()) == -1) { + firstRange = range; + } + } + } + return firstRange ? firstRange->StartRef().AsRaw() : RawRangeBoundary(); +} + +RawRangeBoundary TextComposition::LastIMESelectionEndRef() const { + RefPtr<EditorBase> editorBase = GetEditorBase(); + if (!editorBase) { + return RawRangeBoundary(); + } + + nsISelectionController* selectionController = + editorBase->GetSelectionController(); + if (NS_WARN_IF(!selectionController)) { + return RawRangeBoundary(); + } + + const nsRange* lastRange = nullptr; + static const SelectionType kIMESelectionTypes[] = { + SelectionType::eIMERawClause, SelectionType::eIMESelectedRawClause, + SelectionType::eIMEConvertedClause, SelectionType::eIMESelectedClause}; + for (auto selectionType : kIMESelectionTypes) { + dom::Selection* selection = + selectionController->GetSelection(ToRawSelectionType(selectionType)); + if (!selection) { + continue; + } + const uint32_t rangeCount = selection->RangeCount(); + for (const uint32_t i : IntegerRange(rangeCount)) { + MOZ_ASSERT(selection->RangeCount() == rangeCount); + const nsRange* range = selection->GetRangeAt(i); + MOZ_ASSERT(range); + if (MOZ_UNLIKELY(NS_WARN_IF(!range)) || + MOZ_UNLIKELY(NS_WARN_IF(!range->GetEndContainer()))) { + continue; + } + if (!lastRange) { + lastRange = range; + continue; + } + // In most cases, all composition string should be in same text node. + if (lastRange->GetEndContainer() == range->GetEndContainer()) { + if (lastRange->EndOffset() < range->EndOffset()) { + lastRange = range; + } + continue; + } + // However, if web apps have inserted different nodes in composition + // string, composition string may span 2 or more nodes. + if (lastRange->GetEndContainer() == + range->GetEndContainer()->GetNextSibling()) { + // Fast path for some known applications like Google Keep. + lastRange = range; + continue; + } + // Unfortunately, really slow path. + // The ranges should always have a common ancestor, hence, be comparable. + if (*nsContentUtils::ComparePoints(lastRange->EndRef(), + range->EndRef()) == -1) { + lastRange = range; + } + } + } + return lastRange ? lastRange->EndRef().AsRaw() : RawRangeBoundary(); +} + +/****************************************************************************** + * TextComposition::CompositionEventDispatcher + ******************************************************************************/ + +TextComposition::CompositionEventDispatcher::CompositionEventDispatcher( + TextComposition* aComposition, nsINode* aEventTarget, + EventMessage aEventMessage, const nsAString& aData, + bool aIsSynthesizedEvent) + : Runnable("TextComposition::CompositionEventDispatcher"), + mTextComposition(aComposition), + mEventTarget(aEventTarget), + mData(aData), + mEventMessage(aEventMessage), + mIsSynthesizedEvent(aIsSynthesizedEvent) {} + +NS_IMETHODIMP +TextComposition::CompositionEventDispatcher::Run() { + // The widget can be different from the widget which has dispatched + // composition events because GetWidget() returns a widget which is proper + // for calling NotifyIME(). However, this must no be problem since both + // widget should share native IME context. Therefore, even if an event + // handler uses the widget for requesting IME to commit or cancel, it works. + nsCOMPtr<nsIWidget> widget(mTextComposition->GetWidget()); + if (!mTextComposition->IsValidStateForComposition(widget)) { + return NS_OK; // cannot dispatch any events anymore + } + + RefPtr<nsPresContext> presContext = mTextComposition->mPresContext; + nsCOMPtr<nsINode> eventTarget = mEventTarget; + RefPtr<BrowserParent> browserParent = mTextComposition->mBrowserParent; + nsEventStatus status = nsEventStatus_eIgnore; + switch (mEventMessage) { + case eCompositionStart: { + WidgetCompositionEvent compStart(true, eCompositionStart, widget); + compStart.mNativeIMEContext = mTextComposition->mNativeContext; + WidgetQueryContentEvent querySelectedTextEvent(true, eQuerySelectedText, + widget); + ContentEventHandler handler(presContext); + handler.OnQuerySelectedText(&querySelectedTextEvent); + NS_ASSERTION(querySelectedTextEvent.Succeeded(), + "Failed to get selected text"); + if (querySelectedTextEvent.FoundSelection()) { + compStart.mData = querySelectedTextEvent.mReply->DataRef(); + } + compStart.mFlags.mIsSynthesizedForTests = + mTextComposition->IsSynthesizedForTests(); + IMEStateManager::DispatchCompositionEvent( + eventTarget, presContext, browserParent, &compStart, &status, nullptr, + mIsSynthesizedEvent); + break; + } + case eCompositionChange: + case eCompositionCommitAsIs: + case eCompositionCommit: { + WidgetCompositionEvent compEvent(true, mEventMessage, widget); + compEvent.mNativeIMEContext = mTextComposition->mNativeContext; + if (mEventMessage != eCompositionCommitAsIs) { + compEvent.mData = mData; + } + compEvent.mFlags.mIsSynthesizedForTests = + mTextComposition->IsSynthesizedForTests(); + IMEStateManager::DispatchCompositionEvent( + eventTarget, presContext, browserParent, &compEvent, &status, nullptr, + mIsSynthesizedEvent); + break; + } + default: + MOZ_CRASH("Unsupported event"); + } + return NS_OK; +} + +/****************************************************************************** + * TextCompositionArray + ******************************************************************************/ + +TextCompositionArray::index_type TextCompositionArray::IndexOf( + const NativeIMEContext& aNativeIMEContext) { + if (!aNativeIMEContext.IsValid()) { + return NoIndex; + } + for (index_type i = Length(); i > 0; --i) { + if (ElementAt(i - 1)->GetNativeIMEContext() == aNativeIMEContext) { + return i - 1; + } + } + return NoIndex; +} + +TextCompositionArray::index_type TextCompositionArray::IndexOf( + nsIWidget* aWidget) { + return IndexOf(aWidget->GetNativeIMEContext()); +} + +TextCompositionArray::index_type TextCompositionArray::IndexOf( + nsPresContext* aPresContext) { + for (index_type i = Length(); i > 0; --i) { + if (ElementAt(i - 1)->GetPresContext() == aPresContext) { + return i - 1; + } + } + return NoIndex; +} + +TextCompositionArray::index_type TextCompositionArray::IndexOf( + nsPresContext* aPresContext, nsINode* aNode) { + index_type index = IndexOf(aPresContext); + if (index == NoIndex) { + return NoIndex; + } + nsINode* node = ElementAt(index)->GetEventTargetNode(); + return node == aNode ? index : NoIndex; +} + +TextComposition* TextCompositionArray::GetCompositionFor(nsIWidget* aWidget) { + index_type i = IndexOf(aWidget); + if (i == NoIndex) { + return nullptr; + } + return ElementAt(i); +} + +TextComposition* TextCompositionArray::GetCompositionFor( + const WidgetCompositionEvent* aCompositionEvent) { + index_type i = IndexOf(aCompositionEvent->mNativeIMEContext); + if (i == NoIndex) { + return nullptr; + } + return ElementAt(i); +} + +TextComposition* TextCompositionArray::GetCompositionFor( + nsPresContext* aPresContext) { + index_type i = IndexOf(aPresContext); + if (i == NoIndex) { + return nullptr; + } + return ElementAt(i); +} + +TextComposition* TextCompositionArray::GetCompositionFor( + nsPresContext* aPresContext, nsINode* aNode) { + index_type i = IndexOf(aPresContext, aNode); + if (i == NoIndex) { + return nullptr; + } + return ElementAt(i); +} + +TextComposition* TextCompositionArray::GetCompositionInContent( + nsPresContext* aPresContext, nsIContent* aContent) { + // There should be only one composition per content object. + for (index_type i = Length(); i > 0; --i) { + nsINode* node = ElementAt(i - 1)->GetEventTargetNode(); + if (node && node->IsInclusiveDescendantOf(aContent)) { + return ElementAt(i - 1); + } + } + return nullptr; +} + +} // namespace mozilla diff --git a/dom/events/TextComposition.h b/dom/events/TextComposition.h new file mode 100644 index 0000000000..7199171068 --- /dev/null +++ b/dom/events/TextComposition.h @@ -0,0 +1,631 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#ifndef mozilla_TextComposition_h +#define mozilla_TextComposition_h + +#include "nsCOMPtr.h" +#include "nsINode.h" +#include "nsIWidget.h" +#include "nsTArray.h" +#include "nsThreadUtils.h" +#include "nsPresContext.h" +#include "mozilla/AlreadyAddRefed.h" +#include "mozilla/Attributes.h" +#include "mozilla/EventForwards.h" +#include "mozilla/RangeBoundary.h" +#include "mozilla/TextRange.h" +#include "mozilla/dom/BrowserParent.h" +#include "mozilla/dom/Text.h" + +class nsRange; + +struct CharacterDataChangeInfo; + +namespace mozilla { + +class EditorBase; +class EventDispatchingCallback; +class IMEStateManager; + +/** + * TextComposition represents a text composition. This class stores the + * composition event target and its presContext. At dispatching the event via + * this class, the instances use the stored event target. + */ + +class TextComposition final { + friend class IMEStateManager; + + NS_INLINE_DECL_REFCOUNTING(TextComposition) + + public: + typedef dom::BrowserParent BrowserParent; + typedef dom::Text Text; + + static bool IsHandlingSelectionEvent() { return sHandlingSelectionEvent; } + + TextComposition(nsPresContext* aPresContext, nsINode* aNode, + BrowserParent* aBrowserParent, + WidgetCompositionEvent* aCompositionEvent); + + bool Destroyed() const { return !mPresContext; } + nsPresContext* GetPresContext() const { return mPresContext; } + nsINode* GetEventTargetNode() const { return mNode; } + // The text node which includes composition string. + Text* GetContainerTextNode() const { return mContainerTextNode; } + // The latest CompositionEvent.data value except compositionstart event. + // This value is modified at dispatching compositionupdate. + const nsString& LastData() const { return mLastData; } + // Returns commit string if it'll be commited as-is. + nsString CommitStringIfCommittedAsIs() const; + // The composition string which is already handled by the focused editor. + // I.e., this value must be same as the composition string on the focused + // editor. This value is modified at a call of + // EditorDidHandleCompositionChangeEvent(). + // Note that mString and mLastData are different between dispatcing + // compositionupdate and compositionchange event handled by focused editor. + const nsString& String() const { return mString; } + // The latest clauses range of the composition string. + // During compositionupdate event, GetRanges() returns old ranges. + // So if getting on compositionupdate, Use GetLastRange instead of GetRange(). + TextRangeArray* GetLastRanges() const { return mLastRanges; } + // Returns the clauses and/or caret range of the composition string. + // This is modified at a call of EditorWillHandleCompositionChangeEvent(). + // This may return null if there is no clauses and caret. + // XXX We should return |const TextRangeArray*| here, but it causes compile + // error due to inaccessible Release() method. + TextRangeArray* GetRanges() const { return mRanges; } + // Returns the widget which is proper to call NotifyIME(). + already_AddRefed<nsIWidget> GetWidget() const { + if (!mPresContext) { + return nullptr; + } + return do_AddRef(mPresContext->GetRootWidget()); + } + // Returns the tab parent which has this composition in its remote process. + BrowserParent* GetBrowserParent() const { return mBrowserParent; } + // Returns true if the composition is started with synthesized event which + // came from nsDOMWindowUtils. + bool IsSynthesizedForTests() const { return mIsSynthesizedForTests; } + + const widget::NativeIMEContext& GetNativeIMEContext() const { + return mNativeContext; + } + + /** + * This is called when IMEStateManager stops managing the instance. + */ + void Destroy(); + + /** + * Request to commit (or cancel) the composition to IME. This method should + * be called only by IMEStateManager::NotifyIME(). + */ + nsresult RequestToCommit(nsIWidget* aWidget, bool aDiscard); + + /** + * IsRequestingCommitOrCancelComposition() returns true if the instance is + * requesting widget to commit or cancel composition. + */ + bool IsRequestingCommitOrCancelComposition() const { + return mIsRequestingCancel || mIsRequestingCommit; + } + + /** + * Send a notification to IME. It depends on the IME or platform spec what + * will occur (or not occur). + */ + nsresult NotifyIME(widget::IMEMessage aMessage); + + /** + * the offset of first composition string + */ + uint32_t NativeOffsetOfStartComposition() const { + return mCompositionStartOffset; + } + + /** + * the offset of first selected clause or start of composition + */ + uint32_t NativeOffsetOfTargetClause() const { + return mCompositionStartOffset + mTargetClauseOffsetInComposition; + } + + /** + * Return current composition start and end point in the DOM tree. + * Note that one of or both of those result container may be different + * from GetContainerTextNode() if the DOM tree was modified by the web + * app. If there is no composition string the DOM tree, these return + * unset range boundaries. + */ + RawRangeBoundary FirstIMESelectionStartRef() const; + RawRangeBoundary LastIMESelectionEndRef() const; + + /** + * The offset of composition string in the text node. If composition string + * hasn't been inserted in any text node yet, this returns UINT32_MAX. + */ + uint32_t XPOffsetInTextNode() const { + return mCompositionStartOffsetInTextNode; + } + + /** + * The length of composition string in the text node. If composition string + * hasn't been inserted in any text node yet, this returns 0. + */ + uint32_t XPLengthInTextNode() const { + return mCompositionLengthInTextNode == UINT32_MAX + ? 0 + : mCompositionLengthInTextNode; + } + + /** + * The end offset of composition string in the text node. If composition + * string hasn't been inserted in any text node yet, this returns UINT32_MAX. + */ + uint32_t XPEndOffsetInTextNode() const { + if (mCompositionStartOffsetInTextNode == UINT32_MAX || + mCompositionLengthInTextNode == UINT32_MAX) { + return UINT32_MAX; + } + return mCompositionStartOffsetInTextNode + mCompositionLengthInTextNode; + } + + /** + * Returns true if there is non-empty composition string and it's not fixed. + * Otherwise, false. + */ + bool IsComposing() const { return mIsComposing; } + + /** + * Returns true while editor is handling an event which is modifying the + * composition string. + */ + bool IsEditorHandlingEvent() const { return mIsEditorHandlingEvent; } + + /** + * IsMovingToNewTextNode() returns true if editor detects the text node + * has been removed and still not insert the composition string into + * new text node. + */ + bool IsMovingToNewTextNode() const { + return !mContainerTextNode && mCompositionLengthInTextNode && + mCompositionLengthInTextNode != UINT32_MAX; + } + + /** + * StartHandlingComposition() and EndHandlingComposition() are called by + * editor when it holds a TextComposition instance and release it. + */ + void StartHandlingComposition(EditorBase* aEditorBase); + void EndHandlingComposition(EditorBase* aEditorBase); + + /** + * OnEditorDestroyed() is called when the editor is destroyed but there is + * active composition. + */ + void OnEditorDestroyed(); + + /** + * CompositionChangeEventHandlingMarker class should be created at starting + * to handle text event in focused editor. This calls + * EditorWillHandleCompositionChangeEvent() and + * EditorDidHandleCompositionChangeEvent() automatically. + */ + class MOZ_STACK_CLASS CompositionChangeEventHandlingMarker { + public: + CompositionChangeEventHandlingMarker( + TextComposition* aComposition, + const WidgetCompositionEvent* aCompositionChangeEvent) + : mComposition(aComposition) { + mComposition->EditorWillHandleCompositionChangeEvent( + aCompositionChangeEvent); + } + + ~CompositionChangeEventHandlingMarker() { + mComposition->EditorDidHandleCompositionChangeEvent(); + } + + private: + RefPtr<TextComposition> mComposition; + CompositionChangeEventHandlingMarker(); + CompositionChangeEventHandlingMarker( + const CompositionChangeEventHandlingMarker& aOther); + }; + + /** + * OnUpdateCompositionInEditor() is called when editor updates composition + * string in the DOM tree. + * + * @param aStringToInsert The string to insert the text node actually. + * This may be different from the data of + * dispatching composition event because it may + * be replaced with different character for + * passwords, or truncated due to maxlength. + * @param aTextNode The text node which includes composition string. + * @param aOffset The offset of composition string in aTextNode. + */ + void OnUpdateCompositionInEditor(const nsAString& aStringToInsert, + Text& aTextNode, uint32_t aOffset) { + mContainerTextNode = &aTextNode; + mCompositionStartOffsetInTextNode = aOffset; + NS_WARNING_ASSERTION(mCompositionStartOffsetInTextNode != UINT32_MAX, + "The text node is really too long."); + mCompositionLengthInTextNode = aStringToInsert.Length(); + NS_WARNING_ASSERTION(mCompositionLengthInTextNode != UINT32_MAX, + "The string to insert is really too long."); + } + + /** + * OnTextNodeRemoved() is called when focused editor is reframed and + * mContainerTextNode may be (or have been) replaced with different text + * node, or just removes the text node due to empty. + */ + void OnTextNodeRemoved() { + mContainerTextNode = nullptr; + // Don't reset mCompositionStartOffsetInTextNode nor + // mCompositionLengthInTextNode because editor needs them to restore + // composition in new text node. + } + + /** + * OnCharacterDataChanged() is called when IMEContentObserver receives + * character data change notifications. + */ + void OnCharacterDataChanged(Text& aText, + const CharacterDataChangeInfo& aInfo); + + private: + // Private destructor, to discourage deletion outside of Release(): + ~TextComposition() { + // WARNING: mPresContext may be destroying, so, be careful if you touch it. + } + + // sHandlingSelectionEvent is true while TextComposition sends a selection + // event to ContentEventHandler. + static bool sHandlingSelectionEvent; + + // This class holds nsPresContext weak. This instance shouldn't block + // destroying it. When the presContext is being destroyed, it's notified to + // IMEStateManager::OnDestroyPresContext(), and then, it destroy + // this instance. + nsPresContext* mPresContext; + RefPtr<nsINode> mNode; + RefPtr<BrowserParent> mBrowserParent; + + // The text node which includes the composition string. + RefPtr<Text> mContainerTextNode; + + // This is the clause and caret range information which is managed by + // the focused editor. This may be null if there is no clauses or caret. + RefPtr<TextRangeArray> mRanges; + // Same as mRange, but mRange will have old data during compositionupdate. + // So this will be valied during compositionupdate. + RefPtr<TextRangeArray> mLastRanges; + + // mNativeContext stores a opaque pointer. This works as the "ID" for this + // composition. Don't access the instance, it may not be available. + widget::NativeIMEContext mNativeContext; + + // mEditorBaseWeak is a weak reference to the focused editor handling + // composition. + nsWeakPtr mEditorBaseWeak; + + // mLastData stores the data attribute of the latest composition event (except + // the compositionstart event). + nsString mLastData; + + // mString stores the composition text which has been handled by the focused + // editor. + nsString mString; + + // Offset of the composition string from start of the editor + uint32_t mCompositionStartOffset; + // Offset of the selected clause of the composition string from + // mCompositionStartOffset + uint32_t mTargetClauseOffsetInComposition; + // Offset of the composition string in mContainerTextNode. + // NOTE: This is NOT valid in the main process if focused editor is in a + // remote process. + uint32_t mCompositionStartOffsetInTextNode; + // Length of the composition string in mContainerTextNode. If this instance + // has already dispatched eCompositionCommit(AsIs) and + // EditorDidHandleCompositionChangeEvent() has already been called, + // this may be different from length of mString because committed string + // may be truncated by maxlength attribute of <input> or <textarea>. + // NOTE: This is NOT valid in the main process if focused editor is in a + // remote process. + uint32_t mCompositionLengthInTextNode; + + // See the comment for IsSynthesizedForTests(). + bool mIsSynthesizedForTests; + + // See the comment for IsComposing(). + bool mIsComposing; + + // mIsEditorHandlingEvent is true while editor is modifying the composition + // string. + bool mIsEditorHandlingEvent; + + // mIsRequestingCommit or mIsRequestingCancel is true *only* while we're + // requesting commit or canceling the composition. In other words, while + // one of these values is true, we're handling the request. + bool mIsRequestingCommit; + bool mIsRequestingCancel; + + // mRequestedToCommitOrCancel is true *after* we requested IME to commit or + // cancel the composition. In other words, we already requested of IME that + // it commits or cancels current composition. + // NOTE: Before this is set to true, both mIsRequestingCommit and + // mIsRequestingCancel are set to false. + bool mRequestedToCommitOrCancel; + + // Set to true if the instance dispatches an eCompositionChange event. + bool mHasDispatchedDOMTextEvent; + + // Before this dispatches commit event into the tree, this is set to true. + // So, this means if native IME already commits the composition. + bool mHasReceivedCommitEvent; + + // mWasNativeCompositionEndEventDiscarded is true if this composition was + // requested commit or cancel itself but native compositionend event is + // discarded by PresShell due to not safe to dispatch events. + bool mWasNativeCompositionEndEventDiscarded; + + // Allow control characters appear in composition string. + // When this is false, control characters except + // CHARACTER TABULATION (horizontal tab) are removed from + // both composition string and data attribute of compositionupdate + // and compositionend events. + bool mAllowControlCharacters; + + // mWasCompositionStringEmpty is true if the composition string was empty + // when DispatchCompositionEvent() is called. + bool mWasCompositionStringEmpty; + + // Hide the default constructor and copy constructor. + TextComposition() + : mPresContext(nullptr), + mNativeContext(nullptr), + mCompositionStartOffset(0), + mTargetClauseOffsetInComposition(0), + mCompositionStartOffsetInTextNode(UINT32_MAX), + mCompositionLengthInTextNode(UINT32_MAX), + mIsSynthesizedForTests(false), + mIsComposing(false), + mIsEditorHandlingEvent(false), + mIsRequestingCommit(false), + mIsRequestingCancel(false), + mRequestedToCommitOrCancel(false), + mHasReceivedCommitEvent(false), + mWasNativeCompositionEndEventDiscarded(false), + mAllowControlCharacters(false), + mWasCompositionStringEmpty(true) {} + TextComposition(const TextComposition& aOther); + + /** + * If we're requesting IME to commit or cancel composition, or we've already + * requested it, or we've already known this composition has been ended in + * IME, we don't need to request commit nor cancel composition anymore and + * shouldn't do so if we're in content process for not committing/canceling + * "current" composition in native IME. So, when this returns true, + * RequestIMEToCommit() does nothing. + */ + bool CanRequsetIMEToCommitOrCancelComposition() const { + return !mIsRequestingCommit && !mIsRequestingCancel && + !mRequestedToCommitOrCancel && !mHasReceivedCommitEvent; + } + + /** + * GetEditorBase() returns EditorBase pointer of mEditorBaseWeak. + */ + already_AddRefed<EditorBase> GetEditorBase() const; + + /** + * HasEditor() returns true if mEditorBaseWeak holds EditorBase instance + * which is alive. Otherwise, false. + */ + bool HasEditor() const; + + /** + * EditorWillHandleCompositionChangeEvent() must be called before the focused + * editor handles the compositionchange event. + */ + void EditorWillHandleCompositionChangeEvent( + const WidgetCompositionEvent* aCompositionChangeEvent); + + /** + * EditorDidHandleCompositionChangeEvent() must be called after the focused + * editor handles a compositionchange event. + */ + void EditorDidHandleCompositionChangeEvent(); + + /** + * IsValidStateForComposition() returns true if it's safe to dispatch an event + * to the DOM tree. Otherwise, false. + * WARNING: This doesn't check script blocker state. It should be checked + * before dispatching the first event. + */ + bool IsValidStateForComposition(nsIWidget* aWidget) const; + + /** + * DispatchCompositionEvent() dispatches the aCompositionEvent to the mContent + * synchronously. The caller must ensure that it's safe to dispatch the event. + */ + MOZ_CAN_RUN_SCRIPT void DispatchCompositionEvent( + WidgetCompositionEvent* aCompositionEvent, nsEventStatus* aStatus, + EventDispatchingCallback* aCallBack, bool aIsSynthesized); + + /** + * Simply calling EventDispatcher::Dispatch() with plugin event. + * If dispatching event has no orginal clone, aOriginalEvent can be null. + */ + MOZ_CAN_RUN_SCRIPT void DispatchEvent( + WidgetCompositionEvent* aDispatchEvent, nsEventStatus* aStatus, + EventDispatchingCallback* aCallback, + const WidgetCompositionEvent* aOriginalEvent = nullptr); + + /** + * HandleSelectionEvent() sends the selection event to ContentEventHandler + * or dispatches it to the focused child process. + */ + MOZ_CAN_RUN_SCRIPT + void HandleSelectionEvent(WidgetSelectionEvent* aSelectionEvent) { + RefPtr<nsPresContext> presContext(mPresContext); + RefPtr<BrowserParent> browserParent(mBrowserParent); + HandleSelectionEvent(presContext, browserParent, aSelectionEvent); + } + MOZ_CAN_RUN_SCRIPT + static void HandleSelectionEvent(nsPresContext* aPresContext, + BrowserParent* aBrowserParent, + WidgetSelectionEvent* aSelectionEvent); + + /** + * MaybeDispatchCompositionUpdate() may dispatch a compositionupdate event + * if aCompositionEvent changes composition string. + * @return Returns false if dispatching the compositionupdate event caused + * destroying this composition. + */ + MOZ_CAN_RUN_SCRIPT bool MaybeDispatchCompositionUpdate( + const WidgetCompositionEvent* aCompositionEvent); + + /** + * CloneAndDispatchAs() dispatches a composition event which is + * duplicateed from aCompositionEvent and set the aMessage. + * + * @return Returns BaseEventFlags which is the result of dispatched event. + */ + MOZ_CAN_RUN_SCRIPT BaseEventFlags + CloneAndDispatchAs(const WidgetCompositionEvent* aCompositionEvent, + EventMessage aMessage, nsEventStatus* aStatus = nullptr, + EventDispatchingCallback* aCallBack = nullptr); + + /** + * If IME has already dispatched compositionend event but it was discarded + * by PresShell due to not safe to dispatch, this returns true. + */ + bool WasNativeCompositionEndEventDiscarded() const { + return mWasNativeCompositionEndEventDiscarded; + } + + /** + * OnCompositionEventDiscarded() is called when PresShell discards + * compositionupdate, compositionend or compositionchange event due to not + * safe to dispatch event. + */ + void OnCompositionEventDiscarded(WidgetCompositionEvent* aCompositionEvent); + + /** + * OnCompositionEventDispatched() is called after a composition event is + * dispatched. + */ + MOZ_CAN_RUN_SCRIPT void OnCompositionEventDispatched( + const WidgetCompositionEvent* aDispatchEvent); + + /** + * MaybeNotifyIMEOfCompositionEventHandled() notifies IME of composition + * event handled. This should be called after dispatching a composition + * event which came from widget. + */ + void MaybeNotifyIMEOfCompositionEventHandled( + const WidgetCompositionEvent* aCompositionEvent); + + /** + * GetSelectionStartOffset() returns normal selection start offset in the + * editor which has this composition. + * If it failed or lost focus, this would return 0. + */ + MOZ_CAN_RUN_SCRIPT uint32_t GetSelectionStartOffset(); + + /** + * OnStartOffsetUpdatedInChild() is called when composition start offset + * is updated in the child process. I.e., this is called and never called + * if the composition is in this process. + * @param aStartOffset New composition start offset with native + * linebreaks. + */ + void OnStartOffsetUpdatedInChild(uint32_t aStartOffset); + + /** + * CompositionEventDispatcher dispatches the specified composition (or text) + * event. + */ + class CompositionEventDispatcher : public Runnable { + public: + CompositionEventDispatcher(TextComposition* aTextComposition, + nsINode* aEventTarget, + EventMessage aEventMessage, + const nsAString& aData, + bool aIsSynthesizedEvent = false); + MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHOD Run() override; + + private: + RefPtr<TextComposition> mTextComposition; + nsCOMPtr<nsINode> mEventTarget; + nsString mData; + EventMessage mEventMessage; + bool mIsSynthesizedEvent; + + CompositionEventDispatcher() + : Runnable("TextComposition::CompositionEventDispatcher"), + mEventMessage(eVoidEvent), + mIsSynthesizedEvent(false){}; + }; + + /** + * DispatchCompositionEventRunnable() dispatches a composition event to the + * content. Be aware, if you use this method, nsPresShellEventCB isn't used. + * That means that nsIFrame::HandleEvent() is never called. + * WARNING: The instance which is managed by IMEStateManager may be + * destroyed by this method call. + * + * @param aEventMessage Must be one of composition events. + * @param aData Used for mData value. + * @param aIsSynthesizingCommit true if this is called for synthesizing + * commit or cancel composition. Otherwise, + * false. + */ + void DispatchCompositionEventRunnable(EventMessage aEventMessage, + const nsAString& aData, + bool aIsSynthesizingCommit = false); +}; + +/** + * TextCompositionArray manages the instances of TextComposition class. + * Managing with array is enough because only one composition is typically + * there. Even if user switches native IME context, it's very rare that + * second or more composition is started. + * It's assumed that this is used by IMEStateManager for storing all active + * compositions in the process. If the instance is it, each TextComposition + * in the array can be destroyed by calling some methods of itself. + */ + +class TextCompositionArray final + : public AutoTArray<RefPtr<TextComposition>, 2> { + public: + // Looking for per native IME context. + index_type IndexOf(const widget::NativeIMEContext& aNativeIMEContext); + index_type IndexOf(nsIWidget* aWidget); + + TextComposition* GetCompositionFor(nsIWidget* aWidget); + TextComposition* GetCompositionFor( + const WidgetCompositionEvent* aCompositionEvent); + + // Looking for per nsPresContext + index_type IndexOf(nsPresContext* aPresContext); + index_type IndexOf(nsPresContext* aPresContext, nsINode* aNode); + + TextComposition* GetCompositionFor(nsPresContext* aPresContext); + TextComposition* GetCompositionFor(nsPresContext* aPresContext, + nsINode* aNode); + TextComposition* GetCompositionInContent(nsPresContext* aPresContext, + nsIContent* aContent); +}; + +} // namespace mozilla + +#endif // #ifndef mozilla_TextComposition_h diff --git a/dom/events/Touch.cpp b/dom/events/Touch.cpp new file mode 100644 index 0000000000..b08643dbbf --- /dev/null +++ b/dom/events/Touch.cpp @@ -0,0 +1,233 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#include "mozilla/dom/Touch.h" + +#include "mozilla/dom/EventTarget.h" +#include "mozilla/dom/TouchEvent.h" +#include "nsGlobalWindow.h" +#include "nsContentUtils.h" +#include "nsIContent.h" + +namespace mozilla::dom { + +// static +already_AddRefed<Touch> Touch::Constructor(const GlobalObject& aGlobal, + const TouchInit& aParam) { + // Annoyingly many parameters, make sure the ordering is the same as in the + // Touch constructor. + RefPtr<Touch> touch = new Touch( + aParam.mTarget, aParam.mIdentifier, aParam.mPageX, aParam.mPageY, + aParam.mScreenX, aParam.mScreenY, aParam.mClientX, aParam.mClientY, + aParam.mRadiusX, aParam.mRadiusY, aParam.mRotationAngle, aParam.mForce); + return touch.forget(); +} + +Touch::Touch(EventTarget* aTarget, int32_t aIdentifier, int32_t aPageX, + int32_t aPageY, int32_t aScreenX, int32_t aScreenY, + int32_t aClientX, int32_t aClientY, int32_t aRadiusX, + int32_t aRadiusY, float aRotationAngle, float aForce) + : mIsTouchEventSuppressed(false) { + mTarget = aTarget; + mOriginalTarget = aTarget; + mIdentifier = aIdentifier; + mPagePoint = CSSIntPoint(aPageX, aPageY); + mScreenPoint = CSSIntPoint(aScreenX, aScreenY); + mClientPoint = CSSIntPoint(aClientX, aClientY); + mRefPoint = LayoutDeviceIntPoint(0, 0); + mPointsInitialized = true; + mRadius.x = aRadiusX; + mRadius.y = aRadiusY; + mRotationAngle = aRotationAngle; + mForce = aForce; + + mChanged = false; + mMessage = 0; + nsJSContext::LikelyShortLivingObjectCreated(); +} + +Touch::Touch(int32_t aIdentifier, LayoutDeviceIntPoint aPoint, + LayoutDeviceIntPoint aRadius, float aRotationAngle, float aForce) + : mIsTouchEventSuppressed(false) { + mIdentifier = aIdentifier; + mPagePoint = CSSIntPoint(0, 0); + mScreenPoint = CSSIntPoint(0, 0); + mClientPoint = CSSIntPoint(0, 0); + mRefPoint = aPoint; + mPointsInitialized = false; + mRadius = aRadius; + mRotationAngle = aRotationAngle; + mForce = aForce; + + mChanged = false; + mMessage = 0; + nsJSContext::LikelyShortLivingObjectCreated(); +} + +Touch::Touch(int32_t aIdentifier, LayoutDeviceIntPoint aPoint, + LayoutDeviceIntPoint aRadius, float aRotationAngle, float aForce, + int32_t aTiltX, int32_t aTiltY, int32_t aTwist) + : Touch(aIdentifier, aPoint, aRadius, aRotationAngle, aForce) { + tiltX = aTiltX; + tiltY = aTiltY; + twist = aTwist; +} + +Touch::Touch(const Touch& aOther) + : mOriginalTarget(aOther.mOriginalTarget), + mTarget(aOther.mTarget), + mRefPoint(aOther.mRefPoint), + mChanged(aOther.mChanged), + mIsTouchEventSuppressed(aOther.mIsTouchEventSuppressed), + mMessage(aOther.mMessage), + mIdentifier(aOther.mIdentifier), + mPagePoint(aOther.mPagePoint), + mClientPoint(aOther.mClientPoint), + mScreenPoint(aOther.mScreenPoint), + mRadius(aOther.mRadius), + mRotationAngle(aOther.mRotationAngle), + mForce(aOther.mForce), + mPointsInitialized(aOther.mPointsInitialized) { + nsJSContext::LikelyShortLivingObjectCreated(); +} + +Touch::~Touch() = default; + +// static +bool Touch::PrefEnabled(JSContext* aCx, JSObject* aGlobal) { + return TouchEvent::PrefEnabled(aCx, aGlobal); +} + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(Touch, mTarget, mOriginalTarget) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(Touch) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTING_ADDREF(Touch) +NS_IMPL_CYCLE_COLLECTING_RELEASE(Touch) + +static EventTarget* FindFirstNonChromeOnlyAccessContent(EventTarget* aTarget) { + nsIContent* content = nsIContent::FromEventTargetOrNull(aTarget); + if (content && content->ChromeOnlyAccess() && + !nsContentUtils::LegacyIsCallerNativeCode() && + !nsContentUtils::CanAccessNativeAnon()) { + return content->FindFirstNonChromeOnlyAccessContent(); + } + + return aTarget; +} + +EventTarget* Touch::GetTarget() const { + return FindFirstNonChromeOnlyAccessContent(mTarget); +} + +EventTarget* Touch::GetOriginalTarget() const { + return FindFirstNonChromeOnlyAccessContent(mOriginalTarget); +} + +int32_t Touch::ScreenX(CallerType aCallerType) const { + if (nsContentUtils::ShouldResistFingerprinting(aCallerType, GetParentObject(), + RFPTarget::TouchEvents)) { + return ClientX(); + } + + return mScreenPoint.x; +} + +int32_t Touch::ScreenY(CallerType aCallerType) const { + if (nsContentUtils::ShouldResistFingerprinting(aCallerType, GetParentObject(), + RFPTarget::TouchEvents)) { + return ClientY(); + } + + return mScreenPoint.y; +} + +int32_t Touch::RadiusX(CallerType aCallerType) const { + if (nsContentUtils::ShouldResistFingerprinting(aCallerType, GetParentObject(), + RFPTarget::TouchEvents)) { + return 0; + } + + return mRadius.x; +} + +int32_t Touch::RadiusY(CallerType aCallerType) const { + if (nsContentUtils::ShouldResistFingerprinting(aCallerType, GetParentObject(), + RFPTarget::TouchEvents)) { + return 0; + } + + return mRadius.y; +} + +float Touch::RotationAngle(CallerType aCallerType) const { + if (nsContentUtils::ShouldResistFingerprinting(aCallerType, GetParentObject(), + RFPTarget::TouchEvents)) { + return 0.0f; + } + + return mRotationAngle; +} + +float Touch::Force(CallerType aCallerType) const { + if (nsContentUtils::ShouldResistFingerprinting(aCallerType, GetParentObject(), + RFPTarget::TouchEvents)) { + return 0.0f; + } + + return mForce; +} + +void Touch::InitializePoints(nsPresContext* aPresContext, WidgetEvent* aEvent) { + if (mPointsInitialized) { + return; + } + mClientPoint = + Event::GetClientCoords(aPresContext, aEvent, mRefPoint, mClientPoint); + mPagePoint = + Event::GetPageCoords(aPresContext, aEvent, mRefPoint, mClientPoint); + mScreenPoint = + Event::GetScreenCoords(aPresContext, aEvent, mRefPoint).extract(); + mPointsInitialized = true; +} + +void Touch::SetTouchTarget(EventTarget* aTarget) { + mOriginalTarget = aTarget; + mTarget = aTarget; +} + +bool Touch::Equals(Touch* aTouch) const { + return mRefPoint == aTouch->mRefPoint && mForce == aTouch->mForce && + mRotationAngle == aTouch->mRotationAngle && + mRadius.x == aTouch->mRadius.x && mRadius.y == aTouch->mRadius.y; +} + +void Touch::SetSameAs(const Touch* aTouch) { + mRefPoint = aTouch->mRefPoint; + mForce = aTouch->mForce; + mRotationAngle = aTouch->mRotationAngle; + mRadius.x = aTouch->mRadius.x; + mRadius.y = aTouch->mRadius.y; +} + +JSObject* Touch::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) { + return Touch_Binding::Wrap(aCx, this, aGivenProto); +} + +// Parent ourselves to the global of the target. This achieves the desirable +// effects of parenting to the target, but avoids making the touch inaccessible +// when the target happens to be NAC and therefore reflected into the XBL scope. +nsIGlobalObject* Touch::GetParentObject() const { + if (!mOriginalTarget) { + return nullptr; + } + return mOriginalTarget->GetOwnerGlobal(); +} + +} // namespace mozilla::dom diff --git a/dom/events/Touch.h b/dom/events/Touch.h new file mode 100644 index 0000000000..43c5534808 --- /dev/null +++ b/dom/events/Touch.h @@ -0,0 +1,105 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#ifndef mozilla_dom_Touch_h_ +#define mozilla_dom_Touch_h_ + +#include "mozilla/Attributes.h" +#include "mozilla/EventForwards.h" +#include "mozilla/MouseEvents.h" +#include "mozilla/dom/BindingDeclarations.h" +#include "mozilla/dom/TouchBinding.h" +#include "nsWrapperCache.h" +#include "Units.h" + +class nsPresContext; + +namespace mozilla::dom { + +class EventTarget; + +class Touch final : public nsISupports, + public nsWrapperCache, + public WidgetPointerHelper { + public: + static bool PrefEnabled(JSContext* aCx, JSObject* aGlobal); + + static already_AddRefed<Touch> Constructor(const GlobalObject& aGlobal, + const TouchInit& aParam); + + Touch(EventTarget* aTarget, int32_t aIdentifier, int32_t aPageX, + int32_t aPageY, int32_t aScreenX, int32_t aScreenY, int32_t aClientX, + int32_t aClientY, int32_t aRadiusX, int32_t aRadiusY, + float aRotationAngle, float aForce); + Touch(int32_t aIdentifier, LayoutDeviceIntPoint aPoint, + LayoutDeviceIntPoint aRadius, float aRotationAngle, float aForce); + Touch(int32_t aIdentifier, LayoutDeviceIntPoint aPoint, + LayoutDeviceIntPoint aRadius, float aRotationAngle, float aForce, + int32_t aTiltX, int32_t aTiltY, int32_t aTwist); + Touch(const Touch& aOther); + + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(Touch) + + void InitializePoints(nsPresContext* aPresContext, WidgetEvent* aEvent); + + // Note, this sets both mOriginalTarget and mTarget. + void SetTouchTarget(EventTarget* aTarget); + + bool Equals(Touch* aTouch) const; + + // Update this touch's touch area to be the same as aTouch + void SetSameAs(const Touch* aTouch); + + virtual JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + nsIGlobalObject* GetParentObject() const; + + // WebIDL + int32_t Identifier() const { return mIdentifier; } + EventTarget* GetTarget() const; + int32_t ScreenX(CallerType aCallerType) const; + int32_t ScreenY(CallerType aCallerType) const; + int32_t ClientX() const { return mClientPoint.x; } + int32_t ClientY() const { return mClientPoint.y; } + int32_t PageX() const { return mPagePoint.x; } + int32_t PageY() const { return mPagePoint.y; } + int32_t RadiusX(CallerType aCallerType) const; + int32_t RadiusY(CallerType aCallerType) const; + float RotationAngle(CallerType aCallerType) const; + float Force(CallerType aCallerType) const; + + EventTarget* GetOriginalTarget() const; + + nsCOMPtr<EventTarget> mOriginalTarget; + nsCOMPtr<EventTarget> mTarget; + LayoutDeviceIntPoint mRefPoint; + bool mChanged; + + // Is this touch instance being suppressed to dispatch touch event to content. + // We can't remove touch instance from WidgetTouchEvent::mTouches because we + // still need it when dispatching pointer events. + bool mIsTouchEventSuppressed; + + uint32_t mMessage; + int32_t mIdentifier; + CSSIntPoint mPagePoint; + CSSIntPoint mClientPoint; + CSSIntPoint mScreenPoint; + LayoutDeviceIntPoint mRadius; + float mRotationAngle; + float mForce; + + protected: + ~Touch(); + + bool mPointsInitialized; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_Touch_h_ diff --git a/dom/events/TouchEvent.cpp b/dom/events/TouchEvent.cpp new file mode 100644 index 0000000000..24068703be --- /dev/null +++ b/dom/events/TouchEvent.cpp @@ -0,0 +1,338 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#include "mozilla/dom/Navigator.h" +#include "mozilla/dom/TouchEvent.h" +#include "mozilla/dom/Touch.h" +#include "mozilla/dom/TouchListBinding.h" +#include "mozilla/BasePrincipal.h" +#include "mozilla/LookAndFeel.h" +#include "mozilla/Preferences.h" +#include "mozilla/StaticPrefs_dom.h" +#include "mozilla/TouchEvents.h" +#include "nsContentUtils.h" +#include "nsIDocShell.h" +#include "nsExceptionHandler.h" + +namespace mozilla::dom { + +/****************************************************************************** + * TouchList + *****************************************************************************/ + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(TouchList) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(TouchList, mParent, mPoints) + +NS_IMPL_CYCLE_COLLECTING_ADDREF(TouchList) +NS_IMPL_CYCLE_COLLECTING_RELEASE(TouchList) + +JSObject* TouchList::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return TouchList_Binding::Wrap(aCx, this, aGivenProto); +} + +// static +bool TouchList::PrefEnabled(JSContext* aCx, JSObject* aGlobal) { + return TouchEvent::PrefEnabled(aCx, aGlobal); +} + +/****************************************************************************** + * TouchEvent + *****************************************************************************/ + +TouchEvent::TouchEvent(EventTarget* aOwner, nsPresContext* aPresContext, + WidgetTouchEvent* aEvent) + : UIEvent( + aOwner, aPresContext, + aEvent ? aEvent : new WidgetTouchEvent(false, eVoidEvent, nullptr)) { + if (aEvent) { + mEventIsInternal = false; + + for (uint32_t i = 0; i < aEvent->mTouches.Length(); ++i) { + Touch* touch = aEvent->mTouches[i]; + touch->InitializePoints(mPresContext, aEvent); + } + } else { + mEventIsInternal = true; + } +} + +NS_IMPL_CYCLE_COLLECTION_INHERITED(TouchEvent, UIEvent, + mEvent->AsTouchEvent()->mTouches, mTouches, + mTargetTouches, mChangedTouches) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(TouchEvent) +NS_INTERFACE_MAP_END_INHERITING(UIEvent) + +NS_IMPL_ADDREF_INHERITED(TouchEvent, UIEvent) +NS_IMPL_RELEASE_INHERITED(TouchEvent, UIEvent) + +void TouchEvent::InitTouchEvent(const nsAString& aType, bool aCanBubble, + bool aCancelable, nsGlobalWindowInner* aView, + int32_t aDetail, bool aCtrlKey, bool aAltKey, + bool aShiftKey, bool aMetaKey, + TouchList* aTouches, TouchList* aTargetTouches, + TouchList* aChangedTouches) { + NS_ENSURE_TRUE_VOID(!mEvent->mFlags.mIsBeingDispatched); + + UIEvent::InitUIEvent(aType, aCanBubble, aCancelable, aView, aDetail); + mEvent->AsInputEvent()->InitBasicModifiers(aCtrlKey, aAltKey, aShiftKey, + aMetaKey); + + mEvent->AsTouchEvent()->mTouches.Clear(); + + // To support touch.target retargeting also when the event is + // created by JS, we need to copy Touch objects to the widget event. + // In order to not affect targetTouches, we don't check duplicates in that + // list. + mTargetTouches = aTargetTouches; + AssignTouchesToWidgetEvent(mTargetTouches, false); + mTouches = aTouches; + AssignTouchesToWidgetEvent(mTouches, true); + mChangedTouches = aChangedTouches; + AssignTouchesToWidgetEvent(mChangedTouches, true); +} + +void TouchEvent::AssignTouchesToWidgetEvent(TouchList* aList, + bool aCheckDuplicates) { + if (!aList) { + return; + } + WidgetTouchEvent* widgetTouchEvent = mEvent->AsTouchEvent(); + for (uint32_t i = 0; i < aList->Length(); ++i) { + Touch* touch = aList->Item(i); + if (touch && + (!aCheckDuplicates || !widgetTouchEvent->mTouches.Contains(touch))) { + widgetTouchEvent->mTouches.AppendElement(touch); + } + } +} + +TouchList* TouchEvent::Touches() { + if (!mTouches) { + WidgetTouchEvent* touchEvent = mEvent->AsTouchEvent(); + if (mEvent->mMessage == eTouchEnd || mEvent->mMessage == eTouchCancel) { + // for touchend events, remove any changed touches from mTouches + WidgetTouchEvent::AutoTouchArray unchangedTouches; + const WidgetTouchEvent::TouchArray& touches = touchEvent->mTouches; + for (uint32_t i = 0; i < touches.Length(); ++i) { + if (!touches[i]->mChanged) { + unchangedTouches.AppendElement(touches[i]); + } + } + mTouches = new TouchList(ToSupports(this), unchangedTouches); + } else { + mTouches = new TouchList(ToSupports(this), touchEvent->mTouches); + } + } + return mTouches; +} + +TouchList* TouchEvent::TargetTouches() { + if (!mTargetTouches || !mTargetTouches->Length()) { + WidgetTouchEvent* touchEvent = mEvent->AsTouchEvent(); + if (!mTargetTouches) { + mTargetTouches = new TouchList(ToSupports(this)); + } + const WidgetTouchEvent::TouchArray& touches = touchEvent->mTouches; + for (uint32_t i = 0; i < touches.Length(); ++i) { + // for touchend/cancel events, don't append to the target list if this is + // a touch that is ending + if ((mEvent->mMessage != eTouchEnd && mEvent->mMessage != eTouchCancel) || + !touches[i]->mChanged) { + bool equalTarget = touches[i]->mTarget == mEvent->mTarget; + if (!equalTarget) { + // Need to still check if we're inside native anonymous content + // and the non-NAC target would be the same. + nsIContent* touchTarget = + nsIContent::FromEventTargetOrNull(touches[i]->mTarget); + nsIContent* eventTarget = + nsIContent::FromEventTargetOrNull(mEvent->mTarget); + equalTarget = touchTarget && eventTarget && + touchTarget->FindFirstNonChromeOnlyAccessContent() == + eventTarget->FindFirstNonChromeOnlyAccessContent(); + } + if (equalTarget) { + mTargetTouches->Append(touches[i]); + } + } + } + } + return mTargetTouches; +} + +TouchList* TouchEvent::ChangedTouches() { + if (!mChangedTouches) { + WidgetTouchEvent::AutoTouchArray changedTouches; + WidgetTouchEvent* touchEvent = mEvent->AsTouchEvent(); + const WidgetTouchEvent::TouchArray& touches = touchEvent->mTouches; + for (uint32_t i = 0; i < touches.Length(); ++i) { + if (touches[i]->mChanged) { + changedTouches.AppendElement(touches[i]); + } + } + mChangedTouches = new TouchList(ToSupports(this), changedTouches); + } + return mChangedTouches; +} + +// static +bool TouchEvent::PrefEnabled(JSContext* aCx, JSObject* aGlobal) { + nsIDocShell* docShell = nullptr; + if (aGlobal) { + nsGlobalWindowInner* win = xpc::WindowOrNull(aGlobal); + if (win) { + docShell = win->GetDocShell(); + } + } + return PrefEnabled(docShell); +} + +static bool PlatformSupportsTouch() { + // Touch events are only actually supported if APZ is enabled. If APZ is + // disabled globally, we can check that once and incorporate that into the + // cached state. If APZ is enabled, we need to further check based on the + // widget, which we do in PrefEnabled (and don't cache that result). + static bool sIsTouchDeviceSupportPresent = + !!LookAndFeel::GetInt(LookAndFeel::IntID::TouchDeviceSupportPresent) && + gfxPlatform::AsyncPanZoomEnabled(); + + return sIsTouchDeviceSupportPresent; +} + +// static +bool TouchEvent::PrefEnabled(nsIDocShell* aDocShell) { + MOZ_DIAGNOSTIC_ASSERT(NS_IsMainThread()); + + auto touchEventsOverride = mozilla::dom::TouchEventsOverride::None; + if (aDocShell) { + if (BrowsingContext* bc = aDocShell->GetBrowsingContext()) { + touchEventsOverride = bc->TouchEventsOverride(); + } + } + + bool enabled = false; + if (touchEventsOverride == mozilla::dom::TouchEventsOverride::Enabled) { + enabled = true; + } else if (touchEventsOverride == + mozilla::dom::TouchEventsOverride::Disabled) { + enabled = false; + } else { + const int32_t prefValue = StaticPrefs::dom_w3c_touch_events_enabled(); + if (prefValue == 2) { + enabled = PlatformSupportsTouch(); + + static bool firstTime = true; + // The touch screen data seems to be inaccurate in the parent process, + // and we really need the crash annotation in child processes. + if (firstTime && !XRE_IsParentProcess()) { + CrashReporter::AnnotateCrashReport( + CrashReporter::Annotation::HasDeviceTouchScreen, enabled); + firstTime = false; + } + +#if defined(XP_WIN) || defined(MOZ_WIDGET_GTK) + if (enabled && aDocShell) { + // APZ might be disabled on this particular widget, in which case + // TouchEvent support will also be disabled. Try to detect that. + RefPtr<nsPresContext> pc = aDocShell->GetPresContext(); + if (pc) { + nsCOMPtr<nsIWidget> widget = pc->GetRootWidget(); + if (widget) { + enabled &= widget->AsyncPanZoomEnabled(); + } + } + } +#endif + } else { + enabled = !!prefValue; + } + } + + if (enabled) { + nsContentUtils::InitializeTouchEventTable(); + } + return enabled; +} + +// static +bool TouchEvent::LegacyAPIEnabled(JSContext* aCx, JSObject* aGlobal) { + nsIPrincipal* principal = nsContentUtils::SubjectPrincipal(aCx); + bool isSystem = principal && principal->IsSystemPrincipal(); + + nsIDocShell* docShell = nullptr; + if (aGlobal) { + nsGlobalWindowInner* win = xpc::WindowOrNull(aGlobal); + if (win) { + docShell = win->GetDocShell(); + } + } + return LegacyAPIEnabled(docShell, isSystem); +} + +// static +bool TouchEvent::LegacyAPIEnabled(nsIDocShell* aDocShell, + bool aCallerIsSystem) { + return (aCallerIsSystem || + StaticPrefs::dom_w3c_touch_events_legacy_apis_enabled() || + (aDocShell && aDocShell->GetBrowsingContext() && + aDocShell->GetBrowsingContext()->TouchEventsOverride() == + mozilla::dom::TouchEventsOverride::Enabled)) && + PrefEnabled(aDocShell); +} + +// static +already_AddRefed<TouchEvent> TouchEvent::Constructor( + const GlobalObject& aGlobal, const nsAString& aType, + const TouchEventInit& aParam) { + nsCOMPtr<EventTarget> t = do_QueryInterface(aGlobal.GetAsSupports()); + RefPtr<TouchEvent> e = new TouchEvent(t, nullptr, nullptr); + bool trusted = e->Init(t); + RefPtr<TouchList> touches = e->CopyTouches(aParam.mTouches); + RefPtr<TouchList> targetTouches = e->CopyTouches(aParam.mTargetTouches); + RefPtr<TouchList> changedTouches = e->CopyTouches(aParam.mChangedTouches); + e->InitTouchEvent(aType, aParam.mBubbles, aParam.mCancelable, aParam.mView, + aParam.mDetail, aParam.mCtrlKey, aParam.mAltKey, + aParam.mShiftKey, aParam.mMetaKey, touches, targetTouches, + changedTouches); + e->SetTrusted(trusted); + e->SetComposed(aParam.mComposed); + return e.forget(); +} + +already_AddRefed<TouchList> TouchEvent::CopyTouches( + const Sequence<OwningNonNull<Touch>>& aTouches) { + RefPtr<TouchList> list = new TouchList(GetParentObject()); + size_t len = aTouches.Length(); + for (size_t i = 0; i < len; ++i) { + list->Append(aTouches[i]); + } + return list.forget(); +} + +bool TouchEvent::AltKey() { return mEvent->AsTouchEvent()->IsAlt(); } + +bool TouchEvent::MetaKey() { return mEvent->AsTouchEvent()->IsMeta(); } + +bool TouchEvent::CtrlKey() { return mEvent->AsTouchEvent()->IsControl(); } + +bool TouchEvent::ShiftKey() { return mEvent->AsTouchEvent()->IsShift(); } + +} // namespace mozilla::dom + +using namespace mozilla; +using namespace mozilla::dom; + +already_AddRefed<TouchEvent> NS_NewDOMTouchEvent(EventTarget* aOwner, + nsPresContext* aPresContext, + WidgetTouchEvent* aEvent) { + RefPtr<TouchEvent> it = new TouchEvent(aOwner, aPresContext, aEvent); + return it.forget(); +} diff --git a/dom/events/TouchEvent.h b/dom/events/TouchEvent.h new file mode 100644 index 0000000000..eb88aa5868 --- /dev/null +++ b/dom/events/TouchEvent.h @@ -0,0 +1,121 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ +#ifndef mozilla_dom_TouchEvent_h_ +#define mozilla_dom_TouchEvent_h_ + +#include "mozilla/dom/Touch.h" +#include "mozilla/dom/TouchEventBinding.h" +#include "mozilla/dom/UIEvent.h" +#include "mozilla/Attributes.h" +#include "mozilla/EventForwards.h" +#include "mozilla/TouchEvents.h" +#include "nsJSEnvironment.h" +#include "nsStringFwd.h" +#include "nsWrapperCache.h" + +namespace mozilla::dom { + +class TouchList final : public nsISupports, public nsWrapperCache { + public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(TouchList) + + explicit TouchList(nsISupports* aParent) : mParent(aParent) { + nsJSContext::LikelyShortLivingObjectCreated(); + } + TouchList(nsISupports* aParent, const WidgetTouchEvent::TouchArray& aTouches) + : mParent(aParent), mPoints(aTouches.Clone()) { + nsJSContext::LikelyShortLivingObjectCreated(); + } + + void Append(Touch* aPoint) { mPoints.AppendElement(aPoint); } + + virtual JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + nsISupports* GetParentObject() const { return mParent; } + + static bool PrefEnabled(JSContext* aCx, JSObject* aGlobal); + + uint32_t Length() const { return mPoints.Length(); } + Touch* Item(uint32_t aIndex) const { return mPoints.SafeElementAt(aIndex); } + Touch* IndexedGetter(uint32_t aIndex, bool& aFound) const { + aFound = aIndex < mPoints.Length(); + if (!aFound) { + return nullptr; + } + return mPoints[aIndex]; + } + + void Clear() { mPoints.Clear(); } + + protected: + ~TouchList() = default; + + nsCOMPtr<nsISupports> mParent; + WidgetTouchEvent::TouchArray mPoints; +}; + +class TouchEvent : public UIEvent { + public: + TouchEvent(EventTarget* aOwner, nsPresContext* aPresContext, + WidgetTouchEvent* aEvent); + + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(TouchEvent, UIEvent) + + virtual JSObject* WrapObjectInternal( + JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override { + return TouchEvent_Binding::Wrap(aCx, this, aGivenProto); + } + + already_AddRefed<TouchList> CopyTouches( + const Sequence<OwningNonNull<Touch>>& aTouches); + + TouchList* Touches(); + // TargetTouches() populates mTargetTouches from widget event's touch list. + TouchList* TargetTouches(); + // GetExistingTargetTouches just returns the existing target touches list. + TouchList* GetExistingTargetTouches() { return mTargetTouches; } + TouchList* ChangedTouches(); + + bool AltKey(); + bool MetaKey(); + bool CtrlKey(); + bool ShiftKey(); + + void InitTouchEvent(const nsAString& aType, bool aCanBubble, bool aCancelable, + nsGlobalWindowInner* aView, int32_t aDetail, + bool aCtrlKey, bool aAltKey, bool aShiftKey, + bool aMetaKey, TouchList* aTouches, + TouchList* aTargetTouches, TouchList* aChangedTouches); + + static bool PrefEnabled(JSContext* aCx, JSObject* aGlobal); + static bool PrefEnabled(nsIDocShell* aDocShell); + static bool LegacyAPIEnabled(JSContext* aCx, JSObject* aGlobal); + static bool LegacyAPIEnabled(nsIDocShell* aDocShell, bool aCallerIsSystem); + + static already_AddRefed<TouchEvent> Constructor(const GlobalObject& aGlobal, + const nsAString& aType, + const TouchEventInit& aParam); + + protected: + ~TouchEvent() = default; + + void AssignTouchesToWidgetEvent(TouchList* aList, bool aCheckDuplicates); + + RefPtr<TouchList> mTouches; + RefPtr<TouchList> mTargetTouches; + RefPtr<TouchList> mChangedTouches; +}; + +} // namespace mozilla::dom + +already_AddRefed<mozilla::dom::TouchEvent> NS_NewDOMTouchEvent( + mozilla::dom::EventTarget* aOwner, nsPresContext* aPresContext, + mozilla::WidgetTouchEvent* aEvent); + +#endif // mozilla_dom_TouchEvent_h_ diff --git a/dom/events/TransitionEvent.cpp b/dom/events/TransitionEvent.cpp new file mode 100644 index 0000000000..696f54ce49 --- /dev/null +++ b/dom/events/TransitionEvent.cpp @@ -0,0 +1,68 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#include "mozilla/dom/TransitionEvent.h" +#include "mozilla/ContentEvents.h" +#include "prtime.h" + +namespace mozilla::dom { + +TransitionEvent::TransitionEvent(EventTarget* aOwner, + nsPresContext* aPresContext, + InternalTransitionEvent* aEvent) + : Event(aOwner, aPresContext, + aEvent ? aEvent : new InternalTransitionEvent(false, eVoidEvent)) { + if (aEvent) { + mEventIsInternal = false; + } else { + mEventIsInternal = true; + } +} + +// static +already_AddRefed<TransitionEvent> TransitionEvent::Constructor( + const GlobalObject& aGlobal, const nsAString& aType, + const TransitionEventInit& aParam) { + nsCOMPtr<EventTarget> t = do_QueryInterface(aGlobal.GetAsSupports()); + RefPtr<TransitionEvent> e = new TransitionEvent(t, nullptr, nullptr); + bool trusted = e->Init(t); + + e->InitEvent(aType, aParam.mBubbles, aParam.mCancelable); + + InternalTransitionEvent* internalEvent = e->mEvent->AsTransitionEvent(); + internalEvent->mPropertyName = aParam.mPropertyName; + internalEvent->mElapsedTime = aParam.mElapsedTime; + internalEvent->mPseudoElement = aParam.mPseudoElement; + + e->SetTrusted(trusted); + e->SetComposed(aParam.mComposed); + return e.forget(); +} + +void TransitionEvent::GetPropertyName(nsAString& aPropertyName) const { + aPropertyName = mEvent->AsTransitionEvent()->mPropertyName; +} + +float TransitionEvent::ElapsedTime() { + return mEvent->AsTransitionEvent()->mElapsedTime; +} + +void TransitionEvent::GetPseudoElement(nsAString& aPseudoElement) const { + aPseudoElement = mEvent->AsTransitionEvent()->mPseudoElement; +} + +} // namespace mozilla::dom + +using namespace mozilla; +using namespace mozilla::dom; + +already_AddRefed<TransitionEvent> NS_NewDOMTransitionEvent( + EventTarget* aOwner, nsPresContext* aPresContext, + InternalTransitionEvent* aEvent) { + RefPtr<TransitionEvent> it = + new TransitionEvent(aOwner, aPresContext, aEvent); + return it.forget(); +} diff --git a/dom/events/TransitionEvent.h b/dom/events/TransitionEvent.h new file mode 100644 index 0000000000..74c9001b00 --- /dev/null +++ b/dom/events/TransitionEvent.h @@ -0,0 +1,47 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ +#ifndef mozilla_dom_TransitionEvent_h_ +#define mozilla_dom_TransitionEvent_h_ + +#include "mozilla/EventForwards.h" +#include "mozilla/dom/Event.h" +#include "mozilla/dom/TransitionEventBinding.h" +#include "nsStringFwd.h" + +namespace mozilla::dom { + +class TransitionEvent : public Event { + public: + TransitionEvent(EventTarget* aOwner, nsPresContext* aPresContext, + InternalTransitionEvent* aEvent); + + NS_INLINE_DECL_REFCOUNTING_INHERITED(TransitionEvent, Event) + + static already_AddRefed<TransitionEvent> Constructor( + const GlobalObject& aGlobal, const nsAString& aType, + const TransitionEventInit& aParam); + + virtual JSObject* WrapObjectInternal( + JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override { + return TransitionEvent_Binding::Wrap(aCx, this, aGivenProto); + } + + void GetPropertyName(nsAString& aPropertyName) const; + void GetPseudoElement(nsAString& aPreudoElement) const; + + float ElapsedTime(); + + protected: + ~TransitionEvent() = default; +}; + +} // namespace mozilla::dom + +already_AddRefed<mozilla::dom::TransitionEvent> NS_NewDOMTransitionEvent( + mozilla::dom::EventTarget* aOwner, nsPresContext* aPresContext, + mozilla::InternalTransitionEvent* aEvent); + +#endif // mozilla_dom_TransitionEvent_h_ diff --git a/dom/events/UIEvent.cpp b/dom/events/UIEvent.cpp new file mode 100644 index 0000000000..2f4dd914b8 --- /dev/null +++ b/dom/events/UIEvent.cpp @@ -0,0 +1,336 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#include "base/basictypes.h" +#include "ipc/IPCMessageUtils.h" +#include "ipc/IPCMessageUtilsSpecializations.h" +#include "mozilla/dom/UIEvent.h" +#include "mozilla/ArrayUtils.h" +#include "mozilla/Assertions.h" +#include "mozilla/ContentEvents.h" +#include "mozilla/EventStateManager.h" +#include "mozilla/PointerLockManager.h" +#include "mozilla/PresShell.h" +#include "mozilla/TextEvents.h" +#include "nsCOMPtr.h" +#include "nsContentUtils.h" +#include "nsIContent.h" +#include "nsIInterfaceRequestorUtils.h" +#include "nsIDocShell.h" +#include "nsIFrame.h" +#include "nsLayoutUtils.h" +#include "prtime.h" + +namespace mozilla::dom { + +UIEvent::UIEvent(EventTarget* aOwner, nsPresContext* aPresContext, + WidgetGUIEvent* aEvent) + : Event(aOwner, aPresContext, + aEvent ? aEvent : new InternalUIEvent(false, eVoidEvent, nullptr)), + mDefaultClientPoint(0, 0), + mLayerPoint(0, 0), + mPagePoint(0, 0), + mMovementPoint(0, 0), + mIsPointerLocked(PointerLockManager::IsLocked()), + mLastClientPoint(EventStateManager::sLastClientPoint) { + if (aEvent) { + mEventIsInternal = false; + } else { + mEventIsInternal = true; + } + + // Fill mDetail and mView according to the mEvent (widget-generated + // event) we've got + switch (mEvent->mClass) { + case eUIEventClass: { + mDetail = mEvent->AsUIEvent()->mDetail; + break; + } + + case eScrollPortEventClass: { + InternalScrollPortEvent* scrollEvent = mEvent->AsScrollPortEvent(); + mDetail = static_cast<int32_t>(scrollEvent->mOrient); + break; + } + + default: + mDetail = 0; + break; + } + + mView = nullptr; + if (mPresContext) { + nsIDocShell* docShell = mPresContext->GetDocShell(); + if (docShell) { + mView = docShell->GetWindow(); + } + } +} + +// static +already_AddRefed<UIEvent> UIEvent::Constructor(const GlobalObject& aGlobal, + const nsAString& aType, + const UIEventInit& aParam) { + nsCOMPtr<EventTarget> t = do_QueryInterface(aGlobal.GetAsSupports()); + RefPtr<UIEvent> e = new UIEvent(t, nullptr, nullptr); + bool trusted = e->Init(t); + e->InitUIEvent(aType, aParam.mBubbles, aParam.mCancelable, aParam.mView, + aParam.mDetail); + e->SetTrusted(trusted); + e->SetComposed(aParam.mComposed); + return e.forget(); +} + +NS_IMPL_CYCLE_COLLECTION_INHERITED(UIEvent, Event, mView) + +NS_IMPL_ADDREF_INHERITED(UIEvent, Event) +NS_IMPL_RELEASE_INHERITED(UIEvent, Event) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(UIEvent) +NS_INTERFACE_MAP_END_INHERITING(Event) + +static nsIntPoint DevPixelsToCSSPixels(const LayoutDeviceIntPoint& aPoint, + nsPresContext* aContext) { + return nsIntPoint(aContext->DevPixelsToIntCSSPixels(aPoint.x), + aContext->DevPixelsToIntCSSPixels(aPoint.y)); +} + +nsIntPoint UIEvent::GetMovementPoint() { + if (mEvent->mFlags.mIsPositionless) { + return nsIntPoint(0, 0); + } + + if (mPrivateDataDuplicated || mEventIsInternal) { + return mMovementPoint; + } + + if (!mEvent || !mEvent->AsGUIEvent()->mWidget || + (mEvent->mMessage != eMouseMove && mEvent->mMessage != ePointerMove)) { + // Pointer Lock spec defines that movementX/Y must be zero for all mouse + // events except mousemove. + return nsIntPoint(0, 0); + } + + // Calculate the delta between the last screen point and the current one. + nsIntPoint current = DevPixelsToCSSPixels(mEvent->mRefPoint, mPresContext); + nsIntPoint last = DevPixelsToCSSPixels(mEvent->mLastRefPoint, mPresContext); + return current - last; +} + +void UIEvent::InitUIEvent(const nsAString& typeArg, bool canBubbleArg, + bool cancelableArg, nsGlobalWindowInner* viewArg, + int32_t detailArg) { + if (NS_WARN_IF(mEvent->mFlags.mIsBeingDispatched)) { + return; + } + + Event::InitEvent(typeArg, canBubbleArg, cancelableArg); + + mDetail = detailArg; + mView = viewArg ? viewArg->GetOuterWindow() : nullptr; +} + +already_AddRefed<nsIContent> UIEvent::GetRangeParentContentAndOffset( + int32_t* aOffset) const { + if (NS_WARN_IF(!mPresContext)) { + return nullptr; + } + RefPtr<PresShell> presShell = mPresContext->GetPresShell(); + if (NS_WARN_IF(!presShell)) { + return nullptr; + } + nsCOMPtr<nsIContent> container; + nsLayoutUtils::GetContainerAndOffsetAtEvent( + presShell, mEvent, getter_AddRefs(container), aOffset); + return container.forget(); +} + +int32_t UIEvent::RangeOffset() const { + if (NS_WARN_IF(!mPresContext)) { + return 0; + } + RefPtr<PresShell> presShell = mPresContext->GetPresShell(); + if (NS_WARN_IF(!presShell)) { + return 0; + } + int32_t offset = 0; + nsLayoutUtils::GetContainerAndOffsetAtEvent(presShell, mEvent, nullptr, + &offset); + return offset; +} + +nsIntPoint UIEvent::GetLayerPoint() const { + if (mEvent->mFlags.mIsPositionless) { + return nsIntPoint(0, 0); + } + + if (!mEvent || + (mEvent->mClass != eMouseEventClass && + mEvent->mClass != eMouseScrollEventClass && + mEvent->mClass != eWheelEventClass && + mEvent->mClass != ePointerEventClass && + mEvent->mClass != eTouchEventClass && + mEvent->mClass != eDragEventClass && + mEvent->mClass != eSimpleGestureEventClass) || + !mPresContext || mEventIsInternal) { + return mLayerPoint; + } + // XXX I'm not really sure this is correct; it's my best shot, though + nsIFrame* targetFrame = mPresContext->EventStateManager()->GetEventTarget(); + if (!targetFrame) return mLayerPoint; + nsIFrame* layer = nsLayoutUtils::GetClosestLayer(targetFrame); + nsPoint pt( + nsLayoutUtils::GetEventCoordinatesRelativeTo(mEvent, RelativeTo{layer})); + return nsIntPoint(nsPresContext::AppUnitsToIntCSSPixels(pt.x), + nsPresContext::AppUnitsToIntCSSPixels(pt.y)); +} + +void UIEvent::DuplicatePrivateData() { + mDefaultClientPoint = Event::GetClientCoords( + mPresContext, mEvent, mEvent->mRefPoint, mDefaultClientPoint); + mMovementPoint = GetMovementPoint(); + mLayerPoint = GetLayerPoint(); + mPagePoint = Event::GetPageCoords(mPresContext, mEvent, mEvent->mRefPoint, + mDefaultClientPoint); + // GetScreenPoint converts mEvent->mRefPoint to right coordinates. + CSSIntPoint screenPoint = + Event::GetScreenCoords(mPresContext, mEvent, mEvent->mRefPoint) + .valueOr(CSSIntPoint{0, 0}); + + Event::DuplicatePrivateData(); + + CSSToLayoutDeviceScale scale = mPresContext + ? mPresContext->CSSToDevPixelScale() + : CSSToLayoutDeviceScale(1); + mEvent->mRefPoint = RoundedToInt(screenPoint * scale); +} + +void UIEvent::Serialize(IPC::MessageWriter* aWriter, + bool aSerializeInterfaceType) { + if (aSerializeInterfaceType) { + IPC::WriteParam(aWriter, u"uievent"_ns); + } + + Event::Serialize(aWriter, false); + + IPC::WriteParam(aWriter, Detail()); +} + +bool UIEvent::Deserialize(IPC::MessageReader* aReader) { + NS_ENSURE_TRUE(Event::Deserialize(aReader), false); + NS_ENSURE_TRUE(IPC::ReadParam(aReader, &mDetail), false); + return true; +} + +// XXX Following struct and array are used only in +// UIEvent::ComputeModifierState(), but if we define them in it, +// we fail to build on Mac at calling mozilla::ArrayLength(). +struct ModifierPair { + Modifier modifier; + const char* name; +}; +static const ModifierPair kPairs[] = { + // clang-format off + { MODIFIER_ALT, NS_DOM_KEYNAME_ALT }, + { MODIFIER_ALTGRAPH, NS_DOM_KEYNAME_ALTGRAPH }, + { MODIFIER_CAPSLOCK, NS_DOM_KEYNAME_CAPSLOCK }, + { MODIFIER_CONTROL, NS_DOM_KEYNAME_CONTROL }, + { MODIFIER_FN, NS_DOM_KEYNAME_FN }, + { MODIFIER_FNLOCK, NS_DOM_KEYNAME_FNLOCK }, + { MODIFIER_META, NS_DOM_KEYNAME_META }, + { MODIFIER_NUMLOCK, NS_DOM_KEYNAME_NUMLOCK }, + { MODIFIER_SCROLLLOCK, NS_DOM_KEYNAME_SCROLLLOCK }, + { MODIFIER_SHIFT, NS_DOM_KEYNAME_SHIFT }, + { MODIFIER_SYMBOL, NS_DOM_KEYNAME_SYMBOL }, + { MODIFIER_SYMBOLLOCK, NS_DOM_KEYNAME_SYMBOLLOCK }, + { MODIFIER_OS, NS_DOM_KEYNAME_OS } + // clang-format on +}; + +// static +Modifiers UIEvent::ComputeModifierState(const nsAString& aModifiersList) { + if (aModifiersList.IsEmpty()) { + return 0; + } + + // Be careful about the performance. If aModifiersList is too long, + // parsing it needs too long time. + // XXX Should we abort if aModifiersList is too long? + + Modifiers modifiers = 0; + + nsAString::const_iterator listStart, listEnd; + aModifiersList.BeginReading(listStart); + aModifiersList.EndReading(listEnd); + + for (uint32_t i = 0; i < ArrayLength(kPairs); i++) { + nsAString::const_iterator start(listStart), end(listEnd); + if (!FindInReadable(NS_ConvertASCIItoUTF16(kPairs[i].name), start, end)) { + continue; + } + + if ((start != listStart && !NS_IsAsciiWhitespace(*(--start))) || + (end != listEnd && !NS_IsAsciiWhitespace(*(end)))) { + continue; + } + modifiers |= kPairs[i].modifier; + } + + return modifiers; +} + +bool UIEvent::GetModifierStateInternal(const nsAString& aKey) { + WidgetInputEvent* inputEvent = mEvent->AsInputEvent(); + MOZ_ASSERT(inputEvent, "mEvent must be WidgetInputEvent or derived class"); + return ((inputEvent->mModifiers & WidgetInputEvent::GetModifier(aKey)) != 0); +} + +void UIEvent::InitModifiers(const EventModifierInit& aParam) { + if (NS_WARN_IF(!mEvent)) { + return; + } + WidgetInputEvent* inputEvent = mEvent->AsInputEvent(); + MOZ_ASSERT(inputEvent, + "This method shouldn't be called if it doesn't have modifiers"); + if (NS_WARN_IF(!inputEvent)) { + return; + } + + inputEvent->mModifiers = MODIFIER_NONE; + +#define SET_MODIFIER(aName, aValue) \ + if (aParam.m##aName) { \ + inputEvent->mModifiers |= aValue; \ + } + + SET_MODIFIER(CtrlKey, MODIFIER_CONTROL) + SET_MODIFIER(ShiftKey, MODIFIER_SHIFT) + SET_MODIFIER(AltKey, MODIFIER_ALT) + SET_MODIFIER(MetaKey, MODIFIER_META) + SET_MODIFIER(ModifierAltGraph, MODIFIER_ALTGRAPH) + SET_MODIFIER(ModifierCapsLock, MODIFIER_CAPSLOCK) + SET_MODIFIER(ModifierFn, MODIFIER_FN) + SET_MODIFIER(ModifierFnLock, MODIFIER_FNLOCK) + SET_MODIFIER(ModifierNumLock, MODIFIER_NUMLOCK) + SET_MODIFIER(ModifierOS, MODIFIER_OS) + SET_MODIFIER(ModifierScrollLock, MODIFIER_SCROLLLOCK) + SET_MODIFIER(ModifierSymbol, MODIFIER_SYMBOL) + SET_MODIFIER(ModifierSymbolLock, MODIFIER_SYMBOLLOCK) + +#undef SET_MODIFIER +} + +} // namespace mozilla::dom + +using namespace mozilla; +using namespace mozilla::dom; + +already_AddRefed<UIEvent> NS_NewDOMUIEvent(EventTarget* aOwner, + nsPresContext* aPresContext, + WidgetGUIEvent* aEvent) { + RefPtr<UIEvent> it = new UIEvent(aOwner, aPresContext, aEvent); + return it.forget(); +} diff --git a/dom/events/UIEvent.h b/dom/events/UIEvent.h new file mode 100644 index 0000000000..02c23c69d5 --- /dev/null +++ b/dom/events/UIEvent.h @@ -0,0 +1,124 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#ifndef mozilla_dom_UIEvent_h_ +#define mozilla_dom_UIEvent_h_ + +#include "mozilla/Attributes.h" +#include "mozilla/dom/Event.h" +#include "mozilla/dom/Nullable.h" +#include "mozilla/dom/UIEventBinding.h" +#include "mozilla/dom/WindowProxyHolder.h" +#include "nsDeviceContext.h" +#include "nsDocShell.h" +#include "nsIContent.h" +#include "nsPresContext.h" + +class nsINode; + +namespace mozilla::dom { + +class UIEvent : public Event { + public: + UIEvent(EventTarget* aOwner, nsPresContext* aPresContext, + WidgetGUIEvent* aEvent); + + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(UIEvent, Event) + + void DuplicatePrivateData() override; + void Serialize(IPC::MessageWriter* aWriter, + bool aSerializeInterfaceType) override; + bool Deserialize(IPC::MessageReader* aReader) override; + + static already_AddRefed<UIEvent> Constructor(const GlobalObject& aGlobal, + const nsAString& aType, + const UIEventInit& aParam); + + virtual JSObject* WrapObjectInternal( + JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override { + return UIEvent_Binding::Wrap(aCx, this, aGivenProto); + } + + UIEvent* AsUIEvent() override { return this; } + + void InitUIEvent(const nsAString& typeArg, bool canBubbleArg, + bool cancelableArg, nsGlobalWindowInner* viewArg, + int32_t detailArg); + + Nullable<WindowProxyHolder> GetView() const { + if (!mView) { + return nullptr; + } + return WindowProxyHolder(mView->GetBrowsingContext()); + } + + int32_t Detail() const { return mDetail; } + + int32_t LayerX() const { return GetLayerPoint().x; } + + int32_t LayerY() const { return GetLayerPoint().y; } + + virtual uint32_t Which(CallerType aCallerType = CallerType::System) { + MOZ_ASSERT(mEvent->mClass != eKeyboardEventClass, + "Key events should override Which()"); + MOZ_ASSERT(mEvent->mClass != eMouseEventClass, + "Mouse events should override Which()"); + return 0; + } + + /** + * GetRangeParent() should be used only by JS. C++ callers should use + * GetRangeParentContent() or GetRangeParentContentAndOffset() instead. + */ + MOZ_CAN_RUN_SCRIPT already_AddRefed<nsINode> GetRangeParent() { + return GetRangeParentContent(); + } + MOZ_CAN_RUN_SCRIPT already_AddRefed<nsIContent> GetRangeParentContent() { + return GetRangeParentContentAndOffset(nullptr); + } + /** + * aOffset is optional (i.e., can be nullptr), but when you call this with + * nullptr, you should use GetRangeParentContent() instead. + */ + MOZ_CAN_RUN_SCRIPT already_AddRefed<nsIContent> + GetRangeParentContentAndOffset(int32_t* aOffset) const; + + /** + * If you also need to compute range parent in C++ code, you should use + * GetRangeParentContentAndOffset() instead. + */ + MOZ_CAN_RUN_SCRIPT int32_t RangeOffset() const; + + protected: + ~UIEvent() = default; + + // Internal helper functions + nsIntPoint GetMovementPoint(); + nsIntPoint GetLayerPoint() const; + + nsCOMPtr<nsPIDOMWindowOuter> mView; + int32_t mDetail; + CSSIntPoint mDefaultClientPoint; + // Screenpoint is mEvent->mRefPoint. + nsIntPoint mLayerPoint; + CSSIntPoint mPagePoint; + nsIntPoint mMovementPoint; + bool mIsPointerLocked; + CSSIntPoint mLastClientPoint; + + static Modifiers ComputeModifierState(const nsAString& aModifiersList); + bool GetModifierStateInternal(const nsAString& aKey); + void InitModifiers(const EventModifierInit& aParam); +}; + +} // namespace mozilla::dom + +already_AddRefed<mozilla::dom::UIEvent> NS_NewDOMUIEvent( + mozilla::dom::EventTarget* aOwner, nsPresContext* aPresContext, + mozilla::WidgetGUIEvent* aEvent); + +#endif // mozilla_dom_UIEvent_h_ diff --git a/dom/events/VirtualKeyCodeList.h b/dom/events/VirtualKeyCodeList.h new file mode 100644 index 0000000000..184048d172 --- /dev/null +++ b/dom/events/VirtualKeyCodeList.h @@ -0,0 +1,245 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ +// IWYU pragma: private, include "mozilla/KeyTextEvents.h" + +/** + * This header file defines all DOM keys which are defined in KeyboardEvent. + * You must define NS_DEFINE_VK macro before including this. + * + * It must have two arguments, (aDOMKeyName, aDOMKeyCode) + * aDOMKeyName is a key name in DOM. + * aDOMKeyCode is one of mozilla::dom::KeyboardEvent_Binding::DOM_VK_*. + * + * Optionally, you can define NS_DISALLOW_SAME_KEYCODE. + * + * If NS_DISALLOW_SAME_KEYCODE is defined, same keyCode won't listed up. + * This is useful when you create switch-case statement. + */ + +#define DEFINE_VK_INTERNAL(aKeyName) \ + NS_DEFINE_VK(VK##aKeyName, \ + mozilla::dom::KeyboardEvent_Binding::DOM_VK##aKeyName) + +// Some keycode may have different name in KeyboardEvent from its key name. +#define DEFINE_VK_INTERNAL2(aKeyName, aKeyCodeName) \ + NS_DEFINE_VK(VK##aKeyName, \ + mozilla::dom::KeyboardEvent_Binding::DOM_VK##aKeyCodeName) + +DEFINE_VK_INTERNAL(_CANCEL) +DEFINE_VK_INTERNAL(_HELP) +DEFINE_VK_INTERNAL2(_BACK, _BACK_SPACE) +DEFINE_VK_INTERNAL(_TAB) +DEFINE_VK_INTERNAL(_CLEAR) +DEFINE_VK_INTERNAL(_RETURN) +DEFINE_VK_INTERNAL(_SHIFT) +DEFINE_VK_INTERNAL(_CONTROL) +DEFINE_VK_INTERNAL(_ALT) +DEFINE_VK_INTERNAL(_PAUSE) +DEFINE_VK_INTERNAL(_CAPS_LOCK) +#ifdef NS_DISALLOW_SAME_KEYCODE +DEFINE_VK_INTERNAL2(_KANA_OR_HANGUL, _KANA) +#else // #ifdef NS_DISALLOW_SAME_KEYCODE +DEFINE_VK_INTERNAL(_KANA) +DEFINE_VK_INTERNAL(_HANGUL) +#endif +DEFINE_VK_INTERNAL(_EISU) +DEFINE_VK_INTERNAL(_JUNJA) +DEFINE_VK_INTERNAL(_FINAL) +#ifdef NS_DISALLOW_SAME_KEYCODE +DEFINE_VK_INTERNAL2(_HANJA_OR_KANJI, _HANJA) +#else // #ifdef NS_DISALLOW_SAME_KEYCODE +DEFINE_VK_INTERNAL(_HANJA) +DEFINE_VK_INTERNAL(_KANJI) +#endif +DEFINE_VK_INTERNAL(_ESCAPE) +DEFINE_VK_INTERNAL(_CONVERT) +DEFINE_VK_INTERNAL(_NONCONVERT) +DEFINE_VK_INTERNAL(_ACCEPT) +DEFINE_VK_INTERNAL(_MODECHANGE) +DEFINE_VK_INTERNAL(_SPACE) +DEFINE_VK_INTERNAL(_PAGE_UP) +DEFINE_VK_INTERNAL(_PAGE_DOWN) +DEFINE_VK_INTERNAL(_END) +DEFINE_VK_INTERNAL(_HOME) +DEFINE_VK_INTERNAL(_LEFT) +DEFINE_VK_INTERNAL(_UP) +DEFINE_VK_INTERNAL(_RIGHT) +DEFINE_VK_INTERNAL(_DOWN) +DEFINE_VK_INTERNAL(_SELECT) +DEFINE_VK_INTERNAL(_PRINT) +DEFINE_VK_INTERNAL(_EXECUTE) +DEFINE_VK_INTERNAL(_PRINTSCREEN) +DEFINE_VK_INTERNAL(_INSERT) +DEFINE_VK_INTERNAL(_DELETE) + +DEFINE_VK_INTERNAL(_0) +DEFINE_VK_INTERNAL(_1) +DEFINE_VK_INTERNAL(_2) +DEFINE_VK_INTERNAL(_3) +DEFINE_VK_INTERNAL(_4) +DEFINE_VK_INTERNAL(_5) +DEFINE_VK_INTERNAL(_6) +DEFINE_VK_INTERNAL(_7) +DEFINE_VK_INTERNAL(_8) +DEFINE_VK_INTERNAL(_9) + +DEFINE_VK_INTERNAL(_COLON) +DEFINE_VK_INTERNAL(_SEMICOLON) +DEFINE_VK_INTERNAL(_LESS_THAN) +DEFINE_VK_INTERNAL(_EQUALS) +DEFINE_VK_INTERNAL(_GREATER_THAN) +DEFINE_VK_INTERNAL(_QUESTION_MARK) +DEFINE_VK_INTERNAL(_AT) + +DEFINE_VK_INTERNAL(_A) +DEFINE_VK_INTERNAL(_B) +DEFINE_VK_INTERNAL(_C) +DEFINE_VK_INTERNAL(_D) +DEFINE_VK_INTERNAL(_E) +DEFINE_VK_INTERNAL(_F) +DEFINE_VK_INTERNAL(_G) +DEFINE_VK_INTERNAL(_H) +DEFINE_VK_INTERNAL(_I) +DEFINE_VK_INTERNAL(_J) +DEFINE_VK_INTERNAL(_K) +DEFINE_VK_INTERNAL(_L) +DEFINE_VK_INTERNAL(_M) +DEFINE_VK_INTERNAL(_N) +DEFINE_VK_INTERNAL(_O) +DEFINE_VK_INTERNAL(_P) +DEFINE_VK_INTERNAL(_Q) +DEFINE_VK_INTERNAL(_R) +DEFINE_VK_INTERNAL(_S) +DEFINE_VK_INTERNAL(_T) +DEFINE_VK_INTERNAL(_U) +DEFINE_VK_INTERNAL(_V) +DEFINE_VK_INTERNAL(_W) +DEFINE_VK_INTERNAL(_X) +DEFINE_VK_INTERNAL(_Y) +DEFINE_VK_INTERNAL(_Z) + +DEFINE_VK_INTERNAL(_WIN) +DEFINE_VK_INTERNAL(_CONTEXT_MENU) +DEFINE_VK_INTERNAL(_SLEEP) + +DEFINE_VK_INTERNAL(_NUMPAD0) +DEFINE_VK_INTERNAL(_NUMPAD1) +DEFINE_VK_INTERNAL(_NUMPAD2) +DEFINE_VK_INTERNAL(_NUMPAD3) +DEFINE_VK_INTERNAL(_NUMPAD4) +DEFINE_VK_INTERNAL(_NUMPAD5) +DEFINE_VK_INTERNAL(_NUMPAD6) +DEFINE_VK_INTERNAL(_NUMPAD7) +DEFINE_VK_INTERNAL(_NUMPAD8) +DEFINE_VK_INTERNAL(_NUMPAD9) +DEFINE_VK_INTERNAL(_MULTIPLY) +DEFINE_VK_INTERNAL(_ADD) +DEFINE_VK_INTERNAL(_SEPARATOR) +DEFINE_VK_INTERNAL(_SUBTRACT) +DEFINE_VK_INTERNAL(_DECIMAL) +DEFINE_VK_INTERNAL(_DIVIDE) + +DEFINE_VK_INTERNAL(_F1) +DEFINE_VK_INTERNAL(_F2) +DEFINE_VK_INTERNAL(_F3) +DEFINE_VK_INTERNAL(_F4) +DEFINE_VK_INTERNAL(_F5) +DEFINE_VK_INTERNAL(_F6) +DEFINE_VK_INTERNAL(_F7) +DEFINE_VK_INTERNAL(_F8) +DEFINE_VK_INTERNAL(_F9) +DEFINE_VK_INTERNAL(_F10) +DEFINE_VK_INTERNAL(_F11) +DEFINE_VK_INTERNAL(_F12) +DEFINE_VK_INTERNAL(_F13) +DEFINE_VK_INTERNAL(_F14) +DEFINE_VK_INTERNAL(_F15) +DEFINE_VK_INTERNAL(_F16) +DEFINE_VK_INTERNAL(_F17) +DEFINE_VK_INTERNAL(_F18) +DEFINE_VK_INTERNAL(_F19) +DEFINE_VK_INTERNAL(_F20) +DEFINE_VK_INTERNAL(_F21) +DEFINE_VK_INTERNAL(_F22) +DEFINE_VK_INTERNAL(_F23) +DEFINE_VK_INTERNAL(_F24) + +DEFINE_VK_INTERNAL(_NUM_LOCK) +DEFINE_VK_INTERNAL(_SCROLL_LOCK) + +DEFINE_VK_INTERNAL(_WIN_OEM_FJ_JISHO) +DEFINE_VK_INTERNAL(_WIN_OEM_FJ_MASSHOU) +DEFINE_VK_INTERNAL(_WIN_OEM_FJ_TOUROKU) +DEFINE_VK_INTERNAL(_WIN_OEM_FJ_LOYA) +DEFINE_VK_INTERNAL(_WIN_OEM_FJ_ROYA) + +DEFINE_VK_INTERNAL(_CIRCUMFLEX) +DEFINE_VK_INTERNAL(_EXCLAMATION) +DEFINE_VK_INTERNAL(_DOUBLE_QUOTE) +DEFINE_VK_INTERNAL(_HASH) +DEFINE_VK_INTERNAL(_DOLLAR) +DEFINE_VK_INTERNAL(_PERCENT) +DEFINE_VK_INTERNAL(_AMPERSAND) +DEFINE_VK_INTERNAL(_UNDERSCORE) +DEFINE_VK_INTERNAL(_OPEN_PAREN) +DEFINE_VK_INTERNAL(_CLOSE_PAREN) +DEFINE_VK_INTERNAL(_ASTERISK) +DEFINE_VK_INTERNAL(_PLUS) +DEFINE_VK_INTERNAL(_PIPE) +DEFINE_VK_INTERNAL(_HYPHEN_MINUS) + +DEFINE_VK_INTERNAL(_OPEN_CURLY_BRACKET) +DEFINE_VK_INTERNAL(_CLOSE_CURLY_BRACKET) + +DEFINE_VK_INTERNAL(_TILDE) + +DEFINE_VK_INTERNAL(_VOLUME_MUTE) +DEFINE_VK_INTERNAL(_VOLUME_DOWN) +DEFINE_VK_INTERNAL(_VOLUME_UP) + +DEFINE_VK_INTERNAL(_COMMA) +DEFINE_VK_INTERNAL(_PERIOD) +DEFINE_VK_INTERNAL(_SLASH) +DEFINE_VK_INTERNAL(_BACK_QUOTE) +DEFINE_VK_INTERNAL(_OPEN_BRACKET) +DEFINE_VK_INTERNAL(_BACK_SLASH) +DEFINE_VK_INTERNAL(_CLOSE_BRACKET) +DEFINE_VK_INTERNAL(_QUOTE) + +DEFINE_VK_INTERNAL(_META) +DEFINE_VK_INTERNAL(_ALTGR) + +DEFINE_VK_INTERNAL(_WIN_ICO_HELP) +DEFINE_VK_INTERNAL(_WIN_ICO_00) + +DEFINE_VK_INTERNAL(_PROCESSKEY) + +DEFINE_VK_INTERNAL(_WIN_ICO_CLEAR) +DEFINE_VK_INTERNAL(_WIN_OEM_RESET) +DEFINE_VK_INTERNAL(_WIN_OEM_JUMP) +DEFINE_VK_INTERNAL(_WIN_OEM_PA1) +DEFINE_VK_INTERNAL(_WIN_OEM_PA2) +DEFINE_VK_INTERNAL(_WIN_OEM_PA3) +DEFINE_VK_INTERNAL(_WIN_OEM_WSCTRL) +DEFINE_VK_INTERNAL(_WIN_OEM_CUSEL) +DEFINE_VK_INTERNAL(_WIN_OEM_ATTN) +DEFINE_VK_INTERNAL(_WIN_OEM_FINISH) +DEFINE_VK_INTERNAL(_WIN_OEM_COPY) +DEFINE_VK_INTERNAL(_WIN_OEM_AUTO) +DEFINE_VK_INTERNAL(_WIN_OEM_ENLW) +DEFINE_VK_INTERNAL(_WIN_OEM_BACKTAB) + +DEFINE_VK_INTERNAL(_ATTN) +DEFINE_VK_INTERNAL(_CRSEL) +DEFINE_VK_INTERNAL(_EXSEL) +DEFINE_VK_INTERNAL(_EREOF) +DEFINE_VK_INTERNAL(_PLAY) +DEFINE_VK_INTERNAL(_ZOOM) +DEFINE_VK_INTERNAL(_PA1) +DEFINE_VK_INTERNAL(_WIN_OEM_CLEAR) + +#undef DEFINE_VK_INTERNAL +#undef DEFINE_VK_INTERNAL2 diff --git a/dom/events/WheelEvent.cpp b/dom/events/WheelEvent.cpp new file mode 100644 index 0000000000..56cfe113ea --- /dev/null +++ b/dom/events/WheelEvent.cpp @@ -0,0 +1,178 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#include "mozilla/dom/MouseEventBinding.h" +#include "mozilla/dom/WheelEvent.h" +#include "mozilla/MouseEvents.h" +#include "prtime.h" + +namespace mozilla::dom { + +WheelEvent::WheelEvent(EventTarget* aOwner, nsPresContext* aPresContext, + WidgetWheelEvent* aWheelEvent) + : MouseEvent(aOwner, aPresContext, + aWheelEvent + ? aWheelEvent + : new WidgetWheelEvent(false, eVoidEvent, nullptr)), + mAppUnitsPerDevPixel(0) { + if (aWheelEvent) { + mEventIsInternal = false; + // If the delta mode is pixel, the WidgetWheelEvent's delta values are in + // device pixels. However, JS contents need the delta values in CSS pixels. + // We should store the value of mAppUnitsPerDevPixel here because + // it might be changed by changing zoom or something. + if (aWheelEvent->mDeltaMode == WheelEvent_Binding::DOM_DELTA_PIXEL) { + mAppUnitsPerDevPixel = aPresContext->AppUnitsPerDevPixel(); + } + } else { + mEventIsInternal = true; + mEvent->mRefPoint = LayoutDeviceIntPoint(0, 0); + mEvent->AsWheelEvent()->mInputSource = + MouseEvent_Binding::MOZ_SOURCE_UNKNOWN; + } +} + +void WheelEvent::InitWheelEvent( + const nsAString& aType, bool aCanBubble, bool aCancelable, + nsGlobalWindowInner* aView, int32_t aDetail, int32_t aScreenX, + int32_t aScreenY, int32_t aClientX, int32_t aClientY, uint16_t aButton, + EventTarget* aRelatedTarget, const nsAString& aModifiersList, + double aDeltaX, double aDeltaY, double aDeltaZ, uint32_t aDeltaMode) { + NS_ENSURE_TRUE_VOID(!mEvent->mFlags.mIsBeingDispatched); + + MouseEvent::InitMouseEvent(aType, aCanBubble, aCancelable, aView, aDetail, + aScreenX, aScreenY, aClientX, aClientY, aButton, + aRelatedTarget, aModifiersList); + + WidgetWheelEvent* wheelEvent = mEvent->AsWheelEvent(); + // When specified by the caller (for JS-created events), don't mess with the + // delta mode. + wheelEvent->mDeltaModeCheckingState = + WidgetWheelEvent::DeltaModeCheckingState::Checked; + wheelEvent->mDeltaX = aDeltaX; + wheelEvent->mDeltaY = aDeltaY; + wheelEvent->mDeltaZ = aDeltaZ; + wheelEvent->mDeltaMode = aDeltaMode; + wheelEvent->mAllowToOverrideSystemScrollSpeed = false; +} + +int32_t WheelEvent::WheelDeltaX(CallerType aCallerType) { + WidgetWheelEvent* ev = mEvent->AsWheelEvent(); + if (ev->mWheelTicksX != 0.0) { + return int32_t(-ev->mWheelTicksX * kNativeTicksToWheelDelta); + } + if (IsTrusted()) { + // We always return pixels regardless of the checking-state. + double pixelDelta = + ev->mDeltaMode == WheelEvent_Binding::DOM_DELTA_PIXEL + ? DevToCssPixels(ev->OverriddenDeltaX()) + : ev->OverriddenDeltaX() * + CSSPixel::FromAppUnits(ev->mScrollAmount.width).Rounded(); + return int32_t(-std::round(pixelDelta * kTrustedDeltaToWheelDelta)); + } + return int32_t(-std::round(DeltaX(aCallerType))); // This matches Safari. +} + +int32_t WheelEvent::WheelDeltaY(CallerType aCallerType) { + WidgetWheelEvent* ev = mEvent->AsWheelEvent(); + if (ev->mWheelTicksY != 0.0) { + return int32_t(-ev->mWheelTicksY * kNativeTicksToWheelDelta); + } + + if (IsTrusted()) { + double pixelDelta = + ev->mDeltaMode == WheelEvent_Binding::DOM_DELTA_PIXEL + ? DevToCssPixels(ev->OverriddenDeltaY()) + : ev->OverriddenDeltaY() * + CSSPixel::FromAppUnits(ev->mScrollAmount.height).Rounded(); + return int32_t(-std::round(pixelDelta * kTrustedDeltaToWheelDelta)); + } + return int32_t(-std::round(DeltaY(aCallerType))); // This matches Safari. +} + +double WheelEvent::ToWebExposedDelta(WidgetWheelEvent& aWidgetEvent, + double aDelta, nscoord aLineOrPageAmount, + CallerType aCallerType) { + using DeltaModeCheckingState = WidgetWheelEvent::DeltaModeCheckingState; + if (aCallerType != CallerType::System) { + if (aWidgetEvent.mDeltaModeCheckingState == + DeltaModeCheckingState::Unknown) { + aWidgetEvent.mDeltaModeCheckingState = DeltaModeCheckingState::Unchecked; + } + if (aWidgetEvent.mDeltaModeCheckingState == + DeltaModeCheckingState::Unchecked && + aWidgetEvent.mDeltaMode == WheelEvent_Binding::DOM_DELTA_LINE) { + return aDelta * CSSPixel::FromAppUnits(aLineOrPageAmount).Rounded(); + } + } + return DevToCssPixels(aDelta); +} + +double WheelEvent::DeltaX(CallerType aCallerType) { + WidgetWheelEvent* ev = mEvent->AsWheelEvent(); + return ToWebExposedDelta(*ev, ev->OverriddenDeltaX(), ev->mScrollAmount.width, + aCallerType); +} + +double WheelEvent::DeltaY(CallerType aCallerType) { + WidgetWheelEvent* ev = mEvent->AsWheelEvent(); + return ToWebExposedDelta(*ev, ev->OverriddenDeltaY(), + ev->mScrollAmount.height, aCallerType); +} + +double WheelEvent::DeltaZ(CallerType aCallerType) { + WidgetWheelEvent* ev = mEvent->AsWheelEvent(); + // XXX Unclear what scroll amount we should use for deltaZ... + auto amount = std::max(ev->mScrollAmount.width, ev->mScrollAmount.height); + return ToWebExposedDelta(*ev, ev->mDeltaZ, amount, aCallerType); +} + +uint32_t WheelEvent::DeltaMode(CallerType aCallerType) { + using DeltaModeCheckingState = WidgetWheelEvent::DeltaModeCheckingState; + + WidgetWheelEvent* ev = mEvent->AsWheelEvent(); + uint32_t mode = ev->mDeltaMode; + if (aCallerType != CallerType::System) { + if (ev->mDeltaModeCheckingState == DeltaModeCheckingState::Unknown) { + ev->mDeltaModeCheckingState = DeltaModeCheckingState::Checked; + } else if (ev->mDeltaModeCheckingState == + DeltaModeCheckingState::Unchecked && + mode == WheelEvent_Binding::DOM_DELTA_LINE) { + return WheelEvent_Binding::DOM_DELTA_PIXEL; + } + } + + return mode; +} + +already_AddRefed<WheelEvent> WheelEvent::Constructor( + const GlobalObject& aGlobal, const nsAString& aType, + const WheelEventInit& aParam) { + nsCOMPtr<EventTarget> t = do_QueryInterface(aGlobal.GetAsSupports()); + RefPtr<WheelEvent> e = new WheelEvent(t, nullptr, nullptr); + bool trusted = e->Init(t); + e->InitWheelEvent(aType, aParam.mBubbles, aParam.mCancelable, aParam.mView, + aParam.mDetail, aParam.mScreenX, aParam.mScreenY, + aParam.mClientX, aParam.mClientY, aParam.mButton, + aParam.mRelatedTarget, u""_ns, aParam.mDeltaX, + aParam.mDeltaY, aParam.mDeltaZ, aParam.mDeltaMode); + e->InitializeExtraMouseEventDictionaryMembers(aParam); + e->SetTrusted(trusted); + e->SetComposed(aParam.mComposed); + return e.forget(); +} + +} // namespace mozilla::dom + +using namespace mozilla; +using namespace mozilla::dom; + +already_AddRefed<WheelEvent> NS_NewDOMWheelEvent(EventTarget* aOwner, + nsPresContext* aPresContext, + WidgetWheelEvent* aEvent) { + RefPtr<WheelEvent> it = new WheelEvent(aOwner, aPresContext, aEvent); + return it.forget(); +} diff --git a/dom/events/WheelEvent.h b/dom/events/WheelEvent.h new file mode 100644 index 0000000000..c69764bd57 --- /dev/null +++ b/dom/events/WheelEvent.h @@ -0,0 +1,82 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#ifndef mozilla_dom_WheelEvent_h_ +#define mozilla_dom_WheelEvent_h_ + +#include "mozilla/dom/MouseEvent.h" +#include "mozilla/dom/WheelEventBinding.h" +#include "mozilla/EventForwards.h" + +namespace mozilla::dom { + +class WheelEvent : public MouseEvent { + public: + WheelEvent(EventTarget* aOwner, nsPresContext* aPresContext, + WidgetWheelEvent* aWheelEvent); + + NS_INLINE_DECL_REFCOUNTING_INHERITED(WheelEvent, MouseEvent) + + static already_AddRefed<WheelEvent> Constructor(const GlobalObject& aGlobal, + const nsAString& aType, + const WheelEventInit& aParam); + + virtual JSObject* WrapObjectInternal( + JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override { + return WheelEvent_Binding::Wrap(aCx, this, aGivenProto); + } + + double DevToCssPixels(double aDevPxValue) const { + if (!mAppUnitsPerDevPixel) { + return aDevPxValue; + } + return aDevPxValue * mAppUnitsPerDevPixel / AppUnitsPerCSSPixel(); + } + + // NOTE: DeltaX(), DeltaY() and DeltaZ() return CSS pixels when deltaMode is + // DOM_DELTA_PIXEL. (The internal event's delta values are device pixels + // if it's dispatched by widget) + double DeltaX(CallerType); + double DeltaY(CallerType); + double DeltaZ(CallerType); + uint32_t DeltaMode(CallerType); + + int32_t WheelDelta(CallerType aCallerType) { + int32_t y = WheelDeltaY(aCallerType); + return y ? y : WheelDeltaX(aCallerType); + } + + static constexpr int32_t kNativeTicksToWheelDelta = 120; + static constexpr double kTrustedDeltaToWheelDelta = 3.0; + + int32_t WheelDeltaX(CallerType); + int32_t WheelDeltaY(CallerType); + + void InitWheelEvent(const nsAString& aType, bool aCanBubble, bool aCancelable, + nsGlobalWindowInner* aView, int32_t aDetail, + int32_t aScreenX, int32_t aScreenY, int32_t aClientX, + int32_t aClientY, uint16_t aButton, + EventTarget* aRelatedTarget, + const nsAString& aModifiersList, double aDeltaX, + double aDeltaY, double aDeltaZ, uint32_t aDeltaMode); + + protected: + ~WheelEvent() = default; + + double ToWebExposedDelta(WidgetWheelEvent&, double aDelta, + nscoord aLineOrPageAmount, CallerType); + + private: + int32_t mAppUnitsPerDevPixel; +}; + +} // namespace mozilla::dom + +already_AddRefed<mozilla::dom::WheelEvent> NS_NewDOMWheelEvent( + mozilla::dom::EventTarget* aOwner, nsPresContext* aPresContext, + mozilla::WidgetWheelEvent* aEvent); + +#endif // mozilla_dom_WheelEvent_h_ diff --git a/dom/events/WheelHandlingHelper.cpp b/dom/events/WheelHandlingHelper.cpp new file mode 100644 index 0000000000..3b5653c50e --- /dev/null +++ b/dom/events/WheelHandlingHelper.cpp @@ -0,0 +1,872 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#include "WheelHandlingHelper.h" + +#include <utility> // for std::swap + +#include "mozilla/EventDispatcher.h" +#include "mozilla/EventStateManager.h" +#include "mozilla/MouseEvents.h" +#include "mozilla/Preferences.h" +#include "mozilla/PresShell.h" +#include "mozilla/StaticPrefs_mousewheel.h" +#include "mozilla/StaticPrefs_test.h" +#include "mozilla/TextControlElement.h" +#include "mozilla/dom/WheelEventBinding.h" +#include "nsCOMPtr.h" +#include "nsContentUtils.h" +#include "nsIContent.h" +#include "nsIContentInlines.h" +#include "mozilla/dom/Document.h" +#include "DocumentInlines.h" // for Document and HTMLBodyElement +#include "nsIScrollableFrame.h" +#include "nsITimer.h" +#include "nsPresContext.h" +#include "prtime.h" +#include "Units.h" +#include "ScrollAnimationPhysics.h" + +namespace mozilla { + +/******************************************************************/ +/* mozilla::DeltaValues */ +/******************************************************************/ + +DeltaValues::DeltaValues(WidgetWheelEvent* aEvent) + : deltaX(aEvent->mDeltaX), deltaY(aEvent->mDeltaY) {} + +/******************************************************************/ +/* mozilla::WheelHandlingUtils */ +/******************************************************************/ + +/* static */ +bool WheelHandlingUtils::CanScrollInRange(nscoord aMin, nscoord aValue, + nscoord aMax, double aDirection) { + return aDirection > 0.0 ? aValue < static_cast<double>(aMax) + : static_cast<double>(aMin) < aValue; +} + +/* static */ +bool WheelHandlingUtils::CanScrollOn(nsIFrame* aFrame, double aDirectionX, + double aDirectionY) { + nsIScrollableFrame* scrollableFrame = do_QueryFrame(aFrame); + if (!scrollableFrame) { + return false; + } + return CanScrollOn(scrollableFrame, aDirectionX, aDirectionY); +} + +/* static */ +bool WheelHandlingUtils::CanScrollOn(nsIScrollableFrame* aScrollFrame, + double aDirectionX, double aDirectionY) { + MOZ_ASSERT(aScrollFrame); + NS_ASSERTION(aDirectionX || aDirectionY, + "One of the delta values must be non-zero at least"); + + nsPoint scrollPt = aScrollFrame->GetVisualViewportOffset(); + nsRect scrollRange = aScrollFrame->GetScrollRangeForUserInputEvents(); + layers::ScrollDirections directions = + aScrollFrame->GetAvailableScrollingDirectionsForUserInputEvents(); + + return ((aDirectionX != 0.0) && + (directions.contains(layers::ScrollDirection::eHorizontal)) && + CanScrollInRange(scrollRange.x, scrollPt.x, scrollRange.XMost(), + aDirectionX)) || + ((aDirectionY != 0.0) && + (directions.contains(layers::ScrollDirection::eVertical)) && + CanScrollInRange(scrollRange.y, scrollPt.y, scrollRange.YMost(), + aDirectionY)); +} + +/*static*/ Maybe<layers::ScrollDirection> +WheelHandlingUtils::GetDisregardedWheelScrollDirection(const nsIFrame* aFrame) { + nsIContent* content = aFrame->GetContent(); + if (!content) { + return Nothing(); + } + TextControlElement* textControlElement = TextControlElement::FromNodeOrNull( + content->IsInNativeAnonymousSubtree() + ? content->GetClosestNativeAnonymousSubtreeRootParentOrHost() + : content); + if (!textControlElement || !textControlElement->IsSingleLineTextControl()) { + return Nothing(); + } + // Disregard scroll in the block-flow direction by mouse wheel on a + // single-line text control. For instance, in tranditional Chinese writing + // system, a single-line text control cannot be scrolled horizontally with + // mouse wheel even if they overflow at the right and left edges; Whereas in + // latin-based writing system, a single-line text control cannot be scrolled + // vertically with mouse wheel even if they overflow at the top and bottom + // edges + return Some(aFrame->GetWritingMode().IsVertical() + ? layers::ScrollDirection::eHorizontal + : layers::ScrollDirection::eVertical); +} + +/******************************************************************/ +/* mozilla::WheelTransaction */ +/******************************************************************/ + +AutoWeakFrame WheelTransaction::sScrollTargetFrame(nullptr); +AutoWeakFrame WheelTransaction::sEventTargetFrame(nullptr); +bool WheelTransaction::sHandledByApz(false); +uint32_t WheelTransaction::sTime = 0; +uint32_t WheelTransaction::sMouseMoved = 0; +nsITimer* WheelTransaction::sTimer = nullptr; +int32_t WheelTransaction::sScrollSeriesCounter = 0; +bool WheelTransaction::sOwnScrollbars = false; + +/* static */ +bool WheelTransaction::OutOfTime(uint32_t aBaseTime, uint32_t aThreshold) { + uint32_t now = PR_IntervalToMilliseconds(PR_IntervalNow()); + return (now - aBaseTime > aThreshold); +} + +/* static */ +void WheelTransaction::OwnScrollbars(bool aOwn) { sOwnScrollbars = aOwn; } + +/* static */ +void WheelTransaction::BeginTransaction(nsIFrame* aScrollTargetFrame, + nsIFrame* aEventTargetFrame, + const WidgetWheelEvent* aEvent) { + NS_ASSERTION(!sScrollTargetFrame && !sEventTargetFrame, + "previous transaction is not finished!"); + MOZ_ASSERT(aEvent->mMessage == eWheel, + "Transaction must be started with a wheel event"); + + ScrollbarsForWheel::OwnWheelTransaction(false); + sScrollTargetFrame = aScrollTargetFrame; + + // Only set the static event target if wheel event groups are enabled. + if (StaticPrefs::dom_event_wheel_event_groups_enabled()) { + // Set a static event target for the wheel transaction. This will be used + // to override the event target frame when computing the event target from + // input coordinates. When this preference is not set or there is no stored + // event target for the current wheel transaction, the event target will + // not be overridden by the current wheel transaction, but will be computed + // from the input coordinates. + sEventTargetFrame = aEventTargetFrame; + // If the wheel events will be handled by APZ, set a flag here. We can use + // this later to determine if we need to scroll snap at the end of the + // wheel operation. + sHandledByApz = aEvent->mFlags.mHandledByAPZ; + } + + sScrollSeriesCounter = 0; + if (!UpdateTransaction(aEvent)) { + NS_ERROR("BeginTransaction is called even cannot scroll the frame"); + EndTransaction(); + } +} + +/* static */ +bool WheelTransaction::UpdateTransaction(const WidgetWheelEvent* aEvent) { + nsIFrame* scrollToFrame = GetScrollTargetFrame(); + nsIScrollableFrame* scrollableFrame = scrollToFrame->GetScrollTargetFrame(); + if (scrollableFrame) { + scrollToFrame = do_QueryFrame(scrollableFrame); + } + + if (!WheelHandlingUtils::CanScrollOn(scrollToFrame, aEvent->mDeltaX, + aEvent->mDeltaY)) { + OnFailToScrollTarget(); + // We should not modify the transaction state when the view will not be + // scrolled actually. + return false; + } + + SetTimeout(); + + if (sScrollSeriesCounter != 0 && + OutOfTime(sTime, StaticPrefs::mousewheel_scroll_series_timeout())) { + sScrollSeriesCounter = 0; + } + sScrollSeriesCounter++; + + // We should use current time instead of WidgetEvent.time. + // 1. Some events doesn't have the correct creation time. + // 2. If the computer runs slowly by other processes eating the CPU resource, + // the event creation time doesn't keep real time. + sTime = PR_IntervalToMilliseconds(PR_IntervalNow()); + sMouseMoved = 0; + return true; +} + +/* static */ +void WheelTransaction::MayEndTransaction() { + if (!sOwnScrollbars && ScrollbarsForWheel::IsActive()) { + ScrollbarsForWheel::OwnWheelTransaction(true); + } else { + EndTransaction(); + } +} + +/* static */ +void WheelTransaction::EndTransaction() { + if (sTimer) { + sTimer->Cancel(); + } + sScrollTargetFrame = nullptr; + sEventTargetFrame = nullptr; + sScrollSeriesCounter = 0; + sHandledByApz = false; + if (sOwnScrollbars) { + sOwnScrollbars = false; + ScrollbarsForWheel::OwnWheelTransaction(false); + ScrollbarsForWheel::Inactivate(); + } +} + +/* static */ +bool WheelTransaction::WillHandleDefaultAction( + WidgetWheelEvent* aWheelEvent, AutoWeakFrame& aScrollTargetWeakFrame, + AutoWeakFrame& aEventTargetWeakFrame) { + nsIFrame* lastTargetFrame = GetScrollTargetFrame(); + if (!lastTargetFrame) { + BeginTransaction(aScrollTargetWeakFrame.GetFrame(), + aEventTargetWeakFrame.GetFrame(), aWheelEvent); + } else if (lastTargetFrame != aScrollTargetWeakFrame.GetFrame()) { + EndTransaction(); + BeginTransaction(aScrollTargetWeakFrame.GetFrame(), + aEventTargetWeakFrame.GetFrame(), aWheelEvent); + } else { + UpdateTransaction(aWheelEvent); + } + + // When the wheel event will not be handled with any frames, + // UpdateTransaction() fires MozMouseScrollFailed event which is for + // automated testing. In the event handler, the target frame might be + // destroyed. Then, the caller shouldn't try to handle the default action. + if (!aScrollTargetWeakFrame.IsAlive()) { + EndTransaction(); + return false; + } + + return true; +} + +/* static */ +void WheelTransaction::OnEvent(WidgetEvent* aEvent) { + if (!sScrollTargetFrame) { + return; + } + + if (OutOfTime(sTime, StaticPrefs::mousewheel_transaction_timeout())) { + // Even if the scroll event which is handled after timeout, but onTimeout + // was not fired by timer, then the scroll event will scroll old frame, + // therefore, we should call OnTimeout here and ensure to finish the old + // transaction. + OnTimeout(nullptr, nullptr); + return; + } + + switch (aEvent->mMessage) { + case eWheel: + if (sMouseMoved != 0 && + OutOfTime(sMouseMoved, + StaticPrefs::mousewheel_transaction_ignoremovedelay())) { + // Terminate the current mousewheel transaction if the mouse moved more + // than ignoremovedelay milliseconds ago + EndTransaction(); + } + return; + case eMouseMove: + case eDragOver: { + WidgetMouseEvent* mouseEvent = aEvent->AsMouseEvent(); + if (mouseEvent->IsReal()) { + // If the cursor is moving to be outside the frame, + // terminate the scrollwheel transaction. + LayoutDeviceIntPoint pt = GetScreenPoint(mouseEvent); + auto r = LayoutDeviceIntRect::FromAppUnitsToNearest( + sScrollTargetFrame->GetScreenRectInAppUnits(), + sScrollTargetFrame->PresContext()->AppUnitsPerDevPixel()); + if (!r.Contains(pt)) { + EndTransaction(); + return; + } + + // For mouse move events where the wheel transaction is still valid, the + // stored event target should be reset. + sEventTargetFrame = nullptr; + + // If the cursor is moving inside the frame, and it is less than + // ignoremovedelay milliseconds since the last scroll operation, ignore + // the mouse move; otherwise, record the current mouse move time to be + // checked later + if (!sMouseMoved && + OutOfTime(sTime, + StaticPrefs::mousewheel_transaction_ignoremovedelay())) { + sMouseMoved = PR_IntervalToMilliseconds(PR_IntervalNow()); + } + } + return; + } + case eKeyPress: + case eKeyUp: + case eKeyDown: + case eMouseUp: + case eMouseDown: + case eMouseDoubleClick: + case eMouseAuxClick: + case eMouseClick: + case eContextMenu: + case eDrop: + EndTransaction(); + return; + default: + break; + } +} + +/* static */ +void WheelTransaction::OnRemoveElement(nsIContent* aContent) { + // If dom.event.wheel-event-groups.enabled is not set or we have no current + // wheel event transaction there is no internal state to be updated. + if (!sEventTargetFrame) { + return; + } + + if (sEventTargetFrame->GetContent() == aContent) { + // Only invalidate the wheel transaction event target frame when the + // remove target is the event target of the wheel event group. The + // scroll target frame of the wheel event group may still be valid. + // + // With the stored event target unset, the target for any following + // events will be the frame found using the input coordinates. + sEventTargetFrame = nullptr; + } +} + +/* static */ +void WheelTransaction::Shutdown() { NS_IF_RELEASE(sTimer); } + +/* static */ +void WheelTransaction::OnFailToScrollTarget() { + MOZ_ASSERT(sScrollTargetFrame, "We don't have mouse scrolling transaction"); + + if (StaticPrefs::test_mousescroll()) { + // This event is used for automated tests, see bug 442774. + nsContentUtils::DispatchEventOnlyToChrome( + sScrollTargetFrame->GetContent()->OwnerDoc(), + sScrollTargetFrame->GetContent(), u"MozMouseScrollFailed"_ns, + CanBubble::eYes, Cancelable::eYes); + } + // The target frame might be destroyed in the event handler, at that time, + // we need to finish the current transaction + if (!sScrollTargetFrame) { + EndTransaction(); + } +} + +/* static */ +void WheelTransaction::OnTimeout(nsITimer* aTimer, void* aClosure) { + if (!sScrollTargetFrame) { + // The transaction target was destroyed already + EndTransaction(); + return; + } + // Store the sScrollTargetFrame, the variable becomes null in EndTransaction. + nsIFrame* frame = sScrollTargetFrame; + // We need to finish current transaction before DOM event firing. Because + // the next DOM event might create strange situation for us. + MayEndTransaction(); + + if (StaticPrefs::test_mousescroll()) { + // This event is used for automated tests, see bug 442774. + nsContentUtils::DispatchEventOnlyToChrome( + frame->GetContent()->OwnerDoc(), frame->GetContent(), + u"MozMouseScrollTransactionTimeout"_ns, CanBubble::eYes, + Cancelable::eYes); + } +} + +/* static */ +void WheelTransaction::SetTimeout() { + if (!sTimer) { + sTimer = NS_NewTimer().take(); + if (!sTimer) { + return; + } + } + sTimer->Cancel(); + DebugOnly<nsresult> rv = sTimer->InitWithNamedFuncCallback( + OnTimeout, nullptr, StaticPrefs::mousewheel_transaction_timeout(), + nsITimer::TYPE_ONE_SHOT, "WheelTransaction::SetTimeout"); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "nsITimer::InitWithFuncCallback failed"); +} + +/* static */ +LayoutDeviceIntPoint WheelTransaction::GetScreenPoint(WidgetGUIEvent* aEvent) { + NS_ASSERTION(aEvent, "aEvent is null"); + NS_ASSERTION(aEvent->mWidget, "aEvent-mWidget is null"); + return aEvent->mRefPoint + aEvent->mWidget->WidgetToScreenOffset(); +} + +/* static */ +DeltaValues WheelTransaction::AccelerateWheelDelta(WidgetWheelEvent* aEvent) { + DeltaValues result = OverrideSystemScrollSpeed(aEvent); + + // Don't accelerate the delta values if the event isn't line scrolling. + if (aEvent->mDeltaMode != dom::WheelEvent_Binding::DOM_DELTA_LINE) { + return result; + } + + // Accelerate by the sScrollSeriesCounter + int32_t start = StaticPrefs::mousewheel_acceleration_start(); + if (start >= 0 && sScrollSeriesCounter >= start) { + int32_t factor = StaticPrefs::mousewheel_acceleration_factor(); + if (factor > 0) { + result.deltaX = ComputeAcceleratedWheelDelta(result.deltaX, factor); + result.deltaY = ComputeAcceleratedWheelDelta(result.deltaY, factor); + } + } + + return result; +} + +/* static */ +double WheelTransaction::ComputeAcceleratedWheelDelta(double aDelta, + int32_t aFactor) { + return mozilla::ComputeAcceleratedWheelDelta(aDelta, sScrollSeriesCounter, + aFactor); +} + +/* static */ +DeltaValues WheelTransaction::OverrideSystemScrollSpeed( + WidgetWheelEvent* aEvent) { + MOZ_ASSERT(sScrollTargetFrame, "We don't have mouse scrolling transaction"); + + // If the event doesn't scroll to both X and Y, we don't need to do anything + // here. + if (!aEvent->mDeltaX && !aEvent->mDeltaY) { + return DeltaValues(aEvent); + } + + return DeltaValues(aEvent->OverriddenDeltaX(), aEvent->OverriddenDeltaY()); +} + +/******************************************************************/ +/* mozilla::ScrollbarsForWheel */ +/******************************************************************/ + +const DeltaValues ScrollbarsForWheel::directions[kNumberOfTargets] = { + DeltaValues(-1, 0), DeltaValues(+1, 0), DeltaValues(0, -1), + DeltaValues(0, +1)}; + +AutoWeakFrame ScrollbarsForWheel::sActiveOwner = nullptr; +AutoWeakFrame ScrollbarsForWheel::sActivatedScrollTargets[kNumberOfTargets] = { + nullptr, nullptr, nullptr, nullptr}; + +bool ScrollbarsForWheel::sHadWheelStart = false; +bool ScrollbarsForWheel::sOwnWheelTransaction = false; + +/* static */ +void ScrollbarsForWheel::PrepareToScrollText(EventStateManager* aESM, + nsIFrame* aTargetFrame, + WidgetWheelEvent* aEvent) { + if (aEvent->mMessage == eWheelOperationStart) { + WheelTransaction::OwnScrollbars(false); + if (!IsActive()) { + TemporarilyActivateAllPossibleScrollTargets(aESM, aTargetFrame, aEvent); + sHadWheelStart = true; + } + } else { + DeactivateAllTemporarilyActivatedScrollTargets(); + } +} + +/* static */ +void ScrollbarsForWheel::SetActiveScrollTarget( + nsIScrollableFrame* aScrollTarget) { + if (!sHadWheelStart) { + return; + } + nsIScrollbarMediator* scrollbarMediator = do_QueryFrame(aScrollTarget); + if (!scrollbarMediator) { + return; + } + sHadWheelStart = false; + sActiveOwner = do_QueryFrame(aScrollTarget); + scrollbarMediator->ScrollbarActivityStarted(); +} + +/* static */ +void ScrollbarsForWheel::MayInactivate() { + if (!sOwnWheelTransaction && WheelTransaction::GetScrollTargetFrame()) { + WheelTransaction::OwnScrollbars(true); + } else { + Inactivate(); + } +} + +/* static */ +void ScrollbarsForWheel::Inactivate() { + nsIScrollbarMediator* scrollbarMediator = do_QueryFrame(sActiveOwner); + if (scrollbarMediator) { + scrollbarMediator->ScrollbarActivityStopped(); + } + sActiveOwner = nullptr; + DeactivateAllTemporarilyActivatedScrollTargets(); + if (sOwnWheelTransaction) { + sOwnWheelTransaction = false; + WheelTransaction::OwnScrollbars(false); + WheelTransaction::EndTransaction(); + } +} + +/* static */ +bool ScrollbarsForWheel::IsActive() { + if (sActiveOwner) { + return true; + } + for (size_t i = 0; i < kNumberOfTargets; ++i) { + if (sActivatedScrollTargets[i]) { + return true; + } + } + return false; +} + +/* static */ +void ScrollbarsForWheel::OwnWheelTransaction(bool aOwn) { + sOwnWheelTransaction = aOwn; +} + +/* static */ +void ScrollbarsForWheel::TemporarilyActivateAllPossibleScrollTargets( + EventStateManager* aESM, nsIFrame* aTargetFrame, WidgetWheelEvent* aEvent) { + for (size_t i = 0; i < kNumberOfTargets; i++) { + const DeltaValues* dir = &directions[i]; + AutoWeakFrame* scrollTarget = &sActivatedScrollTargets[i]; + MOZ_ASSERT(!*scrollTarget, "scroll target still temporarily activated!"); + nsIScrollableFrame* target = do_QueryFrame(aESM->ComputeScrollTarget( + aTargetFrame, dir->deltaX, dir->deltaY, aEvent, + EventStateManager::COMPUTE_DEFAULT_ACTION_TARGET)); + nsIScrollbarMediator* scrollbarMediator = do_QueryFrame(target); + if (scrollbarMediator) { + nsIFrame* targetFrame = do_QueryFrame(target); + *scrollTarget = targetFrame; + scrollbarMediator->ScrollbarActivityStarted(); + } + } +} + +/* static */ +void ScrollbarsForWheel::DeactivateAllTemporarilyActivatedScrollTargets() { + for (size_t i = 0; i < kNumberOfTargets; i++) { + AutoWeakFrame* scrollTarget = &sActivatedScrollTargets[i]; + if (*scrollTarget) { + nsIScrollbarMediator* scrollbarMediator = do_QueryFrame(*scrollTarget); + if (scrollbarMediator) { + scrollbarMediator->ScrollbarActivityStopped(); + } + *scrollTarget = nullptr; + } + } +} + +/******************************************************************/ +/* mozilla::WheelDeltaHorizontalizer */ +/******************************************************************/ + +void WheelDeltaHorizontalizer::Horizontalize() { + MOZ_ASSERT(!mWheelEvent.mDeltaValuesHorizontalizedForDefaultHandler, + "Wheel delta values in one wheel scroll event are being adjusted " + "a second time"); + + // Log the old values. + mOldDeltaX = mWheelEvent.mDeltaX; + mOldDeltaZ = mWheelEvent.mDeltaZ; + mOldOverflowDeltaX = mWheelEvent.mOverflowDeltaX; + mOldLineOrPageDeltaX = mWheelEvent.mLineOrPageDeltaX; + + // Move deltaY values to deltaX and set both deltaY and deltaZ to 0. + mWheelEvent.mDeltaX = mWheelEvent.mDeltaY; + mWheelEvent.mDeltaY = 0.0; + mWheelEvent.mDeltaZ = 0.0; + mWheelEvent.mOverflowDeltaX = mWheelEvent.mOverflowDeltaY; + mWheelEvent.mOverflowDeltaY = 0.0; + mWheelEvent.mLineOrPageDeltaX = mWheelEvent.mLineOrPageDeltaY; + mWheelEvent.mLineOrPageDeltaY = 0; + + // Mark it horizontalized in order to restore the delta values when this + // instance is being destroyed. + mWheelEvent.mDeltaValuesHorizontalizedForDefaultHandler = true; + mHorizontalized = true; +} + +void WheelDeltaHorizontalizer::CancelHorizontalization() { + // Restore the horizontalized delta. + if (mHorizontalized && + mWheelEvent.mDeltaValuesHorizontalizedForDefaultHandler) { + mWheelEvent.mDeltaY = mWheelEvent.mDeltaX; + mWheelEvent.mDeltaX = mOldDeltaX; + mWheelEvent.mDeltaZ = mOldDeltaZ; + mWheelEvent.mOverflowDeltaY = mWheelEvent.mOverflowDeltaX; + mWheelEvent.mOverflowDeltaX = mOldOverflowDeltaX; + mWheelEvent.mLineOrPageDeltaY = mWheelEvent.mLineOrPageDeltaX; + mWheelEvent.mLineOrPageDeltaX = mOldLineOrPageDeltaX; + mWheelEvent.mDeltaValuesHorizontalizedForDefaultHandler = false; + mHorizontalized = false; + } +} + +WheelDeltaHorizontalizer::~WheelDeltaHorizontalizer() { + CancelHorizontalization(); +} + +/******************************************************************/ +/* mozilla::AutoDirWheelDeltaAdjuster */ +/******************************************************************/ + +bool AutoDirWheelDeltaAdjuster::ShouldBeAdjusted() { + // Sometimes, this function can be called more than one time. If we have + // already checked if the scroll should be adjusted, there's no need to check + // it again. + if (mCheckedIfShouldBeAdjusted) { + return mShouldBeAdjusted; + } + mCheckedIfShouldBeAdjusted = true; + + // For an auto-dir wheel scroll, if all the following conditions are met, we + // should adjust X and Y values: + // 1. There is only one non-zero value between DeltaX and DeltaY. + // 2. There is only one direction for the target that overflows and is + // scrollable with wheel. + // 3. The direction described in Condition 1 is orthogonal to the one + // described in Condition 2. + if ((mDeltaX && mDeltaY) || (!mDeltaX && !mDeltaY)) { + return false; + } + if (mDeltaX) { + if (CanScrollAlongXAxis()) { + return false; + } + if (IsHorizontalContentRightToLeft()) { + mShouldBeAdjusted = + mDeltaX > 0 ? CanScrollUpwards() : CanScrollDownwards(); + } else { + mShouldBeAdjusted = + mDeltaX < 0 ? CanScrollUpwards() : CanScrollDownwards(); + } + return mShouldBeAdjusted; + } + MOZ_ASSERT(0 != mDeltaY); + if (CanScrollAlongYAxis()) { + return false; + } + if (IsHorizontalContentRightToLeft()) { + mShouldBeAdjusted = + mDeltaY > 0 ? CanScrollLeftwards() : CanScrollRightwards(); + } else { + mShouldBeAdjusted = + mDeltaY < 0 ? CanScrollLeftwards() : CanScrollRightwards(); + } + return mShouldBeAdjusted; +} + +void AutoDirWheelDeltaAdjuster::Adjust() { + if (!ShouldBeAdjusted()) { + return; + } + std::swap(mDeltaX, mDeltaY); + if (IsHorizontalContentRightToLeft()) { + mDeltaX *= -1; + mDeltaY *= -1; + } + mShouldBeAdjusted = false; + OnAdjusted(); +} + +/******************************************************************/ +/* mozilla::ESMAutoDirWheelDeltaAdjuster */ +/******************************************************************/ + +ESMAutoDirWheelDeltaAdjuster::ESMAutoDirWheelDeltaAdjuster( + WidgetWheelEvent& aEvent, nsIFrame& aScrollFrame, bool aHonoursRoot) + : AutoDirWheelDeltaAdjuster(aEvent.mDeltaX, aEvent.mDeltaY), + mLineOrPageDeltaX(aEvent.mLineOrPageDeltaX), + mLineOrPageDeltaY(aEvent.mLineOrPageDeltaY), + mOverflowDeltaX(aEvent.mOverflowDeltaX), + mOverflowDeltaY(aEvent.mOverflowDeltaY) { + mScrollTargetFrame = aScrollFrame.GetScrollTargetFrame(); + MOZ_ASSERT(mScrollTargetFrame); + + nsIFrame* honouredFrame = nullptr; + if (aHonoursRoot) { + // If we are going to honour root, first try to get the frame for <body> as + // the honoured root, because <body> is in preference to <html> if the + // current document is an HTML document. + dom::Document* document = aScrollFrame.PresShell()->GetDocument(); + if (document) { + dom::Element* bodyElement = document->GetBodyElement(); + if (bodyElement) { + honouredFrame = bodyElement->GetPrimaryFrame(); + } + } + + if (!honouredFrame) { + // If there is no <body> frame, fall back to the real root frame. + honouredFrame = aScrollFrame.PresShell()->GetRootScrollFrame(); + } + + if (!honouredFrame) { + // If there is no root scroll frame, fall back to the current scrolling + // frame. + honouredFrame = &aScrollFrame; + } + } else { + honouredFrame = &aScrollFrame; + } + + WritingMode writingMode = honouredFrame->GetWritingMode(); + WritingMode::BlockDir blockDir = writingMode.GetBlockDir(); + WritingMode::InlineDir inlineDir = writingMode.GetInlineDir(); + // Get whether the honoured frame's content in the horizontal direction starts + // from right to left(E.g. it's true either if "writing-mode: vertical-rl", or + // if "writing-mode: horizontal-tb; direction: rtl;" in CSS). + mIsHorizontalContentRightToLeft = + (blockDir == WritingMode::BlockDir::eBlockRL || + (blockDir == WritingMode::BlockDir::eBlockTB && + inlineDir == WritingMode::InlineDir::eInlineRTL)); +} + +void ESMAutoDirWheelDeltaAdjuster::OnAdjusted() { + // Adjust() only adjusted basic deltaX and deltaY, which are not enough for + // ESM, we should continue to adjust line-or-page and overflow values. + if (mDeltaX) { + // A vertical scroll was adjusted to be horizontal. + MOZ_ASSERT(0 == mDeltaY); + + mLineOrPageDeltaX = mLineOrPageDeltaY; + mLineOrPageDeltaY = 0; + mOverflowDeltaX = mOverflowDeltaY; + mOverflowDeltaY = 0; + } else { + // A horizontal scroll was adjusted to be vertical. + MOZ_ASSERT(0 != mDeltaY); + + mLineOrPageDeltaY = mLineOrPageDeltaX; + mLineOrPageDeltaX = 0; + mOverflowDeltaY = mOverflowDeltaX; + mOverflowDeltaX = 0; + } + if (mIsHorizontalContentRightToLeft) { + // If in RTL writing mode, reverse the side the scroll will go towards. + mLineOrPageDeltaX *= -1; + mLineOrPageDeltaY *= -1; + mOverflowDeltaX *= -1; + mOverflowDeltaY *= -1; + } +} + +bool ESMAutoDirWheelDeltaAdjuster::CanScrollAlongXAxis() const { + return mScrollTargetFrame->GetAvailableScrollingDirections().contains( + layers::ScrollDirection::eHorizontal); +} + +bool ESMAutoDirWheelDeltaAdjuster::CanScrollAlongYAxis() const { + return mScrollTargetFrame->GetAvailableScrollingDirections().contains( + layers::ScrollDirection::eVertical); +} + +bool ESMAutoDirWheelDeltaAdjuster::CanScrollUpwards() const { + nsPoint scrollPt = mScrollTargetFrame->GetScrollPosition(); + nsRect scrollRange = mScrollTargetFrame->GetScrollRange(); + return static_cast<double>(scrollRange.y) < scrollPt.y; +} + +bool ESMAutoDirWheelDeltaAdjuster::CanScrollDownwards() const { + nsPoint scrollPt = mScrollTargetFrame->GetScrollPosition(); + nsRect scrollRange = mScrollTargetFrame->GetScrollRange(); + return static_cast<double>(scrollRange.YMost()) > scrollPt.y; +} + +bool ESMAutoDirWheelDeltaAdjuster::CanScrollLeftwards() const { + nsPoint scrollPt = mScrollTargetFrame->GetScrollPosition(); + nsRect scrollRange = mScrollTargetFrame->GetScrollRange(); + return static_cast<double>(scrollRange.x) < scrollPt.x; +} + +bool ESMAutoDirWheelDeltaAdjuster::CanScrollRightwards() const { + nsPoint scrollPt = mScrollTargetFrame->GetScrollPosition(); + nsRect scrollRange = mScrollTargetFrame->GetScrollRange(); + return static_cast<double>(scrollRange.XMost()) > scrollPt.x; +} + +bool ESMAutoDirWheelDeltaAdjuster::IsHorizontalContentRightToLeft() const { + return mIsHorizontalContentRightToLeft; +} + +/******************************************************************/ +/* mozilla::ESMAutoDirWheelDeltaRestorer */ +/******************************************************************/ + +/*explicit*/ +ESMAutoDirWheelDeltaRestorer::ESMAutoDirWheelDeltaRestorer( + WidgetWheelEvent& aEvent) + : mEvent(aEvent), + mOldDeltaX(aEvent.mDeltaX), + mOldDeltaY(aEvent.mDeltaY), + mOldLineOrPageDeltaX(aEvent.mLineOrPageDeltaX), + mOldLineOrPageDeltaY(aEvent.mLineOrPageDeltaY), + mOldOverflowDeltaX(aEvent.mOverflowDeltaX), + mOldOverflowDeltaY(aEvent.mOverflowDeltaY) {} + +ESMAutoDirWheelDeltaRestorer::~ESMAutoDirWheelDeltaRestorer() { + if (mOldDeltaX == mEvent.mDeltaX || mOldDeltaY == mEvent.mDeltaY) { + // The delta of the event wasn't adjusted during the lifetime of this + // |ESMAutoDirWheelDeltaRestorer| instance. No need to restore it. + return; + } + + bool forRTL = false; + + // First, restore the basic deltaX and deltaY. + std::swap(mEvent.mDeltaX, mEvent.mDeltaY); + if (mOldDeltaX != mEvent.mDeltaX || mOldDeltaY != mEvent.mDeltaY) { + // If X and Y still don't equal to their original values after being + // swapped, then it must be because they were adjusted for RTL. + forRTL = true; + mEvent.mDeltaX *= -1; + mEvent.mDeltaY *= -1; + MOZ_ASSERT(mOldDeltaX == mEvent.mDeltaX && mOldDeltaY == mEvent.mDeltaY); + } + + if (mEvent.mDeltaX) { + // A horizontal scroll was adjusted to be vertical during the lifetime of + // this instance. + MOZ_ASSERT(0 == mEvent.mDeltaY); + + // Restore the line-or-page and overflow values to be horizontal. + mEvent.mOverflowDeltaX = mEvent.mOverflowDeltaY; + mEvent.mLineOrPageDeltaX = mEvent.mLineOrPageDeltaY; + if (forRTL) { + mEvent.mOverflowDeltaX *= -1; + mEvent.mLineOrPageDeltaX *= -1; + } + mEvent.mOverflowDeltaY = mOldOverflowDeltaY; + mEvent.mLineOrPageDeltaY = mOldLineOrPageDeltaY; + } else { + // A vertical scroll was adjusted to be horizontal during the lifetime of + // this instance. + MOZ_ASSERT(0 != mEvent.mDeltaY); + + // Restore the line-or-page and overflow values to be vertical. + mEvent.mOverflowDeltaY = mEvent.mOverflowDeltaX; + mEvent.mLineOrPageDeltaY = mEvent.mLineOrPageDeltaX; + if (forRTL) { + mEvent.mOverflowDeltaY *= -1; + mEvent.mLineOrPageDeltaY *= -1; + } + mEvent.mOverflowDeltaX = mOldOverflowDeltaX; + mEvent.mLineOrPageDeltaX = mOldLineOrPageDeltaX; + } +} + +} // namespace mozilla diff --git a/dom/events/WheelHandlingHelper.h b/dom/events/WheelHandlingHelper.h new file mode 100644 index 0000000000..d9b73c2ddd --- /dev/null +++ b/dom/events/WheelHandlingHelper.h @@ -0,0 +1,440 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#ifndef mozilla_WheelHandlingHelper_h_ +#define mozilla_WheelHandlingHelper_h_ + +#include "mozilla/Attributes.h" +#include "mozilla/EventForwards.h" +#include "nsCoord.h" +#include "nsIFrame.h" // for AutoWeakFrame only +#include "nsPoint.h" + +class nsIFrame; +class nsIScrollableFrame; +class nsITimer; + +namespace mozilla { + +class EventStateManager; + +/** + * DeltaValues stores two delta values which are along X and Y axis. This is + * useful for arguments and results of some methods. + */ + +struct DeltaValues { + DeltaValues() : deltaX(0.0), deltaY(0.0) {} + + DeltaValues(double aDeltaX, double aDeltaY) + : deltaX(aDeltaX), deltaY(aDeltaY) {} + + explicit DeltaValues(WidgetWheelEvent* aEvent); + + double deltaX; + double deltaY; +}; + +/** + * WheelHandlingUtils provides some static methods which are useful at handling + * wheel events. + */ + +class WheelHandlingUtils { + public: + /** + * Returns true if aFrame is a scrollable frame and it can be scrolled to + * either aDirectionX or aDirectionY along each axis. Or if aFrame is a + * plugin frame (in this case, aDirectionX and aDirectionY are ignored). + * Otherwise, false. + */ + static bool CanScrollOn(nsIFrame* aFrame, double aDirectionX, + double aDirectionY); + /** + * Returns true if the scrollable frame can be scrolled to either aDirectionX + * or aDirectionY along each axis. Otherwise, false. + */ + static bool CanScrollOn(nsIScrollableFrame* aScrollFrame, double aDirectionX, + double aDirectionY); + + // For more details about the concept of a disregarded direction, refer to the + // code in struct mozilla::layers::ScrollMetadata which defines + // mDisregardedDirection. + static Maybe<layers::ScrollDirection> GetDisregardedWheelScrollDirection( + const nsIFrame* aFrame); + + private: + static bool CanScrollInRange(nscoord aMin, nscoord aValue, nscoord aMax, + double aDirection); +}; + +/** + * ScrollbarsForWheel manages scrollbars state during wheel operation. + * E.g., on some platforms, scrollbars should show only while user attempts to + * scroll. At that time, scrollbars which may be possible to scroll by + * operation of wheel at the point should show temporarily. + */ + +class ScrollbarsForWheel { + public: + static void PrepareToScrollText(EventStateManager* aESM, + nsIFrame* aTargetFrame, + WidgetWheelEvent* aEvent); + static void SetActiveScrollTarget(nsIScrollableFrame* aScrollTarget); + // Hide all scrollbars (both mActiveOwner's and mActivatedScrollTargets') + static void MayInactivate(); + static void Inactivate(); + static bool IsActive(); + static void OwnWheelTransaction(bool aOwn); + + protected: + static const size_t kNumberOfTargets = 4; + static const DeltaValues directions[kNumberOfTargets]; + static AutoWeakFrame sActiveOwner; + static AutoWeakFrame sActivatedScrollTargets[kNumberOfTargets]; + static bool sHadWheelStart; + static bool sOwnWheelTransaction; + + /** + * These two methods are called upon eWheelOperationStart/eWheelOperationEnd + * events to show/hide the right scrollbars. + */ + static void TemporarilyActivateAllPossibleScrollTargets( + EventStateManager* aESM, nsIFrame* aTargetFrame, + WidgetWheelEvent* aEvent); + static void DeactivateAllTemporarilyActivatedScrollTargets(); +}; + +/** + * WheelTransaction manages a series of wheel events as a transaction. + * While in a transaction, every wheel event should scroll the same scrollable + * element even if a different scrollable element is under the mouse cursor. + * + * Additionally, this class also manages wheel scroll speed acceleration. + */ + +class WheelTransaction { + public: + /** + * Get the target scroll frame for this wheel transaction. This should + * the the scrollable fame that will scroll for all wheel events in + * this wheel transaction. + */ + static nsIFrame* GetScrollTargetFrame() { return sScrollTargetFrame; } + /* + * The event target to use for all wheel events in this wheel transaction. + * This should be the event target for all wheel events in this wheel + * transaction. Note that this frame will likely be a child of the + * scrollable frame. + */ + static nsIFrame* GetEventTargetFrame() { return sEventTargetFrame; } + static bool HandledByApz() { return sHandledByApz; } + static void EndTransaction(); + /** + * WillHandleDefaultAction() is called before handling aWheelEvent on + * aScrollTargetWeakFrame given the event target aEventTargetWeakFrame. + * + * @return false if the caller cannot continue to handle the default + * action. Otherwise, true. + */ + static bool WillHandleDefaultAction(WidgetWheelEvent* aWheelEvent, + AutoWeakFrame& aScrollTargetWeakFrame, + AutoWeakFrame& aEventTargetWeakFrame); + static bool WillHandleDefaultAction(WidgetWheelEvent* aWheelEvent, + nsIFrame* aScrollTargetFrame, + nsIFrame* aEventTargetFrame) { + AutoWeakFrame scrollTargetWeakFrame(aScrollTargetFrame); + AutoWeakFrame eventTargetWeakFrame(aEventTargetFrame); + return WillHandleDefaultAction(aWheelEvent, scrollTargetWeakFrame, + eventTargetWeakFrame); + } + static void OnEvent(WidgetEvent* aEvent); + static void OnRemoveElement(nsIContent* aContent); + static void Shutdown(); + + static void OwnScrollbars(bool aOwn); + + static DeltaValues AccelerateWheelDelta(WidgetWheelEvent* aEvent); + + protected: + static void BeginTransaction(nsIFrame* aScrollTargetFrame, + nsIFrame* aEventTargetFrame, + const WidgetWheelEvent* aEvent); + // Be careful, UpdateTransaction may fire a DOM event, therefore, the target + // frame might be destroyed in the event handler. + static bool UpdateTransaction(const WidgetWheelEvent* aEvent); + static void MayEndTransaction(); + + static LayoutDeviceIntPoint GetScreenPoint(WidgetGUIEvent* aEvent); + static void OnFailToScrollTarget(); + static void OnTimeout(nsITimer* aTimer, void* aClosure); + static void SetTimeout(); + static DeltaValues OverrideSystemScrollSpeed(WidgetWheelEvent* aEvent); + static double ComputeAcceleratedWheelDelta(double aDelta, int32_t aFactor); + static bool OutOfTime(uint32_t aBaseTime, uint32_t aThreshold); + + /** + * The scrollable element the current wheel event group is bound to. + */ + static AutoWeakFrame sScrollTargetFrame; + /** + * The initial target of the first wheel event in the wheel event group. + * This frame is typically a child of the scrollable element. The wheel + * event should target the topmost-event-target. For a wheel event + * group, we'll use this target for the entire group. + * + * See https://w3c.github.io/uievents/#topmost-event-target and + * https://w3c.github.io/uievents/#event-type-wheel for details. + * + * Note: this is only populated if dom.event.wheel-event-groups.enabled is + * set. + */ + static AutoWeakFrame sEventTargetFrame; + /** + * The wheel events for this transaction are handled by APZ. + */ + static bool sHandledByApz; + static uint32_t sTime; // in milliseconds + static uint32_t sMouseMoved; // in milliseconds + static nsITimer* sTimer; + static int32_t sScrollSeriesCounter; + static bool sOwnScrollbars; +}; + +// For some kinds of scrollings, the delta values of WidgetWheelEvent are +// possbile to be adjusted. For example, the user has configured the pref to let +// [vertical wheel + Shift key] to perform horizontal scrolling instead of +// vertical scrolling. +// The values in this enumeration list all kinds of scrollings whose delta +// values are possible to be adjusted. +enum class WheelDeltaAdjustmentStrategy : uint8_t { + // There is no strategy, don't adjust delta values in any cases. + eNone, + // This strategy means we're receiving a horizontalized scroll, so we should + // apply horizontalization strategy for its delta values. + // Horizontalized scrolling means treating vertical wheel scrolling as + // horizontal scrolling by adjusting delta values. + // It's important to keep in mind with the percise concept of horizontalized + // scrolling: Delta values are *ONLY* going to be adjusted during the process + // of its default action handling; in views of any programmes other than the + // default action handler, such as a DOM event listener or a plugin, delta + // values are never going to be adjusted, they will still retrive original + // delta values when horizontalization occured for default actions. + eHorizontalize, + // The following two strategies mean we're receving an auto-dir scroll, so we + // should apply auto-dir adjustment to the delta of the wheel event if needed. + // Auto-dir is a feature which treats any single-wheel scroll as a scroll in + // the only one scrollable direction if the target has only one scrollable + // direction. For example, if the user scrolls a vertical wheel inside a + // target which is horizontally scrollable but vertical unscrollable, then the + // vertical scroll is converted to a horizontal scroll for that target. + // So why do we need two different strategies for auto-dir scrolling? That's + // because when a wheel scroll is converted due to auto-dir, there is one + // thing called "honoured target" which decides which side the converted + // scroll goes towards. If the content of the honoured target horizontally + // is RTL content, then an upward scroll maps to a rightward scroll and a + // downward scroll maps to a leftward scroll; otherwise, an upward scroll maps + // to a leftward scroll and a downward scroll maps to a rightward scroll. + // |eAutoDir| considers the scrolling target as the honoured target. + // |eAutoDirWithRootHonour| takes the root element of the document with the + // scrolling element, and considers that as the honoured target. But note that + // there's one exception: for targets in an HTML document, the real root + // element(I.e. the <html> element) is typically not considered as a root + // element, but the <body> element is typically considered as a root element. + // If there is no <body> element, then consider the <html> element instead. + // And also note that like |eHorizontalize|, delta values are *ONLY* going to + // be adjusted during the process of its default action handling; in views of + // any programmes other than the default action handler, such as a DOM event + // listener or a plugin, delta values are never going to be adjusted. + eAutoDir, + eAutoDirWithRootHonour, + // Not an actual strategy. This is just used as an upper bound for + // ContiguousEnumSerializer. + eSentinel, +}; + +/** + * When a *pure* vertical wheel event should be treated as if it was a + * horizontal scroll because the user wants to horizontalize the wheel scroll, + * an instance of this class will adjust the delta values upon calling + * Horizontalize(). And the horizontalized delta values will be restored + * automatically when the instance of this class is being destructed. Or you can + * restore them in advance by calling CancelHorizontalization(). + */ +class MOZ_STACK_CLASS WheelDeltaHorizontalizer final { + public: + /** + * @param aWheelEvent A wheel event whose delta values will be adjusted + * upon calling Horizontalize(). + */ + explicit WheelDeltaHorizontalizer(WidgetWheelEvent& aWheelEvent) + : mWheelEvent(aWheelEvent), + mOldDeltaX(0.0), + mOldDeltaZ(0.0), + mOldOverflowDeltaX(0.0), + mOldLineOrPageDeltaX(0), + mHorizontalized(false) {} + /** + * Converts vertical scrolling into horizontal scrolling by adjusting the + * its delta values. + */ + void Horizontalize(); + ~WheelDeltaHorizontalizer(); + void CancelHorizontalization(); + + private: + WidgetWheelEvent& mWheelEvent; + double mOldDeltaX; + double mOldDeltaZ; + double mOldOverflowDeltaX; + int32_t mOldLineOrPageDeltaX; + bool mHorizontalized; +}; + +/** + * This class is used to adjust the delta values for wheel scrolling with the + * auto-dir functionality. + * A traditional wheel scroll only allows the user use the wheel in the same + * scrollable direction as that of the scrolling target to scroll the target, + * whereas an auto-dir scroll lets the user use any wheel(either a vertical + * wheel or a horizontal tilt wheel) to scroll a frame which is scrollable in + * only one direction. For detailed information on auto-dir scrolling, + * @see mozilla::WheelDeltaAdjustmentStrategy. + */ +class MOZ_STACK_CLASS AutoDirWheelDeltaAdjuster { + protected: + /** + * @param aDeltaX DeltaX for a wheel event whose delta values will + * be adjusted upon calling Adjust() when + * ShouldBeAdjusted() returns true. + * @param aDeltaY DeltaY for a wheel event, like DeltaX. + */ + AutoDirWheelDeltaAdjuster(double& aDeltaX, double& aDeltaY) + : mDeltaX(aDeltaX), + mDeltaY(aDeltaY), + mCheckedIfShouldBeAdjusted(false), + mShouldBeAdjusted(false) {} + + public: + /** + * Gets whether the values of the delta should be adjusted for auto-dir + * scrolling. Note that if Adjust() has been called, this function simply + * returns false. + * + * @return true if the delta should be adjusted; otherwise false. + */ + bool ShouldBeAdjusted(); + /** + * Adjusts the values of the delta values for auto-dir scrolling when + * ShouldBeAdjusted() returns true. If you call it when ShouldBeAdjusted() + * returns false, this function will simply do nothing. + */ + void Adjust(); + + private: + /** + * Called by Adjust() if Adjust() successfully adjusted the delta values. + */ + virtual void OnAdjusted() {} + + virtual bool CanScrollAlongXAxis() const = 0; + virtual bool CanScrollAlongYAxis() const = 0; + virtual bool CanScrollUpwards() const = 0; + virtual bool CanScrollDownwards() const = 0; + virtual bool CanScrollLeftwards() const = 0; + virtual bool CanScrollRightwards() const = 0; + + /** + * Gets whether the horizontal content starts at rightside. + * + * @return If the content is in vertical-RTL writing mode(E.g. "writing-mode: + * vertical-rl" in CSS), or if it's in horizontal-RTL writing-mode + * (E.g. "writing-mode: horizontal-tb; direction: rtl;" in CSS), then + * this function returns true. From the representation perspective, + * frames whose horizontal contents start at rightside also cause + * their horizontal scrollbars, if any, initially start at rightside. + * So we can also learn about the initial side of the horizontal + * scrollbar for the frame by calling this function. + */ + virtual bool IsHorizontalContentRightToLeft() const = 0; + + protected: + double& mDeltaX; + double& mDeltaY; + + private: + bool mCheckedIfShouldBeAdjusted; + bool mShouldBeAdjusted; +}; + +/** + * This is the implementation of AutoDirWheelDeltaAdjuster for EventStateManager + * + * Detailed comments about some member functions are given in the base class + * AutoDirWheelDeltaAdjuster. + */ +class MOZ_STACK_CLASS ESMAutoDirWheelDeltaAdjuster final + : public AutoDirWheelDeltaAdjuster { + public: + /** + * @param aEvent The auto-dir wheel scroll event. + * @param aScrollFrame The scroll target for the event. + * @param aHonoursRoot If set to true, the honoured frame is the root + * frame in the same document where the target is; + * If false, the honoured frame is the scroll + * target. For the concept of an honoured target, + * @see mozilla::WheelDeltaAdjustmentStrategy + */ + ESMAutoDirWheelDeltaAdjuster(WidgetWheelEvent& aEvent, nsIFrame& aScrollFrame, + bool aHonoursRoot); + + private: + virtual void OnAdjusted() override; + virtual bool CanScrollAlongXAxis() const override; + virtual bool CanScrollAlongYAxis() const override; + virtual bool CanScrollUpwards() const override; + virtual bool CanScrollDownwards() const override; + virtual bool CanScrollLeftwards() const override; + virtual bool CanScrollRightwards() const override; + virtual bool IsHorizontalContentRightToLeft() const override; + + nsIScrollableFrame* mScrollTargetFrame; + bool mIsHorizontalContentRightToLeft; + + int32_t& mLineOrPageDeltaX; + int32_t& mLineOrPageDeltaY; + double& mOverflowDeltaX; + double& mOverflowDeltaY; +}; + +/** + * This class is used for restoring the delta in an auto-dir wheel. + * + * An instance of this calss monitors auto-dir adjustment which may happen + * during its lifetime. If the delta values is adjusted during its lifetime, the + * instance will restore the adjusted delta when it's being destrcuted. + */ +class MOZ_STACK_CLASS ESMAutoDirWheelDeltaRestorer final { + public: + /** + * @param aEvent The wheel scroll event to be monitored. + */ + explicit ESMAutoDirWheelDeltaRestorer(WidgetWheelEvent& aEvent); + ~ESMAutoDirWheelDeltaRestorer(); + + private: + WidgetWheelEvent& mEvent; + double mOldDeltaX; + double mOldDeltaY; + int32_t mOldLineOrPageDeltaX; + int32_t mOldLineOrPageDeltaY; + double mOldOverflowDeltaX; + double mOldOverflowDeltaY; +}; + +} // namespace mozilla + +#endif // mozilla_WheelHandlingHelper_h_ diff --git a/dom/events/XULCommandEvent.cpp b/dom/events/XULCommandEvent.cpp new file mode 100644 index 0000000000..b561cd3fb6 --- /dev/null +++ b/dom/events/XULCommandEvent.cpp @@ -0,0 +1,73 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#include "mozilla/dom/XULCommandEvent.h" +#include "prtime.h" + +namespace mozilla::dom { + +XULCommandEvent::XULCommandEvent(EventTarget* aOwner, + nsPresContext* aPresContext, + WidgetInputEvent* aEvent) + : UIEvent( + aOwner, aPresContext, + aEvent ? aEvent : new WidgetInputEvent(false, eVoidEvent, nullptr)), + mInputSource(0) { + if (aEvent) { + mEventIsInternal = false; + } else { + mEventIsInternal = true; + } +} + +NS_IMPL_ADDREF_INHERITED(XULCommandEvent, UIEvent) +NS_IMPL_RELEASE_INHERITED(XULCommandEvent, UIEvent) + +NS_IMPL_CYCLE_COLLECTION_INHERITED(XULCommandEvent, UIEvent, mSourceEvent) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(XULCommandEvent) +NS_INTERFACE_MAP_END_INHERITING(UIEvent) + +bool XULCommandEvent::AltKey() { return mEvent->AsInputEvent()->IsAlt(); } + +bool XULCommandEvent::CtrlKey() { return mEvent->AsInputEvent()->IsControl(); } + +bool XULCommandEvent::ShiftKey() { return mEvent->AsInputEvent()->IsShift(); } + +bool XULCommandEvent::MetaKey() { return mEvent->AsInputEvent()->IsMeta(); } + +uint16_t XULCommandEvent::InputSource() { return mInputSource; } + +void XULCommandEvent::InitCommandEvent( + const nsAString& aType, bool aCanBubble, bool aCancelable, + nsGlobalWindowInner* aView, int32_t aDetail, bool aCtrlKey, bool aAltKey, + bool aShiftKey, bool aMetaKey, int16_t aButton, Event* aSourceEvent, + uint16_t aInputSource, ErrorResult& aRv) { + if (NS_WARN_IF(mEvent->mFlags.mIsBeingDispatched)) { + return; + } + + UIEvent::InitUIEvent(aType, aCanBubble, aCancelable, aView, aDetail); + + mEvent->AsInputEvent()->InitBasicModifiers(aCtrlKey, aAltKey, aShiftKey, + aMetaKey); + mSourceEvent = aSourceEvent; + mInputSource = aInputSource; + mButton = aButton; +} + +} // namespace mozilla::dom + +using namespace mozilla; +using namespace mozilla::dom; + +already_AddRefed<XULCommandEvent> NS_NewDOMXULCommandEvent( + EventTarget* aOwner, nsPresContext* aPresContext, + WidgetInputEvent* aEvent) { + RefPtr<XULCommandEvent> it = + new XULCommandEvent(aOwner, aPresContext, aEvent); + return it.forget(); +} diff --git a/dom/events/XULCommandEvent.h b/dom/events/XULCommandEvent.h new file mode 100644 index 0000000000..0af98666bd --- /dev/null +++ b/dom/events/XULCommandEvent.h @@ -0,0 +1,66 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +// This class implements a XUL "command" event. See XULCommandEvent.webidl + +#ifndef mozilla_dom_XULCommandEvent_h_ +#define mozilla_dom_XULCommandEvent_h_ + +#include "mozilla/RefPtr.h" +#include "mozilla/dom/UIEvent.h" +#include "mozilla/dom/XULCommandEventBinding.h" + +namespace mozilla::dom { + +class XULCommandEvent : public UIEvent { + public: + XULCommandEvent(EventTarget* aOwner, nsPresContext* aPresContext, + WidgetInputEvent* aEvent); + + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(XULCommandEvent, UIEvent) + + virtual JSObject* WrapObjectInternal( + JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override { + return XULCommandEvent_Binding::Wrap(aCx, this, aGivenProto); + } + + virtual XULCommandEvent* AsXULCommandEvent() override { return this; } + + bool AltKey(); + bool CtrlKey(); + bool ShiftKey(); + bool MetaKey(); + uint16_t InputSource(); + int16_t Button() { return mButton; } + + already_AddRefed<Event> GetSourceEvent() { + RefPtr<Event> e = mSourceEvent; + return e.forget(); + } + + void InitCommandEvent(const nsAString& aType, bool aCanBubble, + bool aCancelable, nsGlobalWindowInner* aView, + int32_t aDetail, bool aCtrlKey, bool aAltKey, + bool aShiftKey, bool aMetaKey, int16_t aButton, + Event* aSourceEvent, uint16_t aInputSource, + ErrorResult& aRv); + + protected: + ~XULCommandEvent() = default; + + RefPtr<Event> mSourceEvent; + uint16_t mInputSource; + int16_t mButton = 0; +}; + +} // namespace mozilla::dom + +already_AddRefed<mozilla::dom::XULCommandEvent> NS_NewDOMXULCommandEvent( + mozilla::dom::EventTarget* aOwner, nsPresContext* aPresContext, + mozilla::WidgetInputEvent* aEvent); + +#endif // mozilla_dom_XULCommandEvent_h_ diff --git a/dom/events/crashtests/1033343.html b/dom/events/crashtests/1033343.html new file mode 100644 index 0000000000..99160cebff --- /dev/null +++ b/dom/events/crashtests/1033343.html @@ -0,0 +1,5 @@ +<script> + +document.createEvent('CustomEvent').detail; + +</script> diff --git a/dom/events/crashtests/1035654-1.html b/dom/events/crashtests/1035654-1.html new file mode 100644 index 0000000000..6e3e3c0270 --- /dev/null +++ b/dom/events/crashtests/1035654-1.html @@ -0,0 +1,21 @@ +<!DOCTYPE html> +<html> + +<head> +<script> + +function boom() +{ + var video = document.createElement('video'); + var track = video.addTextTrack('chapters'); + window.meep = new TrackEvent("t", { "track": track }); + document.documentElement.style.expando = null; +} + +</script> +</head> + +<body onload="boom();"> +</body> + +</html> diff --git a/dom/events/crashtests/1035654-2.html b/dom/events/crashtests/1035654-2.html new file mode 100644 index 0000000000..19f5a1cc89 --- /dev/null +++ b/dom/events/crashtests/1035654-2.html @@ -0,0 +1,20 @@ +<!DOCTYPE html> +<html> + +<head> +<script> + +function boom() +{ + var video = document.createElement('video'); + var track = video.addTextTrack('chapters'); + track.expando = new TrackEvent("t", { "track": track }) +} + +</script> +</head> + +<body onload="boom();"> +</body> + +</html> diff --git a/dom/events/crashtests/104310-1.html b/dom/events/crashtests/104310-1.html new file mode 100644 index 0000000000..f9544c2f8c --- /dev/null +++ b/dom/events/crashtests/104310-1.html @@ -0,0 +1,22 @@ +<HTML>
+
+<HEAD>
+</HEAD>
+
+<BODY onload=document.input.name.focus()>
+
+
+<form method="post" action="index.cfm" name="input">
+<CENTER><TABLE BORDER=0>
+<TD><INPUT TYPE="text" NAME="name" onClick="this.focus()" onFocus="this.select()" onSelect="this.select()"></TD>
+
+</TABLE></CENTER>
+<HR>
+
+</FORM>
+
+</BODY>
+
+</HTML>
+
+
diff --git a/dom/events/crashtests/1072137-1.html b/dom/events/crashtests/1072137-1.html new file mode 100644 index 0000000000..ae26188abf --- /dev/null +++ b/dom/events/crashtests/1072137-1.html @@ -0,0 +1,18 @@ +<!DOCTYPE html> +<html> +<script> + +function boom() +{ + var textareaRoot = document.createElement("textarea"); + document.removeChild(document.documentElement); + document.appendChild(textareaRoot); + var input = document.createElement("input"); + textareaRoot.appendChild(input); + input.select(); +} + +</script> +</head> +<body onload="boom();"></body> +</html> diff --git a/dom/events/crashtests/1143972-1.html b/dom/events/crashtests/1143972-1.html new file mode 100644 index 0000000000..992bf94782 --- /dev/null +++ b/dom/events/crashtests/1143972-1.html @@ -0,0 +1,12 @@ +<!DOCTYPE HTML> +<script> +var x; +function doTest() { + f.contentDocument.body.onclick = function (event) { + x = event.offsetX; + } + f.contentDocument.body.dispatchEvent(f.contentWindow.ev); +} +</script> +<iframe id="f" style="display:none" onload="doTest()" + src="data:text/html,<script>var ev = new MouseEvent('click', {clientX:0, clientY:0})</script>"></iframe> diff --git a/dom/events/crashtests/116206-1.html b/dom/events/crashtests/116206-1.html new file mode 100644 index 0000000000..b04c150978 --- /dev/null +++ b/dom/events/crashtests/116206-1.html @@ -0,0 +1,23 @@ +<html> + <head> + <script language="JavaScript"> + function InitialFocus(){ + document.frmSelectUser.radResidence[0].focus(); + } + </script> + </head> + <body onfocus="InitialFocus();" > + <form name="frmSelectUser"> + <table> + <tbody> + <tr> + <td><input name="radResidence" type="radio" value="KOR"></td> + </tr> + <tr> + <td><input name="radResidence" type="radio" value="JPN"></td> + </tr> + </tbody> + </table> + </form> + </body> +</html> diff --git a/dom/events/crashtests/1190036-1.html b/dom/events/crashtests/1190036-1.html new file mode 100644 index 0000000000..00a6a25db6 --- /dev/null +++ b/dom/events/crashtests/1190036-1.html @@ -0,0 +1,15 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="UTF-8"> +<script> + +function boom() { + var ev = new ClipboardEvent("p"); + ev.clipboardData.getFilesAndDirectories(); +} + +</script> +</head> +<body onload="boom();"></body> +</html> diff --git a/dom/events/crashtests/135345-1.html b/dom/events/crashtests/135345-1.html new file mode 100644 index 0000000000..f9bfe0275a --- /dev/null +++ b/dom/events/crashtests/135345-1.html @@ -0,0 +1,14 @@ +<html> +<body> + <form name="frmlogin"> + <input type="text" name="username" + onfocus="frmlogin.username.select();" + onblur="frmlogin.password.focus();"> + <input type="password" name="password" + onfocus="frmlogin.password.select();"> + </form> + <script language="JavaScript"> + document.frmlogin.username.focus(); + </script> +</body> +</html> diff --git a/dom/events/crashtests/1397711.html b/dom/events/crashtests/1397711.html new file mode 100644 index 0000000000..fef4db2fb5 --- /dev/null +++ b/dom/events/crashtests/1397711.html @@ -0,0 +1,11 @@ +<!DOCTYPE html> +<script> + let code = "x".charCodeAt(0); + let e = new KeyboardEvent("keypress", { + keyCode: code, + charCode: code, + bubbles: true + }); + let utils = SpecialPowers.getDOMWindowUtils(window); + utils.dispatchDOMEventViaPresShellForTesting(document.documentElement, e); +</script> diff --git a/dom/events/crashtests/457776-1.html b/dom/events/crashtests/457776-1.html new file mode 100644 index 0000000000..2c3910815d --- /dev/null +++ b/dom/events/crashtests/457776-1.html @@ -0,0 +1,9 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> +<script> +new XMLHttpRequest().upload.onabort = function(){}; +</script> +</head> +<body> +</body> +</html> diff --git a/dom/events/crashtests/496308-1.html b/dom/events/crashtests/496308-1.html new file mode 100644 index 0000000000..29d9a646c6 --- /dev/null +++ b/dom/events/crashtests/496308-1.html @@ -0,0 +1,3 @@ +<script> + document.createEvent("popupblockedevents").requestingWindow; +</script> diff --git a/dom/events/crashtests/682637-1.html b/dom/events/crashtests/682637-1.html new file mode 100644 index 0000000000..6ae7336f25 --- /dev/null +++ b/dom/events/crashtests/682637-1.html @@ -0,0 +1,22 @@ +<!DOCTYPE html> +<html> + +<head> +<script> + +function boom() +{ + var frame = document.getElementById("f"); + var frameWin = frame.contentWindow; + frame.remove(); + frameWin.onmouseover = function(){}; +} + +</script> +</head> + +<body onload="boom();"> +<iframe id="f" src="data:text/html,1"></iframe> +</body> + +</html> diff --git a/dom/events/crashtests/938341.html b/dom/events/crashtests/938341.html new file mode 100644 index 0000000000..3190b4a6b5 --- /dev/null +++ b/dom/events/crashtests/938341.html @@ -0,0 +1,7 @@ +<html hidden> +<script> +function a(){document.getElementById('id1').click();} +window.onload = a; +</script> +<applet <ol onclick='' contenteditable='true' onended='' oncanplay=''> +<article id='id1'></article>
\ No newline at end of file diff --git a/dom/events/crashtests/crashtests.list b/dom/events/crashtests/crashtests.list new file mode 100644 index 0000000000..dec907a693 --- /dev/null +++ b/dom/events/crashtests/crashtests.list @@ -0,0 +1,18 @@ +skip-if(ThreadSanitizer) load 104310-1.html +load 116206-1.html +load 135345-1.html +load 457776-1.html +load 496308-1.html +load 682637-1.html +load 938341.html +load 1033343.html +load 1035654-1.html +load 1035654-2.html +needs-focus load 1072137-1.html +load 1143972-1.html +load 1190036-1.html +load eventctor-nulldictionary.html +load eventctor-nullstorage.html +load recursive-DOMNodeInserted.html +load recursive-onload.html +load 1397711.html diff --git a/dom/events/crashtests/eventctor-nulldictionary.html b/dom/events/crashtests/eventctor-nulldictionary.html new file mode 100644 index 0000000000..f813994139 --- /dev/null +++ b/dom/events/crashtests/eventctor-nulldictionary.html @@ -0,0 +1,4 @@ +<!DOCTYPE html> +<script> +new MouseEvent("click", null); +</script> diff --git a/dom/events/crashtests/eventctor-nullstorage.html b/dom/events/crashtests/eventctor-nullstorage.html new file mode 100644 index 0000000000..beae2f3dfd --- /dev/null +++ b/dom/events/crashtests/eventctor-nullstorage.html @@ -0,0 +1,4 @@ +<!DOCTYPE html> +<script> +new StorageEvent("").storageArea; +</script> diff --git a/dom/events/crashtests/recursive-DOMNodeInserted.html b/dom/events/crashtests/recursive-DOMNodeInserted.html new file mode 100644 index 0000000000..c3e6f41d57 --- /dev/null +++ b/dom/events/crashtests/recursive-DOMNodeInserted.html @@ -0,0 +1,17 @@ +<HTML>
+<HEAD>
+<TITLE></TITLE>
+
+</HEAD>
+<BODY BGCOLOR="#FFFFFF" TEXT="#000000" LINK="#0000FF" VLINK="#800080" onload="test()">
+<INPUT id="txt1" type="text" value="">
+
+<SCRIPT>
+document.addEventListener("DOMNodeInserted",test,0);
+count = 0;
+function test(){
+ n = document.createTextNode("test");
+ document.body.appendChild(n);
+}
+</SCRIPT>
+</BODY>
diff --git a/dom/events/crashtests/recursive-onload.html b/dom/events/crashtests/recursive-onload.html new file mode 100644 index 0000000000..e8f15610b2 --- /dev/null +++ b/dom/events/crashtests/recursive-onload.html @@ -0,0 +1 @@ +<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<HTML>
<head>
<title>Goodjet!</title>
<script language="JavaScript">
function onload()
{
// nothing here
}
</script>
</head>
<body onload ="onload();">
Body text
</body>
</html>
\ No newline at end of file diff --git a/dom/events/moz.build b/dom/events/moz.build new file mode 100644 index 0000000000..891f90d172 --- /dev/null +++ b/dom/events/moz.build @@ -0,0 +1,186 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +with Files("**"): + BUG_COMPONENT = ("Core", "DOM: UI Events & Focus Handling") + +with Files("Event*"): + BUG_COMPONENT = ("Core", "DOM: Events") + +BROWSER_CHROME_MANIFESTS += [ + "test/browser.ini", + "test/clipboard/browser.ini", +] + +MOCHITEST_MANIFESTS += [ + "test/mochitest.ini", + "test/pointerevents/mochitest.ini", +] + +MOCHITEST_CHROME_MANIFESTS += [ + "test/chrome.ini", + "test/clipboard/chrome.ini", + "test/pointerevents/chrome.ini", +] + +TEST_DIRS += ["test/gtest"] + +XPIDL_SOURCES += [ + "nsIEventListenerService.idl", +] + +XPIDL_MODULE = "content_events" + +EXPORTS.mozilla += [ + "AsyncEventDispatcher.h", + "DOMEventTargetHelper.h", + "EventDispatcher.h", + "EventListenerManager.h", + "EventNameList.h", + "EventStateManager.h", + "GlobalKeyListener.h", + "IMEContentObserver.h", + "IMEStateManager.h", + "InputEventOptions.h", + "InputTypeList.h", + "InternalMutationEvent.h", + "JSEventHandler.h", + "KeyEventHandler.h", + "KeyNameList.h", + "PendingFullscreenEvent.h", + "PhysicalKeyCodeNameList.h", + "ShortcutKeys.h", + "TextComposition.h", + "VirtualKeyCodeList.h", + "WheelHandlingHelper.h", +] + +EXPORTS.mozilla.dom += [ + "AnimationEvent.h", + "BeforeUnloadEvent.h", + "Clipboard.h", + "ClipboardEvent.h", + "ClipboardItem.h", + "CommandEvent.h", + "CompositionEvent.h", + "ConstructibleEventTarget.h", + "CustomEvent.h", + "DataTransfer.h", + "DataTransferItem.h", + "DataTransferItemList.h", + "DeviceMotionEvent.h", + "DragEvent.h", + "Event.h", + "EventTarget.h", + "FocusEvent.h", + "ImageCaptureError.h", + "InputEvent.h", + "KeyboardEvent.h", + "MessageEvent.h", + "MouseEvent.h", + "MouseScrollEvent.h", + "MutationEvent.h", + "NotifyPaintEvent.h", + "PaintRequest.h", + "PointerEvent.h", + "PointerEventHandler.h", + "RemoteDragStartData.h", + "ScrollAreaEvent.h", + "SimpleGestureEvent.h", + "StorageEvent.h", + "TextClause.h", + "Touch.h", + "TouchEvent.h", + "TransitionEvent.h", + "UIEvent.h", + "WheelEvent.h", + "XULCommandEvent.h", +] + +if CONFIG["MOZ_WEBSPEECH"]: + EXPORTS.mozilla.dom += ["SpeechRecognitionError.h"] + +UNIFIED_SOURCES += [ + "AnimationEvent.cpp", + "AsyncEventDispatcher.cpp", + "BeforeUnloadEvent.cpp", + "Clipboard.cpp", + "ClipboardEvent.cpp", + "ClipboardItem.cpp", + "CommandEvent.cpp", + "CompositionEvent.cpp", + "ConstructibleEventTarget.cpp", + "ContentEventHandler.cpp", + "CustomEvent.cpp", + "DataTransfer.cpp", + "DataTransferItem.cpp", + "DataTransferItemList.cpp", + "DeviceMotionEvent.cpp", + "DOMEventTargetHelper.cpp", + "DragEvent.cpp", + "Event.cpp", + "EventDispatcher.cpp", + "EventListenerManager.cpp", + "EventListenerService.cpp", + "EventTarget.cpp", + "FocusEvent.cpp", + "GlobalKeyListener.cpp", + "ImageCaptureError.cpp", + "IMEContentObserver.cpp", + "IMEStateManager.cpp", + "InputEvent.cpp", + "JSEventHandler.cpp", + "KeyboardEvent.cpp", + "KeyEventHandler.cpp", + "MessageEvent.cpp", + "MouseEvent.cpp", + "MouseScrollEvent.cpp", + "MutationEvent.cpp", + "NotifyPaintEvent.cpp", + "PaintRequest.cpp", + "PointerEvent.cpp", + "PointerEventHandler.cpp", + "RemoteDragStartData.cpp", + "ScrollAreaEvent.cpp", + "ShortcutKeyDefinitions.cpp", + "ShortcutKeys.cpp", + "SimpleGestureEvent.cpp", + "StorageEvent.cpp", + "TextClause.cpp", + "TextComposition.cpp", + "Touch.cpp", + "TouchEvent.cpp", + "TransitionEvent.cpp", + "UIEvent.cpp", + "WheelEvent.cpp", + "WheelHandlingHelper.cpp", + "XULCommandEvent.cpp", +] + +# nsEventStateManager.cpp should be built separately because of Mac OS X headers. +SOURCES += [ + "EventStateManager.cpp", +] + +if CONFIG["MOZ_WEBSPEECH"]: + UNIFIED_SOURCES += ["SpeechRecognitionError.cpp"] + +include("/ipc/chromium/chromium-config.mozbuild") + +FINAL_LIBRARY = "xul" +LOCAL_INCLUDES += [ + "/docshell/base", + "/dom/base", + "/dom/html", + "/dom/storage", + "/dom/xml", + "/dom/xul", + "/js/xpconnect/wrappers", + "/layout/forms", + "/layout/generic", + "/layout/xul", + "/layout/xul/tree/", +] diff --git a/dom/events/nsIEventListenerService.idl b/dom/events/nsIEventListenerService.idl new file mode 100644 index 0000000000..ead3cc4af1 --- /dev/null +++ b/dom/events/nsIEventListenerService.idl @@ -0,0 +1,128 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +#include "nsISupports.idl" + +webidl EventTarget; + +interface nsIArray; + +/** + * Contains an event target along with a count of event listener changes + * affecting accessibility. + */ +[scriptable, uuid(07222b02-da12-4cf4-b2f7-761da007a8d8)] +interface nsIEventListenerChange : nsISupports +{ + readonly attribute EventTarget target; + + [noscript] + readonly attribute uint32_t countOfEventListenerChangesAffectingAccessibility; +}; + +[scriptable, function, uuid(aa7c95f6-d3b5-44b3-9597-1d9f19b9c5f2)] +interface nsIListenerChangeListener : nsISupports +{ + void listenersChanged(in nsIArray aEventListenerChanges); +}; + +/** + * An instance of this interface describes how an event listener + * was added to an event target. + */ +[scriptable, uuid(11ba5fd7-8db2-4b1a-9f67-342cfa11afad)] +interface nsIEventListenerInfo : nsISupports +{ + /** + * The type of the event for which the listener was added. + * Null if the listener is for all the events. + */ + readonly attribute AString type; + readonly attribute boolean capturing; + readonly attribute boolean allowsUntrusted; + readonly attribute boolean inSystemEventGroup; + + /** + * Changing the enabled state works only with listeners implemented in + * JS. An error is thrown for native listeners. + */ + attribute boolean enabled; + + /** + * The underlying JS object of the event listener, if this listener + * has one. Null otherwise. + */ + [implicit_jscontext] + readonly attribute jsval listenerObject; + + /** + * Tries to serialize event listener to a string. + * Returns null if serialization isn't possible + * (for example with C++ listeners). + */ + AString toSource(); +}; + +[scriptable, uuid(77aab5f7-213d-4db4-9f22-e46dfb774f15)] +interface nsIEventListenerService : nsISupports +{ + /** + * Returns an array of nsIEventListenerInfo objects. + * If aEventTarget doesn't have any listeners, this returns null. + */ + Array<nsIEventListenerInfo> getListenerInfoFor(in EventTarget aEventTarget); + + /** + * Returns an array of event targets. + * aEventTarget will be at index 0. + * The objects are the ones that would be used as DOMEvent.currentTarget while + * dispatching an event to aEventTarget + * @note Some events, especially 'load', may actually have a shorter + * event target chain than what this methods returns. + */ + [can_run_script] + Array<EventTarget> getEventTargetChainFor(in EventTarget aEventTarget, + in boolean composed); + + /** + * Returns true if a event target has any listener for the given type. + */ + boolean hasListenersFor(in EventTarget aEventTarget, + in AString aType); + + /** + * Add a system-group eventlistener to a event target. + */ + [implicit_jscontext] + void addSystemEventListener(in EventTarget target, + in AString type, + in jsval listener, + in boolean useCapture); + + /** + * Remove a system-group eventlistener from a event target. + */ + [implicit_jscontext] + void removeSystemEventListener(in EventTarget target, + in AString type, + in jsval listener, + in boolean useCapture); + + [implicit_jscontext] + void addListenerForAllEvents(in EventTarget target, + in jsval listener, + [optional] in boolean aUseCapture, + [optional] in boolean aWantsUntrusted, + [optional] in boolean aSystemEventGroup); + + [implicit_jscontext] + void removeListenerForAllEvents(in EventTarget target, + in jsval listener, + [optional] in boolean aUseCapture, + [optional] in boolean aSystemEventGroup); + + void addListenerChangeListener(in nsIListenerChangeListener aListener); + void removeListenerChangeListener(in nsIListenerChangeListener aListener); +}; diff --git a/dom/events/test/browser.ini b/dom/events/test/browser.ini new file mode 100644 index 0000000000..722a833d53 --- /dev/null +++ b/dom/events/test/browser.ini @@ -0,0 +1,30 @@ +[DEFAULT] + +[browser_alt_keyup_in_content.js] +skip-if = + os == 'mac' + os == "win" && os_version == "6.1" # Skip on Azure - frequent failure + +[browser_beforeinput_by_execCommand_in_contentscript.js] +support-files = + file_beforeinput_by_execCommand_in_contentscript.html + ../../../browser/base/content/test/general/head.js + +[browser_bug1539497.js] +[browser_dragimage.js] +support-files = + dragimage.html + green.png +[browser_keyboard_event_init_key_event_enabled_in_contentscript.js] +support-files = + file_keyboard_event_init_key_event_enabled_in_contentscript.html + ../../../browser/base/content/test/general/head.js + +[browser_mouse_enterleave_switch_tab.js] +support-files = + ../../../browser/base/content/test/general/dummy_page.html +skip-if = + os == "win" && os_version == "6.1" # Skip on Azure - frequent failure + +[browser_shortcutkey_modifier_conflicts_with_content_accesskey_modifier.js] +skip-if = os == 'mac' # Alt + D is defined only on Linux and Windows diff --git a/dom/events/test/browser_alt_keyup_in_content.js b/dom/events/test/browser_alt_keyup_in_content.js new file mode 100644 index 0000000000..746e442d65 --- /dev/null +++ b/dom/events/test/browser_alt_keyup_in_content.js @@ -0,0 +1,333 @@ +"use strict"; + +add_task(async function runTests() { + const menubar = document.getElementById("toolbar-menubar"); + const autohide = menubar.getAttribute("autohide"); + // This test requires that the window is active because of the limitation of + // menubar. Therefore, we should abort if the window becomes inactive during + // the tests. + let runningTests = true; + function onWindowActive(aEvent) { + // Don't warn after timed out. + if (runningTests && aEvent.target === window) { + info( + "WARNING: This window shouldn't have been inactivated during tests, but received an activated event!" + ); + } + } + function onWindowInactive(aEvent) { + // Don't warn after timed out. + if (runningTests && aEvent.target === window) { + info( + "WARNING: This window should be active during tests, but inactivated!" + ); + window.focus(); + } + } + let menubarActivated = false; + function onMenubarActive() { + menubarActivated = true; + } + // In this test, menu popups shouldn't be open, but this helps avoiding + // intermittent failure after inactivating the menubar. + let popupEvents = 0; + function getPopupInfo(aPopupEventTarget) { + return `<${ + aPopupEventTarget.nodeName + }${aPopupEventTarget.getAttribute("id") !== null ? ` id="${aPopupEventTarget.getAttribute("id")}"` : ""}>`; + } + function onPopupShown(aEvent) { + // Don't warn after timed out. + if (!runningTests) { + return; + } + popupEvents++; + info( + `A popup (${getPopupInfo( + aEvent.target + )}) is shown (visible popups: ${popupEvents})` + ); + } + function onPopupHidden(aEvent) { + // Don't warn after timed out. + if (!runningTests) { + return; + } + if (popupEvents === 0) { + info( + `WARNING: There are some unexpected popups which may be not cleaned up by the previous test (${getPopupInfo( + aEvent.target + )})` + ); + return; + } + popupEvents--; + info( + `A popup (${getPopupInfo( + aEvent.target + )}) is hidden (visible popups: ${popupEvents})` + ); + } + try { + Services.prefs.setBoolPref("ui.key.menuAccessKeyFocuses", true); + // If this fails, you need to replace "KEY_Alt" with a variable whose + // value is considered from the pref. + is( + Services.prefs.getIntPref("ui.key.menuAccessKey"), + 18, + "This test assumes that Alt key activates the menubar" + ); + window.addEventListener("activate", onWindowActive); + window.addEventListener("deactivate", onWindowInactive); + window.addEventListener("popupshown", onPopupShown); + window.addEventListener("popuphidden", onPopupHidden); + menubar.addEventListener("DOMMenuBarActive", onMenubarActive); + async function doTest(aTest) { + await new Promise(resolve => { + if (Services.focus.activeWindow === window) { + resolve(); + return; + } + info( + `${aTest.description}: The testing window is inactive, trying to activate it...` + ); + Services.focus.focusedWindow = window; + TestUtils.waitForCondition(() => { + if (Services.focus.activeWindow === window) { + resolve(); + return true; + } + Services.focus.focusedWindow = window; + return false; + }, `${aTest.description}: Waiting the window is activated`); + }); + let startTime = performance.now(); + info(`Start to test: ${aTest.description}...`); + + async function ensureMenubarInactive() { + if (!menubar.querySelector("[_moz-menuactive=true]")) { + return; + } + info(`${aTest.description}: Inactivating the menubar...`); + let waitForMenuBarInactive = BrowserTestUtils.waitForEvent( + menubar, + "DOMMenuBarInactive" + ); + EventUtils.synthesizeKey("KEY_Escape", {}, window); + await waitForMenuBarInactive; + await TestUtils.waitForCondition(() => { + return popupEvents === 0; + }, `${aTest.description}: Waiting for closing all popups`); + } + + try { + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: aTest.url, + }, + async browser => { + info(`${aTest.description}: Waiting browser getting focus...`); + await SimpleTest.promiseFocus(browser); + await ensureMenubarInactive(); + menubarActivated = false; + + let keyupEventFiredInContent = false; + BrowserTestUtils.addContentEventListener( + browser, + "keyup", + () => { + keyupEventFiredInContent = true; + }, + { capture: true }, + event => { + return event.key === "Alt"; + } + ); + + // For making sure adding the above content event listener and + // it'll get `keyup` event, let's run `SpecialPowers.spawn` and + // wait for focus in the content process. + info( + `${aTest.description}: Waiting content process getting focus...` + ); + await SpecialPowers.spawn( + browser, + [aTest.description], + async aTestDescription => { + await ContentTaskUtils.waitForCondition(() => { + if ( + content.browsingContext.isActive && + content.document.hasFocus() + ) { + return true; + } + content.window.focus(); + return false; + }, `${aTestDescription}: Waiting for content gets focus in content process`); + } + ); + + let waitForAllKeyUpEventsInChrome = new Promise(resolve => { + // Wait 2 `keyup` events in the main process. First one is + // synthesized one. The other is replay event from content. + let firstKeyUpEvent; + window.addEventListener( + "keyup", + function onKeyUpInChrome(event) { + if (!firstKeyUpEvent) { + firstKeyUpEvent = event; + return; + } + window.removeEventListener("keyup", onKeyUpInChrome, { + capture: true, + }); + resolve(); + }, + { capture: true } + ); + }); + + let menubarActivatedPromise; + if (aTest.expectMenubarActive) { + menubarActivatedPromise = BrowserTestUtils.waitForEvent( + menubar, + "DOMMenuBarActive" + ); + } + + EventUtils.synthesizeKey("KEY_Alt", {}, window); + info( + `${aTest.description}: Waiting keyup events of Alt in chrome...` + ); + await waitForAllKeyUpEventsInChrome; + info(`${aTest.description}: Waiting keyup event in content...`); + try { + await TestUtils.waitForCondition(() => { + return keyupEventFiredInContent; + }, `${aTest.description}: Waiting for content gets focus in chrome process`); + } catch (ex) { + ok( + false, + `${aTest.description}: Failed to synthesize Alt key press in the content process` + ); + return; + } + + if (aTest.expectMenubarActive) { + await menubarActivatedPromise; + ok( + menubarActivated, + `${aTest.description}: Menubar should've been activated by the synthesized Alt key press` + ); + } else { + // Wait some ticks to verify not receiving "DOMMenuBarActive" event. + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(resolve => setTimeout(resolve, 500)); + ok( + !menubarActivated, + `${aTest.description}: Menubar should not have been activated by the synthesized Alt key press` + ); + } + } + ); + } catch (ex) { + ok( + false, + `${aTest.description}: Thrown an exception: ${ex.toString()}` + ); + } finally { + await ensureMenubarInactive(); + info(`End testing: ${aTest.description}`); + ChromeUtils.addProfilerMarker( + "browser-test", + { startTime, category: "Test" }, + aTest.description + ); + } + } + + // Testcases for users who use collapsible menubar (by default) + menubar.setAttribute("autohide", "true"); + await doTest({ + description: "Testing menubar is shown by Alt keyup", + url: "data:text/html;charset=utf-8,<p>static page</p>", + expectMenubarActive: true, + }); + await doTest({ + description: + "Testing menubar is shown by Alt keyup when an <input> has focus", + url: + "data:text/html;charset=utf-8,<input>" + + '<script>document.querySelector("input").focus()</script>', + expectMenubarActive: true, + }); + await doTest({ + description: + "Testing menubar is shown by Alt keyup when an editing host has focus", + url: + "data:text/html;charset=utf-8,<p contenteditable></p>" + + '<script>document.querySelector("p[contenteditable]").focus()</script>', + expectMenubarActive: true, + }); + await doTest({ + description: + "Testing menubar won't be shown by Alt keyup due to suppressed by the page", + url: + "data:text/html;charset=utf-8,<p>dynamic page</p>" + + '<script>window.addEventListener("keyup", event => { event.preventDefault(); })</script>', + expectMenubarActive: false, + }); + + // Testcases for users who always show the menubar. + menubar.setAttribute("autohide", "false"); + await doTest({ + description: "Testing menubar is activated by Alt keyup", + url: "data:text/html;charset=utf-8,<p>static page</p>", + expectMenubarActive: true, + }); + await doTest({ + description: + "Testing menubar is activated by Alt keyup when an <input> has focus", + url: + "data:text/html;charset=utf-8,<input>" + + '<script>document.querySelector("input").focus()</script>', + expectMenubarActive: true, + }); + await doTest({ + description: + "Testing menubar is activated by Alt keyup when an editing host has focus", + url: + "data:text/html;charset=utf-8,<p contenteditable></p>" + + '<script>document.querySelector("p[contenteditable]").focus()</script>', + expectMenubarActive: true, + }); + await doTest({ + description: + "Testing menubar won't be activated by Alt keyup due to suppressed by the page", + url: + "data:text/html;charset=utf-8,<p>dynamic page</p>" + + '<script>window.addEventListener("keyup", event => { event.preventDefault(); })</script>', + expectMenubarActive: false, + }); + runningTests = false; + } catch (ex) { + ok( + false, + `Aborting this test due to unexpected the exception (${ex.toString()})` + ); + runningTests = false; + } finally { + if (autohide !== null) { + menubar.setAttribute("autohide", autohide); + } else { + menubar.removeAttribute("autohide"); + } + Services.prefs.clearUserPref("ui.key.menuAccessKeyFocuses"); + menubar.removeEventListener("DOMMenuBarActive", onMenubarActive); + window.removeEventListener("activate", onWindowActive); + window.removeEventListener("deactivate", onWindowInactive); + window.removeEventListener("popupshown", onPopupShown); + window.removeEventListener("popuphidden", onPopupHidden); + } +}); diff --git a/dom/events/test/browser_beforeinput_by_execCommand_in_contentscript.js b/dom/events/test/browser_beforeinput_by_execCommand_in_contentscript.js new file mode 100644 index 0000000000..d995e6f4d4 --- /dev/null +++ b/dom/events/test/browser_beforeinput_by_execCommand_in_contentscript.js @@ -0,0 +1,106 @@ +"use strict"; + +async function installAndStartExtension() { + function contentScript() { + window.addEventListener("keydown", aEvent => { + console.log("keydown event is fired"); + if (aEvent.defaultPrevented) { + return; + } + let selection = window.getSelection(); + if (selection.isCollapsed) { + return; + } + if (aEvent.ctrlKey && aEvent.key === "k") { + document.execCommand("createLink", false, "http://example.com/"); + aEvent.preventDefault(); + } + }); + } + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + content_scripts: [ + { + js: ["content_script.js"], + matches: ["<all_urls>"], + run_at: "document_start", + }, + ], + }, + files: { + "content_script.js": contentScript, + }, + }); + + await extension.startup(); + + return extension; +} + +add_task(async function () { + const extension = await installAndStartExtension(); + const tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "http://example.com/browser/dom/events/test/file_beforeinput_by_execCommand_in_contentscript.html", + true + ); + + function runTest() { + var editor = content.document.querySelector("[contenteditable]"); + editor.focus(); + content.document.getSelection().selectAllChildren(editor); + var beforeinput; + editor.addEventListener("beforeinput", aEvent => { + beforeinput = aEvent; + }); + editor.addEventListener("input", aEvent => { + if (!beforeinput) { + sendAsyncMessage("Test:BeforeInputInContentEditable", { + succeeded: false, + message: "No beforeinput event is fired", + }); + return; + } + sendAsyncMessage("Test:BeforeInputInContentEditable", { + succeeded: + editor.innerHTML === '<a href="http://example.com/">abcdef</a>', + message: `editor.innerHTML=${editor.innerHTML}`, + }); + }); + } + + try { + tab.linkedBrowser.messageManager.loadFrameScript( + "data:,(" + runTest.toString() + ")();", + false + ); + + let received = false; + let testResult = new Promise(resolve => { + let mm = tab.linkedBrowser.messageManager; + mm.addMessageListener( + "Test:BeforeInputInContentEditable", + function onFinish(aMsg) { + mm.removeMessageListener( + "Test:BeforeInputInContentEditable", + onFinish + ); + is(aMsg.data.succeeded, true, aMsg.data.message); + resolve(); + } + ); + }); + info("Sending Ctrl+K..."); + await BrowserTestUtils.synthesizeKey( + "k", + { ctrlKey: true }, + tab.linkedBrowser + ); + info("Waiting test result..."); + await testResult; + } finally { + BrowserTestUtils.removeTab(tab); + await extension.unload(); + } +}); diff --git a/dom/events/test/browser_bug1539497.js b/dom/events/test/browser_bug1539497.js new file mode 100644 index 0000000000..78ea64c119 --- /dev/null +++ b/dom/events/test/browser_bug1539497.js @@ -0,0 +1,25 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function maxTouchPoints() { + await new Promise(resolve => { + SpecialPowers.pushPrefEnv( + { + set: [["dom.maxtouchpoints.testing.value", 5]], + }, + resolve + ); + }); + + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "data:text/html,Test page for navigator.maxTouchPoints" + ); + await SpecialPowers.spawn(tab.linkedBrowser, [], function () { + is(content.navigator.maxTouchPoints, 5, "Should have touch points."); + }); + + BrowserTestUtils.removeTab(tab); +}); diff --git a/dom/events/test/browser_dragimage.js b/dom/events/test/browser_dragimage.js new file mode 100644 index 0000000000..503c0ac449 --- /dev/null +++ b/dom/events/test/browser_dragimage.js @@ -0,0 +1,65 @@ +const TEST_URI = "dragimage.html"; + +// This test checks that dragging an image onto the same document +// does not drop it, even when the page cancels the dragover event. +add_task(async function dragimage_remote_tab() { + var tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "http://www.example.com/browser/dom/events/test/" + TEST_URI + ); + + let dropHappened = false; + let oldHandler = tab.linkedBrowser.droppedLinkHandler; + tab.linkedBrowser.droppedLinkHandler = () => { + dropHappened = true; + }; + + await SpecialPowers.spawn(tab.linkedBrowser, [], async () => { + let image = content.document.body.firstElementChild; + let target = content.document.body.lastElementChild; + + await EventUtils.synthesizePlainDragAndDrop({ + srcElement: image, + destElement: target, + srcWindow: content, + destWindow: content, + id: content.windowUtils.DEFAULT_MOUSE_POINTER_ID, + }); + }); + + tab.linkedBrowser.droppedLinkHandler = oldHandler; + + ok(!dropHappened, "drop did not occur"); + + BrowserTestUtils.removeTab(tab); +}); + +// This test checks the same but with an in-process page. +add_task(async function dragimage_local_tab() { + var tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + getRootDirectory(gTestPath) + TEST_URI + ); + + let dropHappened = false; + let oldHandler = tab.linkedBrowser.droppedLinkHandler; + tab.linkedBrowser.droppedLinkHandler = () => { + dropHappened = true; + }; + + let image = tab.linkedBrowser.contentDocument.body.firstElementChild; + let target = tab.linkedBrowser.contentDocument.body.lastElementChild; + + await EventUtils.synthesizePlainDragAndDrop({ + srcElement: image, + destElement: target, + srcWindow: tab.linkedBrowser.contentWindow, + destWindow: tab.linkedBrowser.contentWindow, + }); + + tab.linkedBrowser.droppedLinkHandler = oldHandler; + + ok(!dropHappened, "drop did not occur"); + + BrowserTestUtils.removeTab(tab); +}); diff --git a/dom/events/test/browser_keyboard_event_init_key_event_enabled_in_contentscript.js b/dom/events/test/browser_keyboard_event_init_key_event_enabled_in_contentscript.js new file mode 100644 index 0000000000..e8d51edb3e --- /dev/null +++ b/dom/events/test/browser_keyboard_event_init_key_event_enabled_in_contentscript.js @@ -0,0 +1,80 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +async function installAndStartExtension() { + function contentScript() { + window.addEventListener( + "load", + () => { + document.documentElement.setAttribute( + "data-initKeyEvent-in-contentscript", + typeof window.KeyboardEvent.prototype.initKeyEvent + ); + }, + { capture: true, once: true } + ); + } + + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + content_scripts: [ + { + js: ["content_script.js"], + matches: ["<all_urls>"], + run_at: "document_start", + }, + ], + }, + files: { + "content_script.js": contentScript, + }, + }); + + await extension.startup(); + + return extension; +} + +add_task(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.keyboardevent.init_key_event.enabled", false], + ["dom.keyboardevent.init_key_event.enabled_in_addons", true], + ], + }); + + const extension = await installAndStartExtension(); + await BrowserTestUtils.withNewTab( + "http://example.com/browser/dom/events/test/file_keyboard_event_init_key_event_enabled_in_contentscript.html", + async browser => { + info("Waiting for the test result..."); + await SpecialPowers.spawn(browser, [], () => { + Assert.equal( + content.document.documentElement.getAttribute( + "data-initKeyEvent-before" + ), + "undefined", + "KeyboardEvent.initKeyEvent shouldn't be available in web-content" + ); + Assert.equal( + content.document.documentElement.getAttribute( + "data-initKeyEvent-in-contentscript" + ), + "function", + "KeyboardEvent.initKeyEvent should be available in contentscript" + ); + Assert.equal( + content.document.documentElement.getAttribute( + "data-initKeyEvent-after" + ), + "undefined", + "KeyboardEvent.initKeyEvent shouldn't be available in web-content even after contentscript accesses it" + ); + }); + } + ); + + await extension.unload(); +}); diff --git a/dom/events/test/browser_mouse_enterleave_switch_tab.js b/dom/events/test/browser_mouse_enterleave_switch_tab.js new file mode 100644 index 0000000000..9c24120101 --- /dev/null +++ b/dom/events/test/browser_mouse_enterleave_switch_tab.js @@ -0,0 +1,158 @@ +"use strict"; + +async function synthesizeMouseAndWait(aBrowser, aEvent) { + let promise = SpecialPowers.spawn(aBrowser, [aEvent], async event => { + await new Promise(resolve => { + content.document.documentElement.addEventListener(event, resolve, { + once: true, + }); + }); + }); + // Ensure content has been added event listener. + await SpecialPowers.spawn(aBrowser, [], () => {}); + EventUtils.synthesizeMouse(aBrowser, 10, 10, { type: aEvent }); + return promise; +} + +function AddMouseEventListener(aBrowser) { + return SpecialPowers.spawn(aBrowser, [], () => { + content.catchedEvents = []; + let listener = function (aEvent) { + content.catchedEvents.push(aEvent.type); + }; + + let target = content.document.querySelector("p"); + target.onmouseenter = listener; + target.onmouseleave = listener; + }); +} + +function clearMouseEventListenerAndCheck(aBrowser, aExpectedEvents) { + return SpecialPowers.spawn(aBrowser, [aExpectedEvents], events => { + let target = content.document.querySelector("p"); + target.onmouseenter = null; + target.onmouseleave = null; + + Assert.deepEqual(content.catchedEvents, events); + }); +} + +add_task(async function testSwitchTabs() { + const tabFirst = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "http://example.com/browser/browser/base/content/test/general/dummy_page.html", + true + ); + + info("Initial mouse move"); + await EventUtils.synthesizeAndWaitNativeMouseMove( + tabFirst.linkedBrowser, + 10, + 10 + ); + + info("Open and move to a new tab"); + await AddMouseEventListener(tabFirst.linkedBrowser); + const tabSecond = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "http://example.com/browser/browser/base/content/test/general/dummy_page.html" + ); + // Synthesize a mousemove to generate corresponding mouseenter and mouseleave + // events. + await EventUtils.synthesizeAndWaitNativeMouseMove( + tabSecond.linkedBrowser, + 10, + 10 + ); + // Wait a bit to see if there is any unexpected mouse event. + await TestUtils.waitForTick(); + await clearMouseEventListenerAndCheck(tabFirst.linkedBrowser, ["mouseleave"]); + + info("switch back to the previous tab"); + await AddMouseEventListener(tabFirst.linkedBrowser); + await AddMouseEventListener(tabSecond.linkedBrowser); + await BrowserTestUtils.switchTab(gBrowser, tabFirst); + // Synthesize a mousemove to generate corresponding mouseenter and mouseleave + // events. + await EventUtils.synthesizeAndWaitNativeMouseMove( + tabFirst.linkedBrowser, + 10, + 10 + ); + // Wait a bit to see if there is any unexpected mouse event. + await TestUtils.waitForTick(); + await clearMouseEventListenerAndCheck(tabFirst.linkedBrowser, ["mouseenter"]); + await clearMouseEventListenerAndCheck(tabSecond.linkedBrowser, [ + "mouseleave", + ]); + + info("Close tabs"); + BrowserTestUtils.removeTab(tabFirst); + BrowserTestUtils.removeTab(tabSecond); +}); + +add_task(async function testSwitchTabsWithMouseDown() { + const tabFirst = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "http://example.com/browser/browser/base/content/test/general/dummy_page.html", + true + ); + + info("Initial mouse move"); + await EventUtils.synthesizeAndWaitNativeMouseMove( + tabFirst.linkedBrowser, + 10, + 10 + ); + + info("mouse down"); + await synthesizeMouseAndWait(tabFirst.linkedBrowser, "mousedown"); + + info("Open and move to a new tab"); + await AddMouseEventListener(tabFirst.linkedBrowser); + const tabSecond = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + "http://example.com/browser/browser/base/content/test/general/dummy_page.html" + ); + // Synthesize a mousemove to generate corresponding mouseenter and mouseleave + // events. + await EventUtils.synthesizeAndWaitNativeMouseMove( + tabSecond.linkedBrowser, + 10, + 10 + ); + + info("mouse up"); + await synthesizeMouseAndWait(tabSecond.linkedBrowser, "mouseup"); + // Wait a bit to see if there is any unexpected mouse event. + await TestUtils.waitForTick(); + await clearMouseEventListenerAndCheck(tabFirst.linkedBrowser, ["mouseleave"]); + + info("mouse down"); + await synthesizeMouseAndWait(tabSecond.linkedBrowser, "mousedown"); + + info("switch back to the previous tab"); + await AddMouseEventListener(tabFirst.linkedBrowser); + await AddMouseEventListener(tabSecond.linkedBrowser); + await BrowserTestUtils.switchTab(gBrowser, tabFirst); + // Synthesize a mousemove to generate corresponding mouseenter and mouseleave + // events. + await EventUtils.synthesizeAndWaitNativeMouseMove( + tabFirst.linkedBrowser, + 10, + 10 + ); + + info("mouse up"); + await synthesizeMouseAndWait(tabFirst.linkedBrowser, "mouseup"); + // Wait a bit to see if there is any unexpected mouse event. + await TestUtils.waitForTick(); + await clearMouseEventListenerAndCheck(tabFirst.linkedBrowser, ["mouseenter"]); + await clearMouseEventListenerAndCheck(tabSecond.linkedBrowser, [ + "mouseleave", + ]); + + info("Close tabs"); + BrowserTestUtils.removeTab(tabFirst); + BrowserTestUtils.removeTab(tabSecond); +}); diff --git a/dom/events/test/browser_shortcutkey_modifier_conflicts_with_content_accesskey_modifier.js b/dom/events/test/browser_shortcutkey_modifier_conflicts_with_content_accesskey_modifier.js new file mode 100644 index 0000000000..b12fd3b1df --- /dev/null +++ b/dom/events/test/browser_shortcutkey_modifier_conflicts_with_content_accesskey_modifier.js @@ -0,0 +1,102 @@ +add_task(async function () { + // Even if modifier of a shortcut key same as modifier of content access key, + // the shortcut key should be executed if (remote) content doesn't handle it. + // This test uses existing shortcut key declaration on Linux and Windows. + // If you remove or change Alt + D, you need to keep check this with changing + // the pref or result check. + + await new Promise(resolve => { + SpecialPowers.pushPrefEnv( + { + set: [ + ["ui.key.generalAccessKey", -1], + ["ui.key.chromeAccess", 0 /* disabled */], + ["ui.key.contentAccess", 4 /* Alt */], + ["browser.search.widget.inNavBar", true], + ], + }, + resolve + ); + }); + + const kTestPage = "data:text/html,<body>simple web page</body>"; + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, kTestPage); + + let searchBar = BrowserSearch.searchBar; + searchBar.focus(); + + function promiseURLBarHasFocus() { + return new Promise(resolve => { + if (gURLBar.focused) { + ok(true, "The URL bar already has focus"); + resolve(); + return; + } + info("Waiting focus event..."); + gURLBar.addEventListener( + "focus", + () => { + ok(true, "The URL bar gets focus"); + resolve(); + }, + { once: true } + ); + }); + } + + function promiseURLBarSelectsAllText() { + return new Promise(resolve => { + function isAllTextSelected() { + return ( + gURLBar.inputField.selectionStart === 0 && + gURLBar.inputField.selectionEnd === gURLBar.inputField.value.length + ); + } + if (isAllTextSelected()) { + ok(true, "All text of the URL bar is already selected"); + isnot( + gURLBar.inputField.value, + "", + "The URL bar should have non-empty text" + ); + resolve(); + return; + } + info("Waiting selection changes..."); + function tryToCheckItLater() { + if (!isAllTextSelected()) { + SimpleTest.executeSoon(tryToCheckItLater); + return; + } + ok(true, "All text of the URL bar should be selected"); + isnot( + gURLBar.inputField.value, + "", + "The URL bar should have non-empty text" + ); + resolve(); + } + SimpleTest.executeSoon(tryToCheckItLater); + }); + } + + // Alt + D is a shortcut key to move focus to the URL bar and selects its text. + info("Pressing Alt + D in the search bar..."); + EventUtils.synthesizeKey("d", { altKey: true }); + + await promiseURLBarHasFocus(); + await promiseURLBarSelectsAllText(); + + // Alt + D in the URL bar should select all text in it. + await gURLBar.focus(); + await promiseURLBarHasFocus(); + gURLBar.inputField.selectionStart = gURLBar.inputField.selectionEnd = + gURLBar.inputField.value.length; + + info("Pressing Alt + D in the URL bar..."); + EventUtils.synthesizeKey("d", { altKey: true }); + await promiseURLBarHasFocus(); + await promiseURLBarSelectsAllText(); + + gBrowser.removeCurrentTab(); +}); diff --git a/dom/events/test/bug1017086_inner.html b/dom/events/test/bug1017086_inner.html new file mode 100644 index 0000000000..e911d993d5 --- /dev/null +++ b/dom/events/test/bug1017086_inner.html @@ -0,0 +1,39 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1017086 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1017086</title> + <meta name="author" content="Maksim Lebedev" /> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript"> + /** Test for Bug 1017086 **/ + var testelem = undefined; + var pointer_events = ["onpointerover", "onpointerenter", + "onpointermove", + "onpointerdown", "onpointerup", + "onpointerout", "onpointerleave", + "onpointercancel"]; + function check(expected_value, event_name, container, container_name) { + var text = event_name + " in " + container_name + " should be " + expected_value; + parent.is(event_name in container, expected_value, text); + } + function runTest() { + testelem = document.getElementById("test"); + is(!!testelem, true, "Document should have element with id 'test'"); + parent.part_of_checks(pointer_events, check, window, document, testelem); + } + </script> +</head> +<body onload="runTest();"> + <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1017086">Mozilla Bug 1017086</a> + <p id="display"></p> + <div id="content" style="display: none"> + </div> + <pre id="test"> + </pre> +</body> +</html> diff --git a/dom/events/test/bug226361_iframe.xhtml b/dom/events/test/bug226361_iframe.xhtml new file mode 100644 index 0000000000..df38a8bcbe --- /dev/null +++ b/dom/events/test/bug226361_iframe.xhtml @@ -0,0 +1,47 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=226361 +--> +<head> + <title>Test for Bug 226361</title> +</head> +<body id="body"> +<p id="display"> + +<a id="a1" tabindex="3" href="http://home.mozilla.org">This is the 1st + +link but the 3rd tabindex</a><br /> + +<br /> + + <a id="a2" tabindex="4" href="http://home.mozilla.org">This is the 2nd + +link but the 4th tabindex</a><br /> + +<br /> + + <a id="a3" tabindex="1" href="http://home.mozilla.org">This is the 3rd + +link but the 1st tabindex</a><br /> + +<br /> + + <a id="a4" tabindex="5" href="http://home.mozilla.org">This is the 4th + +link but the 5th tabindex</a><br /> + +<br /> + + <a id="a5" tabindex="2" href="http://home.mozilla.org">This is the 5th + +link but the 2nd tabindex</a> + +</p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> + +</pre> +</body> +</html> diff --git a/dom/events/test/bug299673.js b/dom/events/test/bug299673.js new file mode 100644 index 0000000000..2775355c40 --- /dev/null +++ b/dom/events/test/bug299673.js @@ -0,0 +1,150 @@ +var popup; + +function OpenWindow() { + log({}, ">>> OpenWindow"); + popup = window.open("", "Test"); + + var output = "<html>"; + + output += "<body>"; + output += "<form>"; + output += + "<input id='popupText1' type='text' onfocus='opener.log(event)' onblur='opener.log(event)'>"; + output += "</form>"; + output += "</body>"; + output += "</html>"; + + popup.document.open(); + popup.document.write(output); + popup.document.close(); + + popup.document.onclick = function (event) { + log(event, "popup-doc"); + }; + popup.document.onfocus = function (event) { + log(event, "popup-doc"); + }; + popup.document.onblur = function (event) { + log(event, "popup-doc"); + }; + popup.document.onchange = function (event) { + log(event, "popup-doc"); + }; + + var e = popup.document.getElementById("popupText1"); + popup.focus(); + e.focus(); + is( + popup.document.activeElement, + e, + "input element in popup should be focused" + ); + log({}, "<<< OpenWindow"); +} + +var result; + +function log(event, message) { + if (event && event.eventPhase == 3) { + return; + } + e = event.currentTarget || event.target || event.srcElement; + var id = e ? (e.id ? e.id : e.name ? e.name : e.value ? e.value : "") : ""; + if (id) { + id = "(" + id + ")"; + } + result += + (e ? (e.tagName ? e.tagName : "") : " ") + + id + + ": " + + (event.type ? event.type : "") + + " " + + (message ? message : "") + + "\n"; +} + +document.onclick = function (event) { + log(event, "top-doc"); +}; +document.onfocus = function (event) { + log(event, "top-doc"); +}; +document.onblur = function (event) { + log(event, "top-doc"); +}; +document.onchange = function (event) { + log(event, "top-doc"); +}; + +function doTest1_rest2(expectedEventLog, focusAfterCloseId) { + try { + is( + document.activeElement, + document.getElementById(focusAfterCloseId), + "wrong element is focused after popup was closed" + ); + is(result, expectedEventLog, "unexpected events"); + SimpleTest.finish(); + } catch (e) { + if (popup) { + popup.close(); + } + throw e; + } +} +function doTest1_rest1(expectedEventLog, focusAfterCloseId) { + try { + synthesizeKey("V", {}, popup); + synthesizeKey("A", {}, popup); + synthesizeKey("L", {}, popup); + is( + popup.document.getElementById("popupText1").value, + "VAL", + "input element in popup did not accept input" + ); + + var p = popup; + popup = null; + p.close(); + + SimpleTest.waitForFocus(function () { + doTest1_rest2(expectedEventLog, focusAfterCloseId); + }, window); + } catch (e) { + if (popup) { + popup.close(); + } + throw e; + } +} + +function doTest1(expectedEventLog, focusAfterCloseId) { + try { + var select1 = document.getElementById("Select1"); + select1.focus(); + is(document.activeElement, select1, "select element should be focused"); + synthesizeKey("KEY_ArrowDown"); + synthesizeKey("KEY_Tab"); + SimpleTest.waitForFocus(function () { + doTest1_rest1(expectedEventLog, focusAfterCloseId); + }, popup); + } catch (e) { + if (popup) { + popup.close(); + } + throw e; + } +} + +function setPrefAndDoTest(expectedEventLog, focusAfterCloseId, prefValue) { + var select1 = document.getElementById("Select1"); + select1.blur(); + result = ""; + log({}, "Test with browser.link.open_newwindow = " + prefValue); + SpecialPowers.pushPrefEnv( + { set: [["browser.link.open_newwindow", prefValue]] }, + function () { + doTest1(expectedEventLog, focusAfterCloseId); + } + ); +} diff --git a/dom/events/test/bug322588-popup.html b/dom/events/test/bug322588-popup.html new file mode 100644 index 0000000000..767eb9db9c --- /dev/null +++ b/dom/events/test/bug322588-popup.html @@ -0,0 +1 @@ +<html><head></head><body onblur="window.close()"><a id="target">a id=target</a></body></html> diff --git a/dom/events/test/bug415498-doc1.html b/dom/events/test/bug415498-doc1.html new file mode 100644 index 0000000000..e8fbca6c9a --- /dev/null +++ b/dom/events/test/bug415498-doc1.html @@ -0,0 +1,15 @@ +<!DOCTYPE HTML> +<html> +<head> +<script type="text/javascript"> +function init() { + // This will throw a HierarchyRequestError exception + var doc = document.implementation.createDocument(null, 'DOC', null); + doc.documentElement.appendChild(doc); +} +window.addEventListener("load", init); +</script> +</head> +<body> + Testcase for bug 415498. This page should show an exception in Error Console on load +</body> diff --git a/dom/events/test/bug415498-doc2.html b/dom/events/test/bug415498-doc2.html new file mode 100644 index 0000000000..e556a4e4ca --- /dev/null +++ b/dom/events/test/bug415498-doc2.html @@ -0,0 +1,15 @@ +<!DOCTYPE HTML> +<html> +<head> +<script type="text/javascript"> +function init() { + // This will throw a HierarchyRequestError exception + var doc = document.implementation.createDocument(null, 'DOC', null); + doc.documentElement.appendChild(doc); +} +onload = init; +</script> +</head> +<body> + Testcase for bug 415498. This page should show an exception in Error Console on load +</body> diff --git a/dom/events/test/bug418986-3.js b/dom/events/test/bug418986-3.js new file mode 100644 index 0000000000..81d04b9b22 --- /dev/null +++ b/dom/events/test/bug418986-3.js @@ -0,0 +1,85 @@ +SimpleTest.waitForExplicitFinish(); + +// The main testing function. +var test = async function (isContent) { + await SpecialPowers.contentTransformsReceived(window); + + // Each definition is [eventType, prefSetting] + // Where we are setting the "privacy.resistFingerprinting" pref. + let eventDefs = [ + ["mousedown", true], + ["mouseup", true], + ["mousedown", false], + ["mouseup", false], + ]; + + let testCounter = 0; + + // Declare ahead of time. + let setup; + + // This function is called when the event handler fires. + let handleEvent = function (event, prefVal) { + let resisting = prefVal && isContent; + if (resisting) { + is( + event.screenX, + event.clientX, + "event.screenX and event.clientX should be the same" + ); + is( + event.screenY, + event.clientY, + "event.screenY and event.clientY should be the same" + ); + } else { + // We can't be sure about X coordinates not being equal, but we can test Y. + isnot(event.screenY, event.clientY, "event.screenY !== event.clientY"); + } + ++testCounter; + if (testCounter < eventDefs.length) { + nextTest(); + } else { + SimpleTest.finish(); + } + }; + + // In this function, we set up the nth div and event handler, + // and then synthesize a mouse event in the div, to test + // whether the resulting events resist fingerprinting by + // suppressing absolute screen coordinates. + nextTest = function () { + let [eventType, prefVal] = eventDefs[testCounter]; + SpecialPowers.pushPrefEnv( + { set: [["privacy.resistFingerprinting", prefVal]] }, + function () { + // The following code creates a new div for each event in eventDefs, + // attaches a listener to listen for the event, and then generates + // a fake event at the center of the div. + let div = document.createElementNS( + "http://www.w3.org/1999/xhtml", + "div" + ); + div.style.width = "10px"; + div.style.height = "10px"; + div.style.backgroundColor = "red"; + // Name the div after the event we're listening for. + div.id = eventType; + document.getElementById("body").appendChild(div); + // Seems we can't add an event listener in chrome unless we run + // it in a later task. + window.setTimeout(function () { + div.addEventListener(eventType, event => handleEvent(event, prefVal)); + // For some reason, the following synthesizeMouseAtCenter call only seems to run if we + // wrap it in a window.setTimeout(..., 0). + window.setTimeout(function () { + synthesizeMouseAtCenter(div, { type: eventType }); + }, 0); + }, 0); + } + ); + }; + + // Now run by starting with the 0th event. + nextTest(); +}; diff --git a/dom/events/test/bug426082.html b/dom/events/test/bug426082.html new file mode 100644 index 0000000000..b8bf5cb243 --- /dev/null +++ b/dom/events/test/bug426082.html @@ -0,0 +1,116 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=426082 +--> +<head> + <title>Test for Bug 426082</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=426082">Mozilla Bug 426082</a> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<p><input type="button" value="Button" id="button"></p> +<p><label for="button" id="label">Label</label></p> +<p id="outside">Something under the label</p> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 426082 **/ + +function runTests() { + SimpleTest.executeSoon(tests); +} + +SimpleTest.waitForFocus(runTests); + +function oneTick() { + return new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve))); +} + +function sendMouseEvent(t, elem) { + let r = elem.getBoundingClientRect(); + synthesizeMouse(elem, r.width / 2, r.height / 2, {type: t}); +} + +async function tests() { + let button = document.getElementById("button"); + let label = document.getElementById("label"); + let outside = document.getElementById("outside"); + + let is = window.opener.is; + let ok = window.opener.ok; + + // Press the label. + sendMouseEvent("mousemove", label); + sendMouseEvent("mousedown", label); + + await oneTick(); + + ok(label.matches(":hover"), "Label is hovered"); + ok(button.matches(":hover"), "Button should be hovered too"); + + ok(label.matches(":active"), "Label is active"); + ok(button.matches(":active"), "Button should be active too"); + + // Move the mouse down from the label. + sendMouseEvent("mousemove", outside); + + await oneTick(); + + ok(!label.matches(":hover"), "Label is no longer hovered"); + ok(!button.matches(":hover"), "Button should not be hovered too"); + + ok(label.matches(":active"), "Label is still active"); + ok(button.matches(":active"), "Button is still active too"); + + // And up again. + sendMouseEvent("mousemove", label); + + await oneTick(); + + + ok(label.matches(":hover"), "Label hovered again"); + ok(button.matches(":hover"), "Button be hovered again"); + ok(label.matches(":active"), "Label is still active"); + ok(button.matches(":active"), "Button is still active too"); + + // Release. + sendMouseEvent("mouseup", label); + + await oneTick(); + + ok(!label.matches(":active"), "Label is no longer active"); + ok(!button.matches(":active"), "Button is no longer active"); + + ok(label.matches(":hover"), "Label is still hovered"); + ok(button.matches(":hover"), "Button is still hovered"); + + // Press the label and remove it. + sendMouseEvent("mousemove", label); + sendMouseEvent("mousedown", label); + + await oneTick(); + + label.remove(); + + await oneTick(); + + ok(!label.matches(":active"), "Removing label should have unpressed it"); + ok(!label.matches(":focus"), "Removing label should have unpressed it"); + ok(!label.matches(":hover"), "Removing label should have unhovered it"); + ok(!button.matches(":active"), "Removing label should have unpressed the button"); + ok(!button.matches(":focus"), "Removing label should have unpressed the button"); + ok(!button.matches(":hover"), "Removing label should have unhovered the button"); + + sendMouseEvent("mouseup", label); + window.opener.finishTests(); +} +</script> +</pre> +</body> +</html> diff --git a/dom/events/test/bug545268.html b/dom/events/test/bug545268.html new file mode 100644 index 0000000000..1f90149a54 --- /dev/null +++ b/dom/events/test/bug545268.html @@ -0,0 +1 @@ +<iframe id='f' style='position:absolute; border:none; width:100%; height:100%; left:0; top:0' srcdoc='<input>'> diff --git a/dom/events/test/bug574663.html b/dom/events/test/bug574663.html new file mode 100644 index 0000000000..a3a5bd30ba --- /dev/null +++ b/dom/events/test/bug574663.html @@ -0,0 +1,3 @@ +<!DOCTYPE html> +<div id="scrollbox" style="height: 100px; overflow: auto;"> +<div style="height: 1000px;"></div></div> diff --git a/dom/events/test/bug591249_iframe.xhtml b/dom/events/test/bug591249_iframe.xhtml new file mode 100644 index 0000000000..7c7d7642b1 --- /dev/null +++ b/dom/events/test/bug591249_iframe.xhtml @@ -0,0 +1,33 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=591249 +--> +<window title="Mozilla Bug 591249" + xmlns:html="http://www.w3.org/1999/xhtml" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <html:style type="text/css"> + #drop-target { + width: 50px; + height: 50px; + border: 4px dotted black; + } + #drop-target { + background-color: red; + } + #drop-target:-moz-drag-over { + background-color: yellow; + } + </html:style> + + <html:body> + <html:h1 id="iframetext">Iframe for Bug 591249</html:h1> + + <html:div id="drop-target" + ondrop="return false;" + ondragenter="return false;" + ondragover="return false;"> + </html:div> + </html:body> +</window> diff --git a/dom/events/test/bug602962.xhtml b/dom/events/test/bug602962.xhtml new file mode 100644 index 0000000000..43affd6b60 --- /dev/null +++ b/dom/events/test/bug602962.xhtml @@ -0,0 +1,8 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> + +<window onload="window.opener.doTest()" xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <scrollbox id="page-scrollbox" style="border: 1px solid red; background-color: black;overflow: auto" flex="1"> + <box id="page-box" style="flex-shrink: 0; border: 1px solid green;"/> + </scrollbox> +</window> diff --git a/dom/events/test/bug607464.html b/dom/events/test/bug607464.html new file mode 100644 index 0000000000..55d1152623 --- /dev/null +++ b/dom/events/test/bug607464.html @@ -0,0 +1,3 @@ +<!DOCTYPE html> +<div id="scrollbox" style="height: 100px; overflow: auto;"> +<div style="height: 1000px;"></div> </div> diff --git a/dom/events/test/bug656379-1.html b/dom/events/test/bug656379-1.html new file mode 100644 index 0000000000..169d40a7c2 --- /dev/null +++ b/dom/events/test/bug656379-1.html @@ -0,0 +1,186 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=656379 +--> +<head> + <title>Test for Bug 656379</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script src="/tests/SimpleTest/WindowSnapshot.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <style> + canvas { + display: none; + } + input[type=button] { + appearance: none; + padding: 0; + border: none; + color: black; + background: white; + } + input[type=button]::-moz-focus-inner { border: none; } + + /* Make sure that normal, focused, hover+active, focused+hover+active + buttons all have different styles so that the test keeps moving along. */ + input[type=button]:hover:active { + background: red; + } + input[type=button]:focus { + background: green; + } + input[type=button]:focus:hover:active { + background: purple; + } + </style> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=656379">Mozilla Bug 656379</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> + + +var normalButtonCanvas, pressedButtonCanvas, normalFocusedButtonCanvas, + pressedFocusedButtonCanvas, currentSnapshot, button, label, outside; + +function runTests() { + button = $("button"); + label = $("label"); + outside = $("outside"); + SimpleTest.executeSoon(executeTests); +} + +SimpleTest.waitForFocus(runTests); + +function isRectContainedInRectFromRegion(rect, region) { + return Array.prototype.some.call(region, function (r) { + return rect.left >= r.left && + rect.top >= r.top && + rect.right <= r.right && + rect.bottom <= r.bottom; + }); +} + +function paintListener(e) { + if (isRectContainedInRectFromRegion(buttonRect(), SpecialPowers.wrap(e).clientRects)) { + gNeedsPaint = false; + currentSnapshot = takeSnapshot(); + } +} + +var gNeedsPaint = false; +function executeTests() { + var testYielder = tests(); + function execNext() { + if (!gNeedsPaint) { + let {done} = testYielder.next(); + if (done) { + return; + } + button.getBoundingClientRect(); // Flush. + gNeedsPaint = true; + } + SimpleTest.executeSoon(execNext); + } + execNext(); +} + +function* tests() { + window.addEventListener("MozAfterPaint", paintListener); + normalButtonCanvas = takeSnapshot(); + // Press the button. + sendMouseEvent("mousemove", button); + sendMouseEvent("mousedown", button); + yield undefined; + pressedFocusedButtonCanvas = takeSnapshot(); + compareSnapshots_(normalButtonCanvas, pressedFocusedButtonCanvas, false, "Pressed focused buttons should look different from normal buttons."); + // Release. + sendMouseEvent("mouseup", button); + yield undefined; + // make sure the button is focused as this doesn't happen on click on Mac + button.focus(); + normalFocusedButtonCanvas = takeSnapshot(); + compareSnapshots_(normalFocusedButtonCanvas, pressedFocusedButtonCanvas, false, "Pressed focused buttons should look different from normal focused buttons."); + // Unfocus the button. + sendMouseEvent("mousedown", outside); + sendMouseEvent("mouseup", outside); + yield undefined; + + // Press the label. + sendMouseEvent("mousemove", label); + sendMouseEvent("mousedown", label); + yield undefined; + compareSnapshots_(normalButtonCanvas, currentSnapshot, false, "Pressing the label should have pressed the button."); + pressedButtonCanvas = takeSnapshot(); + // Move the mouse down from the label. + sendMouseEvent("mousemove", outside); + yield undefined; + compareSnapshots_(normalButtonCanvas, currentSnapshot, true, "Moving the mouse down from the label should have unpressed the button."); + // ... and up again. + sendMouseEvent("mousemove", label); + yield undefined; + compareSnapshots_(pressedButtonCanvas, currentSnapshot, true, "Moving the mouse back on top of the label should have pressed the button."); + // Release. + sendMouseEvent("mouseup", label); + yield undefined; + var focusOnMouse = SpecialPowers.getIntPref("accessibility.mouse_focuses_formcontrol") > 0; + compareSnapshots_(focusOnMouse ? normalFocusedButtonCanvas : normalButtonCanvas, + currentSnapshot, true, "Releasing the mouse over the label should have unpressed" + + (focusOnMouse ? " (and focused)" : "") + " the button."); + // Press the label and remove it. + sendMouseEvent("mousemove", label); + sendMouseEvent("mousedown", label); + yield undefined; + label.remove(); + yield undefined; + compareSnapshots_(normalButtonCanvas, currentSnapshot, true, "Removing the label should have unpressed the button."); + sendMouseEvent("mouseup", label); + window.removeEventListener("MozAfterPaint", paintListener); + window.opener.finishTests(); + } + +function sendMouseEvent(t, elem) { + var r = elem.getBoundingClientRect(); + synthesizeMouse(elem, r.width / 2, r.height / 2, {type: t}); +} + +function compareSnapshots_(c1, c2, shouldBeIdentical, msg) { + var [correct, c1url, c2url] = compareSnapshots(c1, c2, shouldBeIdentical); + if (correct) { + if (shouldBeIdentical) { + window.opener.ok(true, msg + " - expected " + c1url); + } else { + window.opener.ok(true, msg + " - got " + c1url + " and " + c2url); + } + } else { + if (shouldBeIdentical) { + window.opener.ok(false, msg + " - expected " + c1url + " but got " + c2url); + } else { + window.opener.ok(false, msg + " - expected something other than " + c1url); + } + } +} + +function takeSnapshot(canvas) { + var r = buttonRect(); + adjustedRect = { left: r.left - 2, top: r.top - 2, + width: r.width + 4, height: r.height + 4 }; + return SpecialPowers.snapshotRect(window, adjustedRect); +} + +function buttonRect() { + return button.getBoundingClientRect(); +} +</script> +</pre> +<p><input type="button" value="Button" id="button"></p> +<p><label for="button" id="label">Label</label></p> +<p id="outside">Something under the label</p> + +</body> +</html> diff --git a/dom/events/test/chrome.ini b/dom/events/test/chrome.ini new file mode 100644 index 0000000000..6c3014ba1d --- /dev/null +++ b/dom/events/test/chrome.ini @@ -0,0 +1,31 @@ +[DEFAULT] +skip-if = os == 'android' +support-files = + bug415498-doc1.html + bug415498-doc2.html + bug418986-3.js + bug591249_iframe.xhtml + bug602962.xhtml + file_bug679494.html + window_bug617528.xhtml + window_bug1412775.xhtml + test_bug336682.js + +[test_bug336682_2.xhtml] +[test_bug415498.xhtml] +[test_bug418986-3.xhtml] +[test_bug524674.xhtml] +[test_bug547996-3.xhtml] +[test_bug586961.xhtml] +[test_bug591249.xhtml] +[test_bug602962.xhtml] +[test_bug617528.xhtml] +[test_bug679494.xhtml] +[test_bug930374-chrome.html] +[test_bug1128787-1.html] +[test_bug1128787-2.html] +[test_bug1128787-3.html] +[test_bug1412775.xhtml] +[test_eventctors.xhtml] +[test_DataTransferItemList.html] +skip-if = !debug && (os == "linux") #Bug 1421150 diff --git a/dom/events/test/clipboard/browser.ini b/dom/events/test/clipboard/browser.ini new file mode 100644 index 0000000000..eb452c1443 --- /dev/null +++ b/dom/events/test/clipboard/browser.ini @@ -0,0 +1,21 @@ +[DEFAULT] +support-files = + head.js + !/gfx/layers/apz/test/mochitest/apz_test_native_event_utils.js + !/gfx/layers/apz/test/mochitest/apz_test_utils.js + +[browser_navigator_clipboard_readText.js] +support-files = + simple_navigator_clipboard_readText.html +[browser_navigator_clipboard_read.js] +support-files = + simple_navigator_clipboard_read.html +[browser_navigator_clipboard_touch.js] +support-files = + simple_navigator_clipboard_readText.html +[browser_navigator_clipboard_clickjacking.js] +skip-if = + os == "win" # The popupmenus dismiss when access keys for disabled items are pressed on windows + os == "mac" && verify +support-files = + simple_navigator_clipboard_keydown.html diff --git a/dom/events/test/clipboard/browser_navigator_clipboard_clickjacking.js b/dom/events/test/clipboard/browser_navigator_clipboard_clickjacking.js new file mode 100644 index 0000000000..cd3e97f274 --- /dev/null +++ b/dom/events/test/clipboard/browser_navigator_clipboard_clickjacking.js @@ -0,0 +1,69 @@ +/* -*- Mode: JavaScript; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +"use strict"; + +const kBaseUrlForContent = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.com" +); + +const kContentFileName = "simple_navigator_clipboard_keydown.html"; + +const kContentFileUrl = kBaseUrlForContent + kContentFileName; + +const kApzTestNativeEventUtilsUrl = + "chrome://mochitests/content/browser/gfx/layers/apz/test/mochitest/apz_test_native_event_utils.js"; + +Services.scriptloader.loadSubScript(kApzTestNativeEventUtilsUrl, this); + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [["dom.events.asyncClipboard.readText", true]], + }); +}); + +add_task(async function test_paste_button_clickjacking() { + await BrowserTestUtils.withNewTab(kContentFileUrl, async function (browser) { + const pasteButtonIsShown = promisePasteButtonIsShown(); + + // synthesize key to trigger readText() to bring up paste popup. + EventUtils.synthesizeKey("p", {}, window); + await waitForPasteMenuPopupEvent("shown"); + + const pastePopup = document.getElementById(kPasteMenuPopupId); + const pasteButton = document.getElementById(kPasteMenuItemId); + ok( + pasteButton.disabled, + "Paste button should be shown with disabled by default" + ); + + let accesskey = pasteButton.getAttribute("accesskey"); + let delay = Services.prefs.getIntPref("security.dialog_enable_delay") * 3; + while (delay > 0) { + // There's no other way to allow some time to pass and ensure we're + // genuinely testing that these keypresses postpone the enabling of + // the paste button, so disable this check for this line: + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(r => setTimeout(r, 100)); + ok(pasteButton.disabled, "Paste button should still be disabled"); + EventUtils.synthesizeKey(accesskey, {}, window); + is(pastePopup.state, "open", "Paste popup should still be opened"); + delay = delay - 100; + } + + await BrowserTestUtils.waitForMutationCondition( + pasteButton, + { attributeFilter: ["disabled"] }, + () => !pasteButton.disabled, + "Wait for paste button enabled" + ); + + const pasteButtonIsHidden = promisePasteButtonIsHidden(); + EventUtils.synthesizeKey(accesskey, {}, window); + await pasteButtonIsHidden; + }); +}); diff --git a/dom/events/test/clipboard/browser_navigator_clipboard_read.js b/dom/events/test/clipboard/browser_navigator_clipboard_read.js new file mode 100644 index 0000000000..5b3af1d116 --- /dev/null +++ b/dom/events/test/clipboard/browser_navigator_clipboard_read.js @@ -0,0 +1,198 @@ +/* -*- Mode: JavaScript; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +"use strict"; + +const kBaseUrlForContent = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.com" +); + +const kContentFileName = "simple_navigator_clipboard_read.html"; + +const kContentFileUrl = kBaseUrlForContent + kContentFileName; + +const kApzTestNativeEventUtilsUrl = + "chrome://mochitests/content/browser/gfx/layers/apz/test/mochitest/apz_test_native_event_utils.js"; + +Services.scriptloader.loadSubScript(kApzTestNativeEventUtilsUrl, this); + +// @param aBrowser browser object of the content tab. +// @param aMultipleReadTextCalls if false, exactly one call is made, two +// otherwise. +function promiseClickContentToTriggerClipboardRead( + aBrowser, + aMultipleReadTextCalls +) { + return promiseClickContentElement( + aBrowser, + aMultipleReadTextCalls ? "invokeReadTwiceId" : "invokeReadOnceId" + ); +} + +// @param aBrowser browser object of the content tab. +function promiseMutatedReadResultFromContentElement(aBrowser) { + return promiseMutatedTextContentFromContentElement(aBrowser, "readResultId"); +} + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.events.asyncClipboard.clipboardItem", true], + ["test.events.async.enabled", true], + ], + }); +}); + +add_task(async function test_paste_button_position() { + // Ensure there's text on the clipboard. + await promiseWritingRandomTextToClipboard(); + + await BrowserTestUtils.withNewTab(kContentFileUrl, async function (browser) { + const pasteButtonIsShown = promisePasteButtonIsShown(); + const coordsOfClickInContentRelativeToScreenInDevicePixels = + await promiseClickContentToTriggerClipboardRead(browser, false); + info( + "coordsOfClickInContentRelativeToScreenInDevicePixels: " + + coordsOfClickInContentRelativeToScreenInDevicePixels.x + + ", " + + coordsOfClickInContentRelativeToScreenInDevicePixels.y + ); + + const pasteButtonCoordsRelativeToScreenInDevicePixels = + await pasteButtonIsShown; + info( + "pasteButtonCoordsRelativeToScreenInDevicePixels: " + + pasteButtonCoordsRelativeToScreenInDevicePixels.x + + ", " + + pasteButtonCoordsRelativeToScreenInDevicePixels.y + ); + + const mouseCoordsRelativeToScreenInDevicePixels = + getMouseCoordsRelativeToScreenInDevicePixels(); + info( + "mouseCoordsRelativeToScreenInDevicePixels: " + + mouseCoordsRelativeToScreenInDevicePixels.x + + ", " + + mouseCoordsRelativeToScreenInDevicePixels.y + ); + + // Asserting not overlapping is important; otherwise, when the + // "Paste" button is shown via a `mousedown` event, the following + // `mouseup` event could accept the "Paste" button unnoticed by the + // user. + ok( + isCloselyLeftOnTopOf( + mouseCoordsRelativeToScreenInDevicePixels, + pasteButtonCoordsRelativeToScreenInDevicePixels + ), + "'Paste' button is closely left on top of the mouse pointer." + ); + ok( + isCloselyLeftOnTopOf( + coordsOfClickInContentRelativeToScreenInDevicePixels, + pasteButtonCoordsRelativeToScreenInDevicePixels + ), + "Coords of click in content are closely left on top of the 'Paste' button." + ); + + // To avoid disturbing subsequent tests. + const pasteButtonIsHidden = promisePasteButtonIsHidden(); + await promiseClickPasteButton(); + await pasteButtonIsHidden; + }); +}); + +add_task(async function test_accepting_paste_button() { + // Randomized text to avoid overlappings with other tests. + const clipboardText = await promiseWritingRandomTextToClipboard(); + + await BrowserTestUtils.withNewTab(kContentFileUrl, async function (browser) { + const pasteButtonIsShown = promisePasteButtonIsShown(); + await promiseClickContentToTriggerClipboardRead(browser, false); + await pasteButtonIsShown; + const pasteButtonIsHidden = promisePasteButtonIsHidden(); + const mutatedReadResultFromContentElement = + promiseMutatedReadResultFromContentElement(browser); + await promiseClickPasteButton(); + await pasteButtonIsHidden; + await mutatedReadResultFromContentElement.then(value => { + is( + value, + "Resolved: " + clipboardText, + "Text returned from `navigator.clipboard.read()` is as expected." + ); + }); + }); +}); + +add_task(async function test_dismissing_paste_button() { + await BrowserTestUtils.withNewTab(kContentFileUrl, async function (browser) { + const pasteButtonIsShown = promisePasteButtonIsShown(); + await promiseClickContentToTriggerClipboardRead(browser, false); + await pasteButtonIsShown; + const pasteButtonIsHidden = promisePasteButtonIsHidden(); + const mutatedReadResultFromContentElement = + promiseMutatedReadResultFromContentElement(browser); + await promiseDismissPasteButton(); + await pasteButtonIsHidden; + await mutatedReadResultFromContentElement.then(value => { + is( + value, + "Rejected: The user dismissed the 'Paste' button.", + "`navigator.clipboard.read()` rejected after dismissing the 'Paste' button" + ); + }); + }); +}); + +add_task( + async function test_multiple_read_invocations_for_same_user_activation() { + // Randomized text to avoid overlappings with other tests. + const clipboardText = await promiseWritingRandomTextToClipboard(); + + await BrowserTestUtils.withNewTab( + kContentFileUrl, + async function (browser) { + const pasteButtonIsShown = promisePasteButtonIsShown(); + await promiseClickContentToTriggerClipboardRead(browser, true); + await pasteButtonIsShown; + const mutatedReadResultFromContentElement = + promiseMutatedReadResultFromContentElement(browser); + const pasteButtonIsHidden = promisePasteButtonIsHidden(); + await promiseClickPasteButton(); + await mutatedReadResultFromContentElement.then(value => { + is( + value, + "Resolved 1: " + clipboardText + "; Resolved 2: " + clipboardText, + "Two calls of `navigator.clipboard.read()` both resolved with the expected text." + ); + }); + + // To avoid disturbing subsequent tests. + await pasteButtonIsHidden; + } + ); + } +); + +add_task(async function test_new_user_activation_shows_paste_button_again() { + await BrowserTestUtils.withNewTab(kContentFileUrl, async function (browser) { + // Ensure there's text on the clipboard. + await promiseWritingRandomTextToClipboard(); + + for (let i = 0; i < 2; ++i) { + const pasteButtonIsShown = promisePasteButtonIsShown(); + // A click initiates a new user activation. + await promiseClickContentToTriggerClipboardRead(browser, false); + await pasteButtonIsShown; + + const pasteButtonIsHidden = promisePasteButtonIsHidden(); + await promiseClickPasteButton(); + await pasteButtonIsHidden; + } + }); +}); diff --git a/dom/events/test/clipboard/browser_navigator_clipboard_readText.js b/dom/events/test/clipboard/browser_navigator_clipboard_readText.js new file mode 100644 index 0000000000..7563fa8a21 --- /dev/null +++ b/dom/events/test/clipboard/browser_navigator_clipboard_readText.js @@ -0,0 +1,201 @@ +/* -*- Mode: JavaScript; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +"use strict"; + +const kBaseUrlForContent = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.com" +); + +const kContentFileName = "simple_navigator_clipboard_readText.html"; + +const kContentFileUrl = kBaseUrlForContent + kContentFileName; + +const kApzTestNativeEventUtilsUrl = + "chrome://mochitests/content/browser/gfx/layers/apz/test/mochitest/apz_test_native_event_utils.js"; + +Services.scriptloader.loadSubScript(kApzTestNativeEventUtilsUrl, this); + +// @param aBrowser browser object of the content tab. +// @param aMultipleReadTextCalls if false, exactly one call is made, two +// otherwise. +function promiseClickContentToTriggerClipboardReadText( + aBrowser, + aMultipleReadTextCalls +) { + return promiseClickContentElement( + aBrowser, + aMultipleReadTextCalls ? "invokeReadTextTwiceId" : "invokeReadTextOnceId" + ); +} + +// @param aBrowser browser object of the content tab. +function promiseMutatedReadTextResultFromContentElement(aBrowser) { + return promiseMutatedTextContentFromContentElement( + aBrowser, + "readTextResultId" + ); +} + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.events.asyncClipboard.readText", true], + ["test.events.async.enabled", true], + ], + }); +}); + +add_task(async function test_paste_button_position() { + // Ensure there's text on the clipboard. + await promiseWritingRandomTextToClipboard(); + + await BrowserTestUtils.withNewTab(kContentFileUrl, async function (browser) { + const pasteButtonIsShown = promisePasteButtonIsShown(); + const coordsOfClickInContentRelativeToScreenInDevicePixels = + await promiseClickContentToTriggerClipboardReadText(browser, false); + info( + "coordsOfClickInContentRelativeToScreenInDevicePixels: " + + coordsOfClickInContentRelativeToScreenInDevicePixels.x + + ", " + + coordsOfClickInContentRelativeToScreenInDevicePixels.y + ); + + const pasteButtonCoordsRelativeToScreenInDevicePixels = + await pasteButtonIsShown; + info( + "pasteButtonCoordsRelativeToScreenInDevicePixels: " + + pasteButtonCoordsRelativeToScreenInDevicePixels.x + + ", " + + pasteButtonCoordsRelativeToScreenInDevicePixels.y + ); + + const mouseCoordsRelativeToScreenInDevicePixels = + getMouseCoordsRelativeToScreenInDevicePixels(); + info( + "mouseCoordsRelativeToScreenInDevicePixels: " + + mouseCoordsRelativeToScreenInDevicePixels.x + + ", " + + mouseCoordsRelativeToScreenInDevicePixels.y + ); + + // Asserting not overlapping is important; otherwise, when the + // "Paste" button is shown via a `mousedown` event, the following + // `mouseup` event could accept the "Paste" button unnoticed by the + // user. + ok( + isCloselyLeftOnTopOf( + mouseCoordsRelativeToScreenInDevicePixels, + pasteButtonCoordsRelativeToScreenInDevicePixels + ), + "'Paste' button is closely left on top of the mouse pointer." + ); + ok( + isCloselyLeftOnTopOf( + coordsOfClickInContentRelativeToScreenInDevicePixels, + pasteButtonCoordsRelativeToScreenInDevicePixels + ), + "Coords of click in content are closely left on top of the 'Paste' button." + ); + + // To avoid disturbing subsequent tests. + const pasteButtonIsHidden = promisePasteButtonIsHidden(); + await promiseClickPasteButton(); + await pasteButtonIsHidden; + }); +}); + +add_task(async function test_accepting_paste_button() { + // Randomized text to avoid overlappings with other tests. + const clipboardText = await promiseWritingRandomTextToClipboard(); + + await BrowserTestUtils.withNewTab(kContentFileUrl, async function (browser) { + const pasteButtonIsShown = promisePasteButtonIsShown(); + await promiseClickContentToTriggerClipboardReadText(browser, false); + await pasteButtonIsShown; + const pasteButtonIsHidden = promisePasteButtonIsHidden(); + const mutatedReadTextResultFromContentElement = + promiseMutatedReadTextResultFromContentElement(browser); + await promiseClickPasteButton(); + await pasteButtonIsHidden; + await mutatedReadTextResultFromContentElement.then(value => { + is( + value, + "Resolved: " + clipboardText, + "Text returned from `navigator.clipboard.readText()` is as expected." + ); + }); + }); +}); + +add_task(async function test_dismissing_paste_button() { + await BrowserTestUtils.withNewTab(kContentFileUrl, async function (browser) { + const pasteButtonIsShown = promisePasteButtonIsShown(); + await promiseClickContentToTriggerClipboardReadText(browser, false); + await pasteButtonIsShown; + const pasteButtonIsHidden = promisePasteButtonIsHidden(); + const mutatedReadTextResultFromContentElement = + promiseMutatedReadTextResultFromContentElement(browser); + await promiseDismissPasteButton(); + await pasteButtonIsHidden; + await mutatedReadTextResultFromContentElement.then(value => { + is( + value, + "Rejected.", + "`navigator.clipboard.readText()` rejected after dismissing the 'Paste' button" + ); + }); + }); +}); + +add_task( + async function test_multiple_readText_invocations_for_same_user_activation() { + // Randomized text to avoid overlappings with other tests. + const clipboardText = await promiseWritingRandomTextToClipboard(); + + await BrowserTestUtils.withNewTab( + kContentFileUrl, + async function (browser) { + const pasteButtonIsShown = promisePasteButtonIsShown(); + await promiseClickContentToTriggerClipboardReadText(browser, true); + await pasteButtonIsShown; + const mutatedReadTextResultFromContentElement = + promiseMutatedReadTextResultFromContentElement(browser); + const pasteButtonIsHidden = promisePasteButtonIsHidden(); + await promiseClickPasteButton(); + await mutatedReadTextResultFromContentElement.then(value => { + is( + value, + "Resolved 1: " + clipboardText + "; Resolved 2: " + clipboardText, + "Two calls of `navigator.clipboard.read()` both resolved with the expected text." + ); + }); + + // To avoid disturbing subsequent tests. + await pasteButtonIsHidden; + } + ); + } +); + +add_task(async function test_new_user_activation_shows_paste_button_again() { + await BrowserTestUtils.withNewTab(kContentFileUrl, async function (browser) { + // Ensure there's text on the clipboard. + await promiseWritingRandomTextToClipboard(); + + for (let i = 0; i < 2; ++i) { + const pasteButtonIsShown = promisePasteButtonIsShown(); + // A click initiates a new user activation. + await promiseClickContentToTriggerClipboardReadText(browser, false); + await pasteButtonIsShown; + + const pasteButtonIsHidden = promisePasteButtonIsHidden(); + await promiseClickPasteButton(); + await pasteButtonIsHidden; + } + }); +}); diff --git a/dom/events/test/clipboard/browser_navigator_clipboard_touch.js b/dom/events/test/clipboard/browser_navigator_clipboard_touch.js new file mode 100644 index 0000000000..aeea9a612d --- /dev/null +++ b/dom/events/test/clipboard/browser_navigator_clipboard_touch.js @@ -0,0 +1,114 @@ +/* -*- Mode: JavaScript; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +"use strict"; + +const kBaseUrlForContent = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + "https://example.com" +); +const kContentFileUrl = + kBaseUrlForContent + "simple_navigator_clipboard_readText.html"; + +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/gfx/layers/apz/test/mochitest/apz_test_native_event_utils.js", + this +); + +// @param aBrowser browser object of the content tab. +// @param aContentElementId the ID of the element to be tapped. +function promiseTouchTapContent(aBrowser, aContentElementId) { + return SpecialPowers.spawn( + aBrowser, + [aContentElementId], + async _contentElementId => { + await content.wrappedJSObject.waitUntilApzStable(); + + const contentElement = content.document.getElementById(_contentElementId); + let promise = new Promise(resolve => { + contentElement.addEventListener( + "click", + function (e) { + resolve({ x: e.screenX, y: e.screenY }); + }, + { once: true } + ); + }); + + EventUtils.synthesizeTouchAtCenter(contentElement, {}, content.window); + + return promise; + } + ); +} + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.events.asyncClipboard.readText", true], + ["test.events.async.enabled", true], + ], + }); +}); + +add_task(async function test_paste_button_position_touch() { + // Ensure there's text on the clipboard. + await promiseWritingRandomTextToClipboard(); + + await BrowserTestUtils.withNewTab(kContentFileUrl, async function (browser) { + const pasteButtonIsShown = promisePasteButtonIsShown(); + const coordsOfClickInContentRelativeToScreenInDevicePixels = + await promiseTouchTapContent(browser, "invokeReadTextOnceId"); + info( + "coordsOfClickInContentRelativeToScreenInDevicePixels: " + + coordsOfClickInContentRelativeToScreenInDevicePixels.x + + ", " + + coordsOfClickInContentRelativeToScreenInDevicePixels.y + ); + + const pasteButtonCoordsRelativeToScreenInDevicePixels = + await pasteButtonIsShown; + info( + "pasteButtonCoordsRelativeToScreenInDevicePixels: " + + pasteButtonCoordsRelativeToScreenInDevicePixels.x + + ", " + + pasteButtonCoordsRelativeToScreenInDevicePixels.y + ); + + const mouseCoordsRelativeToScreenInDevicePixels = + getMouseCoordsRelativeToScreenInDevicePixels(); + info( + "mouseCoordsRelativeToScreenInDevicePixels: " + + mouseCoordsRelativeToScreenInDevicePixels.x + + ", " + + mouseCoordsRelativeToScreenInDevicePixels.y + ); + + // Asserting not overlapping is important; otherwise, when the + // "Paste" button is shown via a `mousedown` event, the following + // `mouseup` event could accept the "Paste" button unnoticed by the + // user. + ok( + isCloselyLeftOnTopOf( + mouseCoordsRelativeToScreenInDevicePixels, + pasteButtonCoordsRelativeToScreenInDevicePixels + ), + "'Paste' button is closely left on top of the mouse pointer." + ); + ok( + isCloselyLeftOnTopOf( + coordsOfClickInContentRelativeToScreenInDevicePixels, + pasteButtonCoordsRelativeToScreenInDevicePixels + ), + "Coords of click in content are closely left on top of the 'Paste' button." + ); + + // To avoid disturbing subsequent tests. + const pasteButtonIsHidden = promisePasteButtonIsHidden(); + await promiseClickPasteButton(); + await pasteButtonIsHidden; + }); +}); diff --git a/dom/events/test/clipboard/chrome.ini b/dom/events/test/clipboard/chrome.ini new file mode 100644 index 0000000000..ce646f98ef --- /dev/null +++ b/dom/events/test/clipboard/chrome.ini @@ -0,0 +1,3 @@ +[DEFAULT] + +[test_async_clipboard.xhtml] diff --git a/dom/events/test/clipboard/head.js b/dom/events/test/clipboard/head.js new file mode 100644 index 0000000000..0db1e0abf4 --- /dev/null +++ b/dom/events/test/clipboard/head.js @@ -0,0 +1,169 @@ +/* -*- Mode: JavaScript; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const kPasteMenuPopupId = "clipboardReadPasteMenuPopup"; +const kPasteMenuItemId = "clipboardReadPasteMenuItem"; + +function promiseWritingRandomTextToClipboard() { + const clipboardText = "X" + Math.random(); + return navigator.clipboard.writeText(clipboardText).then(() => { + return clipboardText; + }); +} + +function promiseBrowserReflow() { + return new Promise(resolve => + requestAnimationFrame(() => requestAnimationFrame(resolve)) + ); +} + +function waitForPasteMenuPopupEvent(aEventSuffix) { + // The element with id `kPasteMenuPopupId` is inserted dynamically, hence + // calling `BrowserTestUtils.waitForEvent` instead of + // `BrowserTestUtils.waitForPopupEvent`. + return BrowserTestUtils.waitForEvent( + document, + "popup" + aEventSuffix, + false /* capture */, + e => { + return e.target.getAttribute("id") == kPasteMenuPopupId; + } + ); +} + +function promisePasteButtonIsShown() { + return waitForPasteMenuPopupEvent("shown").then(async () => { + ok(true, "Witnessed 'popupshown' event for 'Paste' button."); + + const pasteButton = document.getElementById(kPasteMenuItemId); + ok( + pasteButton.disabled, + "Paste button should be shown with disabled by default" + ); + await BrowserTestUtils.waitForMutationCondition( + pasteButton, + { attributeFilter: ["disabled"] }, + () => !pasteButton.disabled, + "Wait for paste button enabled" + ); + + return promiseBrowserReflow().then(() => { + return coordinatesRelativeToScreen({ + target: pasteButton, + offsetX: 0, + offsetY: 0, + }); + }); + }); +} + +function promisePasteButtonIsHidden() { + return waitForPasteMenuPopupEvent("hidden").then(() => { + ok(true, "Witnessed 'popuphidden' event for 'Paste' button."); + return promiseBrowserReflow(); + }); +} + +function promiseClickPasteButton() { + const pasteButton = document.getElementById(kPasteMenuItemId); + let promise = BrowserTestUtils.waitForEvent(pasteButton, "click"); + EventUtils.synthesizeMouseAtCenter(pasteButton, {}); + return promise; +} + +function getMouseCoordsRelativeToScreenInDevicePixels() { + let mouseXInCSSPixels = {}; + let mouseYInCSSPixels = {}; + window.windowUtils.getLastOverWindowPointerLocationInCSSPixels( + mouseXInCSSPixels, + mouseYInCSSPixels + ); + + return { + x: + (mouseXInCSSPixels.value + window.mozInnerScreenX) * + window.devicePixelRatio, + y: + (mouseYInCSSPixels.value + window.mozInnerScreenY) * + window.devicePixelRatio, + }; +} + +function isCloselyLeftOnTopOf(aCoordsP1, aCoordsP2, aDelta = 10) { + return ( + Math.abs(aCoordsP2.x - aCoordsP1.x) < aDelta && + Math.abs(aCoordsP2.y - aCoordsP1.y) < aDelta + ); +} + +function promiseDismissPasteButton() { + // nsXULPopupManager rollup is handled in widget code, so we have to + // synthesize native mouse events. + return EventUtils.promiseNativeMouseEvent({ + type: "click", + target: document.body, + // Relies on the assumption that the center of chrome document doesn't + // overlay with the paste button showed for clipboard readText request. + atCenter: true, + }); +} + +// @param aBrowser browser object of the content tab. +// @param aContentElementId the ID of the element to be clicked. +function promiseClickContentElement(aBrowser, aContentElementId) { + return SpecialPowers.spawn( + aBrowser, + [aContentElementId], + async _contentElementId => { + const contentElement = content.document.getElementById(_contentElementId); + let promise = new Promise(resolve => { + contentElement.addEventListener( + "click", + function (e) { + resolve({ x: e.screenX, y: e.screenY }); + }, + { once: true } + ); + }); + + EventUtils.synthesizeMouseAtCenter(contentElement, {}, content.window); + + return promise; + } + ); +} + +// @param aBrowser browser object of the content tab. +// @param aContentElementId the ID of the element to observe. +function promiseMutatedTextContentFromContentElement( + aBrowser, + aContentElementId +) { + return SpecialPowers.spawn( + aBrowser, + [aContentElementId], + async _contentElementId => { + const contentElement = content.document.getElementById(_contentElementId); + + const promiseTextContentResult = new Promise(resolve => { + const mutationObserver = new content.MutationObserver( + (aMutationRecord, aMutationObserver) => { + info("Observed mutation."); + aMutationObserver.disconnect(); + resolve(contentElement.textContent); + } + ); + + mutationObserver.observe(contentElement, { + childList: true, + }); + }); + + return await promiseTextContentResult; + } + ); +} diff --git a/dom/events/test/clipboard/simple_navigator_clipboard_keydown.html b/dom/events/test/clipboard/simple_navigator_clipboard_keydown.html new file mode 100644 index 0000000000..15fcdfba2c --- /dev/null +++ b/dom/events/test/clipboard/simple_navigator_clipboard_keydown.html @@ -0,0 +1,15 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="utf-8"> + <script> + function onLoad() { + document.addEventListener("keydown", function() { + navigator.clipboard.readText(); + }); + } + </script> + </head> + <body onload="onLoad()"> + </body> +</html> diff --git a/dom/events/test/clipboard/simple_navigator_clipboard_read.html b/dom/events/test/clipboard/simple_navigator_clipboard_read.html new file mode 100644 index 0000000000..89f38a3240 --- /dev/null +++ b/dom/events/test/clipboard/simple_navigator_clipboard_read.html @@ -0,0 +1,65 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="utf-8"> + <!-- Required by the .js part of the test. In a more ideal world, the script + could be loaded in the .js part; however, currently, that causes other + problems, which would require other changes in test framework code. --> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/paint_listener.js"></script> + <script src="/tests/gfx/layers/apz/test/mochitest/apz_test_native_event_utils.js"></script> + <script src="/tests/gfx/layers/apz/test/mochitest/apz_test_utils.js"></script> + + <script> + function onLoad() { + const readResult = document.getElementById("readResultId"); + + async function getClipboardText() { + let items = await navigator.clipboard.read(); + if (items.length != 1) { + throw Error(`incorrect number of clipboard item (${items.length})`); + return; + } + + let item = items[0]; + for (let type of item.types) { + if (type == "text/plain") { + let blob = await item.getType(type); + return await blob.text(); + } + } + + throw Error("no text/plain type"); + } + + const b1 = document.getElementById("invokeReadOnceId"); + b1.addEventListener("click", async () => { + getClipboardText().then(text => { + readResult.textContent = `Resolved: ${text}`; + }, (e) => { readResult.textContent = `Rejected: ${e.message}`}); + }); + + const b2 = document.getElementById("invokeReadTwiceId"); + b2.addEventListener("click", async () => { + const t1 = getClipboardText(); + const t2 = getClipboardText(); + + const r1 = await t1.then(text => { + return `Resolved 1: ${text}`; + }, (e) => { return `Rejected 1: ${e.message}`;}); + + const r2 = await t2.then(text => { + return "Resolved 2: " + text; + }, (e) => { return `Rejected 2: ${e.message}`;}); + + readResult.textContent = r1 + "; " + r2; + }); + } + </script> + </head> + <body onload="onLoad()"> + <button id="invokeReadOnceId">1</button> + <button id="invokeReadTwiceId">2</button> + <div id="readResultId"/> + </body> +</html> diff --git a/dom/events/test/clipboard/simple_navigator_clipboard_readText.html b/dom/events/test/clipboard/simple_navigator_clipboard_readText.html new file mode 100644 index 0000000000..0b85371091 --- /dev/null +++ b/dom/events/test/clipboard/simple_navigator_clipboard_readText.html @@ -0,0 +1,47 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="utf-8"> + <!-- Required by the .js part of the test. In a more ideal world, the script + could be loaded in the .js part; however, currently, that causes other + problems, which would require other changes in test framework code. --> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/paint_listener.js"></script> + <script src="/tests/gfx/layers/apz/test/mochitest/apz_test_native_event_utils.js"></script> + <script src="/tests/gfx/layers/apz/test/mochitest/apz_test_utils.js"></script> + + <script> + function onLoad() { + const readTextResult = document.getElementById("readTextResultId"); + + const b1 = document.getElementById("invokeReadTextOnceId"); + b1.addEventListener("click", async () => { + navigator.clipboard.readText().then(text => { + readTextResult.textContent = "Resolved: " + text; + }, () => { readTextResult.textContent = "Rejected." }); + }); + + const b2 = document.getElementById("invokeReadTextTwiceId"); + b2.addEventListener("click", async () => { + const t1 = navigator.clipboard.readText(); + const t2 = navigator.clipboard.readText(); + + const r1 = await t1.then(text => { + return "Resolved 1: " + text; + }, () => { return "Rejected: 1";}); + + const r2 = await t2.then(text => { + return "Resolved 2: " + text; + }, () => { return "Rejected: 2";}); + + readTextResult.textContent = r1 + "; " + r2; + }); + } + </script> + </head> + <body onload="onLoad()"> + <button id="invokeReadTextOnceId">1</button> + <button id="invokeReadTextTwiceId">2</button> + <div id="readTextResultId"/> + </body> +</html> diff --git a/dom/events/test/clipboard/test_async_clipboard.xhtml b/dom/events/test/clipboard/test_async_clipboard.xhtml new file mode 100644 index 0000000000..787821efc3 --- /dev/null +++ b/dom/events/test/clipboard/test_async_clipboard.xhtml @@ -0,0 +1,130 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> + +<window title="Async clipboard APIs Test" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="runTest();"> + + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + + SimpleTest.waitForExplicitFinish(); + + const Services = SpecialPowers.Services; + const { AppConstants } = SpecialPowers.ChromeUtils.import( + "resource://gre/modules/AppConstants.jsm" + ); + const { PlacesUtils } = SpecialPowers.ChromeUtils.import( + "resource://gre/modules/PlacesUtils.jsm" + ); + + const kTextPlainMimeType = "text/plain"; + + function clearClipboard() { + Services.clipboard.emptyClipboard(Services.clipboard.kGlobalClipboard); + } + + async function testRead() { + let expected = "x"; + await SimpleTest.promiseClipboardChange(expected, () => { + SpecialPowers.clipboardCopyString(expected); + }, kTextPlainMimeType); + let items = await navigator.clipboard.read(); + is(items.length, 1, "read() read exactly one item"); + const actual = await items[0].getType(kTextPlainMimeType).then(blob => blob.text()); + is(actual, expected, "read() read the right thing"); + } + + async function testWrite() { + await SimpleTest.promiseClipboardChange("", () => { + clearClipboard(); + }); + + let expected = "x"; + // eslint-disable-next-line no-undef + let item = new ClipboardItem({[kTextPlainMimeType]: expected}); + await navigator.clipboard.write([item]); + let actual = SpecialPowers.getClipboardData(kTextPlainMimeType); + is(actual, expected, "write() wrote the right thing"); + } + + async function testReadText() { + let expected = "x"; + await SimpleTest.promiseClipboardChange(expected, () => { + SpecialPowers.clipboardCopyString(expected); + }, kTextPlainMimeType); + let actual = await navigator.clipboard.readText(); + is(actual, expected, "readText() read the right thing"); + } + + async function testWriteText() { + await SimpleTest.promiseClipboardChange("", () => { + clearClipboard(); + }); + + let expected = "x"; + await navigator.clipboard.writeText(expected); + let actual = SpecialPowers.getClipboardData(kTextPlainMimeType); + is(actual, expected, "writeText() wrote the right thing"); + } + + async function testNoContentsRead() { + await SimpleTest.promiseClipboardChange("", () => { + clearClipboard(); + }); + + const items = await navigator.clipboard.read(); + + // Bug 1756955: at least on Ubuntu 20.04, clearing the clipboard leads to + // one item with no types. + if (!items.length || + (items.length == 1 && !items[0].types.length)) { + ok(true, "read() read the right thing from empty clipboard"); + } else { + ok(false, "read() read the wrong thing from empty clipboard"); + } + } + + async function testNoContentsReadText() { + await SimpleTest.promiseClipboardChange("", () => { + clearClipboard(); + }); + let actual = await navigator.clipboard.readText(); + is(actual, "", "readText() read the right thing from empty clipboard"); + } + + function runTest() { + (async function() { + await SpecialPowers.pushPrefEnv({"set": [ + ["dom.events.asyncClipboard.clipboardItem", true], + ]}); + await testRead(); + await testReadText(); + await testWrite(); + await testWriteText(); + + await testNoContentsRead(); + await testNoContentsReadText(); + + SimpleTest.finish(); + })(); + } + ]]> +</script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<p id="display"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> + +</window> diff --git a/dom/events/test/dragimage.html b/dom/events/test/dragimage.html new file mode 100644 index 0000000000..059b1de90f --- /dev/null +++ b/dom/events/test/dragimage.html @@ -0,0 +1,10 @@ +<html> +<head> +<title>Drag Image Test</title> +</head> +<body> +<img src="green.png" width=100 height=100> +<div id='over' style='width: 100px; height: 100px; border: 1px solid black;' + ondragover="event.preventDefault();"></div> +</body> +</html> diff --git a/dom/events/test/empty.js b/dom/events/test/empty.js new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/dom/events/test/empty.js diff --git a/dom/events/test/error_event_worker.js b/dom/events/test/error_event_worker.js new file mode 100644 index 0000000000..13422c6974 --- /dev/null +++ b/dom/events/test/error_event_worker.js @@ -0,0 +1,19 @@ +addEventListener("error", function (e) { + var obj = {}; + for (var prop of ["message", "filename", "lineno"]) { + obj[prop] = e[prop]; + } + obj.type = "event"; + postMessage(obj); +}); +onerror = function (message, filename, lineno) { + var obj = { + message, + filename, + lineno, + type: "callback", + }; + postMessage(obj); + return false; +}; +throw new Error("workerhello"); diff --git a/dom/events/test/event_leak_utils.js b/dom/events/test/event_leak_utils.js new file mode 100644 index 0000000000..86b26a4136 --- /dev/null +++ b/dom/events/test/event_leak_utils.js @@ -0,0 +1,84 @@ +// Any copyright is dedicated to the Public Domain. +// http://creativecommons.org/publicdomain/zero/1.0/ +"use strict"; + +// This function runs a number of tests where: +// +// 1. An iframe is created +// 2. The target callback is executed with the iframe's contentWindow as +// an argument. +// 3. The iframe is destroyed and GC is forced. +// 4. Verifies that the iframe's contentWindow has been GC'd. +// +// Different ways of destroying the iframe are checked. Simple +// remove(), destruction via bfcache, or replacement by document.open(). +// +// Please pass a target callback that exercises the API under +// test using the given window. The callback should try to leave the +// API active to increase the liklihood of provoking an API. Any activity +// should be canceled by the destruction of the window. +async function checkForEventListenerLeaks(name, target) { + // Test if we leak in the case where we do nothing special to + // the frame before removing it from the DOM. + await _eventListenerLeakStep(target, `${name} default`); + + // Test the case where we navigate the frame before removing it + // from the DOM so that the window using the target API ends up + // in bfcache. + await _eventListenerLeakStep(target, `${name} bfcache`, frame => { + frame.src = "about:blank"; + return new Promise(resolve => (frame.onload = resolve)); + }); + + // Test the case where we document.open() the frame before removing + // it from the DOM so that the window using the target API ends + // up getting replaced. + await _eventListenerLeakStep(target, `${name} document.open()`, frame => { + frame.contentDocument.open(); + frame.contentDocument.close(); + }); +} + +// ---------------- +// Internal helpers +// ---------------- + +// Utility function to create a loaded iframe. +async function _withFrame(doc, url) { + let frame = doc.createElement("iframe"); + frame.src = url; + doc.body.appendChild(frame); + await new Promise(resolve => (frame.onload = resolve)); + return frame; +} + +// This function defines the basic form of the test cases. We create an +// iframe, execute the target callback to manipulate the DOM, remove the frame +// from the DOM, and then check to see if the frame was GC'd. The caller +// may optionally pass in a callback that will be executed with the +// frame as an argument before removing it from the DOM. +async function _eventListenerLeakStep(target, name, extra) { + let frame = await _withFrame(document, "empty.html"); + + await target(frame.contentWindow); + + let weakRef = SpecialPowers.Cu.getWeakReference(frame.contentWindow); + ok(weakRef.get(), `should be able to create a weak reference - ${name}`); + + if (extra) { + await extra(frame); + } + + frame.remove(); + frame = null; + + // Perform many GC's to avoid intermittent delayed collection. + await new Promise(resolve => SpecialPowers.exactGC(resolve)); + await new Promise(resolve => SpecialPowers.exactGC(resolve)); + await new Promise(resolve => SpecialPowers.exactGC(resolve)); + + ok( + !weakRef.get(), + `iframe content window should be garbage collected - ${name}` + ); +} diff --git a/dom/events/test/file_beforeinput_by_execCommand_in_contentscript.html b/dom/events/test/file_beforeinput_by_execCommand_in_contentscript.html new file mode 100644 index 0000000000..722b101a3b --- /dev/null +++ b/dom/events/test/file_beforeinput_by_execCommand_in_contentscript.html @@ -0,0 +1,2 @@ +<!doctype html> +<div contenteditable>abcdef</div> diff --git a/dom/events/test/file_bug1446834.html b/dom/events/test/file_bug1446834.html new file mode 100644 index 0000000000..e3832fd2f0 --- /dev/null +++ b/dom/events/test/file_bug1446834.html @@ -0,0 +1,96 @@ +<html> + <head> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script> + + function lazyRequestAnimationFrame(fn) { + requestAnimationFrame( + function() { + setTimeout(fn); + }); + } + + var tests = [ removeHost, removeShadowElement ]; + function nextTest() { + if (tests.length) { + var test = tests.shift(); + lazyRequestAnimationFrame(test); + } else { + parent.SimpleTest.finish(); + } + } + + function removeHost() { + var hostgrandparent = document.getElementById("hostgrandparent"); + var hostparent = document.getElementById("hostparent"); + hostparent.innerHTML = "<div id='host'></div>"; + var host = document.getElementById("host"); + var sr = document.getElementById("host").attachShadow({mode: "open"}); + sr.innerHTML = "<input type='button' value='click'>"; + sr.firstChild.onclick = function() { + parent.is(hostparent.querySelector("div:hover"), host, "host should be hovered."); + host.remove(); + parent.is(hostgrandparent.querySelector("div:hover"), hostparent, + "hostgrandparent element should have descendants marked in :hover state."); + synthesizeMouseAtCenter(document.getElementById('light'), { type: "mousemove" }); + lazyRequestAnimationFrame( + function() { + parent.is(hostgrandparent.querySelector("div:hover"), null, + "hostgrandparent element shouldn't have descendants marked in :hover state anymore."); + nextTest(); + } + ); + } + lazyRequestAnimationFrame( + function() { + synthesizeMouseAtCenter(sr.firstChild, { type: "mousemove" }); + synthesizeMouseAtCenter(sr.firstChild, {}); + } + ); + } + + function removeShadowElement() { + var hostgrandparent = document.getElementById("hostgrandparent"); + var hostparent = document.getElementById("hostparent"); + hostparent.innerHTML = "<div id='host'><input id='input' slot='slot' type='button' value='click'></div>"; + var host = document.getElementById("host"); + var input = document.getElementById("input"); + var sr = document.getElementById("host").attachShadow({mode: "open"}); + sr.innerHTML = "<div><div><slot name='slot'></slot></div></div>"; + var shadowOuterDiv = sr.firstChild; + var shadowInnerDiv = shadowOuterDiv.firstChild; + var slot = shadowInnerDiv.firstChild; + sr.firstChild.onclick = function() { + parent.is(hostparent.querySelector("div:hover"), host, "host should be hovered."); + slot.remove(); + parent.is(shadowOuterDiv.querySelector("div:hover"), shadowInnerDiv, + "Elements in shadow DOM should stay hovered"); + synthesizeMouseAtCenter(document.getElementById('light'), { type: "mousemove" }); + lazyRequestAnimationFrame( + function() { + parent.is(shadowOuterDiv.querySelector("div:hover"), null, + "Shadow DOM shouldn't be marked to be hovered anymore."); + nextTest(); + } + ); + } + lazyRequestAnimationFrame( + function() { + synthesizeMouseAtCenter(input, { type: "mousemove" }); + synthesizeMouseAtCenter(input, {}); + } + ); + } + </script> + <style> + </style> + </head> + <body onload="nextTest()"> + <div id="hostgrandparent"> + <div id="hostparent"> + </div> + foo + </div> + <div id="light">light dom</div> + </body> +</html>
\ No newline at end of file diff --git a/dom/events/test/file_bug1484371.html b/dom/events/test/file_bug1484371.html new file mode 100644 index 0000000000..56c284b733 --- /dev/null +++ b/dom/events/test/file_bug1484371.html @@ -0,0 +1,94 @@ +<html> + <head> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script> + var mouseEnterCount = 0; + function mouseEnter() { + ++mouseEnterCount; + } + var mouseLeaveCount = 0; + function mouseLeave() { + ++mouseLeaveCount; + } + + var pointerEnterCount = 0; + function pointerEnter() { + ++pointerEnterCount; + } + var pointerLeaveCount = 0; + function pointerLeave() { + ++pointerLeaveCount; + } + + function pointerEventsEnabled() { + return "onpointerenter" in document.body; + } + + function checkEventCounts(expected, msg) { + parent.is(mouseEnterCount, expected.mouseEnterCount, msg + ": mouseenter event count"); + parent.is(mouseLeaveCount, expected.mouseLeaveCount, msg + ": mouseleave event count"); + if (pointerEventsEnabled()) { + parent.is(pointerEnterCount, expected.pointerEnterCount, msg + ": pointerenter event count"); + parent.is(pointerLeaveCount, expected.pointerLeaveCount, msg + ": pointerleave event count"); + } + } + + function test() { + var lightDiv = document.getElementById("lightDiv"); + var host = document.getElementById("host"); + var sr = host.attachShadow({mode: "closed"}); + sr.innerHTML = "<div>shadow DOM<div>"; + var shadowDiv = sr.firstChild; + + host.addEventListener("mouseenter", mouseEnter, true); + host.addEventListener("mouseleave", mouseLeave, true); + host.addEventListener("pointerenter", pointerEnter, true); + host.addEventListener("pointerleave", pointerLeave, true); + + shadowDiv.addEventListener("mouseenter", mouseEnter, true); + shadowDiv.addEventListener("mouseleave", mouseLeave, true); + shadowDiv.addEventListener("pointerenter", pointerEnter, true); + shadowDiv.addEventListener("pointerleave", pointerLeave, true); + + synthesizeMouseAtCenter(lightDiv, { type: "mousemove" }); + checkEventCounts({ mouseEnterCount: 0, + mouseLeaveCount: 0, + pointerEnterCount: 0, + pointerLeaveCount: 0 + }, + "Entered light DOM" + ); + + synthesizeMouseAtCenter(shadowDiv, { type: "mousemove" }) + checkEventCounts({ mouseEnterCount: 2, + mouseLeaveCount: 0, + pointerEnterCount: 2, + pointerLeaveCount: 0 + }, + "Entered shadow DOM"); + + synthesizeMouseAtCenter(lightDiv, { type: "mousemove" }) + checkEventCounts({ mouseEnterCount: 2, + mouseLeaveCount: 2, + pointerEnterCount: 2, + pointerLeaveCount: 2 + }, + "Left shadow DOM" + ); + + parent.SimpleTest.finish(); + } + + function lazyRequestAnimationFrame(fn) { + requestAnimationFrame( + function() { + setTimeout(fn); + }); + } + </script> + </head> + <body onload="lazyRequestAnimationFrame(test)"> + <div id="lightDiv">light DOM</div> + <div id="host"></div> + </body> +</html> diff --git a/dom/events/test/file_bug1692052.html b/dom/events/test/file_bug1692052.html new file mode 100644 index 0000000000..23b26d95c7 --- /dev/null +++ b/dom/events/test/file_bug1692052.html @@ -0,0 +1,25 @@ +<!DOCTYPE html> +<html> +<head> + <script> + window.addEventListener('load', () => { + const canvas = document.createElementNS('http://www.w3.org/1999/xhtml', 'canvas') + document.adoptNode(document.documentElement) + document.appendChild(canvas) + canvas.focus() + canvas.contentEditable = 'true' + canvas.focus() + setTimeout(() => { + let params = new URLSearchParams(location.search); + let q = parseInt(params.get("c") || 0) + 1; + if (q > 5) { + window.opener.finishTests(); + window.close(); + return; + } + location.href = location.pathname + "?c=" + q; + }, 0) + }) + </script> +</head> +</html> diff --git a/dom/events/test/file_bug679494.html b/dom/events/test/file_bug679494.html new file mode 100644 index 0000000000..a2e47916c5 --- /dev/null +++ b/dom/events/test/file_bug679494.html @@ -0,0 +1,8 @@ +<html> +<head> + <title>Test for Bug 679494</title> +</head> +<body> + There and back again. +</body> +</html> diff --git a/dom/events/test/file_coalesce_touchmove_browserchild.html b/dom/events/test/file_coalesce_touchmove_browserchild.html new file mode 100644 index 0000000000..28a4ae5252 --- /dev/null +++ b/dom/events/test/file_coalesce_touchmove_browserchild.html @@ -0,0 +1,117 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1"> + <title>touchmove coalescing</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> + <script> + var receivedSingleTouch = false; + var receivedMultiTouch = false; + // This test does three things: + // 1. Ensures adding a new touch to existing touchmove events + // will not be coalesced with the previous single touch touchmove events + // 2. Ensures touchstart flushes the coalesced touchmove events + // 3. Ensures touchend flushes the coalesced touchmove event + async function testBrowserChildCoalescing() { + var ret = new Promise(function(resolve) { + SpecialPowers.loadChromeScript(function() { + /* eslint-env mozilla/chrome-script */ + var element = this.actorParent.rootFrameLoader.ownerElement; + var rect = element.getBoundingClientRect(); + var win = element.ownerDocument.defaultView; + var utils = win.windowUtils; + var x = rect.x + (rect.width / 2); + var y = Math.floor(rect.y + (rect.height / 4)); + var endYForFirstTouch = Math.floor(rect.y + ((rect.height / 4) * 2)); + var endYForSecondTouch = Math.floor(rect.y + ((rect.height / 4) * 4)); + utils.sendTouchEvent("touchstart", [0], [x], [y], [1], [1], [0], [1], + [0], [0], [0], 0, false); + while (y != endYForFirstTouch) { + utils.sendTouchEvent("touchmove", [0], [x], [y], [1], [1], [0], [1], + [0], [0], [0], 0, false); + ++y; + } + + // Add a new touch and move this touch + utils.sendTouchEvent("touchstart", [0, 1], [x, x], [endYForFirstTouch, endYForFirstTouch], [1, 1], [1, 1], [0, 0], [1, 1], + [0, 0], [0, 0], [0, 0], 0, false); + while (y != endYForSecondTouch) { + utils.sendTouchEvent("touchmove", [0, 1], [x, x], [endYForFirstTouch, y], [1, 1], [1, 1], [0, 0], [1, 1], + [0, 0], [0, 0], [0, 0], 0, false); + ++y; + } + + utils.sendTouchEvent("touchend", [0, 1], [x, x], [endYForFirstTouch, endYForSecondTouch], [1, 1], [1, 1], [0, 0], [1, 1], + [0, 0], [0, 0], [0, 0], 0, false); + }); + + let touchStartCount = 0; + let shouldReceiveMultiTouch = false; + let receivedTouchEnd = false; + + window.addEventListener("touchstart", function() { + ++touchStartCount; + if (touchStartCount == 2) { + shouldReceiveMultiTouch = true; + } + }, true); + + + window.addEventListener("touchmove", function(e) { + if (receivedTouchEnd) { + ok(false, "Shouldn't get any touchmove events after touchend"); + } + + // Make touchmove handling slow + var start = performance.now(); + while (performance.now() < (start + 10)); + + if (shouldReceiveMultiTouch) { + is(e.touches.length, 2, "Should get two touches for multi touch"); + receivedMultiTouch = true; + } else { + is(e.touches.length, 1, "Should get one touch for single touch"); + receivedSingleTouch = true; + } + }, true); + + window.addEventListener("touchend", async function(e) { + receivedTouchEnd = true; + // Request a tick to ensure touchend has successfully flushed + // coalesced touchmove events + await new Promise(r => { + window.requestAnimationFrame(() => { + window.requestAnimationFrame(r); + }); + }); + + resolve(); + }, {once: true}); + }); + + return ret; + } + + async function runTests() { + await SpecialPowers.pushPrefEnv({"set": [["dom.events.coalesce.touchmove", true]]}); + + await testBrowserChildCoalescing(); + + ok(receivedSingleTouch, "Should've got single touch"); + ok(receivedMultiTouch, "Should've got multi touch"); + + opener.finish(); + window.close(); + } + + function init() { + SpecialPowers.pushPrefEnv({"set": [["dom.w3c_touch_events.enabled", true]]}, + runTests); + } + </script> +</head> +<body onload="SimpleTest.waitForFocus(init);"> +</body> +</html> diff --git a/dom/events/test/file_coalesce_touchmove_browserchild2.html b/dom/events/test/file_coalesce_touchmove_browserchild2.html new file mode 100644 index 0000000000..b29ae291c2 --- /dev/null +++ b/dom/events/test/file_coalesce_touchmove_browserchild2.html @@ -0,0 +1,193 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1"> + <title>touchmove coalescing</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> + <script> + window.oncontextmenu = function(e) { + e.preventDefault(); + } + + window.addEventListener("touchstart", function(e) { e.preventDefault(); }, + { passive: false} ); + + var touchmoveEvents = []; + function touchmove(e) { + // Make touchmove handling slow + var start = performance.now(); + while (performance.now() < (start + 5)); + touchmoveEvents.push(e); + } + + async function fireLotsOfSingleTouchMoves() { + var ret = new Promise(function(resolve) { + SpecialPowers.loadChromeScript(function() { + /* eslint-env mozilla/chrome-script */ + var element = this.actorParent.rootFrameLoader.ownerElement; + var rect = element.getBoundingClientRect(); + var win = element.ownerDocument.defaultView; + var utils = win.windowUtils; + var x = rect.x + (rect.width / 2); + var y = Math.floor(rect.y + (rect.height / 4)); + var endY = Math.floor(rect.y + ((rect.height / 4) * 2)); + utils.sendTouchEvent("touchstart", [0], [x], [y], [1], [1], [0], [1], + [0], [0], [0], 0, false); + while (y != endY) { + utils.sendTouchEvent("touchmove", [0], [x], [y], [1], [1], [0], [1], + [0], [0], [0], 0, false); + ++y; + } + utils.sendTouchEvent("touchend", [0], [x], [y], [1], [1], [0], [1], + [0], [0], [0], 0, false); + + }); + + touchmoveEvents = []; + window.addEventListener("touchmove", touchmove, true); + window.addEventListener("touchend", function(e) { + window.removeEventListener("touchmove", touchmove, true); + resolve(touchmoveEvents); + }, {once: true}); + }); + + return ret + } + + async function fireTwoSingleTouches() { + var ret = new Promise(function(resolve) { + SpecialPowers.loadChromeScript(function() { + /* eslint-env mozilla/chrome-script */ + var element = this.actorParent.rootFrameLoader.ownerElement; + var rect = element.getBoundingClientRect(); + var win = element.ownerDocument.defaultView; + var utils = win.windowUtils; + var x = rect.x + (rect.width / 2); + var startY = Math.floor(rect.y + (rect.height / 4)); + var endY = Math.floor(rect.y + ((rect.height / 4) * 2)); + utils.sendTouchEvent("touchstart", [0], [x], [startY], [1], [1], [0], + [1], [0], [0], [0], 0, false); + utils.sendTouchEvent("touchmove", [0], [x], [startY], [1], [1], [0], + [1], [0], [0], [0], 0, false); + utils.sendTouchEvent("touchmove", [0], [x], [startY + 1], [1], [1], [0], + [1], [0], [0], [0], 0, false); + utils.sendTouchEvent("touchend", [0], [x], [endY], [1], [1], [0], + [1], [0], [0], [0], 0, false); + }); + + touchmoveEvents = []; + window.addEventListener("touchmove", touchmove, true); + window.addEventListener("touchend", function(e) { + window.removeEventListener("touchmove", touchmove, true); + resolve(touchmoveEvents); + }, {once: true}); + }); + + return ret + } + + async function fireLotsOfMultiTouchMoves() { + var ret = new Promise(function(resolve) { + SpecialPowers.loadChromeScript(function() { + /* eslint-env mozilla/chrome-script */ + var element = this.actorParent.rootFrameLoader.ownerElement; + var rect = element.getBoundingClientRect(); + var win = element.ownerDocument.defaultView; + var utils = win.windowUtils; + var x = rect.x + (rect.width / 2); + var startY = Math.floor(rect.y + (rect.height / 4)); + var endY = Math.floor(rect.y + ((rect.height / 4) * 2)); + utils.sendTouchEvent("touchstart", [0, 1], [x, x + 1], + [startY, startY + 1], [1, 1], [1, 1], [0, 0], + [1, 1], [0, 0], [0, 0], [0, 0], 0, false); + while (startY != endY) { + utils.sendTouchEvent("touchmove", [0, 1], [x, x + 1], + [startY, startY + 1], [1, 1], [1, 1], [0, 0], + [1, 1], [0, 0], [0, 0], [0, 0], 0, false); + ++startY; + } + utils.sendTouchEvent("touchend", [0, 1], [x, x + 1], [endY, endY + 1], + [1, 1], [1, 1], [0, 0], [1, 1], [0, 0], [0, 0], + [0, 0], 0, false); + + }); + + touchmoveEvents = []; + window.addEventListener("touchmove", touchmove, true); + window.addEventListener("touchend", function(e) { + window.removeEventListener("touchmove", touchmove, true); + resolve(touchmoveEvents); + }, {once: true}); + }); + + return ret + } + + function disableCoalescing() { + return SpecialPowers.pushPrefEnv({"set": [["dom.events.compress.touchmove", + false], + ["dom.events.coalesce.touchmove", + false]]}); + } + + function enableCoalescing() { + return SpecialPowers.pushPrefEnv({"set": [["dom.events.compress.touchmove", + false], + ["dom.events.coalesce.touchmove", + true]]}); + } + + async function runTests() { + document.body.clientTop; // Flush layout + + await disableCoalescing(); + var touchMovesWithoutCoalescing = await fireLotsOfSingleTouchMoves(); + await enableCoalescing(); + var touchMovesWithCoalescing = await fireLotsOfSingleTouchMoves(); + opener.ok(touchMovesWithoutCoalescing.length > touchMovesWithCoalescing.length, + "Coalescing should reduce the number of touchmove events"); + opener.isDeeply(touchMovesWithoutCoalescing.shift().touches, + touchMovesWithCoalescing.shift().touches, + "Shouldn't have coalesced the initial touchmove"); + opener.isDeeply(touchMovesWithoutCoalescing.pop().touches, + touchMovesWithCoalescing.pop().touches, + "The last touchmove event should be up-to-date"); + + await disableCoalescing(); + var twoTouchMovesWithoutCoalescing = await fireTwoSingleTouches(); + await enableCoalescing(); + var twoTouchMovesWithCoalescing = await fireTwoSingleTouches(); + opener.is(twoTouchMovesWithoutCoalescing.length, 2, + "Should have got two touchmoves"); + opener.is(twoTouchMovesWithoutCoalescing.length, + twoTouchMovesWithCoalescing.length, + "Shouldn't have coalesced the touchmove."); + + await disableCoalescing(); + var multiTouchMovesWithoutCoalescing = await fireLotsOfMultiTouchMoves(); + await enableCoalescing(); + var multiTouchMovesWithCoalescing = await fireLotsOfMultiTouchMoves(); + opener.ok(multiTouchMovesWithoutCoalescing.length > multiTouchMovesWithCoalescing.length, + "Coalescing should reduce the number of multitouch touchmove events"); + opener.isDeeply(multiTouchMovesWithoutCoalescing.shift().touches, + multiTouchMovesWithCoalescing.shift().touches, + "Shouldn't have coalesced the initial multitouch touchmove event"); + opener.isDeeply(multiTouchMovesWithoutCoalescing.pop().touches, + multiTouchMovesWithCoalescing.pop().touches, + "The last multitouch touchmove event should be up-to-date"); + + opener.finish(); + window.close(); + } + + function init() { + SpecialPowers.pushPrefEnv({"set": [["dom.w3c_touch_events.enabled", true]]}, + runTests); + } + </script> +</head> +<body style="height: 100vh;" onload="SimpleTest.waitForFocus(init);"> +</body> +</html> diff --git a/dom/events/test/file_coalesce_touchmove_ipc.html b/dom/events/test/file_coalesce_touchmove_ipc.html new file mode 100644 index 0000000000..73e5a1157d --- /dev/null +++ b/dom/events/test/file_coalesce_touchmove_ipc.html @@ -0,0 +1,193 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1"> + <title>touchmove coalescing</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> + <script> + window.oncontextmenu = function(e) { + e.preventDefault(); + } + + window.addEventListener("touchstart", function(e) { e.preventDefault(); }, + { passive: false} ); + + var touchmoveEvents = []; + function touchmove(e) { + // Make touchmove handling slow + var start = performance.now(); + while (performance.now() < (start + 10)); + touchmoveEvents.push(e); + } + + async function fireLotsOfSingleTouchMoves() { + var ret = new Promise(function(resolve) { + SpecialPowers.loadChromeScript(function() { + /* eslint-env mozilla/chrome-script */ + var element = this.actorParent.rootFrameLoader.ownerElement; + var rect = element.getBoundingClientRect(); + var win = element.ownerDocument.defaultView; + var utils = win.windowUtils; + var x = rect.x + (rect.width / 2); + var y = Math.floor(rect.y + (rect.height / 4)); + var endY = Math.floor(rect.y + ((rect.height / 4) * 2)); + utils.sendTouchEvent("touchstart", [0], [x], [y], [1], [1], [0], [1], + [0], [0], [0], 0, false); + while (y != endY) { + utils.sendTouchEvent("touchmove", [0], [x], [y], [1], [1], [0], [1], + [0], [0], [0], 0, false); + ++y; + } + utils.sendTouchEvent("touchend", [0], [x], [y], [1], [1], [0], [1], + [0], [0], [0], 0, false); + + }); + + touchmoveEvents = []; + window.addEventListener("touchmove", touchmove, true); + window.addEventListener("touchend", function(e) { + window.removeEventListener("touchmove", touchmove, true); + resolve(touchmoveEvents); + }, {once: true}); + }); + + return ret + } + + async function fireTwoSingleTouches() { + var ret = new Promise(function(resolve) { + SpecialPowers.loadChromeScript(function() { + /* eslint-env mozilla/chrome-script */ + var element = this.actorParent.rootFrameLoader.ownerElement; + var rect = element.getBoundingClientRect(); + var win = element.ownerDocument.defaultView; + var utils = win.windowUtils; + var x = rect.x + (rect.width / 2); + var startY = Math.floor(rect.y + (rect.height / 4)); + var endY = Math.floor(rect.y + ((rect.height / 4) * 2)); + utils.sendTouchEvent("touchstart", [0], [x], [startY], [1], [1], [0], + [1], [0], [0], [0], 0, false); + utils.sendTouchEvent("touchmove", [0], [x], [startY], [1], [1], [0], + [1], [0], [0], [0], 0, false); + utils.sendTouchEvent("touchmove", [0], [x], [startY + 1], [1], [1], [0], + [1], [0], [0], [0], 0, false); + utils.sendTouchEvent("touchend", [0], [x], [endY], [1], [1], [0], + [1], [0], [0], [0], 0, false); + }); + + touchmoveEvents = []; + window.addEventListener("touchmove", touchmove, true); + window.addEventListener("touchend", function(e) { + window.removeEventListener("touchmove", touchmove, true); + resolve(touchmoveEvents); + }, {once: true}); + }); + + return ret + } + + async function fireLotsOfMultiTouchMoves() { + var ret = new Promise(function(resolve) { + SpecialPowers.loadChromeScript(function() { + /* eslint-env mozilla/chrome-script */ + var element = this.actorParent.rootFrameLoader.ownerElement; + var rect = element.getBoundingClientRect(); + var win = element.ownerDocument.defaultView; + var utils = win.windowUtils; + var x = rect.x + (rect.width / 2); + var startY = Math.floor(rect.y + (rect.height / 4)); + var endY = Math.floor(rect.y + ((rect.height / 4) * 2)); + utils.sendTouchEvent("touchstart", [0, 1], [x, x + 1], + [startY, startY + 1], [1, 1], [1, 1], [0, 0], + [1, 1], [0, 0], [0, 0], [0, 0], 0, false); + while (startY != endY) { + utils.sendTouchEvent("touchmove", [0, 1], [x, x + 1], + [startY, startY + 1], [1, 1], [1, 1], [0, 0], + [1, 1], [0, 0], [0, 0], [0, 0], 0, false); + ++startY; + } + utils.sendTouchEvent("touchend", [0, 1], [x, x + 1], [endY, endY + 1], + [1, 1], [1, 1], [0, 0], [1, 1], [0, 0], [0, 0], + [0, 0], 0, false); + + }); + + touchmoveEvents = []; + window.addEventListener("touchmove", touchmove, true); + window.addEventListener("touchend", function(e) { + window.removeEventListener("touchmove", touchmove, true); + resolve(touchmoveEvents); + }, {once: true}); + }); + + return ret + } + + function disableIPCCompression() { + return SpecialPowers.pushPrefEnv({"set": [["dom.events.compress.touchmove", + false], + ["dom.events.coalesce.touchmove", + false]]}); + } + + function enableIPCCompression() { + return SpecialPowers.pushPrefEnv({"set": [["dom.events.compress.touchmove", + true], + ["dom.events.coalesce.touchmove", + false]]}); + } + + async function runTests() { + document.body.clientTop; // Flush layout + + await disableIPCCompression(); + var touchMovesWithoutCoalescing = await fireLotsOfSingleTouchMoves(); + await enableIPCCompression(); + var touchMovesWithCoalescing = await fireLotsOfSingleTouchMoves(); + opener.ok(touchMovesWithoutCoalescing.length > touchMovesWithCoalescing.length, + "Coalescing should reduce the number of touchmove events"); + opener.isDeeply(touchMovesWithoutCoalescing.shift().touches, + touchMovesWithCoalescing.shift().touches, + "Shouldn't have coalesced the initial touchmove"); + opener.isDeeply(touchMovesWithoutCoalescing.pop().touches, + touchMovesWithCoalescing.pop().touches, + "The last touchmove event should be up-to-date"); + + await disableIPCCompression(); + var twoTouchMovesWithoutCoalescing = await fireTwoSingleTouches(); + await enableIPCCompression(); + var twoTouchMovesWithCoalescing = await fireTwoSingleTouches(); + opener.is(twoTouchMovesWithoutCoalescing.length, 2, + "Should have got two touchmoves"); + opener.is(twoTouchMovesWithoutCoalescing.length, + twoTouchMovesWithCoalescing.length, + "Shouldn't have coalesced the touchmove."); + + await disableIPCCompression(); + var multiTouchMovesWithoutCoalescing = await fireLotsOfMultiTouchMoves(); + await enableIPCCompression(); + var multiTouchMovesWithCoalescing = await fireLotsOfMultiTouchMoves(); + opener.ok(multiTouchMovesWithoutCoalescing.length > multiTouchMovesWithCoalescing.length, + "Coalescing should reduce the number of multitouch touchmove events"); + opener.isDeeply(multiTouchMovesWithoutCoalescing.shift().touches, + multiTouchMovesWithCoalescing.shift().touches, + "Shouldn't have coalesced the initial multitouch touchmove event"); + opener.isDeeply(multiTouchMovesWithoutCoalescing.pop().touches, + multiTouchMovesWithCoalescing.pop().touches, + "The last multitouch touchmove event should be up-to-date"); + + opener.finish(); + window.close(); + } + + function init() { + SpecialPowers.pushPrefEnv({"set": [["dom.w3c_touch_events.enabled", true]]}, + runTests); + } + </script> +</head> +<body style="height: 100vh;" onload="SimpleTest.waitForFocus(init);"> +</body> +</html> diff --git a/dom/events/test/file_empty.html b/dom/events/test/file_empty.html new file mode 100644 index 0000000000..e75bdc942f --- /dev/null +++ b/dom/events/test/file_empty.html @@ -0,0 +1,15 @@ +<!DOCTYPE html> +<html> +<head> +<script src="/tests/SimpleTest/paint_listener.js"></script> +<script src="/tests/gfx/layers/apz/test/mochitest/apz_test_utils.js"></script> +<script> +window.addEventListener("load", async function() { + await promiseApzFlushedRepaints(); + parent.postMessage("ready", "*"); +}, { once: true }); +</script> +</head> +<body> +</body> +</html>
\ No newline at end of file diff --git a/dom/events/test/file_event_screenXY.html b/dom/events/test/file_event_screenXY.html new file mode 100644 index 0000000000..d39e28abc2 --- /dev/null +++ b/dom/events/test/file_event_screenXY.html @@ -0,0 +1,23 @@ +<!DOCTYPE html> +<script src="/tests/SimpleTest/paint_listener.js"></script> +<script src="/tests/gfx/layers/apz/test/mochitest/apz_test_utils.js"></script> +<style> +html, body { + height: 100%; + margin: 0px; + padding: 0px; +} +</style> +<div style="width:100%;height:100%;background-color:red;"></div> +<script> + document.querySelector("div").addEventListener("click", event => { + parent.postMessage({ screenX: event.screenX, + screenY: event.screenY, + clientX: event.clientX, + clientY: event.clientY }, "*"); + }); + window.onload = async () => { + await promiseApzFlushedRepaints(); + parent.postMessage("ready", "*"); + } +</script> diff --git a/dom/events/test/file_focus_blur_on_click_in_cross_origin_iframe.html b/dom/events/test/file_focus_blur_on_click_in_cross_origin_iframe.html new file mode 100644 index 0000000000..09eab8fc16 --- /dev/null +++ b/dom/events/test/file_focus_blur_on_click_in_cross_origin_iframe.html @@ -0,0 +1,28 @@ +<!DOCTYPE html> +<script src="/tests/SimpleTest/paint_listener.js"></script> +<script src="/tests/gfx/layers/apz/test/mochitest/apz_test_utils.js"></script> +<style> +html, body { + height: 100%; + margin: 0px; + padding: 0px; +} +</style> +<div style="width:100%;height:100%;background-color:blue;"></div> +<script> + document.querySelector("div").addEventListener("click", event => { + parent.postMessage("click", "*"); + }); + window.onload = async () => { + // Wait for APZ state stable so that mouse event handling APZ works properly + // in out-of-process iframes. + await promiseApzFlushedRepaints(); + parent.postMessage("ready", "*"); + }; + document.body.onfocus = () => { + parent.postMessage("focus", "*"); + }; + document.body.onblur = () => { + parent.postMessage("blur", "*"); + }; +</script> diff --git a/dom/events/test/file_focus_blur_on_click_in_deep_cross_origin_iframe_inner.html b/dom/events/test/file_focus_blur_on_click_in_deep_cross_origin_iframe_inner.html new file mode 100644 index 0000000000..e7127e2397 --- /dev/null +++ b/dom/events/test/file_focus_blur_on_click_in_deep_cross_origin_iframe_inner.html @@ -0,0 +1,28 @@ +<!DOCTYPE html> +<script src="/tests/SimpleTest/paint_listener.js"></script> +<script src="/tests/gfx/layers/apz/test/mochitest/apz_test_utils.js"></script> +<style> +html, body { + height: 100%; + margin: 0px; + padding: 0px; +} +</style> +<div style="width:100%;height:100%;background-color:blue;"></div> +<script> + document.querySelector("div").addEventListener("click", event => { + parent.postMessage("innerclick", "*"); + }); + window.onload = async () => { + // Wait for APZ state stable so that mouse event handling APZ works properly + // in out-of-process iframes. + await promiseApzFlushedRepaints(); + parent.postMessage("innerready", "*"); + }; + document.body.onfocus = () => { + parent.postMessage("innerfocus", "*"); + }; + document.body.onblur = () => { + parent.postMessage("innerblur", "*"); + }; +</script> diff --git a/dom/events/test/file_focus_blur_on_click_in_deep_cross_origin_iframe_middle.html b/dom/events/test/file_focus_blur_on_click_in_deep_cross_origin_iframe_middle.html new file mode 100644 index 0000000000..bb128422ae --- /dev/null +++ b/dom/events/test/file_focus_blur_on_click_in_deep_cross_origin_iframe_middle.html @@ -0,0 +1,37 @@ +<!DOCTYPE html> +<script src="/tests/SimpleTest/paint_listener.js"></script> +<script src="/tests/gfx/layers/apz/test/mochitest/apz_test_utils.js"></script> +<style> +html, body { + height: 200px; + margin: 0px; + padding: 0px; +} +</style> +<body> +<script> + window.addEventListener("message", async (event) => { + if (event.data == "innerready") { + // Wait for APZ state stable so that mouse event handling APZ works properly + // in out-of-process iframes. + await promiseApzFlushedRepaints(); + } + parent.postMessage(event.data, "*"); + }); + window.onload = () => { + parent.postMessage("middleready", "*"); + }; + document.body.onfocus = () => { + parent.postMessage("middlefocus", "*"); + }; + document.body.onblur = () => { + parent.postMessage("middleblur", "*"); + }; +</script> +<div style="width:100px;height:100px;background-color:gray;"></div> +<iframe width="100" height="100" src="https://example.org/tests/dom/events/test/file_focus_blur_on_click_in_deep_cross_origin_iframe_inner.html"></iframe> +<script> + document.querySelector("div").addEventListener("click", event => { + parent.postMessage("middleclick", "*"); + }); +</script> diff --git a/dom/events/test/file_keyboard_event_init_key_event_enabled_in_contentscript.html b/dom/events/test/file_keyboard_event_init_key_event_enabled_in_contentscript.html new file mode 100644 index 0000000000..98691bfe69 --- /dev/null +++ b/dom/events/test/file_keyboard_event_init_key_event_enabled_in_contentscript.html @@ -0,0 +1,21 @@ +<!doctype html> +<html> +<head> +<meta charset="utf-8"> +<title>Storing type of `KeyboardEvent.initKeyEvent`</title> +<script> +document.documentElement.setAttribute( + "data-initKeyEvent-before", + typeof window.KeyboardEvent.prototype.initKeyEvent +); +window.addEventListener("load", () => { + document.documentElement.setAttribute( + "data-initKeyEvent-after", + typeof window.KeyboardEvent.prototype.initKeyEvent + ); +}, { capture: false, once: true }); +</script> +</head> +<body> +</body> +</html> diff --git a/dom/events/test/file_mouse_enterleave.html b/dom/events/test/file_mouse_enterleave.html new file mode 100644 index 0000000000..9e3d1f1d8b --- /dev/null +++ b/dom/events/test/file_mouse_enterleave.html @@ -0,0 +1,40 @@ +<html> +<body> +<style> +#target { + width: 100%; + height: 100%; +} +#reflow { + width: 100%; + height: 10px; + background-color: red; +} +</style> +<div id="target"></div> +<div id="reflow"></div> +<script> +function listener(e) { + parent.postMessage({ eventType: e.type, targetName: e.target.localName }, "*"); +} + +window.addEventListener("message", function(aEvent) { + if (aEvent.data === "reflow") { + let reflow = document.getElementById("reflow"); + reflow.style.display = "none"; + reflow.getBoundingClientRect(); + reflow.style.display = "block"; + reflow.getBoundingClientRect(); + } +}); + +let target = document.getElementById("target"); +target.addEventListener("mouseenter", listener); +target.addEventListener("mouseleave", listener); + +let root = document.documentElement; +root.addEventListener("mouseenter", listener); +root.addEventListener("mouseleave", listener); +</script> +</body> +</html> diff --git a/dom/events/test/green.png b/dom/events/test/green.png Binary files differnew file mode 100644 index 0000000000..7df25f33bd --- /dev/null +++ b/dom/events/test/green.png diff --git a/dom/events/test/gtest/TestShortcutKeyDefinitions.cpp b/dom/events/test/gtest/TestShortcutKeyDefinitions.cpp new file mode 100644 index 0000000000..10adbba49f --- /dev/null +++ b/dom/events/test/gtest/TestShortcutKeyDefinitions.cpp @@ -0,0 +1,634 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#include "gtest/gtest.h" + +#include "KeyEventHandler.h" +#include "ShortcutKeys.h" + +#include "mozilla/RefPtr.h" +#include "mozilla/TextEvents.h" +#include "mozilla/dom/KeyboardEvent.h" + +namespace mozilla { + +/** + * These tests checks whether the key mapping defined by + * `ShortcutKeyDefinitions.cpp` and its including header files. + * Note that these tests do NOT define how shortcut keys mapped. These tests + * exist for checking unexpected change occurs by cleaning up or changing + * the mapping. + */ + +#ifndef MOZ_WIDGET_COCOA +# define MODIFIER_ACCEL MODIFIER_CONTROL +#else +# define MODIFIER_ACCEL MODIFIER_META +#endif + +struct ShortcutKeyMap final { + uint32_t const mKeyCode; + char32_t const mCharCode; + Modifiers const mModifiers; + const char16ptr_t mCommandWin; + const char16ptr_t mCommandMac; + const char16ptr_t mCommandLinux; + const char16ptr_t mCommandAndroid; + const char16ptr_t mCommandEmacs; +}; + +#if defined(XP_WIN) +# define GetCommandForPlatform(aMap) aMap.mCommandWin +#elif defined(MOZ_WIDGET_COCOA) +# define GetCommandForPlatform(aMap) aMap.mCommandMac +#elif defined(MOZ_WIDGET_GTK) +# define GetCommandForPlatform(aMap) aMap.mCommandLinux +#elif defined(MOZ_WIDGET_ANDROID) +# define GetCommandForPlatform(aMap) aMap.mCommandAndroid +#else +# define GetCommandForPlatform(aMap) aMap.mCommandEmacs +#endif + +static bool GetCommandFor(KeyEventHandler* aFirstHandler, + dom::KeyboardEvent* aDOMEvent, nsAString& aCommand) { + if (!aFirstHandler) { + return false; + } + aCommand.Truncate(); + for (KeyEventHandler* handler = aFirstHandler; handler; + handler = handler->GetNextHandler()) { + if (handler->KeyEventMatched(aDOMEvent, 0, IgnoreModifierState())) { + handler->GetCommand(aCommand); + return true; + } + } + return false; +} + +static char16ptr_t GetExpectedCommandFor( + const nsTArray<ShortcutKeyMap>& aExpectedMap, + const WidgetKeyboardEvent& aWidgetEvent) { + MOZ_ASSERT(!aExpectedMap.IsEmpty()); + for (const ShortcutKeyMap& map : aExpectedMap) { + if (aWidgetEvent.mKeyCode == map.mKeyCode && + aWidgetEvent.mCharCode == map.mCharCode && + aWidgetEvent.mModifiers == map.mModifiers) { + return GetCommandForPlatform(map); + } + } + return nullptr; +} + +struct KeyCodeAndStr final { + uint32_t mKeyCode; + KeyNameIndex mKeyNameIndex; + const char* mStr; +}; +static const KeyCodeAndStr kKeyCodes[] = { + {NS_VK_BACK, KEY_NAME_INDEX_Backspace, "Backspace"}, + {NS_VK_RETURN, KEY_NAME_INDEX_Enter, "Enter"}, + {NS_VK_ESCAPE, KEY_NAME_INDEX_Escape, "Escape"}, + {NS_VK_PAGE_UP, KEY_NAME_INDEX_PageUp, "PageUp"}, + {NS_VK_PAGE_DOWN, KEY_NAME_INDEX_PageDown, "PageDown"}, + {NS_VK_END, KEY_NAME_INDEX_End, "End"}, + {NS_VK_HOME, KEY_NAME_INDEX_Home, "Home"}, + {NS_VK_LEFT, KEY_NAME_INDEX_ArrowLeft, "ArrowLeft"}, + {NS_VK_UP, KEY_NAME_INDEX_ArrowUp, "ArrowUp"}, + {NS_VK_RIGHT, KEY_NAME_INDEX_ArrowRight, "ArrowRight"}, + {NS_VK_DOWN, KEY_NAME_INDEX_ArrowDown, "ArrowDown"}, +#ifndef MOZ_WIDGET_COCOA + // No Insert key on macOS + {NS_VK_INSERT, KEY_NAME_INDEX_Insert, "Insert"}, +#endif + {NS_VK_DELETE, KEY_NAME_INDEX_Delete, "Delete"}, + {NS_VK_F1, KEY_NAME_INDEX_F1, "F1"}, + {NS_VK_F2, KEY_NAME_INDEX_F2, "F2"}, + {NS_VK_F3, KEY_NAME_INDEX_F3, "F3"}, + {NS_VK_F4, KEY_NAME_INDEX_F4, "F4"}, + {NS_VK_F5, KEY_NAME_INDEX_F5, "F5"}, + {NS_VK_F6, KEY_NAME_INDEX_F6, "F6"}, + {NS_VK_F7, KEY_NAME_INDEX_F7, "F7"}, + {NS_VK_F8, KEY_NAME_INDEX_F8, "F8"}, + {NS_VK_F9, KEY_NAME_INDEX_F9, "F9"}, + {NS_VK_F10, KEY_NAME_INDEX_F10, "F10"}, + {NS_VK_F11, KEY_NAME_INDEX_F11, "F11"}, + {NS_VK_F12, KEY_NAME_INDEX_F12, "F12"}, +#if defined(XP_WIN) || defined(MOZ_WIDGET_GTK) || defined(MOZ_WIDGET_ANDROID) + {NS_VK_F13, KEY_NAME_INDEX_F13, "F13"}, + {NS_VK_F14, KEY_NAME_INDEX_F14, "F14"}, + {NS_VK_F15, KEY_NAME_INDEX_F15, "F15"}, + {NS_VK_F16, KEY_NAME_INDEX_F16, "F16"}, + {NS_VK_F17, KEY_NAME_INDEX_F17, "F17"}, + {NS_VK_F18, KEY_NAME_INDEX_F18, "F18"}, + {NS_VK_F19, KEY_NAME_INDEX_F19, "F19"}, +#endif +#if defined(XP_WIN) || defined(MOZ_WIDGET_GTK) + {NS_VK_F20, KEY_NAME_INDEX_F20, "F20"}, + {NS_VK_F21, KEY_NAME_INDEX_F21, "F21"}, + {NS_VK_F22, KEY_NAME_INDEX_F22, "F22"}, + {NS_VK_F23, KEY_NAME_INDEX_F23, "F23"}, + {NS_VK_F24, KEY_NAME_INDEX_F24, "F24"}, +#endif +}; + +static const unsigned char kCharCodes[] = { + ' ', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', + 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', + 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'}; + +struct ModifiersAndStr final { + Modifiers mModifiers; + const char* mStr; +}; +static const ModifiersAndStr kModifiers[] = { + {MODIFIER_NONE, ""}, + {MODIFIER_SHIFT, "Shift+"}, + {MODIFIER_CONTROL, "Control+"}, + {MODIFIER_CONTROL | MODIFIER_SHIFT, "Contrl+Shift+"}, + {MODIFIER_ALT, "Alt+"}, + {MODIFIER_ALT | MODIFIER_SHIFT, "Alt+Shift+"}, + {MODIFIER_META, "Meta+"}, + {MODIFIER_META | MODIFIER_SHIFT, "Meta+Shift+"}, + {MODIFIER_META | MODIFIER_ALT | MODIFIER_SHIFT, "Meta+Alt+Shift+"}, + {MODIFIER_CONTROL | MODIFIER_ALT, "Control+Alt+"}, + {MODIFIER_CONTROL | MODIFIER_ALT | MODIFIER_SHIFT, "Control+Alt+Shift+"}, +}; + +static void TestAllKeyCodes(KeyEventHandler* aFirstHandler, + const nsTArray<ShortcutKeyMap>& aExpectedMap) { + WidgetKeyboardEvent widgetEvent(true, eKeyPress, nullptr); + RefPtr<dom::KeyboardEvent> domEvent = + NS_NewDOMKeyboardEvent(nullptr, nullptr, &widgetEvent); + nsAutoString command; + uint32_t foundCommand = 0; + for (const auto& mod : kModifiers) { + widgetEvent.mModifiers = mod.mModifiers; + for (const auto& keyCode : kKeyCodes) { + widgetEvent.mKeyCode = keyCode.mKeyCode; + widgetEvent.mKeyNameIndex = keyCode.mKeyNameIndex; + const char16ptr_t expectedCommand = + GetExpectedCommandFor(aExpectedMap, widgetEvent); + if (GetCommandFor(aFirstHandler, domEvent, command)) { + foundCommand++; + if (expectedCommand) { + ASSERT_TRUE(command.Equals(expectedCommand)) + << mod.mStr << keyCode.mStr << ": got " + << NS_ConvertUTF16toUTF8(command).get() << ", but expected " + << NS_ConvertUTF16toUTF8(expectedCommand).get(); + } else { + ASSERT_TRUE(false) + << mod.mStr << keyCode.mStr << ": got " + << NS_ConvertUTF16toUTF8(command).get() + << ", but expected no command"; + } + } else if (expectedCommand) { + ASSERT_TRUE(false) + << mod.mStr << keyCode.mStr << ": got no command, but expected " + << NS_ConvertUTF16toUTF8(expectedCommand).get(); + } + } + widgetEvent.mKeyCode = 0; + widgetEvent.mKeyNameIndex = KEY_NAME_INDEX_USE_STRING; + for (unsigned char charCode : kCharCodes) { + widgetEvent.mCharCode = charCode; + widgetEvent.mKeyValue.Assign(charCode); + const char16ptr_t expectedCommand = + GetExpectedCommandFor(aExpectedMap, widgetEvent); + if (GetCommandFor(aFirstHandler, domEvent, command)) { + foundCommand++; + if (expectedCommand) { + ASSERT_TRUE(command.Equals(expectedCommand)) + << mod.mStr << "'" << nsAutoCString(charCode).get() << "': got " + << NS_ConvertUTF16toUTF8(command).get() << ", but expected " + << NS_ConvertUTF16toUTF8(expectedCommand).get(); + } else { + ASSERT_TRUE(false) + << mod.mStr << "'" << nsAutoCString(charCode).get() << "': got " + << NS_ConvertUTF16toUTF8(command).get() + << ", but expected no command"; + } + } else if (expectedCommand) { + ASSERT_TRUE(false) + << mod.mStr << "'" << nsAutoCString(charCode).get() + << "': got no command, but expected " + << NS_ConvertUTF16toUTF8(expectedCommand).get(); + } + } + widgetEvent.mCharCode = 0; + widgetEvent.mKeyValue.Truncate(); + } + uint32_t expectedCommandCount = 0; + for (const auto& map : aExpectedMap) { + if (GetCommandForPlatform(map)) { + expectedCommandCount++; + } + } + ASSERT_EQ(foundCommand, expectedCommandCount) + << "Some expected shortcut keys have not been tested"; + uint32_t countOfHandler = 0; + for (KeyEventHandler* handler = aFirstHandler; handler; + handler = handler->GetNextHandler()) { + countOfHandler++; + } + ASSERT_EQ(countOfHandler, expectedCommandCount) + << "Some unnecessary key handlers found"; +} + +TEST(ShortcutKeyDefinitions, HTMLInputElement) +{ + // clang-format off + const nsTArray<ShortcutKeyMap> expectedMap{ + // KeyCode Modifiers Windows macOS Linux Android Emacs + {NS_VK_LEFT, 0, MODIFIER_NONE, u"cmd_moveLeft", nullptr, u"cmd_moveLeft", u"cmd_moveLeft", u"cmd_moveLeft"}, + {NS_VK_RIGHT, 0, MODIFIER_NONE, u"cmd_moveRight", nullptr, u"cmd_moveRight", u"cmd_moveRight", u"cmd_moveRight"}, + {NS_VK_LEFT, 0, MODIFIER_SHIFT, u"cmd_selectLeft", nullptr, u"cmd_selectLeft", u"cmd_selectLeft", u"cmd_selectLeft"}, + {NS_VK_RIGHT, 0, MODIFIER_SHIFT, u"cmd_selectRight", nullptr, u"cmd_selectRight", u"cmd_selectRight", u"cmd_selectRight"}, + {NS_VK_LEFT, 0, MODIFIER_CONTROL, u"cmd_moveLeft2", nullptr, nullptr, u"cmd_wordPrevious", u"cmd_wordPrevious"}, + {NS_VK_RIGHT, 0, MODIFIER_CONTROL, u"cmd_moveRight2", nullptr, nullptr, u"cmd_wordNext", u"cmd_wordNext"}, + {NS_VK_LEFT, 0, MODIFIER_ALT, nullptr, nullptr, nullptr, u"cmd_beginLine", nullptr}, + {NS_VK_RIGHT, 0, MODIFIER_ALT, nullptr, nullptr, nullptr, u"cmd_endLine", nullptr}, + {NS_VK_UP, 0, MODIFIER_NONE, u"cmd_moveUp", nullptr, u"cmd_moveUp", u"cmd_moveUp", u"cmd_moveUp"}, + {NS_VK_DOWN, 0, MODIFIER_NONE, u"cmd_moveDown", nullptr, u"cmd_moveDown", u"cmd_moveDown", u"cmd_moveDown"}, + {NS_VK_UP, 0, MODIFIER_SHIFT, u"cmd_selectUp", nullptr, u"cmd_selectUp", u"cmd_selectUp", u"cmd_selectUp"}, + {NS_VK_DOWN, 0, MODIFIER_SHIFT, u"cmd_selectDown", nullptr, u"cmd_selectDown", u"cmd_selectDown", u"cmd_selectDown"}, + {NS_VK_UP, 0, MODIFIER_CONTROL, u"cmd_moveUp2", nullptr, nullptr, nullptr, nullptr}, + {NS_VK_DOWN, 0, MODIFIER_CONTROL, u"cmd_moveDown2", nullptr, nullptr, nullptr, nullptr}, + + // KeyCode Modifiers Windows macOS Linux Android Emacs + {NS_VK_LEFT, 0, MODIFIER_SHIFT | MODIFIER_CONTROL, u"cmd_selectLeft2", nullptr, nullptr, u"cmd_selectWordPrevious", u"cmd_selectWordPrevious"}, + {NS_VK_RIGHT, 0, MODIFIER_SHIFT | MODIFIER_CONTROL, u"cmd_selectRight2", nullptr, nullptr, u"cmd_selectWordNext", u"cmd_selectWordNext"}, + {NS_VK_LEFT, 0, MODIFIER_SHIFT | MODIFIER_ALT, nullptr, nullptr, nullptr, u"cmd_selectBeginLine", nullptr}, + {NS_VK_RIGHT, 0, MODIFIER_SHIFT | MODIFIER_ALT, nullptr, nullptr, nullptr, u"cmd_selectEndLine", nullptr}, + {NS_VK_UP, 0, MODIFIER_SHIFT | MODIFIER_CONTROL, u"cmd_selectUp2", nullptr, nullptr, nullptr, nullptr}, + {NS_VK_DOWN, 0, MODIFIER_SHIFT | MODIFIER_CONTROL, u"cmd_selectDown2", nullptr, nullptr, nullptr, nullptr}, + + // KeyCode Modifiers Windows macOS Linux Android Emacs + {NS_VK_HOME, 0, MODIFIER_NONE, u"cmd_beginLine", nullptr, nullptr, u"cmd_beginLine", u"cmd_beginLine"}, + {NS_VK_END, 0, MODIFIER_NONE, u"cmd_endLine", nullptr, nullptr, u"cmd_endLine", u"cmd_endLine"}, + {NS_VK_HOME, 0, MODIFIER_SHIFT, u"cmd_selectBeginLine", nullptr, nullptr, u"cmd_selectBeginLine", u"cmd_selectBeginLine"}, + {NS_VK_END, 0, MODIFIER_SHIFT, u"cmd_selectEndLine", nullptr, nullptr, u"cmd_selectEndLine", u"cmd_selectEndLine"}, + {NS_VK_HOME, 0, MODIFIER_CONTROL, u"cmd_moveTop", nullptr, nullptr, nullptr, u"cmd_beginLine"}, + {NS_VK_END, 0, MODIFIER_CONTROL, u"cmd_moveBottom", nullptr, nullptr, nullptr, u"cmd_endLine"}, + + // KeyCode Modifiers Windows macOS Linux Android Emacs + {NS_VK_HOME, 0, MODIFIER_SHIFT | MODIFIER_CONTROL, u"cmd_selectTop", nullptr, nullptr, nullptr, u"cmd_selectBeginLine"}, + {NS_VK_END, 0, MODIFIER_SHIFT | MODIFIER_CONTROL, u"cmd_selectBottom", nullptr, nullptr, nullptr, u"cmd_selectEndLine"}, + + // KeyCode Modifiers Windows macOS Linux Android + {NS_VK_INSERT, 0, MODIFIER_SHIFT, u"cmd_paste", nullptr, nullptr, nullptr, u"cmd_paste"}, + {NS_VK_INSERT, 0, MODIFIER_CONTROL, u"cmd_copy", nullptr, nullptr, nullptr, u"cmd_copy"}, + {NS_VK_DELETE, 0, MODIFIER_SHIFT, u"cmd_cutOrDelete", nullptr, nullptr, nullptr, u"cmd_cutOrDelete"}, + {NS_VK_DELETE, 0, MODIFIER_CONTROL, u"cmd_deleteWordForward", nullptr, nullptr, u"cmd_deleteWordForward", u"cmd_copyOrDelete"}, + {NS_VK_DELETE, 0, MODIFIER_ALT, nullptr, nullptr, nullptr, u"cmd_deleteToEndOfLine", nullptr}, + {NS_VK_BACK, 0, MODIFIER_CONTROL, u"cmd_deleteWordBackward", nullptr, nullptr, u"cmd_deleteWordBackward", u"cmd_deleteWordBackward"}, + {NS_VK_BACK, 0, MODIFIER_ALT, u"cmd_undo", nullptr, nullptr, u"cmd_deleteToBeginningOfLine", nullptr}, + + // KeyCode Modifiers Windows macOS Linux Android Emacs + {NS_VK_BACK, 0, MODIFIER_SHIFT | MODIFIER_ALT, u"cmd_redo", nullptr, nullptr, nullptr, nullptr}, + + // charCode Modifiers, Windows macOS Linux Android Emacs + {0, 'a', MODIFIER_ACCEL, u"cmd_selectAll", u"cmd_selectAll", u"cmd_selectAll", u"cmd_selectAll", nullptr}, + {0, 'a', MODIFIER_ALT, nullptr, nullptr, nullptr, nullptr, u"cmd_selectAll"}, + {0, 'c', MODIFIER_ACCEL, u"cmd_copy", u"cmd_copy", u"cmd_copy", u"cmd_copy", u"cmd_copy"}, + {0, 'x', MODIFIER_ACCEL, u"cmd_cut", u"cmd_cut", u"cmd_cut", u"cmd_cut", u"cmd_cut"}, + {0, 'v', MODIFIER_ACCEL, u"cmd_paste", u"cmd_paste", u"cmd_paste", u"cmd_paste", u"cmd_paste"}, + {0, 'y', MODIFIER_ACCEL, u"cmd_redo", nullptr, u"cmd_redo", nullptr, u"cmd_redo"}, + {0, 'z', MODIFIER_ACCEL, u"cmd_undo", u"cmd_undo", u"cmd_undo", u"cmd_undo", u"cmd_undo"}, + + // charCode Modifiers, Windows macOS Linux Android Emacs + {0, 'v', MODIFIER_SHIFT | MODIFIER_ACCEL, u"cmd_paste", u"cmd_paste", u"cmd_paste", u"cmd_paste", u"cmd_paste"}, + {0, 'v', MODIFIER_SHIFT | MODIFIER_ALT | MODIFIER_ACCEL, nullptr, u"cmd_paste", nullptr, nullptr, nullptr}, + + // charCode Modifiers Windows macOS Linux Android Emacs + {0, 'z', MODIFIER_SHIFT | MODIFIER_ACCEL, u"cmd_redo", u"cmd_redo", u"cmd_redo", u"cmd_redo", u"cmd_redo"}, + + // charCode Modifiers Windows macOS Linux Android Emacs + {0, 'a', MODIFIER_CONTROL, nullptr, nullptr, nullptr, nullptr, u"cmd_beginLine"}, + {0, 'b', MODIFIER_CONTROL, nullptr, nullptr, nullptr, nullptr, u"cmd_charPrevious"}, + {0, 'd', MODIFIER_CONTROL, nullptr, nullptr, nullptr, nullptr, u"cmd_deleteCharForward"}, + {0, 'e', MODIFIER_CONTROL, nullptr, nullptr, nullptr, nullptr, u"cmd_endLine"}, + {0, 'f', MODIFIER_CONTROL, nullptr, nullptr, nullptr, nullptr, u"cmd_charNext"}, + {0, 'h', MODIFIER_CONTROL, nullptr, nullptr, nullptr, nullptr, u"cmd_deleteCharBackward"}, + {0, 'k', MODIFIER_CONTROL, nullptr, nullptr, nullptr, nullptr, u"cmd_deleteToEndOfLine"}, + {0, 'u', MODIFIER_CONTROL, nullptr, nullptr, nullptr, nullptr, u"cmd_deleteToBeginningOfLine"}, + {0, 'w', MODIFIER_CONTROL, nullptr, nullptr, nullptr, nullptr, u"cmd_deleteWordBackward"}, + }; + // clang-format on + + TestAllKeyCodes(ShortcutKeys::GetHandlers(HandlerType::eInput), expectedMap); + + ShortcutKeys::Shutdown(); // Destroy the singleton instance. +} + +TEST(ShortcutKeyDefinitions, HTMLTextAreaElement) +{ + // clang-format off + const nsTArray<ShortcutKeyMap> expectedMap{ + // KeyCode Modifiers Windows macOS Linux Android Emacs + {NS_VK_LEFT, 0, MODIFIER_NONE, u"cmd_moveLeft", nullptr, u"cmd_moveLeft", u"cmd_moveLeft", u"cmd_moveLeft"}, + {NS_VK_RIGHT, 0, MODIFIER_NONE, u"cmd_moveRight", nullptr, u"cmd_moveRight", u"cmd_moveRight", u"cmd_moveRight"}, + {NS_VK_LEFT, 0, MODIFIER_SHIFT, u"cmd_selectLeft", nullptr, u"cmd_selectLeft", u"cmd_selectLeft", u"cmd_selectLeft"}, + {NS_VK_RIGHT, 0, MODIFIER_SHIFT, u"cmd_selectRight", nullptr, u"cmd_selectRight", u"cmd_selectRight", u"cmd_selectRight"}, + {NS_VK_LEFT, 0, MODIFIER_CONTROL, u"cmd_moveLeft2", nullptr, nullptr, u"cmd_wordPrevious", u"cmd_wordPrevious"}, + {NS_VK_RIGHT, 0, MODIFIER_CONTROL, u"cmd_moveRight2", nullptr, nullptr, u"cmd_wordNext", u"cmd_wordNext"}, + {NS_VK_LEFT, 0, MODIFIER_ALT, nullptr, nullptr, nullptr, u"cmd_beginLine", nullptr}, + {NS_VK_RIGHT, 0, MODIFIER_ALT, nullptr, nullptr, nullptr, u"cmd_endLine", nullptr}, + {NS_VK_UP, 0, MODIFIER_NONE, u"cmd_moveUp", nullptr, u"cmd_moveUp", u"cmd_moveUp", u"cmd_moveUp"}, + {NS_VK_DOWN, 0, MODIFIER_NONE, u"cmd_moveDown", nullptr, u"cmd_moveDown", u"cmd_moveDown", u"cmd_moveDown"}, + {NS_VK_UP, 0, MODIFIER_SHIFT, u"cmd_selectUp", nullptr, u"cmd_selectUp", u"cmd_selectUp", u"cmd_selectUp"}, + {NS_VK_DOWN, 0, MODIFIER_SHIFT, u"cmd_selectDown", nullptr, u"cmd_selectDown", u"cmd_selectDown", u"cmd_selectDown"}, + {NS_VK_UP, 0, MODIFIER_CONTROL, u"cmd_moveUp2", nullptr, nullptr, nullptr, nullptr}, + {NS_VK_DOWN, 0, MODIFIER_CONTROL, u"cmd_moveDown2", nullptr, nullptr, nullptr, nullptr}, + {NS_VK_UP, 0, MODIFIER_ALT, nullptr, nullptr, nullptr, u"cmd_moveTop", nullptr}, + {NS_VK_DOWN, 0, MODIFIER_ALT, nullptr, nullptr, nullptr, u"cmd_moveBottom", nullptr}, + + // KeyCode Modifiers Windows macOS Linux Android Emacs + {NS_VK_LEFT, 0, MODIFIER_SHIFT | MODIFIER_CONTROL, u"cmd_selectLeft2", nullptr, nullptr, u"cmd_selectWordPrevious", u"cmd_selectWordPrevious"}, + {NS_VK_RIGHT, 0, MODIFIER_SHIFT | MODIFIER_CONTROL, u"cmd_selectRight2", nullptr, nullptr, u"cmd_selectWordNext", u"cmd_selectWordNext"}, + {NS_VK_LEFT, 0, MODIFIER_SHIFT | MODIFIER_ALT, nullptr, nullptr, nullptr, u"cmd_selectBeginLine", nullptr}, + {NS_VK_RIGHT, 0, MODIFIER_SHIFT | MODIFIER_ALT, nullptr, nullptr, nullptr, u"cmd_selectEndLine", nullptr}, + {NS_VK_UP, 0, MODIFIER_SHIFT | MODIFIER_CONTROL, u"cmd_selectUp2", nullptr, nullptr, nullptr, nullptr}, + {NS_VK_DOWN, 0, MODIFIER_SHIFT | MODIFIER_CONTROL, u"cmd_selectDown2", nullptr, nullptr, nullptr, nullptr}, + {NS_VK_UP, 0, MODIFIER_SHIFT | MODIFIER_ALT, nullptr, nullptr, nullptr, u"cmd_selectTop", nullptr}, + {NS_VK_DOWN, 0, MODIFIER_SHIFT | MODIFIER_ALT, nullptr, nullptr, nullptr, u"cmd_selectBottom", nullptr}, + + // KeyCode Modifiers Windows macOS Linux Android Emacs + {NS_VK_PAGE_UP, 0, MODIFIER_NONE, u"cmd_movePageUp", nullptr, nullptr, u"cmd_movePageUp", u"cmd_movePageUp"}, + {NS_VK_PAGE_DOWN, 0, MODIFIER_NONE, u"cmd_movePageDown", nullptr, nullptr, u"cmd_movePageDown", u"cmd_movePageDown"}, + {NS_VK_PAGE_UP, 0, MODIFIER_SHIFT, u"cmd_selectPageUp", nullptr, nullptr, u"cmd_selectPageUp", u"cmd_selectPageUp"}, + {NS_VK_PAGE_DOWN, 0, MODIFIER_SHIFT, u"cmd_selectPageDown", nullptr, nullptr, u"cmd_selectPageDown", u"cmd_selectPageDown"}, + {NS_VK_PAGE_UP, 0, MODIFIER_ALT, nullptr, nullptr, nullptr, u"cmd_moveTop", nullptr}, + {NS_VK_PAGE_DOWN, 0, MODIFIER_ALT, nullptr, nullptr, nullptr, u"cmd_moveBottom", nullptr}, + + // KeyCode Modifiers Windows macOS Linux Android Emacs + {NS_VK_PAGE_UP, 0, MODIFIER_SHIFT | MODIFIER_ALT, nullptr, nullptr, nullptr, u"cmd_selectTop", nullptr}, + {NS_VK_PAGE_DOWN, 0, MODIFIER_SHIFT | MODIFIER_ALT, nullptr, nullptr, nullptr, u"cmd_selectBottom", nullptr}, + + // KeyCode Modifiers Windows macOS Linux Android Emacs + {NS_VK_HOME, 0, MODIFIER_NONE, u"cmd_beginLine", nullptr, nullptr, u"cmd_beginLine", u"cmd_beginLine"}, + {NS_VK_END, 0, MODIFIER_NONE, u"cmd_endLine", nullptr, nullptr, u"cmd_endLine", u"cmd_endLine"}, + {NS_VK_HOME, 0, MODIFIER_SHIFT, u"cmd_selectBeginLine", nullptr, nullptr, u"cmd_selectBeginLine", u"cmd_selectBeginLine"}, + {NS_VK_END, 0, MODIFIER_SHIFT, u"cmd_selectEndLine", nullptr, nullptr, u"cmd_selectEndLine", u"cmd_selectEndLine"}, + {NS_VK_HOME, 0, MODIFIER_CONTROL, u"cmd_moveTop", nullptr, nullptr, u"cmd_moveTop", u"cmd_moveTop"}, + {NS_VK_END, 0, MODIFIER_CONTROL, u"cmd_moveBottom", nullptr, nullptr, u"cmd_moveBottom", u"cmd_moveBottom"}, + + // KeyCode Modifiers Windows macOS Linux Android Emacs + {NS_VK_HOME, 0, MODIFIER_SHIFT | MODIFIER_CONTROL, u"cmd_selectTop", nullptr, nullptr, u"cmd_selectTop", u"cmd_selectTop"}, + {NS_VK_END, 0, MODIFIER_SHIFT | MODIFIER_CONTROL, u"cmd_selectBottom", nullptr, nullptr, u"cmd_selectBottom", u"cmd_selectBottom"}, + + // KeyCode Modifiers Windows macOS Linux Android Emacs + {NS_VK_INSERT, 0, MODIFIER_SHIFT, u"cmd_paste", nullptr, nullptr, nullptr, u"cmd_paste"}, + {NS_VK_INSERT, 0, MODIFIER_CONTROL, u"cmd_copy", nullptr, nullptr, nullptr, u"cmd_copy"}, + {NS_VK_DELETE, 0, MODIFIER_SHIFT, u"cmd_cutOrDelete", nullptr, nullptr, nullptr, u"cmd_cutOrDelete"}, + {NS_VK_DELETE, 0, MODIFIER_CONTROL, u"cmd_deleteWordForward", nullptr, nullptr, u"cmd_deleteWordForward", u"cmd_copyOrDelete"}, + {NS_VK_DELETE, 0, MODIFIER_ALT, nullptr, nullptr, nullptr, u"cmd_deleteToEndOfLine", nullptr}, + {NS_VK_BACK, 0, MODIFIER_CONTROL, u"cmd_deleteWordBackward", nullptr, nullptr, u"cmd_deleteWordBackward", u"cmd_deleteWordBackward"}, + {NS_VK_BACK, 0, MODIFIER_ALT, u"cmd_undo", nullptr, nullptr, u"cmd_deleteToBeginningOfLine", nullptr}, + + // KeyCode Modifiers Windows macOS Linux Android Emacs + {NS_VK_BACK, 0, MODIFIER_SHIFT | MODIFIER_ALT, u"cmd_redo", nullptr, nullptr, nullptr, nullptr}, + + // charCode Modifiers, Windows macOS Linux Android Emacs + {0, 'a', MODIFIER_ACCEL, u"cmd_selectAll", u"cmd_selectAll", u"cmd_selectAll", u"cmd_selectAll", nullptr}, + {0, 'a', MODIFIER_ALT, nullptr, nullptr, nullptr, nullptr, u"cmd_selectAll"}, + {0, 'c', MODIFIER_ACCEL, u"cmd_copy", u"cmd_copy", u"cmd_copy", u"cmd_copy", u"cmd_copy"}, + {0, 'x', MODIFIER_ACCEL, u"cmd_cut", u"cmd_cut", u"cmd_cut", u"cmd_cut", u"cmd_cut"}, + {0, 'v', MODIFIER_ACCEL, u"cmd_paste", u"cmd_paste", u"cmd_paste", u"cmd_paste", u"cmd_paste"}, + {0, 'y', MODIFIER_ACCEL, u"cmd_redo", nullptr, u"cmd_redo", nullptr, u"cmd_redo"}, + {0, 'z', MODIFIER_ACCEL, u"cmd_undo", u"cmd_undo", u"cmd_undo", u"cmd_undo", u"cmd_undo"}, + + // charCode Modifiers, Windows macOS Linux Android Emacs + {0, 'v', MODIFIER_SHIFT | MODIFIER_ACCEL, u"cmd_paste", u"cmd_paste", u"cmd_paste", u"cmd_paste", u"cmd_paste"}, + {0, 'v', MODIFIER_SHIFT | MODIFIER_ALT | MODIFIER_ACCEL, nullptr, u"cmd_paste", nullptr, nullptr, nullptr}, + + // charCode Modifiers Windows macOS Linux Android Emacs + {0, 'z', MODIFIER_SHIFT | MODIFIER_ACCEL, u"cmd_redo", u"cmd_redo", u"cmd_redo", u"cmd_redo", u"cmd_redo"}, + + // charCode Modifiers Windows macOS Linux Android Emacs + {0, 'a', MODIFIER_CONTROL, nullptr, nullptr, nullptr, nullptr, u"cmd_beginLine"}, + {0, 'b', MODIFIER_CONTROL, nullptr, nullptr, nullptr, nullptr, u"cmd_charPrevious"}, + {0, 'd', MODIFIER_CONTROL, nullptr, nullptr, nullptr, nullptr, u"cmd_deleteCharForward"}, + {0, 'e', MODIFIER_CONTROL, nullptr, nullptr, nullptr, nullptr, u"cmd_endLine"}, + {0, 'f', MODIFIER_CONTROL, nullptr, nullptr, nullptr, nullptr, u"cmd_charNext"}, + {0, 'h', MODIFIER_CONTROL, nullptr, nullptr, nullptr, nullptr, u"cmd_deleteCharBackward"}, + {0, 'k', MODIFIER_CONTROL, nullptr, nullptr, nullptr, nullptr, u"cmd_deleteToEndOfLine"}, + {0, 'n', MODIFIER_CONTROL, nullptr, nullptr, nullptr, nullptr, u"cmd_lineNext"}, + {0, 'p', MODIFIER_CONTROL, nullptr, nullptr, nullptr, nullptr, u"cmd_linePrevious"}, + {0, 'u', MODIFIER_CONTROL, nullptr, nullptr, nullptr, nullptr, u"cmd_deleteToBeginningOfLine"}, + {0, 'w', MODIFIER_CONTROL, nullptr, nullptr, nullptr, nullptr, u"cmd_deleteWordBackward"}, + }; + // clang-format on + + TestAllKeyCodes(ShortcutKeys::GetHandlers(HandlerType::eTextArea), + expectedMap); + + ShortcutKeys::Shutdown(); // Destroy the singleton instance. +} + +TEST(ShortcutKeyDefinitions, Browser) +{ + // clang-format off + const nsTArray<ShortcutKeyMap> expectedMap{ + // KeyCode Modifiers Windows macOS Linux Android Emacs + {NS_VK_LEFT, 0, MODIFIER_NONE, u"cmd_moveLeft", u"cmd_moveLeft", u"cmd_moveLeft", u"cmd_moveLeft", u"cmd_moveLeft"}, + {NS_VK_RIGHT, 0, MODIFIER_NONE, u"cmd_moveRight", u"cmd_moveRight", u"cmd_moveRight", u"cmd_moveRight", u"cmd_moveRight"}, + {NS_VK_LEFT, 0, MODIFIER_SHIFT, u"cmd_selectLeft", u"cmd_selectLeft", u"cmd_selectLeft", u"cmd_selectCharPrevious", u"cmd_selectLeft"}, + {NS_VK_RIGHT, 0, MODIFIER_SHIFT, u"cmd_selectRight", u"cmd_selectRight", u"cmd_selectRight", u"cmd_selectCharNext", u"cmd_selectRight"}, + {NS_VK_LEFT, 0, MODIFIER_CONTROL, u"cmd_moveLeft2", nullptr, u"cmd_moveLeft2", u"cmd_wordPrevious", u"cmd_wordPrevious"}, + {NS_VK_RIGHT, 0, MODIFIER_CONTROL, u"cmd_moveRight2", nullptr, u"cmd_moveRight2", u"cmd_wordNext", u"cmd_wordNext"}, + {NS_VK_LEFT, 0, MODIFIER_ALT, nullptr, u"cmd_moveLeft2", nullptr, u"cmd_beginLine", nullptr}, + {NS_VK_RIGHT, 0, MODIFIER_ALT, nullptr, u"cmd_moveRight2", nullptr, u"cmd_endLine", nullptr}, + {NS_VK_UP, 0, MODIFIER_NONE, u"cmd_moveUp", u"cmd_moveUp", u"cmd_moveUp", u"cmd_moveUp", u"cmd_moveUp"}, + {NS_VK_DOWN, 0, MODIFIER_NONE, u"cmd_moveDown", u"cmd_moveDown", u"cmd_moveDown", u"cmd_moveDown", u"cmd_moveDown"}, + {NS_VK_UP, 0, MODIFIER_SHIFT, u"cmd_selectUp", u"cmd_selectUp", u"cmd_selectUp", u"cmd_selectLinePrevious", u"cmd_selectUp"}, + {NS_VK_DOWN, 0, MODIFIER_SHIFT, u"cmd_selectDown", u"cmd_selectDown", u"cmd_selectDown", u"cmd_selectLineNext", u"cmd_selectDown"}, + {NS_VK_UP, 0, MODIFIER_CONTROL, u"cmd_moveUp2", nullptr, u"cmd_moveUp2", nullptr, nullptr}, + {NS_VK_DOWN, 0, MODIFIER_CONTROL, u"cmd_moveDown2", nullptr, u"cmd_moveDown2", nullptr, nullptr}, + {NS_VK_UP, 0, MODIFIER_ALT, nullptr, nullptr, nullptr, u"cmd_moveTop", nullptr}, + {NS_VK_DOWN, 0, MODIFIER_ALT, nullptr, nullptr, nullptr, u"cmd_moveBottom", nullptr}, + {NS_VK_UP, 0, MODIFIER_META, nullptr, u"cmd_moveUp2", nullptr, nullptr, nullptr}, + {NS_VK_DOWN, 0, MODIFIER_META, nullptr, u"cmd_moveDown2", nullptr, nullptr, nullptr}, + + // KeyCode Modifiers Windows macOS Linux Android Emacs + {NS_VK_LEFT, 0, MODIFIER_SHIFT | MODIFIER_CONTROL, u"cmd_selectLeft2", nullptr, u"cmd_selectLeft2", u"cmd_selectWordPrevious", u"cmd_selectWordPrevious"}, + {NS_VK_RIGHT, 0, MODIFIER_SHIFT | MODIFIER_CONTROL, u"cmd_selectRight2", nullptr, u"cmd_selectRight2", u"cmd_selectWordNext", u"cmd_selectWordNext"}, + {NS_VK_LEFT, 0, MODIFIER_SHIFT | MODIFIER_ALT, nullptr, u"cmd_selectLeft2", nullptr, u"cmd_selectBeginLine", nullptr}, + {NS_VK_RIGHT, 0, MODIFIER_SHIFT | MODIFIER_ALT, nullptr, u"cmd_selectRight2", nullptr, u"cmd_selectEndLine", nullptr}, + {NS_VK_UP, 0, MODIFIER_SHIFT | MODIFIER_CONTROL, u"cmd_selectUp2", nullptr, u"cmd_selectUp2", nullptr, u"cmd_selectWordPrevious"}, + {NS_VK_DOWN, 0, MODIFIER_SHIFT | MODIFIER_CONTROL, u"cmd_selectDown2", nullptr, u"cmd_selectDown2", nullptr, u"cmd_selectWordNext"}, + {NS_VK_UP, 0, MODIFIER_SHIFT | MODIFIER_ALT, nullptr, u"cmd_selectUp2", nullptr, u"cmd_selectTop", nullptr}, + {NS_VK_DOWN, 0, MODIFIER_SHIFT | MODIFIER_ALT, nullptr, u"cmd_selectDown2", nullptr, u"cmd_selectBottom", nullptr}, + + // KeyCode Modifiers Windows macOS Linux Android Emacs + {NS_VK_PAGE_UP, 0, MODIFIER_NONE, u"cmd_movePageUp", u"cmd_scrollPageUp", u"cmd_movePageUp", u"cmd_movePageUp", u"cmd_movePageUp"}, + {NS_VK_PAGE_DOWN, 0, MODIFIER_NONE, u"cmd_movePageDown", u"cmd_scrollPageDown", u"cmd_movePageDown", u"cmd_movePageDown", u"cmd_movePageDown"}, + {NS_VK_PAGE_UP, 0, MODIFIER_SHIFT, u"cmd_selectPageUp", nullptr, u"cmd_selectPageUp", u"cmd_selectPageUp", u"cmd_selectPageUp"}, + {NS_VK_PAGE_DOWN, 0, MODIFIER_SHIFT, u"cmd_selectPageDown", nullptr, u"cmd_selectPageDown", u"cmd_selectPageDown", u"cmd_selectPageDown"}, + {NS_VK_PAGE_UP, 0, MODIFIER_ALT, nullptr, nullptr, nullptr, u"cmd_moveTop", nullptr}, + {NS_VK_PAGE_DOWN, 0, MODIFIER_ALT, nullptr, nullptr, nullptr, u"cmd_moveBottom", nullptr}, + + // KeyCode Modifiers Windows macOS Linux Android Emacs + {NS_VK_PAGE_UP, 0, MODIFIER_SHIFT | MODIFIER_ALT, nullptr, nullptr, nullptr, u"cmd_selectTop", nullptr}, + {NS_VK_PAGE_DOWN, 0, MODIFIER_SHIFT | MODIFIER_ALT, nullptr, nullptr, nullptr, u"cmd_selectBottom", nullptr}, + + // KeyCode Modifiers Windows macOS Linux Android Emacs + {NS_VK_HOME, 0, MODIFIER_NONE, u"cmd_beginLine", u"cmd_scrollTop", u"cmd_beginLine", u"cmd_beginLine", u"cmd_beginLine"}, + {NS_VK_END, 0, MODIFIER_NONE, u"cmd_endLine", u"cmd_scrollBottom", u"cmd_endLine", u"cmd_endLine", u"cmd_endLine"}, + {NS_VK_HOME, 0, MODIFIER_SHIFT, u"cmd_selectBeginLine", nullptr, u"cmd_selectBeginLine", u"cmd_selectBeginLine", u"cmd_selectBeginLine"}, + {NS_VK_END, 0, MODIFIER_SHIFT, u"cmd_selectEndLine", nullptr, u"cmd_selectEndLine", u"cmd_selectEndLine", u"cmd_selectEndLine"}, + {NS_VK_HOME, 0, MODIFIER_CONTROL, u"cmd_moveTop", nullptr, u"cmd_moveTop", u"cmd_moveTop", u"cmd_moveTop"}, + {NS_VK_END, 0, MODIFIER_CONTROL, u"cmd_moveBottom", nullptr, u"cmd_moveBottom", u"cmd_moveBottom", u"cmd_moveBottom"}, + + // KeyCode Modifiers Windows macOS Linux Android Emacs + {NS_VK_HOME, 0, MODIFIER_SHIFT | MODIFIER_CONTROL, u"cmd_selectTop", nullptr, u"cmd_selectTop", u"cmd_selectTop", u"cmd_selectTop"}, + {NS_VK_END, 0, MODIFIER_SHIFT | MODIFIER_CONTROL, u"cmd_selectBottom", nullptr, u"cmd_selectBottom", u"cmd_selectBottom", u"cmd_selectBottom"}, + + // KeyCode Modifiers Windows macOS Linux Android Emacs + {NS_VK_INSERT, 0, MODIFIER_CONTROL, u"cmd_copy", nullptr, u"cmd_copy", nullptr, u"cmd_copy"}, + {NS_VK_DELETE, 0, MODIFIER_SHIFT, u"cmd_cut", nullptr, u"cmd_cut", nullptr, u"cmd_cut"}, + {NS_VK_DELETE, 0, MODIFIER_CONTROL, u"cmd_deleteWordForward", nullptr, u"cmd_copy", u"cmd_deleteWordForward", u"cmd_copy"}, + {NS_VK_DELETE, 0, MODIFIER_ALT, nullptr, nullptr, nullptr, u"cmd_deleteToEndOfLine", nullptr}, + {NS_VK_BACK, 0, MODIFIER_CONTROL, nullptr, nullptr, nullptr, u"cmd_deleteWordBackward", nullptr}, + {NS_VK_BACK, 0, MODIFIER_ALT, nullptr, nullptr, nullptr, u"cmd_deleteToBeginningOfLine", nullptr}, + + // charCode Modifiers, Windows macOS Linux Android Emacs + {0, ' ', MODIFIER_NONE, u"cmd_scrollPageDown", u"cmd_scrollPageDown", u"cmd_scrollPageDown", u"cmd_scrollPageDown", u"cmd_scrollPageDown"}, + {0, ' ', MODIFIER_SHIFT, u"cmd_scrollPageUp", u"cmd_scrollPageUp", u"cmd_scrollPageUp", u"cmd_scrollPageUp", u"cmd_scrollPageUp"}, + {0, 'a', MODIFIER_ACCEL, u"cmd_selectAll", u"cmd_selectAll", u"cmd_selectAll", u"cmd_selectAll", u"cmd_selectAll"}, + {0, 'a', MODIFIER_ALT, nullptr, nullptr, nullptr, nullptr, u"cmd_selectAll"}, + {0, 'c', MODIFIER_ACCEL, u"cmd_copy", u"cmd_copy", u"cmd_copy", u"cmd_copy", u"cmd_copy"}, + {0, 'x', MODIFIER_ACCEL, u"cmd_cut", u"cmd_cut", u"cmd_cut", u"cmd_cut", u"cmd_cut"}, + {0, 'v', MODIFIER_ACCEL, u"cmd_paste", u"cmd_paste", u"cmd_paste", u"cmd_paste", u"cmd_paste"}, + {0, 'y', MODIFIER_ACCEL, u"cmd_redo", nullptr, nullptr, nullptr, nullptr}, + {0, 'z', MODIFIER_ACCEL, u"cmd_undo", u"cmd_undo", u"cmd_undo", u"cmd_undo", u"cmd_undo"}, + + // charCode Modifiers, Windows macOS Linux Android Emacs + {0, 'v', MODIFIER_SHIFT | MODIFIER_ACCEL, u"cmd_pasteNoFormatting", u"cmd_pasteNoFormatting", u"cmd_pasteNoFormatting", u"cmd_pasteNoFormatting", u"cmd_pasteNoFormatting"}, + + // charCode Modifiers, Windows macOS Linux Android Emacs + {0, 'v', MODIFIER_SHIFT | MODIFIER_ALT | MODIFIER_ACCEL, nullptr, u"cmd_pasteNoFormatting", nullptr, nullptr, nullptr}, + + // charCode Modifiers Windows macOS Linux Android Emacs + {0, 'z', MODIFIER_SHIFT | MODIFIER_ACCEL, u"cmd_redo", u"cmd_redo", u"cmd_redo", u"cmd_redo", u"cmd_redo"}, + }; + // clang-format on + + TestAllKeyCodes(ShortcutKeys::GetHandlers(HandlerType::eBrowser), + expectedMap); + + ShortcutKeys::Shutdown(); // Destroy the singleton instance. +} + +TEST(ShortcutKeyDefinitions, HTMLEditor) +{ + // clang-format off + const nsTArray<ShortcutKeyMap> expectedMap{ + // KeyCode Modifiers Windows macOS Linux Android Emacs + {NS_VK_LEFT, 0, MODIFIER_NONE, u"cmd_moveLeft", nullptr, u"cmd_moveLeft", u"cmd_moveLeft", u"cmd_moveLeft"}, + {NS_VK_RIGHT, 0, MODIFIER_NONE, u"cmd_moveRight", nullptr, u"cmd_moveRight", u"cmd_moveRight", u"cmd_moveRight"}, + {NS_VK_LEFT, 0, MODIFIER_SHIFT, u"cmd_selectLeft", nullptr, u"cmd_selectLeft", u"cmd_selectLeft", u"cmd_selectLeft"}, + {NS_VK_RIGHT, 0, MODIFIER_SHIFT, u"cmd_selectRight", nullptr, u"cmd_selectRight", u"cmd_selectRight", u"cmd_selectRight"}, + {NS_VK_LEFT, 0, MODIFIER_CONTROL, u"cmd_moveLeft2", nullptr, nullptr, u"cmd_wordPrevious", u"cmd_wordPrevious"}, + {NS_VK_RIGHT, 0, MODIFIER_CONTROL, u"cmd_moveRight2", nullptr, nullptr, u"cmd_wordNext", u"cmd_wordNext"}, + {NS_VK_LEFT, 0, MODIFIER_ALT, nullptr, nullptr, nullptr, u"cmd_beginLine", nullptr}, + {NS_VK_RIGHT, 0, MODIFIER_ALT, nullptr, nullptr, nullptr, u"cmd_endLine", nullptr}, + {NS_VK_UP, 0, MODIFIER_NONE, u"cmd_moveUp", nullptr, u"cmd_moveUp", u"cmd_moveUp", u"cmd_moveUp"}, + {NS_VK_DOWN, 0, MODIFIER_NONE, u"cmd_moveDown", nullptr, u"cmd_moveDown", u"cmd_moveDown", u"cmd_moveDown"}, + {NS_VK_UP, 0, MODIFIER_SHIFT, u"cmd_selectUp", nullptr, u"cmd_selectUp", u"cmd_selectUp", u"cmd_selectUp"}, + {NS_VK_DOWN, 0, MODIFIER_SHIFT, u"cmd_selectDown", nullptr, u"cmd_selectDown", u"cmd_selectDown", u"cmd_selectDown"}, + {NS_VK_UP, 0, MODIFIER_CONTROL, u"cmd_moveUp2", nullptr, nullptr, nullptr, nullptr}, + {NS_VK_DOWN, 0, MODIFIER_CONTROL, u"cmd_moveDown2", nullptr, nullptr, nullptr, nullptr}, + {NS_VK_UP, 0, MODIFIER_ALT, nullptr, nullptr, nullptr, u"cmd_moveTop", nullptr}, + {NS_VK_DOWN, 0, MODIFIER_ALT, nullptr, nullptr, nullptr, u"cmd_moveBottom", nullptr}, + + // KeyCode Modifiers Windows macOS Linux Android Emacs + {NS_VK_LEFT, 0, MODIFIER_SHIFT | MODIFIER_CONTROL, u"cmd_selectLeft2", nullptr, nullptr, u"cmd_selectWordPrevious", u"cmd_selectWordPrevious"}, + {NS_VK_RIGHT, 0, MODIFIER_SHIFT | MODIFIER_CONTROL, u"cmd_selectRight2", nullptr, nullptr, u"cmd_selectWordNext", u"cmd_selectWordNext"}, + {NS_VK_LEFT, 0, MODIFIER_SHIFT | MODIFIER_ALT, nullptr, nullptr, nullptr, u"cmd_selectBeginLine", nullptr}, + {NS_VK_RIGHT, 0, MODIFIER_SHIFT | MODIFIER_ALT, nullptr, nullptr, nullptr, u"cmd_selectEndLine", nullptr}, + {NS_VK_UP, 0, MODIFIER_SHIFT | MODIFIER_CONTROL, u"cmd_selectUp2", nullptr, nullptr, nullptr, nullptr}, + {NS_VK_DOWN, 0, MODIFIER_SHIFT | MODIFIER_CONTROL, u"cmd_selectDown2", nullptr, nullptr, nullptr, nullptr}, + {NS_VK_UP, 0, MODIFIER_SHIFT | MODIFIER_ALT, nullptr, nullptr, nullptr, u"cmd_selectTop", nullptr}, + {NS_VK_DOWN, 0, MODIFIER_SHIFT | MODIFIER_ALT, nullptr, nullptr, nullptr, u"cmd_selectBottom", nullptr}, + + // KeyCode Modifiers Windows macOS Linux Android Emacs + {NS_VK_PAGE_UP, 0, MODIFIER_NONE, u"cmd_movePageUp", nullptr, nullptr, u"cmd_movePageUp", u"cmd_movePageUp"}, + {NS_VK_PAGE_DOWN, 0, MODIFIER_NONE, u"cmd_movePageDown", nullptr, nullptr, u"cmd_movePageDown", u"cmd_movePageDown"}, + {NS_VK_PAGE_UP, 0, MODIFIER_SHIFT, u"cmd_selectPageUp", nullptr, nullptr, u"cmd_selectPageUp", u"cmd_selectPageUp"}, + {NS_VK_PAGE_DOWN, 0, MODIFIER_SHIFT, u"cmd_selectPageDown", nullptr, nullptr, u"cmd_selectPageDown", u"cmd_selectPageDown"}, + {NS_VK_PAGE_UP, 0, MODIFIER_ALT, nullptr, nullptr, nullptr, u"cmd_moveTop", nullptr}, + {NS_VK_PAGE_DOWN, 0, MODIFIER_ALT, nullptr, nullptr, nullptr, u"cmd_moveBottom", nullptr}, + + // KeyCode Modifiers Windows macOS Linux Android Emacs + {NS_VK_PAGE_UP, 0, MODIFIER_SHIFT | MODIFIER_ALT, nullptr, nullptr, nullptr, u"cmd_selectTop", nullptr}, + {NS_VK_PAGE_DOWN, 0, MODIFIER_SHIFT | MODIFIER_ALT, nullptr, nullptr, nullptr, u"cmd_selectBottom", nullptr}, + + // KeyCode Modifiers Windows macOS Linux Android Emacs + {NS_VK_HOME, 0, MODIFIER_NONE, u"cmd_beginLine", nullptr, nullptr, u"cmd_beginLine", u"cmd_beginLine"}, + {NS_VK_END, 0, MODIFIER_NONE, u"cmd_endLine", nullptr, nullptr, u"cmd_endLine", u"cmd_endLine"}, + {NS_VK_HOME, 0, MODIFIER_SHIFT, u"cmd_selectBeginLine", nullptr, nullptr, u"cmd_selectBeginLine", u"cmd_selectBeginLine"}, + {NS_VK_END, 0, MODIFIER_SHIFT, u"cmd_selectEndLine", nullptr, nullptr, u"cmd_selectEndLine", u"cmd_selectEndLine"}, + {NS_VK_HOME, 0, MODIFIER_CONTROL, u"cmd_moveTop", nullptr, nullptr, u"cmd_moveTop", u"cmd_moveTop"}, + {NS_VK_END, 0, MODIFIER_CONTROL, u"cmd_moveBottom", nullptr, nullptr, u"cmd_moveBottom", u"cmd_moveBottom"}, + + // KeyCode Modifiers Windows macOS Linux Android Emacs + {NS_VK_HOME, 0, MODIFIER_SHIFT | MODIFIER_CONTROL, u"cmd_selectTop", nullptr, nullptr, u"cmd_selectTop", u"cmd_selectTop"}, + {NS_VK_END, 0, MODIFIER_SHIFT | MODIFIER_CONTROL, u"cmd_selectBottom", nullptr, nullptr, u"cmd_selectBottom", u"cmd_selectBottom"}, + + // KeyCode Modifiers Windows macOS Linux Android Emacs + {NS_VK_INSERT, 0, MODIFIER_SHIFT, u"cmd_paste", nullptr, nullptr, nullptr, u"cmd_paste"}, + {NS_VK_INSERT, 0, MODIFIER_CONTROL, u"cmd_copy", nullptr, nullptr, nullptr, u"cmd_copy"}, + {NS_VK_DELETE, 0, MODIFIER_SHIFT, u"cmd_cutOrDelete", nullptr, nullptr, nullptr, u"cmd_cutOrDelete"}, + {NS_VK_DELETE, 0, MODIFIER_CONTROL, u"cmd_deleteWordForward", nullptr, nullptr, u"cmd_deleteWordForward", u"cmd_copyOrDelete"}, + {NS_VK_DELETE, 0, MODIFIER_ALT, nullptr, nullptr, nullptr, u"cmd_deleteToEndOfLine", nullptr}, + {NS_VK_BACK, 0, MODIFIER_CONTROL, u"cmd_deleteWordBackward", nullptr, nullptr, u"cmd_deleteWordBackward", u"cmd_deleteWordBackward"}, + {NS_VK_BACK, 0, MODIFIER_ALT, u"cmd_undo", nullptr, nullptr, u"cmd_deleteToBeginningOfLine", nullptr}, + + // KeyCode Modifiers Windows macOS Linux Android Emacs + {NS_VK_BACK, 0, MODIFIER_SHIFT | MODIFIER_ALT, u"cmd_redo", nullptr, nullptr, nullptr, nullptr}, + + // charCode Modifiers, Windows macOS Linux Android Emacs + {0, ' ', MODIFIER_NONE, u"cmd_scrollPageDown", u"cmd_scrollPageDown", u"cmd_scrollPageDown", u"cmd_scrollPageDown", u"cmd_scrollPageDown"}, + {0, ' ', MODIFIER_SHIFT, u"cmd_scrollPageUp", u"cmd_scrollPageUp", u"cmd_scrollPageUp", u"cmd_scrollPageUp", u"cmd_scrollPageUp"}, + {0, 'a', MODIFIER_ACCEL, u"cmd_selectAll", u"cmd_selectAll", u"cmd_selectAll", u"cmd_selectAll", nullptr}, + {0, 'a', MODIFIER_ALT, nullptr, nullptr, nullptr, nullptr, u"cmd_selectAll"}, + {0, 'c', MODIFIER_ACCEL, u"cmd_copy", u"cmd_copy", u"cmd_copy", u"cmd_copy", u"cmd_copy"}, + {0, 'x', MODIFIER_ACCEL, u"cmd_cut", u"cmd_cut", u"cmd_cut", u"cmd_cut", u"cmd_cut"}, + {0, 'v', MODIFIER_ACCEL, u"cmd_paste", u"cmd_paste", u"cmd_paste", u"cmd_paste", u"cmd_paste"}, + {0, 'y', MODIFIER_ACCEL, u"cmd_redo", nullptr, u"cmd_redo", nullptr, u"cmd_redo"}, + {0, 'z', MODIFIER_ACCEL, u"cmd_undo", u"cmd_undo", u"cmd_undo", u"cmd_undo", u"cmd_undo"}, + + // charCode Modifiers Windows macOS Linux Android Emacs + {0, 'v', MODIFIER_SHIFT | MODIFIER_ACCEL, u"cmd_pasteNoFormatting", u"cmd_pasteNoFormatting", u"cmd_pasteNoFormatting", u"cmd_pasteNoFormatting", u"cmd_pasteNoFormatting"}, + {0, 'z', MODIFIER_SHIFT | MODIFIER_ACCEL, u"cmd_redo", u"cmd_redo", u"cmd_redo", u"cmd_redo", u"cmd_redo"}, + + // charCode Modifiers Windows macOS Linux Android Emacs + {0, 'v', MODIFIER_SHIFT | MODIFIER_ACCEL | MODIFIER_ALT, nullptr, u"cmd_pasteNoFormatting", nullptr, nullptr, nullptr}, + + // charCode Modifiers Windows macOS Linux Android Emacs + {0, 'a', MODIFIER_CONTROL, nullptr, nullptr, nullptr, nullptr, u"cmd_beginLine"}, + {0, 'b', MODIFIER_CONTROL, nullptr, nullptr, nullptr, nullptr, u"cmd_charPrevious"}, + {0, 'd', MODIFIER_CONTROL, nullptr, nullptr, nullptr, nullptr, u"cmd_deleteCharForward"}, + {0, 'e', MODIFIER_CONTROL, nullptr, nullptr, nullptr, nullptr, u"cmd_endLine"}, + {0, 'f', MODIFIER_CONTROL, nullptr, nullptr, nullptr, nullptr, u"cmd_charNext"}, + {0, 'h', MODIFIER_CONTROL, nullptr, nullptr, nullptr, nullptr, u"cmd_deleteCharBackward"}, + {0, 'k', MODIFIER_CONTROL, nullptr, nullptr, nullptr, nullptr, u"cmd_deleteToEndOfLine"}, + {0, 'n', MODIFIER_CONTROL, nullptr, nullptr, nullptr, nullptr, u"cmd_lineNext"}, + {0, 'p', MODIFIER_CONTROL, nullptr, nullptr, nullptr, nullptr, u"cmd_linePrevious"}, + {0, 'u', MODIFIER_CONTROL, nullptr, nullptr, nullptr, nullptr, u"cmd_deleteToBeginningOfLine"}, + {0, 'w', MODIFIER_CONTROL, nullptr, nullptr, nullptr, nullptr, u"cmd_deleteWordBackward"}, + }; + // clang-format on + + TestAllKeyCodes(ShortcutKeys::GetHandlers(HandlerType::eEditor), expectedMap); + + ShortcutKeys::Shutdown(); // Destroy the singleton instance. +} + +#undef MODIFIER_ACCEL +#undef GetCommandForPlatform + +} // namespace mozilla diff --git a/dom/events/test/gtest/moz.build b/dom/events/test/gtest/moz.build new file mode 100644 index 0000000000..d5c0ac6a9e --- /dev/null +++ b/dom/events/test/gtest/moz.build @@ -0,0 +1,18 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +with Files("**"): + BUG_COMPONENT = ("Core", "DOM: UI Events & Focus Handling") + +UNIFIED_SOURCES += [ + "TestShortcutKeyDefinitions.cpp", +] + +LOCAL_INCLUDES += ["/dom/events"] + +include("/ipc/chromium/chromium-config.mozbuild") + +FINAL_LIBRARY = "xul-gtest" diff --git a/dom/events/test/mochitest.ini b/dom/events/test/mochitest.ini new file mode 100644 index 0000000000..846fe12bda --- /dev/null +++ b/dom/events/test/mochitest.ini @@ -0,0 +1,333 @@ +[DEFAULT] +# Skip migration work in BG__migrateUI for browser_startup.js since it increases +# the occurrence of the leak reported in bug 1398563 with test_bug1327798.html. +# Run the font-loader eagerly to minimize the risk that font list finalization +# may disrupt the events received or result in a timeout. +tags = condprof +prefs = + browser.migration.version=9999999 + gfx.font_loader.delay=0 +support-files = + bug226361_iframe.xhtml + bug299673.js + bug322588-popup.html + bug426082.html + bug545268.html + bug574663.html + bug607464.html + bug656379-1.html + bug418986-3.js + error_event_worker.js + empty.js + event_leak_utils.js + window_bug493251.html + window_bug659071.html + window_wheel_default_action.html + !/gfx/layers/apz/test/mochitest/apz_test_utils.js + +[test_accel_virtual_modifier.html] +[test_accesskey.html] +[test_addEventListenerExtraArg.html] +[test_all_synthetic_events.html] +skip-if = + http3 +[test_bug1539497.html] +[test_bug1686716.html] +[test_bug226361.xhtml] +[test_bug238987.html] +[test_bug288392.html] +[test_bug299673-1.html] +[test_bug1037990.html] +[test_bug299673-2.html] +[test_bug322588.html] +[test_bug328885.html] +[test_bug336682_1.html] +support-files = test_bug336682.js +[test_bug367781.html] +[test_bug379120.html] +[test_bug402089.html] +[test_bug405632.html] +[test_bug409604.html] +skip-if = toolkit == 'android' # android: TIMED_OUT +[test_bug412567.html] +[test_bug418986-3.html] +[test_bug422132.html] +[test_bug426082.html] +skip-if = + http3 +[test_bug427537.html] +skip-if = + http3 +[test_bug428988.html] +[test_bug432698.html] +[test_bug443985.html] +skip-if = verify +[test_bug447736.html] +[test_bug448602.html] +[test_bug450876.html] +[test_bug456273.html] +[test_bug457672.html] +skip-if = toolkit == 'android' #CRASH_DUMP, RANDOM +[test_bug489671.html] +[test_bug493251.html] +[test_bug508479.html] +skip-if = toolkit == 'android' #CRASH_DUMP, RANDOM # drag event fails +[test_bug517851.html] +[test_bug534833.html] +skip-if = toolkit == 'android' #CRASH_DUMP, RANDOM +[test_bug545268.html] +skip-if = toolkit == 'android' #CRASH_DUMP, RANDOM +[test_bug547996-1.html] +skip-if = toolkit == 'android' #CRASH_DUMP, RANDOM +[test_bug547996-2.xhtml] +skip-if = toolkit == 'android' #CRASH_DUMP, RANDOM +[test_bug556493.html] +skip-if = toolkit == 'android' #CRASH_DUMP, RANDOM +[test_bug563329.html] +skip-if = true # Disabled due to timeouts. +[test_bug574663.html] +skip-if = toolkit == 'android' #CRASH_DUMP, RANDOM +[test_bug591815.html] +[test_bug593959.html] +[test_bug603008.html] +skip-if = toolkit == 'android' +[test_bug605242.html] +skip-if = toolkit == 'android' #CRASH_DUMP, RANDOM +[test_bug607464.html] +skip-if = toolkit == 'android' + e10s #CRASH_DUMP, RANDOM, bug 1400586 +[test_bug613634.html] +skip-if = toolkit == 'android' #CRASH_DUMP, RANDOM +[test_bug615597.html] +skip-if = toolkit == 'android' # failed +[test_bug624127.html] +skip-if = toolkit == 'android' #CRASH_DUMP, RANDOM +[test_bug635465.html] +skip-if = toolkit == 'android' #CRASH_DUMP, RANDOM +[test_bug641477.html] +[test_bug648573.html] +skip-if = toolkit == 'android' #CRASH_DUMP, RANDOM +[test_bug650493.html] +skip-if = toolkit == 'android' #CRASH_DUMP, RANDOM +[test_bug656379-1.html] +skip-if = toolkit == 'android' +[test_bug656379-2.html] +skip-if = toolkit == 'android' #CRASH_DUMP, RANDOM +[test_bug656954.html] +skip-if = toolkit == 'android' #CRASH_DUMP, RANDOM +[test_bug659071.html] +skip-if = + toolkit == 'android' # fail + http3 +[test_bug659350.html] +skip-if = toolkit == 'android' #CRASH_DUMP, RANDOM +[test_bug662678.html] +skip-if = toolkit == 'android' #CRASH_DUMP, RANDOM +[test_bug667612.html] +skip-if = toolkit == 'android' #CRASH_DUMP, RANDOM +[test_bug667919-1.html] +skip-if = toolkit == 'android' #CRASH_DUMP, RANDOM +[test_bug684208.html] +[test_bug689564.html] +skip-if = toolkit == 'android' #CRASH_DUMP, RANDOM +[test_bug698929.html] +skip-if = toolkit == 'android' #CRASH_DUMP, RANDOM +[test_bug704423.html] +[test_bug741666.html] +skip-if = + toolkit == 'android' # fail +[test_coalesce_mousewheel.html] +skip-if = + toolkit == 'android' # fail +[test_coalesce_touchmove.html] +support-files = + file_coalesce_touchmove_ipc.html + file_coalesce_touchmove_browserchild.html + file_coalesce_touchmove_browserchild2.html +skip-if = debug #In order to be able to test touchmoves, the test needs to synthesize touchstart in a way which asserts +[test_deviceSensor.html] +[test_bug812744.html] +skip-if = + http3 +[test_bug822898.html] +[test_bug855741.html] +[test_bug864040.html] +[test_bug924087.html] +[test_bug930374-content.html] +[test_bug944011.html] +[test_bug944847.html] +[test_bug946632.html] +skip-if = toolkit == 'android' # fail +[test_bug967796.html] +[test_bug985988.html] +[test_bug998809.html] +[test_bug1003432.html] +support-files = test_bug1003432.js +[test_bug1013412.html] +[test_bug1017086_enable.html] +support-files = bug1017086_inner.html +[test_bug1079236.html] +[test_bug1127588.html] +[test_bug1145910.html] +[test_bug1150308.html] +[test_bug1248459.html] +[test_bug1264380.html] +skip-if = toolkit == 'android' # some clipboard types and drag aren't supported +[test_bug1327798.html] +skip-if = headless +[test_bug1692052.html] +support-files = file_bug1692052.html +[test_click_on_reframed_generated_text.html] +[test_click_on_restyled_element.html] +[test_clickevent_on_input.html] +skip-if = toolkit == 'android' #CRASH_DUMP, RANDOM +[test_continuous_wheel_events.html] +skip-if = (verify && debug && (os == 'linux' || os == 'win')) + toolkit == 'android' # wheel event isn't supported +[test_dblclick_explicit_original_target.html] +[test_dom_activate_event.html] +[test_dom_keyboard_event.html] +skip-if = toolkit == 'android' #CRASH_DUMP, RANDOM +[test_dom_mouse_event.html] +skip-if = toolkit == 'android' #CRASH_DUMP, RANDOM +[test_dom_storage_event.html] +[test_dom_wheel_event.html] +skip-if = toolkit == 'android' # wheel scroll isn't supported +[test_draggableprop.html] +skip-if = toolkit == 'android' #CRASH_DUMP, RANDOM +[test_dragstart.html] +skip-if = toolkit == 'android' # not supported +[test_drag_coords.html] +skip-if = toolkit == 'android' # not supported +[test_drag_image_file.html] +skip-if = + toolkit == 'android' + xorigin # Bug 1802904 +support-files = + green.png +[test_error_events.html] +skip-if = toolkit == 'android' #TIMED_OUT +[test_event_handler_cc.html] +[test_eventctors.html] +skip-if = toolkit == 'android' #CRASH_DUMP, RANDOM +[test_eventctors_sensors.html] +[test_disabled_events.html] +[test_event_screenXY_in_cross_origin_iframe.html] +support-files = + file_event_screenXY.html + !/gfx/layers/apz/test/mochitest/apz_test_native_event_utils.js +skip-if = toolkit == 'android' # fail +[test_event_screenXY_with_zoom.html] +skip-if = toolkit == 'android' # Android doesn't have full zoom. +[test_eventhandler_scoping.html] +[test_eventTimeStamp.html] +[test_focus_abspos.html] +[test_legacy_event.html] +[test_legacy_non-primary_click.html] +[test_legacy_touch_api.html] +[test_messageEvent.html] +[test_messageEvent_init.html] +[test_mouse_enterleave_iframe.html] +support-files = + file_mouse_enterleave.html +skip-if = + toolkit == 'android' # timeout + http3 +[test_mouse_capture_iframe.html] +support-files = + file_empty.html +skip-if = + toolkit == 'android' # timeout + http3 +[test_moz_mouse_pixel_scroll_event.html] +[test_offsetxy.html] +[test_onerror_handler_args.html] +[test_passive_listeners.html] +[test_paste_image.html] +skip-if = headless # Bug 1405869 + toolkit == 'android' # Image type isn't supported +[test_moving_and_expanding_selection_per_page.html] +support-files = window_empty_document.html +[test_scroll_per_page.html] +support-files = window_empty_document.html +skip-if = toolkit == 'android' # fail +[test_text_event_in_content.html] +[test_use_conflated_keypress_event_model_on_newer_Office_Online_Server.html] +[test_use_split_keypress_event_model_on_old_Confluence.html] +skip-if = !debug # The mode change event is available only on debug build +[test_use_split_keypress_event_model_on_old_Office_Online_Server.html] +skip-if = !debug # The mode change event is available only on debug build +[test_wheel_default_action.html] +skip-if = os == 'linux' + toolkit == 'android' +[test_bug687787.html] +[test_bug1305458.html] +[test_bug1298970.html] +[test_bug1304044.html] +[test_bug1332699.html] +[test_bug1339758.html] +[test_bug1369072.html] +support-files = window_bug1369072.html +skip-if = toolkit == 'android' +[test_bug1429572.html] +support-files = window_bug1429572.html +skip-if = toolkit == 'android' # failed +[test_bug1446834.html] +support-files = file_bug1446834.html +[test_bug1447993.html] +support-files = window_bug1447993.html +skip-if = toolkit == 'android' +[test_bug1484371.html] +support-files = file_bug1484371.html +[test_bug1534562.html] +skip-if = toolkit == 'android' # Bug 1312791 +[test_bug1581192.html] +[test_bug1673434.html] +[test_bug1681800.html] +[test_deltaMode_lines_always_enabled.html] +[test_dnd_with_modifiers.html] +skip-if = toolkit == 'android' # Dnd isn't supported +[test_hover_mouseleave.html] +[test_marquee_events.html] +[test_slotted_mouse_event.html] +skip-if = toolkit == 'android' # timeout +[test_slotted_text_click.html] +[test_unbound_before_in_active_chain.html] +[test_submitevent_on_form.html] +[test_wheel_zoom_on_form_controls.html] +skip-if = verify + toolkit == 'android' # wheel isn't supported +[test_focus_blur_on_click_in_cross_origin_iframe.html] +support-files = + file_focus_blur_on_click_in_cross_origin_iframe.html +skip-if = toolkit == 'android' # Bug 1701546 +[test_focus_blur_on_click_in_deep_cross_origin_iframe.html] +support-files = + file_focus_blur_on_click_in_deep_cross_origin_iframe_inner.html + file_focus_blur_on_click_in_deep_cross_origin_iframe_middle.html +skip-if = toolkit == 'android' # Bug 1701546 + condprof && os == "win" # 1773806 +[test_bug1635018.html] +[test_bug1637259.html] +disabled = Enable this when the taskcluster Windows machine upgrades to RS5+ +run-if = toolkit == 'windows' # Only Windows supports pen input synthesis +support-files = + !/gfx/layers/apz/test/mochitest/apz_test_native_event_utils.js +[test_bug1692277.html] +disabled = Enable this when the taskcluster Windows machine upgrades to RS5+ +run-if = toolkit == 'windows' # Only Windows supports pen input synthesis +support-files = + !/gfx/layers/apz/test/mochitest/apz_test_native_event_utils.js + !/dom/base/test/Ahem.ttf +[test_bug1709832.html] +support-files = + !/dom/base/test/Ahem.ttf +[test_bug1710509.html] +disabled = Enable this when the taskcluster Windows machine upgrades to RS5+ +run-if = toolkit == 'windows' # Only Windows supports pen input synthesis +support-files = + !/gfx/layers/apz/test/mochitest/apz_test_native_event_utils.js +[test_bug1728171.html] +run-if = win10_2004 # Only Windows 1809+ supports pen input synthesis +support-files = + !/gfx/layers/apz/test/mochitest/apz_test_native_event_utils.js diff --git a/dom/events/test/pointerevents/bug1293174_implicit_pointer_capture_for_touch_1.html b/dom/events/test/pointerevents/bug1293174_implicit_pointer_capture_for_touch_1.html new file mode 100644 index 0000000000..55c4e3cad5 --- /dev/null +++ b/dom/events/test/pointerevents/bug1293174_implicit_pointer_capture_for_touch_1.html @@ -0,0 +1,63 @@ +<!doctype html> +<html> + <head> + <title>Pointer Events properties tests</title> + <meta name="viewport" content="width=device-width"> + <link rel="stylesheet" type="text/css" href="wpt/pointerevent_styles.css"> + <script src="/resources/testharness.js"></script> + <!--script src="/resources/testharnessreport.js"></script--> + <!-- Additional helper script for common checks across event types --> + <script type="text/javascript" src="wpt/pointerevent_support.js"></script> + <script> + var detected_pointertypes = {}; + var test_pointerEvent = async_test("implicit pointer capture for touch"); + // showPointerTypes is defined in pointerevent_support.js + // Requirements: the callback function will reference the test_pointerEvent object and + // will fail unless the async_test is created with the var name "test_pointerEvent". + add_completion_callback(showPointerTypes); + + function run() { + let target0 = window.document.getElementById("target0"); + let target1 = window.document.getElementById("target1"); + + on_event(target0, "pointerdown", function (event) { + pointerdown_event = event; + detected_pointertypes[event.pointerType] = true; + assert_true(true, "target0 receives pointerdown"); + }); + + on_event(target0, "pointermove", function (event) { + assert_true(true, "target0 receives pointermove"); + assert_true(target0.hasPointerCapture(event.pointerId), "target0.hasPointerCapture should be true"); + }); + + on_event(target0, "gotpointercapture", function (event) { + assert_true(true, "target0 should receive gotpointercapture"); + }); + + on_event(target0, "lostpointercapture", function (event) { + assert_true(true, "target0 should receive lostpointercapture"); + }); + + on_event(target1, "pointermove", function (event) { + assert_true(false, "target1 should not receive pointermove"); + }); + + on_event(target0, "pointerup", function (event) { + assert_true(true, "target0 receives pointerup"); + test_pointerEvent.done(); + }); + } + </script> + </head> + <body onload="run()"> + <h1>Pointer Events tests</h1> + <div id="target0" style="width: 200px; height: 200px; background: green" touch-action:none></div> + <div id="target1" style="width: 200px; height: 200px; background: green" touch-action:none></div> + <div id="complete-notice"> + <p>The following pointer types were detected: <span id="pointertype-log"></span>.</p> + <p>Refresh the page to run the tests again with a different pointer type.</p> + </div> + <div id="log"></div> + </body> +</html> diff --git a/dom/events/test/pointerevents/bug1293174_implicit_pointer_capture_for_touch_2.html b/dom/events/test/pointerevents/bug1293174_implicit_pointer_capture_for_touch_2.html new file mode 100644 index 0000000000..a533429acb --- /dev/null +++ b/dom/events/test/pointerevents/bug1293174_implicit_pointer_capture_for_touch_2.html @@ -0,0 +1,64 @@ +<!doctype html> +<html> + <head> + <title>Pointer Events properties tests</title> + <meta name="viewport" content="width=device-width"> + <link rel="stylesheet" type="text/css" href="wpt/pointerevent_styles.css"> + <script src="/resources/testharness.js"></script> + <!--script src="/resources/testharnessreport.js"></script--> + <!-- Additional helper script for common checks across event types --> + <script type="text/javascript" src="wpt/pointerevent_support.js"></script> + <script> + var detected_pointertypes = {}; + var test_pointerEvent = async_test("implicit pointer capture for touch"); + // showPointerTypes is defined in pointerevent_support.js + // Requirements: the callback function will reference the test_pointerEvent object and + // will fail unless the async_test is created with the var name "test_pointerEvent". + add_completion_callback(showPointerTypes); + + function run() { + let target0 = window.document.getElementById("target0"); + let target1 = window.document.getElementById("target1"); + + on_event(target0, "pointerdown", function (event) { + pointerdown_event = event; + detected_pointertypes[event.pointerType] = true; + assert_true(true, "target0 receives pointerdown"); + }); + + on_event(target0, "pointermove", function (event) { + assert_true(true, "target0 receives pointermove"); + assert_false(target0.hasPointerCapture(event.pointerId), "target0.hasPointerCapture should be false"); + }); + + on_event(target0, "gotpointercapture", function (event) { + assert_unreached("target0 should not receive gotpointercapture"); + }); + + on_event(target0, "lostpointercapture", function (event) { + assert_unreached("target0 should not receive lostpointercapture"); + }); + + on_event(target1, "pointermove", function (event) { + assert_true(true, "target1 receives pointermove"); + assert_false(target1.hasPointerCapture(event.pointerId), "target1.hasPointerCapture should be false"); + }); + + on_event(target0, "pointerup", function (event) { + assert_true(true, "target0 receives pointerup"); + test_pointerEvent.done(); + }); + } + </script> + </head> + <body onload="run()"> + <h1>Pointer Events tests</h1> + <div id="target0" style="width: 200px; height: 200px; background: green" touch-action:none></div> + <div id="target1" style="width: 200px; height: 200px; background: green" touch-action:none></div> + <div id="complete-notice"> + <p>The following pointer types were detected: <span id="pointertype-log"></span>.</p> + <p>Refresh the page to run the tests again with a different pointer type.</p> + </div> + <div id="log"></div> + </body> +</html> diff --git a/dom/events/test/pointerevents/bug968148_inner.html b/dom/events/test/pointerevents/bug968148_inner.html new file mode 100644 index 0000000000..464e2e5c38 --- /dev/null +++ b/dom/events/test/pointerevents/bug968148_inner.html @@ -0,0 +1,316 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=968148 +--> +<head> + <title>Test for Bug 968148</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <style> + .test { + width: 20px; + height: 20px; + border: 1px solid black; + -moz-user-select: none; + } + </style> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=968148">Mozilla Bug 968148</a> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** + * Test for Bug 968148, test orignally copied from test_bug582771.html. + * Mouse functionality converted to pointer and all steps duplicated in order to run them in parallel for two different pointer Id's +**/ + +function ok(condition, msg) { + parent.ok(condition, msg); +} + +function is(a, b, msg) { + parent.is(a, b, msg); +} + +var test1d1; +var test1d2; +var test2d1; +var test2d2; +var test1d1pointermovecount = 0; +var test1d2pointermovecount = 0; +var test2d1pointermovecount = 0; +var test2d2pointermovecount = 0; + +var test1d1pointerlostcapture = 0; +var test1d2pointerlostcapture = 0; +var test2d1pointerlostcapture = 0; +var test2d2pointerlostcapture = 0; +var test1d1pointergotcapture = 0; +var test1d2pointergotcapture = 0; +var test2d1pointergotcapture = 0; +var test2d2pointergotcapture = 0; +var test1PointerId = 1; +var test2PointerId = 2; +var rx = 1; +var ry = 1; +var angle = 0; +var force = 1; +var modifiers = 0; +var utils = SpecialPowers.getDOMWindowUtils(window); + +function log(s) { + document.getElementById("l").textContent += s + "\n"; +} + +function test1d1IncreasePointerMoveCount(e) { + log(e.type + ", " + e.target.id); + is(e.target, test1d1, "check target is 1d1."); + ++test1d1pointermovecount; +} + +function test1d2IncreasePointerMoveCount(e) { + log(e.type + ", " + e.target.id); + is(e.target, test1d2, "check target is 1d2."); + ++test1d2pointermovecount; +} + +function test2d1IncreasePointerMoveCount(e) { + log(e.type + ", " + e.target.id); + is(e.target, test2d1, "check target is 2d1."); + ++test2d1pointermovecount; +} + +function test2d2IncreasePointerMoveCount(e) { + log(e.type + ", " + e.target.id); + is(e.target, test2d2, "check target is 2d2."); + ++test2d2pointermovecount; +} + +function test1d1CapturePointer(e) { + log(e.type + ", " + e.target.id); + test1d1.setPointerCapture(e.pointerId); +} + +function test1d2CapturePointer(e) { + log(e.type + ", " + e.target.id); + test1d2.setPointerCapture(e.pointerId); +} + +function test2d1CapturePointer(e) { + log(e.type + ", " + e.target.id); + test2d1.setPointerCapture(e.pointerId); +} + +function test2d2CapturePointer(e) { + log(e.type + ", " + e.target.id); + test2d2.setPointerCapture(e.pointerId); +} + +function test1d1PointerGotCapture(e) { + log(e.type + ", " + e.target.id); + ++test1d1pointergotcapture; +} + +function test1d1PointerLostCapture(e) { + log(e.type + ", " + e.target.id); + ++test1d1pointerlostcapture; +} + +function test2d1PointerGotCapture(e) { + log(e.type + ", " + e.target.id); + ++test2d1pointergotcapture; +} + +function test2d1PointerLostCapture(e) { + log(e.type + ", " + e.target.id); + ++test2d1pointerlostcapture; +} + +function test1d2PointerGotCapture(e) { + log(e.type + ", " + e.target.id); + ++test1d2pointergotcapture; +} + +function test1d2PointerLostCapture(e) { + log(e.type + ", " + e.target.id); + ++test1d2pointerlostcapture; +} + +function test2d2PointerGotCapture(e) { + log(e.type + ", " + e.target.id); + ++test2d2pointergotcapture; +} + +function test2d2PointerLostCapture(e) { + log(e.type + ", " + e.target.id); + ++test2d2pointerlostcapture; +} + +function runTests() { + test1d1 = document.getElementById("test1d1"); + test1d2 = document.getElementById("test1d2"); + test2d1 = document.getElementById("test2d1"); + test2d2 = document.getElementById("test2d2"); + + var rect1d1 = test1d1.getBoundingClientRect(); + var rect1d2 = test1d2.getBoundingClientRect(); + var rect2d1 = test2d1.getBoundingClientRect(); + var rect2d2 = test2d2.getBoundingClientRect(); + + var left1d1 = rect1d1.left + 5; + var top1d1 = rect1d1.top + 5; + var left1d2 = rect1d2.left + 5; + var top1d2 = rect1d2.top + 5; + var left2d1 = rect2d1.left + 5; + var top2d1 = rect2d1.top + 5; + var left2d2 = rect2d2.left + 5; + var top2d2 = rect2d2.top + 5; + + test1d1.addEventListener("pointermove", test1d1IncreasePointerMoveCount, true); + test1d2.addEventListener("pointermove", test1d2IncreasePointerMoveCount, true); + test2d1.addEventListener("pointermove", test2d1IncreasePointerMoveCount, true); + test2d2.addEventListener("pointermove", test2d2IncreasePointerMoveCount, true); + + test1d1.addEventListener("gotpointercapture", test1d1PointerGotCapture, true); + test1d1.addEventListener("lostpointercapture", test1d1PointerLostCapture, true); + + test2d1.addEventListener("gotpointercapture", test2d1PointerGotCapture, true); + test2d1.addEventListener("lostpointercapture", test2d1PointerLostCapture, true); + + test1d2.addEventListener("gotpointercapture", test1d2PointerGotCapture, true); + test1d2.addEventListener("lostpointercapture", test1d2PointerLostCapture, true); + + test2d2.addEventListener("gotpointercapture", test2d2PointerGotCapture, true); + test2d2.addEventListener("lostpointercapture", test2d2PointerLostCapture, true); + + document.body.offsetLeft; + + // This shouldn't enable capturing, since we're not in a right kind of + // event listener. + utils.sendTouchEvent('touchstart', [test1PointerId, test2PointerId], + [left1d1, left2d1], [top1d1, top2d1], [rx, rx], [ry, ry], + [angle, angle], [force, force], [0, 0], [0, 0], + [0, 0], modifiers); + + utils.sendTouchEvent('touchmove', [test1PointerId, test2PointerId], + [left1d2, left2d2], [top1d2, top2d2], [rx, rx], [ry, ry], + [angle, angle], [force, force], [0, 0], [0, 0], + [0, 0], modifiers); + + utils.sendTouchEvent('touchend', [test1PointerId, test2PointerId], + [left1d1, left2d1], [top1d1, top2d1], [rx, rx], [ry, ry], + [angle, angle], [force, force], [0, 0], [0, 0], + [0, 0], modifiers); + + // No implicitly / explicitly pointer capture. pointermove should be + // dispatched to 1d2, 2d2 + is(test1d1pointermovecount, 0, "1d1 shouldn't have got pointermove"); + is(test2d1pointermovecount, 0, "2d1 shouldn't have got pointermove"); + is(test1d2pointermovecount, 1, "1d2 should have got pointermove"); + is(test2d2pointermovecount, 1, "2d2 should have got pointermove"); + + // Explicitly capture pointer to 1d1, 2d1 + test1d1.addEventListener("pointerdown", test1d1CapturePointer, {capture: true, once: true}); + test2d1.addEventListener("pointerdown", test2d1CapturePointer, {capture: true, once: true}); + + utils.sendTouchEvent('touchstart', [test1PointerId, test2PointerId], + [left1d1, left2d1], [top1d1, top2d1], [rx, rx], [ry, ry], + [angle, angle], [force, force], [0, 0], [0, 0], + [0, 0], modifiers); + + utils.sendTouchEvent('touchmove', [test1PointerId, test2PointerId], + [left1d2, left2d2], [top1d2, top2d2], [rx, rx], [ry, ry], + [angle, angle], [force, force], [0, 0], [0, 0], + [0, 0], modifiers); + + // Explicitly capture pointer to 1d1, 2d1. pointermove, gotpointercapture + // should be dispatched to 1d1, 2d1 + is(test1d1pointermovecount, 1, "1d1 should have got pointermove"); + is(test1d1pointergotcapture, 1, "1d1 should have got pointergotcapture"); + is(test1d2pointermovecount, 1, "1d2 shouldn't have got pointermove"); + is(test1d2pointergotcapture, 0, "1d2 shouldn't have got pointergotcapture"); + + is(test2d1pointermovecount, 1, "2d1 should have got pointermove"); + is(test2d1pointergotcapture, 1, "2d1 should have got pointergotcapture"); + is(test2d2pointermovecount, 1, "2d2 shouldn't have got pointermove"); + is(test2d2pointergotcapture, 0, "2d2 shouldn't have got pointergotcapture"); + + utils.sendTouchEvent('touchend', [test1PointerId, test2PointerId], + [left1d1, left2d1], [top1d1, top2d1], [rx, rx], [ry, ry], + [angle, angle], [force, force], [0, 0], [0, 0], + [0, 0], modifiers); + + // Explicitly capture pointer to 1d1, 2d1 when pointerdown + test1d1.addEventListener("pointerdown", test1d1CapturePointer, {capture: true, once: true}); + test2d1.addEventListener("pointerdown", test2d1CapturePointer, {capture: true, once: true}); + + // Explicitly capture pointer to 1d2, 2d2 when pointermove + test1d1.addEventListener("pointermove", test1d2CapturePointer, {capture: true, once: true}); + test2d1.addEventListener("pointermove", test2d2CapturePointer, {capture: true, once: true}); + + utils.sendTouchEvent('touchstart', [test1PointerId, test2PointerId], + [left1d1, left2d1], [top1d1, top2d1], [rx, rx], [ry, ry], + [angle, angle], [force, force], [0, 0], [0, 0], + [0, 0], modifiers); + + // This should send pointer event to test1d1, test2d1. + utils.sendTouchEvent('touchmove', [test1PointerId, test2PointerId], + [left1d1 + 5, left2d1 + 5], [top1d1, top2d1], [rx, rx], [ry, ry], + [angle, angle], [force, force], [0, 0], [0, 0], + [0, 0], modifiers); + + // This should send pointer event to test1d2, test2d2. + utils.sendTouchEvent('touchmove', [test1PointerId, test2PointerId], + [left1d1 + 5, left2d1 + 5], [top1d1 + 5, top2d1 + 5], [rx, rx], [ry, ry], + [angle, angle], [force, force], [0, 0], [0, 0], + [0, 0], modifiers); + + is(test1d1pointermovecount, 2, "1d1 should have got pointermove"); + is(test1d1pointergotcapture, 2, "1d1 should have got pointergotcapture"); + is(test1d2pointermovecount, 2, "1d2 should have got pointermove"); + is(test1d2pointergotcapture, 1, "1d2 should have got pointergotcapture"); + + is(test2d1pointermovecount, 2, "2d1 should have got pointermove"); + is(test2d1pointergotcapture, 2, "2d1 should have got pointergotcapture"); + is(test2d2pointermovecount, 2, "2d2 should have got pointermove"); + is(test2d2pointergotcapture, 1, "2d2 should have got pointergotcapture"); + + utils.sendTouchEvent('touchend', [test1PointerId, test2PointerId], + [left1d1, left2d1], [top1d1, top2d1], [rx, rx], [ry, ry], + [angle, angle], [force, force], [0, 0], [0, 0], + [0, 0], modifiers); + + finishTest(); +} + +function finishTest() { + // Let window.onerror have a chance to fire + setTimeout(function() { + setTimeout(function() { + window.parent.postMessage("run next", "*"); + }, 0); + }, 0); +} + +window.onload = function () { + SpecialPowers.pushPrefEnv({ + "set": [ + ["dom.w3c_pointer_events.implicit_capture", false] + ] + }, runTests); +} + +SimpleTest.waitForExplicitFinish(); + +</script> +</pre> +<div class="test" id="test1d1"> </div><br><div class="test" id="test1d2"> </div> +<div class="test" id="test2d1"> </div><br><div class="test" id="test2d2"> </div> +<pre id="l"></pre> +</body> +</html> diff --git a/dom/events/test/pointerevents/bug968148_inner2.html b/dom/events/test/pointerevents/bug968148_inner2.html new file mode 100644 index 0000000000..ffad5297bc --- /dev/null +++ b/dom/events/test/pointerevents/bug968148_inner2.html @@ -0,0 +1,315 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=968148 +--> +<head> + <title>Test for Bug 968148</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <style> + .test { + width: 20px; + height: 20px; + border: 1px solid black; + -moz-user-select: none; + } + </style> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=968148">Mozilla Bug 968148</a> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** + * Test for Bug 968148, test orignally copied from test_bug582771.html. + * Mouse functionality converted to pointer and all steps duplicated in order to run them in parallel for two different pointer Id's +**/ + +function ok(condition, msg) { + parent.ok(condition, msg); +} + +function is(a, b, msg) { + parent.is(a, b, msg); +} + +var test1d1; +var test1d2; +var test2d1; +var test2d2; +var test1d1pointermovecount = 0; +var test1d2pointermovecount = 0; +var test2d1pointermovecount = 0; +var test2d2pointermovecount = 0; + +var test1d1pointerlostcapture = 0; +var test1d2pointerlostcapture = 0; +var test2d1pointerlostcapture = 0; +var test2d2pointerlostcapture = 0; +var test1d1pointergotcapture = 0; +var test1d2pointergotcapture = 0; +var test2d1pointergotcapture = 0; +var test2d2pointergotcapture = 0; +var test1PointerId = 1; +var test2PointerId = 2; +var rx = 1; +var ry = 1; +var angle = 0; +var force = 1; +var modifiers = 0; +var utils = SpecialPowers.getDOMWindowUtils(window); + +function log(s) { + document.getElementById("l").textContent += s + "\n"; +} + +function test1d1IncreasePointerMoveCount(e) { + log(e.type + ", " + e.target.id); + is(e.target, test1d1, "check target is 1d1."); + ++test1d1pointermovecount; +} + +function test1d2IncreasePointerMoveCount(e) { + log(e.type + ", " + e.target.id); + is(e.target, test1d2, "check target is 1d2."); + ++test1d2pointermovecount; +} + +function test2d1IncreasePointerMoveCount(e) { + log(e.type + ", " + e.target.id); + is(e.target, test2d1, "check target is 2d1."); + ++test2d1pointermovecount; +} + +function test2d2IncreasePointerMoveCount(e) { + log(e.type + ", " + e.target.id); + is(e.target, test2d2, "check target is 2d2."); + ++test2d2pointermovecount; +} + +function test1d1CapturePointer(e) { + log(e.type + ", " + e.target.id); + test1d1.setPointerCapture(e.pointerId); +} + +function test1d2CapturePointer(e) { + log(e.type + ", " + e.target.id); + test1d2.setPointerCapture(e.pointerId); +} + +function test2d1CapturePointer(e) { + log(e.type + ", " + e.target.id); + test2d1.setPointerCapture(e.pointerId); +} + +function test2d2CapturePointer(e) { + log(e.type + ", " + e.target.id); + test2d2.setPointerCapture(e.pointerId); +} + +function test1d1PointerGotCapture(e) { + log(e.type + ", " + e.target.id); + ++test1d1pointergotcapture; +} + +function test1d1PointerLostCapture(e) { + log(e.type + ", " + e.target.id); + ++test1d1pointerlostcapture; +} + +function test2d1PointerGotCapture(e) { + log(e.type + ", " + e.target.id); + ++test2d1pointergotcapture; +} + +function test2d1PointerLostCapture(e) { + log(e.type + ", " + e.target.id); + ++test2d1pointerlostcapture; +} + +function test1d2PointerGotCapture(e) { + log(e.type + ", " + e.target.id); + ++test1d2pointergotcapture; +} + +function test1d2PointerLostCapture(e) { + log(e.type + ", " + e.target.id); + ++test1d2pointerlostcapture; +} + +function test2d2PointerGotCapture(e) { + log(e.type + ", " + e.target.id); + ++test2d2pointergotcapture; +} + +function test2d2PointerLostCapture(e) { + log(e.type + ", " + e.target.id); + ++test2d2pointerlostcapture; +} + +function runTests() { + test1d1 = document.getElementById("test1d1"); + test1d2 = document.getElementById("test1d2"); + test2d1 = document.getElementById("test2d1"); + test2d2 = document.getElementById("test2d2"); + + var rect1d1 = test1d1.getBoundingClientRect(); + var rect1d2 = test1d2.getBoundingClientRect(); + var rect2d1 = test2d1.getBoundingClientRect(); + var rect2d2 = test2d2.getBoundingClientRect(); + + var left1d1 = rect1d1.left + 5; + var top1d1 = rect1d1.top + 5; + var left1d2 = rect1d2.left + 5; + var top1d2 = rect1d2.top + 5; + var left2d1 = rect2d1.left + 5; + var top2d1 = rect2d1.top + 5; + var left2d2 = rect2d2.left + 5; + var top2d2 = rect2d2.top + 5; + + test1d1.addEventListener("pointermove", test1d1IncreasePointerMoveCount, true); + test1d2.addEventListener("pointermove", test1d2IncreasePointerMoveCount, true); + test2d1.addEventListener("pointermove", test2d1IncreasePointerMoveCount, true); + test2d2.addEventListener("pointermove", test2d2IncreasePointerMoveCount, true); + + test1d1.addEventListener("gotpointercapture", test1d1PointerGotCapture, true); + test1d1.addEventListener("lostpointercapture", test1d1PointerLostCapture, true); + + test2d1.addEventListener("gotpointercapture", test2d1PointerGotCapture, true); + test2d1.addEventListener("lostpointercapture", test2d1PointerLostCapture, true); + + test1d2.addEventListener("gotpointercapture", test1d2PointerGotCapture, true); + test1d2.addEventListener("lostpointercapture", test1d2PointerLostCapture, true); + + test2d2.addEventListener("gotpointercapture", test2d2PointerGotCapture, true); + test2d2.addEventListener("lostpointercapture", test2d2PointerLostCapture, true); + + document.body.offsetLeft; + + // This shouldn't enable capturing, since we're not in a right kind of + // event listener. + utils.sendTouchEvent('touchstart', [test1PointerId, test2PointerId], + [left1d1, left2d1], [top1d1, top2d1], [rx, rx], [ry, ry], + [angle, angle], [force, force], [0, 0], [0, 0], + [0, 0], modifiers); + + utils.sendTouchEvent('touchmove', [test1PointerId, test2PointerId], + [left1d2, left2d2], [top1d2, top2d2], [rx, rx], [ry, ry], + [angle, angle], [force, force], [0, 0], [0, 0], + [0, 0], modifiers); + + utils.sendTouchEvent('touchend', [test1PointerId, test2PointerId], + [left1d1, left2d1], [top1d1, top2d1], [rx, rx], [ry, ry], + [angle, angle], [force, force], [0, 0], [0, 0], + [0, 0], modifiers); + + // Implicitly pointer capture. pointermove should be dispatched to 1d1, 2d1 + is(test1d1pointermovecount, 1, "1d1 should have got pointermove"); + is(test2d1pointermovecount, 1, "2d1 should have got pointermove"); + is(test1d2pointermovecount, 0, "1d2 shouldn't have got pointermove"); + is(test2d2pointermovecount, 0, "2d2 shouldn't have got pointermove"); + + // Explicitly capture pointer to 1d1, 2d1 + test1d1.addEventListener("pointerdown", test1d1CapturePointer, {capture: true, once: true}); + test2d1.addEventListener("pointerdown", test2d1CapturePointer, {capture: true, once: true}); + + utils.sendTouchEvent('touchstart', [test1PointerId, test2PointerId], + [left1d1, left2d1], [top1d1, top2d1], [rx, rx], [ry, ry], + [angle, angle], [force, force], [0, 0], [0, 0], + [0, 0], modifiers); + + utils.sendTouchEvent('touchmove', [test1PointerId, test2PointerId], + [left1d2, left2d2], [top1d2, top2d2], [rx, rx], [ry, ry], + [angle, angle], [force, force], [0, 0], [0, 0], + [0, 0], modifiers); + + // Explicitly capture pointer to 1d1, 2d1. pointermove, gotpointercapture + // should be dispatched to 1d1, 2d1 + is(test1d1pointermovecount, 2, "1d1 should have got pointermove"); + is(test1d1pointergotcapture, 2, "1d1 should have got pointergotcapture"); + is(test1d2pointermovecount, 0, "1d2 shouldn't have got pointermove"); + is(test1d2pointergotcapture, 0, "1d2 shouldn't have got pointergotcapture"); + + is(test2d1pointermovecount, 2, "2d1 should have got pointermove"); + is(test2d1pointergotcapture, 2, "2d1 should have got pointergotcapture"); + is(test2d2pointermovecount, 0, "2d2 shouldn't have got pointermove"); + is(test2d2pointergotcapture, 0, "2d2 shouldn't have got pointergotcapture"); + + utils.sendTouchEvent('touchend', [test1PointerId, test2PointerId], + [left1d1, left2d1], [top1d1, top2d1], [rx, rx], [ry, ry], + [angle, angle], [force, force], [0, 0], [0, 0], + [0, 0], modifiers); + + // Explicitly capture pointer to 1d1, 2d1 when pointerdown + test1d1.addEventListener("pointerdown", test1d1CapturePointer, {capture: true, once: true}); + test2d1.addEventListener("pointerdown", test2d1CapturePointer, {capture: true, once: true}); + + // Explicitly capture pointer to 1d2, 2d2 when pointermove + test1d1.addEventListener("pointermove", test1d2CapturePointer, {capture: true, once: true}); + test2d1.addEventListener("pointermove", test2d2CapturePointer, {capture: true, once: true}); + + utils.sendTouchEvent('touchstart', [test1PointerId, test2PointerId], + [left1d1, left2d1], [top1d1, top2d1], [rx, rx], [ry, ry], + [angle, angle], [force, force], [0, 0], [0, 0], + [0, 0], modifiers); + + // This should send pointer event to test1d1, test2d1. + utils.sendTouchEvent('touchmove', [test1PointerId, test2PointerId], + [left1d1 + 5, left2d1 + 5], [top1d1, top2d1], [rx, rx], [ry, ry], + [angle, angle], [force, force], [0, 0], [0, 0], + [0, 0], modifiers); + + // This should send pointer event to test1d2, test2d2. + utils.sendTouchEvent('touchmove', [test1PointerId, test2PointerId], + [left1d1 + 5, left2d1 + 5], [top1d1 + 5, top2d1 + 5], [rx, rx], [ry, ry], + [angle, angle], [force, force], [0, 0], [0, 0], + [0, 0], modifiers); + + is(test1d1pointermovecount, 3, "1d1 shouldn't have got pointermove"); + is(test1d1pointergotcapture, 3, "1d1 should have got pointergotcapture"); + is(test1d2pointermovecount, 1, "1d2 should have got pointermove"); + is(test1d2pointergotcapture, 1, "1d2 should have got pointergotcapture"); + + is(test2d1pointermovecount, 3, "2d1 shouldn't have got pointermove"); + is(test2d1pointergotcapture, 3, "2d1 should have got pointergotcapture"); + is(test2d2pointermovecount, 1, "2d2 should have got pointermove"); + is(test2d2pointergotcapture, 1, "2d2 should have got pointergotcapture"); + + utils.sendTouchEvent('touchend', [test1PointerId, test2PointerId], + [left1d1, left2d1], [top1d1, top2d1], [rx, rx], [ry, ry], + [angle, angle], [force, force], [0, 0], [0, 0], + [0, 0], modifiers); + + finishTest(); +} + +function finishTest() { + // Let window.onerror have a chance to fire + setTimeout(function() { + setTimeout(function() { + window.parent.postMessage("finishTest", "*"); + }, 0); + }, 0); +} + +window.onload = function () { + SpecialPowers.pushPrefEnv({ + "set": [ + ["dom.w3c_pointer_events.implicit_capture", true] + ] + }, runTests); +} + +SimpleTest.waitForExplicitFinish(); + +</script> +</pre> +<div class="test" id="test1d1"> </div><br><div class="test" id="test1d2"> </div> +<div class="test" id="test2d1"> </div><br><div class="test" id="test2d2"> </div> +<pre id="l"></pre> +</body> +</html> diff --git a/dom/events/test/pointerevents/bug_1420589_iframe1.html b/dom/events/test/pointerevents/bug_1420589_iframe1.html new file mode 100644 index 0000000000..b18d808b84 --- /dev/null +++ b/dom/events/test/pointerevents/bug_1420589_iframe1.html @@ -0,0 +1,17 @@ +<body> + <script> + let touchEvents = ["touchstart", "touchmove", "touchend"]; + let pointerEvents = ["pointerdown", "pointermove", "pointerup"]; + + touchEvents.forEach((event) => { + document.addEventListener(event, (e) => { + parent.postMessage("iframe1 " + e.type, "*"); + }, { once: true }); + }); + pointerEvents.forEach((event) => { + document.addEventListener(event, (e) => { + parent.postMessage("iframe1 " + e.type, "*"); + }, { once: true }); + }); + </script> +</body> diff --git a/dom/events/test/pointerevents/bug_1420589_iframe2.html b/dom/events/test/pointerevents/bug_1420589_iframe2.html new file mode 100644 index 0000000000..75aea1d187 --- /dev/null +++ b/dom/events/test/pointerevents/bug_1420589_iframe2.html @@ -0,0 +1,17 @@ +<body> + <script> + let touchEvents = ["touchstart", "touchmove", "touchend"]; + let pointerEvents = ["pointerdown", "pointermove", "pointerup"]; + + touchEvents.forEach((event) => { + document.addEventListener(event, (e) => { + parent.postMessage("iframe2 " + e.type, "*"); + }, { once: true }); + }); + pointerEvents.forEach((event) => { + document.addEventListener(event, (e) => { + parent.postMessage("iframe2 " + e.type, "*"); + }, { once: true }); + }); + </script> +</body> diff --git a/dom/events/test/pointerevents/chrome.ini b/dom/events/test/pointerevents/chrome.ini new file mode 100644 index 0000000000..d5dbf50ed5 --- /dev/null +++ b/dom/events/test/pointerevents/chrome.ini @@ -0,0 +1 @@ +[test_bug1697769.xhtml] diff --git a/dom/events/test/pointerevents/file_pointercapture_xorigin_iframe.html b/dom/events/test/pointerevents/file_pointercapture_xorigin_iframe.html new file mode 100644 index 0000000000..88690748e5 --- /dev/null +++ b/dom/events/test/pointerevents/file_pointercapture_xorigin_iframe.html @@ -0,0 +1,62 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1671849 +--> +<head> +<title>Bug 1671849</title> +<script src="/tests/SimpleTest/EventUtils.js"></script> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<script src="pointerevent_utils.js"></script> +<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +<style> +#target { + width: 100px; + height: 100px; + background-color: green; +} +iframe { + width: 400px; + height: 300px; + border: 1px solid blue; +} +</style> +</head> +<body> +<a target="_blank"href="https://bugzilla.mozilla.org/show_bug.cgi?id=1671849">Mozilla Bug 1671849</a> +<div id="target"></div> +<iframe src="https://example.com/tests/dom/events/test/pointerevents/iframe.html"></iframe> + +<pre id="test"> +<script type="text/javascript"> +/** + * Test for Bug 1671849 + */ +add_task(async function test_pointer_capture_xorigin_iframe() { + let iframe = document.querySelector("iframe"); + await SpecialPowers.spawn(iframe.contentWindow, [], () => { + let unexpected = function(e) { + ok(false, `iframe shoule not get any ${e.type} event`); + }; + content.document.body.addEventListener("pointermove", unexpected); + content.document.body.addEventListener("pointerup", unexpected); + }); + + let target = document.getElementById("target"); + synthesizeMouse(target, 10, 10, { type: "mousedown" }); + await waitForEvent(target, "pointerdown", function(e) { + target.setPointerCapture(e.pointerId); + }); + + synthesizeMouse(iframe, 10, 10, { type: "mousemove" }); + await Promise.all([waitForEvent(target, "gotpointercapture"), + waitForEvent(target, "pointermove")]); + + synthesizeMouse(iframe, 10, 10, { type: "mouseup" }); + await Promise.all([waitForEvent(target, "lostpointercapture"), + waitForEvent(target, "pointerup")]); +}); +</script> +</pre> +</body> +</html> diff --git a/dom/events/test/pointerevents/file_pointercapture_xorigin_iframe_pointerlock.html b/dom/events/test/pointerevents/file_pointercapture_xorigin_iframe_pointerlock.html new file mode 100644 index 0000000000..12174da197 --- /dev/null +++ b/dom/events/test/pointerevents/file_pointercapture_xorigin_iframe_pointerlock.html @@ -0,0 +1,111 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1671849 +--> +<head> +<title>Bug 1671849</title> +<script src="/tests/SimpleTest/EventUtils.js"></script> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<script src="pointerevent_utils.js"></script> +<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +<style> +#target { + width: 100px; + height: 100px; + background-color: green; +} +iframe { + width: 400px; + height: 300px; + border: 1px solid blue; +} +</style> +</head> +<body> +<a target="_blank"href="https://bugzilla.mozilla.org/show_bug.cgi?id=1671849">Mozilla Bug 1671849</a> +<div id="target"></div> +<iframe src="https://example.com/tests/dom/events/test/pointerevents/iframe.html"></iframe> + +<pre id="test"> +<script type="text/javascript"> +/** + * Test for Bug 1671849 + */ +function requestPointerLockOnRemoteTarget(aRemoteTarget, aTagName) { + return SpecialPowers.spawn(aRemoteTarget, [aTagName], async (tagName) => { + SpecialPowers.wrap(content.document).notifyUserGestureActivation(); + let target = content.document.querySelector(tagName); + target.requestPointerLock(); + await new Promise((aResolve) => { + let eventHandler = function(e) { + is(e.type, "pointerlockchange", `Got ${e.type} on iframe`); + is(content.document.pointerLockElement, target, `pointer lock element`); + content.document.removeEventListener("pointerlockchange", eventHandler); + content.document.removeEventListener("pointerlockerror", eventHandler); + aResolve(); + }; + content.document.addEventListener("pointerlockchange", eventHandler); + content.document.addEventListener("pointerlockerror", eventHandler); + }); + }); +} + +function exitPointerLockOnRemoteTarget(aRemoteTarget) { + return SpecialPowers.spawn(aRemoteTarget, [], async () => { + content.document.exitPointerLock(); + await new Promise((aResolve) => { + let eventHandler = function(e) { + is(e.type, "pointerlockchange", `Got ${e.type} on iframe`); + is(content.document.pointerLockElement, null, `pointer lock element`); + content.document.removeEventListener("pointerlockchange", eventHandler); + content.document.removeEventListener("pointerlockerror", eventHandler); + aResolve(); + }; + content.document.addEventListener("pointerlockchange", eventHandler); + content.document.addEventListener("pointerlockerror", eventHandler); + }); + }); +} + +function waitEventOnRemoteTarget(aRemoteTarget, aEventName) { + return SpecialPowers.spawn(aRemoteTarget, [aEventName], async (eventName) => { + await new Promise((aResolve) => { + content.document.body.addEventListener(eventName, (e) => { + ok(true, `got ${e.type} event on ${e.target}`); + aResolve(); + }, { once: true }); + }); + }); +} + +add_task(async function test_pointer_capture_xorigin_iframe_pointer_lock() { + await SimpleTest.promiseFocus(); + + // Request pointer capture on top-level. + let target = document.getElementById("target"); + synthesizeMouse(target, 10, 10, { type: "mousedown" }); + await waitForEvent(target, "pointerdown", function(e) { + target.setPointerCapture(e.pointerId); + }); + + let iframe = document.querySelector("iframe"); + synthesizeMouse(iframe, 10, 10, { type: "mousemove" }); + await Promise.all([waitForEvent(target, "gotpointercapture"), + waitForEvent(target, "pointermove")]); + + // Request pointer lock on iframe. + let iframeWin = iframe.contentWindow; + await Promise.all([requestPointerLockOnRemoteTarget(iframeWin, "div"), + waitForEvent(target, "lostpointercapture")]); + + // Exit pointer lock on iframe. + await exitPointerLockOnRemoteTarget(iframeWin); + + synthesizeMouse(target, 10, 10, { type: "mouseup" }); + await waitForEvent(target, "pointerup"); +}); +</script> +</pre> +</body> +</html> diff --git a/dom/events/test/pointerevents/file_pointercapture_xorigin_iframe_touch.html b/dom/events/test/pointerevents/file_pointercapture_xorigin_iframe_touch.html new file mode 100644 index 0000000000..f34605349f --- /dev/null +++ b/dom/events/test/pointerevents/file_pointercapture_xorigin_iframe_touch.html @@ -0,0 +1,69 @@ +<!DOCTYPE HTML> +<html> +<!--https://bugzilla.mozilla.org/show_bug.cgi?id=1662587--> +<head> +<title>Bug 1671849 - Touch</title> +<script src="/tests/SimpleTest/EventUtils.js"></script> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<script src="/tests/SimpleTest/paint_listener.js"></script> +<script src="/tests/gfx/layers/apz/test/mochitest/apz_test_utils.js"></script> +<script src="pointerevent_utils.js"></script> +<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +<style> +#target { + width: 100px; + height: 100px; + background-color: green; +} +iframe { + width: 400px; + height: 300px; + border: 1px solid blue; +} +</style> +</head> +<body> +<a target="_blank"href="https://bugzilla.mozilla.org/show_bug.cgi?id=1671849">Mozilla Bug 1671849</a> +<div id="target"></div> +<iframe src="https://example.com/tests/dom/events/test/pointerevents/iframe.html"></iframe> + +<pre id="test"> +<script type="text/javascript"> +/** + * Test for Bug 1671849 + */ +add_setup(async function() { + await SpecialPowers.pushPrefEnv({ + set: [["dom.w3c_pointer_events.implicit_capture", true]], + }); + await waitUntilApzStable(); +}); + +add_task(async function test_pointer_capture_xorigin_iframe_touch() { + let iframe = document.querySelector("iframe"); + await SpecialPowers.spawn(iframe.contentWindow, [], () => { + let unexpected = function(e) { + ok(false, `iframe shoule not get any ${e.type} event`); + }; + content.document.body.addEventListener("pointermove", unexpected); + content.document.body.addEventListener("pointerup", unexpected); + }); + + let target = document.getElementById("target"); + synthesizeTouch(target, 10, 10, { type: "touchstart" }); + await waitForEvent(target, "touchstart", async function(e) { + e.preventDefault(); + }); + + synthesizeTouch(iframe, 10, 10, { type: "touchmove" }); + await Promise.all([waitForEvent(target, "gotpointercapture"), + waitForEvent(target, "pointermove")]); + + synthesizeTouch(iframe, 10, 10, { type: "touchend" }); + await Promise.all([waitForEvent(target, "lostpointercapture"), + waitForEvent(target, "pointerup")]); +}); +</script> +</pre> +</body> +</html> diff --git a/dom/events/test/pointerevents/file_test_trigger_fullscreen.html b/dom/events/test/pointerevents/file_test_trigger_fullscreen.html new file mode 100644 index 0000000000..2d6549ede7 --- /dev/null +++ b/dom/events/test/pointerevents/file_test_trigger_fullscreen.html @@ -0,0 +1 @@ +<body><div id='target' style='width: 50px; height: 50px; background: green'></div></body> diff --git a/dom/events/test/pointerevents/iframe.html b/dom/events/test/pointerevents/iframe.html new file mode 100644 index 0000000000..0e3eac19b9 --- /dev/null +++ b/dom/events/test/pointerevents/iframe.html @@ -0,0 +1,7 @@ +<html> + <head> + </head> + <body> + <div id="div"></div><input> + </body> +</html> diff --git a/dom/events/test/pointerevents/mochitest.ini b/dom/events/test/pointerevents/mochitest.ini new file mode 100644 index 0000000000..6940c03b36 --- /dev/null +++ b/dom/events/test/pointerevents/mochitest.ini @@ -0,0 +1,168 @@ +[DEFAULT] +prefs = + gfx.font_loader.delay=0 +support-files = + iframe.html + mochitest_support_external.js + mochitest_support_internal.js + wpt/pointerevent_styles.css + wpt/pointerevent_support.js + pointerevent_utils.js + !/gfx/layers/apz/test/mochitest/apz_test_utils.js + +[test_bug968148.html] +support-files = + bug968148_inner.html + bug968148_inner2.html +[test_bug1285128.html] +[test_bug1293174_implicit_pointer_capture_for_touch_1.html] +support-files = bug1293174_implicit_pointer_capture_for_touch_1.html +skip-if = + http3 +[test_bug1293174_implicit_pointer_capture_for_touch_2.html] +support-files = bug1293174_implicit_pointer_capture_for_touch_2.html +skip-if = + http3 +[test_bug1303704.html] +[test_bug1315862.html] +[test_bug1323158.html] +[test_bug1403055.html] +[test_bug1420589_1.html] +support-files = + bug_1420589_iframe1.html + bug_1420589_iframe2.html +[test_bug1420589_2.html] +support-files = + bug_1420589_iframe1.html +[test_bug1420589_3.html] +support-files = + bug_1420589_iframe1.html +[test_bug1725416.html] +skip-if = + os == 'android' # Bug 1312791 +support-files = + !/gfx/layers/apz/test/mochitest/apz_test_native_event_utils.js +[test_multiple_touches.html] +[test_wpt_pointerevent_attributes_hoverable_pointers-manual.html] +support-files = + wpt/pointerevent_attributes_hoverable_pointers-manual.html + wpt/resources/pointerevent_attributes_hoverable_pointers-iframe.html +skip-if = + http3 +[test_wpt_pointerevent_attributes_nohover_pointers-manual.html] +support-files = + wpt/pointerevent_attributes_nohover_pointers-manual.html + wpt/resources/pointerevent_attributes_hoverable_pointers-iframe.html +skip-if = + http3 +[test_wpt_pointerevent_boundary_events_in_capturing-manual.html] +support-files = wpt/pointerevent_boundary_events_in_capturing-manual.html +skip-if = + http3 +[test_wpt_pointerevent_change-touch-action-onpointerdown_touch-manual.html] +support-files = wpt/pointerevent_change-touch-action-onpointerdown_touch-manual.html +disabled = disabled +[test_wpt_pointerevent_constructor.html] +support-files = wpt/pointerevent_constructor.html +skip-if = + http3 +[test_wpt_pointerevent_multiple_primary_pointers_boundary_events-manual.html] +support-files = wpt/pointerevent_multiple_primary_pointers_boundary_events-manual.html +disabled = should be investigated +[test_wpt_pointerevent_pointercancel_touch-manual.html] +support-files = wpt/pointerevent_pointercancel_touch-manual.html +skip-if = + http3 +[test_wpt_pointerevent_pointerId_scope-manual.html] +support-files = wpt/resources/pointerevent_pointerId_scope-iframe.html +disabled = should be investigated +[test_wpt_pointerevent_pointerleave_after_pointercancel_touch-manual.html] +support-files = wpt/pointerevent_pointerleave_after_pointercancel_touch-manual.html +skip-if = + http3 +[test_wpt_pointerevent_pointerleave_pen-manual.html] +support-files = wpt/pointerevent_pointerleave_pen-manual.html +skip-if = + http3 +[test_wpt_pointerevent_pointerout_after_pointercancel_touch-manual.html] +support-files = wpt/pointerevent_pointerout_after_pointercancel_touch-manual.html +skip-if = + http3 +[test_wpt_pointerevent_pointerout_pen-manual.html] +support-files = wpt/pointerevent_pointerout_pen-manual.html +skip-if = + http3 +[test_wpt_pointerevent_releasepointercapture_events_to_original_target-manual.html] +support-files = wpt/pointerevent_releasepointercapture_events_to_original_target-manual.html +skip-if = + http3 +[test_wpt_pointerevent_releasepointercapture_onpointercancel_touch-manual.html] +support-files = wpt/pointerevent_releasepointercapture_onpointercancel_touch-manual.html +skip-if = + http3 +[test_wpt_pointerevent_sequence_at_implicit_release_on_drag-manual.html] +support-files = wpt/pointerevent_sequence_at_implicit_release_on_drag-manual.html +skip-if = + http3 +[test_wpt_pointerevent_drag_interaction-manual.html] +support-files = wpt/html/pointerevent_drag_interaction-manual.html +skip-if = + http3 +[test_wpt_touch_action.html] +skip-if = + os == 'android' # Bug 1312791 + os == 'linux' && headless # Bug 1722906 +support-files = + ../../../../gfx/layers/apz/test/mochitest/apz_test_utils.js + ../../../../gfx/layers/apz/test/mochitest/apz_test_native_event_utils.js + touch_action_helpers.js + wpt/pointerevent_touch-action-auto-css_touch-manual.html + wpt/pointerevent_touch-action-button-test_touch-manual.html + wpt/pointerevent_touch-action-inherit_child-auto-child-none_touch-manual.html + wpt/pointerevent_touch-action-inherit_child-none_touch-manual.html + wpt/pointerevent_touch-action-inherit_child-pan-x-child-pan-x_touch-manual.html + wpt/pointerevent_touch-action-inherit_child-pan-x-child-pan-y_touch-manual.html + wpt/pointerevent_touch-action-inherit_highest-parent-none_touch-manual.html + wpt/pointerevent_touch-action-inherit_parent-none_touch-manual.html + wpt/pointerevent_touch-action-none-css_touch-manual.html + wpt/pointerevent_touch-action-pan-x-css_touch-manual.html + wpt/pointerevent_touch-action-pan-x-pan-y-pan-y_touch-manual.html + wpt/pointerevent_touch-action-pan-x-pan-y_touch-manual.html + wpt/pointerevent_touch-action-pan-y-css_touch-manual.html + wpt/pointerevent_touch-action-span-test_touch-manual.html + wpt/pointerevent_touch-action-svg-test_touch-manual.html + wpt/pointerevent_touch-action-table-test_touch-manual.html + wpt/pointerevent_touch-action-pan-down-css_touch-manual.html + wpt/pointerevent_touch-action-pan-left-css_touch-manual.html + wpt/pointerevent_touch-action-pan-right-css_touch-manual.html + wpt/pointerevent_touch-action-pan-up-css_touch-manual.html +[test_wpt_pointerevent_movementxy-manual.html] +support-files = + wpt/pointerlock/pointerevent_movementxy-manual.html + wpt/pointerlock/resources/pointerevent_movementxy-iframe.html +skip-if = + http3 +[test_trigger_fullscreen_by_pointer_events.html] +support-files = + file_test_trigger_fullscreen.html +[test_remove_frame_when_got_pointer_capture.html] +[test_getCoalescedEvents.html] +skip-if = + os == 'android' # Bug 1312791 + verify && os == 'win' # Bug 1659744 +[test_getCoalescedEvents_touch.html] +skip-if = + os == 'android' # Bug 1312791 + verify && os == 'win' # Bug 1659744 + win11_2009 # Bug 1781388 +support-files = + !/gfx/layers/apz/test/mochitest/apz_test_native_event_utils.js +[test_pointercapture_xorigin_iframe.html] +support-files = + file_pointercapture_xorigin_iframe.html + file_pointercapture_xorigin_iframe_pointerlock.html + file_pointercapture_xorigin_iframe_touch.html +[test_pointercapture_remove_iframe.html] +[test_pointermove_drag_scrollbar.html] +skip-if = os == 'android' # scrollbar not showed on mobile +[test_synthesized_touch.html] diff --git a/dom/events/test/pointerevents/mochitest_support_external.js b/dom/events/test/pointerevents/mochitest_support_external.js new file mode 100644 index 0000000000..8f190e2c84 --- /dev/null +++ b/dom/events/test/pointerevents/mochitest_support_external.js @@ -0,0 +1,270 @@ +// This file supports translating W3C tests +// to tests on auto MochiTest system with minimum changes. +// Author: Maksim Lebedev <alessarik@gmail.com> + +// Function allows to prepare our tests after load document +addEventListener( + "load", + function (event) { + console.log("OnLoad external document"); + prepareTest(); + }, + false +); + +// Function allows to initialize prerequisites before testing +function prepareTest() { + SimpleTest.waitForExplicitFinish(); + SimpleTest.requestCompleteLog(); + startTest(); +} + +function setImplicitPointerCapture(capture, callback) { + console.log("SET dom.w3c_pointer_events.implicit_capture as " + capture); + SpecialPowers.pushPrefEnv( + { + set: [["dom.w3c_pointer_events.implicit_capture", capture]], + }, + callback + ); +} + +var utils = SpecialPowers.Ci.nsIDOMWindowUtils; + +// Mouse Event Helper Object +var MouseEventHelper = (function () { + return { + MOUSE_ID: utils.DEFAULT_MOUSE_POINTER_ID, + PEN_ID: utils.DEFAULT_PEN_POINTER_ID, + // State + // TODO: Separate this to support mouse and pen simultaneously. + BUTTONS_STATE: utils.MOUSE_BUTTONS_NO_BUTTON, + + // Button + BUTTON_NONE: -1, // Used by test framework only. (replaced before sending) + BUTTON_LEFT: utils.MOUSE_BUTTON_LEFT_BUTTON, + BUTTON_MIDDLE: utils.MOUSE_BUTTON_MIDDLE_BUTTON, + BUTTON_RIGHT: utils.MOUSE_BUTTON_RIGHT_BUTTON, + + // Buttons + BUTTONS_NONE: utils.MOUSE_BUTTONS_NO_BUTTON, + BUTTONS_LEFT: utils.MOUSE_BUTTONS_LEFT_BUTTON, + BUTTONS_MIDDLE: utils.MOUSE_BUTTONS_MIDDLE_BUTTON, + BUTTONS_RIGHT: utils.MOUSE_BUTTONS_RIGHT_BUTTON, + BUTTONS_4TH: utils.MOUSE_BUTTONS_4TH_BUTTON, + BUTTONS_5TH: utils.MOUSE_BUTTONS_5TH_BUTTON, + + // Utils + computeButtonsMaskFromButton(aButton) { + // Since the range of button values is 0 ~ 2 (see nsIDOMWindowUtils.idl), + // we can use an array to find out the desired mask. + var mask = [ + this.BUTTONS_NONE, // -1 (MouseEventHelper.BUTTON_NONE) + this.BUTTONS_LEFT, // 0 + this.BUTTONS_MIDDLE, // 1 + this.BUTTONS_RIGHT, // 2 + ][aButton + 1]; + + ok(mask !== undefined, "Unrecognized button value caught!"); + return mask; + }, + + checkExitState() { + ok(!this.BUTTONS_STATE, "Mismatched mousedown/mouseup caught."); + }, + }; +})(); + +function createMouseEvent(aEventType, aParams) { + var eventObj = { type: aEventType }; + + // Default to mouse. + eventObj.inputSource = + aParams && "inputSource" in aParams + ? aParams.inputSource + : MouseEvent.MOZ_SOURCE_MOUSE; + // Compute pointerId + eventObj.id = + eventObj.inputSource === MouseEvent.MOZ_SOURCE_MOUSE + ? MouseEventHelper.MOUSE_ID + : MouseEventHelper.PEN_ID; + // Check or generate a |button| value. + var isButtonEvent = aEventType === "mouseup" || aEventType === "mousedown"; + + // Set |button| to the default value first. + eventObj.button = isButtonEvent + ? MouseEventHelper.BUTTON_LEFT + : MouseEventHelper.BUTTON_NONE; + + // |button| is passed, use and check it. + if (aParams && "button" in aParams) { + var hasButtonValue = aParams.button !== MouseEventHelper.BUTTON_NONE; + ok( + !isButtonEvent || hasButtonValue, + "Inappropriate |button| value caught." + ); + eventObj.button = aParams.button; + } + + // Generate a |buttons| value and update buttons state + var buttonsMask = MouseEventHelper.computeButtonsMaskFromButton( + eventObj.button + ); + switch (aEventType) { + case "mousedown": + MouseEventHelper.BUTTONS_STATE |= buttonsMask; // Set button flag. + break; + case "mouseup": + MouseEventHelper.BUTTONS_STATE &= ~buttonsMask; // Clear button flag. + break; + } + eventObj.buttons = MouseEventHelper.BUTTONS_STATE; + + // Replace the button value for mousemove events. + // Since in widget level design, even when no button is pressed at all, the + // value of WidgetMouseEvent.button is still 0, which is the same value as + // the one for mouse left button. + if (aEventType === "mousemove") { + eventObj.button = MouseEventHelper.BUTTON_LEFT; + } + return eventObj; +} + +// Helper function to send MouseEvent with different parameters +function sendMouseEvent(int_win, elemId, mouseEventType, params) { + var elem = int_win.document.getElementById(elemId); + if (elem) { + var rect = elem.getBoundingClientRect(); + var eventObj = createMouseEvent(mouseEventType, params); + + // Default to the center of the target element but we can still send to a + // position outside of the target element. + var offsetX = + params && "offsetX" in params ? params.offsetX : rect.width / 2; + var offsetY = + params && "offsetY" in params ? params.offsetY : rect.height / 2; + + console.log(elemId, eventObj); + synthesizeMouse(elem, offsetX, offsetY, eventObj, int_win); + } else { + is(!!elem, true, "Document should have element with id: " + elemId); + } +} + +// Helper function to send MouseEvent with position +function sendMouseEventAtPoint(aWindow, aLeft, aTop, aMouseEventType, aParams) { + var eventObj = createMouseEvent(aMouseEventType, aParams); + console.log(eventObj); + synthesizeMouseAtPoint(aLeft, aTop, eventObj, aWindow); +} + +// Touch Event Helper Object +var TouchEventHelper = { + // State + TOUCH_ID: utils.DEFAULT_TOUCH_POINTER_ID, + TOUCH_STATE: false, + + // Utils + checkExitState() { + ok(!this.TOUCH_STATE, "Mismatched touchstart/touchend caught."); + }, +}; + +// Helper function to send TouchEvent with different parameters +// TODO: Support multiple touch points to test more features such as +// PointerEvent.isPrimary and pinch-zoom. +function sendTouchEvent(int_win, elemId, touchEventType, params) { + var elem = int_win.document.getElementById(elemId); + if (elem) { + var rect = elem.getBoundingClientRect(); + var eventObj = { + type: touchEventType, + id: TouchEventHelper.TOUCH_ID, + }; + + // Update touch state + switch (touchEventType) { + case "touchstart": + TouchEventHelper.TOUCH_STATE = true; // Set touch flag. + break; + case "touchend": + case "touchcancel": + TouchEventHelper.TOUCH_STATE = false; // Clear touch flag. + break; + } + + // Default to the center of the target element but we can still send to a + // position outside of the target element. + var offsetX = + params && "offsetX" in params ? params.offsetX : rect.width / 2; + var offsetY = + params && "offsetY" in params ? params.offsetY : rect.height / 2; + + console.log(elemId, eventObj); + synthesizeTouch(elem, offsetX, offsetY, eventObj, int_win); + } else { + is(!!elem, true, "Document should have element with id: " + elemId); + } +} + +// Helper function to trigger drag and drop. +async function doDragAndDrop(int_win, srcElemId, destElemId, params = {}) { + params.srcElement = int_win.document.getElementById(srcElemId); + params.destElement = int_win.document.getElementById(destElemId); + params.srcWindow = int_win; + params.destWindow = int_win; + params.id = MouseEventHelper.MOUSE_ID; + // This is basically for android which has a larger drag threshold. + params.stepY = params.stepY || 25; + await synthesizePlainDragAndDrop(params); +} + +// Helper function to run Point Event test in a new tab. +function runTestInNewWindow(aFile) { + var testURL = + location.href.substring(0, location.href.lastIndexOf("/") + 1) + aFile; + var testWindow = window.open(testURL, "_blank"); + var testDone = false; + + // We start testing when receiving load event. Inject the mochitest helper js + // to the test case after DOM elements are constructed and before the load + // event is fired. + testWindow.addEventListener( + "DOMContentLoaded", + function () { + var e = testWindow.document.createElement("script"); + e.type = "text/javascript"; + e.src = + "../".repeat(aFile.split("/").length - 1) + + "mochitest_support_internal.js"; + testWindow.document.getElementsByTagName("head")[0].appendChild(e); + }, + { once: true } + ); + + window.addEventListener("message", function (aEvent) { + switch (aEvent.data.type) { + case "START": + // Update constants + MouseEventHelper.MOUSE_ID = aEvent.data.message.mouseId; + MouseEventHelper.PEN_ID = aEvent.data.message.penId; + TouchEventHelper.TOUCH_ID = aEvent.data.message.touchId; + + executeTest(testWindow); + return; + case "RESULT": + // Should not perform checking after SimpleTest.finish(). + if (!testDone) { + ok(aEvent.data.result, aEvent.data.message); + } + return; + case "FIN": + testDone = true; + MouseEventHelper.checkExitState(); + TouchEventHelper.checkExitState(); + testWindow.close(); + SimpleTest.finish(); + return; + } + }); +} diff --git a/dom/events/test/pointerevents/mochitest_support_internal.js b/dom/events/test/pointerevents/mochitest_support_internal.js new file mode 100644 index 0000000000..d46dd14f32 --- /dev/null +++ b/dom/events/test/pointerevents/mochitest_support_internal.js @@ -0,0 +1,125 @@ +// This file supports translating W3C tests +// to tests on auto MochiTest system with minimum changes. +// Author: Maksim Lebedev <alessarik@gmail.com> + +const PARENT_ORIGIN = "http://mochi.test:8888/"; + +// Since web platform tests don't check pointerId, we have to use some heuristic +// to test them. and thus pointerIds are send to mochitest_support_external.js +// before we start sending synthesized widget events. Here, we avoid using +// default values used in Gecko to insure everything works as expected. +const POINTER_MOUSE_ID = 7; +const POINTER_PEN_ID = 8; +const POINTER_TOUCH_ID = 9; // Extend for multiple touch points if needed. + +// Setup environment. +addListeners(document.getElementById("target0")); +addListeners(document.getElementById("target1")); + +// Setup communication between mochitest_support_external.js. +// Function allows to initialize prerequisites before testing +// and adds some callbacks to support mochitest system. +function resultCallback(aTestObj) { + var message = aTestObj.name + " ("; + message += "Get: " + JSON.stringify(aTestObj.status) + ", "; + message += "Expect: " + JSON.stringify(aTestObj.PASS) + ")"; + window.opener.postMessage( + { + type: "RESULT", + message, + result: aTestObj.status === aTestObj.PASS, + }, + PARENT_ORIGIN + ); +} + +add_result_callback(resultCallback); +add_completion_callback(() => { + window.opener.postMessage({ type: "FIN" }, PARENT_ORIGIN); +}); + +window.addEventListener("load", () => { + // Start testing. + var startMessage = { + type: "START", + message: { + mouseId: POINTER_MOUSE_ID, + penId: POINTER_PEN_ID, + touchId: POINTER_TOUCH_ID, + }, + }; + window.opener.postMessage(startMessage, PARENT_ORIGIN); +}); + +function addListeners(elem) { + if (!elem) { + return; + } + var All_Events = [ + "pointerdown", + "pointerup", + "pointercancel", + "pointermove", + "pointerover", + "pointerout", + "pointerenter", + "pointerleave", + "gotpointercapture", + "lostpointercapture", + ]; + All_Events.forEach(function (name) { + elem.addEventListener(name, function (event) { + console.log("(" + event.type + ")-(" + event.pointerType + ")"); + + // Perform checks only for trusted events. + if (!event.isTrusted) { + return; + } + + // Compute the desired event.pointerId from event.pointerType. + var pointerId = { + mouse: POINTER_MOUSE_ID, + pen: POINTER_PEN_ID, + touch: POINTER_TOUCH_ID, + }[event.pointerType]; + + // Compare the pointerId. + resultCallback({ + name: "Mismatched event.pointerId recieved.", + status: event.pointerId, + PASS: pointerId, + }); + }); + }); +} + +// mock the touchScrollInTarget to make the test work. +function touchScrollInTarget() { + return Promise.resolve(); +} + +// mock test_driver to make the test work. +function Actions() {} +Actions.prototype = { + addPointer() { + return this; + }, + pointerMove() { + return this; + }, + pointerDown() { + return this; + }, + pause() { + return this; + }, + pointerUp() { + return this; + }, + send() { + return Promise.resolve(); + }, +}; +const test_driver = { + Actions, +}; diff --git a/dom/events/test/pointerevents/pointerevent_utils.js b/dom/events/test/pointerevents/pointerevent_utils.js new file mode 100644 index 0000000000..ac5eaf61a1 --- /dev/null +++ b/dom/events/test/pointerevents/pointerevent_utils.js @@ -0,0 +1,60 @@ +// Get test filename for page being run in popup so errors are more useful +var testName = location.pathname.split("/").pop(); + +// Wrap test functions and pass to parent window +window.ok = function (a, msg) { + opener.ok(a, testName + ": " + msg); +}; + +window.is = function (a, b, msg) { + opener.is(a, b, testName + ": " + msg); +}; + +window.isnot = function (a, b, msg) { + opener.isnot(a, b, testName + ": " + msg); +}; + +window.todo = function (a, msg) { + opener.todo(a, testName + ": " + msg); +}; + +window.todo_is = function (a, b, msg) { + opener.todo_is(a, b, testName + ": " + msg); +}; + +window.todo_isnot = function (a, b, msg) { + opener.todo_isnot(a, b, testName + ": " + msg); +}; + +window.info = function (msg) { + opener.info(testName + ": " + msg); +}; + +// Override bits of SimpleTest so test files work stand-alone +var SimpleTest = SimpleTest || {}; + +SimpleTest.waitForExplicitFinish = function () { + dump("[POINTEREVENT] Starting " + testName + "\n"); +}; + +SimpleTest.finish = function () { + dump("[POINTEREVENT] Finishing " + testName + "\n"); + opener.nextTest(); +}; + +// Utility functions +function waitForEvent(aTarget, aEvent, aCallback) { + return new Promise(aResolve => { + aTarget.addEventListener( + aEvent, + e => { + ok(true, `got ${e.type} event on ${e.target.id}`); + if (aCallback) { + aCallback(e); + } + aResolve(); + }, + { once: true } + ); + }); +} diff --git a/dom/events/test/pointerevents/readme.md b/dom/events/test/pointerevents/readme.md new file mode 100644 index 0000000000..0cc0190979 --- /dev/null +++ b/dom/events/test/pointerevents/readme.md @@ -0,0 +1,9 @@ +Directory for Pointer Events Tests + +Latest Editor's Draft: https://w3c.github.io/pointerevents/ + +Latest W3C Technical Report: http://www.w3.org/TR/pointerevents/ + +Discussion forum for tests: http://lists.w3.org/Archives/Public/public-test-infra/ + +Test Assertion table: https://www.w3.org/wiki/PointerEvents/TestAssertions diff --git a/dom/events/test/pointerevents/test_bug1285128.html b/dom/events/test/pointerevents/test_bug1285128.html new file mode 100644 index 0000000000..534e265047 --- /dev/null +++ b/dom/events/test/pointerevents/test_bug1285128.html @@ -0,0 +1,51 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1285128 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1285128</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1285128">Mozilla Bug 1285128</a> +<p id="display"></p> +<div id="target0" style="width: 50px; height: 50px; background: green"></div> +<div id="target1" style="width: 50px; height: 50px; background: red"></div> +<script type="text/javascript"> + +/** Test for Bug 1285128 **/ +SimpleTest.waitForExplicitFinish(); + +function runTests() { + let target0 = window.document.getElementById("target0"); + let pointerEventsList = ["pointerover", "pointerenter", "pointerdown", + "pointerup", "pointerleave", "pointerout"]; + let receivedPointerEvents = false; + pointerEventsList.forEach((elem, index, arr) => { + target0.addEventListener(elem, (event) => { + ok(false, "receiving event " + event.type); + receivedPointerEvents = true; + }); + }); + + target1.addEventListener("mouseup", () => { + ok(!receivedPointerEvents, "synthesized mousemove should not trigger any pointer events"); + SimpleTest.finish(); + }); + + synthesizeMouseAtCenter(target0, { type: "mousemove", + inputSource: MouseEvent.MOZ_SOURCE_MOUSE, + isWidgetEventSynthesized: true }); + synthesizeMouseAtCenter(target1, { type: "mousedown" }); + synthesizeMouseAtCenter(target1, { type: "mouseup" }); +} + +SimpleTest.waitForFocus(runTests); + +</script> +</body> +</html> diff --git a/dom/events/test/pointerevents/test_bug1293174_implicit_pointer_capture_for_touch_1.html b/dom/events/test/pointerevents/test_bug1293174_implicit_pointer_capture_for_touch_1.html new file mode 100644 index 0000000000..a02432f3b3 --- /dev/null +++ b/dom/events/test/pointerevents/test_bug1293174_implicit_pointer_capture_for_touch_1.html @@ -0,0 +1,30 @@ +<!DOCTYPE HTML> +<html> + <head> + <meta charset="utf-8"> + <title>Test for Bug 1293174</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="mochitest_support_external.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="text/javascript"> + SimpleTest.waitForExplicitFinish(); + function startTest() { + setImplicitPointerCapture(true, loadSubFrame); + } + function loadSubFrame() { + runTestInNewWindow("bug1293174_implicit_pointer_capture_for_touch_1.html"); + } + function executeTest(int_win) { + sendTouchEvent(int_win, "target0", "touchstart"); + sendTouchEvent(int_win, "target0", "touchmove"); + sendTouchEvent(int_win, "target1", "touchmove"); + sendTouchEvent(int_win, "target0", "touchmove"); + sendTouchEvent(int_win, "target0", "touchend"); + } + </script> + </head> + <body> + </body> +</html> + diff --git a/dom/events/test/pointerevents/test_bug1293174_implicit_pointer_capture_for_touch_2.html b/dom/events/test/pointerevents/test_bug1293174_implicit_pointer_capture_for_touch_2.html new file mode 100644 index 0000000000..2e5aaccccc --- /dev/null +++ b/dom/events/test/pointerevents/test_bug1293174_implicit_pointer_capture_for_touch_2.html @@ -0,0 +1,30 @@ +<!DOCTYPE HTML> +<html> + <head> + <meta charset="utf-8"> + <title>Test for Bug 1293174</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="mochitest_support_external.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="text/javascript"> + SimpleTest.waitForExplicitFinish(); + function startTest() { + setImplicitPointerCapture(false, loadSubFrame); + } + function loadSubFrame() { + runTestInNewWindow("bug1293174_implicit_pointer_capture_for_touch_2.html"); + } + function executeTest(int_win) { + sendTouchEvent(int_win, "target0", "touchstart"); + sendTouchEvent(int_win, "target0", "touchmove"); + sendTouchEvent(int_win, "target1", "touchmove"); + sendTouchEvent(int_win, "target0", "touchmove"); + sendTouchEvent(int_win, "target0", "touchend"); + } + </script> + </head> + <body> + </body> +</html> + diff --git a/dom/events/test/pointerevents/test_bug1303704.html b/dom/events/test/pointerevents/test_bug1303704.html new file mode 100644 index 0000000000..ed8842997c --- /dev/null +++ b/dom/events/test/pointerevents/test_bug1303704.html @@ -0,0 +1,135 @@ +<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1303704
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1303704</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="text/javascript" src="/tests/gfx/layers/apz/test/mochitest/apz_test_utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <style>
+ #scrollable {
+ height: 80px;
+ width: 200px;
+ overflow-y: scroll;
+ margin-bottom: 50px;
+ scroll-behavior: auto;
+ }
+ </style>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1303704">Mozilla Bug 1303704</a>
+<p id="display"></p>
+<a id="link1" href="http://www.google.com">Link 1</a>
+<div id="scrollable">
+<pre>
+scroll
+scroll
+scroll
+scroll
+scroll
+scroll
+scroll
+scroll
+scroll
+scroll
+scroll
+scroll
+scroll
+scroll
+scroll
+scroll
+scroll
+scroll
+scroll
+scroll
+</pre>
+</div>
+<script type="text/javascript">
+
+/** Test for Bug 1303704 **/
+SimpleTest.waitForExplicitFinish();
+
+function runTests() {
+ let link1 = window.document.getElementById("link1");
+ let mouseEvents = ["mousedown", "mouseup", "mousemove"];
+
+ link1.addEventListener("pointerdown", (e) => {
+ e.preventDefault();
+ is(e.defaultPrevented, true, "defaultPrevented should be true");
+ });
+
+ mouseEvents.forEach((elm, index, arr) => {
+ link1.addEventListener(elm, () => {
+ ok(false, "Should not receive " + elm + " after preventDefault on pointerdown");
+ });
+ });
+
+ link1.addEventListener("click", (e) => {
+ e.preventDefault();
+ });
+
+ synthesizeMouseAtCenter(link1, { type: "mousedown",
+ inputSource: MouseEvent.MOZ_SOURCE_MOUSE });
+ synthesizeMouseAtCenter(link1, { type: "mousemove",
+ inputSource: MouseEvent.MOZ_SOURCE_MOUSE });
+ synthesizeMouseAtCenter(link1, { type: "mouseup",
+ inputSource: MouseEvent.MOZ_SOURCE_MOUSE });
+
+ if (navigator.userAgent.includes("Android") ||
+ navigator.userAgent.includes("Mac") ||
+ SpecialPowers.Cc["@mozilla.org/gfx/info;1"].
+ getService(SpecialPowers.Ci.nsIGfxInfo).isHeadless) {
+ SimpleTest.finish();
+ return;
+ }
+
+ async function scrollTest() {
+ var scrollable = document.getElementById("scrollable");
+ scrollable.addEventListener('pointerdown', function(ev) {
+ ev.preventDefault();
+ }, true);
+ var rect = scrollable.getBoundingClientRect();
+ var offsetX = rect.width - 5;
+ var offsetY = rect.height - 5;
+ synthesizeMouse(scrollable, offsetX, offsetY,
+ { type: "mousedown",
+ inputSource: MouseEvent.MOZ_SOURCE_MOUSE });
+
+ synthesizeMouse(scrollable, offsetX, offsetY,
+ { type: "mousemove",
+ inputSource: MouseEvent.MOZ_SOURCE_MOUSE });
+
+ synthesizeMouse(scrollable, offsetX, offsetY,
+ { type: "mouseup",
+ inputSource: MouseEvent.MOZ_SOURCE_MOUSE });
+
+ await waitToClearOutAnyPotentialScrolls(window);
+
+ if (scrollable.scrollTop != 0) {
+ isnot(scrollable.scrollTop, 0,
+ "Scrollable element should have been scrolled.");
+ SimpleTest.finish();
+ } else {
+ setTimeout(scrollTest);
+ }
+ }
+
+ setTimeout(() => {
+ var scrollable = document.getElementById("scrollable");
+ is(scrollable.scrollTop, 0,
+ "Scrollable element shouldn't be scrolled initially");
+ scrollTest();
+ });
+}
+
+SimpleTest.waitForFocus(() => {
+ SpecialPowers.pushPrefEnv({"set": [["general.smoothScroll", false]]}, runTests);
+});
+
+</script>
+</body>
+</html>
diff --git a/dom/events/test/pointerevents/test_bug1315862.html b/dom/events/test/pointerevents/test_bug1315862.html new file mode 100644 index 0000000000..92d61a518f --- /dev/null +++ b/dom/events/test/pointerevents/test_bug1315862.html @@ -0,0 +1,66 @@ +<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1315862
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1315862</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<p id="content">
+ This is a test to check if pointer events are dispatched in the system group
+</p>
+<script type="text/javascript">
+
+/** Test for Bug 1315862 **/
+SimpleTest.waitForExplicitFinish();
+
+function runTests() {
+ let allPointerEvents = ["pointerdown", "pointerup", "pointercancel",
+ "pointermove", "pointerover", "pointerout",
+ "pointerenter", "pointerleave", "gotpointercapture",
+ "lostpointercapture"
+ ];
+ let content = document.getElementById('content');
+ let iframe = document.createElement('iframe');
+ let receivePointerEvents = false;
+ iframe.width = 50;
+ iframe.height = 50;
+ content.appendChild(iframe);
+ iframe.contentDocument.body.innerHTML =
+ "<div style='width: 100%; height: 100%; border: 1px solid black;'></div>";
+
+ let target = iframe.contentDocument.body.firstChild;
+ allPointerEvents.forEach((event, idx, arr) => {
+ SpecialPowers.addSystemEventListener(target, event, () => {
+ ok(false, "Shouldn't dispatch " + event + " in the system group");
+ receivePointerEvents = true;
+ });
+ });
+ target.addEventListener("pointerdown", (e) => {
+ target.setPointerCapture(e.pointerId);
+ });
+ target.addEventListener("pointerup", () => {
+ is(receivePointerEvents, false, "Shouldn't dispatch pointer events in the system group");
+ SimpleTest.finish();
+ });
+ let source = MouseEvent.MOZ_SOURCE_MOUSE;
+ synthesizeMouse(target, 5, 5, { type: "mousemove", inputSource: source },
+ iframe.contentWindow);
+ synthesizeMouse(target, 5, 5, { type: "mousedown", inputSource: source },
+ iframe.contentWindow);
+ synthesizeMouse(target, 5, 5, { type: "mousemove", inputSource: source },
+ iframe.contentWindow);
+ synthesizeMouse(target, 5, 5, { type: "mouseup", inputSource: source },
+ iframe.contentWindow);
+}
+
+SimpleTest.waitForFocus(runTests);
+
+</script>
+</body>
+</html>
diff --git a/dom/events/test/pointerevents/test_bug1323158.html b/dom/events/test/pointerevents/test_bug1323158.html new file mode 100644 index 0000000000..96fd2de12e --- /dev/null +++ b/dom/events/test/pointerevents/test_bug1323158.html @@ -0,0 +1,91 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1323158 +--> +<head> + <meta charset="utf-8"> + <title>This is a test to check if target and relatedTarget of mouse events are the same as pointer events</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<p id="content"></p> +<script type="text/javascript"> + +/** Test for Bug 1323158 **/ +SimpleTest.waitForExplicitFinish(); + +function runTests() { + let content = document.getElementById('content'); + let iframe = document.createElement('iframe'); + iframe.width = 500; + iframe.height = 500; + content.appendChild(iframe); + iframe.contentDocument.body.innerHTML = + "<br><div id='target0' style='width: 30px; height: 30px; background: black;'></div>" + + "<br><div id='target1' style='width: 30px; height: 30px; background: red;'></div>" + + "<br><div id='done' style='width: 30px; height: 30px; background: green;'></div>"; + + let target0 = iframe.contentDocument.getElementById("target0"); + let target1 = iframe.contentDocument.getElementById("target1"); + let done = iframe.contentDocument.getElementById("done"); + let pointerBoundaryEvents = ["pointerover", "pointerenter", "pointerleave", "pointerout"]; + let mouseBoundaryEvents = ["mouseover", "mouseenter", "mouseleave", "mouseout"]; + let receivedPointerBoundaryEvents = {}; + let mouseEvent2pointerEvent = {"mouseover": "pointerover", + "mouseenter": "pointerenter", + "mouseleave": "pointerleave", + "mouseout": "pointerout" + }; + + pointerBoundaryEvents.forEach((event) => { + target1.addEventListener(event, (e) => { + receivedPointerBoundaryEvents[event] = e; + }, {once: true}); + }); + + let attributes = ["target", "relatedTarget"]; + mouseBoundaryEvents.forEach((event) => { + target1.addEventListener(event, (e) => { + let correspondingPointerEv = receivedPointerBoundaryEvents[mouseEvent2pointerEvent[event]]; + ok(correspondingPointerEv, "Should receive " + mouseEvent2pointerEvent[event] + " before " + e.type); + if (correspondingPointerEv) { + attributes.forEach((attr) => { + ok(correspondingPointerEv[attr] == e[attr], + attr + " of " + e.type + " should be the same as the correcponding pointer event expect " + + correspondingPointerEv[attr] + " got " + e[attr]); + }); + } + receivedPointerBoundaryEvents[mouseEvent2pointerEvent[event]] = null; + }, {once: true}); + }); + target0.addEventListener("pointerdown", (e) => { + target1.setPointerCapture(e.pointerId); + }); + done.addEventListener("mouseup", () => { + SimpleTest.finish(); + }); + let source = MouseEvent.MOZ_SOURCE_MOUSE; + synthesizeMouse(target0, 5, 5, { type: "mousemove", inputSource: source }, + iframe.contentWindow); + synthesizeMouse(target0, 5, 5, { type: "mousedown", inputSource: source }, + iframe.contentWindow); + synthesizeMouse(target0, 5, 5, { type: "mousemove", inputSource: source }, + iframe.contentWindow); + synthesizeMouse(target0, 5, 5, { type: "mouseup", inputSource: source }, + iframe.contentWindow); + synthesizeMouse(target0, 5, 5, { type: "mousemove", inputSource: source }, + iframe.contentWindow); + synthesizeMouse(done, 5, 5, { type: "mousedown", inputSource: source }, + iframe.contentWindow); + synthesizeMouse(done, 5, 5, { type: "mouseup", inputSource: source }, + iframe.contentWindow); +} + +SimpleTest.waitForFocus(runTests); + +</script> +</body> +</html> diff --git a/dom/events/test/pointerevents/test_bug1403055.html b/dom/events/test/pointerevents/test_bug1403055.html new file mode 100644 index 0000000000..8327fd147d --- /dev/null +++ b/dom/events/test/pointerevents/test_bug1403055.html @@ -0,0 +1,90 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1403055 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1403055</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1403055">Mozilla Bug 1403055</a> +<p id="display"></p> +<div id="target0" style="width: 50px; height: 50px; background: green"></div> +<div id="target1" style="width: 50px; height: 50px; background: red"></div> +<div id="done" style="width: 50px; height: 50px; background: black"></div> +<script type="text/javascript"> +/** Test for Bug 1403055 **/ +SimpleTest.waitForExplicitFinish(); + +var target0 = window.document.getElementById("target0"); +var target1 = window.document.getElementById("target1"); +var done = window.document.getElementById("done"); + +function withoutImplicitlyPointerCaptureForTouch() { + let target0_events = ["pointerover", "pointerenter", "pointerdown", "pointermove"]; + let target1_events = ["pointerover", "pointerenter", "pointermove", "pointercancel", "pointerout", "pointerleave"]; + + target0_events.forEach((elem, index, arr) => { + target0.addEventListener(elem, (event) => { + is(event.type, target0_events[0], "receive " + event.type + " on target0"); + target0_events = target0_events.filter(item => item !== event.type); + }, { once: true }); + }); + + target1_events.forEach((elem, index, arr) => { + target1.addEventListener(elem, (event) => { + is(event.type, target1_events[0], "receive " + event.type + " on target1"); + target1_events = target1_events.filter(item => item !== event.type); + }, { once: true }); + }); + + done.addEventListener("mouseup", () => { + ok(!target0_events.length, " should receive " + target0_events + " on target0"); + ok(!target1_events.length, " should receive " + target1_events + " on target1"); + SpecialPowers.pushPrefEnv({"set": [["dom.w3c_pointer_events.implicit_capture", true]]}, + withImplicitlyPointerCaptureForTouch); + }, {once : true}); + + synthesizeTouch(target0, 5, 5, { type: "touchstart" }); + synthesizeTouch(target0, 6, 6, { type: "touchmove" }); + synthesizeTouch(target1, 5, 5, { type: "touchmove" }); + synthesizeTouch(target1, 5, 5, { type: "touchcancel" }); + synthesizeMouseAtCenter(done, { type: "mousedown" }); + synthesizeMouseAtCenter(done, { type: "mouseup" }); +} + +function withImplicitlyPointerCaptureForTouch() { + let target0_events = ["pointerover", "pointerenter", "pointerdown", "pointermove", "pointercancel", "pointerout", "pointerleave"]; + + target0_events.forEach((elem, index, arr) => { + target0.addEventListener(elem, (event) => { + is(event.type, target0_events[0], "receive " + event.type + " on target0"); + target0_events = target0_events.filter(item => item !== event.type); + }, { once: true }); + }); + + done.addEventListener("mouseup", () => { + ok(!target0_events.length, " should receive " + target0_events + " on target0"); + SimpleTest.finish(); + }, {once : true}); + + synthesizeTouch(target0, 5, 5, { type: "touchstart" }); + synthesizeTouch(target0, 5, 5, { type: "touchmove" }); + synthesizeTouch(target1, 5, 5, { type: "touchmove" }); + synthesizeTouch(target1, 5, 5, { type: "touchcancel" }); + synthesizeMouseAtCenter(done, { type: "mousedown" }); + synthesizeMouseAtCenter(done, { type: "mouseup" }); +} + +SimpleTest.waitForFocus(() => { + SpecialPowers.pushPrefEnv({"set": [["dom.w3c_pointer_events.implicit_capture", false]]}, + withoutImplicitlyPointerCaptureForTouch); +}); + +</script> +</body> +</html> diff --git a/dom/events/test/pointerevents/test_bug1420589_1.html b/dom/events/test/pointerevents/test_bug1420589_1.html new file mode 100644 index 0000000000..8ecea7a28b --- /dev/null +++ b/dom/events/test/pointerevents/test_bug1420589_1.html @@ -0,0 +1,105 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1420589 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1420589</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1420589">Mozilla Bug 1420589</a> +<p id="display"></p> +<iframe id="iframe1" src="./bug_1420589_iframe1.html"> +</iframe> +<iframe id="iframe2" src="./bug_1420589_iframe2.html"> +</iframe> +<script type="text/javascript"> +/* + Test for Bug 1420589. This test synthesizes touch events with two points. The + first one hits iframe1 and the other hits iframe2. + + We dispatch all touch events to the same document. We stop dispatching touch + events to a target if we can't find any ancestor document that is the same as + the document of the existing target. We check the points of the touch event in + reverse order. That means we choose the document of iframe2 as our targeted + document. We won't dispatch touch events to the document of iframe1 nor the + parent document of iframe1 and iframe2. + + We dispatch pointer events to the hit targets even when there aren't in the + same document. This test expects that pointer events are dispatched to the div + element and the iframe document. +*/ +SimpleTest.waitForExplicitFinish(); + +var rx = 1; +var ry = 1; +var angle = 0; +var force = 1; +var modifiers = 0; +var test1PointerId = 1; +var test2PointerId = 2; + +function withoutImplicitlyPointerCaptureForTouch() { + let expectedEvents = [ + // messages from the document of iframe1 + "iframe1 pointerdown", + "iframe1 pointermove", + "iframe1 pointerup", + + // messages from the document of iframe2 + "iframe2 pointerdown", + "iframe2 pointermove", + "iframe2 pointerup", + "iframe2 touchstart", + "iframe2 touchmove", + "iframe2 touchend", + ]; + + window.addEventListener('message',function(e) { + ok(expectedEvents.includes(e.data), " don't expect " + e.data); + expectedEvents = expectedEvents.filter(item => item !== e.data); + if (e.data == "iframe2 touchend") { + ok(!expectedEvents.length, " expect " + expectedEvents); + SimpleTest.finish(); + } + }) + + let iframe1 = document.getElementById('iframe1'); + let iframe2 = document.getElementById('iframe2'); + + let rect1 = iframe1.getBoundingClientRect(); + let rect2 = iframe2.getBoundingClientRect(); + + let left1 = rect1.left + 5; + let left2 = rect2.left + 5; + + let top1 = rect1.top + 5; + let top2 = rect2.top + 5; + + var utils = SpecialPowers.getDOMWindowUtils(window); + utils.sendTouchEvent('touchstart', [test1PointerId, test2PointerId], + [left1, left2], [top1, top2], [rx, rx], [ry, ry], + [angle, angle], [force, force], [0, 0], [0, 0], + [0, 0], modifiers); + utils.sendTouchEvent('touchmove', [test1PointerId, test2PointerId], + [left1 + 1, left2 + 1], [top1, top2], [rx, rx], [ry, ry], + [angle, angle], [force, force], [0, 0], [0, 0], + [0, 0], modifiers); + utils.sendTouchEvent('touchend', [test1PointerId, test2PointerId], + [left1 + 1, left2 + 1], [top1, top2], [rx, rx], [ry, ry], + [angle, angle], [force, force], [0, 0], [0, 0], + [0, 0], modifiers); +} + +SimpleTest.waitForFocus(() => { + SpecialPowers.pushPrefEnv({"set": [["dom.w3c_pointer_events.implicit_capture", false]]}, + withoutImplicitlyPointerCaptureForTouch); +}); + +</script> +</body> +</html> diff --git a/dom/events/test/pointerevents/test_bug1420589_2.html b/dom/events/test/pointerevents/test_bug1420589_2.html new file mode 100644 index 0000000000..2bfac2cdfc --- /dev/null +++ b/dom/events/test/pointerevents/test_bug1420589_2.html @@ -0,0 +1,122 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1420589 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1420589</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1420589">Mozilla Bug 1420589</a> +<p id="display"></p> +<div id="div1" style="width: 50px; height: 50px; background: green"></div> +<iframe id="iframe1" src="./bug_1420589_iframe1.html"> +</iframe> +<script type="text/javascript"> +/* + Test for Bug 1420589. This test synthesizes touch events with two points. One + hits the div element on the document and the other hits the iframe element. + + We dispatch all touch events to the same document. If we find any target that + is not in the same document of the existed target, we try to find the ancestor + document of the new target which is in the same as the existing target and + dispatch touch events to it. We check the points of the touch event in reverse + order. That means we only dispatch touch events to the document which contains + the div element in this test and expect the div element and iframe element + receive touch events. + + We dispatch pointer events to the hit targets even when there aren't in the + same document. This test expects that pointer events are dispatched to the div + element and the iframe document. +*/ +SimpleTest.waitForExplicitFinish(); + +var rx = 1; +var ry = 1; +var angle = 0; +var force = 1; +var modifiers = 0; +var test1PointerId = 1; +var test2PointerId = 2; + +function withoutImplicitlyPointerCaptureForTouch() { + let expectedEvents = [ + // messages from the document of iframe1 + "iframe1 pointerdown", + "iframe1 pointermove", + "iframe1 pointerup", + + // messages from the parent document + "iframe touchstart", + "iframe touchmove", + "iframe touchend", + "div1 pointerdown", + "div1 pointermove", + "div1 pointerup", + "div1 touchstart", + "div1 touchmove", + "div1 touchend", + ]; + + window.addEventListener('message',function(e) { + ok(expectedEvents.includes(e.data), " don't expect " + e.data); + expectedEvents = expectedEvents.filter(item => item !== e.data); + if (e.data == "div1 touchend") { + ok(!expectedEvents.length, " expect " + expectedEvents); + SimpleTest.finish(); + } + }) + + let iframe1 = document.getElementById('iframe1'); + let div1 = document.getElementById('div1'); + + let events = ["touchstart", "touchmove", "touchend", "pointerdown", "pointermove", "pointerup"]; + events.forEach((event) => { + div1.addEventListener(event, (e) => { + postMessage("div1 " + e.type, "*"); + }, { once: true }); + iframe1.addEventListener(event, (e) => { + postMessage("iframe " + e.type, "*"); + }, { once: true }); + }); + + let rect1 = iframe1.getBoundingClientRect(); + let rect2 = div1.getBoundingClientRect(); + + let left1 = rect1.left + 5; + let left2 = rect2.left + 5; + + let top1 = rect1.top + 5; + let top2 = rect2.top + 5; + + var utils = SpecialPowers.getDOMWindowUtils(window); + utils.sendTouchEvent('touchstart', [test1PointerId, test2PointerId], + [left1, left2], [top1, top2], [rx, rx], [ry, ry], + [angle, angle], [force, force], [0, 0], [0, 0], + [0, 0], modifiers); + + // Move the touch pointers so that we dispatch all of them to content. + left1++; + left2++; + utils.sendTouchEvent('touchmove', [test1PointerId, test2PointerId], + [left1, left2], [top1, top2], [rx, rx], [ry, ry], + [angle, angle], [force, force], [0, 0], [0, 0], + [0, 0], modifiers); + utils.sendTouchEvent('touchend', [test1PointerId, test2PointerId], + [left1, left2], [top1, top2], [rx, rx], [ry, ry], + [angle, angle], [force, force], [0, 0], [0, 0], + [0, 0], modifiers); +} + +SimpleTest.waitForFocus(() => { + SpecialPowers.pushPrefEnv({"set": [["dom.w3c_pointer_events.implicit_capture", false]]}, + withoutImplicitlyPointerCaptureForTouch); +}); + +</script> +</body> +</html> diff --git a/dom/events/test/pointerevents/test_bug1420589_3.html b/dom/events/test/pointerevents/test_bug1420589_3.html new file mode 100644 index 0000000000..9942591f64 --- /dev/null +++ b/dom/events/test/pointerevents/test_bug1420589_3.html @@ -0,0 +1,115 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1420589 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1420589</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1420589">Mozilla Bug 1420589</a> +<p id="display"></p> +<div id="div1" style="width: 50px; height: 50px; background: green"></div> +<iframe id="iframe1" src="./bug_1420589_iframe1.html"> +</iframe> +<script type="text/javascript"> +/* + Test for Bug 1420589. This test is similar to test_bug1420589_2.html but the + first touch point hit the div element and the second point hits the iframe. + + We stop dispatching touch events to a target when we can't find any ancestor + document that is the same as the document of the existing target. This test + expects that we only dispatch touch events to the iframe document. + + We dispatch pointer events to the hit targets even when there aren't in the + same document. This test expects that pointer events are dispatched to the div + element and the iframe document. +*/ +SimpleTest.waitForExplicitFinish(); + +var rx = 1; +var ry = 1; +var angle = 0; +var force = 1; +var modifiers = 0; +var test1PointerId = 1; +var test2PointerId = 2; + +function withoutImplicitlyPointerCaptureForTouch() { + let expectedEvents = [ + // messages from the document of iframe1 + "iframe1 pointerdown", + "iframe1 pointermove", + "iframe1 pointerup", + "iframe1 touchstart", + "iframe1 touchmove", + "iframe1 touchend", + + // messages from the parent document + "div1 pointerdown", + "div1 pointermove", + "div1 pointerup", + ]; + + window.addEventListener('message',function(e) { + ok(expectedEvents.includes(e.data), " don't expect " + e.data); + expectedEvents = expectedEvents.filter(item => item !== e.data); + if (e.data == "iframe1 touchend") { + ok(!expectedEvents.length, " expect " + expectedEvents); + SimpleTest.finish(); + } + }) + + let iframe1 = document.getElementById('iframe1'); + let div1 = document.getElementById('div1'); + + let events = ["touchstart", "touchmove", "touchend", "pointerdown", "pointermove", "pointerup"]; + events.forEach((event) => { + div1.addEventListener(event, (e) => { + postMessage("div1 " + e.type, "*"); + }, { once: true }); + iframe1.addEventListener(event, (e) => { + postMessage("iframe " + e.type, "*"); + }, { once: true }); + }); + + let rect1 = div1.getBoundingClientRect(); + let rect2 = iframe1.getBoundingClientRect(); + + let left1 = rect1.left + 5; + let left2 = rect2.left + 5; + + let top1 = rect1.top + 5; + let top2 = rect2.top + 5; + + var utils = SpecialPowers.getDOMWindowUtils(window); + utils.sendTouchEvent('touchstart', [test1PointerId, test2PointerId], + [left1, left2], [top1, top2], [rx, rx], [ry, ry], + [angle, angle], [force, force], [0, 0], [0, 0], + [0, 0], modifiers); + + // Move the touch pointers so that we dispatch all of them to content. + left1++; + left2++; + utils.sendTouchEvent('touchmove', [test1PointerId, test2PointerId], + [left1, left2], [top1, top2], [rx, rx], [ry, ry], + [angle, angle], [force, force], [0, 0], [0, 0], + [0, 0], modifiers); + utils.sendTouchEvent('touchend', [test1PointerId, test2PointerId], + [left1, left2], [top1, top2], [rx, rx], [ry, ry], + [angle, angle], [force, force], [0, 0], [0, 0], + [0, 0], modifiers); +} + +SimpleTest.waitForFocus(() => { + SpecialPowers.pushPrefEnv({"set": [["dom.w3c_pointer_events.implicit_capture", false]]}, + withoutImplicitlyPointerCaptureForTouch); +}); + +</script> +</body> +</html> diff --git a/dom/events/test/pointerevents/test_bug1697769.xhtml b/dom/events/test/pointerevents/test_bug1697769.xhtml new file mode 100644 index 0000000000..09efdb9826 --- /dev/null +++ b/dom/events/test/pointerevents/test_bug1697769.xhtml @@ -0,0 +1,74 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + title="Test for Bug 1697769"> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1697769 +--> +<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> +<script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + +<hbox height="300" align="center"> +<vbox width="30" align="center"> +<scrollbar id="scrollbar" orient="vertical" curpos="0" maxpos="500" style="height: 300px" /> +</vbox> +</hbox> + +<body id="html_body" xmlns="http://www.w3.org/1999/xhtml"> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1697769">Mozilla Bug 1697769</a> +<p id="display"></p> +<pre id="test"></pre> +</body> + +<script class="testbody" type="application/javascript"> +<![CDATA[ + +let scrollbar; +let scrollbarThumb; + +add_setup(async function() { + scrollbar = document.getElementById("scrollbar"); + scrollbarThumb = null; + for (let child of SpecialPowers.InspectorUtils.getChildrenForNode(scrollbar, true, false)) { + if (child.nodeName === "slider") { + scrollbarThumb = SpecialPowers.unwrap(child.childNodes[0]); + } + } + + ok(scrollbarThumb, "Should find thumb"); + is(scrollbarThumb.nodeName, "thumb", "Should find thumb"); + + // Wait for finishing reflow... + await new Promise(resolve => SimpleTest.executeSoon(resolve)); +}); + +add_task(function testScrollbarMouse() { + // Simulate mouse actions. + synthesizeMouseAtCenter(scrollbarThumb, {type: "mousedown", "inputSource": MouseEvent.MOZ_SOURCE_TOUCH}); + is(scrollbar.getAttribute("curpos"), "0", "scrollbar thumb has been moved already"); + + synthesizeMouseAtCenter(scrollbar, {type: "mousemove", "inputSource": MouseEvent.MOZ_SOURCE_TOUCH}); + ok(scrollbar.getAttribute("curpos") > 0, "scrollbar thumb hasn't been dragged"); + + synthesizeMouseAtCenter(scrollbar, {type: "mouseup", "inputSource": MouseEvent.MOZ_SOURCE_TOUCH}); +}); + +add_task(function testScrollbarTouch() { + // reset scrollbar position + scrollbar.setAttribute("curpos", "0"); + + // Simulate touch actions. + synthesizeTouchAtCenter(scrollbarThumb, {type: "touchstart"}); + is(scrollbar.getAttribute("curpos"), "0", "scrollbar thumb has been moved already"); + + synthesizeTouchAtCenter(scrollbar, {type: "touchmove"}); + ok(scrollbar.getAttribute("curpos") > 0, "scrollbar thumb hasn't been dragged"); + + synthesizeTouchAtCenter(scrollbar, {type: "touchend"}); +}); + +]]> +</script> +</window> diff --git a/dom/events/test/pointerevents/test_bug1725416.html b/dom/events/test/pointerevents/test_bug1725416.html new file mode 100644 index 0000000000..3cc4a6316f --- /dev/null +++ b/dom/events/test/pointerevents/test_bug1725416.html @@ -0,0 +1,37 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>Bug 1725416</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<script src="/tests/SimpleTest/EventUtils.js"></script> +<script src="/tests/SimpleTest/paint_listener.js"></script> +<script src="/tests/gfx/layers/apz/test/mochitest/apz_test_utils.js"></script> +<link rel="stylesheet" href="/tests/SimpleTest/test.css" /> + +<style> + #container { + height: 100px; + touch-action: none; + } +</style> + +<div id="container"></div> + +<script> + add_task(async function testTouch() { + await waitUntilApzStable(); + const container = document.getElementById("container"); + const pointerDownPromise = promiseOneEvent(container, "pointerdown"); + const pointerUpPromise = promiseOneEvent(container, "pointerup"); + synthesizeNativeTapAtCenter(container); + + const pointerDown = await pointerDownPromise; + is(pointerDown.pointerType, "touch", ".pointerType"); + is(pointerDown.button, 0, ".button"); + is(pointerDown.buttons, 1, ".buttons"); + + const pointerUp = await pointerUpPromise; + is(pointerUp.pointerType, "touch", ".pointerType"); + is(pointerUp.button, 0, ".button"); + is(pointerUp.buttons, 0, ".buttons"); + }); +</script> diff --git a/dom/events/test/pointerevents/test_bug968148.html b/dom/events/test/pointerevents/test_bug968148.html new file mode 100644 index 0000000000..e62d819934 --- /dev/null +++ b/dom/events/test/pointerevents/test_bug968148.html @@ -0,0 +1,46 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=968148 +--> +<head> + <title>Test for Bug 968148</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="text/javascript"> + function testWithoutImplicitPointerCapture() { + var iframe = document.getElementById("testFrame"); + iframe.src = "bug968148_inner.html"; + } + + function testWithImplicitPointerCapture() { + var iframe = document.getElementById("testFrame"); + iframe.src = "bug968148_inner2.html"; + } + + function runTest() { + SimpleTest.waitForExplicitFinish(); + window.addEventListener("message", (event) => { + if (event.data == "finishTest") { + SimpleTest.finish(); + } else if (event.data == "run next") { + SpecialPowers.pushPrefEnv({ + "set": [ + ["dom.w3c_pointer_events.implicit_capture", true] + ] + }, testWithImplicitPointerCapture); + } + }); + + SpecialPowers.pushPrefEnv({ + "set": [ + ["dom.w3c_pointer_events.implicit_capture", false] + ] + }, testWithoutImplicitPointerCapture); + } + </script> +</head> +<body onload="runTest();"> + <iframe id="testFrame" height="500" width="500"></iframe> +</body> + diff --git a/dom/events/test/pointerevents/test_getCoalescedEvents.html b/dom/events/test/pointerevents/test_getCoalescedEvents.html new file mode 100644 index 0000000000..69eeac6919 --- /dev/null +++ b/dom/events/test/pointerevents/test_getCoalescedEvents.html @@ -0,0 +1,88 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1303957 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1303957</title> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1303957">Mozilla Bug 1303957</a> +<p id="display"></p> +<div id="target0" style="width: 50px; height: 50px; background: green"></div> +<script type="text/javascript"> +/** Test for Bug 1303957 **/ +SimpleTest.waitForExplicitFinish(); + +function runTests() { + let target0 = window.document.getElementById("target0"); + let utils = SpecialPowers.getDOMWindowUtils(window); + utils.advanceTimeAndRefresh(0); + + SimpleTest.executeSoon(async () => { + // Flush all pending mouse events before synthesizing events. + + target0.addEventListener("pointermove", (ev) => { + let length = ev.getCoalescedEvents().length; + ok(length >= 1, "Coalesced events should >= 1, got " + length); + + let rect = target0.getBoundingClientRect(); + let prevOffsetX = 0; + let prevOffsetY = 0; + + for (let i = 0; i < length; ++i) { + let coalescedEvent = ev.getCoalescedEvents()[i]; + isnot(coalescedEvent.timeStamp, 0, "getCoalescedEvents()[" + i + "].timeStamp"); + is(coalescedEvent.type, "pointermove", "getCoalescedEvents()[" + i + "].type"); + is(coalescedEvent.pointerId, ev.pointerId, "getCoalescedEvents()[" + i + "].pointerId"); + is(coalescedEvent.pointerType, ev.pointerType, "getCoalescedEvents()[" + i + "].pointerType"); + is(coalescedEvent.isPrimary, ev.isPrimary, "getCoalescedEvents()[" + i + "].isPrimary"); + is(coalescedEvent.target, ev.target, "getCoalescedEvents()[" + i + "].target"); + is(coalescedEvent.currentTarget, null, "getCoalescedEvents()[" + i + "].currentTarget"); + is(coalescedEvent.eventPhase, Event.NONE, "getCoalescedEvents()[" + i + "].eventPhase"); + is(coalescedEvent.cancelable, false, "getCoalescedEvents()[" + i + "].cancelable"); + is(coalescedEvent.bubbles, false, "getCoalescedEvents()[" + i + "].bubbles"); + + ok(coalescedEvent.offsetX >= prevOffsetX, "getCoalescedEvents()[" + i + "].offsetX = " + coalescedEvent.offsetX); + ok(coalescedEvent.offsetX == 5 || coalescedEvent.offsetX == 10 || + coalescedEvent.offsetX == 15 || coalescedEvent.offsetX == 20, "expected offsetX"); + + ok(coalescedEvent.offsetY >= prevOffsetY, "getCoalescedEvents()[" + i + "].offsetY = " + coalescedEvent.offsetY); + ok(coalescedEvent.offsetY == 5 || coalescedEvent.offsetY == 10 || + coalescedEvent.offsetY == 15 || coalescedEvent.offsetY == 20, "expected offsetY"); + + prevOffsetX = coalescedEvent.offsetX; + prevOffsetY = coalescedEvent.offsetY; + + let x = rect.left + prevOffsetX; + let y = rect.top + prevOffsetY; + // coordinates may change slightly due to rounding + ok((coalescedEvent.clientX <= x+2) && (coalescedEvent.clientX >= x-2), "getCoalescedEvents()[" + i + "].clientX"); + ok((coalescedEvent.clientY <= y+2) && (coalescedEvent.clientY >= y-2), "getCoalescedEvents()[" + i + "].clientY"); + } + }, { once: true }); + + target0.addEventListener("pointerup", (ev) => { + utils.restoreNormalRefresh(); + SimpleTest.finish(); + }, { once: true }); + + await promiseNativeMouseEvent({ type: "mousemove", target: target0, offsetX: 5, offsetY: 5 }); + await promiseNativeMouseEvent({ type: "mousemove", target: target0, offsetX: 10, offsetY: 10 }); + await promiseNativeMouseEvent({ type: "mousemove", target: target0, offsetX: 15, offsetY: 15 }); + await promiseNativeMouseEvent({ type: "mousemove", target: target0, offsetX: 20, offsetY: 20 }); + synthesizeNativeMouseEvent({ type: "click", target: target0, offsetX: 20, offsetY: 20 }); + }); +} + +SimpleTest.waitForFocus(() => { + SpecialPowers.pushPrefEnv({"set": [["dom.events.coalesce.mousemove", true]]}, runTests); +}); + +</script> +</body> +</html> diff --git a/dom/events/test/pointerevents/test_getCoalescedEvents_touch.html b/dom/events/test/pointerevents/test_getCoalescedEvents_touch.html new file mode 100644 index 0000000000..0df34efb30 --- /dev/null +++ b/dom/events/test/pointerevents/test_getCoalescedEvents_touch.html @@ -0,0 +1,111 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1511231 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1511231</title> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/gfx/layers/apz/test/mochitest/apz_test_native_event_utils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1303957">Mozilla Bug 1511231</a> +<p id="display"></p> +<div id="target0" style="margin: 50px; width: 50px; height: 50px; background: green"></div> +<script type="text/javascript"> +/** Test for Bug 1511231 **/ +SimpleTest.waitForExplicitFinish(); + +if (!window.opener) { + // the utils function in apz can't not be used in remote iframe, so run the + // test in a new tab. + info("run tests in a new tab"); + window.open("test_getCoalescedEvents_touch.html"); +} else { + function runTests() { + let target0 = window.document.getElementById("target0"); + let utils = SpecialPowers.getDOMWindowUtils(window); + utils.advanceTimeAndRefresh(0); + + SimpleTest.executeSoon(async () => { + // Flush all pending touch events before synthesizing events. + + let eventCount = 0; + target0.addEventListener("pointermove", function handler(ev) { + eventCount++; + if (eventCount == 1) { + // Ignore the first pointermove event since the first touch event won't + // be coalesced. + is(ev.getCoalescedEvents().length, 1, + "Coalesced events should be 1 for the first touchmove event"); + return; + } + + let length = ev.getCoalescedEvents().length; + ok(length >= 1, "Coalesced events should >= 1, got " + length); + + let rect = target0.getBoundingClientRect(); + let prevOffsetX = 0; + let prevOffsetY = 0; + + for (let i = 0; i < length; ++i) { + let coalescedEvent = ev.getCoalescedEvents()[i]; + opener.isnot(coalescedEvent.timeStamp, 0, "getCoalescedEvents()[" + i + "].timeStamp"); + opener.is(coalescedEvent.type, "pointermove", "getCoalescedEvents()[" + i + "].type"); + opener.is(coalescedEvent.pointerId, ev.pointerId, "getCoalescedEvents()[" + i + "].pointerId"); + opener.is(coalescedEvent.pointerType, ev.pointerType, "getCoalescedEvents()[" + i + "].pointerType"); + opener.is(coalescedEvent.isPrimary, ev.isPrimary, "getCoalescedEvents()[" + i + "].isPrimary"); + opener.is(coalescedEvent.target, ev.target, "getCoalescedEvents()[" + i + "].target"); + opener.is(coalescedEvent.currentTarget, null, "getCoalescedEvents()[" + i + "].currentTarget"); + opener.is(coalescedEvent.eventPhase, Event.NONE, "getCoalescedEvents()[" + i + "].eventPhase"); + opener.is(coalescedEvent.cancelable, false, "getCoalescedEvents()[" + i + "].cancelable"); + opener.is(coalescedEvent.bubbles, false, "getCoalescedEvents()[" + i + "].bubbles"); + + opener.ok(coalescedEvent.offsetX >= prevOffsetX, "getCoalescedEvents()[" + i + "].offsetX = " + coalescedEvent.offsetX); + opener.ok(coalescedEvent.offsetX >= 10 && coalescedEvent.offsetX <= 40, "expected offsetX"); + + opener.ok(coalescedEvent.offsetY >= prevOffsetY, "getCoalescedEvents()[" + i + "].offsetY = " + coalescedEvent.offsetY); + opener.ok(coalescedEvent.offsetY >= 10 && coalescedEvent.offsetY <= 40, "expected offsetY"); + + prevOffsetX = coalescedEvent.offsetX; + prevOffsetY = coalescedEvent.offsetY; + + let x = rect.left + prevOffsetX; + let y = rect.top + prevOffsetY; + // coordinates may change slightly due to rounding + opener.ok((coalescedEvent.clientX <= x+2) && (coalescedEvent.clientX >= x-2), "getCoalescedEvents()[" + i + "].clientX"); + opener.ok((coalescedEvent.clientY <= y+2) && (coalescedEvent.clientY >= y-2), "getCoalescedEvents()[" + i + "].clientY"); + } + + target0.removeEventListener("pointermove", handler); + }); + + target0.addEventListener("pointerup", (ev) => { + utils.restoreNormalRefresh(); + opener.SimpleTest.finish(); + window.close(); + }, { once: true }); + + let positions = []; + for (let i = 10; i <= 40; i+=5) { + positions.push([{ x: i, y: i }]); + } + + await synthesizeNativeTouchSequences(target0, positions); + }); + } + + SimpleTest.waitForFocus(() => { + SpecialPowers.pushPrefEnv({"set": [ + ["dom.events.coalesce.touchmove", true], + ["dom.events.compress.touchmove", false], + ]}, runTests); + }); +} + +</script> +</body> +</html> diff --git a/dom/events/test/pointerevents/test_multiple_touches.html b/dom/events/test/pointerevents/test_multiple_touches.html new file mode 100644 index 0000000000..15202c3636 --- /dev/null +++ b/dom/events/test/pointerevents/test_multiple_touches.html @@ -0,0 +1,196 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for Multiple Touches</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<p id="display"></p> +<div id="target0" style="width: 100px; height: 100px; background: green"></div> +<div id="target1" style="width: 100px; height: 100px; background: red"></div> +<script type="text/javascript"> +// TODO: We should probably make EventUtils.js to support multiple touch. +// Currently the use case is simple, so we just add support here. +// Once we have more use cases, we could come out a more generic way to +// support it. +var touches = { + ids: [], + lefts: [], + tops: [], + rxs: [], + rys: [], + angles: [], + forces: [], + tiltXs: [], + tiltYs: [], + twists: [], +}; + +function synthesizeTouchEvent(aType, aTouches) { + var utils = _getDOMWindowUtils(window); + if (!utils) { + ok(false, "unable to get nsIDOMWindowUtils"); + return; + } + + utils.sendTouchEvent(aType, aTouches.ids, aTouches.lefts, aTouches.tops, + aTouches.rxs, aTouches.rys, aTouches.angles, + aTouches.forces, aTouches.tiltXs, aTouches.tiltYs, + aTouches.twists, 0 /* modifiers */); +} + +function synthesizeTouchStart(aTarget, aId, aOffsetX, aOffsetY) { + if (touches.ids.some((aElem) => { return aElem === aId; })) { + ok(false, `touch with id=${aTouch.id} is already registered`); + return; + } + + let rect = aTarget.getBoundingClientRect(); + touches.ids.push(aId); + touches.lefts.push(rect.left + aOffsetX); + touches.tops.push(rect.top + aOffsetY); + touches.rxs.push(1); + touches.rys.push(1); + touches.angles.push(0); + touches.forces.push(1); + touches.tiltXs.push(0); + touches.tiltYs.push(0); + touches.twists.push(0); + + synthesizeTouchEvent("touchstart", touches); +} + +function synthesizeTouchEnd(aTarget, aId, aOffsetX, aOffsetY) { + let index = touches.ids.indexOf(aId); + if (-1 === index) { + ok(false, `touch with id=${aTouch.id} isn't registered`); + return; + } + + var removedTouches = { + ids: touches.ids.splice(index, 1), + lefts: touches.lefts.splice(index, 1), + tops: touches.tops.splice(index, 1), + rxs: touches.rxs.splice(index, 1), + rys: touches.rys.splice(index, 1), + angles: touches.angles.splice(index, 1), + forces: touches.forces.splice(index, 1), + tiltXs: touches.tiltXs.splice(index, 1), + tiltYs: touches.tiltYs.splice(index, 1), + twists: touches.twists.splice(index, 1), + }; + + synthesizeTouchEvent("touchend", removedTouches); +} + +function synthesizeTouchMove(aTarget, aId, aOffsetX, aOffsetY) { + let index = touches.ids.indexOf(aId); + if (-1 === index) { + ok(false, `touch with id=${aTouch.id} isn't registered`); + return; + } + + let rect = aTarget.getBoundingClientRect(); + touches.lefts[index] = rect.left + aOffsetX; + touches.tops[index] = rect.top + aOffsetY; + + synthesizeTouchEvent("touchmove", touches); +} + +var target0 = document.getElementById("target0"); +var target1 = document.getElementById("target1"); + +function WaitExpectedEvents(aListenEvents, aExpectedEvents, aEventGenerator) { + let promise = new Promise(function(aResolve) { + let index = 0; + let checkReceivedEvents = function(aEvent) { + if (aExpectedEvents.length === 0) { + ok(false, `receive unexpected ${aEvent.type} event from ${aEvent.target}`); + return; + } + index++; + let expectedResult = aExpectedEvents.shift(); + isDeeply(expectedResult, [aEvent.target, aEvent.type], `${index}. expect receive ${expectedResult[1]} event from ${expectedResult[0]}`); + if (aExpectedEvents.length === 0) { + // Wait a bit to see if there is any additional unexpected event fired. + setTimeout(function() { + // Clean up + aListenEvents.forEach((aElem) => { + target0.removeEventListener(aElem, checkReceivedEvents); + target1.removeEventListener(aElem, checkReceivedEvents); + }); + aResolve(); + }, 0); + } + }; + + aListenEvents.forEach((aElem) => { + target0.addEventListener(aElem, checkReceivedEvents); + target1.addEventListener(aElem, checkReceivedEvents); + }); + }); + + aEventGenerator(); + + return promise; +} + +// eslint-disable-next-line mozilla/no-addtask-setup +add_task(async function setup() { + await SimpleTest.promiseFocus(); + await SpecialPowers.pushPrefEnv({ + set: [["dom.w3c_pointer_events.implicit_capture", false]] + }); +}); + +// Test for bug 1521082 +add_task(async function ShouldNotSendDuplicatedPointerDown() { + return WaitExpectedEvents( + ["pointerup", "pointerdown"], + [ // [event target, event type] + [target0, "pointerdown"], + [target1, "pointerdown"], + [target1, "pointerup"], + [target0, "pointerup"], + ], + function() { + var defaultId = SpecialPowers.Ci.nsIDOMWindowUtils.DEFAULT_TOUCH_POINTER_ID; + synthesizeTouchStart(target0, defaultId, 10, 10); + synthesizeTouchStart(target1, defaultId + 1, 10, 10); + synthesizeTouchEnd(target1, defaultId + 1, 10, 10); + synthesizeTouchEnd(target0, defaultId, 10, 10); + } + ); +}); + +// Test for bug 1323400 +add_task(async function ShouldNotSendDuplicatedPointerMove() { + return WaitExpectedEvents( + ["pointerup", "pointerdown","pointermove"], + [ // [event target, event type] + [target0, "pointerdown"], + [target1, "pointerdown"], + [target1, "pointermove"], + // Should receive only one pointer event for target 1. + [target1, "pointermove"], + [target1, "pointerup"], + [target0, "pointerup"], + ], + function() { + var defaultId = SpecialPowers.Ci.nsIDOMWindowUtils.DEFAULT_TOUCH_POINTER_ID; + synthesizeTouchStart(target0, defaultId, 10, 10); + synthesizeTouchStart(target1, defaultId + 1, 10, 10); + synthesizeTouchMove(target1, defaultId + 1, 11, 11); + synthesizeTouchMove(target1, defaultId + 1, 12, 12); + synthesizeTouchEnd(target1, defaultId + 1, 10, 10); + synthesizeTouchEnd(target0, defaultId, 10, 10); + } + ); +}); + +</script> +</body> +</html> diff --git a/dom/events/test/pointerevents/test_pointercapture_remove_iframe.html b/dom/events/test/pointerevents/test_pointercapture_remove_iframe.html new file mode 100644 index 0000000000..fca3a440bf --- /dev/null +++ b/dom/events/test/pointerevents/test_pointercapture_remove_iframe.html @@ -0,0 +1,80 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1686037 +--> +<head> +<title>Bug 1686037</title> +<script src="/tests/SimpleTest/EventUtils.js"></script> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +<style> +#target { + width: 100px; + height: 100px; + background-color: green; +} +iframe { + width: 400px; + height: 300px; + border: 1px solid blue; +} +</style> +</head> +<body> +<a target="_blank"href="https://bugzilla.mozilla.org/show_bug.cgi?id=1686037">Mozilla Bug 1686037</a> +<div id="target"></div> +<iframe srcdoc="<div style='width: 100px; height: 100px; background-color: blue;'></div>"></iframe> + +<pre id="test"> +<script type="text/javascript"> +/** + * Test for Bug 1686037 + */ +function waitForEvent(aTarget, aEventName, aCallback = null) { + return new Promise((aResolve) => { + aTarget.addEventListener(aEventName, async (e) => { + ok(true, `got ${e.type} event on ${e.target}, pointerid: ${e.pointerId}`); + if (aCallback) { + await aCallback(e); + } + aResolve(); + }, { once: true }); + }); +} + +function waitForPointerDownAndSetPointerCapture(aTarget) { + return waitForEvent(aTarget, "pointerdown", async (event) => { + return new Promise((aResolve) => { + aTarget.addEventListener("gotpointercapture", (e) => { + ok(true, `got ${e.type} event on ${e.target}, pointerid: ${e.pointerId}`); + aResolve(); + }, { once: true }); + + aTarget.setPointerCapture(event.pointerId); + }); + }); +} + +add_task(async function test_remove_iframe_after_pointer_capture() { + await SimpleTest.promiseFocus(); + + let iframe = document.querySelector("iframe"); + let iframeWin = iframe.contentWindow; + let targetInIframe = iframe.contentDocument.querySelector("div"); + let promise = Promise.all([ + waitForPointerDownAndSetPointerCapture(targetInIframe), + waitForEvent(targetInIframe, "pointermove") + ]); + synthesizeTouch(targetInIframe, 10, 10, { type: "touchstart", id: 10 }, iframeWin); + synthesizeTouch(targetInIframe, 11, 11, { type: "touchmove", id: 10 }, iframeWin); + await promise; + + // Intentionally not synthesize touchend event to not trigger implicit releasing + // pointer capture. And iframe removal should trigger pointer capture clean up. + iframe.remove(); +}); +</script> +</pre> +</body> +</html> diff --git a/dom/events/test/pointerevents/test_pointercapture_xorigin_iframe.html b/dom/events/test/pointerevents/test_pointercapture_xorigin_iframe.html new file mode 100644 index 0000000000..2bfb0e8ac1 --- /dev/null +++ b/dom/events/test/pointerevents/test_pointercapture_xorigin_iframe.html @@ -0,0 +1,55 @@ +<!DOCTYPE HTML> +<html> +<head> +<title>Test for pointer capture</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"/> +</head> +<body> +<a target="_blank" href="#">Test for pointer capture</a> +<div id="content"></div> +<pre id="test"> +<script type="application/javascript"> +/** + * Pointer capture tests. + **/ + +SimpleTest.waitForExplicitFinish(); + +let gTestFiles = [ + "file_pointercapture_xorigin_iframe.html", + "file_pointercapture_xorigin_iframe_pointerlock.html", + "file_pointercapture_xorigin_iframe_touch.html", +]; + +let gTestWindow = null; +let gTestIndex = 0; + +SpecialPowers.pushPrefEnv({"set": [ + // This will make dispatched event going through parent process. + ["test.events.async.enabled", true] +]}, nextTest); + +function nextTest() { + if (gTestWindow) { + gTestWindow.close(); + } + SimpleTest.waitForFocus(runNextTest); +} + +function runNextTest() { + if (gTestIndex < gTestFiles.length) { + let file = gTestFiles[gTestIndex]; + gTestIndex++; + + info(`Testing ${file}`); + gTestWindow = window.open(file, "", "width=500,height=500"); + } else { + SimpleTest.finish(); + } +} +</script> +</pre> +</body> +</html> diff --git a/dom/events/test/pointerevents/test_pointermove_drag_scrollbar.html b/dom/events/test/pointerevents/test_pointermove_drag_scrollbar.html new file mode 100644 index 0000000000..27ef2829ef --- /dev/null +++ b/dom/events/test/pointerevents/test_pointermove_drag_scrollbar.html @@ -0,0 +1,77 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1509710 +--> +<head> +<meta charset="utf-8"> +<title>Test for Bug 1509710</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<script src="/tests/SimpleTest/EventUtils.js"></script> +<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +<style> +#scroll { + width: 200px; + height: 200px; + overflow: scroll; +} +#scrolled { + width: 200px; + height: 1000px; /* so the subframe has room to scroll */ + will-change: transform; /* to force active layers */ +} +</style> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1509710">Mozilla Bug 1509710</a> +<p id="display"></p> +<div id="scroll"> +<div id="scrolled"></div> +</div> +<script type="text/javascript"> + +/** Test for Bug 1509710 **/ +add_task(async function test_pointer_mouse_event() { + await SimpleTest.promiseFocus(); + + let subframe = document.getElementById("scroll"); + if (subframe.clientWidth == 200) { + // No scrollbar, abort the test. This can happen e.g. on local macOS runs + // with OS settings to only show scrollbars on trackpad/mouse activity. + ok(false, "No scrollbars found, cannot run this test!"); + return; + } + + let receivedEvent = {}; + let handler = function(e) { + receivedEvent[e.type] = true; + }; + subframe.addEventListener("pointerdown", handler); + subframe.addEventListener("pointermove", handler); + subframe.addEventListener("pointerup", handler); + subframe.addEventListener("mousedown", handler); + subframe.addEventListener("mousemove", handler); + subframe.addEventListener("mouseup", handler); + + // synthesize mouse actions on scrollbar. + let scrollbarX = (200 + subframe.clientWidth) / 2; + synthesizeMouse(subframe, scrollbarX, 30, { type: "mousedown" }); + synthesizeMouse(subframe, scrollbarX, 40, { type: "mousemove" }); + synthesizeMouse(subframe, scrollbarX, 40, { type: "mouseup" }); + + await new Promise(SimpleTest.executeSoon); + + // Test pointer event + ok(receivedEvent.pointerdown, "should receive pointerdown event"); + ok(!receivedEvent.pointermove, "should not receive pointermove event"); + ok(receivedEvent.pointerup, "should receive pointerup event"); + + // Test mouse event + ok(receivedEvent.mousedown, "should receive mousedown event"); + ok(!receivedEvent.mousemove, "should not receive mousemove event"); + ok(receivedEvent.mouseup, "should receive mouseup event"); +}); + +</script> +</body> +</html> diff --git a/dom/events/test/pointerevents/test_remove_frame_when_got_pointer_capture.html b/dom/events/test/pointerevents/test_remove_frame_when_got_pointer_capture.html new file mode 100644 index 0000000000..790f2cdd55 --- /dev/null +++ b/dom/events/test/pointerevents/test_remove_frame_when_got_pointer_capture.html @@ -0,0 +1,166 @@ +<!DOCTYPE html> +<html> +<head> + <meta charset="utf-8"> + <title>Test for triggering popup by pointer events</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<p id="content"> +</p> +<script> + +SimpleTest.waitForExplicitFinish(); + +function startTest() { + let content = document.getElementById('content'); + let iframe = document.createElement('iframe'); + iframe.width = 200; + iframe.height = 200; + content.appendChild(iframe); + iframe.contentDocument.body.innerHTML = + "<div id='div1' style='width: 50px; height: 50px; background: green'></div>" + + "<div id='div2' style='width: 50px; height: 50px; background: red'></div>"; + + let div1 = iframe.contentDocument.getElementById("div1"); + let div2 = iframe.contentDocument.getElementById("div2"); + let divEvents = [ + "pointerdown", + "gotpointercapture", + "pointermove", + "pointerup", + "lostpointercapture", + "mousedown", + "mousemove", + "mouseup", + ]; + + let documentEvents = [ + "pointerdown", + "pointermove", + "pointerup", + "mousedown", + "mousemove", + "mouseup", + ]; + + divEvents.forEach((event) => { + div1.addEventListener(event, (e) => { + ok(divEvents.includes(e.type), " don't expect " + e.type); + divEvents = divEvents.filter(item => item !== e.type); + }, { once: true }); + }); + + documentEvents.forEach((event) => { + iframe.contentDocument.addEventListener(event, (e) => { + is(e.target, div1, e.type + " should be dispatched to div1"); + }, { once: true }); + }); + + div1.addEventListener("pointerdown", (e) => { + div1.setPointerCapture(e.pointerId); + }); + + div1.addEventListener("gotpointercapture", (e) => { + div1.style.display = "none"; + }); + + info("Tests for mouseup"); + synthesizeMouseAtCenter(div1, {type: "mousedown"}, iframe.contentWindow); + synthesizeMouseAtCenter(div2, {type: "mousemove"}, iframe.contentWindow); + synthesizeMouseAtCenter(div2, {type: "mouseup"}, iframe.contentWindow); + + ok(!divEvents.length, " expect " + divEvents); + + divEvents = [ + "pointerdown", + "gotpointercapture", + "pointermove", + "pointerup", + "lostpointercapture", + "touchstart", + "touchmove", + "touchend", + ]; + + documentEvents = [ + "pointerdown", + "pointermove", + "pointerup", + "touchstart", + "touchmove", + "touchend", + ]; + divEvents.forEach((event) => { + div1.addEventListener(event, (e) => { + ok(divEvents.includes(e.type), " don't expect " + e.type); + divEvents = divEvents.filter(item => item !== e.type); + }, { once: true }); + }); + + documentEvents.forEach((event) => { + iframe.contentDocument.addEventListener(event, (e) => { + is(e.target, div1, e.type + " should be dispatched to div1"); + }, { once: true }); + }); + + info("Tests for touchend"); + div1.style.display = "block"; + synthesizeMouseAtCenter(div1, {type: "mousemove"}, iframe.contentWindow); + synthesizeTouch(div1, 5, 5, { type: "touchstart" }, iframe.contentWindow); + synthesizeTouch(div2, 5, 5, { type: "touchmove" }, iframe.contentWindow); + synthesizeTouch(div2, 5, 5, { type: "touchend" }, iframe.contentWindow); + + ok(!divEvents.length, " expect " + divEvents); + + divEvents = [ + "pointerdown", + "gotpointercapture", + "pointermove", + "pointercancel", + "lostpointercapture", + "touchstart", + "touchmove", + "touchcancel", + ]; + + documentEvents = [ + "pointerdown", + "pointermove", + "pointercancel", + "touchstart", + "touchmove", + "touchcancel", + ]; + divEvents.forEach((event) => { + div1.addEventListener(event, (e) => { + ok(divEvents.includes(e.type), " don't expect " + e.type); + divEvents = divEvents.filter(item => item !== e.type); + }, { once: true }); + }); + + documentEvents.forEach((event) => { + iframe.contentDocument.addEventListener(event, (e) => { + is(e.target, div1, e.type + " should be dispatched to div1"); + }, { once: true }); + }); + + info("Tests for touchcancel"); + div1.style.display = "block"; + synthesizeMouseAtCenter(div1, {type: "mousemove"}, iframe.contentWindow); + synthesizeTouch(div1, 5, 5, { type: "touchstart" }, iframe.contentWindow); + synthesizeTouch(div2, 5, 5, { type: "touchmove" }, iframe.contentWindow); + synthesizeTouch(div2, 5, 5, { type: "touchcancel" }, iframe.contentWindow); + + ok(!divEvents.length, " expect " + divEvents); + + SimpleTest.finish(); +} + +SimpleTest.waitForFocus(startTest); + +</script> +</body> +</html> diff --git a/dom/events/test/pointerevents/test_synthesized_touch.html b/dom/events/test/pointerevents/test_synthesized_touch.html new file mode 100644 index 0000000000..8a79303e74 --- /dev/null +++ b/dom/events/test/pointerevents/test_synthesized_touch.html @@ -0,0 +1,47 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>Test synthesized touch input</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<script src="/tests/SimpleTest/EventUtils.js"></script> +<link rel="stylesheet" href="/tests/SimpleTest/test.css" /> + +<style> + #container { + width: 100px; + height: 100px; + background-color: green; + } +</style> + +<div id="container"></div> + +<script> +function waitForEvent(aTarget, aEvent, aCheckFn) { + return new Promise(aResolve => { + aTarget.addEventListener(aEvent, function(e) { + info(`${aEvent} received`); + aCheckFn(e); + aResolve(); + }, { once: true }); + }); +} + +add_task(async function test() { + const tiltX = 10; + const tiltY = -10; + const twist = 5; + const check = function(aEvent) { + is(aEvent.tiltX, tiltX, "check tiltX"); + is(aEvent.tiltY, tiltY, "check tiltY"); + is(aEvent.twist, twist, "check twist"); + }; + + const container = document.getElementById("container"); + const pointerDownPromise = waitForEvent(container, "pointerdown", check); + const pointerUpPromise = waitForEvent(container, "pointerup", check); + + synthesizeTouchAtCenter(container, {tiltX, tiltY, twist}); + + await Promise.all([pointerDownPromise, pointerUpPromise]); +}); +</script> diff --git a/dom/events/test/pointerevents/test_trigger_fullscreen_by_pointer_events.html b/dom/events/test/pointerevents/test_trigger_fullscreen_by_pointer_events.html new file mode 100644 index 0000000000..abdf92831a --- /dev/null +++ b/dom/events/test/pointerevents/test_trigger_fullscreen_by_pointer_events.html @@ -0,0 +1,54 @@ +<!DOCTYPE html> +<html> +<head> + <meta charset="utf-8"> + <title>Test for triggering Fullscreen by pointer events</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<script> +SimpleTest.waitForExplicitFinish(); + +function startTest() { + let win = window.open("file_test_trigger_fullscreen.html", "_blank"); + win.addEventListener("load", () => { + let target = win.document.getElementById("target"); + target.addEventListener("pointerdown", () => { + target.requestFullscreen(); + target.addEventListener("pointerdown", () => { + win.document.exitFullscreen(); + }, {once: true}); + }, {once: true}); + + win.document.addEventListener("fullscreenchange", () => { + if (win.document.fullscreenElement) { + is(win.document.fullscreenElement, target, "fullscreenElement should be the div element"); + // synthesize mouse events to generate pointer events and leave full screen. + synthesizeMouseAtCenter(target, { type: "mousedown" }, win); + synthesizeMouseAtCenter(target, { type: "mouseup" }, win); + } else { + win.close(); + SimpleTest.finish(); + } + }); + // Make sure our window is focused before starting the test + SimpleTest.waitForFocus(() => { + // synthesize mouse events to generate pointer events and enter full screen. + synthesizeMouseAtCenter(target, { type: "mousedown" }, win); + synthesizeMouseAtCenter(target, { type: "mouseup" }, win); + }, win); + }); +} + +SimpleTest.waitForFocus(() => { + SpecialPowers.pushPrefEnv({ + "set": [ + ["full-screen-api.allow-trusted-requests-only", false] + ] + }, startTest); +}); +</script> +</body> +</html> diff --git a/dom/events/test/pointerevents/test_wpt_pointerevent_attributes_hoverable_pointers-manual.html b/dom/events/test/pointerevents/test_wpt_pointerevent_attributes_hoverable_pointers-manual.html new file mode 100644 index 0000000000..557ee80c4f --- /dev/null +++ b/dom/events/test/pointerevents/test_wpt_pointerevent_attributes_hoverable_pointers-manual.html @@ -0,0 +1,53 @@ +<!DOCTYPE HTML> +<html> + <head> + <meta charset="utf-8"> + <title>Test pointerevent attributes for hoverable pointers</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="mochitest_support_external.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="text/javascript"> + SimpleTest.waitForExplicitFinish(); + function startTest() { + runTestInNewWindow("wpt/pointerevent_attributes_hoverable_pointers-manual.html"); + } + function executeTest(int_win) { + let iframeWin = int_win.document.getElementById("innerFrame").contentWindow; + // synthesize mouse events with input source = mouse + sendMouseEvent(int_win, "square1", "mousemove", {button:-1}); + sendMouseEvent(int_win, "square1", "mousedown"); + sendMouseEvent(int_win, "square1", "mouseup"); + sendMouseEvent(int_win, "square1", "mousemove", {button:-1, + offsetX:-1, + offsetY:-1}); + sendMouseEvent(iframeWin, "square2", "mousemove", {button:-1}); + sendMouseEvent(iframeWin, "square2", "mousedown"); + sendMouseEvent(iframeWin, "square2", "mouseup"); + sendMouseEvent(iframeWin, "square2", "mousemove", {button:-1, + offsetX:-1, + offsetY:-1}); + // synthesize mouse events with input source = pen + let inputPen = MouseEvent.MOZ_SOURCE_PEN; + sendMouseEvent(int_win, "square1", "mousemove", {button:-1, + inputSource: inputPen}); + sendMouseEvent(int_win, "square1", "mousedown", {inputSource:inputPen}); + sendMouseEvent(int_win, "square1", "mouseup", {inputSource:inputPen}); + sendMouseEvent(int_win, "square1", "mousemove", {button:-1, + offsetX:-1, + offsetY:-1, + inputSource:inputPen}); + sendMouseEvent(iframeWin, "square2", "mousemove", {button:-1, + inputSource:inputPen}); + sendMouseEvent(iframeWin, "square2", "mousedown", {inputSource:inputPen}); + sendMouseEvent(iframeWin, "square2", "mouseup", {inputSource:inputPen}); + sendMouseEvent(iframeWin, "square2", "mousemove", {button:-1, + offsetX:-1, + offsetY:-1, + inputSource:inputPen}); + } + </script> + </head> + <body> + </body> +</html> diff --git a/dom/events/test/pointerevents/test_wpt_pointerevent_attributes_nohover_pointers-manual.html b/dom/events/test/pointerevents/test_wpt_pointerevent_attributes_nohover_pointers-manual.html new file mode 100644 index 0000000000..f9fde4f92f --- /dev/null +++ b/dom/events/test/pointerevents/test_wpt_pointerevent_attributes_nohover_pointers-manual.html @@ -0,0 +1,26 @@ +<!DOCTYPE HTML> +<html> + <head> + <meta charset="utf-8"> + <title>Test pointerevent attributes for non-hoverable pointers</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="mochitest_support_external.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="text/javascript"> + SimpleTest.waitForExplicitFinish(); + function startTest() { + runTestInNewWindow("wpt/pointerevent_attributes_nohover_pointers-manual.html"); + } + function executeTest(int_win) { + sendTouchEvent(int_win, "square1", "touchstart"); + sendTouchEvent(int_win, "square1", "touchend"); + let iframe = int_win.document.getElementById("innerFrame"); + sendTouchEvent(iframe.contentWindow, "square2", "touchstart"); + sendTouchEvent(iframe.contentWindow, "square2", "touchend"); + } + </script> + </head> + <body> + </body> +</html> diff --git a/dom/events/test/pointerevents/test_wpt_pointerevent_boundary_events_in_capturing-manual.html b/dom/events/test/pointerevents/test_wpt_pointerevent_boundary_events_in_capturing-manual.html new file mode 100644 index 0000000000..24aeb6d9a6 --- /dev/null +++ b/dom/events/test/pointerevents/test_wpt_pointerevent_boundary_events_in_capturing-manual.html @@ -0,0 +1,46 @@ +<!DOCTYPE HTML> +<html> + <head> + <meta charset="utf-8"> + <title>W3C pointerevent_boundary_events_in_capturing-manual.html in Mochitest form</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="mochitest_support_external.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="text/javascript"> + SimpleTest.waitForExplicitFinish(); + function startTest() { + runTestInNewWindow("wpt/pointerevent_boundary_events_in_capturing-manual.html", true); + } + function executeTest(int_win) { + sendMouseEvent(int_win, "target0", "mousemove"); + sendMouseEvent(int_win, "target0", "mousedown"); + sendMouseEvent(int_win, "target0", "mousemove", {buttons: 1}); + sendMouseEvent(int_win, "target0", "mousemove", {buttons: 1}); + sendMouseEvent(int_win, "target0", "mouseup"); + + window.addEventListener("message", function(aEvent) { + if (aEvent.data == "Test Touch") { + // Synthesize touch events to run this test. + sendTouchEvent(int_win, "target0", "touchstart"); + sendTouchEvent(int_win, "target0", "touchmove", {offsetX: 10}); + sendTouchEvent(int_win, "target0", "touchmove", {offsetX: 15}); + sendTouchEvent(int_win, "target0", "touchmove", {offsetX: 20}); + sendTouchEvent(int_win, "target0", "touchend"); + window.postMessage("Test Pen", "*"); + } else if (aEvent.data == "Test Pen") { + // Synthesize pen events to run this test. + sendMouseEvent(int_win, "target0", "mousemove", {inputSource:MouseEvent.MOZ_SOURCE_PEN}); + sendMouseEvent(int_win, "target0", "mousedown", {inputSource:MouseEvent.MOZ_SOURCE_PEN}); + sendMouseEvent(int_win, "target0", "mousemove", {inputSource:MouseEvent.MOZ_SOURCE_PEN, buttons: 1}); + sendMouseEvent(int_win, "target0", "mousemove", {inputSource:MouseEvent.MOZ_SOURCE_PEN, buttons: 1}); + sendMouseEvent(int_win, "target0", "mouseup", {inputSource:MouseEvent.MOZ_SOURCE_PEN}); + } + }); + window.postMessage("Test Touch", "*"); + } + </script> + </head> + <body> + </body> +</html> diff --git a/dom/events/test/pointerevents/test_wpt_pointerevent_change-touch-action-onpointerdown_touch-manual.html b/dom/events/test/pointerevents/test_wpt_pointerevent_change-touch-action-onpointerdown_touch-manual.html new file mode 100644 index 0000000000..f95b16c850 --- /dev/null +++ b/dom/events/test/pointerevents/test_wpt_pointerevent_change-touch-action-onpointerdown_touch-manual.html @@ -0,0 +1,39 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1000870 +--> + <head> + <meta charset="utf-8"> + <title>Test for Bug 1000870</title> + <meta name="author" content="Maksim Lebedev" /> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="mochitest_support_external.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="text/javascript"> + SimpleTest.waitForExplicitFinish(); + function startTest() { + runTestInNewWindow("pointerevent_change-touch-action-onpointerdown_touch-manual.html"); + } + function executeTest(int_win) { + const WM_VSCROLL = 0x0115; + sendTouchEvent(int_win, "target0", "touchstart"); + sendTouchEvent(int_win, "target0", "touchmove"); + sendTouchEvent(int_win, "target0", "touchend"); + + // NOTE: This testcase is about that modifying touch-action during a + // pointerdown callback "should not" affect the gesture detection of the + // touch session started by the pointerdown. That is, a scroll should + // still fired by gesture detection, instead of launching by our own. + var utils = _getDOMWindowUtils(int_win); + var target0 = int_win.document.getElementById("target0"); + utils.sendNativeMouseScrollEvent(target0.getBoundingClientRect().left + 5, + target0.getBoundingClientRect().top + 5, + WM_VSCROLL, 10, 10, 0, 0, 0, target0); + } + </script> + </head> + <body> + </body> +</html> diff --git a/dom/events/test/pointerevents/test_wpt_pointerevent_constructor.html b/dom/events/test/pointerevents/test_wpt_pointerevent_constructor.html new file mode 100644 index 0000000000..058e32a967 --- /dev/null +++ b/dom/events/test/pointerevents/test_wpt_pointerevent_constructor.html @@ -0,0 +1,26 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1000870 +--> + <head> + <meta charset="utf-8"> + <title>Test for Bug 1000870</title> + <meta name="author" content="Maksim Lebedev" /> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="mochitest_support_external.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="text/javascript"> + SimpleTest.waitForExplicitFinish(); + function startTest() { + runTestInNewWindow("wpt/pointerevent_constructor.html"); + } + function executeTest(int_win) { + // Function should be, but can be empty + } + </script> + </head> + <body> + </body> +</html> diff --git a/dom/events/test/pointerevents/test_wpt_pointerevent_drag_interaction-manual.html b/dom/events/test/pointerevents/test_wpt_pointerevent_drag_interaction-manual.html new file mode 100644 index 0000000000..a05ee9557a --- /dev/null +++ b/dom/events/test/pointerevents/test_wpt_pointerevent_drag_interaction-manual.html @@ -0,0 +1,38 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1669673 +--> + <head> + <meta charset="utf-8"> + <title>Test for Bug 1669673</title> + <meta name="author" content="Maksim Lebedev" /> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="mochitest_support_external.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="text/javascript"> + SimpleTest.waitForExplicitFinish(); + function startTest() { + runTestInNewWindow("wpt/html/pointerevent_drag_interaction-manual.html"); + } + async function executeTest(int_win) { + info("executeTest"); + // DndWithoutCapture + await doDragAndDrop(int_win, "target0", "target1"); + // DndWithCapture + await doDragAndDrop(int_win, "target0", "target1"); + // DndWithCaptureMouse + await doDragAndDrop(int_win, "target0", "target1"); + // DndPrevented + await doDragAndDrop(int_win, "target0", "target1", { + expectCancelDragStart: true, + // Move mouse to target1. + stepY: 33 + }); + } + </script> + </head> + <body> + </body> +</html> diff --git a/dom/events/test/pointerevents/test_wpt_pointerevent_movementxy-manual.html b/dom/events/test/pointerevents/test_wpt_pointerevent_movementxy-manual.html new file mode 100644 index 0000000000..3059f868b7 --- /dev/null +++ b/dom/events/test/pointerevents/test_wpt_pointerevent_movementxy-manual.html @@ -0,0 +1,53 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1399740 +--> + <head> + <meta charset="utf-8"> + <title>Test for Bug 1399740</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="mochitest_support_external.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="text/javascript"> + SimpleTest.waitForExplicitFinish(); + function startTest() { + runTestInNewWindow("wpt/pointerlock/pointerevent_movementxy-manual.html"); + } + function executeTest(int_win) { + let box1 = int_win.document.getElementById("box1"); + let box2 = int_win.document.getElementById("box2"); + let rect1 = box1.getBoundingClientRect(); + let rect2 = box2.getBoundingClientRect(); + let offsetX = rect1.left + rect1.width / 2; + let offsetY = rect1.top + rect1.height / 2; + let stepX = (rect2.left + rect2.width / 2 - offsetX) / 10; + let stepY = (rect2.top + rect2.height / 2 - offsetY) / 10; + sendMouseEventAtPoint(int_win, offsetX, offsetY, "mousemove", {inputSource:MouseEvent.MOZ_SOURCE_MOUSE}); + sendMouseEventAtPoint(int_win, offsetX, offsetY, "mousedown", {inputSource:MouseEvent.MOZ_SOURCE_MOUSE}); + sendMouseEventAtPoint(int_win, offsetX, offsetY, "mousemove", {inputSource:MouseEvent.MOZ_SOURCE_MOUSE}); + for (var i = 0; i < 10; ++i) { + offsetX += stepX; + offsetY += stepY; + sendMouseEventAtPoint(int_win, offsetX, offsetY, "mousemove", {inputSource:MouseEvent.MOZ_SOURCE_MOUSE}); + } + sendMouseEventAtPoint(int_win, offsetX, offsetY, "mouseup", {inputSource:MouseEvent.MOZ_SOURCE_MOUSE}); + + offsetX = rect1.left + rect1.width / 2; + offsetY = rect1.top + rect1.height / 2; + sendMouseEventAtPoint(int_win, offsetX, offsetY, "mousemove", {inputSource:MouseEvent.MOZ_SOURCE_TOUCH}); + sendMouseEventAtPoint(int_win, offsetX, offsetY, "mousedown", {inputSource:MouseEvent.MOZ_SOURCE_TOUCH}); + sendMouseEventAtPoint(int_win, offsetX, offsetY, "mousemove", {inputSource:MouseEvent.MOZ_SOURCE_TOUCH}); + for (var i = 0; i < 10; ++i) { + offsetX += stepX; + offsetY += stepY; + sendMouseEventAtPoint(int_win, offsetX, offsetY, "mousemove", {inputSource:MouseEvent.MOZ_SOURCE_TOUCH}); + } + sendMouseEventAtPoint(int_win, offsetX, offsetY, "mouseup", {inputSource:MouseEvent.MOZ_SOURCE_TOUCH}); + } + </script> + </head> + <body> + </body> +</html> diff --git a/dom/events/test/pointerevents/test_wpt_pointerevent_multiple_primary_pointers_boundary_events-manual.html b/dom/events/test/pointerevents/test_wpt_pointerevent_multiple_primary_pointers_boundary_events-manual.html new file mode 100644 index 0000000000..825e23857f --- /dev/null +++ b/dom/events/test/pointerevents/test_wpt_pointerevent_multiple_primary_pointers_boundary_events-manual.html @@ -0,0 +1,31 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1000870 +--> + <head> + <meta charset="utf-8"> + <title>Test for Bug 1000870</title> + <meta name="author" content="Maksim Lebedev" /> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="mochitest_support_external.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="text/javascript"> + SimpleTest.waitForExplicitFinish(); + function startTest() { + runTestInNewWindow("wpt/pointerevent_multiple_primary_pointers_boundary_events-manual.html"); + } + function executeTest(int_win) { + sendMouseEvent(int_win, "target0", "mousemove"); + sendTouchEvent(int_win, "target1", "touchstart"); + sendTouchEvent(int_win, "target1", "touchend"); + sendMouseEvent(int_win, "target0", "mousemove"); + sendMouseEvent(int_win, "done", "mousedown"); + sendMouseEvent(int_win, "done", "mouseup"); + } + </script> + </head> + <body> + </body> +</html> diff --git a/dom/events/test/pointerevents/test_wpt_pointerevent_pointerId_scope-manual.html b/dom/events/test/pointerevents/test_wpt_pointerevent_pointerId_scope-manual.html new file mode 100644 index 0000000000..f52bf7fc20 --- /dev/null +++ b/dom/events/test/pointerevents/test_wpt_pointerevent_pointerId_scope-manual.html @@ -0,0 +1,27 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1000870 +--> + <head> + <meta charset="utf-8"> + <title>Test for Bug 1000870</title> + <meta name="author" content="Maksim Lebedev" /> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="mochitest_support_external.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="text/javascript"> + SimpleTest.waitForExplicitFinish(); + function startTest() { + runTestInNewWindow("wpt/pointerevent_pointerId_scope-manual.html"); + } + function executeTest(int_win) { + sendTouchEvent(int_win, "target0", "touchstart"); + sendTouchEvent(int_win, "target0", "touchend"); + } + </script> + </head> + <body> + </body> +</html> diff --git a/dom/events/test/pointerevents/test_wpt_pointerevent_pointercancel_touch-manual.html b/dom/events/test/pointerevents/test_wpt_pointerevent_pointercancel_touch-manual.html new file mode 100644 index 0000000000..0adba4f756 --- /dev/null +++ b/dom/events/test/pointerevents/test_wpt_pointerevent_pointercancel_touch-manual.html @@ -0,0 +1,30 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1000870 +--> + <head> + <meta charset="utf-8"> + <title>Test for Bug 1000870</title> + <meta name="author" content="Maksim Lebedev" /> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="mochitest_support_external.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="text/javascript"> + SimpleTest.waitForExplicitFinish(); + function startTest() { + runTestInNewWindow("wpt/pointerevent_pointercancel_touch-manual.html"); + } + function executeTest(int_win) { + sendTouchEvent(int_win, "target0", "touchstart"); + sendTouchEvent(int_win, "target0", "touchcancel"); + + // Need a touchend event to terminated the test gracefully. + sendTouchEvent(int_win, "target0", "touchend"); + } + </script> + </head> + <body> + </body> +</html> diff --git a/dom/events/test/pointerevents/test_wpt_pointerevent_pointerleave_after_pointercancel_touch-manual.html b/dom/events/test/pointerevents/test_wpt_pointerevent_pointerleave_after_pointercancel_touch-manual.html new file mode 100644 index 0000000000..53c897bcd0 --- /dev/null +++ b/dom/events/test/pointerevents/test_wpt_pointerevent_pointerleave_after_pointercancel_touch-manual.html @@ -0,0 +1,30 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1000870 +--> + <head> + <meta charset="utf-8"> + <title>Test for Bug 1000870</title> + <meta name="author" content="Maksim Lebedev" /> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="mochitest_support_external.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="text/javascript"> + SimpleTest.waitForExplicitFinish(); + function startTest() { + runTestInNewWindow("wpt/pointerevent_pointerleave_after_pointercancel_touch-manual.html"); + } + function executeTest(int_win) { + sendTouchEvent(int_win, "target0", "touchstart"); + sendTouchEvent(int_win, "target0", "touchcancel"); + + // Need a touchend event to terminated the test gracefully. + sendTouchEvent(int_win, "target0", "touchend"); + } + </script> + </head> + <body> + </body> +</html> diff --git a/dom/events/test/pointerevents/test_wpt_pointerevent_pointerleave_pen-manual.html b/dom/events/test/pointerevents/test_wpt_pointerevent_pointerleave_pen-manual.html new file mode 100644 index 0000000000..e4904780f6 --- /dev/null +++ b/dom/events/test/pointerevents/test_wpt_pointerevent_pointerleave_pen-manual.html @@ -0,0 +1,28 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1000870 +--> + <head> + <meta charset="utf-8"> + <title>Test for Bug 1000870</title> + <meta name="author" content="Maksim Lebedev" /> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="mochitest_support_external.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="text/javascript"> + SimpleTest.waitForExplicitFinish(); + function startTest() { + runTestInNewWindow("wpt/pointerevent_pointerleave_pen-manual.html"); + } + function executeTest(int_win) { + sendMouseEvent(int_win, "target0", "mousedown", {inputSource:MouseEvent.MOZ_SOURCE_PEN}); + sendMouseEvent(int_win, "target0", "mouseup", {inputSource:MouseEvent.MOZ_SOURCE_PEN}); + sendMouseEvent(int_win, "target0", "mousecancel", {inputSource:MouseEvent.MOZ_SOURCE_PEN}); + } + </script> + </head> + <body> + </body> +</html> diff --git a/dom/events/test/pointerevents/test_wpt_pointerevent_pointerout_after_pointercancel_touch-manual.html b/dom/events/test/pointerevents/test_wpt_pointerevent_pointerout_after_pointercancel_touch-manual.html new file mode 100644 index 0000000000..53cf765fb6 --- /dev/null +++ b/dom/events/test/pointerevents/test_wpt_pointerevent_pointerout_after_pointercancel_touch-manual.html @@ -0,0 +1,30 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1000870 +--> + <head> + <meta charset="utf-8"> + <title>Test for Bug 1000870</title> + <meta name="author" content="Maksim Lebedev" /> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="mochitest_support_external.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="text/javascript"> + SimpleTest.waitForExplicitFinish(); + function startTest() { + runTestInNewWindow("wpt/pointerevent_pointerout_after_pointercancel_touch-manual.html"); + } + function executeTest(int_win) { + sendTouchEvent(int_win, "target0", "touchstart"); + sendTouchEvent(int_win, "target0", "touchcancel"); + + // Need a touchend event to terminated the test gracefully. + sendTouchEvent(int_win, "target0", "touchend"); + } + </script> + </head> + <body> + </body> +</html> diff --git a/dom/events/test/pointerevents/test_wpt_pointerevent_pointerout_pen-manual.html b/dom/events/test/pointerevents/test_wpt_pointerevent_pointerout_pen-manual.html new file mode 100644 index 0000000000..6b41f6492b --- /dev/null +++ b/dom/events/test/pointerevents/test_wpt_pointerevent_pointerout_pen-manual.html @@ -0,0 +1,28 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1000870 +--> + <head> + <meta charset="utf-8"> + <title>Test for Bug 1000870</title> + <meta name="author" content="Maksim Lebedev" /> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="mochitest_support_external.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="text/javascript"> + SimpleTest.waitForExplicitFinish(); + function startTest() { + runTestInNewWindow("wpt/pointerevent_pointerout_pen-manual.html"); + } + function executeTest(int_win) { + sendMouseEvent(int_win, "target0", "mousedown", {inputSource:MouseEvent.MOZ_SOURCE_PEN}); + sendMouseEvent(int_win, "target0", "mouseup", {inputSource:MouseEvent.MOZ_SOURCE_PEN}); + sendMouseEvent(int_win, "target0", "mousecancel", {inputSource:MouseEvent.MOZ_SOURCE_PEN}); + } + </script> + </head> + <body> + </body> +</html> diff --git a/dom/events/test/pointerevents/test_wpt_pointerevent_releasepointercapture_events_to_original_target-manual.html b/dom/events/test/pointerevents/test_wpt_pointerevent_releasepointercapture_events_to_original_target-manual.html new file mode 100644 index 0000000000..049c41467f --- /dev/null +++ b/dom/events/test/pointerevents/test_wpt_pointerevent_releasepointercapture_events_to_original_target-manual.html @@ -0,0 +1,49 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1000870 +--> + <head> + <meta charset="utf-8"> + <title>Test for Bug 1000870</title> + <meta name="author" content="Maksim Lebedev" /> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="mochitest_support_external.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="text/javascript"> + SimpleTest.waitForExplicitFinish(); + function startTest() { + runTestInNewWindow("wpt/pointerevent_releasepointercapture_events_to_original_target-manual.html"); + } + function executeTest(int_win) { + // Synthesize mouse events to run this test. + sendMouseEvent(int_win, "target0", "mousemove"); + sendMouseEvent(int_win, "target0", "mousedown"); + sendMouseEvent(int_win, "target0", "mousemove", {buttons: 1}); + sendMouseEvent(int_win, "target0", "mousemove", {buttons: 1}); + sendMouseEvent(int_win, "target0", "mouseup"); + + window.addEventListener("message", function(aEvent) { + if (aEvent.data == "Test Touch") { + // Synthesize touch events to run this test. + sendTouchEvent(int_win, "target0", "touchstart", {offsetX: 10}); + sendTouchEvent(int_win, "target0", "touchmove", {offsetX: 11}); + sendTouchEvent(int_win, "target0", "touchend", {offsetX: 11}); + window.postMessage("Test Pen", "*"); + } else if (aEvent.data == "Test Pen") { + // Synthesize pen events to run this test. + sendMouseEvent(int_win, "target0", "mousemove", {inputSource:MouseEvent.MOZ_SOURCE_PEN}); + sendMouseEvent(int_win, "target0", "mousedown", {inputSource:MouseEvent.MOZ_SOURCE_PEN}); + sendMouseEvent(int_win, "target0", "mousemove", {inputSource:MouseEvent.MOZ_SOURCE_PEN, buttons: 1}); + sendMouseEvent(int_win, "target0", "mousemove", {inputSource:MouseEvent.MOZ_SOURCE_PEN, buttons: 1}); + sendMouseEvent(int_win, "target0", "mouseup", {inputSource:MouseEvent.MOZ_SOURCE_PEN}); + } + }); + window.postMessage("Test Touch", "*"); + } + </script> + </head> + <body> + </body> +</html> diff --git a/dom/events/test/pointerevents/test_wpt_pointerevent_releasepointercapture_onpointercancel_touch-manual.html b/dom/events/test/pointerevents/test_wpt_pointerevent_releasepointercapture_onpointercancel_touch-manual.html new file mode 100644 index 0000000000..eb39b0bee3 --- /dev/null +++ b/dom/events/test/pointerevents/test_wpt_pointerevent_releasepointercapture_onpointercancel_touch-manual.html @@ -0,0 +1,30 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1000870 +--> + <head> + <meta charset="utf-8"> + <title>Test for Bug 1000870</title> + <meta name="author" content="Maksim Lebedev" /> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="mochitest_support_external.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="text/javascript"> + SimpleTest.waitForExplicitFinish(); + function startTest() { + runTestInNewWindow("wpt/pointerevent_releasepointercapture_onpointercancel_touch-manual.html"); + } + function executeTest(int_win) { + sendTouchEvent(int_win, "target0", "touchstart"); + sendTouchEvent(int_win, "target0", "touchcancel"); + + // Need a touchend event to terminated the test gracefully. + sendTouchEvent(int_win, "target0", "touchend"); + } + </script> + </head> + <body> + </body> +</html> diff --git a/dom/events/test/pointerevents/test_wpt_pointerevent_sequence_at_implicit_release_on_drag-manual.html b/dom/events/test/pointerevents/test_wpt_pointerevent_sequence_at_implicit_release_on_drag-manual.html new file mode 100644 index 0000000000..40a6be2e71 --- /dev/null +++ b/dom/events/test/pointerevents/test_wpt_pointerevent_sequence_at_implicit_release_on_drag-manual.html @@ -0,0 +1,27 @@ +<!DOCTYPE HTML> +<html> + <head> + <meta charset="utf-8"> + <title>W3C pointerevent_sequence_at_implicit_release_on_drag-manual.html in Mochitest form</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="mochitest_support_external.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="text/javascript"> + SimpleTest.waitForExplicitFinish(); + function startTest() { + runTestInNewWindow("wpt/pointerevent_sequence_at_implicit_release_on_drag-manual.html"); + } + function executeTest(int_win) { + sendTouchEvent(int_win, "target", "touchstart"); + sendTouchEvent(int_win, "target", "touchmove"); + sendTouchEvent(int_win, "target", "touchmove"); + sendTouchEvent(int_win, "target", "touchcancel"); + sendMouseEvent(int_win, "done", "mousedown"); + sendMouseEvent(int_win, "done", "mouseup"); + } + </script> + </head> + <body> + </body> +</html> diff --git a/dom/events/test/pointerevents/test_wpt_touch_action.html b/dom/events/test/pointerevents/test_wpt_touch_action.html new file mode 100644 index 0000000000..f2edbfb061 --- /dev/null +++ b/dom/events/test/pointerevents/test_wpt_touch_action.html @@ -0,0 +1,98 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="utf-8"> + <title>W3C pointerevents/*touch-action*.html tests in Mochitest form</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript" src="apz_test_utils.js"></script> + <script type="application/javascript" src="apz_test_native_event_utils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript"> +var apz_touch_action_prefs = [ + // Dropping the touch slop to 0 makes the tests easier to write because + // we can just do a one-pixel drag to get over the pan threshold rather + // than having to hard-code some larger value. + ["apz.touch_start_tolerance", "0.0"], + // The touchstart from the drag can turn into a long-tap if the touch-move + // events get held up. Try to prevent that by making long-taps require + // a 10 second hold. Note that we also cannot enable chaos mode on this + // test for this reason, since chaos mode can cause the long-press timer + // to fire sooner than the pref dictates. + ["ui.click_hold_context_menus.delay", 10000], + // The subtests in this test do touch-drags to pan the page, but we don't + // want those pans to turn into fling animations, so we increase the + // fling-stop threshold velocity to absurdly high. + ["apz.fling_stopped_threshold", Number.MAX_VALUE], + // The helper_div_pan's div gets a displayport on scroll, but if the + // test takes too long the displayport can expire before the new scroll + // position is synced back to the main thread. So we disable displayport + // expiry for these tests. + ["apz.displayport_expiry_ms", 0], +]; + +function apzScriptInjector(name) { + return async function(childWin) { + childWin._ACTIVE_TEST_NAME = name; + await injectScript('/tests/SimpleTest/paint_listener.js', childWin); + await injectScript('../apz_test_utils.js', childWin); + await injectScript('../apz_test_native_event_utils.js', childWin); + await injectScript('../touch_action_helpers.js', childWin); + }; +} + +// Each of these test names is turned into an entry in the |subtests| array +// below. +var testnames = [ + 'pointerevent_touch-action-auto-css_touch-manual', + 'pointerevent_touch-action-button-test_touch-manual', + // this one runs as a web-platform-test since it's not a manual test + // 'pointerevent_touch-action-illegal', + 'pointerevent_touch-action-inherit_child-auto-child-none_touch-manual', + 'pointerevent_touch-action-inherit_child-none_touch-manual', + 'pointerevent_touch-action-inherit_child-pan-x-child-pan-x_touch-manual', + 'pointerevent_touch-action-inherit_child-pan-x-child-pan-y_touch-manual', + 'pointerevent_touch-action-inherit_highest-parent-none_touch-manual', + 'pointerevent_touch-action-inherit_parent-none_touch-manual', + // the keyboard-manual and mouse-manual tests require simulating keyboard/ + // mouse input, rather than touch, so we're not going to do that here. + //'pointerevent_touch-action-keyboard-manual', + //'pointerevent_touch-action-mouse-manual', + 'pointerevent_touch-action-none-css_touch-manual', + 'pointerevent_touch-action-pan-x-css_touch-manual', + 'pointerevent_touch-action-pan-x-pan-y-pan-y_touch-manual', + 'pointerevent_touch-action-pan-x-pan-y_touch-manual', + 'pointerevent_touch-action-pan-y-css_touch-manual', + // disable this one because of intermittent failures. see bug 1292134. + // 'pointerevent_touch-action-span-test_touch-manual', + 'pointerevent_touch-action-svg-test_touch-manual', + 'pointerevent_touch-action-table-test_touch-manual', + // this one runs as a web-platform-test since it's not a manual test + //'pointerevent_touch-action-verification', +]; + +// Each entry in |subtests| is loaded in a new window. When loaded, it runs +// the function returned by apzScriptInjector, which injects some helper JS +// files into the vanilla unmodified W3C testcase, and simulates the necessary +// user input to run the test. +var subtests = []; +for (var name of testnames) { + subtests.push({ + 'file': 'wpt/' + name + '.html', + 'prefs': apz_touch_action_prefs, + 'onload': apzScriptInjector(name), + }); +} + +if (isApzEnabled()) { + SimpleTest.waitForExplicitFinish(); + window.onload = function() { + runSubtestsSeriallyInFreshWindows(subtests) + .then(SimpleTest.finish, SimpleTest.finish); + }; +} + + </script> + </head> + <body> + </body> +</html> diff --git a/dom/events/test/pointerevents/touch_action_helpers.js b/dom/events/test/pointerevents/touch_action_helpers.js new file mode 100644 index 0000000000..12bb25b8fa --- /dev/null +++ b/dom/events/test/pointerevents/touch_action_helpers.js @@ -0,0 +1,242 @@ +// Some common helpers + +function promiseTimeout(delay) { + return new Promise(resolve => { + setTimeout(resolve, delay); + }); +} + +function promiseTouchStart(element) { + return new Promise(resolve => { + element.addEventListener("touchstart", resolve, { + passive: true, + once: true, + }); + }); +} + +async function touchScrollRight(aSelector = "#target0", aX = 20, aY = 20) { + const target = document.querySelector(aSelector); + const touchStartPromise = promiseTouchStart(document.body); + const touchEndPromise = promiseTouchEnd(document.body); + dump("Synthesizing horizontal drag\n"); + await promiseNativePointerDrag(target, "touch", aX + 40, aY, -40, 0); + await touchStartPromise; + dump("Got touchstart from the horizontal drag\n"); + await touchEndPromise; + dump("Got touchend from the horizontal drag\n"); +} + +async function touchScrollDown(aSelector = "#target0", aX = 20, aY = 20) { + const target = document.querySelector(aSelector); + const touchStartPromise = promiseTouchStart(document.body); + const touchEndPromise = promiseTouchEnd(document.body); + dump("Synthesizing vertical drag\n"); + await promiseNativePointerDrag(target, "touch", aX, aY + 40, 0, -40); + await touchStartPromise; + dump("Got touchstart from the vertical drag\n"); + await touchEndPromise; + dump("Got touchend from the vertical drag\n"); +} + +async function tapCompleteAndWaitTestDone() { + let testDone = new Promise(resolve => { + add_completion_callback(resolve); + }); + + var button = document.getElementById("btnComplete"); + button.click(); + await testDone; +} + +function promiseResetScrollLeft(aSelector = "#target0") { + var target = document.querySelector(aSelector); + return new Promise(resolve => { + target.addEventListener("scroll", function onScroll() { + if (target.scrollLeft == 0) { + target.removeEventListener("scroll", onScroll); + resolve(); + } + }); + }); +} + +// The main body functions to simulate the input events required for the named test + +async function pointerevent_touch_action_auto_css_touch_manual() { + let testDone = new Promise(resolve => { + add_completion_callback(resolve); + }); + + await touchScrollRight(); + await promiseApzFlushedRepaints(); + await touchScrollDown(); + await testDone; +} + +async function pointerevent_touch_action_button_test_touch_manual() { + await touchScrollDown(); + await promiseApzFlushedRepaints(); + await promiseTimeout(2 * scrollReturnInterval); + await touchScrollRight(); + let resetScrollLeftPromise = promiseResetScrollLeft(); + await promiseApzFlushedRepaints(); + await promiseTimeout(2 * scrollReturnInterval); + // Wait for resetting target0's scrollLeft to avoid the reset break the + // following scroll behaviors. + await resetScrollLeftPromise; + await touchScrollDown("#target0 > button"); + await promiseApzFlushedRepaints(); + await touchScrollRight("#target0 > button"); + await promiseApzFlushedRepaints(); + await tapCompleteAndWaitTestDone(); +} + +async function pointerevent_touch_action_inherit_child_auto_child_none_touch_manual() { + await touchScrollDown("#target0 > div div"); + await promiseApzFlushedRepaints(); + await touchScrollRight("#target0 > div div"); + await promiseApzFlushedRepaints(); + await tapCompleteAndWaitTestDone(); +} + +async function pointerevent_touch_action_inherit_child_none_touch_manual() { + await touchScrollDown("#target0 > div"); + await promiseApzFlushedRepaints(); + await touchScrollRight("#target0 > div"); + await promiseApzFlushedRepaints(); + await tapCompleteAndWaitTestDone(); +} + +async function pointerevent_touch_action_inherit_child_pan_x_child_pan_x_touch_manual() { + await touchScrollDown("#target0 > div div"); + await promiseApzFlushedRepaints(); + await touchScrollRight("#target0 > div div"); + await promiseApzFlushedRepaints(); + await tapCompleteAndWaitTestDone(); +} + +async function pointerevent_touch_action_inherit_child_pan_x_child_pan_y_touch_manual() { + await touchScrollDown("#target0 > div div"); + await promiseApzFlushedRepaints(); + await touchScrollRight("#target0 > div div"); + await promiseApzFlushedRepaints(); + await tapCompleteAndWaitTestDone(); +} + +async function pointerevent_touch_action_inherit_highest_parent_none_touch_manual() { + let testDone = new Promise(resolve => { + add_completion_callback(resolve); + }); + + await touchScrollDown("#target0 > div"); + await promiseApzFlushedRepaints(); + await touchScrollRight("#target0 > div"); + await testDone; +} + +async function pointerevent_touch_action_inherit_parent_none_touch_manual() { + await touchScrollDown(); + await promiseApzFlushedRepaints(); + await touchScrollRight(); + await promiseApzFlushedRepaints(); + await tapCompleteAndWaitTestDone(); +} + +async function pointerevent_touch_action_none_css_touch_manual() { + await touchScrollDown(); + await promiseApzFlushedRepaints(); + await touchScrollRight(); + await promiseApzFlushedRepaints(); + await tapCompleteAndWaitTestDone(); +} + +async function pointerevent_touch_action_pan_x_css_touch_manual() { + await touchScrollDown(); + await promiseApzFlushedRepaints(); + await touchScrollRight(); + await promiseApzFlushedRepaints(); + await tapCompleteAndWaitTestDone(); +} + +async function pointerevent_touch_action_pan_x_pan_y_pan_y_touch_manual() { + await touchScrollDown("#target0 > div div"); + await promiseApzFlushedRepaints(); + await touchScrollRight("#target0 > div div"); + await promiseApzFlushedRepaints(); + await tapCompleteAndWaitTestDone(); +} + +async function pointerevent_touch_action_pan_x_pan_y_touch_manual() { + let testDone = new Promise(resolve => { + add_completion_callback(resolve); + }); + + await touchScrollDown(); + await promiseApzFlushedRepaints(); + await touchScrollRight(); + await testDone; +} + +async function pointerevent_touch_action_pan_y_css_touch_manual() { + await touchScrollDown(); + await promiseApzFlushedRepaints(); + await touchScrollRight(); + await promiseApzFlushedRepaints(); + await tapCompleteAndWaitTestDone(); +} + +async function pointerevent_touch_action_span_test_touch_manual() { + await touchScrollDown(); + await promiseApzFlushedRepaints(); + await promiseTimeout(2 * scrollReturnInterval); + await touchScrollRight(); + let resetScrollLeftPromise = promiseResetScrollLeft(); + await promiseApzFlushedRepaints(); + await promiseTimeout(2 * scrollReturnInterval); + // Wait for resetting target0's scrollLeft to avoid the reset break the + // following scroll behaviors. + await resetScrollLeftPromise; + await touchScrollDown("#testspan"); + await promiseApzFlushedRepaints(); + await touchScrollRight("#testspan"); + await promiseApzFlushedRepaints(); + await tapCompleteAndWaitTestDone(); +} + +async function pointerevent_touch_action_svg_test_touch_manual() { + await touchScrollDown(); + await promiseApzFlushedRepaints(); + await promiseTimeout(2 * scrollReturnInterval); + await touchScrollRight(); + await promiseApzFlushedRepaints(); + await promiseTimeout(2 * scrollReturnInterval); + await touchScrollDown("#target0", 250, 250); + await promiseApzFlushedRepaints(); + await touchScrollRight("#target0", 250, 250); + await promiseApzFlushedRepaints(); + await tapCompleteAndWaitTestDone(); +} + +async function pointerevent_touch_action_table_test_touch_manual() { + await touchScrollDown("#row1"); + await promiseApzFlushedRepaints(); + await promiseTimeout(2 * scrollReturnInterval); + await touchScrollRight("#row1"); + let resetScrollLeftPromise = promiseResetScrollLeft(); + await promiseApzFlushedRepaints(); + await promiseTimeout(2 * scrollReturnInterval); + // Wait for resetting target0's scrollLeft to avoid the reset break the + // following scroll behaviors. + await resetScrollLeftPromise; + await touchScrollDown("#cell3"); + await promiseApzFlushedRepaints(); + await touchScrollRight("#cell3"); + await promiseApzFlushedRepaints(); + await tapCompleteAndWaitTestDone(); +} + +// This the stuff that runs the appropriate body function above + +var test = eval(_ACTIVE_TEST_NAME.replace(/-/g, "_")); +waitUntilApzStable().then(test).then(subtestDone, subtestFailed); diff --git a/dom/events/test/pointerevents/wpt/compat/pointerevent_touch-action_two-finger_interaction-manual.html b/dom/events/test/pointerevents/wpt/compat/pointerevent_touch-action_two-finger_interaction-manual.html new file mode 100644 index 0000000000..3537e0e1e9 --- /dev/null +++ b/dom/events/test/pointerevents/wpt/compat/pointerevent_touch-action_two-finger_interaction-manual.html @@ -0,0 +1,102 @@ +<!doctype html> +<html> + <head> + <title>Pointer Event: touch-action test for two-finger interaction</title> + <meta content="text/html; charset=UTF-8" http-equiv="Content-Type"/> + <link rel="author" title="Google" href="http://www.google.com "/> + <link rel="help" href="https://compat.spec.whatwg.org/#touch-action" /> + <meta name="assert" content="Tests that a two-finger pan gesture is cancelled in 'touch-action: pan-x pan-y' but is allowed in 'touch-action: pinch-zoom'"/> + <link rel="stylesheet" type="text/css" href="../pointerevent_styles.css"> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script type="text/javascript" src="../pointerevent_support.js"></script> + <script type="text/javascript"> + var event_log = []; + var active_pointers = 0; + + function resetTestState() { + event_log = []; + active_pointers = 0; + } + + function run() { + var test_pointer_events = [ + setup_pointerevent_test("two-finger pan on 'touch-action: pan-x pan-y'", ["touch"]), + setup_pointerevent_test("two-finger pan on 'touch-action: pinch-zoom'", ["touch"]) + ]; + var expected_events = [ + "pointerdown@black, pointerdown@black, pointerup@black, pointerup@black", + "pointerdown@grey, pointerdown@grey, pointercancel@grey, pointercancel@grey" + ]; + var current_test_index = 0; + + on_event(document.getElementById("done"), "click", function() { + test_pointer_events[current_test_index].step(function () { + assert_equals(active_pointers, 0); + assert_equals(event_log.join(", "), expected_events[current_test_index]); + }); + event_log = []; + + test_pointer_events[current_test_index++].done(); + }); + + var targets = [document.getElementById("black"), document.getElementById("grey")]; + + ["pointerdown", "pointerup", "pointercancel"].forEach(function(eventName) { + targets.forEach(function(target){ + on_event(target, eventName, function (event) { + event_log.push(event.type + "@" + event.target.id); + + if (event.type == "pointerdown") { + active_pointers++; + + } else { + active_pointers--; + } + }); + }); + }); + } + </script> + <style> + .box { + width: 250px; + height: 150px; + float: left; + margin: 10px; + } + + #black { + touch-action: pan-x pan-y; + background-color: black; + } + + #grey { + touch-action: pinch-zoom; + background-color: grey; + } + + #done { + float: left; + padding: 20px; + } + </style> + </head> + <body onload="run()"> + <h1>Pointer Event: touch-action test for two-finger interaction</h1> + <h2 id="pointerTypeDescription"></h2> + <h4> + Tests that a two-finger pan gesture is cancelled in 'touch-action: pan-x pan-y' but is allowed in 'touch-action: pinch-zoom' + </h4> + <ol> + <li>Touch on Black with two fingers and drag both fingers down at same speed.</li> + <li>Tap on Done.</li> + <li>Touch on Grey with two fingers and drag both fingers down at same speed.</li> + <li>Tap on Done.</li> + </ol> + <div class="box" id="black"></div> + <input type="button" id="done" value="Done" /> + <div class="box" id="grey"></div> + <div id="log"></div> + </body> +</html> diff --git a/dom/events/test/pointerevents/wpt/html/pointerevent_drag_interaction-manual.html b/dom/events/test/pointerevents/wpt/html/pointerevent_drag_interaction-manual.html new file mode 100644 index 0000000000..1a80d239b8 --- /dev/null +++ b/dom/events/test/pointerevents/wpt/html/pointerevent_drag_interaction-manual.html @@ -0,0 +1,103 @@ +<html> + <head> + <title>Pointer Events interaction with drag and drop</title> + <meta name="viewport" content="width=device-width"> + <link rel="stylesheet" type="text/css" href="../pointerevent_styles.css"> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <!-- Additional helper script for common checks across event types --> + <script type="text/javascript" src="../pointerevent_support.js"></script> + <script> + var eventList = ['pointerdown', 'pointerup', 'pointercancel', 'gotpointercapture', 'lostpointercapture', 'dragstart', 'mousedown']; + + PhaseEnum = { + DndWithoutCapture: 0, + DndWithCapture: 1, + DndWithCaptureMouse: 2, + DndPrevented: 3, + Done: 4, + }; + var phase = PhaseEnum.DndWithoutCapture; + var received_events = []; + var pointerId = -1; + + function resetTestState() { + phase = PhaseEnum.DndWithoutCapture; + } + + function run() { + var test_pointerEvent = setup_pointerevent_test("pointer events vs drag and drop", ['mouse']); + + var target0 = document.querySelector('#target0'); + var target1 = document.querySelector('#target1'); + + function handleEvent(e) { + if (e.type == 'pointerdown') { + received_events = []; + if (phase == PhaseEnum.DndWithCapture) { + target0.setPointerCapture(e.pointerId); + } else if (phase == PhaseEnum.DndWithCaptureMouse) { + pointerId = e.pointerId; + } + } + if (e.type == 'mousedown') { + if (phase == PhaseEnum.DndWithCaptureMouse) { + target0.setPointerCapture(pointerId); + } + } + received_events.push(e.type + "@" + e.target.id); + if (e.type == 'dragstart') { + e.dataTransfer.setData('text/plain', 'dragstart test'); + if (phase == PhaseEnum.DndPrevented) + e.preventDefault(); + } + if (phase == PhaseEnum.DndWithoutCapture && e.type == 'pointercancel') { + phase++; + test(() => { + assert_equals(received_events.join(', '), "pointerdown@target0, mousedown@target0, dragstart@target0, pointercancel@target0", "Pointercancel should be fired with the expected order when drag operation starts."); + }, "Pointercancel when drag operation starts"); + } else if (phase == PhaseEnum.DndWithCapture && e.type == 'lostpointercapture') { + test(() => { + assert_equals(received_events.join(', '), "pointerdown@target0, mousedown@target0, gotpointercapture@target0, dragstart@target0, pointercancel@target0, lostpointercapture@target0", "Pointercancel and lostpointercapture should be fired with the expected order when drag operation starts."); + }, "Pointercancel while capturing when drag operation starts"); + phase++; + } else if (phase == PhaseEnum.DndWithCaptureMouse && e.type == 'lostpointercapture') { + test(() => { + assert_equals(received_events.join(', '), "pointerdown@target0, mousedown@target0, gotpointercapture@target0, dragstart@target0, pointercancel@target0, lostpointercapture@target0", "Pointercancel and lostpointercapture should be fired with the expected order when drag operation starts."); + }, "Pointercancel while capturing on mousedown when drag operation starts"); + phase++; + } else if (phase == PhaseEnum.DndPrevented && e.type == 'pointerup') { + test(() => { + assert_equals(received_events.join(', '), "pointerdown@target0, mousedown@target0, dragstart@target0, pointerup@target1", "Pointerevent stream shouldn't get interrupted when drag is prevented."); + }, "Pointerevent stream when drag is prevented."); + phase++; + test_pointerEvent.done(); + } + } + eventList.forEach(function(eventName) { + on_event(target0, eventName, handleEvent); + on_event(target1, eventName, handleEvent); + }); + } + </script> + </head> + <body onload="run()"> + <h1>Pointer Events interaction with drag and drop</h1> + <h2 id="pointerTypeDescription"></h2> + <h4> + Test Description: This test checks that the pointercancel (and if needed lostpointercapture) is dispatched when drag starts. + <ol> + <li>Press down on the black square.</li> + <li>Move your pointer to purple square and release.</li> + <li>Repeat the first two steps.</li> + <li>Repeat the first two steps once again.</li> + <li>Repeat the first two steps once again.</li> + </ol> + Test passes if the proper behavior of the events is observed. + </h4> + <div id="testContainer"> + <div draggable="true" id="target0"></div> + <div id="target1"></div> + </div> + </body> +</html> diff --git a/dom/events/test/pointerevents/wpt/idlharness.html b/dom/events/test/pointerevents/wpt/idlharness.html new file mode 100644 index 0000000000..a4ba4c35f5 --- /dev/null +++ b/dom/events/test/pointerevents/wpt/idlharness.html @@ -0,0 +1,104 @@ +<!doctype html> +<meta charset=utf-8> +<title>idlharness test</title> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<script src="/resources/WebIDLParser.js"></script> +<script src="/resources/idlharness.js"></script> + +<pre id='untested_idl' style='display:none'> +[PrimaryGlobal] +interface Window { +}; + +[TreatNonObjectAsNull] +callback EventHandlerNonNull = any (Event event); +typedef EventHandlerNonNull? EventHandler; + +[NoInterfaceObject] +interface GlobalEventHandlers { +}; +Window implements GlobalEventHandlers; + +interface Navigator { +}; + +interface Element { +}; + +interface HTMLElement : Element { +}; +HTMLElement implements GlobalEventHandlers; + +interface Document { +}; +Document implements GlobalEventHandlers; + +interface MouseEvent { +}; + +</pre> + +<pre id='idl'> +dictionary PointerEventInit : MouseEventInit { + long pointerId = 0; + double width = 1; + double height = 1; + float pressure = 0; + float tangentialPressure = 0; + long tiltX = 0; + long tiltY = 0; + long twist = 0; + DOMString pointerType = ""; + boolean isPrimary = false; +}; + +[Constructor(DOMString type, optional PointerEventInit eventInitDict)] +interface PointerEvent : MouseEvent { + readonly attribute long pointerId; + readonly attribute double width; + readonly attribute double height; + readonly attribute float pressure; + readonly attribute float tangentialPressure; + readonly attribute long tiltX; + readonly attribute long tiltY; + readonly attribute long twist; + readonly attribute DOMString pointerType; + readonly attribute boolean isPrimary; +}; + +partial interface Element { + void setPointerCapture(long pointerId); + void releasePointerCapture(long pointerId); + boolean hasPointerCapture(long pointerId); +}; + +partial interface GlobalEventHandlers { + attribute EventHandler ongotpointercapture; + attribute EventHandler onlostpointercapture; + attribute EventHandler onpointerdown; + attribute EventHandler onpointermove; + attribute EventHandler onpointerup; + attribute EventHandler onpointercancel; + attribute EventHandler onpointerover; + attribute EventHandler onpointerout; + attribute EventHandler onpointerenter; + attribute EventHandler onpointerleave; +}; + +partial interface Navigator { + readonly attribute long maxTouchPoints; +}; +</pre> +<script> + var idl_array = new IdlArray(); + idl_array.add_untested_idls(document.getElementById("untested_idl").textContent); + idl_array.add_idls(document.getElementById("idl").textContent); + + // Note that I don't bother including Document here because there are still + // a bunch of differences between browsers around Document vs HTMLDocument. + idl_array.add_objects({ + Window: ["window"], + Navigator: ["navigator"]}); + idl_array.test(); +</script> diff --git a/dom/events/test/pointerevents/wpt/pointerevent_attributes_hoverable_pointers-manual.html b/dom/events/test/pointerevents/wpt/pointerevent_attributes_hoverable_pointers-manual.html new file mode 100644 index 0000000000..0922ae7448 --- /dev/null +++ b/dom/events/test/pointerevents/wpt/pointerevent_attributes_hoverable_pointers-manual.html @@ -0,0 +1,143 @@ +<!doctype html> +<html> + <head> + <title>Pointer Events properties tests</title> + <meta name="viewport" content="width=device-width"> + <link rel="stylesheet" type="text/css" href="pointerevent_styles.css"> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <!-- Additional helper script for common checks across event types --> + <script type="text/javascript" src="pointerevent_support.js"></script> + <script> + var detected_pointertypes = {}; + var detected_eventTypes = {}; + var eventList = ['pointerover', 'pointerenter', 'pointermove', 'pointerdown', 'pointerup', 'pointerout', 'pointerleave']; + var expectedPointerId = NaN; + + function resetTestState() { + detected_eventTypes = {}; + document.getElementById("square1").style.visibility = 'visible'; + document.getElementById('innerFrame').contentDocument.getElementById("square2").style.visibility = 'hidden'; + expectedPointerId = NaN; + } + function checkPointerEventAttributes(event, targetBoundingClientRect, testNamePrefix) { + if (detected_eventTypes[event.type]) + return; + var expectedEventType = eventList[Object.keys(detected_eventTypes).length]; + detected_eventTypes[event.type] = true; + var pointerTestName = testNamePrefix + ' ' + expectedPointerType + ' ' + expectedEventType; + + detected_pointertypes[event.pointerType] = true; + + test(function() { + assert_equals(event.type, expectedEventType, "Event.type should be " + expectedEventType) + }, pointerTestName + "'s type should be " + expectedEventType); + + // Test button and buttons + if (event.type == 'pointerdown') { + test(function() { + assert_true(event.button == 0, "Button attribute is 0") + }, pointerTestName + "'s button attribute is 0 when left mouse button is pressed."); + test(function() { + assert_true(event.buttons == 1, "Buttons attribute is 1") + }, pointerTestName + "'s buttons attribute is 1 when left mouse button is pressed."); + } else if (event.type == 'pointerup') { + test(function() { + assert_true(event.button == 0, "Button attribute is 0") + }, pointerTestName + "'s button attribute is 0 when left mouse button is just released."); + test(function() { + assert_true(event.buttons == 0, "Buttons attribute is 0") + }, pointerTestName + "'s buttons attribute is 0 when left mouse button is just released."); + } else { + test(function() { + assert_true(event.button == -1, "Button attribute is -1") + }, pointerTestName + "'s button is -1 when mouse buttons are in released state."); + test(function() { + assert_true(event.buttons == 0, "Buttons attribute is 0") + }, pointerTestName + "'s buttons is 0 when mouse buttons are in released state."); + } + + // Test clientX and clientY + if (event.type != 'pointerout' && event.type != 'pointerleave' ) { + test(function () { + assert_true(event.clientX >= targetBoundingClientRect.left && event.clientX < targetBoundingClientRect.right && event.clientY >= targetBoundingClientRect.top && event.clientY < targetBoundingClientRect.bottom, "ClientX/Y should be in the boundaries of the box"); + }, pointerTestName + "'s ClientX and ClientY attributes are correct."); + } else { + test(function () { + assert_true(event.clientX < targetBoundingClientRect.left || event.clientX > targetBoundingClientRect.right - 1 || event.clientY < targetBoundingClientRect.top || event.clientY > targetBoundingClientRect.bottom - 1, "ClientX/Y should be out of the boundaries of the box"); + }, pointerTestName + "'s ClientX and ClientY attributes are correct."); + } + + check_PointerEvent(event, testNamePrefix); + + // Test isPrimary value + test(function () { + assert_equals(event.isPrimary, true, "isPrimary should be true"); + }, pointerTestName + ".isPrimary attribute is correct."); + + // Test pointerId value + if (isNaN(expectedPointerId)) { + expectedPointerId = event.pointerId; + } else { + test(function () { + assert_equals(event.pointerId, expectedPointerId, "pointerId should remain the same for the same active pointer"); + }, pointerTestName + ".pointerId should be the same as previous pointer events for this active pointer."); + } + } + + function run() { + var test_pointerEvent = setup_pointerevent_test("pointerevent attributes", HOVERABLE_POINTERS); + var square1 = document.getElementById("square1"); + var rectSquare1 = square1.getBoundingClientRect(); + var innerFrame = document.getElementById('innerFrame'); + var square2 = innerFrame.contentDocument.getElementById('square2'); + var rectSquare2 = square2.getBoundingClientRect(); + + eventList.forEach(function(eventName) { + on_event(square1, eventName, function (event) { + if (square1.style.visibility == 'hidden') + return; + checkPointerEventAttributes(event, rectSquare1, ""); + if (Object.keys(detected_eventTypes).length == eventList.length) { + square1.style.visibility = 'hidden'; + detected_eventTypes = {}; + square2.style.visibility = 'visible'; + expectedPointerId = NaN; + } + }); + on_event(square2, eventName, function (event) { + checkPointerEventAttributes(event, rectSquare2, "Inner frame "); + if (Object.keys(detected_eventTypes).length == eventList.length) { + square2.style.visibility = 'hidden'; + test_pointerEvent.done(); + } + }); + }); + } + </script> + </head> + <body onload="run()"> + <h1>Pointer Events hoverable pointer attributes test</h1> + <h2 id="pointerTypeDescription"></h2> + <h4> + Test Description: This test checks the properties of hoverable pointer events. If you are using hoverable pen don't leave the range of digitizer while doing the instructions. + <ol> + <li>Move your pointer over the black square and click on it.</li> + <li>Then move it off the black square so that it disappears.</li> + <li>When red square appears move your pointer over the red square and click on it.</li> + <li>Then move it off the red square.</li> + </ol> + + Test passes if the proper behavior of the events is observed. + </h4> + <div id="square1" class="square"></div> + <iframe id="innerFrame" src="resources/pointerevent_attributes_hoverable_pointers-iframe.html"></iframe> + <div class="spacer"></div> + <div id="complete-notice"> + <p>The following pointer types were detected: <span id="pointertype-log"></span>.</p> + <p>Refresh the page to run the tests again with a different pointer type.</p> + </div> + <div id="log"></div> + </body> +</html> + diff --git a/dom/events/test/pointerevents/wpt/pointerevent_attributes_nohover_pointers-manual.html b/dom/events/test/pointerevents/wpt/pointerevent_attributes_nohover_pointers-manual.html new file mode 100644 index 0000000000..0fd7904ef0 --- /dev/null +++ b/dom/events/test/pointerevents/wpt/pointerevent_attributes_nohover_pointers-manual.html @@ -0,0 +1,126 @@ +<!doctype html> +<html> + <head> + <title>Pointer Events properties tests</title> + <meta name="viewport" content="width=device-width"> + <link rel="stylesheet" type="text/css" href="pointerevent_styles.css"> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <!-- Additional helper script for common checks across event types --> + <script type="text/javascript" src="pointerevent_support.js"></script> + <script> + var detected_pointertypes = {}; + var detected_eventTypes = {}; + var eventList = ['pointerover', 'pointerenter', 'pointerdown', 'pointerup', 'pointerout', 'pointerleave']; + var expectedPointerId = NaN; + + function resetTestState() { + detected_eventTypes = {}; + document.getElementById("square1").style.visibility = 'visible'; + document.getElementById('innerFrame').contentDocument.getElementById("square2").style.visibility = 'hidden'; + } + function checkPointerEventAttributes(event, targetBoundingClientRect, testNamePrefix) { + if (detected_eventTypes[event.type]) + return; + var expectedEventType = eventList[Object.keys(detected_eventTypes).length]; + detected_eventTypes[event.type] = true; + var pointerTestName = testNamePrefix + ' ' + expectedPointerType + ' ' + expectedEventType; + + detected_pointertypes[event.pointerType] = true; + + test(function() { + assert_equals(event.type, expectedEventType, "Event.type should be " + expectedEventType) + }, pointerTestName + "'s type should be " + expectedEventType); + + // Test button and buttons + test(function() { + assert_true(event.button == 0, "Button attribute is 0") + }, pointerTestName + "'s button attribute is 0 when left mouse button is pressed."); + + if (event.type == 'pointerdown' || event.type == 'pointerover' || event.type == 'pointerenter') { + test(function() { + assert_true(event.buttons == 1, "Buttons attribute is 1") + }, pointerTestName + "'s buttons attribute is 1 when left mouse button is pressed."); + } else { + test(function() { + assert_true(event.buttons == 0, "Buttons attribute is 0") + }, pointerTestName + "'s buttons is 0 when mouse buttons are in released state."); + } + + // Test clientX and clientY + test(function () { + assert_true(event.clientX >= targetBoundingClientRect.left && event.clientX < targetBoundingClientRect.right && event.clientY >= targetBoundingClientRect.top && event.clientY < targetBoundingClientRect.bottom, "ClientX/Y should be in the boundaries of the box"); + }, pointerTestName + "'s ClientX and ClientY attributes are correct."); + + check_PointerEvent(event, testNamePrefix); + + // Test isPrimary + test(function () { + assert_equals(event.isPrimary, true, "isPrimary should be true"); + }, pointerTestName + ".isPrimary attribute is correct."); + + // Test pointerId value + if (isNaN(expectedPointerId)) { + expectedPointerId = event.pointerId; + } else { + test(function () { + assert_equals(event.pointerId, expectedPointerId, "pointerId should remain the same for the same active pointer"); + }, pointerTestName + ".pointerId should be the same as previous pointer events for this active pointer."); + } + } + + function run() { + var test_pointerEvent = setup_pointerevent_test("pointerevent attributes", NOHOVER_POINTERS); + var square1 = document.getElementById("square1"); + var rectSquare1 = square1.getBoundingClientRect(); + var innerFrame = document.getElementById('innerFrame'); + var square2 = innerFrame.contentDocument.getElementById('square2'); + var rectSquare2 = square2.getBoundingClientRect(); + + eventList.forEach(function(eventName) { + on_event(square1, eventName, function (event) { + if (square1.style.visibility == 'hidden') + return; + checkPointerEventAttributes(event, rectSquare1, ""); + if (Object.keys(detected_eventTypes).length == eventList.length) { + square1.style.visibility = 'hidden'; + detected_eventTypes = {}; + square2.style.visibility = 'visible'; + expectedPointerId = NaN; + } + }); + on_event(square2, eventName, function (event) { + checkPointerEventAttributes(event, rectSquare2, "Inner frame "); + if (Object.keys(detected_eventTypes).length == eventList.length) { + square2.style.visibility = 'hidden'; + test_pointerEvent.done(); + } + }); + }); + } + </script> + </head> + <body onload="run()"> + <h1>Pointer Events no-hover pointer attributes test</h1> + <h2 id="pointerTypeDescription"></h2> + <h4> + Test Description: This test checks the properties of pointer events that do not support hover. + <ol> + <li>Tap the black square.</li> + <li>Then move it off the black square so that it disappears.</li> + <li>When the red square appears tap on that as well.</li> + </ol> + + Test passes if the proper behavior of the events is observed. + </h4> + <div id="square1" class="square"></div> + <iframe id="innerFrame" src="resources/pointerevent_attributes_hoverable_pointers-iframe.html"></iframe> + <div class="spacer"></div> + <div id="complete-notice"> + <p>The following pointer types were detected: <span id="pointertype-log"></span>.</p> + <p>Refresh the page to run the tests again with a different pointer type.</p> + </div> + <div id="log"></div> + </body> +</html> + diff --git a/dom/events/test/pointerevents/wpt/pointerevent_boundary_events_in_capturing-manual.html b/dom/events/test/pointerevents/wpt/pointerevent_boundary_events_in_capturing-manual.html new file mode 100644 index 0000000000..0de4d55ed1 --- /dev/null +++ b/dom/events/test/pointerevents/wpt/pointerevent_boundary_events_in_capturing-manual.html @@ -0,0 +1,97 @@ +<!doctype html> +<html> + <head> + <title>Pointer Events boundary events in capturing tests</title> + <meta name="viewport" content="width=device-width"> + <link rel="stylesheet" type="text/css" href="pointerevent_styles.css"> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <!-- Additional helper script for common checks across event types --> + <script type="text/javascript" src="pointerevent_support.js"></script> + <script> + var detected_pointertypes = {}; + var eventList = All_Pointer_Events; + PhaseEnum = { + WaitingForDown: "down", + WaitingForFirstMove: "firstMove", + WaitingForSecondMove: "secondMove", + WaitingForUp: "up" + } + var phase = PhaseEnum.WaitingForDown; + var eventsRecieved = []; + + function resetTestState() { + eventsRecieved = []; + phase = PhaseEnum.WaitingForDown; + } + function run() { + var test_pointerEvent = setup_pointerevent_test("pointerevent boundary events in capturing", ALL_POINTERS); + var target = document.getElementById("target0"); + var listener = document.getElementById("listener"); + + eventList.forEach(function(eventName) { + on_event(target, eventName, function (event) { + if (phase == PhaseEnum.WaitingForDown) { + if (eventName == 'pointerdown') { + listener.setPointerCapture(event.pointerId); + phase = PhaseEnum.WaitingForFirstMove; + } + } else if (phase == PhaseEnum.WaitingForUp) { + if (event.type == 'pointerup') + test_pointerEvent.done(); + } else { + eventsRecieved.push(event.type + '@target'); + if (phase == PhaseEnum.WaitingForSecondMove && event.type == 'pointermove') { + test(function () { + checkPointerEventType(event); + assert_array_equals(eventsRecieved, ['lostpointercapture@listener', 'pointerout@listener', 'pointerleave@listener', 'pointerover@target', 'pointerenter@target', 'pointermove@target'], + 'lostpointercapture and pointerout/leave should be dispatched to the capturing target and pointerover/enter should be dispatched to the hit-test element before the first pointermove event after releasing pointer capture'); + }, expectedPointerType + " pointer events boundary events when releasing capture"); + phase = PhaseEnum.WaitingForUp; + } + } + }); + on_event(listener, eventName, function (event) { + if (phase == PhaseEnum.WaitingForDown) + return; + eventsRecieved.push(event.type + '@listener'); + if (phase == PhaseEnum.WaitingForFirstMove && eventName == 'pointermove') { + test(function () { + checkPointerEventType(event); + assert_array_equals(eventsRecieved, ['pointerout@target', 'pointerleave@target', 'pointerover@listener', 'pointerenter@listener', 'gotpointercapture@listener', 'pointermove@listener'], + 'pointerout/leave should be dispatched to the previous target and pointerover/enter and gotpointercapture should be dispatched to the capturing element before the first captured pointermove event'); + }, expectedPointerType + " pointer events boundary events when receiving capture"); + listener.releasePointerCapture(event.pointerId); + eventsRecieved = []; + phase = PhaseEnum.WaitingForSecondMove; + } + }); + }); + } + </script> + </head> + <body onload="run()"> + <h1>Pointer Events boundary events in capturing</h1> + <h2 id="pointerTypeDescription"></h2> + <h4> + Test Description: This test checks the boundary events of pointer events while the capturing changes. If you are using hoverable pen don't leave the range of digitizer while doing the instructions. + <ol> + <li>Move your pointer over the black square</li> + <li>Press down the pointer (i.e. press left button with mouse or touch the screen with finger or pen).</li> + <li>Drag the pointer within the black square.</li> + <li>Release the pointer.</li> + </ol> + + Test passes if the proper behavior of the events is observed. + </h4> + <div id="target0" class="touchActionNone"> + </div> + <div id="listener">Do not hover over or touch this element. </div> + <div id="complete-notice"> + <p>The following pointer types were detected: <span id="pointertype-log"></span>.</p> + <p>Refresh the page to run the tests again with a different pointer type.</p> + </div> + <div id="log"></div> + </body> +</html> + diff --git a/dom/events/test/pointerevents/wpt/pointerevent_change-touch-action-onpointerdown_touch-manual.html b/dom/events/test/pointerevents/wpt/pointerevent_change-touch-action-onpointerdown_touch-manual.html new file mode 100644 index 0000000000..04d56cb7a5 --- /dev/null +++ b/dom/events/test/pointerevents/wpt/pointerevent_change-touch-action-onpointerdown_touch-manual.html @@ -0,0 +1,135 @@ +<!doctype html> +<html> + <head> + <title>Change touch-action on pointerdown</title> + <meta name="viewport" content="width=device-width"> + <link rel="stylesheet" type="text/css" href="pointerevent_styles.css"> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="pointerevent_support.js"></script> + <style> + #target0 { + background: black; + width: 700px; + height: 430px; + color: white; + overflow-y: auto; + overflow-x: auto; + white-space: nowrap; + } + </style> + </head> + <body onload="run()"> + <h1>Pointer Events touch-action attribute support</h1> + <h4>Test Description: Press and hold your touch. Try to scroll text in any direction. + Then release your touch and try to scroll again. Expected: no panning. + </h4> + <p>Note: this test is for touch-devices only</p> + <div id="target0" style="touch-action: auto;"> + <p> + Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diem + nonummy nibh euismod tincidunt ut lacreet dolore magna aliguam erat volutpat. + Ut wisis enim ad minim veniam, quis nostrud exerci tution ullamcorper suscipit + lobortis nisl ut aliquip ex ea commodo consequat. + </p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p> + Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diem + nonummy nibh euismod tincidunt ut lacreet dolore magna aliguam erat volutpat. + Ut wisis enim ad minim veniam, quis nostrud exerci tution ullamcorper suscipit + lobortis nisl ut aliquip ex ea commodo consequat. + </p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p> + Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diem + nonummy nibh euismod tincidunt ut lacreet dolore magna aliguam erat volutpat. + Ut wisis enim ad minim veniam, quis nostrud exerci tution ullamcorper suscipit + lobortis nisl ut aliquip ex ea commodo consequat. + </p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p> + Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diem + nonummy nibh euismod tincidunt ut lacreet dolore magna aliguam erat volutpat. + Ut wisis enim ad minim veniam, quis nostrud exerci tution ullamcorper suscipit + lobortis nisl ut aliquip ex ea commodo consequat. + </p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + </div> + <script type='text/javascript'> + var detected_pointertypes = {}; + + var styleIsChanged = false; + var scrollIsReceived = false; + var firstTouchCompleted = false; + var countToPass = 50; + var xScr0, yScr0, xScr1, yScr1; + + setup({ explicit_done: true }); + add_completion_callback(showPointerTypes); + + function run() { + var target0 = document.getElementById("target0"); + + on_event(target0, 'scroll', function(event) { + if(!scrollIsReceived && firstTouchCompleted) { + test(function() { + failOnScroll(); + }, "scroll was received while shouldn't"); + scrollIsReceived = true; + } + done(); + }); + + on_event(target0, 'pointerdown', function(event) { + detected_pointertypes[event.pointerType] = true; + if(!styleIsChanged) { + var before = document.getElementById('target0').style.touchAction; + + document.getElementById('target0').style.touchAction = 'none'; + + var after = document.getElementById('target0').style.touchAction; + + test(function() { + assert_true(before != after, "touch-action was changed"); + }, "touch-action was changed"); + + styleIsChanged = true; + } + }); + + on_event(target0, 'pointerup', function(event) { + firstTouchCompleted = true; + }); + } + </script> + <h1>touch-action: auto to none</h1> + <div id="complete-notice"> + <p>The following pointer types were detected: <span id="pointertype-log"></span>.</p> + </div> + <div id="log"></div> + </body> +</html>
\ No newline at end of file diff --git a/dom/events/test/pointerevents/wpt/pointerevent_constructor.html b/dom/events/test/pointerevents/wpt/pointerevent_constructor.html new file mode 100644 index 0000000000..b2a779d1f7 --- /dev/null +++ b/dom/events/test/pointerevents/wpt/pointerevent_constructor.html @@ -0,0 +1,106 @@ +<!doctype html> +<html> + <head> + <title>PointerEvent: Constructor test</title> + <meta name="viewport" content="width=device-width"> + <link rel="stylesheet" type="text/css" href="pointerevent_styles.css"> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <!-- Additional helper script for common checks across event types --> + <script type="text/javascript" src="pointerevent_support.js"></script> + </head> + <body> + <h1>PointerEvent: Dispatch custom event</h1> + <h4>Test Description: This test checks if PointerEvent constructor works properly using synthetic pointerover and pointerout events. For valid results, this test must be run without generating real (trusted) pointerover or pointerout events on the black rectangle below.</h4> + <div id="target0"></div> + <script> + var detected_pointertypes = {}; + add_completion_callback(showPointerTypes); + + async_test(function() { + var target0 = document.getElementById("target0"); + // set values for non-default constructor + var testBubbles = true; + var testCancelable = true; + var testPointerId = 42; + var testPointerType = 'pen'; + var testClientX = 300; + var testClientY = 500; + var testWidth = 3; + var testHeight = 5; + var testTiltX = -45; + var testTiltY = 30; + var testButton = 0; + var testButtons = 1; + var testPressure = 0.4; + var testIsPrimary = true; + + on_event(target0, "pointerover", this.step_func(function(event) { + detected_pointertypes[ event.pointerType ] = true; + generate_tests(assert_equals, [ + ["custom bubbles", event.bubbles, testBubbles], + ["custom cancelable", event.cancelable, testCancelable], + ["custom pointerId", event.pointerId, testPointerId], + ["custom pointerType", event.pointerType, testPointerType], + ["custom button", event.button, testButton], + ["custom buttons", event.buttons, testButtons], + ["custom width", event.width, testWidth], + ["custom height", event.height, testHeight], + ["custom clientX", event.clientX, testClientX], + ["custom clientY", event.clientY, testClientY], + ["custom tiltX", event.tiltX, testTiltX], + ["custom tiltY", event.tiltY, testTiltY], + ["custom isPrimary", event.isPrimary, testIsPrimary] + ]); + test(function() { + assert_approx_equals(event.pressure, testPressure, 0.00000001, "custom pressure: "); + }, "custom pressure: "); + })); + + on_event(target0, "pointerout", this.step_func(function(event) { + generate_tests(assert_equals, [ + ["default pointerId", event.pointerId, 0], + ["default pointerType", event.pointerType, ""], + ["default width", event.width, 1], + ["default height", event.height, 1], + ["default tiltX", event.tiltX, 0], + ["default tiltY", event.tiltY, 0], + ["default pressure", event.pressure, 0], + ["default isPrimary", event.isPrimary, false] + ]); + })); + + on_event(window, "load", this.step_func_done(function() { + assert_not_equals(window.PointerEvent, undefined); + + var pointerEventCustom = new PointerEvent("pointerover", + {bubbles: testBubbles, + cancelable: testCancelable, + pointerId: testPointerId, + pointerType: testPointerType, + width: testWidth, + height: testHeight, + clientX: testClientX, + clientY: testClientY, + tiltX: testTiltX, + tiltY: testTiltY, + button: testButton, + buttons: testButtons, + pressure: testPressure, + isPrimary: testIsPrimary + }); + // A PointerEvent created with a PointerEvent constructor must have all its attributes set to the corresponding values provided to the constructor. + // For attributes where values are not provided to the constructor, the corresponding default values must be used. + // TA: 12.1 + target0.dispatchEvent(pointerEventCustom); + var pointerEventDefault = new PointerEvent("pointerout"); + target0.dispatchEvent(pointerEventDefault); + }, "PointerEvent constructor")); + }) + </script> + <div id="complete-notice"> + <p>The following pointer types were detected: <span id="pointertype-log"></span>.</p> + </div> + <div id="log"></div> + </body> +</html> diff --git a/dom/events/test/pointerevents/wpt/pointerevent_multiple_primary_pointers_boundary_events-manual.html b/dom/events/test/pointerevents/wpt/pointerevent_multiple_primary_pointers_boundary_events-manual.html new file mode 100644 index 0000000000..eb758c7073 --- /dev/null +++ b/dom/events/test/pointerevents/wpt/pointerevent_multiple_primary_pointers_boundary_events-manual.html @@ -0,0 +1,145 @@ +<!doctype html> +<html> + <head> + <title>Pointer Event: Boundary compatibility events for multiple primary pointers</title> + <meta content="text/html; charset=UTF-8" http-equiv="Content-Type"/> + <link rel="author" title="Google" href="http://www.google.com "/> + <meta name="assert" content="When more than one primary pointers are active, each will have an independent sequence of pointer boundary events but the compatibilty mouse boundary events have their own sequence."/> + <link rel="stylesheet" type="text/css" href="pointerevent_styles.css"> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script type="text/javascript" src="pointerevent_support.js"></script> + <script type="text/javascript"> + var test_pointerEvent = async_test("Multi-pointer boundary compat events"); + add_completion_callback(end_of_test); + + var detected_pointertypes = {}; + var event_log = []; + + // These two ids help us detect two different pointing devices. + var first_entry_pointer_id = -1; + var second_entry_pointer_id = -1; + + // Current node for each pointer id + var current_node_for_id = {}; + + function end_of_test() { + showLoggedEvents(); + showPointerTypes(); + } + + function end_of_interaction() { + test(function () { + assert_equals(event_log.join(", "), + "mouseover@target0, mouseenter@target0, mouseout@target0, mouseleave@target0, " + + "mouseover@target1, mouseenter@target1, mouseout@target1, mouseleave@target1, " + + "mouseover@target0, mouseenter@target0, mouseout@target0, mouseleave@target0" + ); + }, "Event log"); + + test_pointerEvent.done(); // complete test + } + + function log_event(label) { + event_log.push(label); + } + + function run() { + on_event(document.getElementById("done"), "click", end_of_interaction); + + var target_list = ["target0", "target1"]; + var pointer_event_list = ["pointerenter", "pointerleave", "pointerover", "pointerout", "pointerdown"]; + var mouse_event_list = ["mouseenter", "mouseleave", "mouseover", "mouseout"]; + + target_list.forEach(function(targetId) { + var target = document.getElementById(targetId); + + pointer_event_list.forEach(function(eventName) { + on_event(target, eventName, function (event) { + var label = event.type + "@" + targetId; + + detected_pointertypes[event.pointerType] = true; + + if (!event.isPrimary) { + test(function () { + assert_unreached("Non-primary pointer " + label); + }, "Non-primary pointer " + label); + } + + if (event.type === "pointerenter") { + var pointer_id = event.pointerId; + if (current_node_for_id[pointer_id] !== undefined) { + test(function () { + assert_unreached("Double entry " + label); + }, "Double entry " + label); + } + current_node_for_id[pointer_id] = event.target; + + // Test that two different pointing devices are used + if (first_entry_pointer_id === -1) { + first_entry_pointer_id = pointer_id; + } else if (second_entry_pointer_id === -1) { + second_entry_pointer_id = pointer_id; + test(function () { + assert_true(first_entry_pointer_id !== second_entry_pointer_id); + }, "Different pointing devices"); + } + } else if (event.type === "pointerleave") { + var pointer_id = event.pointerId; + if (current_node_for_id[pointer_id] !== event.target) { + test(function () { + assert_unreached("Double exit " + label); + }, "Double exit " + label); + } + current_node_for_id[pointer_id] = undefined; + } + }); + }); + + mouse_event_list.forEach(function(eventName) { + on_event(target, eventName, function (event) { + log_event(event.type + "@" + targetId); + }); + }); + }); + } + </script> + <style> + #target0, #target1 { + margin: 20px; + } + + #done { + margin: 20px; + border: 2px solid black; + } + </style> + </head> + <body onload="run()"> + <h1>Pointer Event: Boundary compatibility events for multiple primary pointers</h1> + <h4> + When more than one primary pointers are active, each will have an independent sequence of pointer boundary events but the compatibilty mouse boundary events have their own sequence. + </h4> + Instruction: + <ol> + <li>Move the mouse directly into Target0 (without going through Target1), and then leave the mouse there unmoved.</li> + <li>Tap directly on Target1 with a finger or a stylus, and then lift the finger/stylus off the screen/digitizer without crossing Target1 boundary.</li> + <li>Move the mouse into Target0 (if not there already) and move inside it.</li> + <li>Click Done (without passing over Target1).</li> + </ol> + <div id="done"> + Done + </div> + <div id="target0"> + Target0 + </div> + <div id="target1"> + Target1 + </div> + <div id="complete-notice"> + <p>The following pointer types were detected: <span id="pointertype-log"></span>.</p> + <p>The following events were logged: <span id="event-log"></span>.</p> + </div> + <div id="log"></div> + </body> +</html> diff --git a/dom/events/test/pointerevents/wpt/pointerevent_pointerId_scope-manual.html b/dom/events/test/pointerevents/wpt/pointerevent_pointerId_scope-manual.html new file mode 100644 index 0000000000..3640cb6f6b --- /dev/null +++ b/dom/events/test/pointerevents/wpt/pointerevent_pointerId_scope-manual.html @@ -0,0 +1,82 @@ +<!doctype html> +<html> + <!-- +Test cases for Pointer Events v1 spec +This document references Test Assertions (abbrev TA below) written by Cathy Chan +http://www.w3.org/wiki/PointerEvents/TestAssertions +--> + <head> + <title>Pointer Events pointerdown tests</title> + <meta name="viewport" content="width=device-width"> + <link rel="stylesheet" type="text/css" href="pointerevent_styles.css"> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <!-- Additional helper script for common checks across event types --> + <script type="text/javascript" src="pointerevent_support.js"></script> + <script> + var detected_pointertypes = {}; + var test_pointerEvent = async_test("pointerId of an active pointer is the same across iframes"); + // showPointerTypes is defined in pointerevent_support.js + // Requirements: the callback function will reference the test_pointerEvent object and + // will fail unless the async_test is created with the var name "test_pointerEvent". + add_completion_callback(showPointerTypes); + var detected_pointertypes = {}; + + function run() { + var target0 = document.getElementById("target0"); + var pointerover_pointerId = null; + var pointerover_pointerType = null; + + var eventList = ['pointerenter', 'pointerover', 'pointermove', 'pointerout', 'pointerleave']; + var receivedEvents = {}; + var receivedEventsInnerFrame = {}; + + + function checkPointerId(event, inner) { + detected_pointertypes[event.pointerType] = true; + var eventName = (inner ? "inner frame " : "" ) + event.type; + test_pointerEvent.step(function() { + assert_equals(event.pointerId, pointerover_pointerId, "PointerId of " + eventName + " is not correct"); + assert_equals(event.pointerType, pointerover_pointerType, "PointerType of " + eventName + " is not correct"); + }, eventName + ".pointerId were the same as first pointerover"); + } + + on_event(window, "message", function(event) { + var pe_event = JSON.parse(event.data); + receivedEventsInnerFrame[pe_event.type] = 1; + checkPointerId(pe_event, true); + if (Object.keys(receivedEvents).length == eventList.length && Object.keys(receivedEventsInnerFrame).length == eventList.length) + test_pointerEvent.done(); + }); + + eventList.forEach(function(eventName) { + on_event(target0, eventName, function (event) { + if (pointerover_pointerId === null && event.type == 'pointerover') { + pointerover_pointerId = event.pointerId; + pointerover_pointerType = event.pointerType; + } else { + checkPointerId(event, false); + } + receivedEvents[event.type] = 1; + }); + }); + } + </script> + </head> + <body onload="run()"> + <h1>Pointer Events pointerdown tests</h1> + Complete the following actions: + <ol> + <li>Start with your pointing device outside of black box, then move it into black box. If using touch just press in black box and don't release. + <li>Move your pointing device into purple box (without leaving the digitizer range if you are using hover supported pen or without releasing touch if using touch). Then move it out of the purple box. + </ol> + <div id="target0" class="touchActionNone"> + </div> + <iframe src="resources/pointerevent_pointerId_scope-iframe.html" id="innerframe"></iframe> + <div id="complete-notice"> + <p>The following pointer types were detected: <span id="pointertype-log"></span>.</p> + <p>Refresh the page to run the tests again with a different pointer type.</p> + </div> + <div id="log"></div> + </body> +</html> diff --git a/dom/events/test/pointerevents/wpt/pointerevent_pointercancel_touch-manual.html b/dom/events/test/pointerevents/wpt/pointerevent_pointercancel_touch-manual.html new file mode 100644 index 0000000000..70a65eeb5c --- /dev/null +++ b/dom/events/test/pointerevents/wpt/pointerevent_pointercancel_touch-manual.html @@ -0,0 +1,77 @@ +<!doctype html> +<html> + <head> + <title>PointerCancel - touch</title> + <meta name="viewport" content="width=device-width"> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <link rel="stylesheet" type="text/css" href="pointerevent_styles.css"> + <!-- Additional helper script for common checks across event types --> + <script type="text/javascript" src="pointerevent_support.js"></script> + </head> + <body class="scrollable" onload="run()"> + <h1>pointercancel test</h1> + <h3>Warning: this test works properly only for devices that have touchscreen</h3> + <h4> + Test Description: This test checks if pointercancel event triggers. + <p>Start touch over the black rectangle and then move your finger to scroll the page.</p> + </h4> + <p> + <div id="target0" style="background: black"></div> + <script> + var detected_pointertypes = {}; + var test_pointerEvent = async_test("pointercancel event received"); + // showPointerTypes is defined in pointerevent_support.js + // Requirements: the callback function will reference the test_pointerEvent object and + // will fail unless the async_test is created with the var name "test_pointerEvent". + add_completion_callback(showPointerTypes); + + var pointerdown_event = null; + var pointercancel_event = null; + + function run() { + var target0 = document.getElementById("target0"); + + on_event(target0, "pointerdown", function (event) { + pointerdown_event = event; + detected_pointertypes[event.pointerType] = true; + }); + + on_event(target0, "pointercancel", function (event) { + pointercancel_event = event; + test_pointerEvent.step(function () { + assert_not_equals(pointerdown_event, null, "pointerdown was received: "); + assert_equals(event.pointerId, pointerdown_event.pointerId, "pointerId should be the same for pointerdown and pointercancel"); + assert_equals(event.pointerType, pointerdown_event.pointerType, "pointerType should be the same for pointerdown and pointercancel"); + assert_equals(event.isPrimary, pointerdown_event.isPrimary, "isPrimary should be the same for pointerdown and pointercancel"); + check_PointerEvent(event); + }); + }); + + on_event(target0, "pointerout", function (event) { + test_pointerEvent.step(function () { + assert_not_equals(pointercancel_event, null, "pointercancel was received before pointerout: "); + assert_equals(event.pointerId, pointerdown_event.pointerId, "pointerId should be the same for pointerout and pointercancel"); + assert_equals(event.pointerType, pointerdown_event.pointerType, "pointerType should be the same for pointerout and pointercancel"); + assert_equals(event.isPrimary, pointerdown_event.isPrimary, "isPrimary should be the same for pointerout and pointercancel"); + }); + }); + + on_event(target0, "pointerleave", function (event) { + test_pointerEvent.step(function () { + assert_not_equals(pointercancel_event, null, "pointercancel was received before pointerleave: "); + assert_equals(event.pointerId, pointerdown_event.pointerId, "pointerId should be the same for pointerleave and pointercancel"); + assert_equals(event.pointerType, pointerdown_event.pointerType, "pointerType should be the same for pointerleave and pointercancel"); + assert_equals(event.isPrimary, pointerdown_event.isPrimary, "isPrimary should be the same for pointerleave and pointercancel"); + }); + test_pointerEvent.done(); + }); + } + </script> + <h1>Pointer Events pointercancel Tests</h1> + <div id="complete-notice"> + <p>The following pointer types were detected: <span id="pointertype-log"></span>.</p> + </div> + <div id="log"></div> + </body> +</html> diff --git a/dom/events/test/pointerevents/wpt/pointerevent_pointerleave_after_pointercancel_touch-manual.html b/dom/events/test/pointerevents/wpt/pointerevent_pointerleave_after_pointercancel_touch-manual.html new file mode 100644 index 0000000000..56be26549f --- /dev/null +++ b/dom/events/test/pointerevents/wpt/pointerevent_pointerleave_after_pointercancel_touch-manual.html @@ -0,0 +1,66 @@ +<!doctype html> +<html> + <head> + <title>pointerleave after pointercancel</title> + <meta name="viewport" content="width=device-width"> + <link rel="stylesheet" type="text/css" href="pointerevent_styles.css"> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <!-- Additional helper script for common checks across event types --> + <script type="text/javascript" src="pointerevent_support.js"></script> + </head> + <body class="scrollable" onload="run()"> + <h2>pointerleave after pointercancel</h2> + <h4>Test Description: This test checks if pointerleave event triggers after pointercancel. Start touch on the black rectangle and move your touch to scroll in any direction. </h4> + <p>Note: this test is for touch devices only</p> + <div id="target0"></div> + <script> + var test_pointerleave = async_test("pointerleave event received"); + // showPointerTypes is defined in pointerevent_support.js + // Requirements: the callback function will reference the test_pointerEvent object and + // will fail unless the async_test is created with the var name "test_pointerEvent". + add_completion_callback(showPointerTypes); + + var eventTested = false; + var pointercancel_event = null; + var detected_pointertypes = {}; + + function run() { + var target0 = document.getElementById("target0"); + + on_event(target0, "pointercancel", function (event) { + detected_pointertypes[event.pointerType] = true; + pointercancel_event = event; + }); + + // After firing the pointercancel event the pointerleave event must be dispatched. + // TA: 4.3.1 + on_event(target0, "pointerleave", function (event) { + if(event.pointerType == 'touch') { + if(pointercancel_event != null) { + if(eventTested == false) { + test_pointerleave.step(function() { + assert_equals(event.pointerType, pointercancel_event.pointerType, "pointerType is same for pointercancel and pointerleave"); + assert_equals(event.isPrimary, pointercancel_event.isPrimary, "isPrimary is same for pointercancel and pointerleave"); + }); + eventTested = true; + test_pointerleave.done(); + } + } + else { + test_pointerleave.step(function() { + assert_unreached("pointerleave received before pointercancel"); + }, "pointerleave received before pointercancel"); + } + } + }); + } + + </script> + <h1>Pointer Events pointerleave tests</h1> + <div id="complete-notice"> + <p>The following pointer types were detected: <span id="pointertype-log"></span>.</p> + </div> + <div id="log"></div> + </body> +</html> diff --git a/dom/events/test/pointerevents/wpt/pointerevent_pointerleave_pen-manual.html b/dom/events/test/pointerevents/wpt/pointerevent_pointerleave_pen-manual.html new file mode 100644 index 0000000000..38a2f69792 --- /dev/null +++ b/dom/events/test/pointerevents/wpt/pointerevent_pointerleave_pen-manual.html @@ -0,0 +1,58 @@ +<!doctype html> +<html> + <head> + <title>Pointer Event: Dispatch pointerleave (pen). </title> + <meta content="text/html; charset=UTF-8" http-equiv="Content-Type"/> + <link rel="author" title="Microsoft" href="http://www.microsoft.com/"/> + <meta name="assert" content="When a pointing device that supports hover (pen stylus) leaves the range of the digitizer while over an element, the pointerleave event must be dispatched."/> + <link rel="stylesheet" type="text/css" href="pointerevent_styles.css"> + <!-- /resources/testharness.js --> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <!-- Additional helper script for common checks across event types --> + <script type="text/javascript" src="pointerevent_support.js"></script> + <script type="text/javascript"> + var detected_pointertypes = {}; + var test_pointerEvent = async_test("pointerleave event"); // set up test harness + // showPointerTypes is defined in pointerevent_support.js + // Requirements: the callback function will reference the test_pointerEvent object and + // will fail unless the async_test is created with the var name "test_pointerEvent". + add_completion_callback(showPointerTypes); + + function run() { + var target0 = document.getElementById("target0"); + + on_event(target0, "pointerleave", function (event) { + detected_pointertypes[event.pointerType] = true; + check_PointerEvent(event); + test_pointerEvent.step(function () { + assert_equals(event.pointerType, "pen", "Test should be run using a pen as input"); + assert_equals(event.type, "pointerleave", "The " + event.type + " event was received"); + assert_true((event.clientX > target0.getBoundingClientRect().left)&& + (event.clientX < target0.getBoundingClientRect().right)&& + (event.clientY > target0.getBoundingClientRect().top)&& + (event.clientY < target0.getBoundingClientRect().bottom), + "pointerleave should be received inside of target bounds"); + }); + test_pointerEvent.done(); // complete test + }); + } + </script> + </head> + <body onload="run()"> + <h1>Pointer Event: Dispatch pointerleave (pen)</h1> + <h4> + Test Description: + When a pointing device that supports hover (pen stylus) leaves the range of the digitizer while over an element, the pointerleave event must be dispatched. + </h4> + <br /> + <div id="target0"> + Use a pen to hover over then lift up away from this element. + </div> + <div id="complete-notice"> + <p>Test complete: Scroll to Summary to view Pass/Fail Results.</p> + <p>The following pointer types were detected: <span id="pointertype-log"></span>.</p> + </div> + <div id="log"></div> + </body> +</html>
\ No newline at end of file diff --git a/dom/events/test/pointerevents/wpt/pointerevent_pointerout_after_pointercancel_touch-manual.html b/dom/events/test/pointerevents/wpt/pointerevent_pointerout_after_pointercancel_touch-manual.html new file mode 100644 index 0000000000..1888591a7c --- /dev/null +++ b/dom/events/test/pointerevents/wpt/pointerevent_pointerout_after_pointercancel_touch-manual.html @@ -0,0 +1,67 @@ +<!doctype html> +<html> + <head> + <title>pointerout</title> + <meta name="viewport" content="width=device-width"> + <link rel="stylesheet" type="text/css" href="pointerevent_styles.css"> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <!-- Additional helper script for common checks across event types --> + <script type="text/javascript" src="pointerevent_support.js"></script> + </head> + <body class="scrollable" onload="run()"> + <h2>pointerout</h2> + <h4>Test Description: This test checks if pointerout event triggers after pointercancel. Start touch on the black rectangle and move your touch to scroll in any direction. </h4> + <p>Note: this test is for touch devices only</p> + <div id="target0"></div> + <script> + var test_pointerout = async_test("pointerout event received"); + // showPointerTypes is defined in pointerevent_support.js + // Requirements: the callback function will reference the test_pointerEvent object and + // will fail unless the async_test is created with the var name "test_pointerEvent". + add_completion_callback(showPointerTypes); + + var eventTested = false; + var pointercancel_event = null; + var detected_pointertypes = {}; + + function run() { + var target0 = document.getElementById("target0"); + + on_event(target0, "pointercancel", function (event) { + detected_pointertypes[event.pointerType] = true; + pointercancel_event = event; + }); + + // After firing the pointercancel event the pointerout event must be dispatched. + // TA: 4.3 + on_event(target0, "pointerout", function (event) { + if(event.pointerType == 'touch') { + if(pointercancel_event != null) { + if (eventTested == false) { + test_pointerout.step(function() { + assert_equals(event.pointerType, pointercancel_event.pointerType, "pointerType is same for pointercancel and pointerout"); + assert_equals(event.isPrimary, pointercancel_event.isPrimary, "isPrimary is same for pointercancel and pointerout"); + }); + eventTested = true; + test_pointerout.done(); + } + } + else { + test_pointerout.step(function() { + assert_true(false, + "pointercancel received before pointerout"); + }, "pointercancel received before pointerout"); + } + } + }); + } + + </script> + <h1>Pointer Events pointerout tests</h1> + <div id="complete-notice"> + <p>The following pointer types were detected: <span id="pointertype-log"></span>.</p> + </div> + <div id="log"></div> + </body> +</html> diff --git a/dom/events/test/pointerevents/wpt/pointerevent_pointerout_pen-manual.html b/dom/events/test/pointerevents/wpt/pointerevent_pointerout_pen-manual.html new file mode 100644 index 0000000000..3973948c16 --- /dev/null +++ b/dom/events/test/pointerevents/wpt/pointerevent_pointerout_pen-manual.html @@ -0,0 +1,57 @@ +<!doctype html> +<html> + <head> + <title>pointerout</title> + <meta name="viewport" content="width=device-width"> + <link rel="stylesheet" type="text/css" href="pointerevent_styles.css"> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <!-- Additional helper script for common checks across event types --> + <script type="text/javascript" src="pointerevent_support.js"></script> + </head> + <body onload="run()"> + <h2>pointerout</h2> + <h4>Test Description: This test checks if pointerout event triggers for pen. Place your pen over the black rectangle and then pull the pen out of the digitizer's detectable range. </h4> + <p>Note: this test is for devices that support hover - for pen only</p> + <div id="target0"></div> + <script> + var test_pointerout = async_test("pointerout event received"); + // showPointerTypes is defined in pointerevent_support.js + // Requirements: the callback function will reference the test_pointerEvent object and + // will fail unless the async_test is created with the var name "test_pointerEvent". + add_completion_callback(showPointerTypes); + + var eventTested = false; + var isPointerupReceived = false; + var detected_pointertypes = {}; + + function run() { + var target0 = document.getElementById("target0"); + + // When a pen stylus leaves the hover range detectable by the digitizer the pointerout event must be dispatched. + // TA: 7.2 + on_event(target0, "pointerout", function (event) { + detected_pointertypes[event.pointerType] = true; + if(event.pointerType == 'pen') { + if (eventTested == false) { + eventTested = true; + test_pointerout.done(); + } + } + else { + test_pointerout.step(function() { + assert_true(false, + "you have to use pen for this test"); + }, "you have to use pen for this test"); + } + }); + } + + </script> + <h1>Pointer Events pointerout tests</h1> + <div id="complete-notice"> + <p>The following pointer types were detected: <span id="pointertype-log"></span>.</p> + </div> + <div id="log"></div> + </body> +</html>
\ No newline at end of file diff --git a/dom/events/test/pointerevents/wpt/pointerevent_releasepointercapture_events_to_original_target-manual.html b/dom/events/test/pointerevents/wpt/pointerevent_releasepointercapture_events_to_original_target-manual.html new file mode 100644 index 0000000000..3386fafb5a --- /dev/null +++ b/dom/events/test/pointerevents/wpt/pointerevent_releasepointercapture_events_to_original_target-manual.html @@ -0,0 +1,137 @@ +<!doctype html> +<html> + <head> + <title>Pointer Event: releasePointerCapture() - subsequent events follow normal hitting testing mechanisms</title> + <meta content="text/html; charset=UTF-8" http-equiv="Content-Type"/> + <link rel="author" title="Microsoft" href="http://www.microsoft.com/"/> + <meta name="assert" content="After invoking the releasePointerCapture method on an element, subsequent events for the specified pointer must follow normal hit testing mechanisms for determining the event target"/> + <link rel="stylesheet" type="text/css" href="pointerevent_styles.css"> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <!-- Additional helper script for common checks across event types --> + <script type="text/javascript" src="pointerevent_support.js"></script> + <script type="text/javascript"> + var test_pointerEvent; + var detected_pointertypes = {}; + var captured_event = null; + var test_done = false; + var overEnterEventsFail = false; + var outLeaveEventsFail = false; + var f_gotPointerCapture = false; + var f_lostPointerCapture = false; + + function resetTestState() { + captured_event = null; + test_done = false; + overEnterEventsFail = false; + outLeaveEventsFail = false; + f_gotPointerCapture = false; + f_lostPointerCapture = false; + } + + function listenerEventHandler(event) { + if (test_done) + return; + detected_pointertypes[event.pointerType] = true; + if (event.type == "gotpointercapture") { + f_gotPointerCapture = true; + check_PointerEvent(event); + } + else if (event.type == "lostpointercapture") { + f_lostPointerCapture = true; + f_gotPointerCapture = false; + check_PointerEvent(event); + } + else if(event.type == "pointerover" || event.type == "pointerenter") { + if(captured_event && !overEnterEventsFail) { + test(function() { + assert_false(f_gotPointerCapture, "pointerover/enter should be received before the target receives gotpointercapture even when the pointer is not over it."); + }, expectedPointerType + " pointerover/enter should be received before the target receives gotpointercapture even when the pointer is not over it."); + overEnterEventsFail = true; + } + } + else if(event.type == "pointerout" || event.type == "pointerleave") { + if(!outLeaveEventsFail) { + test(function() { + assert_true(f_lostPointerCapture, "pointerout/leave should not be received unless the target just lost the capture."); + }, expectedPointerType + " pointerout/leave should not be received unless the target just lost the capture."); + outLeaveEventsFail = true; + } + } + else if (event.pointerId == captured_event.pointerId) { + if (f_gotPointerCapture && event.type == "pointermove") { + // on first event received for capture, release capture + listener.releasePointerCapture(event.pointerId); + } + else { + // if any other events are received after releaseCapture, then the test fails + test(function () { + assert_unreached(event.target.id + "-" + event.type + " should be handled by target element handler"); + }, expectedPointerType + " No other events should be recieved by capturing node after release"); + } + } + } + + function targetEventHandler(event) { + if (test_done) + return; + if (f_gotPointerCapture) { + if(event.type != "pointerout" && event.type != "pointerleave") { + test(function () { + assert_unreached("The Target element should not have received any events while capture is active. Event recieved:" + event.type + ". "); + }, expectedPointerType + " The target element should not receive any events while capture is active"); + } + } + + if (event.type == "pointerdown") { + // pointerdown event received will be used to capture events. + listener.setPointerCapture(event.pointerId); + captured_event = event; + } + + if (f_lostPointerCapture) { + test_pointerEvent.step(function () { + assert_equals(event.pointerId, captured_event.pointerId, "pointerID is same for event captured and after release"); + }); + if (event.type == "pointerup") { + test_done = true; + test_pointerEvent.done(); // complete test + } + } + } + + function run() { + test_pointerEvent = setup_pointerevent_test("got/lost pointercapture: subsequent events to target", ALL_POINTERS); // set up test harness + var listener = document.getElementById("listener"); + var target0 = document.getElementById("target0"); + target0.style.touchAction = "none"; + + // target0 and listener - handle all events + for (var i = 0; i < All_Pointer_Events.length; i++) { + on_event(target0, All_Pointer_Events[i], targetEventHandler); + on_event(listener, All_Pointer_Events[i], listenerEventHandler); + } + } + </script> + </head> + <body onload="run()"> + <h2 id="pointerTypeDescription"></h2> + <div id="listener"></div> + <h1>Pointer Event: releasePointerCapture() - subsequent events follow normal hitting testing mechanisms</h1> + <h4> + Test Description: + Use your pointer and press down in the black box. Then move around in the box and release your pointer. + After invoking the releasePointerCapture method on an element, subsequent events for the specified + pointer must follow normal hit testing mechanisms for determining the event target. + </h4> + <br /> + <div id="target0"> + </div> + <div id="complete-notice"> + <p>Test complete: Scroll to Summary to view Pass/Fail Results.</p> + <p>The following pointer types were detected: <span id="pointertype-log"></span>.</p> + <p>Refresh the page to run the tests again with a different pointer type.</p> + </div> + <div id="log"></div> + </body> +</html> diff --git a/dom/events/test/pointerevents/wpt/pointerevent_releasepointercapture_onpointercancel_touch-manual.html b/dom/events/test/pointerevents/wpt/pointerevent_releasepointercapture_onpointercancel_touch-manual.html new file mode 100644 index 0000000000..105e3b5a97 --- /dev/null +++ b/dom/events/test/pointerevents/wpt/pointerevent_releasepointercapture_onpointercancel_touch-manual.html @@ -0,0 +1,71 @@ +<!doctype html> +<html> + <head> + <title>Release capture on pointercancel</title> + <meta name="viewport" content="width=device-width"> + <link rel="stylesheet" type="text/css" href="pointerevent_styles.css"> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="pointerevent_support.js"></script> + </head> + <body class="scrollable"> + <h1>Pointer Events Capture Test - release capture on pointercancel</h1> + <h4> + Test Description: This test checks if setCapture/releaseCapture functions works properly. Complete the following actions: + <ol> + <li> Touch black rectangle and do not release your touch + <li> Move your touch to scroll the page. "lostpointercapture" should be logged inside of the black rectangle immediately after "pointercancel" + </ol> + </h4> + Test passes if the proper behavior of the events is observed. + <div id="target0" style="background:black; color:white"></div> + + <script type='text/javascript'> + var pointercancelGot = false; + var count=0; + var detected_pointertypes = {}; + var test_pointerEvent = async_test("pointer capture is released on pointercancel"); + + var target0 = document.getElementById('target0'); + + add_completion_callback(showPointerTypes); + + window.onload = function() { + on_event(target0, 'pointerdown', function(e) { + detected_pointertypes[e.pointerType] = true; + test_pointerEvent.step(function () { + assert_equals(e.pointerType, "touch", "Test should be run using a touch as input"); + }); + isPointerCapture = true; + sPointerCapture(e); + pointercancelGot = false; + }); + + on_event(target0, 'gotpointercapture', function(e) { + log("gotpointercapture", document.getElementById('target0')); + }); + + // If the setPointerCapture method has been invoked on the pointer specified by pointerId, and the releasePointerCapture method has not been invoked, a lostpointercapture event must be dispatched to the element on which the setPointerCapture method was invoked. Furthermore, subsequent events for the specified pointer must follow normal hit testing mechanisms for determining the event target. + // TA: 4.4 + on_event(target0, 'lostpointercapture', function(e) { + log("lostpointercapture", document.getElementById('target0')); + test_pointerEvent.step(function () { + assert_true(pointercancelGot, "pointercancel was received before lostpointercapture"); + }); + test_pointerEvent.done(); + }); + + on_event(target0, 'pointercancel', function(e) { + log("pointercancel", target0); + pointercancelGot = true; + }); + } + </script> + <h1>Pointer Events Capture Test</h1> + <div id="complete-notice"> + <p>Test complete: Scroll to Summary to view Pass/Fail Results.</p> + <p>The following pointer types were detected: <span id="pointertype-log"></span>.</p> + </div> + <div id="log"></div> + </body> +</html> diff --git a/dom/events/test/pointerevents/wpt/pointerevent_sequence_at_implicit_release_on_click-manual.html b/dom/events/test/pointerevents/wpt/pointerevent_sequence_at_implicit_release_on_click-manual.html new file mode 100644 index 0000000000..274f9a435b --- /dev/null +++ b/dom/events/test/pointerevents/wpt/pointerevent_sequence_at_implicit_release_on_click-manual.html @@ -0,0 +1,83 @@ +<!doctype html> +<html> + <head> + <title>Pointer Event: Event sequence at implicit release on click</title> + <meta content="text/html; charset=UTF-8" http-equiv="Content-Type"/> + <link rel="author" title="Google" href="http://www.google.com "/> + <meta name="assert" content="When a captured pointer is implicitly released after a click, the boundary events should follow the lostpointercapture event."/> + <link rel="stylesheet" type="text/css" href="pointerevent_styles.css"> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script type="text/javascript" src="pointerevent_support.js"></script> + <script type="text/javascript"> + var detected_pointertypes = {}; + var event_log = []; + var start_logging = false; + + function resetTestState() { + detected_eventTypes = {}; + event_log = []; + start_logging = false; + } + + function run() { + var test_pointer_event = setup_pointerevent_test("Event sequence at implicit release on click", ALL_POINTERS); + + on_event(document.getElementById("done"), "click", function() { + test_pointer_event.step(function () { + var expected_events = "pointerup, lostpointercapture, pointerout, pointerleave"; + assert_equals(event_log.join(", "), expected_events); + }); + test_pointer_event.done(); + }); + + var target = document.getElementById("target"); + + All_Pointer_Events.forEach(function(eventName) { + on_event(target, eventName, function (event) { + detected_pointertypes[event.pointerType] = true; + + if (event.type == "pointerdown") { + event.target.setPointerCapture(event.pointerId); + + } else if (event.type == "gotpointercapture") { + start_logging = true; + + } else if (event.type != "pointermove" && start_logging) { + event_log.push(event.type); + } + }); + }); + } + </script> + <style> + #target { + margin: 20px; + background-color: black; + } + + #done { + margin: 20px; + background-color: green; + } + </style> + </head> + <body onload="run()"> + <h1>Pointer Event: Event sequence at implicit release on click<h1> + <h2 id="pointerTypeDescription"></h2> + <h4> + When a captured pointer is implicitly released after a click, the boundary events should follow the lostpointercapture event. + </h4> + <ol> + <li>Click or tap on Black.</li> + <li>Click or tap on Green.</li> + </ol> + <div id="target"></div> + <div id="done"></div> + <div id="complete-notice"> + <p>The following pointer types were detected: <span id="pointertype-log"></span>.</p> + <p>The following events were logged: <span id="event-log"></span>.</p> + </div> + <div id="log"></div> + </body> +</html> diff --git a/dom/events/test/pointerevents/wpt/pointerevent_sequence_at_implicit_release_on_drag-manual.html b/dom/events/test/pointerevents/wpt/pointerevent_sequence_at_implicit_release_on_drag-manual.html new file mode 100644 index 0000000000..7b8e39b94d --- /dev/null +++ b/dom/events/test/pointerevents/wpt/pointerevent_sequence_at_implicit_release_on_drag-manual.html @@ -0,0 +1,84 @@ +<!doctype html> +<html> + <head> + <title>Pointer Event: Event sequence at implicit release on drag</title> + <meta content="text/html; charset=UTF-8" http-equiv="Content-Type"/> + <link rel="author" title="Google" href="http://www.google.com "/> + <meta name="assert" content="When a captured pointer is implicitly released after a drag, the boundary events should follow the lostpointercapture event."/> + <link rel="stylesheet" type="text/css" href="pointerevent_styles.css"> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script type="text/javascript" src="pointerevent_support.js"></script> + <script type="text/javascript"> + var detected_pointertypes = {}; + var event_log = []; + var start_logging = false; + + function resetTestState() { + detected_eventTypes = {}; + event_log = []; + start_logging = false; + } + + function run() { + var test_pointer_event = setup_pointerevent_test("Event sequence at implicit release on drag", ["touch"]); + + on_event(document.getElementById("done"), "click", function() { + test_pointer_event.step(function () { + var expected_events = "pointercancel, lostpointercapture, pointerout, pointerleave"; + assert_equals(event_log.join(", "), expected_events); + }); + test_pointer_event.done(); + }); + + var target = document.getElementById("target"); + + All_Pointer_Events.forEach(function(eventName) { + on_event(target, eventName, function (event) { + detected_pointertypes[event.pointerType] = true; + + if (event.type == "pointerdown") { + event.target.setPointerCapture(event.pointerId); + + } else if (event.type == "gotpointercapture") { + start_logging = true; + + } else if (event.type != "pointermove" && start_logging) { + event_log.push(event.type); + } + }); + }); + } + </script> + <style> + #target { + margin: 20px; + background-color: black; + touch-action: auto; + } + + #done { + margin: 20px; + background-color: green; + } + </style> + </head> + <body onload="run()"> + <h1>Pointer Event: Event sequence at implicit release on drag<h1> + <h2 id="pointerTypeDescription"></h2> + <h4> + When a captured pointer is implicitly released after a drag, the boundary events should follow the lostpointercapture event. + </h4> + <ol> + <li>Drag quickly down starting on Black.</li> + <li>Click or tap on Green.</li> + </ol> + <div id="target"></div> + <div id="done"></div> + <div id="complete-notice"> + <p>The following pointer types were detected: <span id="pointertype-log"></span>.</p> + <p>The following events were logged: <span id="event-log"></span>.</p> + </div> + <div id="log"></div> + </body> +</html> diff --git a/dom/events/test/pointerevents/wpt/pointerevent_styles.css b/dom/events/test/pointerevents/wpt/pointerevent_styles.css new file mode 100644 index 0000000000..1ee3b0b396 --- /dev/null +++ b/dom/events/test/pointerevents/wpt/pointerevent_styles.css @@ -0,0 +1,112 @@ +#innerFrame { +position: absolute; +top: 300px; +left: 200px; +height: 100px; +width: 100px; +} + +.spacer { +height: 100px; +} + +#square1 { +top: 330px; +left: 150px; +background: black; +} + +#square2 { +top: 50px; +left: 30px; +visibility: hidden; +background: red; +} + +.square { +height: 20px; +width: 20px; +position: absolute; +padding: 0px; +} + +#target0 { +background: black; +color: white; +white-space: nowrap; +overflow-y: auto; +overflow-x: auto; +} + +#target1 { +background: purple; +color: white; +white-space: nowrap; +overflow-y: auto; +overflow-x: auto; +} + +#scrollTarget { + background: darkblue; +} + +.touchActionNone { +touch-action: none; +} + +#innerframe { +width: 90%; +margin: 10px; +margin-left: 10%; +height: 200px; +} + +.scroller { +width: 700px; +height: 430px; +margin: 20px; +overflow: auto; +background: black; +} + +.scroller > div { +height: 1000px; +width: 1000px; +color: white; +} + +.scroller > div div { +height: 100%; +width: 100%; +color: white; +} + +div { +margin: 0em; +padding: 2em; +} + +#complete-notice { +background: #afa; +border: 1px solid #0a0; +display: none; +} + +#pointertype-log { +font-weight: bold; +} + +#event-log { +font-weight: bold; +} + +#listener { +background: orange; +border: 1px solid orange; +position: absolute; +top: -100px; +} + +body.scrollable { +min-height: 5000px; +} diff --git a/dom/events/test/pointerevents/wpt/pointerevent_support.js b/dom/events/test/pointerevents/wpt/pointerevent_support.js new file mode 100644 index 0000000000..cd3c799f73 --- /dev/null +++ b/dom/events/test/pointerevents/wpt/pointerevent_support.js @@ -0,0 +1,333 @@ +var All_Pointer_Events = [ + "pointerdown", + "pointerup", + "pointercancel", + "pointermove", + "pointerover", + "pointerout", + "pointerenter", + "pointerleave", + "gotpointercapture", + "lostpointercapture", +]; + +// Check for conformance to PointerEvent interface +// TA: 1.1, 1.2, 1.6, 1.7, 1.8, 1.9, 1.10, 1.11, 1.12, 1.13 +function check_PointerEvent(event, testNamePrefix) { + if (testNamePrefix === undefined) { + testNamePrefix = ""; + } + + // Use expectedPointerType if set otherwise just use the incoming event pointerType in the test name. + var pointerTestName = + testNamePrefix + + " " + + (expectedPointerType == null ? event.pointerType : expectedPointerType) + + " " + + event.type; + + if (expectedPointerType != null) { + test(function () { + assert_equals( + event.pointerType, + expectedPointerType, + "pointerType should be the one specified in the test page." + ); + }, pointerTestName + " event pointerType is correct."); + } + + test(function () { + assert_true( + event instanceof event.target.ownerDocument.defaultView.PointerEvent, + "event is a PointerEvent event" + ); + }, pointerTestName + " event is a PointerEvent event"); + + // Check attributes for conformance to WebIDL: + // * attribute exists + // * has proper type + // * if the attribute is "readonly", it cannot be changed + // TA: 1.1, 1.2 + var idl_type_check = { + long(v) { + return typeof v === "number" && Math.round(v) === v; + }, + float(v) { + return typeof v === "number"; + }, + string(v) { + return typeof v === "string"; + }, + boolean(v) { + return typeof v === "boolean"; + }, + }; + [ + ["readonly", "long", "pointerId"], + ["readonly", "float", "width"], + ["readonly", "float", "height"], + ["readonly", "float", "pressure"], + ["readonly", "long", "tiltX"], + ["readonly", "long", "tiltY"], + ["readonly", "string", "pointerType"], + ["readonly", "boolean", "isPrimary"], + ["readonly", "long", "detail", 0], + ].forEach(function (attr) { + var readonly = attr[0]; + var type = attr[1]; + var name = attr[2]; + var value = attr[3]; + + // existence check + test(function () { + assert_true( + name in event, + name + " attribute in " + event.type + " event" + ); + }, pointerTestName + "." + name + " attribute exists"); + + // readonly check + if (readonly === "readonly") { + test(function () { + assert_readonly( + event.type, + name, + event.type + "." + name + " cannot be changed" + ); + }, pointerTestName + "." + name + " is readonly"); + } + + // type check + test(function () { + assert_true( + idl_type_check[type](event[name]), + name + " attribute of type " + type + ); + }, pointerTestName + + "." + + name + + " IDL type " + + type + + " (JS type was " + + typeof event[name] + + ")"); + + // value check if defined + if (value != undefined) { + test(function () { + assert_equals(event[name], value, name + " attribute value"); + }, pointerTestName + "." + name + " value is " + value + "."); + } + }); + + // Check the pressure value + // TA: 1.6, 1.7, 1.8 + test(function () { + // TA: 1.6 + assert_greater_than_equal( + event.pressure, + 0, + "pressure is greater than or equal to 0" + ); + assert_less_than_equal( + event.pressure, + 1, + "pressure is less than or equal to 1" + ); + + if (event.type === "pointerup") { + assert_equals(event.pressure, 0, "pressure is 0 during pointerup"); + } + + // TA: 1.7, 1.8 + if (event.pointerType === "mouse") { + if (event.buttons === 0) { + assert_equals( + event.pressure, + 0, + "pressure is 0 for mouse with no buttons pressed" + ); + } else { + assert_equals( + event.pressure, + 0.5, + "pressure is 0.5 for mouse with a button pressed" + ); + } + } + }, pointerTestName + ".pressure value is valid"); + + // Check mouse-specific properties + if (event.pointerType === "mouse") { + // TA: 1.9, 1.10, 1.13 + test(function () { + assert_equals(event.width, 1, "width of mouse should be 1"); + assert_equals(event.height, 1, "height of mouse should be 1"); + assert_equals(event.tiltX, 0, event.type + ".tiltX is 0 for mouse"); + assert_equals(event.tiltY, 0, event.type + ".tiltY is 0 for mouse"); + assert_true(event.isPrimary, event.type + ".isPrimary is true for mouse"); + }, pointerTestName + " properties for pointerType = mouse"); + // Check properties for pointers other than mouse + } +} + +function showPointerTypes() { + var complete_notice = document.getElementById("complete-notice"); + var pointertype_log = document.getElementById("pointertype-log"); + var pointertypes = Object.keys(detected_pointertypes); + pointertype_log.innerHTML = pointertypes.length + ? pointertypes.join(",") + : "(none)"; + complete_notice.style.display = "block"; +} + +function showLoggedEvents() { + var event_log_elem = document.getElementById("event-log"); + event_log_elem.innerHTML = event_log.length ? event_log.join(", ") : "(none)"; + + var complete_notice = document.getElementById("complete-notice"); + complete_notice.style.display = "block"; +} + +function log(msg, el) { + if (++count > 10) { + count = 0; + el.innerHTML = " "; + } + el.innerHTML = msg + "; " + el.innerHTML; +} + +function failOnScroll() { + assert_true(false, "scroll received while shouldn't"); +} + +function updateDescriptionNextStep() { + document.getElementById("desc").innerHTML = + "Test Description: Try to scroll text RIGHT."; +} + +function updateDescriptionComplete() { + document.getElementById("desc").innerHTML = "Test Description: Test complete"; +} + +function updateDescriptionSecondStepTouchActionElement( + target, + scrollReturnInterval +) { + window.setTimeout(function () { + objectScroller(target, "up", 0); + }, scrollReturnInterval); + document.getElementById("desc").innerHTML = + "Test Description: Try to scroll element RIGHT moving your outside of the red border"; +} + +function updateDescriptionThirdStepTouchActionElement( + target, + scrollReturnInterval, + callback = null +) { + window.setTimeout(function () { + objectScroller(target, "left", 0); + if (callback) { + callback(); + } + }, scrollReturnInterval); + document.getElementById("desc").innerHTML = + "Test Description: Try to scroll element DOWN then RIGHT starting your touch inside of the element. Then tap complete button"; +} + +function updateDescriptionFourthStepTouchActionElement( + target, + scrollReturnInterval +) { + document.getElementById("desc").innerHTML = + "Test Description: Try to scroll element RIGHT starting your touch inside of the element"; +} + +function objectScroller(target, direction, value) { + if (direction == "up") { + target.scrollTop = 0; + } else if (direction == "left") { + target.scrollLeft = 0; + } +} + +function sPointerCapture(e) { + try { + target0.setPointerCapture(e.pointerId); + } catch (ex) {} +} + +function rPointerCapture(e) { + try { + captureButton.value = "Set Capture"; + target0.releasePointerCapture(e.pointerId); + } catch (ex) {} +} + +var globalPointerEventTest = null; +var expectedPointerType = null; +const ALL_POINTERS = ["mouse", "touch", "pen"]; +const HOVERABLE_POINTERS = ["mouse", "pen"]; +const NOHOVER_POINTERS = ["touch"]; + +function MultiPointerTypeTest(testName, types) { + this.testName = testName; + this.types = types; + this.currentTypeIndex = 0; + this.currentTest = null; + this.createNextTest(); +} + +MultiPointerTypeTest.prototype.skip = function () { + var prevTest = this.currentTest; + this.createNextTest(); + prevTest.timeout(); +}; + +MultiPointerTypeTest.prototype.done = function () { + var prevTest = this.currentTest; + this.createNextTest(); + if (prevTest != null) { + prevTest.done(); + } +}; + +MultiPointerTypeTest.prototype.step = function (stepFunction) { + this.currentTest.step(stepFunction); +}; + +MultiPointerTypeTest.prototype.createNextTest = function () { + if (this.currentTypeIndex < this.types.length) { + var pointerTypeDescription = document.getElementById( + "pointerTypeDescription" + ); + document.getElementById("pointerTypeDescription").innerHTML = + "Follow the test instructions with <span style='color: red'>" + + this.types[this.currentTypeIndex] + + "</span>. If you don't have the device <a href='javascript:;' onclick='globalPointerEventTest.skip()'>skip it</a>."; + this.currentTest = async_test( + this.types[this.currentTypeIndex] + " " + this.testName + ); + expectedPointerType = this.types[this.currentTypeIndex]; + this.currentTypeIndex++; + } else { + document.getElementById("pointerTypeDescription").innerHTML = ""; + } + resetTestState(); +}; + +function setup_pointerevent_test(testName, supportedPointerTypes) { + return (globalPointerEventTest = new MultiPointerTypeTest( + testName, + supportedPointerTypes + )); +} + +function checkPointerEventType(event) { + assert_equals( + event.pointerType, + expectedPointerType, + "pointerType should be the same as the requested device." + ); +} diff --git a/dom/events/test/pointerevents/wpt/pointerevent_touch-action-auto-css_touch-manual.html b/dom/events/test/pointerevents/wpt/pointerevent_touch-action-auto-css_touch-manual.html new file mode 100644 index 0000000000..f5e9d12c35 --- /dev/null +++ b/dom/events/test/pointerevents/wpt/pointerevent_touch-action-auto-css_touch-manual.html @@ -0,0 +1,129 @@ +<!doctype html> +<html> + <head> + <title>touch-action: auto</title> + <meta name="viewport" content="width=device-width"> + <link rel="stylesheet" type="text/css" href="pointerevent_styles.css"> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="pointerevent_support.js"></script> + <style> + #target0 { + width: 700px; + height: 430px; + touch-action: auto; + } + </style> + </head> + <body onload="run()"> + <h1>Pointer Events touch-action attribute support</h1> + <h4 id="desc">Test Description: Try to scroll text DOWN. Wait for description update. Expected: pan enabled</h4> + <p>Note: this test is for touch-devices only</p> + <div id="target0"> + <p> + Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diem + nonummy nibh euismod tincidunt ut lacreet dolore magna aliguam erat volutpat. + Ut wisis enim ad minim veniam, quis nostrud exerci tution ullamcorper suscipit + lobortis nisl ut aliquip ex ea commodo consequat. + </p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p> + Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diem + nonummy nibh euismod tincidunt ut lacreet dolore magna aliguam erat volutpat. + Ut wisis enim ad minim veniam, quis nostrud exerci tution ullamcorper suscipit + lobortis nisl ut aliquip ex ea commodo consequat. + </p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p> + Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diem + nonummy nibh euismod tincidunt ut lacreet dolore magna aliguam erat volutpat. + Ut wisis enim ad minim veniam, quis nostrud exerci tution ullamcorper suscipit + lobortis nisl ut aliquip ex ea commodo consequat. + </p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p> + Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diem + nonummy nibh euismod tincidunt ut lacreet dolore magna aliguam erat volutpat. + Ut wisis enim ad minim veniam, quis nostrud exerci tution ullamcorper suscipit + lobortis nisl ut aliquip ex ea commodo consequat. + </p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + </div> + <script type='text/javascript'> + var detected_pointertypes = {}; + + var xScrollIsReceived = false; + var yScrollIsReceived = false; + var xScr0, yScr0, xScr1, yScr1; + + add_completion_callback(showPointerTypes); + + function run() { + var target0 = document.getElementById("target0"); + + var test_touchaction = async_test("touch-action attribute test"); + + xScr0 = target0.scrollLeft; + yScr0 = target0.scrollTop; + + on_event(target0, 'pointerdown', function(event) { + detected_pointertypes[event.pointerType] = true; + test_touchaction.step(function() { + assert_equals(event.pointerType, "touch", "wrong pointer type was detected: "); + }); + }); + + on_event(target0, 'scroll', function(event) { + xScr1 = target0.scrollLeft; + yScr1 = target0.scrollTop; + + if(xScr1 != xScr0) { + xScrollIsReceived = true; + } + + if(yScr1 != yScr0) { + test_touchaction.step(function () { + yScrollIsReceived = true; + assert_true(true, "y-scroll received."); + }); + updateDescriptionNextStep(); + } + + if(xScrollIsReceived && yScrollIsReceived) { + test_touchaction.done(); + updateDescriptionComplete(); + } + }); + } + </script> + <h1>touch-action: auto</h1> + <div id="complete-notice"> + <p>The following pointer types were detected: <span id="pointertype-log"></span>.</p> + </div> + <div id="log"></div> + </body> +</html>
\ No newline at end of file diff --git a/dom/events/test/pointerevents/wpt/pointerevent_touch-action-button-test_touch-manual.html b/dom/events/test/pointerevents/wpt/pointerevent_touch-action-button-test_touch-manual.html new file mode 100644 index 0000000000..c7c5d9a440 --- /dev/null +++ b/dom/events/test/pointerevents/wpt/pointerevent_touch-action-button-test_touch-manual.html @@ -0,0 +1,110 @@ +<!doctype html> +<html> + <head> + <title>Button touch-action test</title> + <meta name="assert" content="TA15.11 -The touch-action CSS property applies to button elements."> + <meta name="viewport" content="width=device-width"> + <link rel="stylesheet" type="text/css" href="pointerevent_styles.css"> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="pointerevent_support.js"></script> + <style> + #target0 { + height: 150px; + width: 200px; + overflow-y: auto; + background: black; + padding: 100px; + position: relative; + } + button { + touch-action: none; + width: 350px; + height: 350px; + border: 2px solid red; + } + </style> + </head> + <body onload="run()"> + <h2>Pointer Events touch-action attribute support</h2> + <h4 id="desc">Test Description: Try to scroll black element DOWN moving your touch outside of the red border. Wait for description update.</h4> + <p>Note: this test is for touch only</p> + <div id="target0"> + <button id="testButton">Test Button</button> + </div> + <br> + <input type="button" id="btnComplete" value="Complete test"> + + <script type='text/javascript'> + var detected_pointertypes = {}; + var xScrollIsReceived = false; + var yScrollIsReceived = false; + var xScr0, yScr0, xScr1, yScr1; + var scrollReturnInterval = 1000; + var isFirstPart = true; + setup({ explicit_timeout: true }); + add_completion_callback(showPointerTypes); + + function run() { + var target0 = document.getElementById("target0"); + var btnComplete = document.getElementById("btnComplete"); + + //TA 15.11 + var test_touchaction_div = async_test("touch-action attribute test out of element"); + var test_touchaction_button = async_test("touch-action attribute test in element"); + + xScr0 = target0.scrollLeft; + yScr0 = target0.scrollTop; + + on_event(btnComplete, 'click', function(event) { + test_touchaction_button.step(function() { + assert_equals(target0.scrollLeft, 0, "button scroll x offset should be 0 in the end of the test"); + assert_equals(target0.scrollTop, 0, "button scroll y offset should be 0 in the end of the test"); + assert_true(xScrollIsReceived && yScrollIsReceived, "target0 x and y scroll offsets should be greater than 0 after first two interactions (outside red border) respectively"); + }); + test_touchaction_button.done(); + updateDescriptionComplete(); + }); + + on_event(btnComplete, 'pointerdown', function(event) { + detected_pointertypes[event.pointerType] = true; + }); + + on_event(target0, 'scroll', function(event) { + if(isFirstPart) { + xScr1 = target0.scrollLeft; + yScr1 = target0.scrollTop; + + if(xScr1 != xScr0) { + xScrollIsReceived = true; + } + + if(yScr1 != yScr0) { + test_touchaction_div.step(function () { + yScrollIsReceived = true; + }); + updateDescriptionSecondStepTouchActionElement(target0, scrollReturnInterval); + } + + if(xScrollIsReceived && yScrollIsReceived) { + test_touchaction_div.done(); + updateDescriptionThirdStepTouchActionElement(target0, scrollReturnInterval, function () { + setTimeout(function() { + isFirstPart = false; + }, scrollReturnInterval); // avoid immediate triggering while scroll is still being performed + }); + } + } + else { + test_touchaction_button.step(failOnScroll, "scroll received while shouldn't"); + } + }); + } + </script> + <h1>touch-action: none</h1> + <div id="complete-notice"> + <p>The following pointer types were detected: <span id="pointertype-log"></span>.</p> + </div> + <div id="log"></div> + </body> +</html> diff --git a/dom/events/test/pointerevents/wpt/pointerevent_touch-action-illegal.html b/dom/events/test/pointerevents/wpt/pointerevent_touch-action-illegal.html new file mode 100644 index 0000000000..5fe6179840 --- /dev/null +++ b/dom/events/test/pointerevents/wpt/pointerevent_touch-action-illegal.html @@ -0,0 +1,67 @@ +<!doctype html> +<html> + <head> + <title>touch-action: illegal</title> + <meta name="viewport" content="width=device-width"> + <link rel="stylesheet" type="text/css" href="pointerevent_styles.css"> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="pointerevent_support.js"></script> + <style> + #target0 { + width: 700px; + height: 50px; + touch-action: pan-x none; + } + #target1 { + width: 700px; + height: 50px; + background: black; + margin-top: 5px; + touch-action: pan-y none; + } + #target2 { + width: 700px; + height: 50px; + background: black; + margin-top: 5px; + touch-action: auto none; + } + </style> + </head> + <body onload="run()"> + <h1>Pointer Events touch-action attribute support</h1> + <h4 id="desc">Test Description: Test will automatically check behaviour of following combinations: 'pan-x none', 'pan-y none', 'auto none'</h4> + <div id="target0"></div> + <div id="target1"></div> + <div id="target2"></div> + <script type='text/javascript'> + var detected_pointertypes = {}; + + setup({ explicit_done: true }); + add_completion_callback(showPointerTypes); + + function run() { + var target0 = document.getElementById('target0'); + var target1 = document.getElementById('target1'); + var target2 = document.getElementById('target2'); + + test(function() { + assert_true(getComputedStyle(target0).touchAction == 'auto', "'pan-x none' is corrected properly"); + }, "'pan-x none' is corrected properly"); + test(function() { + assert_true(getComputedStyle(target1).touchAction == 'auto', "'pan-y none' is corrected properly"); + }, "'pan-y none' is corrected properly"); + test(function() { + assert_true(getComputedStyle(target2).touchAction == 'auto', "'auto none' is corrected properly"); + }, "'auto none' is corrected properly"); + done(); + } + </script> + <h1>touch-action: none</h1> + <div id="complete-notice"> + <p>The following pointer types were detected: <span id="pointertype-log"></span>.</p> + </div> + <div id="log"></div> + </body> +</html>
\ No newline at end of file diff --git a/dom/events/test/pointerevents/wpt/pointerevent_touch-action-inherit_child-auto-child-none_touch-manual.html b/dom/events/test/pointerevents/wpt/pointerevent_touch-action-inherit_child-auto-child-none_touch-manual.html new file mode 100644 index 0000000000..dcea283750 --- /dev/null +++ b/dom/events/test/pointerevents/wpt/pointerevent_touch-action-inherit_child-auto-child-none_touch-manual.html @@ -0,0 +1,117 @@ +<!doctype html> +<html> + <head> + <title>touch-action: parent > child: auto > child: none</title> + <meta name="assert" content="TA15.5 - when a user touches an element, the effect of that touch is determined by the value of the touch-action property and the default touch behaviors on the element and its ancestors. Scrollable-Parent, Child: `auto`, Grand-Child: `none`"> + <meta name="viewport" content="width=device-width"> + <link rel="stylesheet" type="text/css" href="pointerevent_styles.css"> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="pointerevent_support.js"></script> + <style> + .scroller > div { + touch-action: auto; + } + .scroller > div div { + touch-action: none; + } + </style> + </head> + <body onload="run()"> + <h1>Pointer Events touch-action attribute support</h1> + <h4 id="desc">Test Description: Try to scroll element DOWN then RIGHT inside blue rectangle. Tap Complete button under the rectangle when done. Expected: no panning.</h4> + <p>Note: this test is for touch-devices only</p> + <div class="scroller" id="target0"> + <div> + <div id="scrollTarget"> + <p> + Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diem + nonummy nibh euismod tincidunt ut lacreet dolore magna aliguam erat volutpat. + Ut wisis enim ad minim veniam, quis nostrud exerci tution ullamcorper suscipit + lobortis nisl ut aliquip ex ea commodo consequat. + </p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p> + Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diem + nonummy nibh euismod tincidunt ut lacreet dolore magna aliguam erat volutpat. + Ut wisis enim ad minim veniam, quis nostrud exerci tution ullamcorper suscipit + lobortis nisl ut aliquip ex ea commodo consequat. + </p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p> + Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diem + nonummy nibh euismod tincidunt ut lacreet dolore magna aliguam erat volutpat. + Ut wisis enim ad minim veniam, quis nostrud exerci tution ullamcorper suscipit + lobortis nisl ut aliquip ex ea commodo consequat. + </p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p> + Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diem + nonummy nibh euismod tincidunt ut lacreet dolore magna aliguam erat volutpat. + Ut wisis enim ad minim veniam, quis nostrud exerci tution ullamcorper suscipit + lobortis nisl ut aliquip ex ea commodo consequat. + </p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + </div> + </div> + </div> + <input type="button" id="btnComplete" value="Complete test"> + <script type='text/javascript'> + var detected_pointertypes = {}; + add_completion_callback(showPointerTypes); + + var test_touchaction = async_test("touch-action attribute test"); + + function run() { + var target0 = document.getElementById("target0"); + var btnComplete = document.getElementById("btnComplete"); + + // Check if touch-action attribute works properly for embedded divs + // Scrollable-Parent, Child: `auto`, Grand-Child: `none` + // TA: 15.5 + on_event(btnComplete, 'click', function(event) { + detected_pointertypes[event.pointerType] = true; + test_touchaction.step(function() { + assert_equals(target0.scrollLeft, 0, "scroll x offset should be 0 in the end of the test"); + assert_equals(target0.scrollTop, 0, "scroll y offset should be 0 in the end of the test"); + }); + test_touchaction.done(); + updateDescriptionComplete(); + }); + + on_event(target0, 'scroll', function(event) { + test_touchaction.step(failOnScroll, "scroll received while touch-action is none"); + }); + } + </script> + <h1>behaviour: none</h1> + <div id="complete-notice"> + <p>The following pointer types were detected: <span id="pointertype-log"></span>.</p> + </div> + <div id="log"></div> + </body> +</html>
\ No newline at end of file diff --git a/dom/events/test/pointerevents/wpt/pointerevent_touch-action-inherit_child-none_touch-manual.html b/dom/events/test/pointerevents/wpt/pointerevent_touch-action-inherit_child-none_touch-manual.html new file mode 100644 index 0000000000..16e42954e5 --- /dev/null +++ b/dom/events/test/pointerevents/wpt/pointerevent_touch-action-inherit_child-none_touch-manual.html @@ -0,0 +1,112 @@ +<!doctype html> +<html> + <head> + <title>touch-action: child: none</title> + <meta name="assert" content="TA15.9 - when a user touches an element, the effect of that touch is determined by the value of the touch-action property and the default touch behaviors on the element and its ancestors. Scrollable-Parent, Child: `none`"> + <meta name="viewport" content="width=device-width"> + <link rel="stylesheet" type="text/css" href="pointerevent_styles.css"> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="pointerevent_support.js"></script> + <style> + .scroller > div { + touch-action: none; + } + </style> + </head> + <body onload="run()"> + <h1>Pointer Events touch-action attribute support</h1> + <h4 id="desc">Test Description: Try to scroll element DOWN then RIGHT inside blue rectangle. Tap Complete button under the rectangle when done. Expected: no panning</h4> + <p>Note: this test is for touch-devices only</p> + <div class="scroller" id="target0"> + <div id="scrollTarget"> + <p> + Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diem + nonummy nibh euismod tincidunt ut lacreet dolore magna aliguam erat volutpat. + Ut wisis enim ad minim veniam, quis nostrud exerci tution ullamcorper suscipit + lobortis nisl ut aliquip ex ea commodo consequat. + </p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p> + Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diem + nonummy nibh euismod tincidunt ut lacreet dolore magna aliguam erat volutpat. + Ut wisis enim ad minim veniam, quis nostrud exerci tution ullamcorper suscipit + lobortis nisl ut aliquip ex ea commodo consequat. + </p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p> + Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diem + nonummy nibh euismod tincidunt ut lacreet dolore magna aliguam erat volutpat. + Ut wisis enim ad minim veniam, quis nostrud exerci tution ullamcorper suscipit + lobortis nisl ut aliquip ex ea commodo consequat. + </p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p> + Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diem + nonummy nibh euismod tincidunt ut lacreet dolore magna aliguam erat volutpat. + Ut wisis enim ad minim veniam, quis nostrud exerci tution ullamcorper suscipit + lobortis nisl ut aliquip ex ea commodo consequat. + </p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + </div> + </div> + <input type="button" id="btnComplete" value="Complete test"> + <script type='text/javascript'> + var detected_pointertypes = {}; + add_completion_callback(showPointerTypes); + + var test_touchaction = async_test("touch-action attribute test"); + + function run() { + var target0 = document.getElementById("target0"); + var btnComplete = document.getElementById("btnComplete"); + + // Check if touch-action attribute works properly for embedded divs + // + // TA: 15.9 + on_event(btnComplete, 'click', function(event) { + detected_pointertypes[event.pointerType] = true; + test_touchaction.step(function() { + assert_equals(target0.scrollLeft, 0, "scroll x offset should be 0 in the end of the test"); + assert_equals(target0.scrollTop, 0, "scroll y offset should be 0 in the end of the test"); + }); + test_touchaction.done(); + updateDescriptionComplete(); + }); + + on_event(target0, 'scroll', function(event) { + test_touchaction.step(failOnScroll, "scroll received while touch-action is none"); + }); + } + </script> + <h1>behaviour: none</h1> + <div id="complete-notice"> + <p>The following pointer types were detected: <span id="pointertype-log"></span>.</p> + </div> + <div id="log"></div> + </body> +</html>
\ No newline at end of file diff --git a/dom/events/test/pointerevents/wpt/pointerevent_touch-action-inherit_child-pan-x-child-pan-x_touch-manual.html b/dom/events/test/pointerevents/wpt/pointerevent_touch-action-inherit_child-pan-x-child-pan-x_touch-manual.html new file mode 100644 index 0000000000..c75d067e44 --- /dev/null +++ b/dom/events/test/pointerevents/wpt/pointerevent_touch-action-inherit_child-pan-x-child-pan-x_touch-manual.html @@ -0,0 +1,112 @@ +<!doctype html> +<html> + <head> + <title>touch-action: parent > child: pan-x > child: pan-x</title> + <meta name="assert" content="TA15.6 - when a user touches an element, the effect of that touch is determined by the value of the touch-action property and the default touch behaviors on the element and its ancestors. Scrollable-Parent, Child: `pan-x`, Grand-Child: `pan-x`"> + <meta name="viewport" content="width=device-width"> + <link rel="stylesheet" type="text/css" href="pointerevent_styles.css"> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="pointerevent_support.js"></script> + <style> + .scroller > div { + touch-action: pan-x; + } + .scroller > div div { + touch-action: pan-x; + } + </style> + </head> + <body onload="run()"> + <h1>Pointer Events touch-action attribute support</h1> + <h4 id="desc">Test Description: Try to scroll element DOWN then RIGHT inside blue rectangle. Tap Complete button under the rectangle when done. Expected: only pans in x direction.</h4> + <p>Note: this test is for touch-devices only</p> + <div class="scroller" id="target0"> + <div> + <div id="scrollTarget"> + <p> + Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diem + nonummy nibh euismod tincidunt ut lacreet dolore magna aliguam erat volutpat. + Ut wisis enim ad minim veniam, quis nostrud exerci tution ullamcorper suscipit + lobortis nisl ut aliquip ex ea commodo consequat. + </p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p> + Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diem + nonummy nibh euismod tincidunt ut lacreet dolore magna aliguam erat volutpat. + Ut wisis enim ad minim veniam, quis nostrud exerci tution ullamcorper suscipit + lobortis nisl ut aliquip ex ea commodo consequat. + </p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p> + Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diem + nonummy nibh euismod tincidunt ut lacreet dolore magna aliguam erat volutpat. + Ut wisis enim ad minim veniam, quis nostrud exerci tution ullamcorper suscipit + lobortis nisl ut aliquip ex ea commodo consequat. + </p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p> + Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diem + nonummy nibh euismod tincidunt ut lacreet dolore magna aliguam erat volutpat. + Ut wisis enim ad minim veniam, quis nostrud exerci tution ullamcorper suscipit + lobortis nisl ut aliquip ex ea commodo consequat. + </p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + </div> + </div> + </div> + <input type="button" id="btnComplete" value="Complete test"> + <script type='text/javascript'> + var detected_pointertypes = {}; + var test_touchaction = async_test("touch-action attribute test"); + add_completion_callback(showPointerTypes); + + function run() { + var target0 = document.getElementById("target0"); + var btnComplete = document.getElementById("btnComplete"); + + // Check if touch-action attribute works properly for embedded divs + // + // TA: 15.6 + on_event(btnComplete, 'click', function(event) { + detected_pointertypes[event.pointerType] = true; + test_touchaction.step(function() { + assert_not_equals(target0.scrollLeft, 0, "scroll x offset should not be 0 in the end of the test"); + assert_equals(target0.scrollTop, 0, "scroll y offset should be 0 in the end of the test"); + }); + test_touchaction.done(); + updateDescriptionComplete(); + }); + } + </script> + <h1>behaviour: pan-x</h1> + <div id="complete-notice"> + <p>The following pointer types were detected: <span id="pointertype-log"></span>.</p> + </div> + <div id="log"></div> + </body> +</html>
\ No newline at end of file diff --git a/dom/events/test/pointerevents/wpt/pointerevent_touch-action-inherit_child-pan-x-child-pan-y_touch-manual.html b/dom/events/test/pointerevents/wpt/pointerevent_touch-action-inherit_child-pan-x-child-pan-y_touch-manual.html new file mode 100644 index 0000000000..d420cc56c7 --- /dev/null +++ b/dom/events/test/pointerevents/wpt/pointerevent_touch-action-inherit_child-pan-x-child-pan-y_touch-manual.html @@ -0,0 +1,117 @@ +<!doctype html> +<html> + <head> + <title>touch-action: parent > child: pan-x > child: pan-y</title> + <meta name="assert" content="TA15.13 - Touch action inherits child 'pan-x' -> child 'pan-y' test"> + <meta name="viewport" content="width=device-width"> + <link rel="stylesheet" type="text/css" href="pointerevent_styles.css"> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="pointerevent_support.js"></script> + <style> + .scroller > div { + touch-action: pan-x; + } + .scroller > div div { + touch-action: pan-y; + } + </style> + </head> + <body onload="run()"> + <h1>Pointer Events touch-action attribute support</h1> + <h4 id="desc">Test Description: Try to scroll element DOWN then RIGHT inside blue rectangle. Tap Complete button under the rectangle when done. Expected: no panning/zooming/etc.</h4> + <p>Note: this test is for touch-devices only</p> + <div class="scroller" id="target0"> + <div> + <div id="scrollTarget"> + <p> + Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diem + nonummy nibh euismod tincidunt ut lacreet dolore magna aliguam erat volutpat. + Ut wisis enim ad minim veniam, quis nostrud exerci tution ullamcorper suscipit + lobortis nisl ut aliquip ex ea commodo consequat. + </p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p> + Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diem + nonummy nibh euismod tincidunt ut lacreet dolore magna aliguam erat volutpat. + Ut wisis enim ad minim veniam, quis nostrud exerci tution ullamcorper suscipit + lobortis nisl ut aliquip ex ea commodo consequat. + </p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p> + Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diem + nonummy nibh euismod tincidunt ut lacreet dolore magna aliguam erat volutpat. + Ut wisis enim ad minim veniam, quis nostrud exerci tution ullamcorper suscipit + lobortis nisl ut aliquip ex ea commodo consequat. + </p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p> + Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diem + nonummy nibh euismod tincidunt ut lacreet dolore magna aliguam erat volutpat. + Ut wisis enim ad minim veniam, quis nostrud exerci tution ullamcorper suscipit + lobortis nisl ut aliquip ex ea commodo consequat. + </p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + </div> + </div> + </div> + <input type="button" id="btnComplete" value="Complete test"> + <script type='text/javascript'> + var detected_pointertypes = {}; + add_completion_callback(showPointerTypes); + + var test_touchaction = async_test("touch-action attribute test"); + + function run() { + var target0 = document.getElementById("target0"); + var btnComplete = document.getElementById("btnComplete"); + + // Check if touch-action attribute works properly for embedded divs + // Scrollable-Parent, Child: `pan-x`, Grand-Child: `pan-y` + // TA: 15.13 + on_event(btnComplete, 'click', function(event) { + detected_pointertypes[event.pointerType] = true; + test_touchaction.step(function() { + assert_equals(target0.scrollLeft, 0, "scroll x offset should be 0 in the end of the test"); + assert_equals(target0.scrollTop, 0, "scroll y offset should be 0 in the end of the test"); + }); + test_touchaction.done(); + updateDescriptionComplete(); + }); + + on_event(target0, 'scroll', function(event) { + test_touchaction.step(failOnScroll, "scroll received while touch-action is none"); + }); + } + </script> + <h1>behaviour: none</h1> + <div id="complete-notice"> + <p>The following pointer types were detected: <span id="pointertype-log"></span>.</p> + </div> + <div id="log"></div> + </body> +</html>
\ No newline at end of file diff --git a/dom/events/test/pointerevents/wpt/pointerevent_touch-action-inherit_highest-parent-none_touch-manual.html b/dom/events/test/pointerevents/wpt/pointerevent_touch-action-inherit_highest-parent-none_touch-manual.html new file mode 100644 index 0000000000..d87d2b3a34 --- /dev/null +++ b/dom/events/test/pointerevents/wpt/pointerevent_touch-action-inherit_highest-parent-none_touch-manual.html @@ -0,0 +1,133 @@ +<!doctype html> +<html> + <head> + <title>touch-action: parent: none + two embedded children</title> + <meta name="viewport" content="width=device-width"> + <link rel="stylesheet" type="text/css" href="pointerevent_styles.css"> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="pointerevent_support.js"></script> + <style> + #divParent { + touch-action: none; + } + </style> + </head> + <body onload="run()"> + <h1>Pointer Events touch-action attribute support</h1> + <h4 id="desc">Test Description: Try to scroll text DOWN inside blue rectangle. Wait for description update. Expected: pan enabled</h4> + <p>Note: this test is for touch-devices only</p> + <div id="divParent"> + <div class="scroller" id="target0"> + <div id="scrollTarget"> + <p> + Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diem + nonummy nibh euismod tincidunt ut lacreet dolore magna aliguam erat volutpat. + Ut wisis enim ad minim veniam, quis nostrud exerci tution ullamcorper suscipit + lobortis nisl ut aliquip ex ea commodo consequat. + </p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p> + Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diem + nonummy nibh euismod tincidunt ut lacreet dolore magna aliguam erat volutpat. + Ut wisis enim ad minim veniam, quis nostrud exerci tution ullamcorper suscipit + lobortis nisl ut aliquip ex ea commodo consequat. + </p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p> + Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diem + nonummy nibh euismod tincidunt ut lacreet dolore magna aliguam erat volutpat. + Ut wisis enim ad minim veniam, quis nostrud exerci tution ullamcorper suscipit + lobortis nisl ut aliquip ex ea commodo consequat. + </p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p> + Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diem + nonummy nibh euismod tincidunt ut lacreet dolore magna aliguam erat volutpat. + Ut wisis enim ad minim veniam, quis nostrud exerci tution ullamcorper suscipit + lobortis nisl ut aliquip ex ea commodo consequat. + </p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + </div> + </div> + </div> + <script type='text/javascript'> + var detected_pointertypes = {}; + + var xScrollIsReceived = false; + var yScrollIsReceived = false; + var xScr0, yScr0, xScr1, yScr1; + + add_completion_callback(showPointerTypes); + add_completion_callback(enableScrolling); + + function run() { + var target0 = document.getElementById("target0"); + + var test_touchaction = async_test("touch-action attribute test"); + + xScr0 = target0.scrollLeft; + yScr0 = target0.scrollTop; + + on_event(target0, 'pointerdown', function(event) { + detected_pointertypes[event.pointerType] = true; + }); + + // Check if touch-action attribute works properly for embedded divs + // + // TA: 15. + on_event(target0, 'scroll', function(event) { + xScr1 = target0.scrollLeft; + yScr1 = target0.scrollTop; + + if(xScr1 != xScr0) { + xScrollIsReceived = true; + } + + if(yScr1 != yScr0) { + yScrollIsReceived = true; + updateDescriptionNextStep(); + } + + if(xScrollIsReceived && yScrollIsReceived) { + test_touchaction.done(); + updateDescriptionComplete(); + } + }); + } + + function enableScrolling() { + document.getElementById('divParent').setAttribute('style', 'touch-action: auto'); + } + </script> + <h1>behaviour: auto</h1> + <div id="complete-notice"> + <p>The following pointer types were detected: <span id="pointertype-log"></span>.</p> + </div> + <div id="log"></div> + </body> +</html>
\ No newline at end of file diff --git a/dom/events/test/pointerevents/wpt/pointerevent_touch-action-inherit_parent-none_touch-manual.html b/dom/events/test/pointerevents/wpt/pointerevent_touch-action-inherit_parent-none_touch-manual.html new file mode 100644 index 0000000000..5e674a14da --- /dev/null +++ b/dom/events/test/pointerevents/wpt/pointerevent_touch-action-inherit_parent-none_touch-manual.html @@ -0,0 +1,112 @@ +<!doctype html> +<html> + <head> + <title>touch-action: inherit from parent: none</title> + <meta name="assert" content="TA15.8 - when a user touches an element, the effect of that touch is determined by the value of the touch-action property and the default touch behaviors on the element and its ancestors. Scrollable-Parent: `none` Child: `auto`"> + <meta name="viewport" content="width=device-width"> + <link rel="stylesheet" type="text/css" href="pointerevent_styles.css"> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="pointerevent_support.js"></script> + <style> + .scroller { + touch-action: none; + } + </style> + </head> + <body onload="run()"> + <h1>Pointer Events touch-action attribute support</h1> + <h4 id="desc">Test Description: Try to scroll element DOWN then RIGHT inside blue rectangle. Tap Complete button under the rectangle when done. Expected: no panning</h4> + <p>Note: this test is for touch-devices only</p> + <div class="scroller" id="target0"> + <div id="scrollTarget"> + <p> + Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diem + nonummy nibh euismod tincidunt ut lacreet dolore magna aliguam erat volutpat. + Ut wisis enim ad minim veniam, quis nostrud exerci tution ullamcorper suscipit + lobortis nisl ut aliquip ex ea commodo consequat. + </p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p> + Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diem + nonummy nibh euismod tincidunt ut lacreet dolore magna aliguam erat volutpat. + Ut wisis enim ad minim veniam, quis nostrud exerci tution ullamcorper suscipit + lobortis nisl ut aliquip ex ea commodo consequat. + </p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p> + Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diem + nonummy nibh euismod tincidunt ut lacreet dolore magna aliguam erat volutpat. + Ut wisis enim ad minim veniam, quis nostrud exerci tution ullamcorper suscipit + lobortis nisl ut aliquip ex ea commodo consequat. + </p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p> + Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diem + nonummy nibh euismod tincidunt ut lacreet dolore magna aliguam erat volutpat. + Ut wisis enim ad minim veniam, quis nostrud exerci tution ullamcorper suscipit + lobortis nisl ut aliquip ex ea commodo consequat. + </p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + </div> + </div> + <input type="button" id="btnComplete" value="Complete test"> + <script type='text/javascript'> + var detected_pointertypes = {}; + add_completion_callback(showPointerTypes); + + var test_touchaction = async_test("touch-action attribute test"); + + function run() { + var target0 = document.getElementById("target0"); + var btnComplete = document.getElementById("btnComplete"); + + // Check if touch-action attribute works properly for embedded divs + // + // TA: 15.8 + on_event(btnComplete, 'click', function(event) { + detected_pointertypes[event.pointerType] = true; + test_touchaction.step(function() { + assert_equals(target0.scrollLeft, 0, "scroll x offset should be 0 in the end of the test"); + assert_equals(target0.scrollTop, 0, "scroll y offset should be 0 in the end of the test"); + }); + test_touchaction.done(); + updateDescriptionComplete(); + }); + + on_event(target0, 'scroll', function(event) { + test_touchaction.step(failOnScroll, "scroll received while touch-action is none"); + }); + } + </script> + <h1>behaviour: none</h1> + <div id="complete-notice"> + <p>The following pointer types were detected: <span id="pointertype-log"></span>.</p> + </div> + <div id="log"></div> + </body> +</html>
\ No newline at end of file diff --git a/dom/events/test/pointerevents/wpt/pointerevent_touch-action-keyboard-manual.html b/dom/events/test/pointerevents/wpt/pointerevent_touch-action-keyboard-manual.html new file mode 100644 index 0000000000..3fef3f646f --- /dev/null +++ b/dom/events/test/pointerevents/wpt/pointerevent_touch-action-keyboard-manual.html @@ -0,0 +1,124 @@ +<!doctype html> +<html> + <head> + <title>touch-action: keyboard</title> + <meta name="viewport" content="width=device-width"> + <link rel="stylesheet" type="text/css" href="pointerevent_styles.css"> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="pointerevent_support.js"></script> + <style> + #target0 { + width: 700px; + height: 430px; + touch-action: none; + } + </style> + </head> + <body onload="run()"> + <h1>Pointer Events touch-action attribute support</h1> + <h4 id="desc">Test Description: Press DOWN ARROW key. Wait for description update. Expected: pan enabled</h4> + <p>Note: this test is for keyboard only</p> + <div id="target0"> + <p> + Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diem + nonummy nibh euismod tincidunt ut lacreet dolore magna aliguam erat volutpat. + Ut wisis enim ad minim veniam, quis nostrud exerci tution ullamcorper suscipit + lobortis nisl ut aliquip ex ea commodo consequat. + </p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p> + Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diem + nonummy nibh euismod tincidunt ut lacreet dolore magna aliguam erat volutpat. + Ut wisis enim ad minim veniam, quis nostrud exerci tution ullamcorper suscipit + lobortis nisl ut aliquip ex ea commodo consequat. + </p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p> + Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diem + nonummy nibh euismod tincidunt ut lacreet dolore magna aliguam erat volutpat. + Ut wisis enim ad minim veniam, quis nostrud exerci tution ullamcorper suscipit + lobortis nisl ut aliquip ex ea commodo consequat. + </p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p> + Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diem + nonummy nibh euismod tincidunt ut lacreet dolore magna aliguam erat volutpat. + Ut wisis enim ad minim veniam, quis nostrud exerci tution ullamcorper suscipit + lobortis nisl ut aliquip ex ea commodo consequat. + </p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + </div> + <script type='text/javascript'> + + var xScrollIsReceived = false; + var yScrollIsReceived = false; + var xScr0, yScr0, xScr1, yScr1; + + function run() { + var target0 = document.getElementById("target0"); + + var test_touchaction = async_test("touch-action attribute test"); + + xScr0 = target0.scrollLeft; + yScr0 = target0.scrollTop; + + target0.focus(); + + on_event(target0, 'scroll', function(event) { + xScr1 = target0.scrollLeft; + yScr1 = target0.scrollTop; + + if(xScr1 != xScr0) { + xScrollIsReceived = true; + } + + if(yScr1 != yScr0) { + test_touchaction.step(function () { + yScrollIsReceived = true; + assert_true(true, "y-scroll received."); + }); + updateDescriptionNextStepKeyboard(); + } + + if(xScrollIsReceived && yScrollIsReceived) { + test_touchaction.done(); + updateDescriptionComplete(); + } + }); + } + + function updateDescriptionNextStepKeyboard() { + document.getElementById('desc').innerHTML = "Test Description: press RIGHT ARROW key."; + } + </script> + <h1>touch-action: none</h1> + <div id="complete-notice"> + </div> + <div id="log"></div> + </body> +</html>
\ No newline at end of file diff --git a/dom/events/test/pointerevents/wpt/pointerevent_touch-action-mouse-manual.html b/dom/events/test/pointerevents/wpt/pointerevent_touch-action-mouse-manual.html new file mode 100644 index 0000000000..fcc8584515 --- /dev/null +++ b/dom/events/test/pointerevents/wpt/pointerevent_touch-action-mouse-manual.html @@ -0,0 +1,130 @@ +<!doctype html> +<html> + <head> + <title>touch-action: mouse</title> + <meta name="viewport" content="width=device-width"> + <link rel="stylesheet" type="text/css" href="pointerevent_styles.css"> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="pointerevent_support.js"></script> + <style> + #target0 { + width: 700px; + height: 430px; + touch-action: none; + } + </style> + </head> + <body onload="run()"> + <h1>Pointer Events touch-action attribute support</h1> + <h4 id="desc">Test Description: Try to scroll text down using mouse (use mouse wheel or click on the scrollbar). Wait for description update.</h4> + <p>Note: this test is for mouse only</p> + <div id="target0"> + <p> + Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diem + nonummy nibh euismod tincidunt ut lacreet dolore magna aliguam erat volutpat. + Ut wisis enim ad minim veniam, quis nostrud exerci tution ullamcorper suscipit + lobortis nisl ut aliquip ex ea commodo consequat. + </p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p> + Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diem + nonummy nibh euismod tincidunt ut lacreet dolore magna aliguam erat volutpat. + Ut wisis enim ad minim veniam, quis nostrud exerci tution ullamcorper suscipit + lobortis nisl ut aliquip ex ea commodo consequat. + </p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p> + Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diem + nonummy nibh euismod tincidunt ut lacreet dolore magna aliguam erat volutpat. + Ut wisis enim ad minim veniam, quis nostrud exerci tution ullamcorper suscipit + lobortis nisl ut aliquip ex ea commodo consequat. + </p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p> + Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diem + nonummy nibh euismod tincidunt ut lacreet dolore magna aliguam erat volutpat. + Ut wisis enim ad minim veniam, quis nostrud exerci tution ullamcorper suscipit + lobortis nisl ut aliquip ex ea commodo consequat. + </p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + </div> + <script type='text/javascript'> + var detected_pointertypes = {}; + + var xScrollIsReceived = false; + var yScrollIsReceived = false; + var xScr0, yScr0, xScr1, yScr1; + + add_completion_callback(showPointerTypes); + + function run() { + var target0 = document.getElementById("target0"); + + var test_touchaction = async_test("touch-action attribute test"); + + xScr0 = target0.scrollLeft; + yScr0 = target0.scrollTop; + + on_event(target0, 'pointerdown', function(event) { + detected_pointertypes[event.pointerType] = true; + }); + + on_event(target0, 'scroll', function(event) { + xScr1 = target0.scrollLeft; + yScr1 = target0.scrollTop; + + if(xScr1 != xScr0) { + xScrollIsReceived = true; + } + + if(yScr1 != yScr0) { + test_touchaction.step(function () { + yScrollIsReceived = true; + assert_true(true, "y-scroll received."); + }); + updateDescriptionNextStepMouse(); + } + + if(xScrollIsReceived && yScrollIsReceived) { + test_touchaction.done(); + updateDescriptionComplete(); + } + }); + } + + function updateDescriptionNextStepMouse() { + document.getElementById('desc').innerHTML = "Test Description: Try to scroll text right using mouse (use mouse wheel or click on the scrollbar)."; + } + </script> + <h1>touch-action: none</h1> + <div id="complete-notice"> + <p>The following pointer types were detected: <span id="pointertype-log"></span>.</p> + </div> + <div id="log"></div> + </body> +</html>
\ No newline at end of file diff --git a/dom/events/test/pointerevents/wpt/pointerevent_touch-action-none-css_touch-manual.html b/dom/events/test/pointerevents/wpt/pointerevent_touch-action-none-css_touch-manual.html new file mode 100644 index 0000000000..dec694f3ec --- /dev/null +++ b/dom/events/test/pointerevents/wpt/pointerevent_touch-action-none-css_touch-manual.html @@ -0,0 +1,111 @@ +<!doctype html> +<html> + <head> + <title>touch-action: none</title> + <meta name="assert" content="TA15.2 - With `touch-action: none` on a swiped or click/dragged element, `pointerdown+(optional pointermove)+pointerup` must be dispatched."> + <meta name="viewport" content="width=device-width"> + <link rel="stylesheet" type="text/css" href="pointerevent_styles.css"> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="pointerevent_support.js"></script> + <style> + #target0 { + width: 700px; + height: 430px; + touch-action: none; + } + </style> + </head> + <body onload="run()"> + <h1>Pointer Events touch-action attribute support</h1> + <h4 id="desc">Test Description: Try to scroll element DOWN then RIGHT. Tap Complete button under the rectangle when done. Expected: no panning/zooming/etc.</h4> + <p>Note: this test is for touch-devices only</p> + <div id="target0"> + <p> + Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diem + nonummy nibh euismod tincidunt ut lacreet dolore magna aliguam erat volutpat. + Ut wisis enim ad minim veniam, quis nostrud exerci tution ullamcorper suscipit + lobortis nisl ut aliquip ex ea commodo consequat. + </p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p> + Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diem + nonummy nibh euismod tincidunt ut lacreet dolore magna aliguam erat volutpat. + Ut wisis enim ad minim veniam, quis nostrud exerci tution ullamcorper suscipit + lobortis nisl ut aliquip ex ea commodo consequat. + </p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p> + Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diem + nonummy nibh euismod tincidunt ut lacreet dolore magna aliguam erat volutpat. + Ut wisis enim ad minim veniam, quis nostrud exerci tution ullamcorper suscipit + lobortis nisl ut aliquip ex ea commodo consequat. + </p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p> + Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diem + nonummy nibh euismod tincidunt ut lacreet dolore magna aliguam erat volutpat. + Ut wisis enim ad minim veniam, quis nostrud exerci tution ullamcorper suscipit + lobortis nisl ut aliquip ex ea commodo consequat. + </p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + </div> + <input type="button" id="btnComplete" value="Complete test"> + <script type='text/javascript'> + var detected_pointertypes = {}; + + var test_touchaction = async_test("touch-action attribute test"); + add_completion_callback(showPointerTypes); + + function run() { + var target0 = document.getElementById("target0"); + var btnComplete = document.getElementById("btnComplete"); + + // Check if "touch-action: none" attribute works properly + //TA: 15.2 + on_event(btnComplete, 'click', function(event) { + detected_pointertypes[event.pointerType] = true; + test_touchaction.step(function() { + assert_equals(target0.scrollLeft, 0, "scroll x offset should be 0 in the end of the test"); + assert_equals(target0.scrollTop, 0, "scroll y offset should be 0 in the end of the test"); + }); + test_touchaction.done(); + updateDescriptionComplete(); + }); + + on_event(target0, 'scroll', function(event) { + test_touchaction.step(failOnScroll, "scroll received while touch-action is none"); + }); + } + </script> + <h1>touch-action: none</h1> + <div id="complete-notice"> + <p>The following pointer types were detected: <span id="pointertype-log"></span>.</p> + </div> + <div id="log"></div> + </body> +</html>
\ No newline at end of file diff --git a/dom/events/test/pointerevents/wpt/pointerevent_touch-action-pan-down-css_touch-manual.html b/dom/events/test/pointerevents/wpt/pointerevent_touch-action-pan-down-css_touch-manual.html new file mode 100644 index 0000000000..16e1cb2fab --- /dev/null +++ b/dom/events/test/pointerevents/wpt/pointerevent_touch-action-pan-down-css_touch-manual.html @@ -0,0 +1,114 @@ +<!doctype html> +<html> + <head> + <title>touch-action: pan-down</title> + <meta name="assert" content="TA15.4 - With `touch-action: pan-down` on a swiped or click/dragged element, only panning in the y-axis down direction should be possible."> + <meta name="viewport" content="width=device-width"> + <link rel="stylesheet" type="text/css" href="pointerevent_styles.css"> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="pointerevent_support.js"></script> + <style> + #target0 { + width: 700px; + height: 430px; + touch-action: pan-down; + } + </style> + </head> + <body onload="run()"> + <h1>Pointer Events touch-action attribute support</h1> + <h4 id="desc">Test Description: Try to scroll element UP (drag down), then RIGHT (drag left), then DOWN (drag up). Tap Complete button under the rectangle when done. Expected: only pans in down direction.</h4> + <p>Note: this test is for touch-devices only</p> + <div id="target0"> + <p> + Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diem + nonummy nibh euismod tincidunt ut lacreet dolore magna aliguam erat volutpat. + Ut wisis enim ad minim veniam, quis nostrud exerci tution ullamcorper suscipit + lobortis nisl ut aliquip ex ea commodo consequat. + </p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p> + Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diem + nonummy nibh euismod tincidunt ut lacreet dolore magna aliguam erat volutpat. + Ut wisis enim ad minim veniam, quis nostrud exerci tution ullamcorper suscipit + lobortis nisl ut aliquip ex ea commodo consequat. + </p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p> + Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diem + nonummy nibh euismod tincidunt ut lacreet dolore magna aliguam erat volutpat. + Ut wisis enim ad minim veniam, quis nostrud exerci tution ullamcorper suscipit + lobortis nisl ut aliquip ex ea commodo consequat. + </p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p> + Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diem + nonummy nibh euismod tincidunt ut lacreet dolore magna aliguam erat volutpat. + Ut wisis enim ad minim veniam, quis nostrud exerci tution ullamcorper suscipit + lobortis nisl ut aliquip ex ea commodo consequat. + </p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + </div> + <input type="button" id="btnComplete" value="Complete test"> + <script type='text/javascript'> + var detected_pointertypes = {}; + var test_touchaction = async_test("touch-action attribute test"); + add_completion_callback(showPointerTypes); + + function run() { + var target0 = document.getElementById("target0"); + var btnComplete = document.getElementById("btnComplete"); + target0.scrollTop = 200; + + var scrollListenerExecuted = false; + target0.addEventListener("scroll", function(event) { + scrollListenerExecuted = true; + assert_greater_than_equal(target0.scrollTop, 200); + }); + + // Check if "touch-action: pan-down" attribute works properly + //TA: 15.4 + on_event(btnComplete, 'click', function(event) { + detected_pointertypes[event.pointerType] = true; + test_touchaction.step(function() { + assert_true(scrollListenerExecuted, "scroll listener should have been executed by the end of the test"); + assert_equals(target0.scrollLeft, 0, "scroll x offset should be 0 in the end of the test"); + assert_greater_than(target0.scrollTop, 200, "scroll y offset should be greater than 200 in the end of the test"); + }); + test_touchaction.done(); + updateDescriptionComplete(); + }); + } + </script> + <h1>touch-action: pan-down</h1> + <div id="complete-notice"> + <p>The following pointer types were detected: <span id="pointertype-log"></span>.</p> + </div> + <div id="log"></div> + </body> +</html> diff --git a/dom/events/test/pointerevents/wpt/pointerevent_touch-action-pan-left-css_touch-manual.html b/dom/events/test/pointerevents/wpt/pointerevent_touch-action-pan-left-css_touch-manual.html new file mode 100644 index 0000000000..53fd2de138 --- /dev/null +++ b/dom/events/test/pointerevents/wpt/pointerevent_touch-action-pan-left-css_touch-manual.html @@ -0,0 +1,114 @@ +<!doctype html> +<html> + <head> + <title>touch-action: pan-left</title> + <meta name="assert" content="TA15.3 - With `touch-action: pan-left` on a swiped or click/dragged element, only panning on the x-axis left direction should be possible."> + <meta name="viewport" content="width=device-width"> + <link rel="stylesheet" type="text/css" href="pointerevent_styles.css"> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="pointerevent_support.js"></script> + <style> + #target0 { + width: 700px; + height: 430px; + touch-action: pan-left; + } + </style> + </head> + <body onload="run()"> + <h1>Pointer Events touch-action attribute support</h1> + <h4 id="desc">Test Description: Try to scroll element DOWN (drag up), then RIGHT (drag left), then LEFT (drag right). Tap Complete button under the rectangle when done. Expected: only pans in left direction.</h4> + <p>Note: this test is for touch-devices only</p> + <div id="target0"> + <p> + Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diem + nonummy nibh euismod tincidunt ut lacreet dolore magna aliguam erat volutpat. + Ut wisis enim ad minim veniam, quis nostrud exerci tution ullamcorper suscipit + lobortis nisl ut aliquip ex ea commodo consequat. + </p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p> + Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diem + nonummy nibh euismod tincidunt ut lacreet dolore magna aliguam erat volutpat. + Ut wisis enim ad minim veniam, quis nostrud exerci tution ullamcorper suscipit + lobortis nisl ut aliquip ex ea commodo consequat. + </p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p> + Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diem + nonummy nibh euismod tincidunt ut lacreet dolore magna aliguam erat volutpat. + Ut wisis enim ad minim veniam, quis nostrud exerci tution ullamcorper suscipit + lobortis nisl ut aliquip ex ea commodo consequat. + </p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p> + Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diem + nonummy nibh euismod tincidunt ut lacreet dolore magna aliguam erat volutpat. + Ut wisis enim ad minim veniam, quis nostrud exerci tution ullamcorper suscipit + lobortis nisl ut aliquip ex ea commodo consequat. + </p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + </div> + <input type="button" id="btnComplete" value="Complete test"> + <script type='text/javascript'> + var detected_pointertypes = {}; + var test_touchaction = async_test("touch-action attribute test"); + add_completion_callback(showPointerTypes); + + function run() { + var target0 = document.getElementById("target0"); + var btnComplete = document.getElementById("btnComplete"); + target0.scrollLeft = 200; + + var scrollListenerExecuted = false; + target0.addEventListener("scroll", function(event) { + scrollListenerExecuted = true; + assert_less_than_equal(target0.scrollLeft, 200); + }); + + // Check if "touch-action: pan-left" attribute works properly + //TA: 15.3 + on_event(btnComplete, 'click', function(event) { + detected_pointertypes[event.pointerType] = true; + test_touchaction.step(function() { + assert_true(scrollListenerExecuted, "scroll listener should have been executed by the end of the test"); + assert_less_than(target0.scrollLeft, 200, "scroll x offset should be less than 200 in the end of the test"); + assert_equals(target0.scrollTop, 0, "scroll y offset should be 0 in the end of the test"); + }); + test_touchaction.done(); + updateDescriptionComplete(); + }); + } + </script> + <h1>touch-action: pan-left</h1> + <div id="complete-notice"> + <p>The following pointer types were detected: <span id="pointertype-log"></span>.</p> + </div> + <div id="log"></div> + </body> +</html> diff --git a/dom/events/test/pointerevents/wpt/pointerevent_touch-action-pan-right-css_touch-manual.html b/dom/events/test/pointerevents/wpt/pointerevent_touch-action-pan-right-css_touch-manual.html new file mode 100644 index 0000000000..53bbac65ec --- /dev/null +++ b/dom/events/test/pointerevents/wpt/pointerevent_touch-action-pan-right-css_touch-manual.html @@ -0,0 +1,114 @@ +<!doctype html> +<html> + <head> + <title>touch-action: pan-right</title> + <meta name="assert" content="TA15.3 - With `touch-action: pan-right` on a swiped or click/dragged element, only panning on the x-axis right direction should be possible."> + <meta name="viewport" content="width=device-width"> + <link rel="stylesheet" type="text/css" href="pointerevent_styles.css"> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="pointerevent_support.js"></script> + <style> + #target0 { + width: 700px; + height: 430px; + touch-action: pan-right; + } + </style> + </head> + <body onload="run()"> + <h1>Pointer Events touch-action attribute support</h1> + <h4 id="desc">Test Description: Try to scroll element DOWN (drag up), then LEFT (drag right), then RIGHT (drag left). Tap Complete button under the rectangle when done. Expected: only pans in right direction.</h4> + <p>Note: this test is for touch-devices only</p> + <div id="target0"> + <p> + Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diem + nonummy nibh euismod tincidunt ut lacreet dolore magna aliguam erat volutpat. + Ut wisis enim ad minim veniam, quis nostrud exerci tution ullamcorper suscipit + lobortis nisl ut aliquip ex ea commodo consequat. + </p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p> + Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diem + nonummy nibh euismod tincidunt ut lacreet dolore magna aliguam erat volutpat. + Ut wisis enim ad minim veniam, quis nostrud exerci tution ullamcorper suscipit + lobortis nisl ut aliquip ex ea commodo consequat. + </p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p> + Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diem + nonummy nibh euismod tincidunt ut lacreet dolore magna aliguam erat volutpat. + Ut wisis enim ad minim veniam, quis nostrud exerci tution ullamcorper suscipit + lobortis nisl ut aliquip ex ea commodo consequat. + </p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p> + Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diem + nonummy nibh euismod tincidunt ut lacreet dolore magna aliguam erat volutpat. + Ut wisis enim ad minim veniam, quis nostrud exerci tution ullamcorper suscipit + lobortis nisl ut aliquip ex ea commodo consequat. + </p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + </div> + <input type="button" id="btnComplete" value="Complete test"> + <script type='text/javascript'> + var detected_pointertypes = {}; + var test_touchaction = async_test("touch-action attribute test"); + add_completion_callback(showPointerTypes); + + function run() { + var target0 = document.getElementById("target0"); + var btnComplete = document.getElementById("btnComplete"); + target0.scrollLeft = 200; + + var scrollListenerExecuted = false; + target0.addEventListener("scroll", function(event) { + scrollListenerExecuted = true; + assert_greater_than_equal(target0.scrollLeft, 200); + }); + + // Check if "touch-action: pan-right" attribute works properly + //TA: 15.3 + on_event(btnComplete, 'click', function(event) { + detected_pointertypes[event.pointerType] = true; + test_touchaction.step(function() { + assert_true(scrollListenerExecuted, "scroll listener should have been executed by the end of the test"); + assert_greater_than(target0.scrollLeft, 200, "scroll x offset should be greater than 200 in the end of the test"); + assert_equals(target0.scrollTop, 0, "scroll y offset should be 0 in the end of the test"); + }); + test_touchaction.done(); + updateDescriptionComplete(); + }); + } + </script> + <h1>touch-action: pan-right</h1> + <div id="complete-notice"> + <p>The following pointer types were detected: <span id="pointertype-log"></span>.</p> + </div> + <div id="log"></div> + </body> +</html> diff --git a/dom/events/test/pointerevents/wpt/pointerevent_touch-action-pan-up-css_touch-manual.html b/dom/events/test/pointerevents/wpt/pointerevent_touch-action-pan-up-css_touch-manual.html new file mode 100644 index 0000000000..0902700d2d --- /dev/null +++ b/dom/events/test/pointerevents/wpt/pointerevent_touch-action-pan-up-css_touch-manual.html @@ -0,0 +1,114 @@ +<!doctype html> +<html> + <head> + <title>touch-action: pan-up</title> + <meta name="assert" content="TA15.4 - With `touch-action: pan-up` on a swiped or click/dragged element, only panning in the y-axis up direction should be possible."> + <meta name="viewport" content="width=device-width"> + <link rel="stylesheet" type="text/css" href="pointerevent_styles.css"> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="pointerevent_support.js"></script> + <style> + #target0 { + width: 700px; + height: 430px; + touch-action: pan-up; + } + </style> + </head> + <body onload="run()"> + <h1>Pointer Events touch-action attribute support</h1> + <h4 id="desc">Test Description: Try to scroll element DOWN (drag up), then RIGHT (drag left), then UP (drag down). Tap Complete button under the rectangle when done. Expected: only pans in up direction.</h4> + <p>Note: this test is for touch-devices only</p> + <div id="target0"> + <p> + Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diem + nonummy nibh euismod tincidunt ut lacreet dolore magna aliguam erat volutpat. + Ut wisis enim ad minim veniam, quis nostrud exerci tution ullamcorper suscipit + lobortis nisl ut aliquip ex ea commodo consequat. + </p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p> + Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diem + nonummy nibh euismod tincidunt ut lacreet dolore magna aliguam erat volutpat. + Ut wisis enim ad minim veniam, quis nostrud exerci tution ullamcorper suscipit + lobortis nisl ut aliquip ex ea commodo consequat. + </p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p> + Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diem + nonummy nibh euismod tincidunt ut lacreet dolore magna aliguam erat volutpat. + Ut wisis enim ad minim veniam, quis nostrud exerci tution ullamcorper suscipit + lobortis nisl ut aliquip ex ea commodo consequat. + </p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p> + Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diem + nonummy nibh euismod tincidunt ut lacreet dolore magna aliguam erat volutpat. + Ut wisis enim ad minim veniam, quis nostrud exerci tution ullamcorper suscipit + lobortis nisl ut aliquip ex ea commodo consequat. + </p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + </div> + <input type="button" id="btnComplete" value="Complete test"> + <script type='text/javascript'> + var detected_pointertypes = {}; + var test_touchaction = async_test("touch-action attribute test"); + add_completion_callback(showPointerTypes); + + function run() { + var target0 = document.getElementById("target0"); + var btnComplete = document.getElementById("btnComplete"); + target0.scrollTop = 200; + + var scrollListenerExecuted = false; + target0.addEventListener("scroll", function(event) { + scrollListenerExecuted = true; + assert_less_than_equal(target0.scrollTop, 200); + }); + + // Check if "touch-action: pan-up" attribute works properly + //TA: 15.4 + on_event(btnComplete, 'click', function(event) { + detected_pointertypes[event.pointerType] = true; + test_touchaction.step(function() { + assert_true(scrollListenerExecuted, "scroll listener should have been executed by the end of the test"); + assert_equals(target0.scrollLeft, 0, "scroll x offset should be 0 in the end of the test"); + assert_less_than(target0.scrollTop, 200, "scroll y offset should be less than 200 in the end of the test"); + }); + test_touchaction.done(); + updateDescriptionComplete(); + }); + } + </script> + <h1>touch-action: pan-up</h1> + <div id="complete-notice"> + <p>The following pointer types were detected: <span id="pointertype-log"></span>.</p> + </div> + <div id="log"></div> + </body> +</html> diff --git a/dom/events/test/pointerevents/wpt/pointerevent_touch-action-pan-x-css_touch-manual.html b/dom/events/test/pointerevents/wpt/pointerevent_touch-action-pan-x-css_touch-manual.html new file mode 100644 index 0000000000..e757baec6b --- /dev/null +++ b/dom/events/test/pointerevents/wpt/pointerevent_touch-action-pan-x-css_touch-manual.html @@ -0,0 +1,106 @@ +<!doctype html> +<html> + <head> + <title>touch-action: pan-x</title> + <meta name="assert" content="TA15.3 - With `touch-action: pan-x` on a swiped or click/dragged element, only panning on the x-axis should be possible."> + <meta name="viewport" content="width=device-width"> + <link rel="stylesheet" type="text/css" href="pointerevent_styles.css"> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="pointerevent_support.js"></script> + <style> + #target0 { + width: 700px; + height: 430px; + touch-action: pan-x; + } + </style> + </head> + <body onload="run()"> + <h1>Pointer Events touch-action attribute support</h1> + <h4 id="desc">Test Description: Try to scroll element DOWN then RIGHT. Tap Complete button under the rectangle when done. Expected: only pans in x direction.</h4> + <p>Note: this test is for touch-devices only</p> + <div id="target0"> + <p> + Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diem + nonummy nibh euismod tincidunt ut lacreet dolore magna aliguam erat volutpat. + Ut wisis enim ad minim veniam, quis nostrud exerci tution ullamcorper suscipit + lobortis nisl ut aliquip ex ea commodo consequat. + </p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p> + Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diem + nonummy nibh euismod tincidunt ut lacreet dolore magna aliguam erat volutpat. + Ut wisis enim ad minim veniam, quis nostrud exerci tution ullamcorper suscipit + lobortis nisl ut aliquip ex ea commodo consequat. + </p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p> + Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diem + nonummy nibh euismod tincidunt ut lacreet dolore magna aliguam erat volutpat. + Ut wisis enim ad minim veniam, quis nostrud exerci tution ullamcorper suscipit + lobortis nisl ut aliquip ex ea commodo consequat. + </p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p> + Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diem + nonummy nibh euismod tincidunt ut lacreet dolore magna aliguam erat volutpat. + Ut wisis enim ad minim veniam, quis nostrud exerci tution ullamcorper suscipit + lobortis nisl ut aliquip ex ea commodo consequat. + </p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + </div> + <input type="button" id="btnComplete" value="Complete test"> + <script type='text/javascript'> + var detected_pointertypes = {}; + var test_touchaction = async_test("touch-action attribute test"); + add_completion_callback(showPointerTypes); + + function run() { + var target0 = document.getElementById("target0"); + var btnComplete = document.getElementById("btnComplete"); + + // Check if "touch-action: pan-x" attribute works properly + //TA: 15.3 + on_event(btnComplete, 'click', function(event) { + detected_pointertypes[event.pointerType] = true; + test_touchaction.step(function() { + assert_not_equals(target0.scrollLeft, 0, "scroll x offset should not be 0 in the end of the test"); + assert_equals(target0.scrollTop, 0, "scroll y offset should be 0 in the end of the test"); + }); + test_touchaction.done(); + updateDescriptionComplete(); + }); + } + </script> + <h1>touch-action: pan-x</h1> + <div id="complete-notice"> + <p>The following pointer types were detected: <span id="pointertype-log"></span>.</p> + </div> + <div id="log"></div> + </body> +</html>
\ No newline at end of file diff --git a/dom/events/test/pointerevents/wpt/pointerevent_touch-action-pan-x-pan-y-pan-y_touch-manual.html b/dom/events/test/pointerevents/wpt/pointerevent_touch-action-pan-x-pan-y-pan-y_touch-manual.html new file mode 100644 index 0000000000..e2a4386b27 --- /dev/null +++ b/dom/events/test/pointerevents/wpt/pointerevent_touch-action-pan-x-pan-y-pan-y_touch-manual.html @@ -0,0 +1,111 @@ +<!doctype html> +<html> + <head> + <title>touch-action: parent > child: pan-x pan-y > child: pan-y</title> + <meta name="assert" content="TA15.17 - Touch action 'pan-x pan-y' 'pan-y' test"> + <meta name="viewport" content="width=device-width"> + <link rel="stylesheet" type="text/css" href="pointerevent_styles.css"> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="pointerevent_support.js"></script> + <style> + .scroller > div { + touch-action: pan-x pan-y; + } + .scroller > div div { + touch-action: pan-y; + } + </style> + </head> + <body onload="run()"> + <h1>Pointer Events touch-action attribute support</h1> + <h4 id="desc">Test Description: Try to scroll element DOWN then RIGHT inside blue rectangle. Tap Complete button under the rectangle when done. Expected: only pans in y direction.</h4> + <p>Note: this test is for touch-devices only</p> + <div class="scroller" id="target0"> + <div> + <div id="scrollTarget"> + Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diem + nonummy nibh euismod tincidunt ut lacreet dolore magna aliguam erat volutpat. + Ut wisis enim ad minim veniam, quis nostrud exerci tution ullamcorper suscipit + lobortis nisl ut aliquip ex ea commodo consequat. + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p> + Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diem + nonummy nibh euismod tincidunt ut lacreet dolore magna aliguam erat volutpat. + Ut wisis enim ad minim veniam, quis nostrud exerci tution ullamcorper suscipit + lobortis nisl ut aliquip ex ea commodo consequat. + </p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p> + Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diem + nonummy nibh euismod tincidunt ut lacreet dolore magna aliguam erat volutpat. + Ut wisis enim ad minim veniam, quis nostrud exerci tution ullamcorper suscipit + lobortis nisl ut aliquip ex ea commodo consequat. + </p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p> + Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diem + nonummy nibh euismod tincidunt ut lacreet dolore magna aliguam erat volutpat. + Ut wisis enim ad minim veniam, quis nostrud exerci tution ullamcorper suscipit + lobortis nisl ut aliquip ex ea commodo consequat. + </p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + </div> + </div> + </div> + <input type="button" id="btnComplete" value="Complete test"> + <script type='text/javascript'> + var detected_pointertypes = {}; + add_completion_callback(showPointerTypes); + + var test_touchaction = async_test("touch-action attribute test"); + + function run() { + var target0 = document.getElementById("target0"); + var btnComplete = document.getElementById("btnComplete"); + + // Check if touch-action attribute works properly for embedded divs + // + // TA: 15.17 + on_event(btnComplete, 'click', function(event) { + detected_pointertypes[event.pointerType] = true; + test_touchaction.step(function() { + assert_equals(target0.scrollLeft, 0, "scroll x offset should be 0 in the end of the test"); + assert_not_equals(target0.scrollTop, 0, "scroll y offset should not be 0 in the end of the test"); + }); + test_touchaction.done(); + updateDescriptionComplete(); + }); + } + </script> + <h1>behaviour: pan-y</h1> + <div id="complete-notice"> + <p>The following pointer types were detected: <span id="pointertype-log"></span>.</p> + </div> + <div id="log"></div> + </body> +</html>
\ No newline at end of file diff --git a/dom/events/test/pointerevents/wpt/pointerevent_touch-action-pan-x-pan-y_touch-manual.html b/dom/events/test/pointerevents/wpt/pointerevent_touch-action-pan-x-pan-y_touch-manual.html new file mode 100644 index 0000000000..0c900ff740 --- /dev/null +++ b/dom/events/test/pointerevents/wpt/pointerevent_touch-action-pan-x-pan-y_touch-manual.html @@ -0,0 +1,126 @@ +<!doctype html> +<html> + <head> + <title>touch-action: pan-x pan-y</title> + <meta name="viewport" content="width=device-width"> + <link rel="stylesheet" type="text/css" href="pointerevent_styles.css"> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="pointerevent_support.js"></script> + <style> + #target0 { + width: 700px; + height: 430px; + touch-action: pan-x pan-y; + } + </style> + </head> + <body onload="run()"> + <h1>Pointer Events touch-action attribute support</h1> + <h4 id="desc">Test Description: Try to scroll text DOWN. Wait for description update. Expected: pan enabled</h4> + <p>Note: this test is for touch-devices only</p> + <div id="target0"> + <p> + Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diem + nonummy nibh euismod tincidunt ut lacreet dolore magna aliguam erat volutpat. + Ut wisis enim ad minim veniam, quis nostrud exerci tution ullamcorper suscipit + lobortis nisl ut aliquip ex ea commodo consequat. + </p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p> + Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diem + nonummy nibh euismod tincidunt ut lacreet dolore magna aliguam erat volutpat. + Ut wisis enim ad minim veniam, quis nostrud exerci tution ullamcorper suscipit + lobortis nisl ut aliquip ex ea commodo consequat. + </p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p> + Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diem + nonummy nibh euismod tincidunt ut lacreet dolore magna aliguam erat volutpat. + Ut wisis enim ad minim veniam, quis nostrud exerci tution ullamcorper suscipit + lobortis nisl ut aliquip ex ea commodo consequat. + </p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p> + Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diem + nonummy nibh euismod tincidunt ut lacreet dolore magna aliguam erat volutpat. + Ut wisis enim ad minim veniam, quis nostrud exerci tution ullamcorper suscipit + lobortis nisl ut aliquip ex ea commodo consequat. + </p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + </div> + <script type='text/javascript'> + var detected_pointertypes = {}; + + var xScrollIsReceived = false; + var yScrollIsReceived = false; + var xScr0, yScr0, xScr1, yScr1; + + add_completion_callback(showPointerTypes); + + function run() { + var target0 = document.getElementById("target0"); + + var test_touchaction = async_test("touch-action attribute test"); + + xScr0 = target0.scrollLeft; + yScr0 = target0.scrollTop; + + on_event(target0, 'pointerdown', function(event) { + detected_pointertypes[event.pointerType] = true; + }); + + on_event(target0, 'scroll', function(event) { + xScr1 = target0.scrollLeft; + yScr1 = target0.scrollTop; + + if(xScr1 != xScr0) { + xScrollIsReceived = true; + } + + if(yScr1 != yScr0) { + test_touchaction.step(function () { + yScrollIsReceived = true; + assert_true(true, "y-scroll received."); + }); + updateDescriptionNextStep(); + } + + if(xScrollIsReceived && yScrollIsReceived) { + test_touchaction.done(); + updateDescriptionComplete(); + } + }); + } + </script> + <h1>touch-action: pan-x pan-y</h1> + <div id="complete-notice"> + <p>The following pointer types were detected: <span id="pointertype-log"></span>.</p> + </div> + <div id="log"></div> + </body> +</html>
\ No newline at end of file diff --git a/dom/events/test/pointerevents/wpt/pointerevent_touch-action-pan-y-css_touch-manual.html b/dom/events/test/pointerevents/wpt/pointerevent_touch-action-pan-y-css_touch-manual.html new file mode 100644 index 0000000000..4ad39ecc83 --- /dev/null +++ b/dom/events/test/pointerevents/wpt/pointerevent_touch-action-pan-y-css_touch-manual.html @@ -0,0 +1,106 @@ +<!doctype html> +<html> + <head> + <title>touch-action: pan-y</title> + <meta name="assert" content="TA15.4 - With `touch-action: pan-y` on a swiped or click/dragged element, only panning in the y-axis should be possible."> + <meta name="viewport" content="width=device-width"> + <link rel="stylesheet" type="text/css" href="pointerevent_styles.css"> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="pointerevent_support.js"></script> + <style> + #target0 { + width: 700px; + height: 430px; + touch-action: pan-y; + } + </style> + </head> + <body onload="run()"> + <h1>Pointer Events touch-action attribute support</h1> + <h4 id="desc">Test Description: Try to scroll element DOWN then RIGHT. Tap Complete button under the rectangle when done. Expected: only pans in y direction.</h4> + <p>Note: this test is for touch-devices only</p> + <div id="target0"> + <p> + Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diem + nonummy nibh euismod tincidunt ut lacreet dolore magna aliguam erat volutpat. + Ut wisis enim ad minim veniam, quis nostrud exerci tution ullamcorper suscipit + lobortis nisl ut aliquip ex ea commodo consequat. + </p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p> + Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diem + nonummy nibh euismod tincidunt ut lacreet dolore magna aliguam erat volutpat. + Ut wisis enim ad minim veniam, quis nostrud exerci tution ullamcorper suscipit + lobortis nisl ut aliquip ex ea commodo consequat. + </p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p> + Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diem + nonummy nibh euismod tincidunt ut lacreet dolore magna aliguam erat volutpat. + Ut wisis enim ad minim veniam, quis nostrud exerci tution ullamcorper suscipit + lobortis nisl ut aliquip ex ea commodo consequat. + </p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p> + Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diem + nonummy nibh euismod tincidunt ut lacreet dolore magna aliguam erat volutpat. + Ut wisis enim ad minim veniam, quis nostrud exerci tution ullamcorper suscipit + lobortis nisl ut aliquip ex ea commodo consequat. + </p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + <p>Lorem ipsum dolor sit amet...</p> + </div> + <input type="button" id="btnComplete" value="Complete test"> + <script type='text/javascript'> + var detected_pointertypes = {}; + var test_touchaction = async_test("touch-action attribute test"); + add_completion_callback(showPointerTypes); + + function run() { + var target0 = document.getElementById("target0"); + var btnComplete = document.getElementById("btnComplete"); + + // Check if "touch-action: pan-y" attribute works properly + //TA: 15.4 + on_event(btnComplete, 'click', function(event) { + detected_pointertypes[event.pointerType] = true; + test_touchaction.step(function() { + assert_equals(target0.scrollLeft, 0, "scroll x offset should be 0 in the end of the test"); + assert_not_equals(target0.scrollTop, 0, "scroll y offset should not be 0 in the end of the test"); + }); + test_touchaction.done(); + updateDescriptionComplete(); + }); + } + </script> + <h1>touch-action: pan-y</h1> + <div id="complete-notice"> + <p>The following pointer types were detected: <span id="pointertype-log"></span>.</p> + </div> + <div id="log"></div> + </body> +</html>
\ No newline at end of file diff --git a/dom/events/test/pointerevents/wpt/pointerevent_touch-action-span-test_touch-manual.html b/dom/events/test/pointerevents/wpt/pointerevent_touch-action-span-test_touch-manual.html new file mode 100644 index 0000000000..61f0e8d329 --- /dev/null +++ b/dom/events/test/pointerevents/wpt/pointerevent_touch-action-span-test_touch-manual.html @@ -0,0 +1,114 @@ +<!doctype html> +<html> + <head> + <title>Span touch-action test</title> + <meta name="assert" content="TA15.18 - The touch-action CSS property applies to all elements except non-replaced inline elements." + <meta name="viewport" content="width=device-width"> + <link rel="stylesheet" type="text/css" href="pointerevent_styles.css"> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="pointerevent_support.js"></script> + <style> + #target0 { + height: 150px; + width: 200px; + overflow-y: auto; + background: black; + padding: 100px; + position: relative; + } + #testspan { + touch-action: none; + font-size: 72pt; + padding: 0px 0px 180px 0px; + border: 2px solid red; + } + </style> + </head> + <body onload="run()"> + <h2>Pointer Events touch-action attribute support</h2> + <h4 id="desc">Test Description: Try to scroll black element DOWN moving your touch outside of the red border. Wait for description update.</h4> + <p>Note: this test is for touch only</p> + <div id="target0"> + <span id="testspan"> + Test span + </span> + </div> + <input type="button" id="btnComplete" value="Complete test"> + + <script type='text/javascript'> + var detected_pointertypes = {}; + + var xScrollIsReceived = false; + var yScrollIsReceived = false; + var failScrollIsReceived = false; + var xScr0, yScr0, xScr1, yScr1; + var scrollReturnInterval = 500; + var isFirstPart = true; + setup({ explicit_timeout: true }); + add_completion_callback(showPointerTypes); + + function run() { + var target0 = document.getElementById("target0"); + var btnComplete = document.getElementById("btnComplete"); + + //TA 15.18 + var test_touchaction_div = async_test("touch-action attribute test out of element"); + var test_touchaction_span = async_test("touch-action attribute test in element"); + + xScr0 = target0.scrollLeft; + yScr0 = target0.scrollTop; + + on_event(btnComplete, 'click', function(event) { + test_touchaction_span.step(function() { + assert_not_equals(target0.scrollLeft, 0, "span scroll x offset should not be 0 in the end of the test"); + assert_not_equals(target0.scrollTop, 0, "span scroll y offset should not be 0 in the end of the test"); + assert_true(!isFirstPart, "target0 x and y scroll offsets should be greater than 0 after first two interactions (outside red border) respectively"); + }); + test_touchaction_span.done(); + updateDescriptionComplete(); + }); + + on_event(btnComplete, 'pointerdown', function(event) { + detected_pointertypes[event.pointerType] = true; + }); + + on_event(target0, 'scroll', function(event) { + if(isFirstPart) { + xScr1 = target0.scrollLeft; + yScr1 = target0.scrollTop; + + if(xScr1 != xScr0) { + xScrollIsReceived = true; + } + + if(yScr1 != yScr0) { + test_touchaction_div.step(function () { + yScrollIsReceived = true; + }); + updateDescriptionSecondStepTouchActionElement(target0, scrollReturnInterval); + } + + if(xScrollIsReceived && yScrollIsReceived) { + test_touchaction_div.done(); + updateDescriptionThirdStepTouchActionElement(target0, scrollReturnInterval, function () { + setTimeout(function() { + isFirstPart = false; + xScr0 = target0.scrollLeft; + xScr0 = target0.scrollLeft; + xScrollIsReceived = false; + yScrollIsReceived = false; + }, scrollReturnInterval); // avoid immediate triggering while scroll is still being performed + }); + } + } + }); + } + </script> + <h1>touch-action: none</h1> + <div id="complete-notice"> + <p>The following pointer types were detected: <span id="pointertype-log"></span>.</p> + </div> + <div id="log"></div> + </body> +</html>
\ No newline at end of file diff --git a/dom/events/test/pointerevents/wpt/pointerevent_touch-action-svg-test_touch-manual.html b/dom/events/test/pointerevents/wpt/pointerevent_touch-action-svg-test_touch-manual.html new file mode 100644 index 0000000000..e9dc9d78ee --- /dev/null +++ b/dom/events/test/pointerevents/wpt/pointerevent_touch-action-svg-test_touch-manual.html @@ -0,0 +1,122 @@ +<!doctype html> +<html> + <head> + <title>SVG test</title> + <meta name="viewport" content="width=device-width"> + <link rel="stylesheet" type="text/css" href="pointerevent_styles.css"> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="pointerevent_support.js"></script> + <style> + #target0 { + height: 350px; + width: 300px; + overflow-y: auto; + background: black; + padding: 100px; + position: relative; + } + </style> + </head> + <body onload="run()"> + <h2>Pointer Events touch-action attribute support</h2> + <h4 id="desc">Test Description: Try to scroll black element DOWN moving your touch outside of the red border. Wait for description update.</h4> + <p>Note: this test is for touch only</p> + <div id="target0"> + <svg id="testSvg" width="555" height="555" style="touch-action: none; border: 4px double red;"> + <circle cx="305" cy="305" r="250" stroke="green" stroke-width="4" fill="yellow" /> + Sorry, your browser does not support inline SVG. + </svg> + </div> + <br> + <input type="button" id="btnComplete" value="Complete test"> + <script type='text/javascript'> + var detected_pointertypes = {}; + var xScrollIsReceived = false; + var yScrollIsReceived = false; + var xScr0, yScr0, xScr1, yScr1; + var scrollReturnInterval = 1000; + var isFirstPart = true; + setup({ explicit_timeout: true }); + add_completion_callback(showPointerTypes); + + function run() { + var target0 = document.getElementById("target0"); + var btnComplete = document.getElementById("btnComplete"); + + var test_touchaction_div = async_test("touch-action attribute test out of SVG"); + var test_touchaction_svg = async_test("touch-action attribute test in SVG"); + + xScr0 = target0.scrollLeft; + yScr0 = target0.scrollTop; + + on_event(btnComplete, 'click', function(event) { + test_touchaction_svg.step(function() { + assert_equals(target0.scrollLeft, 0, "SVG scroll x offset should be 0 in the end of the test"); + assert_equals(target0.scrollTop, 0, "SVG scroll y offset should be 0 in the end of the test"); + }); + test_touchaction_svg.done(); + updateDescriptionComplete(); + }); + + on_event(btnComplete, 'pointerdown', function(event) { + detected_pointertypes[event.pointerType] = true; + }); + + on_event(target0, 'scroll', function(event) { + if(isFirstPart) { + xScr1 = target0.scrollLeft; + yScr1 = target0.scrollTop; + + if(xScr1 != xScr0) { + xScrollIsReceived = true; + } + + if(yScr1 != yScr0) { + test_touchaction_div.step(function () { + yScrollIsReceived = true; + assert_true(true, "y-scroll received."); + }); + updateDescriptionSecondStepSVG(); + } + + if(xScrollIsReceived && yScrollIsReceived) { + test_touchaction_div.done(); + updateDescriptionThirdStepSVG(); + setTimeout(function() { + isFirstPart = false; + }, 2 * scrollReturnInterval); + } + } + }); + } + + function updateDescriptionSecondStepSVG() { + window.setTimeout(function() { + objectScroller(target0, 'up', 0);} + , scrollReturnInterval); + document.getElementById('desc').innerHTML = "Test Description: Try to scroll element RIGHT moving your touch outside of the red border"; + } + + function updateDescriptionThirdStepSVG() { + window.setTimeout(function() { + objectScroller(target0, 'left', 0);} + , scrollReturnInterval); + document.getElementById('desc').innerHTML = "Test Description: Try to scroll element DOWN then RIGHT starting your touch inside of the circle. Tap Complete button under the rectangle when done"; + } + + function objectScroller(target, direction, value) { + if (direction == 'up') { + target.scrollTop = 0; + } else if (direction == 'left') { + target.scrollLeft = 0; + } + } + </script> + <h1>touch-action: none</h1> + <div id="complete-notice"> + <p>The following pointer types were detected: <span id="pointertype-log"></span>.</p> + </div> + <div id="log"></div> + </body> +</html> diff --git a/dom/events/test/pointerevents/wpt/pointerevent_touch-action-table-test_touch-manual.html b/dom/events/test/pointerevents/wpt/pointerevent_touch-action-table-test_touch-manual.html new file mode 100644 index 0000000000..17d5a29575 --- /dev/null +++ b/dom/events/test/pointerevents/wpt/pointerevent_touch-action-table-test_touch-manual.html @@ -0,0 +1,145 @@ +<!doctype html> +<html> + <head> + <title>Table touch-action test</title> + <meta name="assert" content="TA15.19 The touch-action CSS property applies to all elements except table rows, row groups, table columns, and column groups."> + <meta name="viewport" content="width=device-width"> + <link rel="stylesheet" type="text/css" href="pointerevent_styles.css"> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="pointerevent_support.js"></script> + <style> + #target0 { + height: 150px; + width: 200px; + overflow-y: auto; + background: black; + padding: 100px; + position: relative; + } + #testtable{ + color: white; + width: 350px; + padding: 0px 0px 200px 0px; + border: 2px solid green; + } + .testtd, .testth { + border: 2px solid green; + height: 80px; + } + #row1 { + touch-action: none; + } + #cell3 { + touch-action: none; + } + </style> + </head> + <body onload="run()"> + <h2>Pointer Events touch-action attribute support</h2> + <h4 id="desc">Test Description: Try to scroll element DOWN starting your touch over the 1st Row. Wait for description update.</h4> + <p>Note: this test is for touch only</p> + <div id="target0"> + <table id="testtable"> + <caption>The caption, first row element, and cell 3 have touch-action: none.</caption> + <tr id="row1"><th class="testth">Header 1 <td class="testtd">Cell 1 <td class="testtd">Cell 2</tr> + <tr id="row2"><th class="testth">Header 2 <td id="cell3" class="testtd">Cell 3 <td class="testtd">Cell 4</tr> + <tr id="row3"> <th class="testth">Header 3 <td class="testtd">Cell 5 <td class="testtd"> Cell 6</tr> + </table> + </div> + <br> + <input type="button" id="btnComplete" value="Complete test"> + + <script type='text/javascript'> + var detected_pointertypes = {}; + var xScrollIsReceived = false; + var yScrollIsReceived = false; + var xScr0, yScr0, xScr1, yScr1; + var scrollReturnInterval = 1000; + var isFirstPart = true; + setup({ explicit_timeout: true }); + add_completion_callback(showPointerTypes); + + function run() { + var target0 = document.getElementById("target0"); + var btnComplete = document.getElementById("btnComplete"); + + //TA 15.19 + var test_touchaction_cell = async_test("touch-action attribute test on the cell"); + var test_touchaction_row = async_test("touch-action attribute test on the row"); + + xScr0 = target0.scrollLeft; + yScr0 = target0.scrollTop; + + on_event(btnComplete, 'click', function(event) { + test_touchaction_cell.step(function() { + assert_equals(target0.scrollLeft, 0, "table scroll x offset should be 0 in the end of the test"); + assert_equals(target0.scrollTop, 0, "table scroll y offset should be 0 in the end of the test"); + assert_true(xScrollIsReceived && yScrollIsReceived, "target0 x and y scroll offsets should be greater than 0 after first two interactions (outside red border) respectively"); + }); + test_touchaction_cell.done(); + updateDescriptionComplete(); + }); + + on_event(btnComplete, 'pointerdown', function(event) { + detected_pointertypes[event.pointerType] = true; + }); + + on_event(target0, 'scroll', function(event) { + if(isFirstPart) { + xScr1 = target0.scrollLeft; + yScr1 = target0.scrollTop; + + if(xScr1 != xScr0) { + xScrollIsReceived = true; + } + + if(yScr1 != yScr0) { + test_touchaction_row.step(function () { + yScrollIsReceived = true; + }); + updateDescriptionSecondStepTable(target0, scrollReturnInterval); + } + + if(xScrollIsReceived && yScrollIsReceived) { + test_touchaction_row.done(); + updateDescriptionThirdStepTable(target0, scrollReturnInterval, function() { + setTimeout(function() { + isFirstPart = false; + }, scrollReturnInterval); // avoid immediate triggering while scroll is still being performed + }); + } + } + else { + test_touchaction_cell.step(failOnScroll, "scroll received while shouldn't"); + } + }); + } + + function updateDescriptionSecondStepTable(target, returnInterval, element) { + window.setTimeout(function() { + objectScroller(target, 'up', 0); + } + , returnInterval); + document.getElementById('desc').innerHTML = "Test Description: Try to scroll element RIGHT staring your touch over the Row 1"; + } + + function updateDescriptionThirdStepTable(target, returnInterval, callback = null) { + window.setTimeout(function() { + objectScroller(target, 'left', 0); + if (callback) { + callback(); + } + } + , returnInterval); + document.getElementById('desc').innerHTML = "Test Description: Try to scroll element DOWN then RIGHT starting your touch inside of the Cell 3"; + } + + </script> + <h1>touch-action: none</h1> + <div id="complete-notice"> + <p>The following pointer types were detected: <span id="pointertype-log"></span>.</p> + </div> + <div id="log"></div> + </body> +</html> diff --git a/dom/events/test/pointerevents/wpt/pointerevent_touch-action-verification.html b/dom/events/test/pointerevents/wpt/pointerevent_touch-action-verification.html new file mode 100644 index 0000000000..7800f2c9da --- /dev/null +++ b/dom/events/test/pointerevents/wpt/pointerevent_touch-action-verification.html @@ -0,0 +1,91 @@ +<!doctype html> +<html> + <head> + <title>touch-action: basic verification</title> + <meta name="assert" content="TA15.20 - The touch-action CSS property determines whether touch input MAY trigger default behavior supplied by the user agent. + auto: The user agent MAY determine any permitted touch behaviors, such as panning and zooming manipulations of the viewport, for touches that begin on the element. + none: Touches that begin on the element MUST NOT trigger default touch behaviors. + pan-x: The user agent MAY consider touches that begin on the element only for the purposes of horizontally scrolling the element's nearest ancestor with horizontally scrollable content. + pan-y: The user agent MAY consider touches that begin on the element only for the purposes of vertically scrolling the element's nearest ancestor with vertically scrollable content. + manipulation: The user agent MAY consider touches that begin on the element only for the purposes of scrolling and continuous zooming. Any additional behaviors supported by auto are out of scope for this specification."> + <meta name="viewport" content="width=device-width"> + <link rel="stylesheet" type="text/css" href="pointerevent_styles.css"> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="pointerevent_support.js"></script> + <style> + /* + Give some rules below something to override in order to test + that they really are being parsed + */ + .defnone { + touch-action: none; + } + </style> + </head> + <body onload="run()"> + <h2>Pointer Events touch-action attribute support</h2> + <h4 id="desc">Test Description: Test will automatically check parsing behaviour of various touch-action combinations.</h4> + <script type='text/javascript'> + var detected_pointertypes = {}; + + setup({ explicit_done: true }); + + function run() { + var tests = document.querySelectorAll('.test'); + //TA 15.20 + for (var i = 0; i < tests.length; i++) { + test(function() { + var style = window.getComputedStyle(tests[i]); + assert_equals(tests[i].attributes.expected.value, style.touchAction); + }, tests[i].id); + } + done(); + } + </script> + <h1>touch-action: basic verification</h1> + <div id="complete-notice"> + <p>The following pointer types were detected: <span id="pointertype-log"></span>.</p> + </div> + <div id="log"></div> + <div class="test" id="default" expected="auto"></div> + <div class="test defnone" id="stylesheet-none" expected="none"></div> + <div class="test defnone" id="explicit-auto" style="touch-action: auto;" expected="auto"></div> + <div class="test" id="explicit-pan-x" style="touch-action: pan-x;" expected="pan-x"></div> + <div class="test" id="explicit-pan-left" style="touch-action: pan-left;" expected="pan-left"></div> + <div class="test" id="explicit-pan-right" style="touch-action: pan-right;" expected="pan-right"></div> + <div class="test" id="explicit-pan-y" style="touch-action: pan-y;" expected="pan-y"></div> + <div class="test" id="explicit-pan-up" style="touch-action: pan-up;" expected="pan-up"></div> + <div class="test" id="explicit-pan-down" style="touch-action: pan-down;" expected="pan-down"></div> + <div class="test" id="explicit-pan-x-pan-y" style="touch-action: pan-x pan-y;" expected="pan-x pan-y"></div> + <div class="test" id="explicit-pan-y-pan-x" style="touch-action: pan-y pan-x;" expected="pan-x pan-y"></div> + <div class="test" id="explicit-pan-left-pan-up" style="touch-action: pan-left pan-up;" expected="pan-left pan-up"></div> + <div class="test" id="explicit-pan-left-pan-down" style="touch-action: pan-left pan-down;" expected="pan-left pan-down"></div> + <div class="test" id="explicit-pan-right-pan-up" style="touch-action: pan-right pan-up;" expected="pan-right pan-up"></div> + <div class="test" id="explicit-pan-right-pan-down" style="touch-action: pan-right pan-down;" expected="pan-right pan-down"></div> + <div class="test" id="explicit-pan-up-pan-left" style="touch-action: pan-up pan-left;" expected="pan-left pan-up"></div> + <div class="test" id="explicit-pan-up-pan-right" style="touch-action: pan-up pan-right;" expected="pan-right pan-up"></div> + <div class="test" id="explicit-pan-down-pan-left" style="touch-action: pan-down pan-left;" expected="pan-left pan-down"></div> + <div class="test" id="explicit-pan-down-pan-right" style="touch-action: pan-down pan-right;" expected="pan-right pan-down"></div> + <div class="test" id="explicit-manipulation" style="touch-action: manipulation;" expected="manipulation"></div> + <div class="test" id="explicit-none" style="touch-action: none;" expected="none"></div> + <div class="test" id="explicit-invalid-1" style="touch-action: bogus;" expected="auto"></div> + <div class="test defnone" id="explicit-invalid-2" style="touch-action: auto pan-x;" expected="none"></div> + <div class="test" id="explicit-invalid-3" style="touch-action: pan-y none;" expected="auto"></div> + <div class="test" id="explicit-invalid-4" style="touch-action: pan-x pan-x;" expected="auto"></div> + <div class="test" id="explicit-invalid-5" style="touch-action: manipulation pan-x;" expected="auto"></div> + <div class="test" id="explicit-invalid-6" style="touch-action: pan-x pan-left;" expected="auto"></div> + <div class="test" id="explicit-invalid-7" style="touch-action: auto pan-left;" expected="auto"></div> + <div class="test" id="explicit-invalid-8" style="touch-action: none pan-left;" expected="auto"></div> + <div class="test" id="explicit-invalid-9" style="touch-action: pan-x pan-right;" expected="auto"></div> + <div class="test" id="explicit-invalid-10" style="touch-action: pan-y pan-up;" expected="auto"></div> + <div class="test" id="explicit-invalid-11" style="touch-action: pan-y pan-down;" expected="auto"></div> + <div class="test" id="explicit-invalid-12" style="touch-action: pan-left pan-right;" expected="auto"></div> + <div class="test" id="explicit-invalid-13" style="touch-action: pan-up pan-down;" expected="auto"></div> + <div style="touch-action: none;"> + <div class="test" id="not-inherited" expected="auto"></div> + <div class="test" id="inherit" style="touch-action: inherit;" expected="none"></div> + </div> + <div class="test defnone" id="initial" style="touch-action: initial;" expected="auto"></div> + </body> +</html> diff --git a/dom/events/test/pointerevents/wpt/pointerlock/pointerevent_movementxy-manual.html b/dom/events/test/pointerevents/wpt/pointerlock/pointerevent_movementxy-manual.html new file mode 100644 index 0000000000..5b0edd3c61 --- /dev/null +++ b/dom/events/test/pointerevents/wpt/pointerlock/pointerevent_movementxy-manual.html @@ -0,0 +1,99 @@ +<!doctype html> +<html> + <head> + <title>Pointer Events properties tests</title> + <meta name="viewport" content="width=device-width"> + <link rel="stylesheet" type="text/css" href="../pointerevent_styles.css"> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <!-- Additional helper script for common checks across event types --> + <script type="text/javascript" src="../pointerevent_support.js"></script> + <style> + #testContainer { + touch-action: none; + user-select: none; + position: relative; + } + #box1 { + top: 30px; + left: 50px; + background: black; + } + #box2 { + top: 70px; + left: 250px; + background: red; + } + #innerFrame { + top: 10px; + left: 100px; + } + #square2 { + visibility: block; + } + </style> + <script> + var expectedPointerId = NaN; + var startSummation = false; + var lastScreenX = 0; + var lastScreenY = 0; + + function resetTestState() { + startSummation = false; + lastScreenX = 0; + lastScreenY = 0; + } + + function run() { + var test_pointerEvent = setup_pointerevent_test("pointerevent attributes", ['mouse', 'touch']); + + [document, document.getElementById('innerFrame').contentDocument].forEach(function(element) { + on_event(element, 'pointermove', function (event) { + if (startSummation) { + test_pointerEvent.step(function() { + assert_equals(event.movementX, event.screenX - lastScreenX, "movementX should be the delta between current event's and last event's screenX"); + assert_equals(event.movementY, event.screenY - lastScreenY, "movementY should be the delta between current event's and last event's screenY"); + }); + lastScreenX = event.screenX; + lastScreenY = event.screenY; + } + }); + }); + on_event(document.querySelector('#box1'), 'pointerdown', function(event) { + event.target.releasePointerCapture(event.pointerId); + test_pointerEvent.step(function() { + assert_equals(event.pointerType, expectedPointerType, "Use the instructed pointer type."); + }); + startSummation = true; + lastScreenX = event.screenX; + lastScreenY = event.screenY; + }); + on_event(document.querySelector('#box2'), 'pointerup', function(event) { + startSummation = false; + test_pointerEvent.done(); + }); + } + </script> + </head> + <body onload="run()"> + <h1>Pointer Events movementX/Y attribute test</h1> + <h2 id="pointerTypeDescription"></h2> + <h4> + Test Description: This test checks the properties of pointer events that do not support hover. + <ol> + <li>Press down on the black square.</li> + <li>Move your pointer slowly along a straight line to the red square.</li> + <li>Release the pointer when you are over the red square.</li> + </ol> + + Test passes if the proper behavior of the events is observed. + </h4> + <div id="testContainer"> + <div id="box1" class="square"></div> + <div id="box2" class="square"></div> + <iframe id="innerFrame" src="resources/pointerevent_movementxy-iframe.html"></iframe> + </div> + <div class="spacer"></div> + </body> +</html> + diff --git a/dom/events/test/pointerevents/wpt/pointerlock/resources/pointerevent_movementxy-iframe.html b/dom/events/test/pointerevents/wpt/pointerlock/resources/pointerevent_movementxy-iframe.html new file mode 100644 index 0000000000..627af3b61c --- /dev/null +++ b/dom/events/test/pointerevents/wpt/pointerlock/resources/pointerevent_movementxy-iframe.html @@ -0,0 +1,8 @@ +<!doctype html> +<html> + <head> + <meta name="viewport" content="width=device-width"> + </head> + <body> + </body> +</html> diff --git a/dom/events/test/pointerevents/wpt/resources/pointerevent_attributes_hoverable_pointers-iframe.html b/dom/events/test/pointerevents/wpt/resources/pointerevent_attributes_hoverable_pointers-iframe.html new file mode 100644 index 0000000000..5e55868282 --- /dev/null +++ b/dom/events/test/pointerevents/wpt/resources/pointerevent_attributes_hoverable_pointers-iframe.html @@ -0,0 +1,10 @@ +<!doctype html> +<html> + <head> + <meta name="viewport" content="width=device-width"> + <link rel="stylesheet" type="text/css" href="../pointerevent_styles.css"> + </head> + <body> + <div id="square2" class="square"></div> + </body> +</html> diff --git a/dom/events/test/pointerevents/wpt/resources/pointerevent_pointerId_scope-iframe.html b/dom/events/test/pointerevents/wpt/resources/pointerevent_pointerId_scope-iframe.html new file mode 100644 index 0000000000..ab33560b35 --- /dev/null +++ b/dom/events/test/pointerevents/wpt/resources/pointerevent_pointerId_scope-iframe.html @@ -0,0 +1,35 @@ +<!doctype html> +<html> + <!-- +Test cases for Pointer Events v1 spec +This document references Test Assertions (abbrev TA below) written by Cathy Chan +http://www.w3.org/wiki/PointerEvents/TestAssertions +--> + <head> + <title>Pointer Events pointerdown tests</title> + <meta name="viewport" content="width=device-width"> + <link rel="stylesheet" type="text/css" href="../pointerevent_styles.css"> + <script> + function run() { + var target1 = document.getElementById("target1"); + + var eventList = ['pointerenter', 'pointerover', 'pointermove', 'pointerout', 'pointerleave']; + + eventList.forEach(function(eventName) { + target1.addEventListener(eventName, function (event) { + var pass_data = { + 'pointerId' : event.pointerId, + 'type' : event.type, + 'pointerType' : event.pointerType + }; + top.postMessage(JSON.stringify(pass_data), "*"); + }); + }); + } + </script> + </head> + <body onload="run()"> + <div id="target1" class="touchActionNone"> + </div> + </body> +</html> diff --git a/dom/events/test/test_DataTransferItemList.html b/dom/events/test/test_DataTransferItemList.html new file mode 100644 index 0000000000..1d093c8cf7 --- /dev/null +++ b/dom/events/test/test_DataTransferItemList.html @@ -0,0 +1,233 @@ +<html> +<head> + <title>Tests for the DataTransferItemList object</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"> +</head> +<body style="height: 300px; overflow: auto;"> +<p id="display"> </p> +<img id="image" draggable="true" src="data:image/png,%89PNG%0D%0A%1A%0A%00%00%00%0DIHDR%00%00%00%18%00%00%00%18%02%03%00%00%00%9D%19%D5k%00%00%00%04gAMA%00%00%B1%8F%0B%FCa%05%00%00%00%0CPLTE%FF%FF%FF%FF%FF%FF%F7%DC%13%00%00%00%03%80%01X%00%00%00%01tRNS%08N%3DPT%00%00%00%01bKGD%00%88%05%1DH%00%00%00%09pHYs%00%00%0B%11%00%00%0B%11%01%7Fd_%91%00%00%00%07tIME%07%D2%05%0C%14%0C%0D%D8%3F%1FQ%00%00%00%5CIDATx%9C%7D%8E%CB%09%C0%20%10D%07r%B7%20%2F%E9wV0%15h%EA%D9%12D4%BB%C1x%CC%5C%1E%0C%CC%07%C0%9C0%9Dd7()%C0A%D3%8D%E0%B8%10%1DiCHM%D0%AC%D2d%C3M%F1%B4%E7%FF%10%0BY%AC%25%93%CD%CBF%B5%B2%C0%3Alh%CD%AE%13%DF%A5%F7%E0%03byW%09A%B4%F3%E2%00%00%00%00IEND%AEB%60%82"> +<div id="over" style="width: 100px; height: 100px; border: 2px black dashed;"> + drag over here +</div> + +<script> + function spin() { + // Defer to the event loop twice to wait for any events to be flushed out. + return new Promise(function(a) { + SimpleTest.executeSoon(function() { + SimpleTest.executeSoon(a) + }); + }); + } + + add_task(async function() { + await spin(); + var draggable = document.getElementById('image'); + var over = document.getElementById('over'); + + var dragstartFired = 0; + draggable.addEventListener('dragstart', onDragStart); + function onDragStart(e) { + draggable.removeEventListener('dragstart', onDragStart); + + var dt = e.dataTransfer; + dragstartFired++; + + ok(true, "dragStart event fired"); + var dtList = e.dataTransfer.items; + ok(dtList instanceof DataTransferItemList, + "DataTransfer.items returns a DataTransferItemList"); + + for (var i = 0; i < dtList.length; i++) { + var item = dtList[i]; + ok(item instanceof DataTransferItem, + "operator[] returns DataTransferItem objects"); + if (item.kind == "file") { + var file = item.getAsFile(); + ok(file instanceof File, "getAsFile() returns File objects"); + } + } + + dtList.clear(); + is(dtList.length, 0, "after .clear() DataTransferItemList should be empty"); + + dtList.add("this is some text", "text/plain"); + dtList.add("<a href='www.mozilla.org'>this is a link</a>", "text/html"); + dtList.add("http://www.mozilla.org", "text/uri-list"); + dtList.add("this is custom-data", "custom-data"); + + + var file = new File(['<a id="a"><b id="b">hey!</b></a>'], "myfile.html", + {type: "text/html"}); + + dtList.add(file); + + checkTypes(["text/plain", "text/html", "text/uri-list", "custom-data", "text/html"], + dtList, "DataTransferItemList.add test"); + + var files = e.dataTransfer.files; + is(files.length, 1, "DataTransfer.files should contain the one file we added earlier"); + is(files[0], file, "It should be the same file as the file we originally created"); + is(file, e.dataTransfer.mozGetDataAt("text/html", 1), + "It should be stored in index 1 for mozGetDataAt"); + + var file2 = new File(['<a id="c"><b id="d">yo!</b></a>'], "myotherfile.html", + {type: "text/html"}); + dtList.add(file2); + + todo(files.length == 2, "This test has chrome privileges, so the FileList objects aren't updated live"); + files = e.dataTransfer.files; + is(files.length, 2, "The files property should have been updated in place"); + is(files[1], file2, "It should be the same file as the file we originally created"); + is(file2, e.dataTransfer.mozGetDataAt("text/html", 2), + "It should be stored in index 2 for mozGetDataAt"); + + var oldLength = dtList.length; + var randomString = "foo!"; + e.dataTransfer.mozSetDataAt("random/string", randomString, 3); + is(oldLength, dtList.length, + "Adding a non-file entry to a non-zero index should not add an item to the items list"); + + var file3 = new File(['<a id="e"><b id="f">heya!</b></a>'], "yetanotherfile.html", + {type: "text/html"}); + e.dataTransfer.mozSetDataAt("random/string", file3, 3); + is(oldLength + 1, dtList.length, + "Replacing the entry with a file should add it to the list!"); + is(dtList[oldLength].getAsFile(), file3, "It should be stored in the last index as a file"); + is(dtList[oldLength].type, "text/html", "It should have the correct type"); + is(dtList[oldLength].kind, "file", "It should have the correct kind"); + + todo(files.length == 3, "This test has chrome privileges, so the FileList objects aren't updated live"); + files = e.dataTransfer.files; + is(files[files.length - 1], file3, "It should also be in the files list"); + + oldLength = dtList.length; + var nonstring = {}; + e.dataTransfer.mozSetDataAt("jsobject", nonstring, 0); + is(oldLength + 1, dtList.length, + "Adding a non-string object using the mozAPIs to index 0 should add an item to the dataTransfer"); + is(dtList[oldLength].type, "jsobject", "It should have the correct type"); + is(dtList[oldLength].kind, "other", "It should have the correct kind"); + + // Clear the event's data and get it set up so we can read it later! + dtList.clear(); + + dtList.add(file); + dtList.add("this is some text", "text/plain"); + is(e.dataTransfer.mozGetDataAt("text/html", 1), file); + } + + var getAsStringCalled = 0; + var dragenterFired = 0; + over.addEventListener('dragenter', onDragEnter); + function onDragEnter(e) { + over.removeEventListener('dragenter', onDragEnter); + + var dt = e.dataTransfer; + dragenterFired++; + + // NOTE: This test is run with chrome privileges. + // For back-compat reasons, protected mode acts like readonly mode for + // chrome documents. + readOnly(e); + } + + var dropFired = 0; + over.addEventListener('drop', onDrop); + function onDrop(e) { + over.removeEventListener('drop', onDrop); + + var dt = e.dataTransfer; + dropFired++; + e.preventDefault(); + + readOnly(e); + } + + + function readOnly(e) { + var dtList = e.dataTransfer.items; + var num = dtList.length; + + // .clear() should have no effect + dtList.clear(); + is(dtList.length, num, + ".clear() should have no effect on the object during a readOnly event"); + + // .remove(i) should throw InvalidStateError + for (var i = 0; i < dtList.length; i++) { + expectError(function() { dtList.remove(i); }, + "InvalidStateError", ".remove(" + i + ") during a readOnly event"); + } + + // .add() should return null and have no effect + var data = [["This is a plain string", "text/plain"], + ["This is <em>HTML!</em>", "text/html"], + ["http://www.mozilla.org/", "text/uri-list"], + ["this is some custom data", "custom-data"]]; + + for (var i = 0; i < data.length; i++) { + is(dtList.add(data[i][0], data[i][1]), null, + ".add() should return null during a readOnly event"); + + is(dtList.length, num, ".add() should have no effect during a readOnly event"); + } + + // .add() with a file should return null and have no effect + var file = new File(['<a id="a"><b id="b">hey!</b></a>'], "myfile.html", + {type: "text/html"}); + is(dtList.add(file), null, ".add() with a file should return null during a readOnly event"); + is(dtList.length, num, ".add() should have no effect during a readOnly event"); + + // We should be able to access the files + is(e.dataTransfer.files.length, 1, "Should be able to access files"); + ok(e.dataTransfer.files[0], "File should be the same file!"); + is(e.dataTransfer.items.length, 2, "Should be able to see there are 2 items"); + + is(e.dataTransfer.items[0].kind, "file", "First item should be a file"); + is(e.dataTransfer.items[1].kind, "string", "Second item should be a string"); + + is(e.dataTransfer.items[0].type, "text/html", "first item should be text/html"); + is(e.dataTransfer.items[1].type, "text/plain", "second item should be text/plain"); + + ok(e.dataTransfer.items[0].getAsFile(), "Should be able to get file"); + e.dataTransfer.items[1].getAsString(function(s) { + getAsStringCalled++; + is(s, "this is some text", "Should provide the correct string"); + }); + } + + synthesizeDrop(draggable, over, null, null); + + // Wait for the getAsString callbacks to complete + await spin(); + is(getAsStringCalled, 2, "getAsString should be called twice"); + + // Sanity-check to make sure that the events were actually run + is(dragstartFired, 1, "dragstart fired"); + is(dragenterFired, 1, "dragenter fired"); + is(dropFired, 1, "drop fired"); + }); + + function expectError(fn, eid, testid) { + var error = ""; + try { + fn(); + } catch (ex) { + error = ex.name; + } + is(error, eid, testid + " causes exception " + eid); + } + + function checkTypes(aExpectedList, aDtList, aTestid) { + is(aDtList.length, aExpectedList.length, aTestid + " length test"); + for (var i = 0; i < aExpectedList.length; i++) { + is(aDtList[i].type, aExpectedList[i], aTestid + " type " + i); + } + } +</script> + +</body> +</html> diff --git a/dom/events/test/test_accel_virtual_modifier.html b/dom/events/test/test_accel_virtual_modifier.html new file mode 100644 index 0000000000..6d98053fa2 --- /dev/null +++ b/dom/events/test/test_accel_virtual_modifier.html @@ -0,0 +1,90 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for DOM "Accel" virtual modifier</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> + +var kAccel = "Accel"; +var kAccelKeyCode = SpecialPowers.getIntPref("ui.key.accelKey"); + +var mouseEvent = new MouseEvent("mousedown", {}); +is(mouseEvent.getModifierState(kAccel), false, + "MouseEvent.getModifierState(\"" + kAccel + "\") should be false"); +mouseEvent = new MouseEvent("wheel", { accelKey: true}); +is(mouseEvent.getModifierState(kAccel), false, + "MouseEvent.getModifierState(\"" + kAccel + "\") should be false due to not supporting accelKey attribute"); +mouseEvent = new MouseEvent("mousedown", { ctrlKey: true }); +is(mouseEvent.getModifierState(kAccel), kAccelKeyCode == KeyboardEvent.DOM_VK_CONTROL, + "MouseEvent.getModifierState(\"" + kAccel + "\") should be true if ctrlKey is an accel modifier"); +mouseEvent = new MouseEvent("mousedown", { altKey: true }); +is(mouseEvent.getModifierState(kAccel), kAccelKeyCode == KeyboardEvent.DOM_VK_ALT, + "MouseEvent.getModifierState(\"" + kAccel + "\") should be true if altKey is an accel modifier"); +mouseEvent = new MouseEvent("mousedown", { metaKey: true }); +is(mouseEvent.getModifierState(kAccel), kAccelKeyCode == KeyboardEvent.DOM_VK_META, + "MouseEvent.getModifierState(\"" + kAccel + "\") should be true if metaKey is an accel modifier"); +mouseEvent = new MouseEvent("mousedown", { ctrlKey: true, altKey: true, metaKey: true }); +is(mouseEvent.getModifierState(kAccel), kAccelKeyCode == KeyboardEvent.DOM_VK_CONTROL || + kAccelKeyCode == KeyboardEvent.DOM_VK_ALT || + kAccelKeyCode == KeyboardEvent.DOM_VK_META, + "MouseEvent.getModifierState(\"" + kAccel + "\") should be true if one of ctrlKey, altKey or metaKey is an accel modifier"); + +var wheelEvent = new WheelEvent("wheel", {}); +is(wheelEvent.getModifierState(kAccel), false, + "WheelEvent.getModifierState(\"" + kAccel + "\") should be false"); +wheelEvent = new WheelEvent("wheel", { accelKey: true}); +is(wheelEvent.getModifierState(kAccel), false, + "WheelEvent.getModifierState(\"" + kAccel + "\") should be false due to not supporting accelKey attribute"); +wheelEvent = new WheelEvent("wheel", { ctrlKey: true }); +is(wheelEvent.getModifierState(kAccel), kAccelKeyCode == KeyboardEvent.DOM_VK_CONTROL, + "WheelEvent.getModifierState(\"" + kAccel + "\") should be true if ctrlKey is an accel modifier"); +wheelEvent = new WheelEvent("wheel", { altKey: true }); +is(wheelEvent.getModifierState(kAccel), kAccelKeyCode == KeyboardEvent.DOM_VK_ALT, + "WheelEvent.getModifierState(\"" + kAccel + "\") should be true if altKey is an accel modifier"); +wheelEvent = new WheelEvent("wheel", { metaKey: true }); +is(wheelEvent.getModifierState(kAccel), kAccelKeyCode == KeyboardEvent.DOM_VK_META, + "WheelEvent.getModifierState(\"" + kAccel + "\") should be true if metaKey is an accel modifier"); +wheelEvent = new WheelEvent("wheel", { ctrlKey: true, altKey: true, metaKey: true }); +is(wheelEvent.getModifierState(kAccel), kAccelKeyCode == KeyboardEvent.DOM_VK_CONTROL || + kAccelKeyCode == KeyboardEvent.DOM_VK_ALT || + kAccelKeyCode == KeyboardEvent.DOM_VK_META, + "WheelEvent.getModifierState(\"" + kAccel + "\") should be true if one of ctrlKey, altKey or metaKey is an accel modifier"); + +var keyboardEvent = new KeyboardEvent("keydown", {}); +is(keyboardEvent.getModifierState(kAccel), false, + "KeyboardEvent.getModifierState(\"" + kAccel + "\") should be false"); +keyboardEvent = new KeyboardEvent("keydown", { accelKey: true}); +is(keyboardEvent.getModifierState(kAccel), false, + "KeyboardEvent.getModifierState(\"" + kAccel + "\") should be false due to not supporting accelKey attribute"); +keyboardEvent = new KeyboardEvent("keydown", { ctrlKey: true }); +is(keyboardEvent.getModifierState(kAccel), kAccelKeyCode == KeyboardEvent.DOM_VK_CONTROL, + "KeyboardEvent.getModifierState(\"" + kAccel + "\") should be true if ctrlKey is an accel modifier"); +keyboardEvent = new KeyboardEvent("keydown", { altKey: true }); +is(keyboardEvent.getModifierState(kAccel), kAccelKeyCode == KeyboardEvent.DOM_VK_ALT, + "KeyboardEvent.getModifierState(\"" + kAccel + "\") should be true if altKey is an accel modifier"); +keyboardEvent = new KeyboardEvent("keydown", { metaKey: true }); +is(keyboardEvent.getModifierState(kAccel), kAccelKeyCode == KeyboardEvent.DOM_VK_META, + "KeyboardEvent.getModifierState(\"" + kAccel + "\") should be true if metaKey is an accel modifier"); +keyboardEvent = new KeyboardEvent("keydown", { ctrlKey: true, altKey: true, metaKey: true }); +is(keyboardEvent.getModifierState(kAccel), kAccelKeyCode == KeyboardEvent.DOM_VK_CONTROL || + kAccelKeyCode == KeyboardEvent.DOM_VK_ALT || + kAccelKeyCode == KeyboardEvent.DOM_VK_META, + "KeyboardEvent.getModifierState(\"" + kAccel + "\") should be true if one of ctrlKey, altKey or metaKey is an accel modifier"); + +// "Accel" virtual modifier must be supported with getModifierState(). So, any legacy init*Event()'s +// modifiers list argument shouldn't accept "Accel". +ok(typeof(KeyboardEvent.initKeyboardEvent) != "function", + "If we would support KeyboardEvent.initKeyboardEvent, we should test its modifier list argument doesn't accept \"" + kAccel + "\""); + +</script> +</pre> +</body> +</html> diff --git a/dom/events/test/test_accesskey.html b/dom/events/test/test_accesskey.html new file mode 100644 index 0000000000..cdfff54a28 --- /dev/null +++ b/dom/events/test/test_accesskey.html @@ -0,0 +1,160 @@ +<!DOCTYPE HTML> +<html> +<head> +<meta charset="utf-8"> +<title>Test for Accesskey</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<script src="/tests/SimpleTest/EventUtils.js"></script> +<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<button id="activation" accesskey="e">Should be activated</button> +<!-- Tests for label --> +<label id="label1" accesskey="a">Label 1</label><br> +<label id="label2" accesskey="a" for="checkbox2">Label 2</label><input type="checkbox" id="checkbox2" disabled>Checkbox 2</input><br> +<label id="label3" accesskey="a" for="checkbox3">Label 3</label><input type="checkbox" id="checkbox3">Checkbox 3</input><br> +<!-- Tests for button --> +<button id="button1" accesskey="b" style="display: none;">Button 1</button><br> +<button id="button2" accesskey="b">Button 2</button><br> +<button id="button3" accesskey="b" disabled>Button 3</button><br> +<button id="button4" accesskey="b">Button 4</button><br> +<!-- Tests for legend --> +<fieldset> +<legend accesskey="c">Legend 1</legend> +<input type="radio" id="radio1" style="display: none;"><label for="radio1">Radio 1</label><br> +<input type="radio" id="radio2" disabled><label for="radio2">Radio 2</label><br> +<input type="radio" id="radio3"><label for="radio3">Radio 3</label><br> +</fieldset> +<!-- Tests for legend2 --> +<fieldset> +<legend accesskey="d">Legend 2</legend> +<input type="radio" id="radio4" disabled><label for="radio4">Radio 4</label><br> +</fieldset> +<input type="text" id="text1" accesskey="d"><br> +<!-- Tests for bug 1723010 --> +<button id="button5" style="display:none" accesskey="1">Button 5</button> +<button id="button6" style="display:none" accesskey="2">Button 6</button> +<textarea id="textarea1" accesskey="2"></textarea> +<!-- Test for file input --> +<input type=file id="file" accesskey="f"> +<script> + +function performAccessKey(aKey) { + synthesizeKey(aKey, (navigator.platform.includes("Mac")) ? + { altKey : true, ctrlKey : true } : + { altKey : true, shiftKey: true }); +} + +add_setup(async function() { + // A workaround for bug 1726811. + await SpecialPowers.pushPrefEnv({ set: [[ "accessibility.tabfocus", 7 ]] }); +}); + +add_task(function activation() { + ok(!SpecialPowers.wrap(document).hasValidTransientUserGestureActivation, "Shouldn't be activated yet"); + performAccessKey("e"); + is(document.activeElement, document.getElementById("activation"), "Focus moved"); + ok(SpecialPowers.wrap(document).hasValidTransientUserGestureActivation, "Accesskey triggers activation"); +}); + +add_task(function label() { + let checkbox3 = document.getElementById("checkbox3"); + + performAccessKey("a"); + is(document.activeElement.id, checkbox3.id, `focus should move to ${checkbox3.id}`); + ok(!checkbox3.checked, `${checkbox3.id} should be still unchecked`); +}); + +add_task(function button() { + let button2 = document.getElementById("button2"); + let button4 = document.getElementById("button4"); + + [button2, button4].forEach(function(element) { + element.addEventListener("click", function() { + ok(false, `${element.id} should not be clicked`); + }); + }); + + performAccessKey("b"); + is(document.activeElement.id, button2.id, `focus should move to ${button2.id}`); + + performAccessKey("b"); + is(document.activeElement.id, button4.id, `focus should move to ${button4.id}`); +}); + +add_task(function legend() { + let radio3 = document.getElementById("radio3"); + + performAccessKey("c"); + is(document.activeElement.id, radio3.id, `focus should move to ${radio3.id}`); + ok(!radio3.checked, `${radio3.id} should be still unchecked`); +}); + +add_task(function legend2() { + let text1 = document.getElementById("text1"); + + performAccessKey("d"); + is(document.activeElement.id, text1.id, `focus should move to ${text1.id}`); +}); + +/** Test for Bug 1723010 **/ + +add_task(async function removeElement() { + let button5 = document.getElementById("button5"); + let textarea1 = document.getElementById("textarea1"); + let promise = new Promise((resolve) => { + button5.addEventListener("click", function() { + textarea1.remove(); + SimpleTest.executeSoon(() => { + ok(true, "should not crash"); + resolve(); + }); + }, { once: true }); + }); + + performAccessKey("1"); + await promise; +}); + +add_task(async function modifyAccessKey() { + let button5 = document.getElementById("button5"); + let button6 = document.getElementById("button6"); + let textarea1 = document.querySelector("textarea1"); + let promise = new Promise((resolve) => { + button5.addEventListener("click", function() { + button5.setAttribute("accesskey", "2"); + button6.setAttribute("accesskey", "1"); + SimpleTest.executeSoon(() => { + ok(true, "Button 5 should be clicked"); + resolve(); + }); + }, { once: true }); + + button6.addEventListener("click", function() { + ok(false, "Button 6 should not be clicked"); + }, { once: true }); + }); + + performAccessKey("1"); + await promise; +}); + +add_task(async function file_picker() { + const file = document.getElementById("file"); + const MockFilePicker = SpecialPowers.MockFilePicker; + MockFilePicker.init(window); + MockFilePicker.returnValue = MockFilePicker.returnCancel; + + let clicked = false; + file.addEventListener("click", function(e) { clicked = true; }); + + performAccessKey("f"); + ok(clicked, "Should've activated the picker"); + + MockFilePicker.reset(); +}); + +</script> +</body> +</html> diff --git a/dom/events/test/test_addEventListenerExtraArg.html b/dom/events/test/test_addEventListenerExtraArg.html new file mode 100644 index 0000000000..58b18545de --- /dev/null +++ b/dom/events/test/test_addEventListenerExtraArg.html @@ -0,0 +1,31 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=828554 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 828554</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript"> + + /** Test for Bug 828554 **/ + SimpleTest.waitForExplicitFinish(); + window.addEventListener("message", function() { + ok(true, "We got called"); + SimpleTest.finish(); + }, false, undefined); + window.postMessage("Hey there", "*"); + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=828554">Mozilla Bug 828554</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/dom/events/test/test_all_synthetic_events.html b/dom/events/test/test_all_synthetic_events.html new file mode 100644 index 0000000000..cd9eab3e33 --- /dev/null +++ b/dom/events/test/test_all_synthetic_events.html @@ -0,0 +1,479 @@ +<!DOCTYPE html> +<html> +<head> + <title>Test all synthetic events</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** + * kEventConstructors is a helper and database of all events. + * The sort order of the definition is by A to Z (ignore the Event postfix). + * + * XXX: should we move this into EventUtils.js? + * + * create: function or null. If this is null, it's impossible to create untrusted event for it. + * Otherwise, create(aName, aProps) returns an instance of the event initialized with aProps. + * aName specifies the event's type name. See each create() code for the detail of aProps. + */ +const kEventConstructors = { + Event: { create (aName, aProps) { + return new Event(aName, aProps); + }, + }, + AnimationEvent: { create (aName, aProps) { + return new AnimationEvent(aName, aProps); + }, + }, + AnimationPlaybackEvent: { create (aName, aProps) { + return new AnimationPlaybackEvent(aName, aProps); + }, + }, + AudioProcessingEvent: { create: null, // Cannot create untrusted event from JS. + }, + BeforeUnloadEvent: { create (aName, aProps) { + var e = document.createEvent("beforeunloadevent"); + e.initEvent(aName, aProps.bubbles, aProps.cancelable); + return e; + }, + }, + BlobEvent: { create (aName, aProps) { + return new BlobEvent(aName, { + data: new Blob([]), + }); + }, + }, + CallEvent: { create (aName, aProps) { + return new CallEvent(aName, aProps); + }, + }, + CallGroupErrorEvent: { create (aName, aProps) { + return new CallGroupErrorEvent(aName, aProps); + }, + }, + CFStateChangeEvent: { create (aName, aProps) { + return new CFStateChangeEvent(aName, aProps); + }, + }, + CloseEvent: { create (aName, aProps) { + return new CloseEvent(aName, aProps); + }, + }, + ClipboardEvent: { create (aName, aProps) { + return new ClipboardEvent(aName, aProps); + }, + }, + CompositionEvent: { create (aName, aProps) { + var e = document.createEvent("compositionevent"); + e.initCompositionEvent(aName, aProps.bubbles, aProps.cancelable, + aProps.view, aProps.data, aProps.locale); + return e; + }, + }, + ContentVisibilityAutoStateChangeEvent: { create (aName, aProps) { + return new ContentVisibilityAutoStateChangeEvent(aName, aProps); + }, + }, + CustomEvent: { create (aName, aProps) { + return new CustomEvent(aName, aProps); + }, + }, + DataErrorEvent: { create (aName, aProps) { + return new DataErrorEvent(aName, aProps); + }, + }, + DeviceLightEvent: { create (aName, aProps) { + return new DeviceLightEvent(aName, aProps); + }, + }, + DeviceMotionEvent: { create (aName, aProps) { + var e = document.createEvent("devicemotionevent"); + e.initDeviceMotionEvent(aName, aProps.bubbles, aProps.cancelable, aProps.acceleration, + aProps.accelerationIncludingGravity, aProps.rotationRate, + aProps.interval || 0.0); + return e; + }, + }, + DeviceOrientationEvent: { create (aName, aProps) { + return new DeviceOrientationEvent(aName, aProps); + }, + }, + DeviceProximityEvent: { create (aName, aProps) { + return new DeviceProximityEvent(aName, aProps); + }, + }, + DownloadEvent: { create (aName, aProps) { + return new DownloadEvent(aName, aProps); + }, + }, + DragEvent: { create (aName, aProps) { + var e = document.createEvent("dragevent"); + e.initDragEvent(aName, aProps.bubbles, aProps.cancelable, + aProps.view, aProps.detail, + aProps.screenX, aProps.screenY, + aProps.clientX, aProps.clientY, + aProps.ctrlKey, aProps.altKey, aProps.shiftKey, aProps.metaKey, + aProps.button, aProps.relatedTarget, aProps.dataTransfer); + return e; + }, + }, + ErrorEvent: { create (aName, aProps) { + return new ErrorEvent(aName, aProps); + }, + }, + FocusEvent: { create (aName, aProps) { + return new FocusEvent(aName, aProps); + }, + }, + FontFaceSetLoadEvent: { create (aName, aProps) { + return new FontFaceSetLoadEvent(aName, aProps); + }, + }, + FormDataEvent: { create (aName, aProps) { + return new FormDataEvent(aName, { + formData: new FormData() + }); + }, + }, + GamepadEvent: { create (aName, aProps) { + return new GamepadEvent(aName, aProps); + }, + }, + GamepadAxisMoveEvent: { create (aName, aProps) { + return new GamepadAxisMoveEvent(aName, aProps); + }, + }, + GamepadButtonEvent: { create (aName, aProps) { + return new GamepadButtonEvent(aName, aProps); + }, + }, + GPUUncapturedErrorEvent: { create: null, //TODO: constructor test + }, + HashChangeEvent: { create (aName, aProps) { + return new HashChangeEvent(aName, aProps); + }, + }, + IDBVersionChangeEvent: { create (aName, aProps) { + return new IDBVersionChangeEvent(aName, aProps); + }, + }, + ImageCaptureErrorEvent: { create (aName, aProps) { + return new ImageCaptureErrorEvent(aName, aProps); + }, + }, + InputEvent: { create (aName, aProps) { + return new InputEvent(aName, aProps); + }, + }, + KeyEvent: { create (aName, aProps) { + return new KeyboardEvent(aName, aProps); + }, + }, + KeyboardEvent: { create (aName, aProps) { + return new KeyboardEvent(aName, aProps); + }, + }, + MediaEncryptedEvent: { create (aName, aProps) { + return new MediaEncryptedEvent(aName, aProps); + }, + }, + MediaKeyMessageEvent: { create (aName, aProps) { + return new MediaKeyMessageEvent(aName, { + messageType: "license-request", + message: new ArrayBuffer(0) + }); + }, + }, + MediaQueryListEvent: { create (aName, aProps) { + return new MediaQueryListEvent(aName, aProps); + }, + }, + MediaRecorderErrorEvent: { create (aName, aProps) { + aProps.error = new DOMException(); + return new MediaRecorderErrorEvent(aName, aProps); + }, + }, + MediaStreamEvent: { create (aName, aProps) { + return new MediaStreamEvent(aName, aProps); + }, + }, + MediaStreamTrackEvent: { + // Difficult to test required arguments. + }, + MessageEvent: { create (aName, aProps) { + var e = new MessageEvent("messageevent", { bubbles: aProps.bubbles, + cancelable: aProps.cancelable, data: aProps.data, origin: aProps.origin, + lastEventId: aProps.lastEventId, source: aProps.source }); + return e; + }, + }, + MouseEvent: { create (aName, aProps) { + return new MouseEvent(aName, aProps); + }, + }, + MouseScrollEvent: { create: null + // Cannot create untrusted event from JS + }, + MozApplicationEvent: { create (aName, aProps) { + return new MozApplicationEvent(aName, aProps); + }, + }, + MozClirModeEvent: { create (aName, aProps) { + return new MozClirModeEvent(aName, aProps); + }, + }, + MozContactChangeEvent: { create (aName, aProps) { + return new MozContactChangeEvent(aName, aProps); + }, + }, + MozEmergencyCbModeEvent: { create (aName, aProps) { + return new MozEmergencyCbModeEvent(aName, aProps); + }, + }, + MozMessageDeletedEvent: { create (aName, aProps) { + return new MozMessageDeletedEvent(aName, aProps); + }, + }, + MozMmsEvent: { create (aName, aProps) { + return new MozMmsEvent(aName, aProps); + }, + }, + MozOtaStatusEvent: { create (aName, aProps) { + return new MozOtaStatusEvent(aName, aProps); + }, + }, + MozSmsEvent: { create (aName, aProps) { + return new MozSmsEvent(aName, aProps); + }, + }, + MozStkCommandEvent: { create (aName, aProps) { + return new MozStkCommandEvent(aName, aProps); + }, + }, + MutationEvent: { create (aName, aProps) { + var e = document.createEvent("mutationevent"); + e.initMutationEvent(aName, aProps.bubbles, aProps.cancelable, + aProps.relatedNode, aProps.prevValue, aProps.newValue, + aProps.attrName, aProps.attrChange); + return e; + }, + }, + OfflineAudioCompletionEvent: { create: "AudioContext" in self + ? function (aName, aProps) { + var ac = new AudioContext(); + var ab = new AudioBuffer({ length: 42, sampleRate: ac.sampleRate }); + aProps.renderedBuffer = ab; + return new OfflineAudioCompletionEvent(aName, aProps); + } + : null, + }, + PageTransitionEvent: { create (aName, aProps) { + return new PageTransitionEvent(aName, aProps); + }, + }, + PointerEvent: { create (aName, aProps) { + return new PointerEvent(aName, aProps); + }, + }, + PopStateEvent: { create (aName, aProps) { + return new PopStateEvent(aName, aProps); + }, + }, + PopupBlockedEvent: { create (aName, aProps) { + return new PopupBlockedEvent(aName, aProps); + }, + }, + ProgressEvent: { create (aName, aProps) { + return new ProgressEvent(aName, aProps); + }, + }, + PromiseRejectionEvent: { create (aName, aProps) { + aProps.promise = new Promise(() => {}); + return new PromiseRejectionEvent(aName, aProps); + }, + }, + RTCDataChannelEvent: { create (aName, aProps) { + let pc = new RTCPeerConnection(); + aProps.channel = pc.createDataChannel("foo"); + let e = new RTCDataChannelEvent(aName, aProps); + aProps.channel.close(); + pc.close(); + return e; + }, + }, + RTCDTMFToneChangeEvent: { create (aName, aProps) { + return new RTCDTMFToneChangeEvent(aName, aProps); + }, + }, + RTCPeerConnectionIceEvent: { create (aName, aProps) { + return new RTCPeerConnectionIceEvent(aName, aProps); + }, + }, + RTCTrackEvent: { + // Difficult to test required arguments. + }, + ScrollAreaEvent: { create: null + // Cannot create untrusted event from JS + }, + SpeechRecognitionError: { create (aName, aProps) { + return new SpeechRecognitionError(aName, aProps); + }, + }, + SpeechRecognitionEvent: { create (aName, aProps) { + return new SpeechRecognitionEvent(aName, aProps); + }, + }, + SpeechSynthesisErrorEvent: { create (aName, aProps) { + aProps.error = "synthesis-unavailable"; + aProps.utterance = new SpeechSynthesisUtterance("Hello World"); + return new SpeechSynthesisErrorEvent(aName, aProps); + }, + }, + SpeechSynthesisEvent: { create (aName, aProps) { + aProps.utterance = new SpeechSynthesisUtterance("Hello World"); + return new SpeechSynthesisEvent(aName, aProps); + }, + }, + StorageEvent: { create (aName, aProps) { + return new StorageEvent(aName, aProps); + }, + }, + StyleSheetApplicableStateChangeEvent: { create (aName, aProps) { + return new StyleSheetApplicableStateChangeEvent(aName, aProps); + }, + chromeOnly: true, + }, + SubmitEvent: { create (aName, aProps) { + return new SubmitEvent(aName, aProps); + }, + }, + TaskPriorityChangeEvent: { create (aName, aProps) { + aProps.previousPriority = "user-blocking"; + return new TaskPriorityChangeEvent(aName, aProps); + }, + }, + TCPSocketErrorEvent: { create(aName, aProps) { + return new TCPSocketErrorEvent(aName, aProps); + }, + }, + TCPSocketEvent: { create(aName, aProps) { + return new TCPSocketEvent(aName, aProps); + }, + }, + TCPServerSocketEvent: { create(aName, aProps) { + return new TCPServerSocketEvent(aName, aProps); + }, + }, + TimeEvent: { create: null + // Cannot create untrusted event from JS + }, + TouchEvent: { create (aName, aProps) { + var e = document.createEvent("touchevent"); + e.initTouchEvent(aName, aProps.bubbles, aProps.cancelable, + aProps.view, aProps.detail, + aProps.ctrlKey, aProps.altKey, aProps.shiftKey, aProps.metaKey, + aProps.touches, aProps.targetTouches, aProps.changedTouches); + return e; + }, + }, + TrackEvent: { create (aName, aProps) { + return new TrackEvent(aName, aProps); + }, + }, + TransitionEvent: { create (aName, aProps) { + return new TransitionEvent(aName, aProps); + }, + }, + UIEvent: { create (aName, aProps) { + return new UIEvent(aName, aProps); + }, + }, + UserProximityEvent: { create (aName, aProps) { + return new UserProximityEvent(aName, aProps); + }, + }, + USSDReceivedEvent: { create (aName, aProps) { + return new USSDReceivedEvent(aName, aProps); + }, + }, + VRDisplayEvent: { create: null, + // Required argument expects a VRDisplay that can not + // be created from Javascript without physical VR hardware + // connected. When Bug 1229480 lands, this test can be + // updated to use the puppet VR device. + }, + WheelEvent: { create (aName, aProps) { + return new WheelEvent(aName, aProps); + }, + }, + WebGLContextEvent: { create (aName, aProps) { + return new WebGLContextEvent(aName, aProps); + }, + }, + SecurityPolicyViolationEvent: { create (aName, aProps) { + return new SecurityPolicyViolationEvent(aName, aProps); + }, + }, +}; + +function test() { + for (var name of Object.keys(kEventConstructors)) { + if (!kEventConstructors[name].chromeOnly) { + continue; + } + if (window[name]) { + ok(false, name + " should be chrome only."); + } + window[name] = SpecialPowers.unwrap(SpecialPowers.wrap(window)[name]); + } + + var props = Object.getOwnPropertyNames(window); + for (var i = 0; i < props.length; i++) { + // Assume that event object must be named as "FooBarEvent". + if (!props[i].match(/^([A-Z][a-zA-Z]+)?Event$/)) { + continue; + } + if (!kEventConstructors[props[i]]) { + ok(false, "Unknown event found: " + props[i]); + continue; + } + if (!kEventConstructors[props[i]].create) { + todo(false, "Cannot create untrusted event of " + props[i]); + continue; + } + ok(true, "Creating " + props[i] + "..."); + var event = kEventConstructors[props[i]].create("foo", {}); + if (!event) { + ok(false, "Failed to create untrusted event: " + props[i]); + continue; + } + if (typeof(event.getModifierState) == "function") { + const kModifiers = [ "Shift", "Control", "Alt", "AltGr", "Meta", "CapsLock", "ScrollLock", "NumLock", "OS", "Fn", "FnLock", "Symbol", "SymbolLock" ]; + for (var j = 0; j < kModifiers.length; j++) { + ok(true, "Calling " + props[i] + ".getModifierState(" + kModifiers[j] + ")..."); + var modifierState = event.getModifierState(kModifiers[j]); + ok(true, props[i] + ".getModifierState(" + kModifiers[j] + ") = " + modifierState); + } + } + } +} + +SimpleTest.waitForExplicitFinish(); +SpecialPowers.pushPrefEnv( + {"set": [["dom.w3c_touch_events.legacy_apis.enabled", true], + ["layout.css.content-visibility.enabled", true]]}, + function() { + test(); + SimpleTest.finish(); + }); +</script> +</pre> +</body> +</html> diff --git a/dom/events/test/test_bug1003432.html b/dom/events/test/test_bug1003432.html new file mode 100644 index 0000000000..d74aefb886 --- /dev/null +++ b/dom/events/test/test_bug1003432.html @@ -0,0 +1,45 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1003432 +--> +<head> + <title>Test for Bug 1003432</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1003432">Mozilla Bug 1003432</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 1003432 **/ +// Test CustomEvent on worker +SimpleTest.waitForExplicitFinish(); +var worker = new Worker("test_bug1003432.js"); +ok(worker, "Should have worker!"); + +var count = 0; +worker.onmessage = function(evt) { + is(evt.data.type, "foobar", "Should get 'foobar' event!"); + is(evt.data.detail, "test", "Detail should be 'test'."); + ok(evt.data.bubbles, "Event should bubble!"); + ok(evt.data.cancelable, "Event should be cancelable."); + + // wait for test results of constructor and initCustomEvent + if (++count == 2) { + worker.terminate(); + SimpleTest.finish(); + } +}; + +worker.postMessage(""); + +</script> +</pre> +</body> +</html> diff --git a/dom/events/test/test_bug1003432.js b/dom/events/test/test_bug1003432.js new file mode 100644 index 0000000000..2f92ebf8da --- /dev/null +++ b/dom/events/test/test_bug1003432.js @@ -0,0 +1,31 @@ +addEventListener( + "foobar", + function (evt) { + postMessage({ + type: evt.type, + bubbles: evt.bubbles, + cancelable: evt.cancelable, + detail: evt.detail, + }); + }, + true +); + +addEventListener( + "message", + function (evt) { + // Test the constructor of CustomEvent + var e = new CustomEvent("foobar", { + bubbles: true, + cancelable: true, + detail: "test", + }); + dispatchEvent(e); + + // Test initCustomEvent + e = new CustomEvent("foobar"); + e.initCustomEvent("foobar", true, true, "test"); + dispatchEvent(e); + }, + true +); diff --git a/dom/events/test/test_bug1013412.html b/dom/events/test/test_bug1013412.html new file mode 100644 index 0000000000..03730b09b4 --- /dev/null +++ b/dom/events/test/test_bug1013412.html @@ -0,0 +1,116 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1013412 +https://bugzilla.mozilla.org/show_bug.cgi?id=1168182 +--> +<head> + <title>Test for Bug 1013412 and 1168182</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/paint_listener.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <style> + #content { + height: 800px; + overflow: scroll; + } + + #scroller { + height: 2000px; + background: repeating-linear-gradient(#EEE, #EEE 100px, #DDD 100px, #DDD 200px); + } + + #scrollbox { + margin-top: 200px; + width: 500px; + height: 500px; + border-radius: 250px; + box-shadow: inset 0 0 0 60px #555; + background: #777; + } + + #circle { + position: relative; + left: 240px; + top: 20px; + border: 10px solid white; + border-radius: 10px; + width: 0px; + height: 0px; + transform-origin: 10px 230px; + will-change: transform; + } + </style> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1013412">Mozilla Bug 1013412</a> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1168182">Mozilla Bug 1168182</a> +<p id="display"></p> +<div id="content"> + <p>Scrolling the page should be async and scrolling over the dark circle should scroll the page and avoid rotating the white ball.</p> + <div id="scroller"> + <div id="scrollbox"> + <div id="circle"></div> + </div> + </div> +</div> +<pre id="test"> +<script type="application/javascript"> + +var rotation = 0; +var rotationAdjusted = false; + +var incrementForMode = function (mode) { + switch (mode) { + case WheelEvent.DOM_DELTA_PIXEL: return 1; + case WheelEvent.DOM_DELTA_LINE: return 15; + case WheelEvent.DOM_DELTA_PAGE: return 400; + } + return 0; +}; + +document.getElementById("scrollbox").addEventListener("wheel", function (e) { + rotation += e.deltaY * incrementForMode(e.deltaMode) * 0.2; + document.getElementById("circle").style.transform = "rotate(" + rotation + "deg)"; + rotationAdjusted = true; + e.preventDefault(); +}); + +var iteration = 0; +function runTest() { + var content = document.getElementById('content'); + // enough iterations that we would scroll to the bottom of 'content' + if (iteration < 600 && content.scrollTop != content.scrollTopMax) { + iteration++; + sendWheelAndPaint(content, 100, 10, + { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaY: 1.0, lineOrPageDeltaY: 1 }, + runTest); + return; + } + var scrollbox = document.getElementById('scrollbox'); + is(content.scrollTop, content.scrollTopMax, "We should have scrolled to the bottom of the scrollframe"); + is(rotationAdjusted, false, "The rotation should not have been adjusted"); + SimpleTest.finish(); +} + +function startTest() { + // If we allow smooth scrolling the "smooth" scrolling may cause the page to + // glide past the scrollbox (which is supposed to stop the scrolling) and so + // we might end up at the bottom of the page. + SpecialPowers.pushPrefEnv({"set": [["general.smoothScroll", false], + ["test.events.async.enabled", true], + ["mousewheel.transaction.timeout", 100000], + ["dom.event.wheel-event-groups.enabled", true]]}, + runTest); +} + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(startTest, window); + +</script> +</pre> + +</body> +</html> diff --git a/dom/events/test/test_bug1017086_enable.html b/dom/events/test/test_bug1017086_enable.html new file mode 100644 index 0000000000..e7e6097c63 --- /dev/null +++ b/dom/events/test/test_bug1017086_enable.html @@ -0,0 +1,35 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1017086 +--> + <head> + <meta charset="utf-8"> + <title>Test for Bug 1017086</title> + <meta name="author" content="Maksim Lebedev" /> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="text/javascript"> + /** Test for Bug 1017086 **/ + function prepareTest() { + SimpleTest.waitForExplicitFinish(); + SimpleTest.executeSoon(startTest); + } + function startTest() { + var iframe = document.getElementById("testFrame"); + iframe.src = "bug1017086_inner.html"; + } + function part_of_checks(pointer_events, check, window, document, testelem) { + for(item in pointer_events) { check(true, pointer_events[item], window, "window"); } + /** TODO + for(item in pointer_events) { check(false, pointer_events[item], document, "document"); } + **/ + for(item in pointer_events) { check(true, pointer_events[item], testelem, "element"); } + SimpleTest.finish(); + } + </script> + </head> + <body onload="prepareTest()"> + <iframe id="testFrame" height="700" width="700"></iframe> + </body> +</html> diff --git a/dom/events/test/test_bug1037990.html b/dom/events/test/test_bug1037990.html new file mode 100644 index 0000000000..c148debcf7 --- /dev/null +++ b/dom/events/test/test_bug1037990.html @@ -0,0 +1,61 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1037990 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1037990</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1037990">Mozilla Bug 1037990</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +<script type="application/javascript"> + + /** Test for Bug 1037990 **/ + + var pre, node, detachedAccess, attachedAcess; + + node = document.createElement('a'); + node.href = 'http://example.org'; + node.accessKey = 'e'; + detachedAccess = node.accessKeyLabel; + info('[window.document] detached: ' + detachedAccess); + document.body.appendChild(node); + attachedAcess = node.accessKeyLabel; + info('[window.document] attached: ' + attachedAcess); + is(detachedAccess, attachedAcess, "Both values are same for the window.document"); + + var parser=new DOMParser(); + var xmlDoc=parser.parseFromString("<root></root>","text/xml"); + var nn = xmlDoc.createElementNS('http://www.w3.org/1999/xhtml','a'); + nn.setAttribute('accesskey','t') + detachedAccess = nn.accessKeyLabel; + info('[xmlDoc] detached: ' + detachedAccess); + var root = xmlDoc.getElementsByTagName('root')[0]; + root.appendChild(nn); + attachedAcess = nn.accessKeyLabel; + info('[xmlDoc] attached: ' + attachedAcess); + is(detachedAccess, attachedAcess, "Both values are same for the xmlDoc"); + + var myDoc = new Document(); + var newnode = myDoc.createElementNS('http://www.w3.org/1999/xhtml','a'); + newnode.href = 'http://example.org'; + newnode.accessKey = 'f'; + detachedAccess = newnode.accessKeyLabel; + info('[new document] detached: ' + detachedAccess); + myDoc.appendChild(newnode); + attachedAcess = newnode.accessKeyLabel; + info('[new document] attached: ' + attachedAcess); + is(detachedAccess, attachedAcess, "Both values are same for the new Document()"); + +</script> +</body> +</html> diff --git a/dom/events/test/test_bug1079236.html b/dom/events/test/test_bug1079236.html new file mode 100644 index 0000000000..4c89101574 --- /dev/null +++ b/dom/events/test/test_bug1079236.html @@ -0,0 +1,66 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1079236 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1079236</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript"> + + /** Test for Bug 1079236 **/ + + function runTests() { + var iframe = document.createElement('iframe'); + document.body.appendChild(iframe); + iframe.contentDocument.body.innerHTML = '<div id="content"></div>'; + + var c = iframe.contentDocument.getElementById("content"); + var sr = c.attachShadow({mode: 'open'}); + sr.innerHTML = "<input type='file'" + ">"; + var file = sr.firstChild; + is(file.type, "file"); + file.offsetLeft; // Flush layout because dispatching mouse events. + iframe.contentDocument.body.onmousemove = function(e) { + is(e.target, c, "Event target should be the element in non-Shadow DOM"); + if (e.originalTarget == file) { + is(e.originalTarget, file, + "type='file' implementation doesn't seem to have native anonymous content"); + } else { + var wrapped = SpecialPowers.wrap(e.originalTarget); + isnot(wrapped, file, "Shouldn't have the same event.target and event.originalTarget"); + } + + ok(!("composedTarget" in e), "Events shouldn't have composedTarget in non-chrome context!"); + e = SpecialPowers.wrap(e); + var composedTarget = SpecialPowers.unwrap(e.composedTarget); + is(composedTarget, file, "composedTarget should be the file object."); + + SimpleTest.finish(); + } + + var r = file.getBoundingClientRect(); + synthesizeMouse(file, r.width / 6, r.height / 2, { type: "mousemove"}, iframe.contentWindow); + iframe.contentDocument.body.onmousemove = null; + } + + SimpleTest.waitForExplicitFinish(); + window.onload = () => { + SimpleTest.waitForFocus(runTests); + }; + + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1079236">Mozilla Bug 1079236</a> +<p id="display"></p> +<div id="content"> + +</div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/dom/events/test/test_bug1127588.html b/dom/events/test/test_bug1127588.html new file mode 100644 index 0000000000..7d00a68eee --- /dev/null +++ b/dom/events/test/test_bug1127588.html @@ -0,0 +1,61 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1127588 +--> +<head> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1127588">Mozilla Bug 1127588</a> +<p id="display"></p> +<div id="content" style="display: none"> +<script type="application/javascript"> + +/** Test for Bug 1127588 **/ + +SimpleTest.waitForExplicitFinish(); + +window.onload = function () { + let insertedEventCount = 0; + let insertedListener = function() { + insertedEventCount++; + }; + + let removedEventCount = 0; + let removedListener = function() { + removedEventCount++; + }; + + // Tests for no title element. + document.addEventListener('DOMNodeRemoved', removedListener); + document.addEventListener('DOMNodeInserted', insertedListener); + document.title = "Test for Bug 1127588"; + document.removeEventListener('DOMNodeInserted', insertedListener); + document.removeEventListener('DOMNodeRemoved', removedListener); + + // Check result. + is(insertedEventCount, 2, "Should get 'DOMNodeInserted' mutation event"); + is(removedEventCount, 0, "Should not get 'DOMNodeRemoved' mutation event"); + + // Test for updating title element. + insertedEventCount = 0; + removedEventCount = 0; + document.addEventListener('DOMNodeRemoved', removedListener); + document.addEventListener('DOMNodeInserted', insertedListener); + document.title = document.title; + document.removeEventListener('DOMNodeInserted', insertedListener); + document.removeEventListener('DOMNodeRemoved', removedListener); + + // Check result. + is(insertedEventCount, 1, "Should get 'DOMNodeInserted' mutation event"); + is(removedEventCount, 1, "Should get 'DOMNodeRemoved' mutation event"); + + SimpleTest.finish(); +}; + +</script> +</body> +</html> + diff --git a/dom/events/test/test_bug1128787-1.html b/dom/events/test/test_bug1128787-1.html new file mode 100644 index 0000000000..f7f05523ab --- /dev/null +++ b/dom/events/test/test_bug1128787-1.html @@ -0,0 +1,52 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1128787 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1128787</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://global/skin"/> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/> + <script type="application/javascript"> + + /** Test for Bug 1128787 **/ + SimpleTest.waitForExplicitFinish(); + + window.onload = function (aEvent) { + var blurEventFired = false; + var input = document.getElementsByTagName("input")[0]; + input.addEventListener("blur", function (event) { + ok(true, "input element gets blur event correctly"); + + var utils = SpecialPowers.getDOMWindowUtils(window); + is(utils.IMEStatus, utils.IME_STATUS_ENABLED, "IME should be enabled"); + + SimpleTest.executeSoon(function () { + document.designMode = "off"; + + is(utils.IMEStatus, utils.IME_STATUS_DISABLED, "IME should be disabled"); + + SimpleTest.finish(); + }); + }, {once: true}); + document.designMode = "on"; + } + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1128787">Mozilla Bug 1128787</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<input type="button"/> +<script> +var input = document.getElementsByTagName("input")[0]; +input.focus(); +</script> +<pre id="test"> +</pre> +</body> +</html> diff --git a/dom/events/test/test_bug1128787-2.html b/dom/events/test/test_bug1128787-2.html new file mode 100644 index 0000000000..cb10ea431c --- /dev/null +++ b/dom/events/test/test_bug1128787-2.html @@ -0,0 +1,54 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1128787 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1128787</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://global/skin"/> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/> + <script type="application/javascript"> + + /** Test for Bug 1128787 **/ + SimpleTest.waitForExplicitFinish(); + + window.onload = function (aEvent) { + var blurEventFired = false; + var input = document.getElementsByTagName("input")[0]; + input.addEventListener("blur", function (event) { + ok(true, "input element gets blur event correctly"); + + var utils = SpecialPowers.getDOMWindowUtils(window); + is(utils.IMEStatus, utils.IME_STATUS_ENABLED, "IME should be enabled"); + + SimpleTest.executeSoon(function () { + document.designMode = "off"; + + // XXX Should be fixed. + todo_is(utils.IMEStatus, utils.IME_STATUS_DISABLED, "IME should be disabled"); + + SimpleTest.finish(); + }); + }, {once: true}); + document.designMode = "on"; + } + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1128787">Mozilla Bug 1128787</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<p contenteditable="true"></p> +<input type="button"/> +<script> +var input = document.getElementsByTagName("input")[0]; +input.focus(); +</script> +<pre id="test"> +</pre> +</body> +</html> diff --git a/dom/events/test/test_bug1128787-3.html b/dom/events/test/test_bug1128787-3.html new file mode 100644 index 0000000000..d70b67bfbe --- /dev/null +++ b/dom/events/test/test_bug1128787-3.html @@ -0,0 +1,53 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1128787 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1128787</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://global/skin"/> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/> + <script type="application/javascript"> + + /** Test for Bug 1128787 **/ + SimpleTest.waitForExplicitFinish(); + + window.onload = function (aEvent) { + var blurEventFired = false; + var input = document.getElementsByTagName("input")[0]; + input.addEventListener("blur", function (event) { + ok(true, "input element gets blur event correctly"); + + var utils = SpecialPowers.getDOMWindowUtils(window); + is(utils.IMEStatus, utils.IME_STATUS_ENABLED, "IME should be enabled"); + + is(document.designMode, "on", + "designMode should be \"on\" when blur event caused by enabling designMode is fired"); + document.designMode = "off"; + is(document.designMode, "off", + "designMode should become \"off\" even if it's reset by the blur event handler caused by enabling designMode"); + + is(utils.IMEStatus, utils.IME_STATUS_DISABLED, "IME should be disabled"); + SimpleTest.finish(); + }, {once: true}); + document.designMode = "on"; + } + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1128787">Mozilla Bug 1128787</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<input type="button"/> +<script> +var input = document.getElementsByTagName("input")[0]; +input.focus(); +</script> +<pre id="test"> +</pre> +</body> +</html> diff --git a/dom/events/test/test_bug1145910.html b/dom/events/test/test_bug1145910.html new file mode 100644 index 0000000000..2f7942937c --- /dev/null +++ b/dom/events/test/test_bug1145910.html @@ -0,0 +1,54 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1145910 +--> +<head> + <title>Test for Bug 1145910</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<script type="application/javascript"> + +/** Test for Bug 1145910 **/ + +function runTests() { + + var iframe = document.createElement('iframe'); + document.body.appendChild(iframe); + iframe.contentDocument.body.innerHTML = + '<style> div:active { color: rgb(0, 255, 0); } </style> <div id="host">Foo</div>'; + + var host = iframe.contentDocument.getElementById("host"); + var shadow = host.attachShadow({mode: 'open'}); + shadow.innerHTML = '<style>div:active { color: rgb(0, 255, 0); }</style><div id="inner">Bar</div>'; + var inner = shadow.getElementById("inner"); + var iframeWin = iframe.contentWindow; + + is(iframeWin.getComputedStyle(host).color, "rgb(0, 0, 0)", "The host should not be active"); + is(iframeWin.getComputedStyle(inner).color, "rgb(0, 0, 0)", "The div inside the shadow root should not be active."); + + synthesizeMouseAtCenter(host, { type: "mousedown" }, iframeWin); + + is(iframeWin.getComputedStyle(inner).color, "rgb(0, 255, 0)", "Div inside shadow root should be active."); + is(iframeWin.getComputedStyle(host).color, "rgb(0, 255, 0)", "Host should be active when the inner div is made active."); + + synthesizeMouseAtCenter(host, { type: "mouseup" }, iframeWin); + + is(iframeWin.getComputedStyle(inner).color, "rgb(0, 0, 0)", "Div inside shadow root should no longer be active."); + is(iframeWin.getComputedStyle(host).color, "rgb(0, 0, 0)", "Host should no longer be active."); + + SimpleTest.finish(); +}; + +SimpleTest.waitForExplicitFinish(); +window.onload = () => { + SimpleTest.waitForFocus(runTests); +}; + +</script> +</body> +</html> diff --git a/dom/events/test/test_bug1150308.html b/dom/events/test/test_bug1150308.html new file mode 100644 index 0000000000..e9f9b480de --- /dev/null +++ b/dom/events/test/test_bug1150308.html @@ -0,0 +1,49 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1150308 +--> +<head> + <title>Test for Bug 1150308</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<script type="application/javascript"> + +/** Test for Bug 1150308 **/ + +function runTests() { + var iframe = document.createElement('iframe'); + document.body.appendChild(iframe); + iframe.contentDocument.body.innerHTML = + '<div id="host"><span id="distributeme">Foo</span></div>'; + + var host = iframe.contentDocument.getElementById("host"); + var shadow = host.attachShadow({mode: 'open'}); + shadow.innerHTML = '<style>.bar:active { color: rgb(0, 255, 0); }</style><div class="bar" id="inner"><slot></slot></div>'; + var inner = shadow.getElementById("inner"); + var distributed = iframe.contentDocument.getElementById("distributeme"); + var iframeWin = iframe.contentWindow; + + is(iframeWin.getComputedStyle(inner).color, "rgb(0, 0, 0)", "The div inside the shadow root should not be active."); + + synthesizeMouseAtCenter(distributed, { type: "mousedown" }, iframeWin); + + is(iframeWin.getComputedStyle(inner).color, "rgb(0, 255, 0)", "Div inside shadow root should be active."); + + synthesizeMouseAtCenter(distributed, { type: "mouseup" }, iframeWin); + + is(iframeWin.getComputedStyle(inner).color, "rgb(0, 0, 0)", "Div inside shadow root should no longer be active."); + + SimpleTest.finish(); +}; + +SimpleTest.waitForExplicitFinish(); +window.onload = () => { + SimpleTest.waitForFocus(runTests); +}; +</script> +</body> +</html> diff --git a/dom/events/test/test_bug1248459.html b/dom/events/test/test_bug1248459.html new file mode 100644 index 0000000000..40aa162f7e --- /dev/null +++ b/dom/events/test/test_bug1248459.html @@ -0,0 +1,58 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1248459 +--> +<head> + <title>Test for Bug 1248459</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<input id="input" value="foo"> +<div id="div">bar</div> +<script type="application/javascript"> + +/** Test for Bug 1248459 **/ +/** + * The bug occurs when a piece of text outside of the editor's root element is + * somehow selected when the editor is focused. In the bug's case, it's the + * placeholder anonymous div that's selected. In this test's case, it's a + * document div that's selected. + */ +SimpleTest.waitForExplicitFinish(); + +SimpleTest.waitForFocus(function() { + var div = document.getElementById("div"); + var input = document.getElementById("input"); + + input.appendChild(div); + input.focus(); + + var editor = SpecialPowers.wrap(input).editor; + var sel = editor.selection; + + sel.selectAllChildren(editor.rootElement); + var result = synthesizeQuerySelectedText(); + + ok(result.succeeded, "Query selected text should succeed"); + is(result.offset, 0, "Selected text should be at offset 0"); + is(result.text, "foo", "Selected text should match"); + + var range = document.createRange(); + range.selectNode(div); + + sel.removeAllRanges(); + sel.addRange(range); + + result = synthesizeQuerySelectedText(); + + ok(!result.succeeded, "Query out-of-bounds selection should fail"); + + SimpleTest.finish(); +}); + +</script> +</body> +</html> diff --git a/dom/events/test/test_bug1264380.html b/dom/events/test/test_bug1264380.html new file mode 100644 index 0000000000..9349e9712f --- /dev/null +++ b/dom/events/test/test_bug1264380.html @@ -0,0 +1,82 @@ +<html> +<head> + <title>Test the dragstart event on the anchor in side shadow DOM</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> +<script> + +async function runTests() +{ + let dragService = SpecialPowers.Cc["@mozilla.org/widget/dragservice;1"]. + getService(SpecialPowers.Ci.nsIDragService); + + let iframe = document.querySelector("iframe"); + let iframeDoc = iframe.contentDocument; + let iframeWin = iframe.contentWindow; + + let shadow = iframeDoc.querySelector('#outer').attachShadow({mode: 'open'}); + + let target = iframeDoc.createElement('a'); + target.textContent = "Drag me if you can!"; + const URL = "http://www.mozilla.org/"; + target.href = URL; + shadow.appendChild(target); + + // Some of the drag data we don't actually care about for this test, + // so we'll use this comparator function to ignore them. + function ignoreFunc(actualData, expectedData) { + return true; + } + + const EXPECTED_DRAG_DATA = [[{ + type: "text/x-moz-url", + data: "", + eqTest: ignoreFunc, + }, { + type: "text/x-moz-url-data", + data: "", + eqTest: ignoreFunc, + }, { + type: "text/x-moz-url-desc", + data: "", + eqTest: ignoreFunc, + }, { + type: "text/uri-list", + data: URL, + }, { + type: "text/_moz_htmlinfo", + data: "", + eqTest: ignoreFunc, + }, { + type: "text/html", + data: "", + eqTest: ignoreFunc, + }, { + type: "text/plain", + data: URL, + }]]; + + let result = await synthesizePlainDragAndCancel( + { + srcElement: target, + srcWindow: iframeWin, + finalY: -10, // Avoid clicking the link + }, + EXPECTED_DRAG_DATA); + ok(result === true, "Should have gotten the expected drag data."); + SimpleTest.finish(); +} + + +SimpleTest.waitForExplicitFinish(); +window.onload = () => { + SimpleTest.waitForFocus(runTests); +}; + +</script> + +<body> + <iframe srcdoc='<div id="outer"/>'></iframe> +</body> +</html> diff --git a/dom/events/test/test_bug1298970.html b/dom/events/test/test_bug1298970.html new file mode 100644 index 0000000000..dc7c15a9d6 --- /dev/null +++ b/dom/events/test/test_bug1298970.html @@ -0,0 +1,33 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1298970 +--> +<head> + <title>Test for Bug 1298970</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1298970">Mozilla Bug 1298970</a> +<p id="display"></p> +<div id="inner"></div> +<script class="testbody" type="text/javascript"> + +/** Test for Bug 1298970 **/ +var target = document.getElementById("inner"); +var event = new Event("test", { bubbles: true, cancelable: true }); + +is(event.cancelBubble, false, "Event.cancelBubble should be false by default"); + +target.addEventListener("test", (e) => { + e.stopPropagation(); + is(e.cancelBubble, true, "Event.cancelBubble should be true after stopPropagation"); +}, true); + +target.dispatchEvent(event); + +</script> +</body> +</html> + diff --git a/dom/events/test/test_bug1304044.html b/dom/events/test/test_bug1304044.html new file mode 100644 index 0000000000..39eb39df15 --- /dev/null +++ b/dom/events/test/test_bug1304044.html @@ -0,0 +1,133 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1304044 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1304044</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript"> + var eventsFired = []; + var target; + var eventsExpected; + + function GetNodeString(node) { + if (node == window) + return "window"; + if (node == document) + return "document"; + if (node.id) + return node.id; + if (node.nodeName) + return node.nodeName; + return node; + } + + function TargetAndListener(listener, targetInner) { + this.listener = listener; + this.target = targetInner; + } + + TargetAndListener.prototype.toString = function() { + var targetName = GetNodeString(this.target); + var listenerName = GetNodeString(this.listener); + return "(listener: " + listenerName + ", target: " + targetName + ")"; + } + + var tests = [ + TestAuxClickBubblesForEventListener, + TestAuxClickBubblesForOnAuxClick, + ]; + + function CompareEvents(evt, expected) { + return evt && expected && evt.listener == expected.listener && + evt.target == expected.target; + } + + function ResetEventsFired() { + eventsFired = []; + } + + function ClearEventListeners() { + for (i in arguments) { + arguments[i].removeEventListener("auxclick", log_event); + } + } + + function ClickTarget(tgt) { + synthesizeMouseAtCenter(tgt, {type : "mousedown", button: 2}, window); + synthesizeMouseAtCenter(tgt, {type : "mouseup", button: 2}, window); + } + + function log_event(e) { + eventsFired[eventsFired.length] = new TargetAndListener(this, e.target); + } + + function CompareEventsToExpected(expected, actual) { + for (var i = 0; i < expected.length || i < actual.length; i++) { + ok(CompareEvents(actual[i], expected[i]), + "Auxclick receiver's don't match: TargetAndListener " + + i + ": Expected: " + expected[i] + ", Actual: " + actual[i]); + } + } + + function TestAuxClickBubblesForEventListener() { + target.addEventListener("auxclick", log_event); + document.addEventListener("auxclick", log_event); + window.addEventListener("auxclick", log_event); + + ClickTarget(target) + CompareEventsToExpected(eventsExpected, eventsFired); + ResetEventsFired(); + ClearEventListeners(target, document, window); + } + + function TestAuxClickBubblesForOnAuxClick() { + target.onauxclick = log_event; + document.onauxclick = log_event; + window.onauxclick = log_event; + + ClickTarget(target); + CompareEventsToExpected(eventsExpected, eventsFired); + ResetEventsFired(); + } + + function RunTests(){ + for (var i = 0; i < tests.length; i++) { + tests[i](); + } + } + + function Begin() { + target = document.getElementById("target"); + eventsExpected = [ + new TargetAndListener(target, target), + new TargetAndListener(document, target), + new TargetAndListener(window, target), + ]; + RunTests(); + target.remove(); + SimpleTest.finish(); + } + + window.onload = function() { + SimpleTest.waitForExplicitFinish(); + SimpleTest.executeSoon(Begin); + } + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1304044">Mozilla Bug 1304044</a> +<p id="display"> + <div id="target">Target</div> +</p> +<div id="content" style:"display:none"> + +</div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/dom/events/test/test_bug1305458.html b/dom/events/test/test_bug1305458.html new file mode 100644 index 0000000000..a595502892 --- /dev/null +++ b/dom/events/test/test_bug1305458.html @@ -0,0 +1,50 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1305458 +--> +<head> + <title>Test for Bug 1305458</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <style> + input[type=number] { + appearance: textfield; + } + input[type=number]:focus, + input[type=number]:hover { + appearance: auto; + } + </style> +</head> +<body onload="doTest()"> + <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1305458">Mozilla Bug 1305458</a> + <input id="test_input" type="number"> + <div id="test_div">bar</div> + <script> + SimpleTest.waitForExplicitFinish(); + var change_count = 0; + function doTest() { + let input = document.getElementById("test_input"); + let div = document.getElementById("test_div"); + input.addEventListener("change", () => { + ++change_count; + }); + // mouse hover + input.focus(); + synthesizeMouse(input, 1, 1, {type: "mousemove"}); + sendString("1"); + input.blur(); + is(change_count, 1, "input should fire change when blur"); + + input.focus(); + synthesizeMouse(div, 1, 1, {type: "mousemove"}); + sendString("1"); + input.blur(); + is(change_count, 2, "input should fire change when blur"); + SimpleTest.finish(); + } + </script> +</body> +</html> diff --git a/dom/events/test/test_bug1327798.html b/dom/events/test/test_bug1327798.html new file mode 100644 index 0000000000..ac903654f1 --- /dev/null +++ b/dom/events/test/test_bug1327798.html @@ -0,0 +1,47 @@ +<!DOCTYPE html> +<html> +<head> +<title>Test for bug 1327798</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<script src="/tests/SimpleTest/EventUtils.js"></script> +<link rel="stylesheet" href="/tests/SimpleTest/test.css"> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?=id=1327798">Mozilla Bug 1327798</a> +<p id="display"></p> +<div id="content" style="display: none;"></div> + +<div contenteditable="true" id="editable1"><b>Formatted Text</b><br></div> +<pre> +<script class="testbody" type="application/javascript"> +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(() => { + var editable = document.getElementById("editable1"); + editable.focus(); + + window.getSelection().selectAllChildren(editable1); + + SpecialPowers.doCommand(window, "cmd_copy"); + + //--------- now check the content of the clipboard + var clipboard = SpecialPowers.Cc["@mozilla.org/widget/clipboard;1"] + .getService(SpecialPowers.Ci.nsIClipboard); + // does the clipboard contain text/plain data ? + ok(clipboard.hasDataMatchingFlavors(["text/plain"], clipboard.kGlobalClipboard), + "clipboard contains unicode text"); + // does the clipboard contain text/html data ? + ok(clipboard.hasDataMatchingFlavors(["text/html"], clipboard.kGlobalClipboard), + "clipboard contains html text"); + + window.addEventListener("paste", (e) => { + is(e.clipboardData.types.indexOf('text/html'), -1, "clipboardData shouldn't have text/html"); + is(e.clipboardData.getData('text/plain'), "Formatted Text", "getData(text/plain) should return plain text"); + SimpleTest.finish(); + }); + + SpecialPowers.doCommand(window, "cmd_pasteNoFormatting"); +}); +</script> +</pre> +</body> +</html> diff --git a/dom/events/test/test_bug1332699.html b/dom/events/test/test_bug1332699.html new file mode 100644 index 0000000000..c2a858d8ad --- /dev/null +++ b/dom/events/test/test_bug1332699.html @@ -0,0 +1,37 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>Test for bug 1332699</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<script src="/tests/SimpleTest/EventUtils.js"></script> +<link rel="stylesheet" href="/tests/SimpleTest/test.css"> +<style> +#test { + color: red; + transition: color 100ms; +} +#test.changed { + color: green; +} +</style> +<div id="test"></div> +<script> +SimpleTest.waitForExplicitFinish(); + +window.onload = function () { + let $test = document.getElementById('test'); + is(getComputedStyle($test).color, 'rgb(255, 0, 0)', + 'color should be red before transition'); + let numEvents = 0; + $test.addEventListener('webkitTransitionEnd', function() { + ++numEvents; + if (numEvents == 1) { + is(getComputedStyle($test).color, 'rgb(0, 128, 0)', + 'color should be green after transition'); + $test.dispatchEvent(new TransitionEvent('transitionend')); + is(numEvents, 1, "Shouldn't receive the prefixed event again"); + SimpleTest.finish(); + } + }); + $test.className = 'changed'; +}; +</script> diff --git a/dom/events/test/test_bug1339758.html b/dom/events/test/test_bug1339758.html new file mode 100644 index 0000000000..0a437f57de --- /dev/null +++ b/dom/events/test/test_bug1339758.html @@ -0,0 +1,80 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1339758 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1339758</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript"> + + /** Test for Bug 1339758 **/ + var expectNonZeroCoordinates = false; + SimpleTest.waitForExplicitFinish(); + + function testCoordinates(e) { + var coordinateProperties = + [ "screenX", + "screenY", + "clientX", + "clientY", + "offsetX", + "offsetY", + "movementX", + "movementY", + "layerX", + "layerY", + "x", + "y" ]; + for (var i in coordinateProperties) { + if (e[coordinateProperties[i]] != 0) { + ok(expectNonZeroCoordinates, e.target.id + " got at least some non-zero coordinate property: " + i); + return; + } + } + ok(!expectNonZeroCoordinates, "Non-zero coordinates weren't expected"); + } + + function runTests() { + info("Testing click events which should have only 0 coordinates."); + document.getElementById("div_target").click(); + document.getElementById("a_target").focus(); + sendKey("RETURN"); + document.getElementById("input_target").focus(); + sendKey("RETURN"); + + info("Testing click events which should have also non-zero coordinates."); + expectNonZeroCoordinates = true; + // Test script created MouseEvents + sendMouseEvent({ type: "click"}, document.getElementById("a_target")); + sendMouseEvent({ type: "click"}, document.getElementById("input_target")); + + // Test widget level mouse events + synthesizeMouse(document.getElementById("a_target"), 2, 2, {}); + synthesizeMouse(document.getElementById("input_target"), 2, 2, {}); + SimpleTest.finish(); + } + + SimpleTest.waitForFocus(runTests); + + + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1339758">Mozilla Bug 1339758</a> +<p id="display"></p> + +<div id="div_target" onclick="testCoordinates(event)"> </div> +<a href="#" id="a_target" onclick="testCoordinates(event);">test link</a><br> +<input type="button" id="input_target" onclick="testCoordinates(event);" value="test button"> + +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/dom/events/test/test_bug1369072.html b/dom/events/test/test_bug1369072.html new file mode 100644 index 0000000000..9f117c4902 --- /dev/null +++ b/dom/events/test/test_bug1369072.html @@ -0,0 +1,37 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1369072 +--> +<head> + <title>Test for Bug 1369072</title> + <script type="text/javascript" src="/MochiKit/MochiKit.js"></script> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1369072">Mozilla Bug 1369072</a> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +/** Test for Bug 1369072 **/ + +SimpleTest.waitForExplicitFinish(); + +var subWin = window.open("window_bug1369072.html", "_blank", + "width=500,height=500"); + +function finish() +{ + subWin.close(); + SimpleTest.finish(); +} + +</script> +</pre> +</body> +</html> diff --git a/dom/events/test/test_bug1412775.xhtml b/dom/events/test/test_bug1412775.xhtml new file mode 100644 index 0000000000..3146a489f1 --- /dev/null +++ b/dom/events/test/test_bug1412775.xhtml @@ -0,0 +1,66 @@ +<?xml version="1.0"?> +<?xml-stylesheet type="text/css" href="chrome://global/skin"?> +<?xml-stylesheet type="text/css" href="/tests/SimpleTest/test.css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1412775 +--> +<window title="Mozilla Bug 1412775" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="init()"> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script src="chrome://mochikit/content/chrome-harness.js"></script> + + <!-- test code goes here --> + <script type="application/javascript"> + <![CDATA[ + + /** Test for Bug 1412775 **/ + var win; + function init() { + SimpleTest.waitForExplicitFinish(); + win = window.browsingContext.topChromeWindow.open("window_bug1412775.xhtml", "_new", "chrome"); + win.onload = function() { + var b = win.document.getElementById("browser"); + var d = b.contentWindow.document; + var e = new d.defaultView.Event("foo"); + var didCallChromeSide = false; + var didCallContentSide = false; + /* eslint-disable-next-line no-shadow */ + b.addEventListener("foo", function(e) { + didCallChromeSide = true; + var path = e.composedPath(); + var mm = d.defaultView.docShell.messageManager; + is(path.length, 5, "Should have 5 items in composedPath in chrome."); + is(path[0], mm, "BrowserChildGlobal is the chrome handler."); + is(path[1], b, "browser element should be in the path."); + is(path[2], b.parentNode, "window element should be in the path."); + is(path[3], win.document, "Document object should be in the path."); + is(path[4], win, "Window object should be in the path."); + }, true, true); + /* eslint-disable-next-line no-shadow */ + d.addEventListener("foo", function(e) { + didCallContentSide = true;; + var path = e.composedPath(); + is(path.length, 4, "Should have 4 items in composedPath in content."); + is(path[0], d.body, "body element should be in the path."); + is(path[1], d.documentElement, "html element should be in the path."); + is(path[2], d, "Document object should be in the path."); + is(path[3], d.defaultView, "Window object should be in the path."); + }, true, true); + d.body.dispatchEvent(e); + ok(didCallChromeSide, "didCallChromeSide"); + ok(didCallContentSide, "didCallContentSide"); + win.close(); + SimpleTest.finish(); + } + } + + ]]> + </script> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1412775" + target="_blank">Mozilla Bug 1412775</a> + </body> +</window> diff --git a/dom/events/test/test_bug1429572.html b/dom/events/test/test_bug1429572.html new file mode 100644 index 0000000000..d793a44fd9 --- /dev/null +++ b/dom/events/test/test_bug1429572.html @@ -0,0 +1,43 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1429572 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1429572</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript"> + + /** Test for Bug 1429572 **/ + SimpleTest.waitForExplicitFinish(); + + var win; + function start() { + SpecialPowers.pushPrefEnv({"set": [["dom.w3c_touch_events.enabled", 1], + ["dom.w3c_touch_events.legacy_apis.enabled", true]]}, + function() { + ok(true, "Starting the test."); + win = window.open("window_bug1429572.html", "testwindow", + "width=" + window.screen.width + + ",height=" + window.screen.height); + }); + } + + function done() { + setTimeout("SimpleTest.finish();"); + } + + </script> +</head> +<body onload="start();"> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1429572">Mozilla Bug 1429572</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/dom/events/test/test_bug1446834.html b/dom/events/test/test_bug1446834.html new file mode 100644 index 0000000000..495f9b4b3c --- /dev/null +++ b/dom/events/test/test_bug1446834.html @@ -0,0 +1,33 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1446834 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1446834</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript"> + + /** Test for Bug 1446834 **/ + + SimpleTest.waitForExplicitFinish(); + + window.onload = function() { + document.getElementById("iframe").src = "file_bug1446834.html"; + } + + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1446834">Mozilla Bug 1446834</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +<iframe id="iframe"></iframe> +</body> +</html> diff --git a/dom/events/test/test_bug1447993.html b/dom/events/test/test_bug1447993.html new file mode 100644 index 0000000000..e0b4144248 --- /dev/null +++ b/dom/events/test/test_bug1447993.html @@ -0,0 +1,41 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1447993 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1447993</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript"> + + /** Test for Bug 1447993 **/ + SimpleTest.waitForExplicitFinish(); + + var win; + function start() { + SpecialPowers.pushPrefEnv({"set": [["dom.w3c_touch_events.enabled", 1]]}, + function() { + win = window.open("window_bug1447993.html", "testwindow", + "width=" + window.screen.width + + ",height=" + window.screen.height); + }); + } + + function done() { + setTimeout("SimpleTest.finish();"); + } + + </script> +</head> +<body onload="start();"> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1447993">Mozilla Bug 1447993</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/dom/events/test/test_bug1484371.html b/dom/events/test/test_bug1484371.html new file mode 100644 index 0000000000..c1b2a31217 --- /dev/null +++ b/dom/events/test/test_bug1484371.html @@ -0,0 +1,27 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1484371 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1484371</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript"> + + /** Test for Bug 1484371 **/ + + SimpleTest.waitForExplicitFinish(); + + window.onload = function() { + document.getElementById("iframe").src = "file_bug1484371.html"; + } + + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1484371">Mozilla Bug 1484371</a> +<iframe id="iframe"></iframe> +</body> +</html> diff --git a/dom/events/test/test_bug1534562.html b/dom/events/test/test_bug1534562.html new file mode 100644 index 0000000000..7ac6c31dd7 --- /dev/null +++ b/dom/events/test/test_bug1534562.html @@ -0,0 +1,51 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1534562 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1534562</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript"> + + /** Test for Bug 1534562 **/ + + function runTest() { + var host = document.getElementById("host"); + var shadow = host.attachShadow({mode: 'open'}); + var shadowDiv = document.createElement('div'); + shadowDiv.style.cssText = "height: 100%; width: 100%"; + shadowDiv.onpointerdown = function (e) { + shadowDiv.setPointerCapture(e.pointerId); + }; + var shadowDivGotPointerMove = false; + shadowDiv.onpointermove = function(e) { + shadowDivGotPointerMove = true; + } + shadow.appendChild(shadowDiv); + host.offsetLeft; // Flush layout. + + synthesizeMouseAtCenter(shadowDiv, { type: "mousedown" }); + synthesizeMouseAtCenter(document.getElementById("lightDOM"), { type: "mousemove" }); + ok(shadowDivGotPointerMove, "shadowDiv should have got pointermove event."); + synthesizeMouseAtCenter(document.getElementById("lightDOM"), { type: "mouseup" }); + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + SimpleTest.waitForFocus(runTest); + + + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1534562">Mozilla Bug 1534562</a> +<div id="host" style="height: 50px; width: 50px;"> +</div> +<div id="lightDOM" style="height: 50px; width: 50px;"> +</div> +</body> +</html> diff --git a/dom/events/test/test_bug1539497.html b/dom/events/test/test_bug1539497.html new file mode 100644 index 0000000000..4f49f1a044 --- /dev/null +++ b/dom/events/test/test_bug1539497.html @@ -0,0 +1,29 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>bug 1539497</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> + <script> + SimpleTest.waitForExplicitFinish(); + function runTest() { + var win = window.open("about:blank"); + win.onload = function() { + is(win.navigator.maxTouchPoints, 5, "Should have max touch points"); + win.close(); + SimpleTest.finish(); + } + } + function init() { + SpecialPowers.pushPrefEnv( + {"set": [["dom.maxtouchpoints.testing.value", 5]]}, runTest); + } + </script> +</head> +<body onload="init()"> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +</body> +</html> diff --git a/dom/events/test/test_bug1581192.html b/dom/events/test/test_bug1581192.html new file mode 100644 index 0000000000..3a3a2ab322 --- /dev/null +++ b/dom/events/test/test_bug1581192.html @@ -0,0 +1,73 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Redispatching test with PresShell</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<button>click me!</button> +<script> +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(async () => { + /** + * We have same tests in Event-dispatch-redispatch.html of WPT. However, + * it does not send the event to the main process. Therefore the reported + * crash couldn't reproduce. + */ + await SpecialPowers.pushPrefEnv({set: [["test.events.async.enabled", true]]}); + let button = document.querySelector("button"); + try { + await promiseElementReadyForUserInput(button, window, info); + } catch (ex) { + ok(false, ex.message); + SimpleTest.finish(); + return; + } + let mouseupEvent; + button.addEventListener("mouseup", aNativeMouseUpEvent => { + ok(aNativeMouseUpEvent.isTrusted,"First mouseup event should be trusted"); + mouseupEvent = aNativeMouseUpEvent; + try { + button.dispatchEvent(aNativeMouseUpEvent); + ok(false, "Dispatching trusted mouseup event which is being dispatched should throw an exception"); + } catch (e) { + is(e.name, "InvalidStateError", "Trusted mouseup event which is being dispatched shouldn't be able to be dispatched"); + } + }, {once: true}); + + button.addEventListener("click", aNativeClickEvent => { + ok(aNativeClickEvent.isTrusted, "First click event should be trusted"); + try { + button.dispatchEvent(aNativeClickEvent); + ok(false, "Dispatching trusted click event which is being dispatched should throw an exception"); + } catch (e) { + is(e.name, "InvalidStateError", "Trusted click event which is being dispatched shouldn't be able to be dispatched"); + } + let mouseupEventFired = false; + button.addEventListener("mouseup", aDispatchedMouseUpEvent => { + ok(!aDispatchedMouseUpEvent.isTrusted, "Redispatched mouseup event shouldn't be trusted"); + mouseupEventFired = true; + }, {once: true}); + function onClick(aNonDispatchedClickEvent) { + ok(false, "Redispatched mouseup event shouldn't cause dispatching another click event"); + } + button.addEventListener("click", onClick); + ok(mouseupEvent.isTrusted, "Received mouseup event should be trusted before redispatching from click event listener"); + button.dispatchEvent(mouseupEvent); + ok(!mouseupEvent.isTrusted, "Received mouseup event shouldn't be trusted after redispatching"); + ok(mouseupEventFired, "Redispatched mouseup event should've been received"); + button.removeEventListener("click", onClick); + ok(aNativeClickEvent.isTrusted, "First click event should still be trusted even after redispatching mouseup event"); + SimpleTest.finish(); + }, {once: true}); + synthesizeMouseAtCenter(button, {}); +}); +</script> +</body> +</html> diff --git a/dom/events/test/test_bug1635018.html b/dom/events/test/test_bug1635018.html new file mode 100644 index 0000000000..ebfbce4abe --- /dev/null +++ b/dom/events/test/test_bug1635018.html @@ -0,0 +1,27 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>Test for Bug 1709832</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<script src="/tests/SimpleTest/EventUtils.js"></script> +<link rel="stylesheet" href="/tests/SimpleTest/test.css" /> +<div id="container"> + <span id="start" draggable="true">start</span><span id="dest" draggable="true">dest</span> +</div> +<script> + SimpleTest.waitForExplicitFinish(); + + SpecialPowers.pushPrefEnv({ + set: [["dom.event.dragexit.enabled", false]] + }).then(() => { + start.addEventListener("dragexit", ev => { + ok(false, "dragexit should not fire at non-chrome element") + }); + start.addEventListener("dragleave", ev => { + ok(true, "got dragleave") + SimpleTest.finish(); + }); + + sendDragEvent({ type: "dragover" }, start); + sendDragEvent({ type: "dragover" }, dest); + }); +</script> diff --git a/dom/events/test/test_bug1637259.html b/dom/events/test/test_bug1637259.html new file mode 100644 index 0000000000..e6e0a4bd38 --- /dev/null +++ b/dom/events/test/test_bug1637259.html @@ -0,0 +1,71 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>Bug 1692277</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<script src="/tests/gfx/layers/apz/test/mochitest/apz_test_utils.js"></script> +<script src="/tests/gfx/layers/apz/test/mochitest/apz_test_native_event_utils.js"></script> +<link rel="stylesheet" href="/tests/SimpleTest/test.css" /> + +<style> + #container { + width: 100px; + height: 100px; + overflow: scroll; + } + + #child { + width: 200px; + height: 200px; + } +</style> + +<div id="container"> + <div id="child"></div> +</div> + +<script> + /** + * @template {keyof HTMLElementEventMap} K + * @param {HTMLElemnt} target + * @param {K} eventName + * @return {HTMLElementEventMap[K]} + */ + function waitForEvent(target, eventName) { + return new Promise(resolve => { + target.addEventListener(eventName, resolve, { once: true }); + }); + } + + add_task(async function testPenDrag() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.w3c_pointer_events.dispatch_by_pointer_messages", true], + ["dom.w3c_pointer_events.scroll_by_pen.enabled", true], + ], + }); + + await SimpleTest.promiseFocus(); + const container = document.getElementById("container"); + const scrollPromise = waitForEvent(container, "scroll"); + const pointerPromise = waitForEvent(container, "pointerdown"); + await promiseNativePointerDrag(container, "pen", 50, 50, -50, -50, 0, { + pressure: 0.25, + tiltX: 40, + tiltY: 50, + twist: 80, + }); + await scrollPromise; + + const pointerdown = await pointerPromise; + is(pointerdown.pointerType, "pen"); + is(pointerdown.type, "pointerdown", ".type"); + is(pointerdown.button, 0, ".button"); + is(pointerdown.buttons, 1, ".buttons"); + is(pointerdown.layerX, 50, ".layerX"); + is(pointerdown.layerY, 50, ".layerY"); + is(pointerdown.pressure, 0.25, ".pressure"); + is(pointerdown.tiltX, 40, ".tiltX"); + is(pointerdown.tiltY, 50, ".tiltY"); + is(pointerdown.twist, 80, ".twist"); + }); +</script> diff --git a/dom/events/test/test_bug1673434.html b/dom/events/test/test_bug1673434.html new file mode 100644 index 0000000000..f9bb09c69e --- /dev/null +++ b/dom/events/test/test_bug1673434.html @@ -0,0 +1,75 @@ +<!DOCTYPE html>
+<html>
+<!--
+bugzilla.mozilla.org/show_bug.cgi?id=1673434
+-->
+<head>
+<title>Test for bug 1673434</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src="/tests/SimpleTest/EventUtils.js"></script>
+<link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1673434">Mozilla Bug 1673434</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+<input type="checkbox">
+<input type="radio" name="group" value="foo">
+<input type="radio" name="group" value="bar" checked>
+<input type="text">
+<script>
+const utils = SpecialPowers.DOMWindowUtils;
+
+function test_events(element, resolve) {
+ element.addEventListener("input", () => {
+ is(utils.isHandlingUserInput, false,
+ "isHandlingUserInput is false on input event by element.click()");
+ }, { once: true });
+ element.addEventListener("change", () => {
+ is(utils.isHandlingUserInput, false,
+ "isHandlingUserInput is false on change event by element.click()");
+ resolve();
+ }, { once: true });
+
+ element.click();
+}
+
+add_task(function testCheckboxEvent() {
+ return new Promise(resolve => {
+ let element = document.querySelector("input[type=checkbox]");
+ test_events(element, resolve);
+ });
+});
+
+add_task(function testRadioEvent() {
+ return new Promise(resolve => {
+ let element = document.querySelector("input[type=radio]");
+ test_events(element, resolve);
+ });
+});
+
+add_task(function testUserInput() {
+ // setUserInput should be handled as user input.
+ //
+ // XXX <textarea> won't fire input event by setUserInput.
+ return new Promise(resolve => {
+ let element = document.querySelector("input[type=text]");
+ element.addEventListener("input", () => {
+ is(utils.isHandlingUserInput, true,
+ "isHandlingUserInput is true on input event by setUserInput");
+ }, { once: true });
+ element.addEventListener("change", () => {
+ is(utils.isHandlingUserInput, true,
+ "isHandlingUserInput is true on change event by setUserInput");
+ resolve();
+ }, { once: true });
+
+ SpecialPowers.wrap(element).setUserInput("a");
+ });
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/events/test/test_bug1681800.html b/dom/events/test/test_bug1681800.html new file mode 100644 index 0000000000..f9e73fb397 --- /dev/null +++ b/dom/events/test/test_bug1681800.html @@ -0,0 +1,26 @@ +<!DOCTYPE html> +<html> +<!-- +bugzilla.mozilla.org/show_bug.cgi?id=1681800 +--> +<head> +<title>Test for bug 1681800</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<link rel="stylesheet" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1681800">Mozilla Bug 1681800</a> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +<script> +add_task(function() { + document.documentElement.focus({}) + document.documentElement.focus({}) + ok(true, "no crash"); +}) +</script> +</pre> +</body> +</html> diff --git a/dom/events/test/test_bug1686716.html b/dom/events/test/test_bug1686716.html new file mode 100644 index 0000000000..8779413a1a --- /dev/null +++ b/dom/events/test/test_bug1686716.html @@ -0,0 +1,28 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>bug 1686716</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> + <script> + SimpleTest.waitForExplicitFinish(); + function test() { + var ifr = document.getElementsByTagName("iframe")[0]; + ifr.contentWindow.addEventListener("drop", + function(event) { + ifr.remove(); + event.preventDefault(); + }); + sendDragEvent({type: "drop"}, ifr.contentDocument.body, ifr.contentWindow); + ok(true, "Should not crash."); + SimpleTest.finish(); + } + </script> +</head> +<body onload="test()"> +<iframe></iframe> +<p id="display"></p> +</body> +</html> diff --git a/dom/events/test/test_bug1692052.html b/dom/events/test/test_bug1692052.html new file mode 100644 index 0000000000..da7a9810d8 --- /dev/null +++ b/dom/events/test/test_bug1692052.html @@ -0,0 +1,36 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1692052 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1692052</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript"> + SimpleTest.expectAssertions(0, 6); // 6 times runs + SimpleTest.waitForExplicitFinish(); + + window.onload = function() { + window.open("file_bug1692052.html?c=0"); + ok(true, "Test window is opened"); + } + + function finishTests() { + ok(true, "Test window doesn't crash"); + SimpleTest.finish(); + } + + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1692052">Mozilla Bug 1692052</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/dom/events/test/test_bug1692277.html b/dom/events/test/test_bug1692277.html new file mode 100644 index 0000000000..4615c4c48b --- /dev/null +++ b/dom/events/test/test_bug1692277.html @@ -0,0 +1,43 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>Bug 1692277</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<script src="/tests/gfx/layers/apz/test/mochitest/apz_test_utils.js"></script> +<script src="/tests/gfx/layers/apz/test/mochitest/apz_test_native_event_utils.js"></script> +<link rel="stylesheet" href="/tests/SimpleTest/test.css" /> +<style> + @font-face { + font-family: Ahem; + src: url("/tests/dom/base/test/Ahem.ttf"); + } + + #anchor { + font: 16px/1 Ahem; + } +</style> + +<div> + <a href="#" id="anchor">A draggable link</a> +</div> + +<script> + function waitForEvent(target, eventName) { + return new Promise(resolve => { + target.addEventListener(eventName, resolve, { once: true }); + }); + } + + add_task(async function testPenDrag() { + await SpecialPowers.pushPrefEnv({ + set: [["dom.w3c_pointer_events.dispatch_by_pointer_messages", true]], + }); + + await SimpleTest.promiseFocus(); + const anchor = document.getElementById("anchor"); + await promiseNativePointerDrag(anchor, "pen", 5, 5, 50, 50); + const promise = waitForEvent(anchor, "click"); + await promiseNativePointerDrag(anchor, "pen", 5, 5, 5, 5); + await promise; + ok(true, "Got the click event"); + }); +</script> diff --git a/dom/events/test/test_bug1709832.html b/dom/events/test/test_bug1709832.html new file mode 100644 index 0000000000..b277c4e0fb --- /dev/null +++ b/dom/events/test/test_bug1709832.html @@ -0,0 +1,46 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>Test for Bug 1709832</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<script src="/tests/SimpleTest/EventUtils.js"></script> +<link rel="stylesheet" href="/tests/SimpleTest/test.css" /> +<style> + @font-face { + font-family: Ahem; + src: url("/tests/dom/base/test/Ahem.ttf"); + } + + #container { + font: 16px/1 Ahem; + } +</style> +<div id="container"> + <span id="start" draggable="true">start</span><span id="dest" draggable="true">dest</span> +</div> +<script> + SimpleTest.waitForExplicitFinish(); + + dest.addEventListener("dragenter", ev => { + is(ev.target, dest, "dragenter target should be element"); + is(ev.relatedTarget, start, "dragenter relatedTarget should be element"); + }); + start.addEventListener("dragleave", ev => { + is(ev.target, start, "dragleave target should be element"); + is(ev.relatedTarget, dest, "dragleave relatedTarget should be element"); + SimpleTest.finish(); + }); + + synthesizeMouse(start, 5, 5, { type: "mousedown" }); + + const utils = SpecialPowers.getDOMWindowUtils(window); + + // Intentionally passing a text node to test EventStateManager behavior + // when GetContentForEvent gives a text node + // EventUtils.sendDragEvent is unusable because it requires an element as a target + utils.dispatchDOMEventViaPresShellForTesting(start.childNodes[0], new DragEvent("dragover", { + ...createDragEventObject("dragover", start, window, null, {}) + })); + utils.dispatchDOMEventViaPresShellForTesting(dest.childNodes[0], new DragEvent("dragover", { + ...createDragEventObject("dragover", dest, window, null, {}) + })); +</script> diff --git a/dom/events/test/test_bug1710509.html b/dom/events/test/test_bug1710509.html new file mode 100644 index 0000000000..4454a20b9e --- /dev/null +++ b/dom/events/test/test_bug1710509.html @@ -0,0 +1,49 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>Bug 1710509</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<script src="/tests/gfx/layers/apz/test/mochitest/apz_test_utils.js"></script> +<script src="/tests/gfx/layers/apz/test/mochitest/apz_test_native_event_utils.js"></script> +<link rel="stylesheet" href="/tests/SimpleTest/test.css" /> + +<style> + #container { + width: 100px; + height: 100px; + touch-action: none; + } +</style> + +<div id="container"></div> + +<script> + /** + * @template {keyof HTMLElementEventMap} K + * @param {HTMLElemnt} target + * @param {K} eventName + * @return {HTMLElementEventMap[K]} + */ + function waitForEvent(target, eventName) { + return new Promise(resolve => { + target.addEventListener(eventName, resolve, { once: true }); + }); + } + + add_task(async function testPenDrag() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.w3c_pointer_events.dispatch_by_pointer_messages", false], + ], + }); + + await SimpleTest.promiseFocus(); + const container = document.getElementById("container"); + const touchMovePromise = waitForEvent(container, "touchmove"); + await promiseNativePointerDrag(container, "pen", 50, 50, -50, -50); + + const touchmove = await touchMovePromise; + const [touch] = touchmove.touches; + is(touch.radiusX, 1, ".radiusX"); + is(touch.radiusY, 1, ".radiusX"); + }); +</script> diff --git a/dom/events/test/test_bug1728171.html b/dom/events/test/test_bug1728171.html new file mode 100644 index 0000000000..e6136f06d4 --- /dev/null +++ b/dom/events/test/test_bug1728171.html @@ -0,0 +1,70 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>Bug 1728171</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<script src="/tests/SimpleTest/paint_listener.js"></script> +<script src="/tests/gfx/layers/apz/test/mochitest/apz_test_utils.js"></script> +<script src="/tests/gfx/layers/apz/test/mochitest/apz_test_native_event_utils.js"></script> +<link rel="stylesheet" href="/tests/SimpleTest/test.css" /> + +<style> + #container { + width: 100px; + height: 100px; + } +</style> + +<div id="container"></div> + +<script> + const container = document.getElementById("container"); + + add_task(async function testPenContextMenu() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.w3c_pointer_events.dispatch_by_pointer_messages", true], + ...getPrefs("TOUCH_EVENTS:PAN"), + ], + }); + + await waitUntilApzStable(); + + let pointerUpSeen = false + container.addEventListener("pointerup", () => { + pointerUpSeen = true; + }, { once: true }); + const contextMenuPromise = promiseOneEvent(container, "contextmenu"); + await promiseNativePointerTap(container, "pen", 50, 50, { button: 2 }); + + const contextmenu = await contextMenuPromise; + ok(pointerUpSeen, "pointerup event was seen"); + is(contextmenu.button, 2, ".button indicates secondary button"); + + // Close the context menu. + await promiseNativePointerTap(container, "pen", 30, 30); + }); + + add_task(async function testPenContextMenuWhenButtonChange() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.w3c_pointer_events.dispatch_by_pointer_messages", true], + ...getPrefs("TOUCH_EVENTS:PAN"), + ], + }); + + await waitUntilApzStable(); + + const contextMenuPromise = promiseOneEvent(container, "contextmenu"); + + await promiseNativePointerInput(container, "pen", SpecialPowers.DOMWindowUtils.TOUCH_CONTACT, 50, 50, { button: 2 }); + // no button + await promiseNativePointerInput(container, "pen", SpecialPowers.DOMWindowUtils.TOUCH_CONTACT, 50, 50); + await promiseNativePointerInput(container, "pen", SpecialPowers.DOMWindowUtils.TOUCH_REMOVE, 50, 50); + + const contextmenu = await contextMenuPromise; + is(contextmenu.button, 2, ".button still indicates secondary button"); + + // Close the context menu. + await promiseNativePointerTap(container, "pen", 30, 30); + }); +</script> diff --git a/dom/events/test/test_bug226361.xhtml b/dom/events/test/test_bug226361.xhtml new file mode 100644 index 0000000000..143a485757 --- /dev/null +++ b/dom/events/test/test_bug226361.xhtml @@ -0,0 +1,82 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=226361 +--> +<head> + <title>Test for Bug 226361</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body id="body1"> +<p id="display"> + + <a id="b1" tabindex="1" href="http://home.mozilla.org">start</a><br /> +<br /> + +<iframe id="iframe" tabindex="2" src="bug226361_iframe.xhtml"></iframe> + + <a id="b2" tabindex="3" href="http://home.mozilla.org">end</a> + +</p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> +<![CDATA[ + +/** Test for Bug 226361 **/ + +// accessibility.tabfocus must be set to value 7 before running test also +// on a mac. +function setTabFocus() { + SpecialPowers.pushPrefEnv({ set: [[ "accessibility.tabfocus", 7 ]] }, doTest); +} + +// ================================= + +var doc = document; +function tab_to(id) { + synthesizeKey("KEY_Tab", {}); + is(doc.activeElement.id, id, "element with id=" + id + " should have focus"); +} + +function tab_iframe() { + doc = document; + tab_to('iframe'); + + // inside iframe + doc = document.getElementById('iframe').contentDocument + tab_to('a3');tab_to('a5');tab_to('a1');tab_to('a2');tab_to('a4'); +} + + +function doTest() { + window.getSelection().removeAllRanges(); + document.getElementById('body1').focus(); + is(document.activeElement.id, document.body.id, "body element should be focused"); + + doc = document; + tab_to('b1'); + + tab_iframe(); + + doc=document + document.getElementById('iframe').focus() + tab_to('b2'); + // Change tabindex so the next TAB goes back to the IFRAME + document.getElementById('iframe').setAttribute('tabindex','4'); + + tab_iframe(); + SimpleTest.finish(); +} + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(setTabFocus); + +]]> +</script> +</pre> +</body> +</html> diff --git a/dom/events/test/test_bug238987.html b/dom/events/test/test_bug238987.html new file mode 100644 index 0000000000..3a7b12653b --- /dev/null +++ b/dom/events/test/test_bug238987.html @@ -0,0 +1,279 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=238987 +--> +<head> + <title>Test for Bug 238987</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=238987">Mozilla Bug 238987</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + + /** Test for Bug 238987 **/ + + var shouldStop = false; + var activateShift = false; + var expectedResult = "i1,i2,i3,i4,i5,i6,i7,i8,number,i9,i10,i11,i12"; + var forwardFocusArray = expectedResult.split(","); + var backwardFocusArray = expectedResult.split(","); + var forwardBlurArray = expectedResult.split(","); + var backwardBlurArray = expectedResult.split(","); + // Adding 3 for "begin", "end", "begin" and one for the <a> in the Mochitest template, + var expectedWindowFocusCount = forwardFocusArray.length + backwardFocusArray.length + 4; + // but the last blur event goes to i1, not "begin". + var expectedWindowBlurCount = forwardFocusArray.length + backwardFocusArray.length + 3; + + function handleFocus(e) { + if (e.target.id == "begin") { + // if the activateShift is set, the test is coming back from the end. + if (activateShift) { + shouldStop = true; + } + } else if (e.target.id == "end") { + activateShift = true; + } else if (activateShift) { + var expected = backwardFocusArray.pop(); + ok(expected == e.target.id, + "(focus) Backward tabbing, expected [" + + expected + "], got [" + e.target.id + "]"); + } else { + var expected = forwardFocusArray.shift(); + is(e.target, document.activeElement, "Wrong activeElement!"); + ok(expected == e.target.id, + "(focus) Forward tabbing, expected [" + + expected + "], got [" + e.target.id + "]"); + } + } + + function handleWindowFocus(e) { + --expectedWindowFocusCount; + var s = "target " + e.target; + if ("id" in e.target) { + s = s + ", id=\"" + e.target.id + "\""; + } + ok(e.eventPhase == Event.CAPTURING_PHASE, + "|window| should not have got a focus event, " + s); + } + + function handleBlur(e) { + if (e.target.id == "begin" || e.target.id == "end") { + return; + } else if (activateShift) { + var expected = backwardBlurArray.pop(); + ok(expected == e.target.id, + "(blur) backward tabbing, expected [" + + expected + "], got [" + e.target.id + "]"); + } else { + var expected = forwardBlurArray.shift(); + ok(expected == e.target.id, + "(blur) forward tabbing, expected [" + + expected + "], got [" + e.target.id + "]"); + } + } + + function handleWindowBlur(e) { + --expectedWindowBlurCount; + var s = "target " + e.target; + if ("id" in e.target) { + s = s + ", id=\"" + e.target.id + "\""; + } + ok(e.eventPhase == Event.CAPTURING_PHASE, + "|window| should not have got a blur event, " + s); + } + + function tab() { + // Send tab key events. + synthesizeKey("KEY_Tab", {shiftKey: activateShift}); + if (shouldStop) { + // Did focus handling succeed + is(forwardFocusArray.length, 0, + "Not all forward tabbing focus tests were run, " + + forwardFocusArray.toString()); + is(backwardFocusArray.length, 0, + "Not all backward tabbing focus tests were run, " + + backwardFocusArray.toString()); + is(expectedWindowFocusCount, 0, + "|window| didn't get the right amount of focus events"); + + // and blur. + is(forwardBlurArray.length, 0, + "Not all forward tabbing blur tests were run, " + + forwardBlurArray.toString()); + is(backwardBlurArray.length, 0, + "Not all backward tabbing blur tests were run, " + + backwardBlurArray.toString()); + is(expectedWindowBlurCount, 0, + "|window| didn't get the right amount of blur events"); + + // Cleanup + window.removeEventListener("focus", handleWindowFocus, true); + window.removeEventListener("focus", handleWindowFocus); + window.removeEventListener("blur", handleWindowBlur, true); + window.removeEventListener("blur", handleWindowBlur); + var elements = document.getElementsByTagName("*"); + for (var i = 0; i < elements.length; ++i) { + if (elements[i].hasAttribute("id")) { + elements[i].removeEventListener("focus", handleFocus); + elements[i].removeEventListener("blur", handleBlur); + } + } + + SimpleTest.finish(); + } else { + setTimeout(tab, 0); + } + } + + function start() { + window.focus(); + window.addEventListener("focus", handleWindowFocus, true); + window.addEventListener("focus", handleWindowFocus); + window.addEventListener("blur", handleWindowBlur, true); + window.addEventListener("blur", handleWindowBlur); + var elements = document.getElementsByTagName("*"); + for (var i = 0; i < elements.length; ++i) { + if (elements[i].hasAttribute("id")) { + elements[i].addEventListener("focus", handleFocus); + elements[i].addEventListener("blur", handleBlur); + } + if (elements[i].getAttribute("tabindex") == "1") { + elements[i].setAttribute("tabindex", "-1"); + } + } + tab(); + } + + // accessibility.tabfocus must be set to value 7 before running test also + // on a mac. + function doTest() { + SpecialPowers.pushPrefEnv({"set": [["accessibility.tabfocus", 7]]}, start); + } + + SimpleTest.waitForExplicitFinish(); + addLoadEvent(doTest); + +</script> +</pre> + <h4 tabindex="0" id="begin">Test:</h4> + <table> + <tbody> + <tr> + <td>type="text"</td><td><input type="text" id="i1" value=""></td> + </tr> + <tr> + <td>type="button"</td><td><input type="button" id="i2" value="type='button'"></td> + </tr> + <tr> + <td>type="checkbox"</td><td><input type="checkbox" id="i3" ></td> + </tr> + <tr> + <td>type="radio" checked</td><td><input type="radio" id="i4" name="radio" checked> + <input type="radio" id="i4b" name="radio"></td> + </tr> + <tr> + <td>type="radio"</td><td><input type="radio" id="i5" name="radio2"> + <input type="radio" id="i6" name="radio2"></td> + </tr> + <tr> + <td>type="password"</td><td><input type="password" id="i7"></td> + </tr> + <tr> + <td>type="file"</td><td><input type="file" id="i8"></td> + </tr> + <tr> + <td>type="number"</td><td><input type="number" id="number"></td> + </tr> + <tr> + <td>button</td><td><button id="i9">button</button></td> + </tr> + <tr> + <td>select</td><td><select id="i10"><option>select</option></select></td> + </tr> + <tr> + <td>a</td><td><a href="#radio" id="i11">a link</a></td> + </tr> + <tr> + <td>tabindex="0"</td><td><span tabindex="0" id="i12">span</span></td> + </tr> + + <tr> + <td><h3>Form elements with tabindex="-1"</h3></td> + </tr> + <tr> + <td>type="text"</td><td><input type="text" tabindex="-1" value=""></td> + </tr> + <tr> + <td>type="button"</td><td><input type="button" tabindex="-1" value="type='button'"></td> + </tr> + <tr> + <td>type="checkbox"</td><td><input type="checkbox" tabindex="-1"></td> + </tr> + <tr> + <td>type="radio" checked</td><td><input type="radio" tabindex="-1" name="radio3" checked> + <input type="radio" tabindex="-1" name="radio3"></td> + </tr> + <tr> + <td>type="radio"</td><td><input type="radio" tabindex="-1" name="radio4"> + <input type="radio" tabindex="-1" name="radio4"></td> + </tr> + <tr> + <td>type="password"</td><td><input type="password" tabindex="-1"></td> + </tr> + <tr> + <td>type="file"</td><td><input type="file" tabindex="-1"></td> + </tr> + <tr> + <td>button</td><td><button tabindex="-1">button</button></td> + </tr> + <tr> + <td>select</td><td><select tabindex="-1"><option>select</option></select></td> + </tr> + + <tr> + <td><h3>Form elements with .setAttribute("tabindex", "-1")</h3></td> + </tr> + <tr> + <td>type="text"</td><td><input type="text" tabindex="1" value=""></td> + </tr> + <tr> + <td>type="button"</td><td><input type="button" tabindex="1" value="type='button'"></td> + </tr> + <tr> + <td>type="checkbox"</td><td><input type="checkbox" tabindex="1"></td> + </tr> + <tr> + <td>type="radio" checked</td><td><input type="radio" tabindex="1" name="radio5" checked> + <input type="radio" tabindex="1" name="radio5"></td> + </tr> + <tr> + <td>type="radio"</td><td><input type="radio" tabindex="1" name="radio6"> + <input type="radio" tabindex="1" name="radio6"></td> + </tr> + <tr> + <td>type="password"</td><td><input type="password" tabindex="1"></td> + </tr> + <tr> + <td>type="file"</td><td><input type="file" tabindex="1"></td> + </tr> + <tr> + <td>button</td><td><button tabindex="1">button</button></td> + </tr> + <tr> + <td>select</td><td><select tabindex="1"><option>select</option></select></td> + </tr> + + </tbody> + </table> + <h4 tabindex="0" id="end">done.</h4> +</body> +</html> + diff --git a/dom/events/test/test_bug288392.html b/dom/events/test/test_bug288392.html new file mode 100644 index 0000000000..f50327eed3 --- /dev/null +++ b/dom/events/test/test_bug288392.html @@ -0,0 +1,103 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=288392 +--> +<head> + <title>Test for Bug 288392</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=288392">Mozilla Bug 288392</a> +<p id="display"></p> +<div id="content" style="display: none"> +<div id="mutationTarget"> +</div> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +/** Test for Bug 288392 **/ +var subtreeModifiedCount; + +function subtreeModified(e) +{ + ++subtreeModifiedCount; +} + +function doTest() { + var targetNode = document.getElementById("mutationTarget"); + targetNode.addEventListener("DOMSubtreeModified", subtreeModified); + + subtreeModifiedCount = 0; + var temp = document.createElement("DIV"); + targetNode.appendChild(temp); + is(subtreeModifiedCount, 1, + "Appending a child node should have dispatched a DOMSubtreeModified event"); + + subtreeModifiedCount = 0; + temp.setAttribute("foo", "bar"); + is(subtreeModifiedCount, 1, + "Setting an attribute should have dispatched a DOMSubtreeModified event"); + + subtreeModifiedCount = 0; + targetNode.removeChild(temp); + is(subtreeModifiedCount, 1, + "Removing a child node should have dispatched a DOMSubtreeModified event"); + + // Testing events in a subtree, which is not in the document. + var subtree = document.createElement("div"); + var s = "<e1 attr1='value1'>Something1</e1><e2 attr2='value2'>Something2</e2>"; + subtree.innerHTML = s; + subtree.addEventListener("DOMSubtreeModified", subtreeModified); + + subtreeModifiedCount = 0; + subtree.firstChild.firstChild.data = "foo"; + is(subtreeModifiedCount, 1, + "Editing character data should have dispatched a DOMSubtreeModified event"); + + subtreeModifiedCount = 0; + subtree.firstChild.removeChild(subtree.firstChild.firstChild); + is(subtreeModifiedCount, 1, + "Removing a child node should have dispatched a DOMSubtreeModified event"); + + subtree.innerHTML = s; + subtreeModifiedCount = 0; + subtree.firstChild.firstChild.remove(); + is(subtreeModifiedCount, 1, + "Removing a child node should have dispatched a DOMSubtreeModified event"); + + subtreeModifiedCount = 0; + subtree.firstChild.setAttribute("foo", "bar"); + is(subtreeModifiedCount, 1, + "Setting an attribute should have dispatched a DOMSubtreeModified event"); + + subtreeModifiedCount = 0; + subtree.textContent = "foobar"; + is(subtreeModifiedCount, 1, + "Setting .textContent should have dispatched a DOMSubtreeModified event"); + + subtreeModifiedCount = 0; + subtree.innerHTML = s; + is(subtreeModifiedCount, 1, + "Setting .innerHTML should have dispatched a DOMSubtreeModified event"); + + subtreeModifiedCount = 0; + subtree.removeEventListener("DOMSubtreeModified", subtreeModified); + subtree.appendChild(document.createTextNode("")); + subtree.addEventListener("DOMSubtreeModified", subtreeModified); + subtree.normalize(); + is(subtreeModifiedCount, 1, + "Calling normalize() should have dispatched a DOMSubtreeModified event"); +} + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(doTest); +addLoadEvent(SimpleTest.finish); + +</script> +</pre> +</body> +</html> + diff --git a/dom/events/test/test_bug299673-1.html b/dom/events/test/test_bug299673-1.html new file mode 100644 index 0000000000..c3662cc9b2 --- /dev/null +++ b/dom/events/test/test_bug299673-1.html @@ -0,0 +1,61 @@ +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=299673 +--> +<head> + <title>Test #1 for Bug 299673</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body id="Body"> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=299673">Mozilla Bug 299673</a> +<p id="display"> + + <SELECT id="Select1" onchange="log(event); OpenWindow()" onfocus="log(event); " onblur="log(event)"> + <OPTION selected>option1</OPTION> + <OPTION>option2</OPTION> + <OPTION>option3</OPTION> + </SELECT> + + <INPUT id="Text1" type="text" onfocus="log(event)" onblur="log(event)"> + <INPUT id="Text2" type="text" onfocus="log(event)" onblur="log(event)"> + +</p> +<div id="content" style="display: none"> + +</div> + +<pre id="test"> + +<script src="bug299673.js"></script> + +<script class="testbody" type="text/javascript"> + +/** Test #1 for Bug 299673 **/ +function doTest(expectedEventLog) { + var eventLogForNewWindow = '\ + : Test with browser.link.open_newwindow = 2\n\ +: focus top-doc\n\ +SELECT(Select1): focus \n\ +SELECT(Select1): change \n\ + : >>> OpenWindow\n\ +: blur top-doc\n\ +: focus popup-doc\n\ +INPUT(popupText1): focus \n\ + : <<< OpenWindow\n\ +SELECT(Select1): blur \n\ +INPUT(popupText1): blur \n\ +: blur popup-doc\n\ +: focus top-doc\n\ +' + + setPrefAndDoTest(eventLogForNewWindow,'Body',2); // 2 = open new window as window +} + +todo(false, "Please write a test for bug 299673 that actually works, see bug 553417"); + +</script> +</pre> +</body> +</html> diff --git a/dom/events/test/test_bug299673-2.html b/dom/events/test/test_bug299673-2.html new file mode 100644 index 0000000000..c26a08009f --- /dev/null +++ b/dom/events/test/test_bug299673-2.html @@ -0,0 +1,60 @@ +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=299673 +--> +<head> + <title>Test #2 for Bug 299673</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body id="Body"> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=299673">Mozilla Bug 299673</a> +<p id="display"> + + <SELECT id="Select1" onchange="log(event); OpenWindow()" onfocus="log(event); " onblur="log(event)"> + <OPTION selected>option1</OPTION> + <OPTION>option2</OPTION> + <OPTION>option3</OPTION> + </SELECT> + + <INPUT id="Text1" type="text" onfocus="log(event)" onblur="log(event)"> + <INPUT id="Text2" type="text" onfocus="log(event)" onblur="log(event)"> + +</p> +<div id="content" style="display: none"> + +</div> + +<pre id="test"> + +<script src="bug299673.js"></script> + +<script class="testbody" type="text/javascript"> + +/** Test #2 for Bug 299673 **/ +function doTest(expectedEventLog) { + var eventLogForNewTab = '\ + : Test with browser.link.open_newwindow = 3\n\ +: focus top-doc\n\ +SELECT(Select1): focus \n\ +SELECT(Select1): change \n\ + : >>> OpenWindow\n\ +: blur top-doc\n\ +: focus popup-doc\n\ +INPUT(popupText1): focus \n\ + : <<< OpenWindow\n\ +SELECT(Select1): blur \n\ +INPUT(popupText1): blur \n\ +: blur popup-doc\n\ +: focus top-doc\n\ +' + setPrefAndDoTest(eventLogForNewTab,'Body',3); // 3 = open new window as tab +} + +todo(false, "Please write a test for bug 299673 that actually works, see bug 553417"); + +</script> +</pre> +</body> +</html> diff --git a/dom/events/test/test_bug322588.html b/dom/events/test/test_bug322588.html new file mode 100644 index 0000000000..25008cb375 --- /dev/null +++ b/dom/events/test/test_bug322588.html @@ -0,0 +1,66 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=322588 +--> +<head> + <title>Test for Bug 322588 - onBlur window close no longer works</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=322588">Mozilla Bug 322588 - onBlur window close no longer works</a> +<p id="display"> +<a id="link" href="javascript:pop350d('bug322588-popup.html#target')">Openwindow</a><br> +The opened window should not directly close when clicking on the Openwindow link +</p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 322588 **/ + +var result = ""; + +var w; +function pop350d(url) { + w = window.open(); + w.addEventListener("unload", function () { result += " unload";}); + w.addEventListener("load", function () { result += " load"; setTimeout(done, 1000);}); + w.addEventListener("blur", function () { result += " blur";}); + w.location = url; +} + +function doTest() { + try { + sendMouseEvent({type:'click'}, 'link'); + } catch(e) { + if (w) + w.close(); + throw e; + } +} + +function done() { + is(result," unload load","unexpected events"); // The first unload is for about:blank + if (w) + w.close(); + SimpleTest.finish(); +} + +SimpleTest.waitForExplicitFinish(); +SimpleTest.requestFlakyTimeout("untriaged"); +addLoadEvent(function() { + SpecialPowers.pushPrefEnv({"set": [ + ["browser.newtab.preload", false] + ]}, doTest); +}); + + +</script> +</pre> +</body> +</html> diff --git a/dom/events/test/test_bug328885.html b/dom/events/test/test_bug328885.html new file mode 100644 index 0000000000..2906c7e024 --- /dev/null +++ b/dom/events/test/test_bug328885.html @@ -0,0 +1,135 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=328885 +--> +<head> + <title>Test for Bug 328885</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=328885">Mozilla Bug 328885</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<input type="text" id="inputelement" + style="position: absolute; left: 0px; top: 0px;"> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +/** Test for Bug 328885 **/ + + var inputelement = null; + var mutationCount = 0; + + function mutationListener(evt) { + ++mutationCount; + } + + function clickTest() { + inputelement.addEventListener("DOMSubtreeModified", mutationListener); + inputelement.addEventListener("DOMNodeInserted", mutationListener); + inputelement.addEventListener("DOMNodeRemoved", mutationListener); + inputelement.addEventListener("DOMNodeRemovedFromDocument", mutationListener); + inputelement.addEventListener("DOMNodeInsertedIntoDocument", mutationListener); + inputelement.addEventListener("DOMAttrModified", mutationListener); + inputelement.addEventListener("DOMCharacterDataModified", mutationListener); + + inputelement.addEventListener('click', + function(event) { + var evt = SpecialPowers.wrap(event); + ok(SpecialPowers.wrap(HTMLDivElement).isInstance(evt.originalTarget), + "(1) Wrong originalTarget!"); + is(SpecialPowers.unwrap(evt.originalTarget.parentNode), inputelement, + "(2) Wront parent node!"); + ok(mutationCount == 0, "(3) No mutations should have happened! [" + mutationCount + "]"); + evt.originalTarget.textContent = "foo"; + ok(mutationCount == 0, "(4) Mutation listener shouldn't have been called! [" + mutationCount + "]"); + evt.originalTarget.innerHTML = "foo2"; + ok(mutationCount == 0, "(5) Mutation listener shouldn't have been called! [" + mutationCount + "]"); + evt.originalTarget.lastChild.data = "bar"; + ok(mutationCount == 0, "(6) Mutation listener shouldn't have been called! [" + mutationCount + "]"); + + var r = SpecialPowers.wrap(document.createRange()); + r.selectNodeContents(evt.originalTarget); + r.deleteContents(); + ok(mutationCount == 0, "(7) Mutation listener shouldn't have been called! [" + mutationCount + "]"); + + evt.originalTarget.textContent = "foo"; + ok(mutationCount == 0, "(8) Mutation listener shouldn't have been called! [" + mutationCount + "]"); + r = SpecialPowers.wrap(document.createRange()); + r.selectNodeContents(evt.originalTarget); + r.extractContents(); + ok(mutationCount == 0, "(9) Mutation listener shouldn't have been called! [" + mutationCount + "]"); + + evt.originalTarget.setAttribute("foo", "bar"); + ok(mutationCount == 0, "(10) Mutation listener shouldn't have been called! ["+ mutationCount + "]"); + + // We played with the internal div, now restore its initial state + evt.originalTarget.replaceChildren(new Text()); + + // Same tests with non-native-anononymous element. + // mutationCount should be increased by 2 each time, since there is + // first a mutation specific event and then DOMSubtreeModified. + inputelement.textContent = "foo"; + ok(mutationCount == 2, "(11) Mutation listener should have been called! [" + mutationCount + "]"); + inputelement.lastChild.data = "bar"; + ok(mutationCount == 4, "(12) Mutation listener should have been called! [" + mutationCount + "]"); + + r = document.createRange(); + r.selectNodeContents(inputelement); + r.deleteContents(); + ok(mutationCount == 6, "(13) Mutation listener should have been called! [" + mutationCount + "]"); + + inputelement.textContent = "foo"; + ok(mutationCount == 8, "(14) Mutation listener should have been called! [" + mutationCount + "]"); + r = document.createRange(); + r.selectNodeContents(inputelement); + r.extractContents(); + ok(mutationCount == 10, "(15) Mutation listener should have been called! [" + mutationCount + "]"); + + inputelement.setAttribute("foo", "bar"); + ok(mutationCount == 12, "(16) Mutation listener should have been called! ["+ mutationCount + "]"); + + // Then try some mixed mutations. The mutation handler of non-native-a + inputelement.addEventListener("DOMAttrModified", + function (evt2) { + evt.originalTarget.setAttribute("foo", "bar" + mutationCount); + ok(evt.originalTarget.getAttribute("foo") == "bar" + mutationCount, + "(17) Couldn't update the attribute?!?"); + }); + inputelement.setAttribute("foo", ""); + ok(mutationCount == 14, "(18) Mutation listener should have been called! ["+ mutationCount + "]"); + + inputelement.textContent = "foo"; + ok(mutationCount == 16, "(19) Mutation listener should have been called! ["+ mutationCount + "]"); + inputelement.addEventListener("DOMCharacterDataModified", + function (evt2) { + evt.originalTarget.textContent = "bar" + mutationCount; + }); + // This one deletes and inserts a new node, then DOMSubtreeModified. + inputelement.textContent = "bar"; + ok(mutationCount == 19, "(20) Mutation listener should have been called! ["+ mutationCount + "]"); + }); + synthesizeMouseAtCenter(inputelement, {}, window); + SimpleTest.finish(); + } + + function doTest() { + inputelement = document.getElementById('inputelement'); + inputelement.focus(); + setTimeout(clickTest, 100); + } + + SimpleTest.waitForExplicitFinish(); + SimpleTest.requestFlakyTimeout("untriaged"); + addLoadEvent(doTest); + +</script> +</pre> +</body> +</html> + diff --git a/dom/events/test/test_bug336682.js b/dom/events/test/test_bug336682.js new file mode 100644 index 0000000000..e673f1eb99 --- /dev/null +++ b/dom/events/test/test_bug336682.js @@ -0,0 +1,96 @@ +/* + * Helper functions for online/offline events tests. + * + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/licenses/publicdomain/ + */ +var gState = 0; +/** + * After all the on/offline handlers run, + * gState is expected to be equal to MAX_STATE. + */ +var MAX_STATE; + +function trace(text) { + var t = text.replace(/&/g, "&" + "amp;").replace(/</g, "&" + "lt;") + "<br>"; + //document.getElementById("display").innerHTML += t; +} + +/** + * Returns a handler function for an online/offline event. The returned handler + * ensures the passed event object has expected properties and that the handler + * is called at the right moment (according to the gState variable). + * @param nameTemplate The string identifying the hanlder. '%1' in that + * string will be replaced with the event name. + * @param eventName 'online' or 'offline' + * @param expectedStates an array listing the possible values of gState at the + * moment the handler is called. The handler increases + * gState by one before checking if it's listed in + * expectedStates. + */ +function makeHandler(nameTemplate, eventName, expectedStates) { + return function (e) { + var name = nameTemplate.replace(/%1/, eventName); + ++gState; + trace(name + ": gState=" + gState); + ok( + expectedStates.includes(gState), + "handlers called in the right order: " + + name + + " is called, " + + "gState=" + + gState + + ", expectedStates=" + + expectedStates + ); + ok(e.constructor == Event, "event should be an Event"); + ok(e.type == eventName, "event type should be " + eventName); + ok(!e.bubbles, "event should not bubble"); + ok(!e.cancelable, "event should not be cancelable"); + ok(e.target == window, "target should be the window"); + }; +} + +function doTest() { + var iosvc = SpecialPowers.Cc["@mozilla.org/network/io-service;1"].getService( + SpecialPowers.Ci.nsIIOService + ); + iosvc.manageOfflineStatus = false; + iosvc.offline = false; + ok( + navigator.onLine, + "navigator.onLine should be true, since we've just " + + "set nsIIOService.offline to false" + ); + + gState = 0; + + trace("setting iosvc.offline = true"); + iosvc.offline = true; + trace("done setting iosvc.offline = true"); + ok( + !navigator.onLine, + "navigator.onLine should be false when iosvc.offline == true" + ); + ok( + gState == window.MAX_STATE, + "offline event: all registered handlers should have been invoked, " + + "actual: " + + gState + ); + + gState = 0; + trace("setting iosvc.offline = false"); + iosvc.offline = false; + trace("done setting iosvc.offline = false"); + ok( + navigator.onLine, + "navigator.onLine should be true when iosvc.offline == false" + ); + ok( + gState == window.MAX_STATE, + "online event: all registered handlers should have been invoked, " + + "actual: " + + gState + ); +} diff --git a/dom/events/test/test_bug336682_1.html b/dom/events/test/test_bug336682_1.html new file mode 100644 index 0000000000..7d3ef6b345 --- /dev/null +++ b/dom/events/test/test_bug336682_1.html @@ -0,0 +1,55 @@ +<!DOCTYPE HTML> +<html> +<!-- +Bug 336682: online/offline events tests. + +Any copyright is dedicated to the Public Domain. +http://creativecommons.org/licenses/publicdomain/ +--> +<head> + <title>Test for Bug 336682 (online/offline events)</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body ononline="trace('<body ononline=...>'); + bodyOnonline(this, event)" + onoffline="trace('<body onoffline=...>'); bodyOnoffline(this, event)" + > +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=336682">Mozilla Bug 336682</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +<script type="text/javascript" src="test_bug336682.js"></script> + +<script class="testbody" type="text/javascript"> + +function makeBodyHandler(eventName) { + return function (aThis, aEvent) { + var handler = makeHandler("<body on%1='...'>", eventName, [1]); + handler(aEvent); + } +} +addLoadEvent(function() { + /** @see test_bug336682.js */ + MAX_STATE = 2; + + for (var event of ["online", "offline"]) { + window["bodyOn" + event] = makeBodyHandler(event); + + window.addEventListener( + event, + makeHandler("window.addEventListener('%1', ..., false)", + event, [2])); + } + + doTest(); + SimpleTest.finish(); +}); + +SimpleTest.waitForExplicitFinish(); +</script> +</body> +</html> diff --git a/dom/events/test/test_bug336682_2.xhtml b/dom/events/test/test_bug336682_2.xhtml new file mode 100644 index 0000000000..d8012a15e1 --- /dev/null +++ b/dom/events/test/test_bug336682_2.xhtml @@ -0,0 +1,59 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<!-- +Bug 336682: online/offline events tests. + +Any copyright is dedicated to the Public Domain. +http://creativecommons.org/licenses/publicdomain/ +--> +<window title="Mozilla Bug 336682" + onoffline="trace('lt;body onoffline=...'); windowOnoffline(this, event)" + ononline="trace('lt;body ononline=...'); windowOnonline(this, event)" + + xmlns:html="http://www.w3.org/1999/xhtml" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + +<body xmlns="http://www.w3.org/1999/xhtml"> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=336682"> +Mozilla Bug 336682 (online/offline events)</a> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +</body> + +<script type="text/javascript" src="test_bug336682.js"/> +<script class="testbody" type="text/javascript"> +<![CDATA[ +addLoadEvent(function() { + /** @see test_bug336682.js */ + MAX_STATE = 2; + + function makeWindowHandler(eventName) { + return function (aThis, aEvent) { + var handler = makeHandler("<body on%1='...'>", eventName, [1]); + handler(aEvent); + } + } + + for (var event of ["online", "offline"]) { + window["windowOn" + event] = makeWindowHandler(event); + + window.addEventListener( + event, + makeHandler("window.addEventListener('%1', ..., false)", + event, [2]), + false); + } + + doTest(); + SimpleTest.finish(); +}); + +SimpleTest.waitForExplicitFinish(); +]]> +</script> + +</window> diff --git a/dom/events/test/test_bug367781.html b/dom/events/test/test_bug367781.html new file mode 100644 index 0000000000..30f5592498 --- /dev/null +++ b/dom/events/test/test_bug367781.html @@ -0,0 +1,53 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=367781 +--> +<head> + <title>Test for Bug 367781</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=367781">Mozilla Bug 367781</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +/** Test for Bug **/ +var eventCounter = 0; + +function handler(e) { + if (e.type == "DOMNodeInserted") { + ++eventCounter; + } +} + +function doTest() { + var i1 = document.getElementById('i1'); + var i2 = document.getElementById('i2'); + var pre = i1.contentDocument.getElementsByTagName("pre")[0]; + pre.addEventListener("DOMNodeInserted", handler); + pre.textContent = pre.textContent + pre.textContent; + ok(eventCounter == 1, "DOMNodeInserted should have been dispatched"); + + pre.remove(); + i2.contentDocument.adoptNode(pre); + i2.contentDocument.body.appendChild(pre); + ok(eventCounter == 2, "DOMNodeInserted should have been dispatched in the new document"); +} + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(doTest); +addLoadEvent(SimpleTest.finish); + +</script> +</pre> +<iframe id="i1" srcdoc="<html><body><pre>Foobar</pre></body></html>"></iframe> +<iframe id="i2" srcdoc="<html><body></body></html>"></iframe> +</body> +</html> + diff --git a/dom/events/test/test_bug379120.html b/dom/events/test/test_bug379120.html new file mode 100644 index 0000000000..6df6b2c639 --- /dev/null +++ b/dom/events/test/test_bug379120.html @@ -0,0 +1,55 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=379120 +--> +<head> + <title>Test for Bug 379120</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=379120">Mozilla Bug 379120</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +/** Test for Bug 379120 **/ + + var originalString = "<test></test>"; + + // Parse the content into an XMLDocument + var parser = new DOMParser(); + var originalDoc = parser.parseFromString(originalString, "text/xml"); + + var stylesheetText = + "<xsl:stylesheet xmlns:xsl='http://www.w3.org/1999/XSL/Transform' " + + "version='1.0' xmlns='http://www.w3.org/1999/xhtml'> " + + + "<xsl:output method='xml' version='1.0' encoding='UTF-8' " + + "doctype-system='http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd' " + + "doctype-public='-//W3C//DTD XHTML 1.0 Transitional//EN' /> " + + + "<xsl:template match='/'>" + + "<div onload='var i = 1'/>" + + "<xsl:apply-templates />" + + "</xsl:template>" + + "</xsl:stylesheet>"; + var stylesheet = parser.parseFromString(stylesheetText, "text/xml"); + + var processor = new XSLTProcessor(); + + var targetDocument; + processor.importStylesheet (stylesheet); + var transformedDocument = processor.transformToDocument (originalDoc); + is(transformedDocument.documentElement.getAttribute("onload"), + "var i = 1"); + is(transformedDocument.documentElement.onload, null, "Shouldn't have onload handler"); +</script> +</pre> +</body> +</html> + diff --git a/dom/events/test/test_bug402089.html b/dom/events/test/test_bug402089.html new file mode 100644 index 0000000000..905a47a5c4 --- /dev/null +++ b/dom/events/test/test_bug402089.html @@ -0,0 +1,67 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=402089 +--> +<head> + <title>Test for Bug 402089</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<!-- setTimeout so that the test starts after paint suppression ends --> +<body onload="setTimeout(doTest,0);"> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=402089">Mozilla Bug 402089</a> +<p id="display"></p> +<div id="content"> + <pre id="result1"></pre> + <pre id="result2"></pre> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +/** Test for Bug 402089 **/ + +var cachedEvent = null; + +function testCachedEvent() { + testEvent('result2'); + ok((document.getElementById('result1').textContent == + document.getElementById('result2').textContent), + "Event coordinates should be the same after dispatching."); + SimpleTest.finish(); +} + +function testEvent(res) { + var s = cachedEvent.type + "\n"; + s += "clientX: " + cachedEvent.clientX + ", clientY: " + cachedEvent.clientY + "\n"; + s += "screenX: " + cachedEvent.screenX + ", screenY: " + cachedEvent.screenY + "\n"; + s += "layerX: " + cachedEvent.layerX + ", layerY: " + cachedEvent.layerY + "\n"; + s += "pageX: " + cachedEvent.pageX + ", pageY: " + cachedEvent.pageY + "\n"; + document.getElementById(res).textContent += s; +} + +function clickHandler(e) { + cachedEvent = e; + testEvent('result1'); + e.stopPropagation(); + e.preventDefault(); + window.removeEventListener("click", clickHandler, true); + setTimeout(testCachedEvent, 10); +} + +function doTest() { + window.addEventListener("click", clickHandler, true); + var utils = SpecialPowers.getDOMWindowUtils(window); + utils.sendMouseEvent("mousedown", 1, 1, 0, 1, 0); + utils.sendMouseEvent("mouseup", 1, 1, 0, 1, 0); + +} + +SimpleTest.waitForExplicitFinish(); +SimpleTest.requestFlakyTimeout("untriaged"); +</script> +</pre> +</body> +</html> + diff --git a/dom/events/test/test_bug405632.html b/dom/events/test/test_bug405632.html new file mode 100644 index 0000000000..5eae056307 --- /dev/null +++ b/dom/events/test/test_bug405632.html @@ -0,0 +1,34 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=405632 +--> +<head> + <title>Test for Bug 405632</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=405632">Mozilla Bug 405632</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +/** Test for Bug 405632 **/ + + var me = document.createEvent("mouseevent"); + me.initMouseEvent("foo", false, false, window, 0, 100, 100, 100, 100, + false, false, false, false, 0, null); + ok(me.clientX == me.pageX, + "mouseEvent.clientX should be the same as mouseEvent.pageX when event is initialized manually"); + ok(me.clientY == me.pageY, + "mouseEvent.clientY should be the same as mouseEvent.pageY when event is initialized manually"); + +</script> +</pre> +</body> +</html> + diff --git a/dom/events/test/test_bug409604.html b/dom/events/test/test_bug409604.html new file mode 100644 index 0000000000..91dcb5defc --- /dev/null +++ b/dom/events/test/test_bug409604.html @@ -0,0 +1,379 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=409604 +--> +<head> + <title>Test for Bug 409604</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body id="body"> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=409604">Mozilla Bug 409604</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + + /** Test for Bug 409604 **/ + + var expectedFocus = "a,c,d,e,f,g,h,i,j,k,l,m,n,p,x,y"; + // XXX the "map" test is causing trouble, see bug 433089 + var focusArray = expectedFocus.split(","); + var unfocusableElementId = "invalid"; + var unfocusableTags = [ + {tag: "abbr", content: "text", attribs: {title: "something"}}, + {tag: "acronym", content: "text", attribs: {title: "something"}}, + {tag: "address", content: "text"}, + {tag: "b", content: "text"}, + {tag: "bdo", content: "text"}, + {tag: "big", content: "text"}, + {tag: "blockquote", content: "text"}, + {tag: "caption", content: "text", parent: "table", where: "first"}, + {tag: "cite", content: "text"}, + {tag: "code", content: "text"}, + {tag: "dd", content: "text", parent: "dl"}, + {tag: "del", content: "text"}, + {tag: "dfn", content: "text", attribs: {title: "something"}}, + {tag: "div", content: "text"}, + {tag: "dl", content: "<dd>text</dd>", parent: "dl"}, + {tag: "dt", content: "text", parent: "dl"}, + {tag: "em", content: "text"}, + {tag: "fieldset", content: "text"}, + {tag: "form", content: "text", attribs: {action: "any.html"}}, + {tag: "h1", content: "text"}, + {tag: "h2", content: "text"}, + {tag: "h3", content: "text"}, + {tag: "h4", content: "text"}, + {tag: "h5", content: "text"}, + {tag: "h6", content: "text"}, + {tag: "hr"}, + {tag: "i", content: "text"}, + {tag: "img", attribs: {src: "any.png", alt: "image"}}, + {tag: "ins", content: "text"}, + {tag: "kbd", content: "text"}, + {tag: "li", content: "text", parent: "ol"}, + {tag: "li", content: "text", parent: "ul"}, + {tag: "noscript", content: "text"}, + {tag: "ol", content: "<li>text</li>"}, + {tag: "optgroup", content: "<option>text</option>", attribs: {label: "some label"}, parent: "select"}, + {tag: "option", content: "text", parent: "select"}, + {tag: "p", content: "text"}, + {tag: "pre", content: "text"}, + {tag: "q", content: "text"}, + {tag: "samp", content: "text"}, + {tag: "small", content: "text"}, + {tag: "span", content: "text"}, + {tag: "strong", content: "text"}, + {tag: "sub", content: "text"}, + {tag: "sup", content: "text"}, + {tag: "tt", content: "text"}, + {tag: "ul", content: "<li>text</li>"}, + {tag: "var", content: "text"} + ]; + var invalidElements = [ + "body", + "col", + "colgroup", +// XXX the "map" test is causing trouble, see bug 433089 +// "map", + "table", + "tbody", + "td", + "tfoot", + "th", + "thead", + "tr" + ]; + + function handleFocus(e) { + ok("accessKey" in e, "(focus) accesskey property not found on element"); + var expected = focusArray.shift(); + // "k" and "n" are a special cases because the element receiving the focus + // is not the element which has the accesskey. + if (expected == "k" || expected == "n") { + ok(e.value == "test for label", "(focus) unexpected element: " + e.value + + " expected: " + "test for label"); + // "l" is a special case because the element receiving the focus is not + // the element which has the accesskey. + } else if (expected == "l") { + ok(e.value == "test for legend", "(focus) unexpected element: " + e.value + + " expected: " + "test for legend"); + } else { + ok(expected == e.accessKey, "(focus) unexpected element: " + e.accessKey + + " expected: " + expected); + } + } + + function handleClick(e) { + ok("accessKey" in e, "(click) accesskey property not found on element"); + } + + function handleInvalid(e) { + ok("accessKey" in e, "(invalid) accesskey property not found on element"); + ok(false, "(invalid) accesskey should not have any effect on this element: " + + e.localName); + } + + function pressAccessKey(key) { + synthesizeKey(key.key, {altKey: true, shiftKey: true}); + } + + function testFocusableElements() { + for (var code = "a".charCodeAt(0); code <= "y".charCodeAt(0); ++ code) { + // XXX the "map" test is causing trouble, see bug 433089 + if (code == "b".charCodeAt(0)) + continue; + var accessChar = String.fromCharCode(code).toUpperCase(); + pressAccessKey({key: accessChar}); + } + ok(!focusArray.length, "(focus) unhandled elements remaining: " + focusArray.join(",")); + } + + function createUnfocusableElement(elem, accesskey) { + ok("tag" in elem, "invalid object passed to createUnfocusableElement: " + elem.toString()); + var e = document.createElement(elem.tag); + if ("content" in elem) { + e.innerHTML = elem.content; + } + if ("attribs" in elem) { + for (var attr in elem.attribs) { + e.setAttribute(attr, elem.attribs[attr]); + } + } + e.setAttribute("accesskey", accesskey); + e.setAttribute("onclick", "handleClick(event.target); event.preventDefault();"); + e.setAttribute("onfocus", "handleInvalid(event.target);"); + var parent = null; + var elementToInsert = null; + if ("parent" in elem) { + parent = document.getElementById(elem.parent); + elementToInsert = e; + } else { + parent = document.getElementById("tbody"); + elementToInsert = document.createElement("tr"); + var td = document.createElement("td"); + td.textContent = elem.tag; + elementToInsert.appendChild(td); + td = document.createElement("td"); + td.appendChild(e); + elementToInsert.appendChild(td); + } + ok(parent != null, "parent element not specified for element: " + elem.tag); + ok(elementToInsert != null, "elementToInsert not specified for element: " + elem.tag); + elementToInsert.setAttribute("id", unfocusableElementId); + if ("where" in elem) { + if (elem.where == "first") { + parent.insertBefore(elementToInsert, parent.firstChild); + } else { + ok(false, "invalid where value specified for element: " + elem.tag); + } + } else { + parent.appendChild(elementToInsert); + } + } + + function destroyUnfocusableElement() { + var el = document.getElementById(unfocusableElementId); + ok(el != null, "unfocusable element not found"); + el.remove(); + ok(document.getElementById(unfocusableElementId) == null, "unfocusable element not properly removed"); + } + + function testUnfocusableElements() { + var i, e; + for (i = 0; i < unfocusableTags.length; ++ i) { + createUnfocusableElement(unfocusableTags[i], "z"); + pressAccessKey({key: "Z"}); + destroyUnfocusableElement(); + } + for (i = 0; i < invalidElements.length; ++ i) { + e = document.getElementById(invalidElements[i]); + ok(e != null, "element with ID " + invalidElements[i] + " not found"); + e.setAttribute("accesskey", "z"); + e.setAttribute("onclick", "handleClick(event.target); event.preventDefault();"); + e.setAttribute("onfocus", "handleInvalid(event.target);"); + pressAccessKey({key: "Z"}); + e.removeAttribute("accesskey"); + e.removeAttribute("onclick"); + e.removeAttribute("onfocus"); + } + } + + function start() { + testFocusableElements(); + testUnfocusableElements(); + SimpleTest.finish(); + } + + function doTest() { + SpecialPowers.pushPrefEnv({"set": [["ui.key.contentAccess", 5]]}, start); + } + + SimpleTest.waitForExplicitFinish(); + addLoadEvent(doTest); + +</script> +</pre> + <table id="table"> + <thead id="thead"> + <tr id="tr"><th id="th">Test header</th><th></th></tr> + </thead> + <tfoot id="tfoot"> + <tr><td id="td">Test footer</td><td></td></tr> + </tfoot> + <tbody id="tbody"> + <colgroup id="colgroup"> + <col id="col"></col> + <col></col> + </colgroup> + <tr> + <td>a</td><td><a href="#" onclick="handleClick(event.target); return false;" accesskey="a" onfocus="handleFocus(event.target);">test link"</a></td> + </tr> +<!-- the "map" test is causing trouble, see bug 433089 + <tr> + <td>area</td><td><img src="about:logo" width="300" height="236" usemap="#map"> + <map id="map" name="map"><area shape="rect" coords="0,0,82,126" href="#" + onclick="handleClick(event.target); return false;" accesskey="b"></map> + </td> + </tr> +--> + <tr> + <td>button</td><td><button onclick="handleClick(event.target);" accesskey="c" onfocus="handleFocus(event.target);">test button"</button></td> + </tr> + <tr> + <td>input type="text"</td><td><input type="text" value="" onclick="handleClick(event.target);" onfocus="handleFocus(event.target);" accesskey="d"></td> + </tr> + <tr> + <td>input type="button"</td><td><input type="button" value="type='button'" onclick="handleClick(event.target);" onfocus="handleFocus(event.target);" accesskey="e"></td> + </tr> + <tr> + <td>input type="checkbox"</td><td><input type="checkbox" onclick="handleClick(event.target);" onfocus="handleFocus(event.target)" accesskey="f"></td> + </tr> + <tr> + <td>input type="radio"</td><td><input type="radio" name="radio" onclick="handleClick(event.target);" onfocus="handleFocus(event.target);" accesskey="g"></td> + </tr> + <tr> + <td>input type="password"</td><td><input type="password" onclick="handleClick(event.target);" onfocus="handleFocus(event.target);" accesskey="h"></td> + </tr> + <tr> + <td>input type="submit"</td><td><input type="submit" value="type='submit'" onclick="handleClick(event.target); return false;" + onfocus="handleFocus(event.target);" accesskey="i"></td> + </tr> + <tr> + <td>input type="reset"</td><td><input type="submit" value="type='reset'" onclick="handleClick(event.target);" + onfocus="handleFocus(event.target);" accesskey="j"></td> + </tr> + <tr> + <td>label</td><td><label accesskey="k" onclick="handleClick(event.target);" onfocus="handleInvalid(event.target);">test label + <input type="text" value="test for label" onfocus="handleFocus(event.target);" onclick="handleClick(event.target);"></label></td> + </tr> + <tr> + <td>legend</td><td><fieldset><legend accesskey="l">test legend</legend> + <input type="text" value="test for legend" onfocus="handleFocus(event.target);" onclick="handleClick(event.target);" ></fieldset></td> + </tr> + <tr> + <td>textarea</td><td><textarea onfocus="handleFocus(event.target);" onclick="handleClick(event.target);" accesskey="m">test text</textarea></td> + </tr> + <tr> + <td>label (label invisible)</td><td><label for="txt1" accesskey="n" style="display:none" + onclick="handleClick(event.target);" onfocus="handleInvalid(event.target);">test label</label> + <input type="text" id="txt1" value="test for label" onclick="handleClick(event.target);" onfocus="handleFocus(event.target);"></td> + </tr> + <tr> + <td>label (control invisible)</td><td><label for="txt2" accesskey="o" + onclick="handleClick(event.target);" onfocus="handleInvalid(event.target);">test label</label> + <input type="text" id="txt2" value="test for label" onclick="handleClick(event.target);" + onfocus="handleInvalid(event.target);" style="display:none"></td> + </tr> + <tr> + <td>select</td> + <td> + <select onclick="handleClick(event.target);" onfocus="handleFocus(event.target)" accesskey="p"><option>option</option></select> + </td> + </tr> + <tr> + <td>object</td> + <td> + <object onclick="handleClick(event.target);" onfocus="handleInvalid(event.target)" accesskey="q">an object</object> + </td> + </tr> + <tr> + <td>a without href</td> + <td> + <a onclick="handleClick(event.target);" onfocus="handleInvalid(event.target)" accesskey="r">an object</object> + </td> + </tr> + <tr> + <td>disabled button</td> + <td> + <button disabled="" onclick="handleClick(event.target);" onfocus="handleInvalid(event.target)" accesskey="s">disabled</button> + </td> + </tr> + <tr> + <td>disabled input</td> + <td> + <input disabled="" onclick="handleClick(event.target);" onfocus="handleInvalid(event.target)" accesskey="t"></input> + </td> + </tr> + <tr> + <td>hidden input</td> + <td> + <input type="hidden" onclick="handleClick(event.target);" onfocus="handleInvalid(event.target)" accesskey="u">disabled</input> + </td> + </tr> + <tr> + <td>disabled select</td> + <td> + <select disabled onclick="handleClick(event.target);" onfocus="handleInvalid(event.target)" accesskey="v"> + <option>disabled</option> + </select> + </td> + </tr> + <tr> + <td>disabled textarea</td> + <td> + <textarea disabled onclick="handleClick(event.target);" onfocus="handleInvalid(event.target)" accesskey="w">disabled</textarea> + </td> + </tr> + <tr> + <td>scrollable div(focusable)</td> + <td> + <div onclick="handleClick(event.target);" onfocus="handleFocus(event.target)" accesskey="x" style="height: 50px; overflow: auto;"> + The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy + + dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the + + lazy dog. The quick brown fox jumps over the lazy dog. + The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy + + dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the + + lazy dog. The quick brown fox jumps over the lazy dog. + The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy + + dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the + + lazy dog. The quick brown fox jumps over the lazy dog. + </div> + </td> + </tr> + <tr> + <td>contenteditable div(focusable)</td> + <td> + <div onclick="handleClick(event.target);" onfocus="handleFocus(event.target)" accesskey="y" contenteditable="true"> + Test text..... + </div> + </td> + </tr> + </tbody> + </table> + <dl id="dl"></dl> + <ul id="ul"></ul> + <ol id="ol"></ol> + <select id="select"></select> +</body> +</html> diff --git a/dom/events/test/test_bug412567.html b/dom/events/test/test_bug412567.html new file mode 100644 index 0000000000..86f7001fc2 --- /dev/null +++ b/dom/events/test/test_bug412567.html @@ -0,0 +1,47 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=412567 +--> +<head> + <title>Test for Bug 412567</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body onload="testRedispatching(event);"> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=412567">Mozilla Bug 412567</a> +<p id="display"></p> +<div id="content" style="display: none" onload="redispatchinHandler(event)"> + +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +/** Test for Bug 412567 **/ + +var loadEvent = null; + +function redispatchinHandler(evt) { + is(evt.type, "load", "Wrong event type!"); + ok(!evt.isTrusted, "Event should not be trusted!"); + SimpleTest.finish(); +} + +function redispatch() { + ok(loadEvent.isTrusted, "Event should be trusted before redispatching!"); + document.getElementById('content').dispatchEvent(loadEvent); +} + +function testRedispatching(evt) { + is(evt.type, "load", "Wrong event type!"); + ok(evt.isTrusted, "Event should be trusted!"); + loadEvent = evt; + setTimeout(redispatch, 0); +} + +SimpleTest.waitForExplicitFinish(); +</script> +</pre> +</body> +</html> + diff --git a/dom/events/test/test_bug415498.xhtml b/dom/events/test/test_bug415498.xhtml new file mode 100644 index 0000000000..8327f2e6bb --- /dev/null +++ b/dom/events/test/test_bug415498.xhtml @@ -0,0 +1,95 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=415498 +--> +<window title="Mozilla Bug 415498" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="init()"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/chrome-harness.js"></script> +<body xmlns="http://www.w3.org/1999/xhtml"> + <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=415498">Mozilla Bug 415498</a> + + <p id="display"></p> + + <pre id="test"> + <script class="testbody" type="application/javascript"><![CDATA[ + const {BrowserTestUtils} = ChromeUtils.import("resource://testing-common/BrowserTestUtils.jsm"); + + /** Test for Bug 415498 **/ + SimpleTest.waitForExplicitFinish(); + + var gTestsIterator; + var gConsole; + var gConsoleListener; + var gMessages = []; + + function init() { + gTestsIterator = testsIterator(); + + gConsole = Cc["@mozilla.org/consoleservice;1"]. + getService(Ci.nsIConsoleService); + + gConsoleListener = { + observe: function(aObject) { + gMessages.push(aObject); + } + }; + gConsole.registerListener(gConsoleListener); + + nextTest(); + } + + function nextTest() { + let {done} = gTestsIterator.next(); + if (done) { + if (gConsole && gConsoleListener) { + gConsole.unregisterListener(gConsoleListener); + } + SimpleTest.finish(); + } + } + + function* testsIterator() { + + var browser = $("browser"); + browser.addEventListener("load", function() { + setTimeout(nextTest, 0) + }, false); + + // 1) This document uses addEventListener to register a method throwing an exception + var chromeDir = getRootDirectory(window.location.href); + BrowserTestUtils.loadURIString(browser, chromeDir + "bug415498-doc1.html"); + yield undefined; + + ok(verifyErrorReceived("HierarchyRequestError"), + "Error message not reported in event listener callback!"); + gMessages = []; + + // 2) This document sets window.onload to register a method throwing an exception + var chromeDir = getRootDirectory(window.location.href); + BrowserTestUtils.loadURIString(browser, chromeDir + "bug415498-doc2.html"); + yield undefined; + + ok(verifyErrorReceived("HierarchyRequestError"), + "Error message not reported in window.onload!"); + } + + function verifyErrorReceived(errorString) { + for (var i = 0; i < gMessages.length; i++) { + if (gMessages[i].message.includes(errorString)) + return true; + } + return false; + } + ]]></script> + </pre> +</body> + +<browser id="browser" type="content" flex="1" src="about:blank"/> + +</window> diff --git a/dom/events/test/test_bug418986-3.html b/dom/events/test/test_bug418986-3.html new file mode 100644 index 0000000000..3ede005902 --- /dev/null +++ b/dom/events/test/test_bug418986-3.html @@ -0,0 +1,25 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=418986 +--> +<head> + <meta charset="utf-8"> + <title>Test 3/3 for Bug 418986 - Resist fingerprinting by preventing exposure of screen and system info</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body id="body"> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=418986">Bug 418986</a> +<p id="display"></p> +<pre id="test"></pre> +<script type="application/javascript" src="bug418986-3.js"></script> +<script type="application/javascript"> + // This test produces fake mouse events and checks that the screenX and screenY + // properties of the received event objects provide client window coordinates. + // Run the test once the window has loaded. + window.onload = () => test(true); +</script> +</body> +</html> diff --git a/dom/events/test/test_bug418986-3.xhtml b/dom/events/test/test_bug418986-3.xhtml new file mode 100644 index 0000000000..426d888998 --- /dev/null +++ b/dom/events/test/test_bug418986-3.xhtml @@ -0,0 +1,27 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<!-- +Bug 418986 +--> +<window title="Mozilla Bug 418986" + xmlns:html="http://www.w3.org/1999/xhtml" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + +<body id="body" xmlns="http://www.w3.org/1999/xhtml"> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=418986"> +Mozilla Bug 418986</a> +</body> + +<script type="application/javascript" src="bug418986-3.js"></script> +<script type="application/javascript"><![CDATA[ + // This test produces fake mouse events and checks that the screenX and screenY + // properties of the received event objects provide client window coordinates. + // Run the test once the window has loaded. + test(false); +]]></script> + +</window> diff --git a/dom/events/test/test_bug422132.html b/dom/events/test/test_bug422132.html new file mode 100644 index 0000000000..f9728c7f8b --- /dev/null +++ b/dom/events/test/test_bug422132.html @@ -0,0 +1,125 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=422132 +--> +<head> + <title>Test for Bug 422132</title> + <script type="text/javascript" src="/MochiKit/MochiKit.js"></script> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script src="/tests/SimpleTest/paint_listener.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=422132">Mozilla Bug 422132</a> +<p id="display"></p> +<div id="target" style="font-size: 0; width: 200px; height: 200px; overflow: auto;"> + <div style="width: 1000px; height: 1000px;"></div> +</div> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +/** Test for Bug 422132 **/ + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + SpecialPowers.pushPrefEnv({ + "set":[["general.smoothScroll", false], + ["mousewheel.min_line_scroll_amount", 1], + ["mousewheel.system_scroll_override.enabled", false], + ["mousewheel.transaction.timeout", 100000], + ["test.events.async.enabled", true]]}, runTests)}, window); + +function runTests() +{ + var target = document.getElementById("target"); + + var scrollLeft = target.scrollLeft; + var scrollTop = target.scrollTop; + + var tests = [ + { + prepare() { + scrollLeft = target.scrollLeft; + scrollTop = target.scrollTop; + }, + event: { + deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 0.5, + deltaY: 0.5, + lineOrPageDeltaX: 0, + lineOrPageDeltaY: 0 + }, + }, { + event: { + deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 0.5, + deltaY: 0.5, + lineOrPageDeltaX: 0, + lineOrPageDeltaY: 0 + }, + check() { + is(target.scrollLeft - scrollLeft, 1, + "not scrolled to right by 0.5px delta value with pending 0.5px delta"); + is(target.scrollTop - scrollTop, 1, + "not scrolled to bottom by 0.5px delta value with pending 0.5px delta"); + }, + }, { + prepare() { + scrollLeft = target.scrollLeft; + scrollTop = target.scrollTop; + }, + event: { + deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: 0.5, + deltaY: 0.5, + lineOrPageDeltaX: 0, + lineOrPageDeltaY: 0 + }, + }, { + event: { + deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: 0.5, + deltaY: 0.5, + lineOrPageDeltaX: 1, + lineOrPageDeltaY: 1 + }, + check() { + is(target.scrollLeft - scrollLeft, 1, + "not scrolled to right by 0.5 line delta value with pending 0.5 line delta"); + is(target.scrollTop - scrollTop, 1, + "not scrolled to bottom by 0.5 line delta value with pending 0.5 line delta"); + } + } + ]; + + var nextTest = function() { + var test = tests.shift(); + if (test.prepare) { + test.prepare(); + } + + sendWheelAndPaint(target, 10, 10, test.event, function() { + if (test.check) { + test.check(); + } + if (!tests.length) { + SimpleTest.finish(); + return; + } + + setTimeout(nextTest, 0); + }); + } + + nextTest(); +} + +</script> +</pre> +</body> +</html> diff --git a/dom/events/test/test_bug426082.html b/dom/events/test/test_bug426082.html new file mode 100644 index 0000000000..1f68ea867f --- /dev/null +++ b/dom/events/test/test_bug426082.html @@ -0,0 +1,30 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=426082 +--> +<head> + <title>Test for Bug 426082</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script src="/tests/SimpleTest/WindowSnapshot.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 426082 **/ +SimpleTest.waitForExplicitFinish(); +var subwindow = window.open("./bug426082.html", "bug426082", "width=800,height=1000"); + +function finishTests() { + subwindow.close(); + SimpleTest.finish(); +} +</script> +</pre> + +</body> +</html> diff --git a/dom/events/test/test_bug427537.html b/dom/events/test/test_bug427537.html new file mode 100644 index 0000000000..f3d0641a97 --- /dev/null +++ b/dom/events/test/test_bug427537.html @@ -0,0 +1,61 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=427537 +--> +<head> + <title>Test for Bug 427537</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=427537">Mozilla Bug 427537</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 427537 **/ + +var e = document.createEvent("CustomEvent"); +ok(e, "Should have custom event!"); + +// Test initCustomEvent and also cycle collection handling by +// passing reference to the event as 'detail' parameter. +e.initCustomEvent("foobar", true, true, e); + +var didCallListener = false; +document.addEventListener("foobar", + function(evt) { + didCallListener = true; + is(evt.type, "foobar", "Should get 'foobar' event!"); + is(evt.detail, evt, ".detail should point to the event itself."); + ok(e.bubbles, "Event should bubble!"); + ok(e.cancelable, "Event should be cancelable."); + }, true); + +document.dispatchEvent(e); +ok(didCallListener, "Should have called listener!"); + +e = document.createEvent("CustomEvent"); +e.initEvent("foo", true, true); +is(e.detail, null, "Default detail should be null."); + +e = document.createEvent("CustomEvent"); +e.initCustomEvent("foobar", true, true, 1); +is(e.detail, 1, "Detail should be 1."); + +e = document.createEvent("CustomEvent"); +e.initCustomEvent("foobar", true, true, "test"); +is(e.detail, "test", "Detail should be 'test'."); + +e = document.createEvent("CustomEvent"); +e.initCustomEvent("foobar", true, true, true); +is(e.detail, true, "Detail should be true."); + +</script> +</pre> +</body> +</html> diff --git a/dom/events/test/test_bug428988.html b/dom/events/test/test_bug428988.html new file mode 100644 index 0000000000..5caec887a0 --- /dev/null +++ b/dom/events/test/test_bug428988.html @@ -0,0 +1,44 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=428988 +--> +<head> + <title>Test for Bug 428988</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=428988">Mozilla Bug 428988</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 428988 **/ + +function listenerForClick(evt) { + is(Math.round(evt.mozPressure*100), 56, "Wrong .mozPressure"); +} + +function doTest() { + var target = document.getElementById("testTarget"); + target.addEventListener("click", listenerForClick, true); + var me = document.createEvent("MouseEvent"); + me.initNSMouseEvent("click", true, true, window, 0, 0, 0, 0, 0, + false, false, false, false, 0, null, 0.56, 0); + target.dispatchEvent(me); + target.removeEventListener("click", listenerForClick, true); + SimpleTest.finish(); +} + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(doTest); + +</script> +</pre> +<span id="testTarget" style="border: 1px solid black;">testTarget</span> +</body> +</html> diff --git a/dom/events/test/test_bug432698.html b/dom/events/test/test_bug432698.html new file mode 100644 index 0000000000..8dec5c2cec --- /dev/null +++ b/dom/events/test/test_bug432698.html @@ -0,0 +1,223 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=432698 +--> +<head> + <title>Test for Bug 432698</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=432698">Mozilla Bug 432698</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 432698 **/ +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(runTests); +var outer; +var middle; +var inner; +var outside; +var container; +var file; +var iframe; +var checkRelatedTarget = false; +var expectedRelatedEnter = null; +var expectedRelatedLeave = null; +var mouseentercount = 0; +var mouseleavecount = 0; +var mouseovercount = 0; +var mouseoutcount = 0; + +function sendMouseEvent(t, elem) { + var r = elem.getBoundingClientRect(); + synthesizeMouse(elem, r.width / 2, r.height / 2, {type: t}); +} + +var expectedMouseEnterTargets = []; +var expectedMouseLeaveTargets = []; + +function runTests() { + outer = document.getElementById("outertest"); + middle = document.getElementById("middletest"); + inner = document.getElementById("innertest"); + outside = document.getElementById("outside"); + container = document.getElementById("container"); + file = document.getElementById("file"); + iframe = document.getElementById("iframe"); + + // Make sure ESM thinks mouse is outside the test elements. + sendMouseEvent("mousemove", outside); + + mouseentercount = 0; + mouseleavecount = 0; + mouseovercount = 0; + mouseoutcount = 0; + checkRelatedTarget = true; + expectedRelatedEnter = outside; + expectedRelatedLeave = inner; + expectedMouseEnterTargets = ["outertest", "middletest", "innertest"]; + sendMouseEvent("mousemove", inner); + is(mouseentercount, 3, "Unexpected mouseenter event count!"); + is(mouseovercount, 1, "Unexpected mouseover event count!"); + is(mouseoutcount, 0, "Unexpected mouseout event count!"); + is(mouseleavecount, 0, "Unexpected mouseleave event count!"); + expectedRelatedEnter = inner; + expectedRelatedLeave = outside; + expectedMouseLeaveTargets = ["innertest", "middletest", "outertest"]; + sendMouseEvent("mousemove", outside); + is(mouseentercount, 3, "Unexpected mouseenter event count!"); + is(mouseovercount, 1, "Unexpected mouseover event count!"); + is(mouseoutcount, 1, "Unexpected mouseout event count!"); + is(mouseleavecount, 3, "Unexpected mouseleave event count!"); + + // Event handling over native anonymous content. + var r = file.getBoundingClientRect(); + expectedRelatedEnter = outside; + expectedRelatedLeave = file; + synthesizeMouse(file, r.width / 6, r.height / 2, {type: "mousemove"}); + is(mouseentercount, 4, "Unexpected mouseenter event count!"); + is(mouseovercount, 2, "Unexpected mouseover event count!"); + is(mouseoutcount, 1, "Unexpected mouseout event count!"); + is(mouseleavecount, 3, "Unexpected mouseleave event count!"); + + // Moving mouse over type="file" shouldn't cause mouseover/out/enter/leave events + synthesizeMouse(file, r.width - (r.width / 6), r.height / 2, {type: "mousemove"}); + is(mouseentercount, 4, "Unexpected mouseenter event count!"); + is(mouseovercount, 2, "Unexpected mouseover event count!"); + is(mouseoutcount, 1, "Unexpected mouseout event count!"); + is(mouseleavecount, 3, "Unexpected mouseleave event count!"); + + expectedRelatedEnter = file; + expectedRelatedLeave = outside; + sendMouseEvent("mousemove", outside); + is(mouseentercount, 4, "Unexpected mouseenter event count!"); + is(mouseovercount, 2, "Unexpected mouseover event count!"); + is(mouseoutcount, 2, "Unexpected mouseout event count!"); + is(mouseleavecount, 4, "Unexpected mouseleave event count!"); + + // Initialize iframe + iframe.contentDocument.documentElement.style.overflow = "hidden"; + iframe.contentDocument.body.style.margin = "0px"; + iframe.contentDocument.body.style.width = "100%"; + iframe.contentDocument.body.style.height = "100%"; + iframe.contentDocument.body.innerHTML = + "<div style='width: 100%; height: 50%; border: 1px solid black;'></div>" + + "<div style='width: 100%; height: 50%; border: 1px solid black;'></div>"; + iframe.contentDocument.body.offsetLeft; // flush + + iframe.contentDocument.body.firstChild.onmouseenter = menter; + iframe.contentDocument.body.firstChild.onmouseleave = mleave; + iframe.contentDocument.body.lastChild.onmouseenter = menter; + iframe.contentDocument.body.lastChild.onmouseleave = mleave; + r = iframe.getBoundingClientRect(); + expectedRelatedEnter = outside; + expectedRelatedLeave = iframe; + // Move mouse inside the iframe. + synthesizeMouse(iframe.contentDocument.body, r.width / 2, r.height / 4, {type: "mousemove"}, + iframe.contentWindow); + synthesizeMouse(iframe.contentDocument.body, r.width / 2, r.height - (r.height / 4), {type: "mousemove"}, + iframe.contentWindow); + is(mouseentercount, 7, "Unexpected mouseenter event count!"); + expectedRelatedEnter = iframe; + expectedRelatedLeave = outside; + sendMouseEvent("mousemove", outside); + is(mouseleavecount, 7, "Unexpected mouseleave event count!"); + + checkRelatedTarget = false; + + iframe.contentDocument.body.firstChild.onmouseenter = null; + iframe.contentDocument.body.firstChild.onmouseleave = null; + iframe.contentDocument.body.lastChild.onmouseenter = null; + iframe.contentDocument.body.lastChild.onmouseleave = null; + + container.onmouseenter = null; + container.onmouseleave = null; + container.onmouseout = null; + container.onmouseover = null; + + var children = container.getElementsByTagName('*'); + for (var i=0;i<children.length;i++) { + children[i].onmouseenter = null; + children[i].onmouseleave = null; + children[i].onmouseout = null; + children[i].onmouseover = null; + } + + SimpleTest.finish(); +} + +function menter(evt) { + ++mouseentercount; + evt.stopPropagation(); + if (expectedMouseEnterTargets.length) { + var t = expectedMouseEnterTargets.shift(); + is(evt.target.id, t, "Wrong event target!"); + } + is(evt.bubbles, false, evt.type + " should not bubble!"); + is(evt.cancelable, false, evt.type + " is not cancelable!"); + is(evt.target, evt.currentTarget, "Wrong event target!"); + ok(!evt.relatedTarget || evt.target.ownerDocument == evt.relatedTarget.ownerDocument, + "Leaking nodes to another document?"); + if (checkRelatedTarget && evt.target.ownerDocument == document) { + is(evt.relatedTarget, expectedRelatedEnter, "Wrong related target (mouseenter)"); + } +} + +function mleave(evt) { + ++mouseleavecount; + evt.stopPropagation(); + if (expectedMouseLeaveTargets.length) { + var t = expectedMouseLeaveTargets.shift(); + is(evt.target.id, t, "Wrong event target!"); + } + is(evt.bubbles, false, evt.type + " should not bubble!"); + is(evt.cancelable, false, evt.type + " is not cancelable!"); + is(evt.target, evt.currentTarget, "Wrong event target!"); + ok(!evt.relatedTarget || evt.target.ownerDocument == evt.relatedTarget.ownerDocument, + "Leaking nodes to another document?"); + if (checkRelatedTarget && evt.target.ownerDocument == document) { + is(evt.relatedTarget, expectedRelatedLeave, "Wrong related target (mouseleave)"); + } +} + +function mover(evt) { + ++mouseovercount; + evt.stopPropagation(); +} + +function mout(evt) { + ++mouseoutcount; + evt.stopPropagation(); +} + +</script> +</pre> +<div id="container" onmouseenter="menter(event)" onmouseleave="mleave(event)" + onmouseout="mout(event)" onmouseover="mover(event)"> + <div id="outside" onmouseout="event.stopPropagation()" onmouseover="event.stopPropagation()">foo</div> + <div id="outertest" onmouseenter="menter(event)" onmouseleave="mleave(event)" + onmouseout="mout(event)" onmouseover="mover(event)"> + <div id="middletest" onmouseenter="menter(event)" onmouseleave="mleave(event)" + onmouseout="mout(event)" onmouseover="mover(event)"> + <div id="innertest" onmouseenter="menter(event)" onmouseleave="mleave(event)" + onmouseout="mout(event)" onmouseover="mover(event)">foo</div> + </div> + </div> + <input type="file" id="file" + onmouseenter="menter(event)" onmouseleave="mleave(event)" + onmouseout="mout(event)" onmouseover="mover(event)"> + <br> + <iframe id="iframe" width="50px" height="50px" + onmouseenter="menter(event)" onmouseleave="mleave(event)" + onmouseout="mout(event)" onmouseover="mover(event)"></iframe> +</div> +</body> +</html> diff --git a/dom/events/test/test_bug443985.html b/dom/events/test/test_bug443985.html new file mode 100644 index 0000000000..01180be00e --- /dev/null +++ b/dom/events/test/test_bug443985.html @@ -0,0 +1,76 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=443985 +--> +<head> + <title>Test for Bug 443985</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=443985">Mozilla Bug 443985</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +/** Test for Bug 443985 **/ + + +function listenerForNoScroll(evt) { + is(evt.clientX, evt.pageX, "Wrong .pageX"); + is(evt.clientY, evt.pageY, "Wrong .pageY"); + is(evt.screenX, 0, "Wrong .screenX"); + is(evt.screenY, 0, "Wrong .screenY"); + is(evt.clientX, 10, "Wrong .clientX"); + is(evt.clientY, 10, "Wrong .clientY"); +} + +function listenerForScroll(evt) { + isnot(evt.clientX, evt.pageX, "Wrong .pageX"); + isnot(evt.clientY, evt.pageY, "Wrong .pageY"); + ok(evt.pageX > 3000, "Wrong .pageX"); + ok(evt.pageY > 3000, "Wrong .pageY"); + is(evt.screenX, 0, "Wrong .screenX"); + is(evt.screenY, 0, "Wrong .screenY"); + is(evt.clientX, 10, "Wrong .clientX"); + is(evt.clientY, 10, "Wrong .clientY"); +} + +function doTest() { + window.scrollTo(0, 0); + var target = document.getElementById("testTarget"); + target.addEventListener("click", listenerForNoScroll, true); + var me = document.createEvent("MouseEvent"); + me.initMouseEvent("click", true, true, window, 0, 0, 0, 10, 10, + false, false, false, false, 0, null); + target.dispatchEvent(me); + target.removeEventListener("click", listenerForNoScroll, true); + + target.scrollIntoView(true); + target.addEventListener("click", listenerForScroll, true); + me = document.createEvent("MouseEvent"); + me.initMouseEvent("click", true, true, window, 0, 0, 0, 10, 10, + false, false, false, false, 0, null); + target.dispatchEvent(me); + target.addEventListener("click", listenerForNoScroll, true); + + document.getElementsByTagName("a")[0].scrollIntoView(true); + SimpleTest.finish(); +} + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(doTest); + +</script> +</pre> +<div style="min-height: 4000px; min-width: 4000px;"></div> +<div style="min-width: 4000px; text-align: right;"> + <span id="testTarget" style="border: 1px solid black;">testTarget</span> +</div> +</body> +</html> + diff --git a/dom/events/test/test_bug447736.html b/dom/events/test/test_bug447736.html new file mode 100644 index 0000000000..0e6bca10e3 --- /dev/null +++ b/dom/events/test/test_bug447736.html @@ -0,0 +1,47 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=447736 +--> +<head> + <title>Test for Bug 447736</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=447736">Mozilla Bug 447736</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<div id="secondTarget"></div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +/** Test for Bug 447736 **/ + +var loadEvent = null; +window.addEventListener("load", + function (evt) { + is(evt.target, window.document, "Wrong target!"); + is(evt.originalTarget, window.document, "Wrong originalTarget!"); + ok(evt.isTrusted, "Event should be trusted!"); + loadEvent = evt; + setTimeout("st.dispatchEvent(loadEvent)", 0); + }, true); + +var st = document.getElementById("secondTarget"); +st.addEventListener("load", + function (evt) { + is(evt.target, st, "Wrong target! (2)"); + is(evt.originalTarget, st, "Wrong originalTarget! (2)"); + ok(!evt.isTrusted, "Event shouldn't be trusted anymore!"); + SimpleTest.finish(); + }, true); + +SimpleTest.waitForExplicitFinish(); +</script> +</pre> +</body> +</html> + diff --git a/dom/events/test/test_bug448602.html b/dom/events/test/test_bug448602.html new file mode 100644 index 0000000000..5c07689427 --- /dev/null +++ b/dom/events/test/test_bug448602.html @@ -0,0 +1,294 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=448602 +--> +<head> + <title>Test for Bug 448602</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=448602">Mozilla Bug 448602</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 448602 **/ + +var els, root, l2, l3; + +var handlerCalled = false; +var capturingListenerCalled = false; +var bubblingListenerCalled = false; + +function clearListenerStates() { + handlerCalled = false; + capturingListenerCalled = false; + bubblingListenerCalled = false; +} + +function runTests() { + els = SpecialPowers.Cc["@mozilla.org/eventlistenerservice;1"] + .getService(SpecialPowers.Ci.nsIEventListenerService); + + // Event listener info tests + root = document.getElementById("testroot"); + var infos = els.getListenerInfoFor(root); + is(infos.length, 0, "Element shouldn't have listeners (1)"); + + var listenerSource = 'handlerCalled = true;'; + root.setAttribute("onclick", listenerSource); + infos = els.getListenerInfoFor(root); + is(infos.length, 1, "Element should have listeners (1)"); + is(infos[0].toSource(), 'function onclick(event) {\n' + listenerSource + '\n}', + "Unexpected serialization (1)"); + is(infos[0].type, "click", "Wrong type (1)"); + is(infos[0].capturing, false, "Wrong phase (1)"); + is(infos[0].allowsUntrusted, true, "Should allow untrusted events (1)"); + is(SpecialPowers.unwrap(infos[0].listenerObject), root.onclick, + "Should have the right listener object (1)"); + + // Test disabling and enabling the listener. + ok(!handlerCalled); + root.click(); + ok(handlerCalled); + + clearListenerStates() + infos[0].enabled = false; + root.click(); + ok(!handlerCalled); + + clearListenerStates() + infos[0].enabled = true; + root.click(); + ok(handlerCalled); + clearListenerStates(); + + function capturingListener() { + capturingListenerCalled = true; + } + function bubblingListener() { + bubblingListenerCalled = true; + } + root.addEventListener("click", capturingListener, true); + root.addEventListener("click", bubblingListener); + root.addEventListener("fooevent", capturingListener, true); + root.addEventListener("fooevent", bubblingListener); + infos = els.getListenerInfoFor(root); + // Use a child node to dispatch events so that both capturing and bubbling + // listeners get called. + l2 = document.getElementById("testlevel2"); + l2.click(); + ok(handlerCalled); + ok(capturingListenerCalled); + ok(bubblingListenerCalled); + clearListenerStates(); + + infos[0].enabled = false; + l2.click(); + ok(!handlerCalled); + ok(capturingListenerCalled); + ok(bubblingListenerCalled); + clearListenerStates(); + infos[0].enabled = true; + + infos[1].enabled = false; + l2.click(); + ok(handlerCalled); + ok(!capturingListenerCalled); + ok(bubblingListenerCalled); + clearListenerStates(); + infos[1].enabled = true; + + infos[2].enabled = false; + l2.click(); + ok(handlerCalled); + ok(capturingListenerCalled); + ok(!bubblingListenerCalled); + clearListenerStates(); + infos[2].enabled = true; + + root.removeEventListener("click", capturingListener, true); + root.removeEventListener("click", bubblingListener); + root.removeEventListener("fooevent", capturingListener, true); + root.removeEventListener("fooevent", bubblingListener); + root.removeAttribute("onclick"); + + root.setAttribute("onclick", "...invalid script..."); + SimpleTest.expectUncaughtException(true); + infos = els.getListenerInfoFor(root); + SimpleTest.expectUncaughtException(false); + is(infos.length, 1); + is(infos[0].listenerObject, null); + + root.removeAttribute("onclick"); + infos = els.getListenerInfoFor(root); + is(infos.length, 0, "Element shouldn't have listeners (2)"); + + var l = function (e) { alert(e); }; + root.addEventListener("foo", l, true, true); + root.addEventListener("foo", l, false, false); + infos = els.getListenerInfoFor(root); + is(infos.length, 2, "Element should have listeners (2)"); + is(infos[0].toSource(), "(function (e) { alert(e); })", + "Unexpected serialization (2)"); + is(infos[0].type, "foo", "Wrong type (2)"); + is(infos[0].capturing, true, "Wrong phase (2)"); + is(infos[0].allowsUntrusted, true, "Should allow untrusted events (2)"); + is(SpecialPowers.unwrap(infos[0].listenerObject), l, + "Should have the right listener object (2)"); + is(infos[1].toSource(), "(function (e) { alert(e); })", + "Unexpected serialization (3)"); + is(infos[1].type, "foo", "Wrong type (3)"); + is(infos[1].capturing, false, "Wrong phase (3)"); + is(infos[1].allowsUntrusted, false, "Shouldn't allow untrusted events (1)"); + is(SpecialPowers.unwrap(infos[1].listenerObject), l, + "Should have the right listener object (3)"); + + root.removeEventListener("foo", l, true); + root.removeEventListener("foo", l); + infos = els.getListenerInfoFor(root); + is(infos.length, 0, "Element shouldn't have listeners (3)"); + + root.onclick = l; + infos = els.getListenerInfoFor(root); + is(infos.length, 1, "Element should have listeners (3)"); + is(infos[0].toSource(), '(function (e) { alert(e); })', + "Unexpected serialization (4)"); + is(infos[0].type, "click", "Wrong type (4)"); + is(infos[0].capturing, false, "Wrong phase (4)"); + is(infos[0].allowsUntrusted, true, "Should allow untrusted events (3)"); + is(SpecialPowers.unwrap(infos[0].listenerObject), l, + "Should have the right listener object (4)"); + + // Event target chain tests + l3 = document.getElementById("testlevel3"); + var textnode = l3.firstChild; + var chain = els.getEventTargetChainFor(textnode, true); + ok(chain.length > 3, "Too short event target chain."); + ok(SpecialPowers.compare(chain[0], textnode), "Wrong chain item (1)"); + ok(SpecialPowers.compare(chain[1], l3), "Wrong chain item (2)"); + ok(SpecialPowers.compare(chain[2], l2), "Wrong chain item (3)"); + ok(SpecialPowers.compare(chain[3], root), "Wrong chain item (4)"); + + var hasDocumentInChain = false; + var hasWindowInChain = false; + for (var i = 0; i < chain.length; ++i) { + if (SpecialPowers.compare(chain[i], document)) { + hasDocumentInChain = true; + } else if (SpecialPowers.compare(chain[i], window)) { + hasWindowInChain = true; + } + } + + ok(hasDocumentInChain, "Should have document in event target chain!"); + ok(hasWindowInChain, "Should have window in event target chain!"); + + try { + els.getListenerInfoFor(null); + ok(false, "Should have thrown an exception."); + } catch (ex) { + ok(true, "We should be still running."); + } + setTimeout(testAllListener, 0); +} + +function dispatchTrusted(t, o) { + SpecialPowers.dispatchEvent(window, t, new Event("testevent", o)); +} + +function testAllListener() { + els = SpecialPowers.wrap(els); + var results = []; + var expectedResults = + [ { target: "testlevel3", phase: 3, trusted: false }, + { target: "testlevel3", phase: 3, trusted: false }, + { target: "testlevel3", phase: 3, trusted: true }, + { target: "testlevel3", phase: 3, trusted: true }, + { target: "testlevel3", phase: 3, trusted: true } + ]; + + function allListener(e) { + results.push({ + target: e.target.id, + phase: e.eventPhase, + trusted: e.isTrusted + }); + e.stopPropagation(); + } + function allListenerTrustedOnly(e) { + results.push({ + target: e.target.id, + phase: e.eventPhase, + trusted: e.isTrusted + }); + e.stopPropagation(); + } + + els.addListenerForAllEvents(root, allListener, false, true); + var infos = els.getListenerInfoFor(root); + var nullTypes = 0; + for (var i = 0; i < infos.length; ++i) { + if (infos[i].type == null) { + ++nullTypes; + } + } + is(nullTypes, 1, "Should have one all-event-listener!"); + + els.addListenerForAllEvents(root, allListener, false, true, true); + els.addListenerForAllEvents(root, allListenerTrustedOnly, false, false, true); + l3.dispatchEvent(new Event("testevent", { bubbles: true, composed: true })); + dispatchTrusted(l3, { bubbles: true, composed: true }); + els.removeListenerForAllEvents(root, allListener, false); + els.removeListenerForAllEvents(root, allListener, false, true); + els.removeListenerForAllEvents(root, allListenerTrustedOnly, false, true); + // make sure removeListenerForAllEvents works. + l3.dispatchEvent(new Event("testevent", { bubbles: true, composed : true })); + dispatchTrusted(l3, { bubbles: true, composed: true }); + + // Test the order of event listeners. + var clickListenerCalled = false; + var allListenerCalled = false; + function clickListener() { + clickListenerCalled = true; + ok(allListenerCalled, "Should have called '*' listener before normal listener!"); + } + function allListener2() { + allListenerCalled = true; + ok(!clickListenerCalled, "Shouldn't have called click listener before '*' listener!"); + } + root.onclick = null; // Remove the listener added in earlier tests. + root.addEventListener("click", clickListener); + els.addListenerForAllEvents(root, allListener2, false, true); + l3.dispatchEvent(new MouseEvent("click", { bubbles: true })); + root.removeEventListener("click", clickListener); + els.removeListenerForAllEvents(root, allListener2, false); + ok(allListenerCalled, "Should have called '*' listener"); + ok(clickListenerCalled, "Should have called click listener"); + + is(results.length, expectedResults.length, "count"); + for (var i = 0; i < expectedResults.length; ++i) { + for (var p in expectedResults[i]) { + is(results[i][p], expectedResults[i][p], p); + } + } + SimpleTest.finish(); +} + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(runTests); +</script> +</pre> +<div id="testroot"> + <div id="testlevel2"> + <div id="testlevel3"> + Test + </div> + </div> +</div> +</body> +</html> diff --git a/dom/events/test/test_bug450876.html b/dom/events/test/test_bug450876.html new file mode 100644 index 0000000000..1f522a9e79 --- /dev/null +++ b/dom/events/test/test_bug450876.html @@ -0,0 +1,47 @@ +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=450876 +--> +<head> + <title>Test for Bug 450876 - Crash [@ nsEventStateManager::GetNextTabbableMapArea] with img usemap and tabindex</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=450876">Mozilla Bug 450876</a> +<p id="display"><a href="#" id="a">link to focus from</a><img usemap="#a" tabindex="1"></p> +<div id="content" style="display: none"> + +</div> + +<pre id="test"> +<script class="testbody" type="text/javascript"> + +/** Test for Bug 450876 **/ + +function setTabFocus() { + // Override tab focus behavior on Mac */ + SpecialPowers.pushPrefEnv({ set: [[ "accessibility.tabfocus", 7 ]] }, doTest); +} + +function doTest() { + is(document.activeElement, document.body, "body element should be focused"); + document.getElementById('a').focus(); + is(document.activeElement, document.getElementById('a'), "link should have focus"); + is(document.hasFocus(), true, "document should be focused"); + synthesizeKey("KEY_Tab"); + is(document.activeElement, document.body, "body element should be focused"); + is(document.hasFocus(), false, "document should not be focused"); + + SimpleTest.finish(); +} + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(setTabFocus); + +</script> +</pre> +</body> +</html> + diff --git a/dom/events/test/test_bug456273.html b/dom/events/test/test_bug456273.html new file mode 100644 index 0000000000..a4fdda958a --- /dev/null +++ b/dom/events/test/test_bug456273.html @@ -0,0 +1,46 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=456273 +--> +<head> + <title>Test for Bug 456273</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=456273">Mozilla Bug 456273</a> +<p id="display">PASS if Firefox does not crash.</p> +<div id="content" style="display: none"> + +</div> + +<div id="edit456273" contenteditable="true">text</div> + +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 456273 **/ + +function doTest() { + var ev = new KeyboardEvent("keypress", { + bubbles: true, + cancelable: true, + view: null, + ctrlKey: true, + keyCode: 0, + charCode: "z".charCodeAt(0), + }); + SpecialPowers.dispatchEvent(window, document.getElementById('edit456273'), ev); + + ok(true, "PASS"); + SimpleTest.finish(); +} + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(doTest); + +</script> +</pre> +</body> +</html> diff --git a/dom/events/test/test_bug457672.html b/dom/events/test/test_bug457672.html new file mode 100644 index 0000000000..7be6b79eb2 --- /dev/null +++ b/dom/events/test/test_bug457672.html @@ -0,0 +1,55 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=457672 +--> +<head> + <title>Test for Bug 457672</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=457672">Mozilla Bug 457672</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 457672 **/ + +var windowBlurCount = 0; + +function listener(evt) { + if (evt.type == "focus") { + is(windowBlurCount, 1, + "Window should have got blur event when opening a new tab!"); + document.getElementsByTagName("a")[0].focus(); + SimpleTest.finish(); + } else if (evt.type == "blur") { + ++windowBlurCount; + } + document.getElementById('log').textContent += evt.target + ":" + evt.type + "\n"; +} + +function startTest() { + SpecialPowers.pushPrefEnv({"set": [["browser.link.open_newwindow", 3]]}, function() { + document.getElementsByTagName("a")[0].focus(); + // Note, focus/blur don't bubble + window.addEventListener("focus", listener); + window.addEventListener("blur", listener); + var subwin = window.open("about:blank", "", ""); + subwin.addEventListener("focus", function(e) { subwin.close(); }); + }); +} + +addLoadEvent(startTest); +SimpleTest.waitForExplicitFinish(); + +</script> +</pre> +<pre id="log"> +</pre> +</body> +</html> diff --git a/dom/events/test/test_bug489671.html b/dom/events/test/test_bug489671.html new file mode 100644 index 0000000000..4def80cba1 --- /dev/null +++ b/dom/events/test/test_bug489671.html @@ -0,0 +1,55 @@ +<!doctype html> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=489671 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 489671</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"> +</head> +<body> +<a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=489671" + >Mozilla Bug 489671</a> +<p id="display" onclick="queueNextTest(); throw 'Got click 1';"></p> +<script> +// override window.onerror so it won't see our exceptions +window.onerror = function() {} + +var testNum = 0; +function doTest() { + switch(testNum++) { + case 0: + var event = document.createEvent("MouseEvents"); + event.initMouseEvent("click", true, true, document.defaultView, + 0, 0, 0, 0, 0, false, false, false, false, 0, null); + $("display").dispatchEvent(event); + break; + case 1: + var script = document.createElement("script"); + script.textContent = "queueNextTest(); throw 'Got click 2'"; + document.body.appendChild(script); + break; + case 2: + window.setTimeout("queueNextTest(); throw 'Got click 3'", 0); + break; + case 3: + SimpleTest.endMonitorConsole(); + return; + } +} +function queueNextTest() { SimpleTest.executeSoon(doTest); } + +SimpleTest.waitForExplicitFinish(); +SimpleTest.monitorConsole(SimpleTest.finish, [ + { errorMessage: "uncaught exception: Got click 1" }, + { errorMessage: "uncaught exception: Got click 2" }, + { errorMessage: "uncaught exception: Got click 3" } +]); + +doTest(); +</script> +</body> +</html> diff --git a/dom/events/test/test_bug493251.html b/dom/events/test/test_bug493251.html new file mode 100644 index 0000000000..e7c0be2d3a --- /dev/null +++ b/dom/events/test/test_bug493251.html @@ -0,0 +1,181 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=493251 +--> +<head> + <title>Test for Bug 493251</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=493251">Mozilla Bug 493251</a> +<p id="display"> +</p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 493251 **/ + + var win; + + var mouseDown = 0; + var mouseUp = 0; + var mouseClick = 0; + + var keyDown = 0; + var keyPress = 0; + var keyUp = 0; + + function suppressEventHandling(aSuppress) { + var utils = SpecialPowers.getDOMWindowUtils(win); + ok(true, "suppressEventHandling: aSuppress=" + aSuppress); + utils.suppressEventHandling(aSuppress); + } + + function dispatchMouseEvent(aType, aX, aY, aButton, aClickCount, aModifiers) { + var utils = SpecialPowers.getDOMWindowUtils(win); + ok(true, "Dipatching mouse event: aType=" + aType + ", aX=" + aX + ", aY" + + aY + ", aButton=" + aButton + ", aClickCount=" + aClickCount + + ", aModifiers=" + aModifiers); + utils.sendMouseEvent(aType, aX, aY, aButton, aClickCount, aModifiers); + } + + function dumpEvent(aEvent) { + var detail = "target=" + aEvent.target + ", originalTarget=" + + aEvent.originalTarget + ", defaultPrevented=" + + aEvent.defaultPrevented + ", isTrusted=" + aEvent.isTrusted; + switch (aEvent.type) { + case "keydown": + case "keypress": + case "keyup": + detail += ", charCode=0x" + aEvent.charCode.toString(16) + + ", keyCode=0x" + aEvent.keyCode.toString(16) + + ", altKey=" + (aEvent.altKey ? "PRESSED" : "no") + + ", ctrlKey=" + (aEvent.ctrlKey ? "PRESSED" : "no") + + ", shiftKey=" + (aEvent.shiftKey ? "PRESSED" : "no") + + ", metaKey=" + (aEvent.metaKey ? "PRESSED" : "no"); + break; + case "mousedown": + case "mouseup": + case "click": + detail += ", screenX=" + aEvent.screenX + ", screenY=" + aEvent.screenY + + ", clientX=" + aEvent.clientX + ", clientY=" + aEvent.clientY + + ", altKey=" + (aEvent.altKey ? "PRESSED" : "no") + + ", ctrlKey=" + (aEvent.ctrlKey ? "PRESSED" : "no") + + ", shiftKey=" + (aEvent.shiftKey ? "PRESSED" : "no") + + ", metaKey=" + (aEvent.metaKey ? "PRESSED" : "no") + + ", button=" + aEvent.button + + ", relatedTarget=" + aEvent.relatedTarget; + break; + } + ok(true, aEvent.type + " event is handled: " + detail); + + var fm = SpecialPowers.Cc["@mozilla.org/focus-manager;1"]. + getService(SpecialPowers.Ci.nsIFocusManager); + ok(true, "focused element is \"" + fm.focusedElement + + "\" and focused window is \"" + fm.focusedWindow + + "\" (the testing window is \"" + win + "\""); + } + + function doTest() { + win.document.getElementsByTagName("input")[0].focus(); + win.addEventListener("keydown", + function(e) { dumpEvent(e); ++keyDown; }, true); + win.addEventListener("keypress", + function(e) { dumpEvent(e); ++keyPress; }, true); + win.addEventListener("keyup", + function(e) { dumpEvent(e); ++keyUp; }, true); + win.addEventListener("mousedown", + function(e) { dumpEvent(e); ++mouseDown; }, true); + win.addEventListener("mouseup", + function(e) { dumpEvent(e); ++mouseUp; }, true); + win.addEventListener("click", + function(e) { dumpEvent(e); ++mouseClick; }, true); + + ok(true, "doTest #1..."); + synthesizeKey("a", {}, win); + is(keyDown, 1, "Wrong number events (1)"); + is(keyPress, 1, "Wrong number events (2)"); + is(keyUp, 1, "Wrong number events (3)"); + + ok(true, "doTest #2..."); + suppressEventHandling(true); + synthesizeKey("a", {}, win); + is(keyDown, 1, "Wrong number events (4)"); + is(keyPress, 1, "Wrong number events (5)"); + is(keyUp, 1, "Wrong number events (6)"); + suppressEventHandling(false); + is(keyDown, 1, "Wrong number events (7)"); + is(keyPress, 1, "Wrong number events (8)"); + is(keyUp, 1, "Wrong number events (9)"); + + setTimeout(continueTest1, 0); + } + + function continueTest1() { + ok(true, "continueTest1..."); + win.addEventListener("keydown", () => { suppressEventHandling(true); }, {once: true}); + synthesizeKey("a", {}, win); + is(keyDown, 2, "Wrong number events (10)"); + is(keyPress, 1, "Wrong number events (11)"); + is(keyUp, 1, "Wrong number events (12)"); + suppressEventHandling(false); + setTimeout(continueTest2, 0); + } + + function continueTest2() { + ok(true, "continueTest2 #1..."); + is(keyDown, 2, "Wrong number events (13)"); + is(keyPress, 2, "Wrong number events (14)"); + is(keyUp, 2, "Wrong number events (15)"); + + dispatchMouseEvent("mousedown", 5, 5, 0, 1, 0); + dispatchMouseEvent("mouseup", 5, 5, 0, 1, 0); + is(mouseDown, 1, "Wrong number events (16)"); + is(mouseUp, 1, "Wrong number events (17)"); + is(mouseClick, 1, "Wrong number events (18)"); + + ok(true, "continueTest2 #2..."); + suppressEventHandling(true); + dispatchMouseEvent("mousedown", 5, 5, 0, 1, 0); + dispatchMouseEvent("mouseup", 5, 5, 0, 1, 0); + suppressEventHandling(false); + is(mouseDown, 1, "Wrong number events (19)"); + is(mouseUp, 1, "Wrong number events (20)"); + is(mouseClick, 1, "Wrong number events (21)"); + + setTimeout(continueTest3, 0); + } + + function continueTest3() { + ok(true, "continueTest3..."); + dispatchMouseEvent("mousedown", 5, 5, 0, 1, 0); + suppressEventHandling(true); + dispatchMouseEvent("mouseup", 5, 5, 0, 1, 0); + suppressEventHandling(false); + setTimeout(continueTest4, 1000); + } + + function continueTest4() { + ok(true, "continueTest4..."); + is(mouseDown, 2, "Wrong number events (19)"); + is(mouseUp, 2, "Wrong number events (20)"); + is(mouseClick, 2, "Wrong number events (21)"); + win.close(); + SimpleTest.finish(); + } + + + SimpleTest.waitForExplicitFinish(); + SimpleTest.requestFlakyTimeout("untriaged"); + win = window.open("window_bug493251.html", "_blank" , "width=500,height=500"); + +</script> +</pre> +</body> +</html> diff --git a/dom/events/test/test_bug508479.html b/dom/events/test/test_bug508479.html new file mode 100644 index 0000000000..4967dd1ce8 --- /dev/null +++ b/dom/events/test/test_bug508479.html @@ -0,0 +1,110 @@ +<html> +<head> + <title>Tests for the dragstart event</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + +<script> + +var gGotHandlingDrop = false; +var gGotNotHandlingDrop = false; + +SimpleTest.waitForExplicitFinish(); + +function fireEvent(target, event) { + SpecialPowers.DOMWindowUtils.dispatchDOMEventViaPresShellForTesting(target, event); +} + +async function fireDrop(element, shouldAllowDrop, shouldAllowOnlyChromeDrop) { + var ds = SpecialPowers.Cc["@mozilla.org/widget/dragservice;1"]. + getService(SpecialPowers.Ci.nsIDragService); + + var dataTransfer; + var trapDrag = function(event) { + dataTransfer = event.dataTransfer; + dataTransfer.setData("text/plain", "Hello");; + dataTransfer.dropEffect = "move"; + event.preventDefault(); + event.stopPropagation(); + } + + // need to use real mouse action + window.addEventListener("dragstart", trapDrag, true); + await synthesizePlainDragAndDrop({ + srcElement: element, + stepX: 9, + stepY: 9, + expectCancelDragStart: true, + }); + window.removeEventListener("dragstart", trapDrag, true); + + ds.startDragSessionForTests( + SpecialPowers.Ci.nsIDragService.DRAGDROP_ACTION_MOVE | + SpecialPowers.Ci.nsIDragService.DRAGDROP_ACTION_COPY | + SpecialPowers.Ci.nsIDragService.DRAGDROP_ACTION_LINK + ); // Session for emulating dnd coming from another app. + try { + var event = document.createEvent("DragEvent"); + event.initDragEvent("dragover", true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null, dataTransfer); + fireEvent(element, event); + + is(ds.getCurrentSession().canDrop, shouldAllowDrop, "Unexpected .canDrop"); + is(ds.getCurrentSession().onlyChromeDrop, shouldAllowOnlyChromeDrop, + "Unexpected .onlyChromeDrop"); + + event = document.createEvent("DragEvent"); + event.initDragEvent("drop", true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null, dataTransfer); + fireEvent(element, event); + } finally { + ds.endDragSession(false); + ok(!ds.getCurrentSession(), "There shouldn't be a drag session anymore!"); + } +} + +var chromeGotEvent = false; +function chromeListener(e) { + chromeGotEvent = true; +} + +async function runTests() +{ + var targetHandling = document.getElementById("handling_target"); + await fireDrop(targetHandling, true, false); + + is(gGotHandlingDrop, true, "Got drop on accepting element (1)"); + is(gGotNotHandlingDrop, false, "Didn't get drop on unaccepting element (1)"); + + // reset + gGotHandlingDrop = false; + gGotNotHandlingDrop = false; + + SpecialPowers.addChromeEventListener("drop", chromeListener, true, false); + var targetNotHandling = document.getElementById("nothandling_target"); + await fireDrop(targetNotHandling, true, true); + SpecialPowers.removeChromeEventListener("drop", chromeListener, true); + ok(chromeGotEvent, "Chrome should have got drop event!"); + is(gGotHandlingDrop, false, "Didn't get drop on accepting element (2)"); + is(gGotNotHandlingDrop, false, "Didn't get drop on unaccepting element (2)"); + + SimpleTest.finish(); +} + +</script> + +<body onload="window.setTimeout(runTests, 0);"> + +<img style="width: 100px; height: 100px;" + src="data:image/png,%89PNG%0D%0A%1A%0A%00%00%00%0DIHDR%00%00%00%18%00%00%00%18%02%03%00%00%00%9D%19%D5k%00%00%00%04gAMA%00%00%B1%8F%0B%FCa%05%00%00%00%0CPLTE%FF%FF%FF%FF%FF%FF%F7%DC%13%00%00%00%03%80%01X%00%00%00%01tRNS%08N%3DPT%00%00%00%01bKGD%00%88%05%1DH%00%00%00%09pHYs%00%00%0B%11%00%00%0B%11%01%7Fd_%91%00%00%00%07tIME%07%D2%05%0C%14%0C%0D%D8%3F%1FQ%00%00%00%5CIDATx%9C%7D%8E%CB%09%C0%20%10D%07r%B7%20%2F%E9wV0%15h%EA%D9%12D4%BB%C1x%CC%5C%1E%0C%CC%07%C0%9C0%9Dd7()%C0A%D3%8D%E0%B8%10%1DiCHM%D0%AC%D2d%C3M%F1%B4%E7%FF%10%0BY%AC%25%93%CD%CBF%B5%B2%C0%3Alh%CD%AE%13%DF%A5%F7%E0%03byW%09A%B4%F3%E2%00%00%00%00IEND%AEB%60%82" + id="handling_target" + ondragenter="event.preventDefault()" + ondragover="event.preventDefault()" + ondrop="gGotHandlingDrop = true;"> + +<img style="width: 100px; height: 100px;" + src="data:image/png,%89PNG%0D%0A%1A%0A%00%00%00%0DIHDR%00%00%00%18%00%00%00%18%02%03%00%00%00%9D%19%D5k%00%00%00%04gAMA%00%00%B1%8F%0B%FCa%05%00%00%00%0CPLTE%FF%FF%FF%FF%FF%FF%F7%DC%13%00%00%00%03%80%01X%00%00%00%01tRNS%08N%3DPT%00%00%00%01bKGD%00%88%05%1DH%00%00%00%09pHYs%00%00%0B%11%00%00%0B%11%01%7Fd_%91%00%00%00%07tIME%07%D2%05%0C%14%0C%0D%D8%3F%1FQ%00%00%00%5CIDATx%9C%7D%8E%CB%09%C0%20%10D%07r%B7%20%2F%E9wV0%15h%EA%D9%12D4%BB%C1x%CC%5C%1E%0C%CC%07%C0%9C0%9Dd7()%C0A%D3%8D%E0%B8%10%1DiCHM%D0%AC%D2d%C3M%F1%B4%E7%FF%10%0BY%AC%25%93%CD%CBF%B5%B2%C0%3Alh%CD%AE%13%DF%A5%F7%E0%03byW%09A%B4%F3%E2%00%00%00%00IEND%AEB%60%82" + id="nothandling_target" + ondrop="gGotNotHandlingDrop = true;"> + +</body> +</html> diff --git a/dom/events/test/test_bug517851.html b/dom/events/test/test_bug517851.html new file mode 100644 index 0000000000..a550ea55be --- /dev/null +++ b/dom/events/test/test_bug517851.html @@ -0,0 +1,122 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=517851 +--> +<head> + <title>Test for Bug 517851</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=517851">Mozilla Bug 517851</a> +<p id="display"></p> +<div id="content" style="display: none"> + <iframe id="subframe"></iframe> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 517851 **/ +SimpleTest.waitForExplicitFinish(); + +addLoadEvent(function() { +window.handledCount = 0; +window.testReturnValue = false; +var target = document.createElement("div"); +var target2 = $("subframe").contentDocument.body; +target.setAttribute("onerror", "++window.handledCount; return window.testReturnValue;"); +target2.setAttribute("onerror", "++window.parent.handledCount; return window.parent.testReturnValue;"); +target.setAttribute("onmouseover", "++window.handledCount; return window.testReturnValue;"); +target.setAttribute("onbeforeunload", "++window.handledCount; return window.testReturnValue;"); +target2.setAttribute("onbeforeunload", "++window.parent.handledCount; return window.parent.testReturnValue;"); +target.setAttribute("onmousemove", "++window.handledCount; return window.testReturnValue;"); + +var e = new ErrorEvent("error", {bubbles: true, cancelable: true}); +window.testReturnValue = true; +is(target.dispatchEvent(e), window.testReturnValue, + "error event should not have reverse return value handling on div!"); +is(handledCount, 1, "Wrong event count!"); +window.testReturnValue = false; +is(target.dispatchEvent(e), window.testReturnValue, + "error event should not have reverse return value handling on div (2)!"); +is(handledCount, 2, "Wrong event count!"); + +var e = new ErrorEvent("error", {bubbles: true, cancelable: true}); +window.testReturnValue = false; +is(target2.dispatchEvent(e), !window.testReturnValue, + "error event should have reverse return value handling!"); +is(handledCount, 3, "Wrong event count!"); +window.testReturnValue = true; +is(target2.dispatchEvent(e), !window.testReturnValue, + "error event should have reverse return value handling (2)!"); +is(handledCount, 4, "Wrong event count!"); + +e = document.createEvent("MouseEvent"); +e.initEvent("mouseover", true, true); +window.testReturnValue = true; +is(target.dispatchEvent(e), window.testReturnValue, + "mouseover event should not have reverse return value handling!"); +is(handledCount, 5, "Wrong event count!"); +window.testReturnValue = false; +is(target.dispatchEvent(e), window.testReturnValue, + "mouseover event should not have reverse return value handling (2)!"); +is(handledCount, 6, "Wrong event count!"); + +e = document.createEvent("BeforeUnloadEvent"); +e.initEvent("beforeunload", true, true); +window.testReturnValue = true; +is(target.dispatchEvent(e), true, + "beforeunload event on random element should not be prevented!"); +is(handledCount, 6, "Wrong event count; handler should not have run!"); +is(target2.dispatchEvent(e), false, + "beforeunload event should be prevented!"); +is(handledCount, 7, "Wrong event count!"); +window.testReturnValue = false; +is(target.dispatchEvent(e), false, + "beforeunload event on random element should be prevented because the event was already cancelled!"); +is(handledCount, 7, "Wrong event count; handler should not have run! (2)"); + +e = document.createEvent("BeforeUnloadEvent"); +e.initEvent("beforeunload", true, true); +window.testReturnValue = false; +is(target.dispatchEvent(e), true, + "beforeunload event on random element should not be prevented (2)!"); +is(handledCount, 7, "Wrong event count; handler should not have run! (2)"); + +is(target2.dispatchEvent(e), false, + "beforeunload event should be prevented (2)!"); +is(handledCount, 8, "Wrong event count!"); + +// Create normal event for beforeunload. +e = document.createEvent("Event"); +e.initEvent("beforeunload", true, true); +window.testReturnValue = true; +is(target.dispatchEvent(e), true, + "beforeunload event shouldn't be prevented (3)!"); +is(handledCount, 8, "Wrong event count: handler should not have run(3)!"); +is(target2.dispatchEvent(e), true, + "beforeunload event shouldn't be prevented (3)!"); +is(handledCount, 9, "Wrong event count!"); + +e = document.createEvent("MouseEvent"); +e.initEvent("mousemove", true, true); +window.testReturnValue = true; +is(target.dispatchEvent(e), window.testReturnValue, + "mousemove event shouldn't have reverse return value handling!"); +is(handledCount, 10, "Wrong event count!"); +window.testReturnValue = false; +is(target.dispatchEvent(e), window.testReturnValue, + "mousemove event shouldn't have reverse return value handling (2)!"); +is(handledCount, 11, "Wrong event count!"); + +// Now unhook the beforeunload handler in the subframe, so we don't prompt to +// unload. +target2.onbeforeunload = null; + +SimpleTest.finish(); +}); +</script> +</pre> +</body> +</html> diff --git a/dom/events/test/test_bug524674.xhtml b/dom/events/test/test_bug524674.xhtml new file mode 100644 index 0000000000..d1ed83d8b1 --- /dev/null +++ b/dom/events/test/test_bug524674.xhtml @@ -0,0 +1,130 @@ +<?xml version="1.0"?> +<?xml-stylesheet type="text/css" href="chrome://global/skin"?> +<?xml-stylesheet type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=524674 +--> +<window title="Mozilla Bug 524674" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=524674" + target="_blank">Mozilla Bug 524674</a> + </body> + + <!-- test code goes here --> + <script type="application/javascript"> + <![CDATA[ + /** Test for Bug 524674 **/ + + var els = Cc["@mozilla.org/eventlistenerservice;1"] + .getService(Ci.nsIEventListenerService); + + function dummyListener() {} + + var runningTest = null; + var d = document.createElementNS("http://www.w3.org/1999/xhtml", "div"); + var xhr = new XMLHttpRequest(); + + // Test also double removals and such. + var tests = [ + function() { + els.addListenerChangeListener(changeListener); + d.addEventListener("foo", dummyListener); + d.addEventListener("foo", dummyListener); + xhr.addEventListener("foo", dummyListener); + tests[0] = [{target: d, listeners: ["onfoo"]}, + {target: xhr, listeners: ["onfoo"]}]; + }, + function() { + d.addEventListener("bar", dummyListener); + d.addEventListener("baz", dummyListener); + xhr.addEventListener("bar", dummyListener); + xhr.addEventListener("baz", dummyListener); + tests[0] = [{target: d, listeners: ["onbaz", "onbar"]}, + {target: xhr, listeners: ["onbaz", "onbar"]}]; + }, + function() { + d.onclick = dummyListener; + d.onclick = dummyListener; + xhr.onload = dummyListener; + tests[0] = [{target: d, listeners: ["onclick"]}, + {target: xhr, listeners: ["onload"]}]; + }, + function() { + d.onclick = function() {}; + tests[0] = [{target: d, listeners: ["onclick"]}]; + }, + function() { + d.removeEventListener("foo", dummyListener); + d.removeEventListener("foo", dummyListener); + xhr.removeEventListener("foo", dummyListener); + tests[0] = [{target: d, listeners: ["onfoo"]}, + {target: xhr, listeners: ["onfoo"]}]; + }, + function() { + d.removeEventListener("bar", dummyListener); + d.removeEventListener("baz", dummyListener); + xhr.removeEventListener("bar", dummyListener); + xhr.removeEventListener("baz", dummyListener); + tests[0] = [{target: d, listeners: ["onbar", "onbaz"]}, + {target: xhr, listeners: ["onbar", "onbaz"]}]; + }, + function() { + d.onclick = null; + d.onclick = null; + xhr.onload = null; + tests[0] = [{target: d, listeners: ["onclick"]}, + {target: xhr, listeners: ["onload"]}]; + }, + function() { + els.removeListenerChangeListener(changeListener); + // Check that once we've removed the change listener, it isn't called anymore. + d.addEventListener("foo", dummyListener); + xhr.addEventListener("foo", dummyListener); + SimpleTest.executeSoon(function() { + SimpleTest.finish(); + }); + } + ]; + + SimpleTest.executeSoon(tests[0]); + + function changeListener(array) { + if (typeof tests[0] == "function") { + return; + } + var expectedEventChanges = tests[0]; + var eventChanges = array.enumerate(); + var i = 0; + while (eventChanges.hasMoreElements() && i < expectedEventChanges.length) { + var current; + try { + current = eventChanges.getNext().QueryInterface(Ci.nsIEventListenerChange); + var expected = expectedEventChanges[i]; + + if (current.target == expected.target) { + is(current.target, expected.target, current.target + " = " + expected.target); + ++i; + } + } catch(ex) { + continue; + } + } + if (expectedEventChanges.length != i) { + return; + } + + is(expectedEventChanges.length, i, "Should have got notification for all the changes."); + tests.shift(); + + ok(tests.length); + SimpleTest.executeSoon(tests[0]); + } + + SimpleTest.waitForExplicitFinish(); + ]]> + </script> +</window> diff --git a/dom/events/test/test_bug534833.html b/dom/events/test/test_bug534833.html new file mode 100644 index 0000000000..d8b2000f25 --- /dev/null +++ b/dom/events/test/test_bug534833.html @@ -0,0 +1,156 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=534833 +--> +<head> + <title>Test for Bug 534833</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=534833">Mozilla Bug 534833</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 534833 **/ +SimpleTest.waitForExplicitFinish(); +addLoadEvent(runTests); + +var input1GotClick = 0; +var input2GotClick = 0; +var textarea1GotClick = 0; +var textarea2GotClick = 0; +var div1GotClick = 0; +var div2GotClick = 0; + +var tests = [ { element: "text", clickText: true }, + { element: "text2", clickText: false }, + { element: "area", clickText: true }, + { element: "area2", clickText: false }, + { element: "d", clickText: true }, + { element: "d", clickText: false }, + { element: "d2", clickText: true }, + { element: "d2", clickText: false } + ]; + +function nextTest_() { + if (!tests.length) { + finishTests(); + return; + } + + var test = tests.shift(); + var el = document.getElementById(test.element); + el.scrollIntoView(true); + if (test.clickText) { + synthesizeMouse(el, 5, 5, {type : "mousedown" }); + synthesizeMouse(el, 5, 5, {type : "mouseup" }); + } else { + synthesizeMouse(el, el.getBoundingClientRect().width - 5, 5, {type : "mousedown" }); + synthesizeMouse(el, el.getBoundingClientRect().width - 5, 5, {type : "mouseup" }); + } + nextTest(); +} + +function nextTest() { + var el = document.getElementById("initialfocus"); + + el.addEventListener("focus", function() { + setTimeout(nextTest_, 0); + }, {once: true}); + el.focus(); +} + +function runTests() { + var t = document.getElementById("text"); + var t2 = document.getElementById("text2"); + var a = document.getElementById("area"); + var a2 = document.getElementById("area2"); + var d = document.getElementById("d"); + var d2 = document.getElementById("d2"); + + // input 1 + t.onfocus = function(e) { + t.value = ""; + } + t.onclick = function(e) { + ++input1GotClick; + } + + // input 2 + t2.onfocus = function(e) { + t2.value = ""; + } + t2.onclick = function(e) { + ++input2GotClick; + } + + // textarea 1 + a.onfocus = function(e) { + a.value = ""; + } + a.onclick = function(e) { + ++textarea1GotClick; + } + + // textarea 2 + a2.onfocus = function(e) { + a2.value = ""; + } + a2.onclick = function(e) { + ++textarea2GotClick; + } + + // div 1 + var c = 0; + d.onmousedown = function(e) { + d.textContent = (++c) + " / click before or after |"; + } + d.onclick = function(e) { + ++div1GotClick; + } + + // div 2 + var c2 = 0; + d2.onmousedown = function(e) { + d2.firstChild.data = (++c2) + " / click before or after |"; + } + d2.onclick = function(e) { + ++div2GotClick; + } + nextTest(); +} + +function finishTests() { + is(input1GotClick, 1, "input element should have got a click!"); + is(input2GotClick, 1, "input element should have got a click! (2)"); + is(textarea1GotClick, 1, "textarea element should have got a click!"); + is(textarea2GotClick, 1, "textarea element should have got a click! (2)"); + is(div1GotClick, 2, "div element's content text was replaced, it should have got 2 click!"); + is(div2GotClick, 2, "div element's content text was modified, it should have got 2 clicks!"); + SimpleTest.finish(); +} + +</script> +</pre> +<input type="text" id="initialfocus"><br> +<input type="text" id="text" value="click before |" style="width: 95%;"><br> +<input type="text" id="text2" value="click after |" style="width: 95%;"> +<br> +<textarea id="area" rows="2" style="width: 95%;"> + click before + | +</textarea><br> +<textarea id="area2" rows="2" style="width: 95%;"> + click after | +</textarea> +<div id="d" style="border: 1px solid black;">click before or after |</div> +<div id="d2" style="border: 1px solid black;">click before or after |</div> +</body> +</html> diff --git a/dom/events/test/test_bug545268.html b/dom/events/test/test_bug545268.html new file mode 100644 index 0000000000..da4d0d1649 --- /dev/null +++ b/dom/events/test/test_bug545268.html @@ -0,0 +1,129 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=545268 +--> +<head> + <title>Test for Bug 545268</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=545268">Mozilla Bug 545268</a> +<p id="display"> +</p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 545268 + Like the test for bug 493251, but we test that suppressing events in + a parent window stops the events from reaching the child window. */ + + var win; + var subwin; + + var mouseDown = 0; + var mouseUp = 0; + var mouseClick = 0; + + var keyDown = 0; + var keyPress = 0; + var keyUp = 0; + + function doTest() { + var utils = SpecialPowers.getDOMWindowUtils(win); + var f = win.document.getElementById("f"); + subwin = f.contentWindow; + subwin.document.getElementsByTagName("input")[0].focus(); + subwin.addEventListener("keydown", function(e) { ++keyDown; }, true); + subwin.addEventListener("keypress", function(e) { ++keyPress; }, true); + subwin.addEventListener("keyup", function(e) { ++keyUp; }, true); + subwin.addEventListener("mousedown", function(e) { ++mouseDown; }, true); + subwin.addEventListener("mouseup", function(e) { ++mouseUp; }, true); + subwin.addEventListener("click", function(e) { ++mouseClick; }, true); + + synthesizeKey("a", {}, subwin); + is(keyDown, 1, "Wrong number events (1)"); + is(keyPress, 1, "Wrong number events (2)"); + is(keyUp, 1, "Wrong number events (3)"); + + // Test that suppressing events on the parent window prevents key + // events in the subdocument window + utils.suppressEventHandling(true); + synthesizeKey("a", {}, subwin); + is(keyDown, 1, "Wrong number events (4)"); + is(keyPress, 1, "Wrong number events (5)"); + is(keyUp, 1, "Wrong number events (6)"); + utils.suppressEventHandling(false); + is(keyDown, 1, "Wrong number events (7)"); + is(keyPress, 1, "Wrong number events (8)"); + is(keyUp, 1, "Wrong number events (9)"); + + setTimeout(continueTest1, 0); + } + + function continueTest1() { + var utils = SpecialPowers.getDOMWindowUtils(win); + subwin.addEventListener("keydown", () => { utils.suppressEventHandling(true); }, {once: true}); + synthesizeKey("a", {}, subwin); + is(keyDown, 2, "Wrong number events (10)"); + is(keyPress, 1, "Wrong number events (11)"); + is(keyUp, 1, "Wrong number events (12)"); + utils.suppressEventHandling(false); + setTimeout(continueTest2, 0); + } + + function continueTest2() { + var utils = SpecialPowers.getDOMWindowUtils(win); + is(keyDown, 2, "Wrong number events (13)"); + is(keyPress, 2, "Wrong number events (14)"); + is(keyUp, 2, "Wrong number events (15)"); + + utils.sendMouseEvent("mousedown", 5, 5, 0, 1, 0); + utils.sendMouseEvent("mouseup", 5, 5, 0, 1, 0); + is(mouseDown, 1, "Wrong number events (16)"); + is(mouseUp, 1, "Wrong number events (17)"); + is(mouseClick, 1, "Wrong number events (18)"); + + utils.suppressEventHandling(true); + utils.sendMouseEvent("mousedown", 5, 5, 0, 1, 0); + utils.sendMouseEvent("mouseup", 5, 5, 0, 1, 0); + utils.suppressEventHandling(false); + is(mouseDown, 1, "Wrong number events (19)"); + is(mouseUp, 1, "Wrong number events (20)"); + is(mouseClick, 1, "Wrong number events (21)"); + + setTimeout(continueTest3, 0); + } + + function continueTest3() { + var utils = SpecialPowers.getDOMWindowUtils(win); + utils.sendMouseEvent("mousedown", 5, 5, 0, 1, 0); + utils.suppressEventHandling(true); + utils.sendMouseEvent("mouseup", 5, 5, 0, 1, 0); + utils.suppressEventHandling(false); + setTimeout(continueTest4, 1000); + } + + function continueTest4() { + is(mouseDown, 2, "Wrong number events (19)"); + is(mouseUp, 2, "Wrong number events (20)"); + is(mouseClick, 2, "Wrong number events (21)"); + win.close(); + SimpleTest.finish(); + } + + + SimpleTest.waitForExplicitFinish(); + SimpleTest.requestFlakyTimeout("untriaged"); + win = window.open("bug545268.html", "" , ""); + win.onload = doTest; + +</script> +</pre> +</body> +</html> diff --git a/dom/events/test/test_bug547996-1.html b/dom/events/test/test_bug547996-1.html new file mode 100644 index 0000000000..6ff112c555 --- /dev/null +++ b/dom/events/test/test_bug547996-1.html @@ -0,0 +1,87 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=547996 +--> +<head> + <title>Test for Bug 547996</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=547996">Mozilla Bug 547996</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 547996 **/ +/* mouseEvent.mozInputSource attribute */ + +function prepareListener(eventName, expectedValue) { + return function(event) { + is(event.mozInputSource, expectedValue, "Correct .mozInputSource value in " + eventName); + }; +} + +const INPUT_SOURCE_UNKNOWN = MouseEvent.MOZ_SOURCE_UNKNOWN; +const INPUT_SOURCE_KEYBOARD = MouseEvent.MOZ_SOURCE_KEYBOARD; + +function doTest() { + var eventNames = [ + "mousedown", + "mouseup", + "click", + "dblclick", + "contextmenu", + "DOMMouseScroll", + "dragdrop", + "dragstart", + "dragend", + "dragenter", + "dragleave", + "dragover" + ]; + + var target = document.getElementById("testTarget"); + + for (var i in eventNames) { + for(var value = INPUT_SOURCE_UNKNOWN; value <= INPUT_SOURCE_KEYBOARD; value++) { + var eventName = eventNames[i]; + var listener = prepareListener(eventName, value); + + target.addEventListener(eventName, listener); + + var newEvent = document.createEvent("MouseEvent"); + newEvent.initNSMouseEvent(eventName, true, true, window, 0, 0, 0, 0, 0, + false, false, false, false, 0, null, 0, value); + target.dispatchEvent(newEvent); + target.removeEventListener(eventName, listener); + } + + // Events created by script that do not initialize the mozInputSource + // value should have the value MOZ_SOURCE_UNKNOWN + var listener = prepareListener(eventName, INPUT_SOURCE_UNKNOWN); + target.addEventListener(eventName, listener); + + var newEvent = document.createEvent("MouseEvent"); + newEvent.initMouseEvent(eventName, true, true, window, 0, 0, 0, 0, 0, + false, false, false, false, 0, null); + target.dispatchEvent(newEvent); + target.removeEventListener(eventName, listener); + + } + + SimpleTest.finish(); +} + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(doTest); + +</script> +</pre> +<span id="testTarget" style="border: 1px solid black;">testTarget</span> +</body> +</html> diff --git a/dom/events/test/test_bug547996-2.xhtml b/dom/events/test/test_bug547996-2.xhtml new file mode 100644 index 0000000000..e3f45bdb97 --- /dev/null +++ b/dom/events/test/test_bug547996-2.xhtml @@ -0,0 +1,120 @@ +<?xml version="1.0"?> +<html xmlns="http://www.w3.org/1999/xhtml" + xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=547996 +--> +<head> + <title>Test for Bug 547996</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=547996">Mozilla Bug 547996</a> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"> +<script type="application/javascript"><![CDATA[ + +/** Test for Bug 547996 **/ +/* mouseEvent.mozInputSource attribute */ + +var expectedInputSource = null; + +function check(event) { + is(event.mozInputSource, expectedInputSource, ".mozInputSource"); +} + +function doTest() { + setup(); + + expectedInputSource = MouseEvent.MOZ_SOURCE_KEYBOARD; + testKeyboard(); + + expectedInputSource = MouseEvent.MOZ_SOURCE_MOUSE; + testMouse(); + + expectedInputSource = MouseEvent.MOZ_SOURCE_UNKNOWN; + testScriptedClicks(); + + cleanup(); + SimpleTest.finish(); +} + +function testKeyboard() { + + $("inputTarget").focus(); + synthesizeKey("VK_SPACE", {}); + synthesizeKey("VK_RETURN", {}); + + $("buttonTarget").focus(); + synthesizeKey("VK_SPACE", {}); + synthesizeKey("VK_RETURN", {}); + + //XUL buttons do not generate click on ENTER or SPACE, + //they do only on accessKey + + $("anchorTarget").focus(); + synthesizeKey("VK_RETURN", {}); + + synthesizeKey("VK_TAB", {}); + synthesizeKey("VK_SPACE", {}); + synthesizeKey("VK_RIGHT", {}); + + $("checkboxTarget").focus(); + synthesizeKey("VK_SPACE", {}); + + var accessKeyDetails = (navigator.platform.includes("Mac")) ? + { ctrlKey : true } : { altKey : true, shiftKey: true }; + + synthesizeKey("o", accessKeyDetails); + synthesizeKey("t", accessKeyDetails); +} + +function testMouse() { + synthesizeMouse($("inputTarget"), 0, 0, {}); + synthesizeMouse($("buttonTarget"), 0, 0, {}); + synthesizeMouse($("anchorTarget"), 0, 0, {}); + synthesizeMouse($("radioTarget1"), 0, 0, {}); + synthesizeMouse($("radioTarget2"), 0, 0, {}); + synthesizeMouse($("checkboxTarget"), 0, 0, {}); +} + +function testScriptedClicks() { + $("inputTarget").click(); + $("buttonTarget").click(); +} + +function setup() { + $("inputTarget").addEventListener("click", check); + $("buttonTarget").addEventListener("click", check); + $("anchorTarget").addEventListener("click", check); + $("radioTarget1").addEventListener("click", check); + $("radioTarget2").addEventListener("click", check); + $("checkboxTarget").addEventListener("click", check); + +} + +function cleanup() { + $("inputTarget").removeEventListener("click", check); + $("buttonTarget").removeEventListener("click", check); + $("anchorTarget").removeEventListener("click", check); + $("radioTarget1").removeEventListener("click", check); + $("radioTarget2").removeEventListener("click", check); + $("checkboxTarget").removeEventListener("click", check); +} + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(doTest, window); + +]]></script> +</pre> +<input type="checkbox" id="checkboxTarget">Checkbox target</input> +<input id="inputTarget" type="button" value="HTML Input" accesskey="o"/> +<button id="buttonTarget">HTML Button</button> +<a href="#" id="anchorTarget">Anchor</a> +<input type="radio" id="radioTarget1" name="group">Radio Target 1</input> +<input type="radio" id="radioTarget2" name="group">Radio Target 2</input> +</body> +</html> diff --git a/dom/events/test/test_bug547996-3.xhtml b/dom/events/test/test_bug547996-3.xhtml new file mode 100644 index 0000000000..403949c17f --- /dev/null +++ b/dom/events/test/test_bug547996-3.xhtml @@ -0,0 +1,65 @@ +<?xml version="1.0"?> +<html xmlns="http://www.w3.org/1999/xhtml" + xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=547996 +--> +<head> + <title>Test for Bug 547996</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=547996">Mozilla Bug 547996</a> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"> +<script type="application/javascript"><![CDATA[ + +/** Test for Bug 547996 **/ +/* mouseEvent.mozInputSource attribute */ + +var expectedInputSource = null; + +function check(event) { + is(event.mozInputSource, expectedInputSource, ".mozInputSource"); +} + +function doTest() { + setup(); + + expectedInputSource = MouseEvent.MOZ_SOURCE_MOUSE; + testMouse(); + + expectedInputSource = MouseEvent.MOZ_SOURCE_UNKNOWN; + testScriptedClicks(); + + cleanup(); + SimpleTest.finish(); +} + +function testMouse() { + synthesizeMouse($("xulButtonTarget"), 0, 0, {}); +} + +function testScriptedClicks() { + $("xulButtonTarget").click(); +} + +function setup() { + $("xulButtonTarget").addEventListener("click", check); +} + +function cleanup() { + $("xulButtonTarget").removeEventListener("click", check); +} + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(doTest, window); + +]]></script> +</pre> +<xul:button id="xulButtonTarget" accesskey="t">XUL Button</xul:button> +</body> +</html> diff --git a/dom/events/test/test_bug556493.html b/dom/events/test/test_bug556493.html new file mode 100644 index 0000000000..e2b106942e --- /dev/null +++ b/dom/events/test/test_bug556493.html @@ -0,0 +1,74 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=556493 +--> +<head> + <title>Test for Bug 556493</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <style> + div { + border: 1px solid; + } + </style> +</head> +<body onload="setTimeout(runTest, 0)"> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=556493">Mozilla Bug 556493</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 556493 **/ + +SimpleTest.waitForExplicitFinish(); + +var downCount = 0; +var upCount = 0; +var clickCount = 0; +function runTest() { + var d0 = document.getElementById("d0"); + var d1 = document.getElementById("d1"); + var d2 = document.getElementById("d2"); + + d0.onmousedown = function(e) { ++downCount; }; + d0.onmouseup = function(e) { ++upCount; } + d0.onclick = function(e) { ++clickCount; } + + synthesizeMouse(d1, 3, 3, { type: "mousedown"}); + synthesizeMouse(d1, 3, 3, { type: "mouseup"}); + + is(downCount, 1, "Wrong mousedown event count!"); + is(upCount, 1, "Wrong mouseup event count!"); + is(clickCount, 1, "Wrong click event count!"); + + synthesizeMouse(d1, 3, 3, { type: "mousedown"}); + synthesizeMouse(d1, 30, 3, { type: "mouseup"}); + + is(downCount, 2, "Wrong mousedown event count!"); + is(upCount, 2, "Wrong mouseup event count!"); + is(clickCount, 2, "Wrong click event count!"); + + synthesizeMouse(d1, 3, 3, { type: "mousedown"}); + synthesizeMouse(d2, 3, 3, { type: "mouseup"}); + + is(downCount, 3, "Wrong mousedown event count!"); + is(upCount, 3, "Wrong mouseup event count!"); + is(clickCount, 3, "Wrong click event count!"); + + SimpleTest.finish(); +} + +</script> +</pre> +<div id="d0"> +Test divs -- +<div id="d1">t</div><div id="d2">t</div> +-- +</div> +</body> +</html> diff --git a/dom/events/test/test_bug563329.html b/dom/events/test/test_bug563329.html new file mode 100644 index 0000000000..fd4e9fd8a6 --- /dev/null +++ b/dom/events/test/test_bug563329.html @@ -0,0 +1,82 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=563329 +--> +<head> + <title>Test for Bug 563329</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=563329">Mozilla Bug 563329</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 563329 **/ +/* ui.click_hold_context_menus preference */ + +var target = null; +var testGen = getTests(); +var currentTest = null; + +function* getTests() { + let tests = [ + { "func": function() { setTimeout(doCheckContextMenu, 100)}, "message": "Context menu should has fired"}, + { "func": function() { setTimeout(doCheckDuration, 100)}, "message": "Context menu should has fired with delay"}, + { "func": function() { setTimeout(finishTest, 100)}, "message": "" } + ]; + + let i = 0; + while (i < tests.length) + yield tests[i++]; +} + +function doTest() { + target = document.getElementById("testTarget"); + + document.documentElement.addEventListener("contextmenu", function() { + SimpleTest.ok(true, currentTest.message); + synthesizeMouse(target, 0, 0, {type: "mouseup"}); + SimpleTest.executeSoon(function() { + currentTest = testGen.next(); + currentTest.func(); + }); + }); + + SimpleTest.executeSoon(function() { + currentTest = testGen.next(); + currentTest.func(); + }); +} + +function doCheckContextMenu() { + synthesizeMouse(target, 0, 0, {type: "mousedown"}); +} + +function doCheckDuration() { + var duration = 50; + + // Change click hold delay + SpecialPowers.pushPrefEnv({"set":[["ui.click_hold_context_menus.delay", duration]]}, function() { synthesizeMouse(target, 0, 0, {type: "mousedown"}); }); +} + +function finishTest() { + synthesizeKey("VK_ESCAPE", {}, window); + SimpleTest.finish(); +} + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(function() { + SpecialPowers.pushPrefEnv({"set":[["ui.click_hold_context_menus", true]]}, doTest); +}); +</script> +</pre> +<span id="testTarget" style="border: 1px solid black;">testTarget</span> +</body> +</html> diff --git a/dom/events/test/test_bug574663.html b/dom/events/test/test_bug574663.html new file mode 100644 index 0000000000..332bddbf1a --- /dev/null +++ b/dom/events/test/test_bug574663.html @@ -0,0 +1,194 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=574663 +--> +<head> + <title>Test for Bug 574663</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=574663">Mozilla Bug 574663</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script> + +/** Test for Bug 574663 **/ + +// SimpleTest's paint_listener does not work on other windows, so we inline +// a smaller version here. +function waitForPaint(win, utils, callback) { + win.document.documentElement.getBoundingClientRect(); + if (!utils.isMozAfterPaintPending) { + win.requestAnimationFrame(function() { + setTimeout(callback); + }); + return; + } + + var onpaint = function() { + if (!utils.isMozAfterPaintPending) { + win.removeEventListener("MozAfterPaint", onpaint); + callback(); + return; + } + if (utils.isTestControllingRefreshes) { + utils.advanceTimeAndRefresh(0); + } + } + win.addEventListener("MozAfterPaint", onpaint); + if (utils.isTestControllingRefreshes) { + utils.advanceTimeAndRefresh(0); + } +} + +// This is a magic number representing how many device pixels we are attempting to +// scroll or zoom. We use it for sending the wheel events, but we don't actually +// check that we have scrolled by that amount. +var kDelta = 3; + +function sendTouchpadScrollMotion(scrollbox, direction, ctrl, momentum, callback) { + var win = scrollbox.ownerDocument.defaultView; + let winUtils = SpecialPowers.getDOMWindowUtils(win); + + let event = { + deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaY: direction * kDelta, + lineOrPageDeltaY: direction, + ctrlKey: ctrl, + isMomentum: momentum + }; + + // Construct a promise that will resolve when either scroll or zoom has changed. + // --- Intermittent Warning --- + // Two wheel events are sent, but our promise resolves when any change has been + // made to scroll or zoom. That makes it possible that the effect of the second + // event may not yet be applied when the promise resolves. This shouldn't lead + // to any errors, since the two wheel events are moving in the same direction, + // and our later checks will only ensure that the value has changed from its + // initial value. This was done intentionally, because attempting to wait after + // both events yields problems when the second event has no effect, which does + // happen in testing. It's not clear why this is happening. Since the testing + // pattern is scroll (twice), then scroll back (twice), it's possible that the + // first scroll back event is sufficient to return the scrollbox to its minimal + // scrollTop position, and so the second event doesn't scroll any further. + const initialZoom = SpecialPowers.getFullZoom(win); + const initialScroll = scrollbox.scrollTop; + + const effectOfWheelEvent = SimpleTest.promiseWaitForCondition(() => { + return ((SpecialPowers.getFullZoom(win) != initialZoom) || (scrollbox.scrollTop != initialScroll)); + }, "Mouse wheel should have caused us to either zoom or scroll."); + + synthesizeWheel(scrollbox, 10, 10, event, win); + + // then additional pixel scroll + event.lineOrPageDeltaY = 0; + synthesizeWheel(scrollbox, 10, 10, event, win); + + effectOfWheelEvent.then(callback); +} + +function runTest() { + var win = open('bug574663.html', '_blank', 'width=300,height=300'); + let winUtils = SpecialPowers.getDOMWindowUtils(win); + + let waitUntilPainted = function(callback) { + // Until the first non-blank paint, the parent will set the opacity of our + // browser to 0 using the 'blank' attribute. + // Until the blank attribute is removed, we can't send scroll events. + SimpleTest.waitForFocus(function() { + /* eslint-disable no-shadow */ + let chromeScript = SpecialPowers.loadChromeScript(_ => { + /* eslint-env mozilla/chrome-script */ + let win = Services.wm.getMostRecentWindow("navigator:browser"); + win.requestAnimationFrame(() => { + win.gBrowser.selectedBrowser.removeAttribute("blank"); + win.requestAnimationFrame(() => { + sendAsyncMessage("blank-attribute-removed"); + }); + }); + }); + /* eslint-enable no-shadow */ + chromeScript.promiseOneMessage("blank-attribute-removed").then(() => { + chromeScript.destroy(); + waitForPaint(win, winUtils, callback); + }); + }, win); + }; + + waitUntilPainted(function () { + var scrollbox = win.document.getElementById("scrollbox"); + let outstandingTests = [ + [false, false], + [false, true], + [true, false], + [true, true], + ]; + + // grab the refresh driver, since we want to make sure + // async scrolls happen in deterministic time + winUtils.advanceTimeAndRefresh(1000); + + function nextTest() { + let [ctrlKey, isMomentum] = outstandingTests.shift(); + let scrollTopBefore = scrollbox.scrollTop; + let zoomFactorBefore = SpecialPowers.getFullZoom(win); + + let check = function() { + if (!ctrlKey) { + let postfix = isMomentum ? ", even after releasing the touchpad" : ""; + // Normal scroll: scroll + is(SpecialPowers.getFullZoom(win), zoomFactorBefore, "Normal scrolling shouldn't change zoom" + postfix); + isnot(scrollbox.scrollTop, scrollTopBefore, + "Normal scrolling should scroll" + postfix); + } else { + if (!isMomentum) { + isnot(SpecialPowers.getFullZoom(win), zoomFactorBefore, "Ctrl-scrolling should zoom while the user is touching the touchpad"); + is(scrollbox.scrollTop, scrollTopBefore, "Ctrl-scrolling shouldn't scroll while the user is touching the touchpad"); + } else { + is(SpecialPowers.getFullZoom(win), zoomFactorBefore, "Momentum scrolling shouldn't zoom, even when pressing Ctrl"); + isnot(scrollbox.scrollTop, scrollTopBefore, + "Momentum scrolling should scroll, even when pressing Ctrl"); + } + } + + if (!outstandingTests.length) { + winUtils.restoreNormalRefresh(); + win.close(); + SimpleTest.finish(); + return; + } + + // Revert the effect for the next test. + sendTouchpadScrollMotion(scrollbox, -1, ctrlKey, isMomentum, function() { + setTimeout(nextTest, 0); + }); + } + + sendTouchpadScrollMotion(scrollbox, 1, ctrlKey, isMomentum, check); + } + nextTest(); + }); +} + +window.onload = function() { + SpecialPowers.pushPrefEnv({ + "set":[["general.smoothScroll", false], + ["mousewheel.acceleration.start", -1], + ["mousewheel.system_scroll_override.enabled", false], + ["mousewheel.with_control.action", 3], + ["test.events.async.enabled", true]]}, runTest); +} + +SimpleTest.waitForExplicitFinish(); + +</script> +</pre> + +</body> +</html> diff --git a/dom/events/test/test_bug586961.xhtml b/dom/events/test/test_bug586961.xhtml new file mode 100644 index 0000000000..ebdcc248d6 --- /dev/null +++ b/dom/events/test/test_bug586961.xhtml @@ -0,0 +1,46 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=586961 +--> +<window title="Mozilla Bug 586961" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> +<body xmlns="http://www.w3.org/1999/xhtml"> + <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=586961">Mozilla Bug 586961</a> + + <p id="display"></p> +<div id="content" style="display: none"> +</div> +</body> + +<box onclick="clicked(event)"> + <label id="controllabel" control="controlbutton" accesskey="k" value="Click here" /> + <button id="controlbutton" label="Button" /> +</box> + +<script class="testbody" type="application/javascript"><![CDATA[ + +/** Test for Bug 586961 **/ + +function clicked(event) { + is(event.target.id, "controlbutton", "Accesskey was directed to controlled element."); + SimpleTest.finish(); +} + +function test() { + var accessKeyDetails = (navigator.platform.includes("Mac")) ? + { altKey : true, ctrlKey : true } : + { altKey : true, shiftKey: true }; + synthesizeKey("k", accessKeyDetails); +} + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(test, window); + +]]></script> + +</window> diff --git a/dom/events/test/test_bug591249.xhtml b/dom/events/test/test_bug591249.xhtml new file mode 100644 index 0000000000..c95bf501de --- /dev/null +++ b/dom/events/test/test_bug591249.xhtml @@ -0,0 +1,73 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=591249 +--> +<window title="Mozilla Bug 591249" onload="RunTest()" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + +<body xmlns="http://www.w3.org/1999/xhtml"> + <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=591249">Mozilla Bug 591249</a> + <img id="image" + src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAIAAAAC64paAAAAG0lEQVR42mP8z0A%2BYKJA76jmUc2jmkc1U0EzACKcASfOgGoMAAAAAElFTkSuQmCC" + ondragstart="event.preventDefault();"/> + <iframe id="iframe" src="chrome://mochitests/content/chrome/dom/events/test/bug591249_iframe.xhtml" style="height: 300px; width: 100%;"></iframe> +</body> + +<script class="testbody" type="application/javascript"><![CDATA[ +/** Test for Bug 591249 **/ + +SimpleTest.waitForExplicitFinish(); + +function completeTest(aBox) { + ok(window.getComputedStyle(aBox).backgroundColor == "rgb(255, 0, 0)", "The -moz-drag-over style should be removed."); + SimpleTest.finish(); +} + +function fireEvent(target, event) { + var win = target.ownerGlobal; + var utils = win.windowUtils; + utils.dispatchDOMEventViaPresShellForTesting(target, event); +} + +function RunTest() { + var image = document.getElementById("image"); + var iframe = document.getElementById("iframe"); + var iBox = iframe.contentDocument.getElementById("drop-target"); + var insideBoxX = iBox.offsetWidth + 10; + var insideBoxY = iBox.offsetHeight + 10; + + var dataTransfer; + var trapDrag = function(event) { + dataTransfer = event.dataTransfer; + dataTransfer.setData("text/plain", "Hello"); + dataTransfer.dropEffect = "move"; + event.preventDefault(); + event.stopPropagation(); + } + + // need to use real mouse action to get the dataTransfer + window.addEventListener("dragstart", trapDrag, true); + synthesizeMouse(image, 2, 2, { type: "mousedown" }); + synthesizeMouse(image, 11, 11, { type: "mousemove" }); + synthesizeMouse(image, 20, 20, { type: "mousemove" }); + window.removeEventListener("dragstart", trapDrag, true); + synthesizeMouse(image, 20, 20, { type: "mouseup" }); + + var event = document.createEvent("DragEvent"); + event.initDragEvent("dragover", true, true, iBox.ownerGlobal, 0, 0, 0, 0, 0, false, false, false, false, 0, iBox, dataTransfer); + fireEvent(iBox, event); + synthesizeMouse(image, 3, 3, { type: "mousedown" }); + synthesizeMouse(image, 23, 23, { type: "mousemove" }); + synthesizeMouse(iBox, insideBoxX, insideBoxY, { type: "mousemove" }); + ok(window.getComputedStyle(iBox).backgroundColor == "rgb(255, 255, 0)", "The -moz-drag-over style should be applied."); + synthesizeMouse(iBox, insideBoxX, insideBoxY, { type: "mouseup" }); + window.setTimeout(function () { completeTest(iBox); }, 40); +} +]]></script> + +</window> diff --git a/dom/events/test/test_bug591815.html b/dom/events/test/test_bug591815.html new file mode 100644 index 0000000000..c3987bb957 --- /dev/null +++ b/dom/events/test/test_bug591815.html @@ -0,0 +1,68 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=591815 +--> +<head> + <title>Test for Bug 591815</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body onload="setTimeout(runTest, 0)"> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=591815">Mozilla Bug 591815</a> +<p id="display"></p> +<div id="content"> + <div id="wrapper"> + <!-- 20x20 of red --> + <img id="image" ondragstart="fail();" + src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAIAAAAC64paAAAAG0lEQVR42mP8z0A%2BYKJA76jmUc2jmkc1U0EzACKcASfOgGoMAAAAAElFTkSuQmCC"/> + </div> +</div> +<pre id="test"> + +<script type="application/javascript"> + +/** Test for Bug 591815 **/ + +SimpleTest.waitForExplicitFinish(); + +function fail() { + ok(false, "drag started but should not have"); +} + +function runTest() { + var image = document.getElementById("image"); + var wrapper = document.getElementById("wrapper"); + var preventDefault = function(event) { + event.preventDefault(); + }; + wrapper.addEventListener('mousedown', preventDefault); + + synthesizeMouse(image, 3, 3, { type: "mousedown"}); + synthesizeMouse(image, 53, 53, { type: "mousemove"}); + synthesizeMouse(image, 53, 53, { type: "mouseup"}); + + wrapper.removeEventListener('mousedown', preventDefault); + + var relocateElementAndPreventDefault = function(event) { + document.body.appendChild(wrapper); + event.preventDefault(); + } + wrapper.addEventListener('mousedown', relocateElementAndPreventDefault); + + synthesizeMouse(image, 3, 3, { type: "mousedown"}); + synthesizeMouse(image, 53, 53, { type: "mousemove"}); + synthesizeMouse(image, 53, 53, { type: "mouseup"}); + + wrapper.removeEventListener('mousedown', relocateElementAndPreventDefault); + + ok(true, "passed the test"); + SimpleTest.finish(); +} + +</script> +</pre> +</body> +</html> + diff --git a/dom/events/test/test_bug593959.html b/dom/events/test/test_bug593959.html new file mode 100644 index 0000000000..a809e028c3 --- /dev/null +++ b/dom/events/test/test_bug593959.html @@ -0,0 +1,60 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=593959 +--> +<head> + <title>Test for Bug 593959</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <style> + body:active { + background: red; + } + </style> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=593959">Mozilla Bug 593959</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 593959 **/ + + function doTest() { + var utils = SpecialPowers.getDOMWindowUtils(window); + var e = document.createEvent("MouseEvent"); + e.initEvent("mousedown", false, false, window, 0, 1, 1, 1, 1, + false, false, false, false, 0, null); + utils.dispatchDOMEventViaPresShellForTesting(document.body, e); + + is(document.querySelector("body:active"), document.body, "body should be active!") + + var ifrwindow = document.getElementById("ifr").contentWindow; + + var utils2 = SpecialPowers.getDOMWindowUtils(ifrwindow); + + var e2 = ifrwindow.document.createEvent("MouseEvent"); + e2.initEvent("mouseup", false, false, ifrwindow, 0, 1, 1, 1, 1, + false, false, false, false, 0, null); + utils2.dispatchDOMEventViaPresShellForTesting(ifrwindow.document.body, e2); + + isnot(document.querySelector("body:active"), document.body, "body shouldn't be active!") + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addLoadEvent(doTest); + + + +</script> +</pre> +<iframe id="ifr"></iframe> +</body> +</html> diff --git a/dom/events/test/test_bug602962.xhtml b/dom/events/test/test_bug602962.xhtml new file mode 100644 index 0000000000..aac3d5b474 --- /dev/null +++ b/dom/events/test/test_bug602962.xhtml @@ -0,0 +1,87 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=602962 +--> +<window title="Mozilla Bug 602962" onload="openWindow()" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + +<body xmlns="http://www.w3.org/1999/xhtml"> + <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=602962">Mozilla Bug 602962</a> + <p id="display"></p> +<div id="content" style="display: none"> +</div> +</body> + +<script class="testbody" type="application/javascript"><![CDATA[ +/** Test for Bug 602962 **/ +var scrollbox, content; +var scrollX = 0, scrollY = 0; + +var oldWidth = 0, oldHeight = 0; +var win = null; + +function openWindow() { + win = window.open("chrome://mochitests/content/chrome/dom/events/test/bug602962.xhtml", "_blank", "width=600,height=600"); +} + +function doTest() { + scrollbox = win.document.getElementById("page-scrollbox"); + content = win.document.getElementById("page-box"); + content.style.width = 400 + "px"; + + win.addEventListener("resize", function() { + win.removeEventListener("resize", arguments.callee, false); + + setTimeout(function(){ + scrollbox.scrollBy(200,0); + resize(); + },0); + }, false); + + oldWidth = win.outerWidth; + oldHeight = win.outerHeight; + win.resizeTo(200, 400); +} + +function resize() { + scrollX = scrollbox.scrollLeft; + scrollY = scrollbox.scrollTop; + + win.addEventListener("resize", function() { + content.style.width = (oldWidth + 400) + "px"; + win.removeEventListener("resize", arguments.callee, true); + + setTimeout(function() { + finish(); + }, 0); + }, true); + + win.resizeTo(oldWidth, oldHeight); +} + +function finish() { + if (win.outerWidth != oldWidth || + win.outerHeight != oldHeight) { + // We should eventually get back to the original size. + setTimeout(finish, 0); + return; + } + scrollbox.scrollBy(scrollX, scrollY); + + is(scrollbox.scrollLeft, 200, "Scroll Left should have been restored to the value before the resize"); + is(scrollbox.scrollTop, 0, "Scroll Top should have been restored to the value before the resize"); + + is(win.outerWidth, oldWidth, "Width should be resized"); + is(win.outerHeight, oldHeight, "Height should be resized"); + win.close(); + SimpleTest.finish(); +} + +SimpleTest.waitForExplicitFinish(); +]]></script> + +</window> diff --git a/dom/events/test/test_bug603008.html b/dom/events/test/test_bug603008.html new file mode 100644 index 0000000000..ccba289a51 --- /dev/null +++ b/dom/events/test/test_bug603008.html @@ -0,0 +1,559 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=508906 +--> +<head> + <title>Test for Bug 603008</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=508906">Mozilla Bug 603008</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody" type="application/javascript"> + +/** Test for Bug 306008 - Touch* Events **/ + +let tests = [], testTarget, parent; + +let touch = { + id: 0, + point: {x: 0, y: 0}, + radius: {x: 0, y: 0}, + rotation: 0, + force: 0.5, + target: null +} + +function nextTest() { + if (tests.length) + SimpleTest.executeSoon(tests.shift()); +} + +function random() { + return Math.floor(Math.random() * 100); +} + +function checkEvent(aFakeEvent) { + return function(aEvent) { + is(aFakeEvent.ctrlKey, aEvent.ctrlKey, "Correct ctrlKey"); + is(aFakeEvent.altKey, aEvent.altKey, "Correct altKey"); + is(aFakeEvent.shiftKey, aEvent.shiftKey, "Correct shiftKey"); + is(aFakeEvent.metaKey, aEvent.metaKey, "Correct metaKey"); + checkTouches(aFakeEvent.touches, aEvent.touches); + checkTouches(aFakeEvent.targetTouches, aEvent.targetTouches); + checkTouches(aFakeEvent.changedTouches, aEvent.changedTouches); + } +} + +function checkTouches(aTouches1, aTouches2) { + is(aTouches1.length, aTouches2.length, "Correct touches length"); + for (var i = 0; i < aTouches1.length; i++) { + checkTouch(aTouches1[i], aTouches2[i]); + } +} + +function checkTouch(aFakeTouch, aTouch) { + is(aFakeTouch.identifier, aTouch.identifier, "Touch has correct identifier"); + is(aFakeTouch.target, aTouch.target, "Touch has correct target"); + is(aFakeTouch.page.x, aTouch.pageX, "Touch has correct pageX"); + is(aFakeTouch.page.y, aTouch.pageY, "Touch has correct pageY"); + is(aFakeTouch.page.x + Math.round(window.mozInnerScreenX), aTouch.screenX, "Touch has correct screenX"); + is(aFakeTouch.page.y + Math.round(window.mozInnerScreenY), aTouch.screenY, "Touch has correct screenY"); + is(aFakeTouch.page.x, aTouch.clientX, "Touch has correct clientX"); + is(aFakeTouch.page.y, aTouch.clientY, "Touch has correct clientY"); + is(aFakeTouch.radius.x, aTouch.radiusX, "Touch has correct radiusX"); + is(aFakeTouch.radius.y, aTouch.radiusY, "Touch has correct radiusY"); + is(aFakeTouch.rotationAngle, aTouch.rotationAngle, "Touch has correct rotationAngle"); + is(aFakeTouch.force, aTouch.force, "Touch has correct force"); +} + +function sendTouchEvent(windowUtils, aType, aEvent, aModifiers) { + var ids = [], xs=[], ys=[], rxs = [], rys = [], + rotations = [], forces = [], tiltXs = [], tiltYs = [], twists = []; + + for (var touchType of ["touches", "changedTouches", "targetTouches"]) { + for (var i = 0; i < aEvent[touchType].length; i++) { + if (!ids.includes(aEvent[touchType][i].identifier)) { + ids.push(aEvent[touchType][i].identifier); + xs.push(aEvent[touchType][i].page.x); + ys.push(aEvent[touchType][i].page.y); + rxs.push(aEvent[touchType][i].radius.x); + rys.push(aEvent[touchType][i].radius.y); + rotations.push(aEvent[touchType][i].rotationAngle); + forces.push(aEvent[touchType][i].force); + tiltXs.push(0); + tiltYs.push(0); + twists.push(0); + } + } + } + return windowUtils.sendTouchEvent(aType, + ids, xs, ys, rxs, rys, + rotations, forces, tiltXs, tiltYs, twists, + aModifiers, 0); +} + +function touchEvent(aOptions) { + if (!aOptions) { + aOptions = {}; + } + this.ctrlKey = aOptions.ctrlKey || false; + this.altKey = aOptions.altKey || false; + this.shiftKey = aOptions.shiftKey || false; + this.metaKey = aOptions.metaKey || false; + this.touches = aOptions.touches || []; + this.targetTouches = aOptions.targetTouches || []; + this.changedTouches = aOptions.changedTouches || []; +} + +function testtouch(aOptions) { + if (!aOptions) + aOptions = {}; + this.identifier = aOptions.identifier || 0; + this.target = aOptions.target || 0; + this.page = aOptions.page || {x: 0, y: 0}; + this.radius = aOptions.radius || {x: 0, y: 0}; + this.rotationAngle = aOptions.rotationAngle || 0; + this.force = aOptions.force || 1; +} + +function testSingleTouch(name) { + let cwu = SpecialPowers.getDOMWindowUtils(window); + let target = document.getElementById("testTarget"); + let target2 = document.getElementById("testTarget2"); + let bcr = target.getBoundingClientRect(); + let bcr2 = target2.getBoundingClientRect(); + + let touch1 = new testtouch({ + page: {x: Math.round(bcr.left + bcr.width/2), + y: Math.round(bcr.top + bcr.height/2)}, + target + }); + let event = new touchEvent({ + touches: [touch1], + targetTouches: [touch1], + changedTouches: [touch1] + }); + + // test touchstart event fires correctly + var checkFunction = checkEvent(event); + window.addEventListener("touchstart", checkFunction); + sendTouchEvent(cwu, "touchstart", event, 0); + window.removeEventListener("touchstart", checkFunction); + + // test touchmove event fires correctly + event.touches[0].page.x -= 1; + event.targetTouches[0].page.x -= 1; + event.changedTouches[0].page.x -= 1; + checkFunction = checkEvent(event); + window.addEventListener("touchmove", checkFunction); + sendTouchEvent(cwu, "touchmove", event, 0); + window.removeEventListener("touchmove", checkFunction); + + // test touchend event fires correctly + event.touches = []; + event.targetTouches = []; + checkFunction = checkEvent(event); + window.addEventListener("touchend", checkFunction); + sendTouchEvent(cwu, "touchend", event, 0); + window.removeEventListener("touchend", checkFunction); + + nextTest(); +} + +function testSingleTouch2(name) { + // firing a touchstart that includes only one touch will evict any touches in the queue with touchend messages + let cwu = SpecialPowers.getDOMWindowUtils(window); + let target = document.getElementById("testTarget"); + let target2 = document.getElementById("testTarget2"); + let bcr = target.getBoundingClientRect(); + let bcr2 = target2.getBoundingClientRect(); + + let touch1 = new testtouch({ + identifier: 0, + page: {x: Math.round(bcr.left + bcr.width/2), + y: Math.round(bcr.top + bcr.height/2)}, + target + }); + let event1 = new touchEvent({ + touches: [touch1], + targetTouches: [touch1], + changedTouches: [touch1] + }); + let touch2 = new testtouch({ + identifier: 1, + page: {x: Math.round(bcr2.left + bcr2.width/2), + y: Math.round(bcr2.top + bcr2.height/2)}, + target: target2 + }); + let event2 = new touchEvent({ + touches: [touch2], + targetTouches: [touch2], + changedTouches: [touch2] + }); + + // test touchstart event fires correctly + var checkFunction1 = checkEvent(event1); + window.addEventListener("touchstart", checkFunction1); + sendTouchEvent(cwu, "touchstart", event1, 0); + window.removeEventListener("touchstart", checkFunction1); + + event1.touches = []; + event1.targetTouches = []; + checkFunction1 = checkEvent(event1); + var checkFunction2 = checkEvent(event2); + + window.addEventListener("touchend", checkFunction1); + window.addEventListener("touchstart", checkFunction2); + sendTouchEvent(cwu, "touchstart", event2, 0); + window.removeEventListener("touchend", checkFunction1); + window.removeEventListener("touchstart", checkFunction2); + + sendTouchEvent(cwu, "touchstart", event1, 0); + + nextTest(); +} + + +function testMultiTouch(name) { + let cwu = SpecialPowers.getDOMWindowUtils(window); + let target1 = document.getElementById("testTarget"); + let target2 = document.getElementById("testTarget2"); + let bcr = target1.getBoundingClientRect(); + let bcr2 = target2.getBoundingClientRect(); + + let touch1 = new testtouch({ + identifier: 0, + page: {x: Math.round(bcr.left + bcr.width/2), + y: Math.round(bcr.top + bcr.height/2)}, + target: target1 + }); + let touch2 = new testtouch({ + identifier: 1, + page: {x: Math.round(bcr2.left + bcr2.width/2), + y: Math.round(bcr2.top + bcr2.height/2)}, + target: target2 + }); + let event = new touchEvent({ + touches: [touch1], + targetTouches: [touch1], + changedTouches: [touch1] + }); + + // test touchstart event fires correctly + var checkFunction = checkEvent(event); + window.addEventListener("touchstart", checkFunction); + sendTouchEvent(cwu, "touchstart", event, 0); + window.removeEventListener("touchstart", checkFunction); + + event.touches.push(touch2); + event.targetTouches = [touch2]; + event.changedTouches = [touch2]; + window.addEventListener("touchstart", checkFunction); + sendTouchEvent(cwu, "touchstart", event, 0); + window.removeEventListener("touchstart", checkFunction); + + // test moving one touch point + event.touches[0].page.x -= 1; + event.targetTouches = [event.touches[0]]; + event.changedTouches = [event.touches[0]]; + window.addEventListener("touchmove", checkFunction); + sendTouchEvent(cwu, "touchmove", event, 0); + window.removeEventListener("touchmove", checkFunction); + + // test moving both touch points -- two touchmove events should fire, one on each target + event.touches[0].page.x -= 1; + event.touches[1].page.x -= 1; + event.targetTouches = event.touches; + event.changedTouches = event.touches; + var touchMoveEvents = 0; + var checkFunction2 = function(aEvent) { + is(event.ctrlKey, aEvent.ctrlKey, "Correct ctrlKey"); + is(event.altKey, aEvent.altKey, "Correct altKey"); + is(event.shiftKey, aEvent.shiftKey, "Correct shiftKey"); + is(event.metaKey, aEvent.metaKey, "Correct metaKey"); + checkTouches(event.touches, aEvent.touches); + checkTouches(event.changedTouches, aEvent.changedTouches); + if (aEvent.targetTouches[0].target == target1) { + checkTouches([event.touches[0]], aEvent.targetTouches); + } else if (aEvent.targetTouches[0].target == target2) { + checkTouches([event.touches[1]], aEvent.targetTouches); + } else + ok(false, "Event target is incorrect: " + event.targetTouches[0].target.nodeName + "#" + event.targetTouches[0].target.id); + touchMoveEvents++; + }; + window.addEventListener("touchmove", checkFunction2); + sendTouchEvent(cwu, "touchmove", event, 0); + is(touchMoveEvents, 2, "Correct number of touchmove events fired"); + window.removeEventListener("touchmove", checkFunction2); + + // test removing just one finger + var expected = new touchEvent({ + touches: [touch2], + targetTouches: [], + changedTouches: [touch1] + }); + checkFunction = checkEvent(expected); + + event.touches = []; + event.targetTouches = []; + event.changedTouches = [touch1]; + + // test removing the other finger + window.addEventListener("touchend", checkFunction); + sendTouchEvent(cwu, "touchend", event, 0); + window.removeEventListener("touchend", checkFunction); + + event.touches = []; + event.targetTouches = []; + event.changedTouches = [touch2]; + checkFunction = checkEvent(event); + window.addEventListener("touchend", checkFunction); + sendTouchEvent(cwu, "touchend", event, 0); + window.removeEventListener("touchend", checkFunction); + + nextTest(); +} + +function testTouchChanged() { + let cwu = SpecialPowers.getDOMWindowUtils(window); + let target1 = document.getElementById("testTarget"); + let bcr = target1.getBoundingClientRect(); + + let touch1 = new testtouch({ + identifier: 0, + page: {x: Math.round(bcr.left + bcr.width/2), + y: Math.round(bcr.top + bcr.height/2)}, + target: target1 + }); + let event = new touchEvent({ + touches: [touch1], + targetTouches: [touch1], + changedTouches: [touch1] + }); + + var checkFunction = checkEvent(event); + sendTouchEvent(cwu, "touchstart", event, 0); + + var moveEvents = 0; + function onMove(aEvent) { + moveEvents++; + } + + window.addEventListener("touchmove", onMove); + + // the first touchmove should always fire an event + sendTouchEvent(cwu, "touchmove", event, 0); + + // changing nothing should not fire a touchmove event + sendTouchEvent(cwu, "touchmove", event, 0); + + // test moving x + event.touches[0].page.x -= 1; + sendTouchEvent(cwu, "touchmove", event, 0); + + // test moving y + event.touches[0].page.y -= 1; + sendTouchEvent(cwu, "touchmove", event, 0); + + // test changing y radius + event.touches[0].radius.y += 1; + sendTouchEvent(cwu, "touchmove", event, 0); + + // test changing x radius + event.touches[0].radius.x += 1; + sendTouchEvent(cwu, "touchmove", event, 0); + + // test changing rotationAngle + event.touches[0].rotationAngle += 1; + sendTouchEvent(cwu, "touchmove", event, 0); + + // test changing force + event.touches[0].force += 1; + sendTouchEvent(cwu, "touchmove", event, 0); + + // changing nothing again + sendTouchEvent(cwu, "touchmove", event, 0); + + is(moveEvents, 7, "Six move events fired"); + + window.removeEventListener("touchmove", onMove); + sendTouchEvent(cwu, "touchend", event, 0); + nextTest(); +} + +function testPreventDefault() { + let cwu = SpecialPowers.getDOMWindowUtils(window); + let target = document.getElementById("testTarget"); + let target2 = document.getElementById("testTarget2"); + let bcr = target.getBoundingClientRect(); + let bcr2 = target2.getBoundingClientRect(); + + let touch1 = new testtouch({ + page: {x: bcr.left + bcr.width/2, + y: bcr.top + bcr.height/2}, + target + }); + let event = new touchEvent({ + touches: [touch1], + targetTouches: [touch1], + changedTouches: [touch1] + }); + + let preventFunction = function(aEvent) { + aEvent.preventDefault(); + } + + let subTests = [ + [{ name: "touchstart", prevent: false }, + { name: "touchmove", prevent: false }, + { name: "touchmove", prevent: false }, + { name: "touchend", prevent: false }], + [{ name: "touchstart", prevent: true, doPrevent: true }, + { name: "touchmove", prevent: false }, + { name: "touchmove", prevent: false }, + { name: "touchend", prevent: false }], + [{ name: "touchstart", prevent: false }, + { name: "touchmove", prevent: true, doPrevent: true }, + { name: "touchmove", prevent: false }, + { name: "touchend", prevent: false }], + [{ name: "touchstart", prevent: false }, + { name: "touchmove", prevent: false }, + { name: "touchmove", prevent: false, doPrevent: true }, + { name: "touchend", prevent: false }], + [{ name: "touchstart", prevent: false }, + { name: "touchmove", prevent: false }, + { name: "touchmove", prevent: false }, + { name: "touchend", prevent: true, doPrevent: true }] + ]; + + var dotest = function(aTest) { + if (aTest.doPrevent) { + target.addEventListener(aTest.name, preventFunction); + } + + if (aTest.name == "touchmove") { + touch1.page.x++; + event.touches[0] = touch1; + } + + is(sendTouchEvent(cwu, aTest.name, event, 0), aTest.prevent, "Got correct status"); + + if (aTest.doPrevent) + target.removeEventListener(aTest.name, preventFunction); + } + + for (var i = 0; i < subTests.length; i++) { + for (var j = 0; j < subTests[i].length; j++) { + dotest(subTests[i][j]); + } + } + + nextTest(); +} + +function testRemovingElement() { + let cwu = SpecialPowers.getDOMWindowUtils(window); + let target = document.getElementById("testTarget"); + let bcr = document.getElementById("testTarget").getBoundingClientRect(); + + let touch1 = new testtouch({ + page: {x: bcr.left + bcr.width/2, + y: bcr.top + bcr.height/2}, + }); + let e = new touchEvent({ + touches: [touch1], + targetTouches: [touch1], + changedTouches: [touch1] + }); + + var touchEvents = 0; + var removeTarget = function(aEvent) { + aEvent.target.remove(); + }; + + var checkTarget = function(aEvent) { + is(aEvent.target, target, "Event has correct target"); + touchEvents++; + }; + + target.addEventListener("touchstart", removeTarget); + target.addEventListener("touchmove", checkTarget); + target.addEventListener("touchend", checkTarget); + + sendTouchEvent(cwu, "touchstart", e, 0); + + e.touches[0].page.x++; + sendTouchEvent(cwu, "touchmove", e, 0); + sendTouchEvent(cwu, "touchend", e, 0); + + target.removeEventListener("touchstart", removeTarget); + target.removeEventListener("touchmove", checkTarget); + target.removeEventListener("touchend", checkTarget); + + is(touchEvents, 2, "Check target was called twice"); + + nextTest(); +} + +function testNAC() { + let cwu = SpecialPowers.getDOMWindowUtils(window); + let target = document.getElementById("testTarget3"); + let bcr = target.getBoundingClientRect(); + + let touch1 = new testtouch({ + page: {x: Math.round(bcr.left + bcr.width/2), + y: Math.round(bcr.top + bcr.height/2)}, + target + }); + let event = new touchEvent({ + touches: [touch1], + targetTouches: [touch1], + changedTouches: [touch1] + }); + + // test touchstart event fires correctly + var checkFunction = checkEvent(event); + window.addEventListener("touchstart", checkFunction); + sendTouchEvent(cwu, "touchstart", event, 0); + window.removeEventListener("touchstart", checkFunction); + + sendTouchEvent(cwu, "touchend", event, 0); + + nextTest(); +} + +function doTest() { + tests.push(testSingleTouch); + tests.push(testSingleTouch2); + tests.push(testMultiTouch); + tests.push(testPreventDefault); + tests.push(testTouchChanged); + tests.push(testRemovingElement); + tests.push(testNAC); + + tests.push(function() { + SimpleTest.finish(); + }); + + nextTest(); +} + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(doTest); + +</script> +</pre> +<div id="parent"> + <span id="testTarget" style="padding: 5px; border: 1px solid black;">testTarget</span> + <span id="testTarget2" style="padding: 5px; border: 1px solid blue;">testTarget</span> + <input type="text" id="testTarget3"> +</div> +</body> +</html> diff --git a/dom/events/test/test_bug605242.html b/dom/events/test/test_bug605242.html new file mode 100644 index 0000000000..732501f558 --- /dev/null +++ b/dom/events/test/test_bug605242.html @@ -0,0 +1,58 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=605242 +--> +<head> + <title>Test for Bug 605242</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body onload="setTimeout('runTest()', 0)"> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=605242">Mozilla Bug 605242</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 605242 **/ + +SimpleTest.waitForExplicitFinish(); + +var utils = SpecialPowers.getDOMWindowUtils(window); +function sendMouseDown(el) { + var rect = el.getBoundingClientRect(); + utils.sendMouseEvent('mousedown', rect.left + 5, rect.top + 5, 0, 1, 0); +} + +function sendMouseUp(el) { + var rect = el.getBoundingClientRect(); + utils.sendMouseEvent('mouseup', rect.left + 5, rect.top + 5, 0, 1, 0); +} + +function runTest() { + var b = document.getElementById("testbutton"); + sendMouseDown(b); + var l = document.querySelectorAll(":active"); + + var contains = false; + for (var i = 0; i < l.length; ++i) { + if (l[i] == b) { + contains = true; + } + } + + ok(contains, "Wrong active content! \n"); + sendMouseUp(b); + SimpleTest.finish(); +} + +</script> +</pre> +<button id="testbutton">A button</button> +<pre id="log"> +</pre> +</body> +</html> diff --git a/dom/events/test/test_bug607464.html b/dom/events/test/test_bug607464.html new file mode 100644 index 0000000000..b9a176ae33 --- /dev/null +++ b/dom/events/test/test_bug607464.html @@ -0,0 +1,84 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=607464 +--> +<head> + <title>Test for Bug 607464</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="/tests/gfx/layers/apz/test/mochitest/apz_test_utils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=607464">Mozilla Bug 607464</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> + +/** + * Test for Bug 607464: + * Pixel scrolling shouldn't scroll smoothly, even if general.smoothScroll is on. + **/ + +function scrollDown150PxWithPixelScrolling(scrollbox) { + var win = scrollbox.ownerDocument.defaultView; + let event = { + deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaY: 30.0, + lineOrPageDeltaY: 1 + }; + // A pixel scroll with lineOrPageDeltaY. + synthesizeWheel(scrollbox, 10, 10, event, win); + // then 4 pixel scrolls without lineOrPageDeltaY. + event.lineOrPageDeltaY = 0; + for (let i = 0; i < 4; ++i) { + synthesizeWheel(scrollbox, 10, 10, event, win); + } + + // Note: the line scroll shouldn't have any effect because it has + // hasPixels = true set on it. We send it to emulate normal + // behavior. +} + +function runTest() { + var win = open('bug607464.html', '_blank', 'width=300,height=300'); + SimpleTest.waitForFocus(function () { + var scrollbox = win.document.getElementById("scrollbox"); + let scrollTopBefore = scrollbox.scrollTop; + + win.addEventListener("scroll", function(e) { + is(scrollbox.scrollTop % 30, 0, + "Pixel scrolling should happen instantly, not smoothly. The " + + "scroll position " + scrollbox.scrollTop + " in this sequence of wheel " + + "events should be a multiple of 30."); + if (scrollbox.scrollTop == 150) { + win.close(); + SimpleTest.finish(); + } + }, true); + + promiseOnlyApzControllerFlushed(win).then(function() { + scrollDown150PxWithPixelScrolling(scrollbox); + }); + }, win); +} + +window.onload = function() { + SpecialPowers.pushPrefEnv({ + "set":[["general.smoothScroll", true], + ["mousewheel.acceleration.start", -1], + ["mousewheel.system_scroll_override.enabled", false]]}, runTest); +} + +SimpleTest.waitForExplicitFinish(); +SimpleTest.testInChaosMode(); + +</script> +</pre> + +</body> +</html> diff --git a/dom/events/test/test_bug613634.html b/dom/events/test/test_bug613634.html new file mode 100644 index 0000000000..18205d23df --- /dev/null +++ b/dom/events/test/test_bug613634.html @@ -0,0 +1,90 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=613634 +--> +<head> + <title>Test for Bug 613634</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=613634">Mozilla Bug 613634</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 613634 **/ + +var eventCount = 0; +function l(e) { + if (e.eventPhase != Event.CAPTURING_PHASE) { + ++eventCount; + } else { + ok(false, "Listener shouldn't be called!"); + } +} + +var d1 = document.createElement("div"); +var d2 = document.createElement("div"); + +d1.appendChild(d2); + +var x = new XMLHttpRequest(); + +try { +d1.addEventListener("foo", l); +document.addEventListener("foo", l); +window.addEventListener("foo", l); +x.addEventListener("foo", l); +} catch(ex) { + ok(false, "Shouldn't throw " + ex); +} + +var ev = document.createEvent("Event"); +ev.initEvent("foo", true, true); +d2.dispatchEvent(ev); +is(eventCount, 1, "Event listener should have been called!"); + +ev = document.createEvent("Event"); +ev.initEvent("foo", false, false); +d2.dispatchEvent(ev); +is(eventCount, 1, "Event listener shouldn't have been called!"); + +d1.removeEventListener("foo", l); +ev = document.createEvent("Event"); +ev.initEvent("foo", true, true); +d2.dispatchEvent(ev); +is(eventCount, 1, "Event listener shouldn't have been called!"); + + +ev = document.createEvent("Event"); +ev.initEvent("foo", true, true); +document.body.dispatchEvent(ev); +is(eventCount, 3, "Event listener should have been called on document and window!"); + +document.removeEventListener("foo", l); +window.removeEventListener("foo", l); +ev = document.createEvent("Event"); +ev.initEvent("foo", true, true); +document.body.dispatchEvent(ev); +is(eventCount, 3, "Event listener shouldn't have been called on document and window!"); + +ev = document.createEvent("Event"); +ev.initEvent("foo", true, true); +x.dispatchEvent(ev); +is(eventCount, 4, "Event listener should have been called on XMLHttpRequest!"); + +x.removeEventListener("foo", l); +ev = document.createEvent("Event"); +ev.initEvent("foo", true, true); +x.dispatchEvent(ev); +is(eventCount, 4, "Event listener shouldn't have been called on XMLHttpRequest!"); + +</script> +</pre> +</body> +</html> diff --git a/dom/events/test/test_bug615597.html b/dom/events/test/test_bug615597.html new file mode 100644 index 0000000000..ddca4a6869 --- /dev/null +++ b/dom/events/test/test_bug615597.html @@ -0,0 +1,39 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=615597 +--> +<head> + <title>Test for Bug 615597</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=615597">Mozilla Bug 615597</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 615597 **/ + +window.addEventListener("deviceorientation", function(event) { + is(event.alpha, 1.5); + is(event.beta, 2.25); + is(event.gamma, 3.667); + is(event.absolute, true); +}, true); + +var event = DeviceOrientationEvent; +ok(!!event, "Should have seen DeviceOrientationEvent!"); + +event = document.createEvent("DeviceOrientationEvent"); +event.initDeviceOrientationEvent('deviceorientation', true, true, 1.5, 2.25, 3.667, true); +window.dispatchEvent(event); + +</script> +</pre> +</body> +</html> diff --git a/dom/events/test/test_bug617528.xhtml b/dom/events/test/test_bug617528.xhtml new file mode 100644 index 0000000000..f40dd2fe4e --- /dev/null +++ b/dom/events/test/test_bug617528.xhtml @@ -0,0 +1,94 @@ +<?xml version="1.0"?> +<?xml-stylesheet type="text/css" href="chrome://global/skin"?> +<?xml-stylesheet type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=617528 +--> +<window title="Mozilla Bug 617528" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + + <body xmlns="http://www.w3.org/1999/xhtml"> + <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=617528" + target="_blank">Mozilla Bug 617528</a> + </body> + + <script type="application/javascript"><![CDATA[ + const {BrowserTestUtils} = ChromeUtils.import("resource://testing-common/BrowserTestUtils.jsm"); + var _window; + var browser; + + function start() { + _window = window.browsingContext.topChromeWindow.open("window_bug617528.xhtml", "_new", "chrome"); + _window.addEventListener("load", onLoad, false); + } + + function onLoad() { + _window.removeEventListener("load", onLoad, false); + + browser = _window.document.getElementById("browser"); + browser.addEventListener("pageshow", onPageShow, false); + + var uri='data:text/html,\ +<html>\ + <body>\ + <div oncontextmenu="event.preventDefault()">\ + <input id="node" type="text" value="Click here"></input>\ + </div>\ + </body>\ +</html>'; + BrowserTestUtils.loadURIString(browser, uri); + } + + function onPageShow() { + browser.removeEventListener("pageshow", onPageShow, true); + SimpleTest.executeSoon(doTest); + } + + function onContextMenu1(event) { + is(event.defaultPrevented, true, + "expected event.defaultPrevented to be true (1)"); + is(event.target.localName, "input", + "expected event.target.localName to be 'input' (1)"); + is(event.originalTarget.localName, "div", + "expected event.originalTarget.localName to be 'div' (1)"); + } + + function onContextMenu2(event) { + is(event.defaultPrevented, false, + "expected event.defaultPrevented to be false (2)"); + is(event.target.localName, "input", + "expected event.target.localName to be 'input' (2)"); + is(event.originalTarget.localName, "div", + "expected event.originalTarget.localName to be 'div' (2)"); + } + + function doTest() { + var win = browser.contentWindow; + win.focus(); + var node = win.document.getElementById("node"); + var rect = node.getBoundingClientRect(); + var left = rect.left + rect.width / 2; + var top = rect.top + rect.height / 2; + + var wu = win.windowUtils; + + browser.addEventListener("contextmenu", onContextMenu1, false); + wu.sendMouseEvent("contextmenu", left, top, 2, 1, 0); + browser.removeEventListener("contextmenu", onContextMenu1, false); + + browser.addEventListener("contextmenu", onContextMenu2, false); + var shiftMask = Event.SHIFT_MASK; + wu.sendMouseEvent("contextmenu", left, top, 2, 1, shiftMask); + browser.removeEventListener("contextmenu", onContextMenu2, false); + + _window.close(); + SimpleTest.finish(); + } + + addLoadEvent(start); + SimpleTest.waitForExplicitFinish(); + ]]></script> +</window> diff --git a/dom/events/test/test_bug624127.html b/dom/events/test/test_bug624127.html new file mode 100644 index 0000000000..7099f9f936 --- /dev/null +++ b/dom/events/test/test_bug624127.html @@ -0,0 +1,35 @@ +<html> +<head> + <title>Test for Bug 624127</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body onload="setTimeout('runTest()', 0)"> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> + +SimpleTest.waitForExplicitFinish(); + +function runTest() { + synthesizeMouse($("text"), 2, 2, { type: "mousedown" }); + synthesizeMouse(frames[0].document.body, 2, 2, { type: "mouseup" }, frames[0]); + synthesizeMouse($("text2"), 50, 8, { type: "mousemove" }); + + is(window.getSelection().toString(), "", "no selection made"); + SimpleTest.finish(); +} + +</script> +</pre> + +<p id="text">Normal text</p> +<iframe srcdoc="text in iframe"></iframe> +<p id="text2">Normal text</p> + +</body> +</html> diff --git a/dom/events/test/test_bug635465.html b/dom/events/test/test_bug635465.html new file mode 100644 index 0000000000..1e1bf7330f --- /dev/null +++ b/dom/events/test/test_bug635465.html @@ -0,0 +1,90 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=635465 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 635465</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <style type="text/css"> + #item { + position: relative; + } + .s-menu-section-submenu { + position: absolute; + display: none; + } + .open .s-menu-section-submenu { + display: block; + } +</style> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=635465">Mozilla Bug 635465</a> +<div id="display"> + <div class="item" id="item" + onmouseover="showSubmenu(event)" onmouseout="hideSubmenu(event)"> + <a href="#" id="firsthover">Hover me</a> + <div class="s-menu-section-submenu" id="menu"> + <a href="#" id="secondhover">Now hover me</a> + </div> + </div> +</div> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 635465 **/ +function showSubmenu(event) { + var item = document.getElementById('item'); + + var width = item.offsetWidth; // IT WORKS IF YOU REMOVE THIS LINE + + item.className='open'; +} + +function hideSubmenu(event) { + document.getElementById('item').className=''; +} + +SimpleTest.waitForExplicitFinish(); + +function executeTests() { + // First flush out layout of firsthover + ok($("firsthover").getBoundingClientRect().height > 0, + "Should have a nonzero height before hover"); + + // Now trigger a mouseover on firsthover + synthesizeMouseAtCenter($("firsthover"), { type: "mousemove" }); + + ok($("secondhover").getBoundingClientRect().height > 0, + "Should have a nonzero height for submenu after hover"); + + // Now determine where secondhover is hanging out + var rect = $("secondhover").getBoundingClientRect(); + synthesizeMouseAtCenter($("secondhover"), { type: "mousemove" }); + + // And another mouseover one pixel to the right of where the center used to be + synthesizeMouseAtPoint(rect.left + rect.width/2 + 1, + rect.top + rect.height/2, + { type: "mousemove" }); + + ok($("secondhover").getBoundingClientRect().height > 0, + "Should have a nonzero height for submenu after second hover"); + + // And check computed display of the menu + is(getComputedStyle($("menu"), "").display, "block", "Should have block display"); + + SimpleTest.finish(); +} + +SimpleTest.waitForFocus(executeTests); +</script> +</pre> +</body> +</html> diff --git a/dom/events/test/test_bug641477.html b/dom/events/test/test_bug641477.html new file mode 100644 index 0000000000..b669c3145b --- /dev/null +++ b/dom/events/test/test_bug641477.html @@ -0,0 +1,37 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=641477 +--> +<head> + <title>Test for Bug 641477</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=641477">Mozilla Bug 641477</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 641477 **/ + +var didThrow = false; + +var e = document.createEvent("Event"); +try { + is(e.type, "", "Event type should be empty string before initialization"); + document.dispatchEvent(e); +} catch(ex) { + didThrow = (ex.name == "InvalidStateError" && ex.code == DOMException.INVALID_STATE_ERR); +} + +ok(didThrow, "Should have thrown InvalidStateError!"); + +</script> +</pre> +</body> +</html> diff --git a/dom/events/test/test_bug648573.html b/dom/events/test/test_bug648573.html new file mode 100644 index 0000000000..11348349ba --- /dev/null +++ b/dom/events/test/test_bug648573.html @@ -0,0 +1,120 @@ +<!-- 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/. --> + +<!DOCTYPE html> +<html> + <!-- + https://bugzilla.mozilla.org/show_bug.cgi?id=648573 + --> + <head> + <title>Test for Bug 648573</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + </head> + <body> + <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=648573">Mozilla Bug 648573</a> + <p id="display"></p> + <div id="content" style="display: none"> + + </div> + <pre id="test"> + <script type="application/javascript"> + + /** Test for Bug 648573 **/ + SimpleTest.waitForExplicitFinish(); + + function runTest() { + var iframe = document.createElement("iframe"); + document.body.appendChild(iframe); + var win = iframe.contentWindow; + var doc = iframe.contentDocument; + + var utils = SpecialPowers.getDOMWindowUtils(win); + + ok("createTouch" in doc, "Should have createTouch function"); + ok("createTouchList" in doc, "Should have createTouchList function"); + ok(doc.createEvent("touchevent"), "Should be able to create TouchEvent objects"); + + var t1 = doc.createTouch(win, doc, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11); + is(t1.target, doc, "Wrong target"); + is(t1.identifier, 1, "Wrong identifier"); + is(t1.pageX, 2, "Wrong pageX"); + is(t1.pageY, 3, "Wrong pageY"); + is(t1.screenX, 4, "Wrong screenX"); + is(t1.screenY, 5, "Wrong screenY"); + is(t1.clientX, 6, "Wrong clientX"); + is(t1.clientY, 7, "Wrong clientY"); + is(t1.radiusX, 8, "Wrong radiusX"); + is(t1.radiusY, 9, "Wrong radiusY"); + is(t1.rotationAngle, 10, "Wrong rotationAngle"); + is(t1.force, 11, "Wrong force"); + + var t2 = doc.createTouch(); + + var l1 = doc.createTouchList(t1); + is(l1.length, 1, "Wrong length"); + is(l1.item(0), t1, "Wront item (1)"); + is(l1[0], t1, "Wront item (2)"); + + var l2 = doc.createTouchList([t1, t2]); + is(l2.length, 2, "Wrong length"); + is(l2.item(0), t1, "Wront item (3)"); + is(l2.item(1), t2, "Wront item (4)"); + is(l2[0], t1, "Wront item (5)"); + is(l2[1], t2, "Wront item (6)"); + + var l3 = doc.createTouchList(); + + var e = doc.createEvent("touchevent"); + e.initTouchEvent("touchmove", true, true, win, 0, true, true, true, true, + l1, l2, l3); + is(e.touches, l1, "Wrong list (1)"); + is(e.targetTouches, l2, "Wrong list (2)"); + is(e.changedTouches, l3, "Wrong list (3)"); + ok(e.altKey, "Alt should be true"); + ok(e.metaKey, "Meta should be true"); + ok(e.ctrlKey, "Ctrl should be true"); + ok(e.shiftKey, "Shift should be true"); + + + var events = + ["touchstart", + "touchend", + "touchmove", + "touchcancel"]; + + function runEventTest(type) { + var event = doc.createEvent("touchevent"); + event.initTouchEvent(type, true, true, win, 0, true, true, true, true, + l1, l2, l3); + var t = doc.createElement("div"); + // Testing target.onFoo; + var didCall = false; + t["on" + type] = function (evt) { + is(evt, event, "Wrong event"); + evt.target.didCall = true; + } + t.dispatchEvent(event); + ok(t.didCall, "Should have called the listener(1)"); + + // Testing <element onFoo=""> + t = doc.createElement("div"); + t.setAttribute("on" + type, "this.didCall = true;"); + t.dispatchEvent(event); + ok(t.didCall, "Should have called the listener(2)"); + } + + for (var i = 0; i < events.length; ++i) { + runEventTest(events[i]); + } + + SimpleTest.finish(); + } + + SpecialPowers.pushPrefEnv( + {"set": [["dom.w3c_touch_events.legacy_apis.enabled", true]]}, runTest); + </script> + </pre> + </body> +</html> diff --git a/dom/events/test/test_bug650493.html b/dom/events/test/test_bug650493.html new file mode 100644 index 0000000000..a830c688fd --- /dev/null +++ b/dom/events/test/test_bug650493.html @@ -0,0 +1,215 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=650493 +--> +<head> + <title>Test for Bug 650493</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=650493">Mozilla Bug 650493</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +function getNodes() { + var walker = document.createTreeWalker($('content'), NodeFilter.SHOW_ALL, null); + var nodes = []; + do { + nodes.push(walker.currentNode); + } while(walker.nextNode()); + + return nodes; +} + +function check() { + var current = getNodes(); + is(nodes.length, current.length, "length after " + testName); + nodes.forEach(function(val, index) { + ok(current.indexOf(val) > -1, "nodes[" + index + "] (" + val + ") shouldn't exist after " + testName); + }); +} + +var nodes = getNodes(); +var testName = "empty"; +var mutateCount = 0; + +check(); + +// Set up listeners +root = $('content'); +root.addEventListener("DOMNodeInserted", function(e) { + mutateCount++; + is(e.isTrusted, true, "untrusted mutation event"); + var w = document.createTreeWalker(e.target, NodeFilter.SHOW_ALL, null); + do { + is(nodes.indexOf(w.currentNode), -1, "already have inserted node (" + w.currentNode + ") when " + testName); + nodes.push(w.currentNode); + } while(w.nextNode()); +}); +root.addEventListener("DOMNodeRemoved", function(e) { + mutateCount++; + is(e.isTrusted, true, "untrusted mutation event"); + var w = document.createTreeWalker(e.target, NodeFilter.SHOW_ALL, null); + do { + var index = nodes.indexOf(w.currentNode); + ok(index != -1, "missing removed node (" + w.currentNode + ") when " + testName); + nodes.splice(index, 1); + } while(w.nextNode()); +}); + +testName = "text-only innerHTML"; +root.innerHTML = "hello world"; +check(); + +testName = "innerHTML with <b>"; +root.innerHTML = "<b>bold</b> world"; +check(); + +testName = "complex innerHTML"; +root.innerHTML = "<b>b<span>old</span></b> <strong>world"; +check(); + +testName = "replacing using .textContent"; +root.textContent = "i'm just a plain text minding my own business"; +check(); + +testName = "clearing using .textContent"; +root.textContent = ""; +check(); + +testName = "inserting using .textContent"; +root.textContent = "i'm new text!!"; +check(); + +testName = "inserting using .textContent"; +root.textContent = "i'm new text!!"; +check(); + +testName = "preparing to normalize"; +root.innerHTML = "<u><b>foo</b></u> "; +var u = root.firstChild; +is(u.nodeName, "U", "got the right node"); +var b = u.firstChild; +is(b.nodeName, "B", "got the right node"); +b.insertBefore(document.createTextNode(""), b.firstChild); +b.insertBefore(document.createTextNode(""), b.firstChild); +b.appendChild(document.createTextNode("")); +b.appendChild(document.createTextNode("hello")); +b.appendChild(document.createTextNode("world")); +u.appendChild(document.createTextNode("foo")); +u.appendChild(document.createTextNode("")); +u.appendChild(document.createTextNode("bar")); +check(); + +testName = "normalizing"; +root.normalize(); +check(); + +testName = "self replace firstChild"; +mutateCount = 0; +root.replaceChild(root.firstChild, root.firstChild); +check(); +is(mutateCount, 2, "should remove and reinsert " + testName); + +testName = "self replace second child"; +mutateCount = 0; +root.replaceChild(root.firstChild.nextSibling, root.firstChild.nextSibling); +check(); +is(mutateCount, 2, "should remove and reinsert " + testName); + +testName = "self replace lastChild"; +mutateCount = 0; +root.replaceChild(root.lastChild, root.lastChild); +check(); +is(mutateCount, 2, "should remove and reinsert " + testName); + +testName = "self insertBefore firstChild"; +mutateCount = 0; +root.insertBefore(root.firstChild, root.firstChild); +check(); +is(mutateCount, 2, "should remove and reinsert " + testName); + +testName = "self insertBefore second child"; +mutateCount = 0; +root.insertBefore(root.firstChild.nextSibling, root.firstChild.nextSibling); +check(); +is(mutateCount, 2, "should remove and reinsert " + testName); + +testName = "self insertBefore lastChild"; +mutateCount = 0; +root.insertBefore(root.lastChild, root.lastChild); +check(); +is(mutateCount, 2, "should remove and reinsert " + testName); + +testName = "appendChild last"; +mutateCount = 0; +root.appendChild(root.lastChild); +check(); +is(mutateCount, 2, "should remove and reinsert " + testName); + +testName = "prepare script/style"; +script = document.createElement("script"); +script.appendChild(document.createTextNode("void(0);")); +root.appendChild(script); +style = document.createElement("style"); +root.appendChild(style); +check(); + +testName = "set something in script"; +script.text = "something"; +check(); + +testName = "set something in style"; +style.innerHTML = "something { dislay: none; }"; +check(); + +testName = "moving style"; +root.insertBefore(style, root.firstChild); +check(); + +testName = "replacing script"; +root.replaceChild(b, script); +check(); + +testName = "doc-fragment insert in the middle"; +frag = document.createDocumentFragment(); +frag.addEventListener("DOMNodeRemoved", function(e) { + var index = children.indexOf(e.target); + ok(index != -1, "unknown child removed from fragment"); + children.splice(index, 1); +}); +var children = []; +children.push(document.createTextNode("foo")); +children.push(document.createTextNode("bar")); +children.push(document.createElement("span")); +children.push(document.createElement("b")); +children[2].appendChild(document.createElement("i")); +children.forEach(function(child) { frag.appendChild(child); }); +ok(root.firstChild, "need to have children in order to test inserting before end"); +root.replaceChild(frag, root.firstChild); +check(); +is(children.length, 0, "should have received DOMNodeRemoved for all frag children when inserting"); +is(frag.childNodes.length, 0, "fragment should be empty when inserting"); + +testName = "doc-fragment append at the end"; +children.push(document.createTextNode("foo")); +children.push(document.createTextNode("bar")); +children.push(document.createElement("span")); +children.push(document.createElement("b")); +children[2].appendChild(document.createElement("i")); +children.forEach(function(child) { frag.appendChild(child); }); +root.appendChild(frag); +check(); +is(children.length, 0, "should have received DOMNodeRemoved for all frag children when appending"); +is(frag.childNodes.length, 0, "fragment should be empty when appending"); + +</script> +</body> +</html> + diff --git a/dom/events/test/test_bug656379-1.html b/dom/events/test/test_bug656379-1.html new file mode 100644 index 0000000000..9eee769c04 --- /dev/null +++ b/dom/events/test/test_bug656379-1.html @@ -0,0 +1,30 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=656379 +--> +<head> + <title>Test for Bug 656379</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script src="/tests/SimpleTest/WindowSnapshot.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 656379 **/ +SimpleTest.waitForExplicitFinish(); +var subwindow = window.open("./bug656379-1.html", "bug656379", "width=800,height=1000"); + +function finishTests() { + subwindow.close(); + SimpleTest.finish(); +} +</script> +</pre> + +</body> +</html> diff --git a/dom/events/test/test_bug656379-2.html b/dom/events/test/test_bug656379-2.html new file mode 100644 index 0000000000..b7dd47bbc3 --- /dev/null +++ b/dom/events/test/test_bug656379-2.html @@ -0,0 +1,115 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=656379 +--> +<head> + <title>Test for Bug 656379</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <style> + input[type="button"]:hover { color: green; } + </style> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=656379">Mozilla Bug 656379</a> +<p id="display"> + <label for="button1" id="label1">Label 1</label> + <input type="button" id="button1" value="Button 1"> + <label> + <span id="label2">Label 2</span> + <input type="button" id="button2" value="Button 2"> + </label> +</p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> + +function log(aEvent) { + function getPath() { + if (!aEvent.target) { + return "(null)"; + } + function getNodeName(aNode) { + if (aNode.id) { + return `${aNode.nodeName}#${aNode.id}`; + } + return aNode.nodeName; + } + let path = getNodeName(aEvent.target); + for (let parent = aEvent.target.parentElement; + parent && document.body != parent; + parent = parent.parentElement) { + path = `${getNodeName(parent)} > ${path}`; + } + return path; + } + info(`${aEvent.type} on ${getPath()}`); +} + +window.addEventListener("mousemove", log, {capture: true}); +window.addEventListener("mouseenter", log, {capture: true}); +window.addEventListener("mouseleave", log, {capture: true}); +window.addEventListener("mouseover", log, {capture: true}); +window.addEventListener("mouseout", log, {capture: true}); + +/** Test for Bug 656379 **/ +SimpleTest.waitForExplicitFinish(); +function* tests() { + info("Synthesizing mousemove on label1..."); + synthesizeMouseAtCenter($("label1"), { type: "mousemove" }); + yield undefined; + is($("button1").matches(":hover"), true, + "Button 1 should be hovered after mousemove over label1"); + is($("label1").matches(":hover"), true, + "Label 1 should be hovered after mousemove over label1"); + is($("button2").matches(":hover"), false, + "Button 2 should not be hovered after mousemove over label1"); + is($("label2").matches(":hover"), false, + "Label 2 should not be hovered after mousemove over label1"); + info("Synthesizing mousemove on button2..."); + synthesizeMouseAtCenter($("button2"), { type: "mousemove" }); + yield undefined; + is($("button1").matches(":hover"), false, + "Button 1 should not be hovered after mousemove over button2"); + is($("label1").matches(":hover"), false, + "Label 1 should not be hovered after mousemove over button2"); + is($("button2").matches(":hover"), true, + "Button 2 should be hovered after mousemove over button2"); + is($("label2").matches(":hover"), false, + "Label 2 should not be hovered after mousemove over label2"); + info("Synthesizing mousemove on label2..."); + synthesizeMouseAtCenter($("label2"), { type: "mousemove" }); + yield undefined; + is($("button1").matches(":hover"), false, + "Button 1 should not be hovered after mousemove over label2"); + is($("label1").matches(":hover"), false, + "Label 1 should not be hovered after mousemove over label2"); + is($("button2").matches(":hover"), true, + "Button 2 should be hovered after mousemove over label2"); + is($("label2").matches(":hover"), true, + "Label 2 should be hovered after mousemove over label2"); + SimpleTest.finish(); +} + +function executeTests() { + var testYielder = tests(); + function execNext() { + let {done} = testYielder.next(); + if (done) { + return; + } + SimpleTest.executeSoon(execNext); + } + execNext(); +} + +SimpleTest.waitForFocus(executeTests); + +</script> +</pre> +</body> +</html> diff --git a/dom/events/test/test_bug656954.html b/dom/events/test/test_bug656954.html new file mode 100644 index 0000000000..60f8cf4133 --- /dev/null +++ b/dom/events/test/test_bug656954.html @@ -0,0 +1,42 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=656954 +--> +<head> + <title>Test for Bug 656954</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=656954">Mozilla Bug 656954</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 656954 **/ + +var e = document.createEvent("Event"); +is(e.defaultPrevented, false, + "After creating event defaultPrevented should be false"); +e.initEvent("foo", true, true); +var el = document.createElement("div"); +el.addEventListener("foo", + function(evt) { + evt.preventDefault(); + }); +el.dispatchEvent(e); +is(e.defaultPrevented, true, "preventDefault() should have been called!"); + +e = document.createEvent("Event"); +e.initEvent("foo", true, false); +el.dispatchEvent(e); +is(e.defaultPrevented, false, "preventDefault() should not have any effect!"); + +</script> +</pre> +</body> +</html> diff --git a/dom/events/test/test_bug659071.html b/dom/events/test/test_bug659071.html new file mode 100644 index 0000000000..920d86da29 --- /dev/null +++ b/dom/events/test/test_bug659071.html @@ -0,0 +1,39 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=659071 +--> +<head> + <title>Test for Bug 659071</title> + <script type="text/javascript" src="/MochiKit/MochiKit.js"></script> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=659071">Mozilla Bug 659071</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +/** Test for Bug 659071 **/ + +SimpleTest.waitForExplicitFinish(); +SimpleTest.requestFlakyTimeout("untriaged"); + +var subWin = window.open("window_bug659071.html", "_blank", + "width=500,height=500"); + +function finish() +{ + subWin.close(); + SimpleTest.finish(); +} + +</script> +</pre> +</body> +</html> diff --git a/dom/events/test/test_bug659350.html b/dom/events/test/test_bug659350.html new file mode 100644 index 0000000000..f4d64d1f35 --- /dev/null +++ b/dom/events/test/test_bug659350.html @@ -0,0 +1,111 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=659350 +--> +<head> + <title>Test for Bug 659350</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=659350">Mozilla Bug 659350</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 659350 **/ +function testIn(eventName, obj, objName, expected) { + is(eventName in obj, expected, "'" + eventName + "' shuld be in " + objName); +} + +var div = document.createElement("div"); + +// Forwarded events +testIn("onscroll", window, "window", true); +testIn("onscroll", document.body, "body", true); +testIn("onscroll", div, "div", true); +// Window events +testIn("onpopstate", window, "window", true); +testIn("onpopstate", document.body, "body", true); +testIn("onpopstate", div, "div", false); +// Non-idl events +testIn("onopen", window, "window", false); +testIn("onopen", document.body, "body", false); +testIn("onopen", div, "div", false); + +function f() {} +function g() {} + +// Basic sanity of interaction between the IDL and content attributes +div.onload = f; +is(div.onload, f, "Should have 'f' as div's onload"); +div.setAttribute("onload", ""); +isnot(div.onload, f, "Should not longer have 'f' as div's onload"); +is(div.onload.toString(), "function onload(event) {\n\n}", + "Should have wrapped empty string in a function"); +div.setAttribute("onload", "foopy();"); +is(div.onload.toString(), "function onload(event) {\nfoopy();\n}", + "Should have wrapped call in a function"); +div.removeAttribute("onload"); +is(div.onload, null, "Should have null onload now"); + +// Test forwarding to window for both events that are window-specific and that +// exist on all elements +function testPropagationToWindow(eventName) { + is(window["on"+eventName], null, "Shouldn't have " + eventName + " stuff yet"); + document.body["on"+eventName] = f; + is(window["on"+eventName], f, + "Setting on"+eventName+" on body should propagate to window"); + document.createElement("body")["on"+eventName] = g; + is(window["on"+eventName], g, + "Setting on"+eventName+" on body not in document should propagate to window"); + document.createElement("frameset")["on"+eventName] = f; + is(window["on"+eventName], f, + "Setting on"+eventName+" on frameset not in document should propagate to window"); + + document.body.setAttribute("on"+eventName, eventName); + is(window["on"+eventName].toString(), + "function on"+eventName+"(event) {\n"+eventName+"\n}", + "Setting on"+eventName+"attribute on body should propagate to window"); + document.createElement("body").setAttribute("on"+eventName, eventName+"2"); + is(window["on"+eventName].toString(), + "function on"+eventName+"(event) {\n"+eventName+"2\n}", + "Setting on"+eventName+"attribute on body outside the document should propagate to window"); +} + +testPropagationToWindow("popstate"); +testPropagationToWindow("scroll"); + +// Test |this| and scoping +var called; +div.onscroll = function(event) { + is(this, div, "This should be div when invoking event listener"); + is(event, ev, "Event argument should be the event that was dispatched"); + called = true; +} +var ev = document.createEvent("Events"); +ev.initEvent("scroll", true, true); +called = false; +div.dispatchEvent(ev); +is(called, true, "Event listener set via on* property not called"); + +div.foopy = "Found me"; +document.foopy = "Didn't find me"; +document.foopy2 = "Found me"; +div.setAttribute("onscroll", + "is(this, div, 'This should be div when invoking via attribute');\ + is(foopy, 'Found me', 'div should be on the scope chain when invoking handler compiled from content attribute');\ + is(foopy2, 'Found me', 'document should be on the scope chain when invking handler compiled from content attribute');\ + is(event, ev, 'Event argument should be the event that was dispatched');\ + called = true;"); +called = false; +div.dispatchEvent(ev); +is(called, true, "Event listener set via on* attribute not called"); +</script> +</pre> +</body> +</html> diff --git a/dom/events/test/test_bug662678.html b/dom/events/test/test_bug662678.html new file mode 100644 index 0000000000..22fe8e75a3 --- /dev/null +++ b/dom/events/test/test_bug662678.html @@ -0,0 +1,153 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=662678 +--> +<head> + <title>Test for Bug 662678</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=662678">Mozilla Bug 662678</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 662678 **/ +SimpleTest.waitForExplicitFinish(); + +var checkMotion = function(event) { + window.removeEventListener("devicemotion", checkMotion, true); + + is(event.acceleration.x, 1.5, "acceleration.x"); + is(event.acceleration.y, 2.5, "acceleration.y"); + is(event.acceleration.z, 3.5, "acceleration.z"); + is(event.accelerationIncludingGravity.x, 4.5, "accelerationIncludingGravity.x"); + is(event.accelerationIncludingGravity.y, 5.5, "accelerationIncludingGravity.y"); + is(event.accelerationIncludingGravity.z, 6.5, "accelerationIncludingGravity.z"); + is(event.rotationRate.alpha, 7.5, "rotationRate.alpha"); + is(event.rotationRate.beta, 8.5, "rotationRate.beta"); + is(event.rotationRate.gamma, 9.5, "rotationRate.gamma"); + is(event.interval, 0.5, "interval"); + + var e = document.createEvent("DeviceMotionEvent"); + e.initDeviceMotionEvent('devicemotion', true, true, + null, null, null, null); + is(e.acceleration.x, null, "acceleration.x"); + is(e.acceleration.y, null, "acceleration.y"); + is(e.acceleration.z, null, "acceleration.z"); + is(e.accelerationIncludingGravity.x, null, "accelerationIncludingGravity.x"); + is(e.accelerationIncludingGravity.y, null, "accelerationIncludingGravity.y"); + is(e.accelerationIncludingGravity.z, null, "accelerationIncludingGravity.z"); + is(e.rotationRate.alpha, null, "rotationRate.alpha"); + is(e.rotationRate.beta, null, "rotationRate.beta"); + is(e.rotationRate.gamma, null, "rotationRate.gamma"); + is(e.interval, null, "interval"); + + e.initDeviceMotionEvent('devicemotion', true, true, + {}, {}, {}, 0); + is(e.acceleration.x, null, "acceleration.x"); + is(e.acceleration.y, null, "acceleration.y"); + is(e.acceleration.z, null, "acceleration.z"); + is(e.accelerationIncludingGravity.x, null, "accelerationIncludingGravity.x"); + is(e.accelerationIncludingGravity.y, null, "accelerationIncludingGravity.y"); + is(e.accelerationIncludingGravity.z, null, "accelerationIncludingGravity.z"); + is(e.rotationRate.alpha, null, "rotationRate.alpha"); + is(e.rotationRate.beta, null, "rotationRate.beta"); + is(e.rotationRate.gamma, null, "rotationRate.gamma"); + is(e.interval, 0, "interval"); + + window.addEventListener("devicemotion", checkMotionCtor, true); + + event = new DeviceMotionEvent('devicemotion', { + bubbles: true, cancelable: true, + acceleration: {x:1.5,y:2.5,z:3.5}, + accelerationIncludingGravity: {x:4.5,y:5.5,z:6.5}, + rotationRate: {alpha:7.5,beta:8.5,gamma:9.5}, + interval: 0.5 + }); + window.dispatchEvent(event); +}; + +var checkMotionCtor = function(event) { + window.removeEventListener("devicemotion", checkMotionCtor, true); + + is(event.acceleration.x, 1.5, "acceleration.x"); + is(event.acceleration.y, 2.5, "acceleration.y"); + is(event.acceleration.z, 3.5, "acceleration.z"); + is(event.accelerationIncludingGravity.x, 4.5, "accelerationIncludingGravity.x"); + is(event.accelerationIncludingGravity.y, 5.5, "accelerationIncludingGravity.y"); + is(event.accelerationIncludingGravity.z, 6.5, "accelerationIncludingGravity.z"); + is(event.rotationRate.alpha, 7.5, "rotationRate.alpha"); + is(event.rotationRate.beta, 8.5, "rotationRate.beta"); + is(event.rotationRate.gamma, 9.5, "rotationRate.gamma"); + is(event.interval, 0.5, "interval"); + + var e = new DeviceMotionEvent('devicemotion'); + is(e.acceleration.x, null, "acceleration.x"); + is(e.acceleration.y, null, "acceleration.y"); + is(e.acceleration.z, null, "acceleration.z"); + is(e.accelerationIncludingGravity.x, null, "accelerationIncludingGravity.x"); + is(e.accelerationIncludingGravity.y, null, "accelerationIncludingGravity.y"); + is(e.accelerationIncludingGravity.z, null, "accelerationIncludingGravity.z"); + is(e.rotationRate.alpha, null, "rotationRate.alpha"); + is(e.rotationRate.beta, null, "rotationRate.beta"); + is(e.rotationRate.gamma, null, "rotationRate.gamma"); + is(e.interval, null, "interval"); + + e = new DeviceMotionEvent('devicemotion', { + bubbles: true, cancelable: true, + acceleration: null, accelerationIncludingGravity: null, + rotationRate: null, interval: null + }); + is(e.acceleration.x, null, "acceleration.x"); + is(e.acceleration.y, null, "acceleration.y"); + is(e.acceleration.z, null, "acceleration.z"); + is(e.accelerationIncludingGravity.x, null, "accelerationIncludingGravity.x"); + is(e.accelerationIncludingGravity.y, null, "accelerationIncludingGravity.y"); + is(e.accelerationIncludingGravity.z, null, "accelerationIncludingGravity.z"); + is(e.rotationRate.alpha, null, "rotationRate.alpha"); + is(e.rotationRate.beta, null, "rotationRate.beta"); + is(e.rotationRate.gamma, null, "rotationRate.gamma"); + is(e.interval, null, "interval"); + + e = new DeviceMotionEvent('devicemotion', { + bubbles: true, cancelable: true, + acceleration: {}, accelerationIncludingGravity: {}, + rotationRate: {}, interval: 0 + }); + is(e.acceleration.x, null, "acceleration.x"); + is(e.acceleration.y, null, "acceleration.y"); + is(e.acceleration.z, null, "acceleration.z"); + is(e.accelerationIncludingGravity.x, null, "accelerationIncludingGravity.x"); + is(e.accelerationIncludingGravity.y, null, "accelerationIncludingGravity.y"); + is(e.accelerationIncludingGravity.z, null, "accelerationIncludingGravity.z"); + is(e.rotationRate.alpha, null, "rotationRate.alpha"); + is(e.rotationRate.beta, null, "rotationRate.beta"); + is(e.rotationRate.gamma, null, "rotationRate.gamma"); + is(e.interval, 0, "interval"); + + SimpleTest.finish(); +}; + +window.addEventListener("devicemotion", checkMotion, true); + +var event = DeviceMotionEvent; +ok(!!event, "Should have seen DeviceMotionEvent!"); + +event = document.createEvent("DeviceMotionEvent"); +event.initDeviceMotionEvent('devicemotion', true, true, + {x:1.5,y:2.5,z:3.5}, + {x:4.5,y:5.5,z:6.5}, + {alpha:7.5,beta:8.5,gamma:9.5}, + 0.5); +window.dispatchEvent(event); + +</script> +</pre> +</body> +</html> diff --git a/dom/events/test/test_bug667612.html b/dom/events/test/test_bug667612.html new file mode 100644 index 0000000000..95e7f430de --- /dev/null +++ b/dom/events/test/test_bug667612.html @@ -0,0 +1,38 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=667612 +--> +<head> + <title>Test for Bug 667612</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=667612">Mozilla Bug 667612</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +xhr = new XMLHttpRequest; +w = new Worker("empty.js"); +window.addEventListener("load", null); +document.addEventListener("load", null); +document.body.addEventListener("load", null); +xhr.addEventListener("load", null); +w.addEventListener("load", null); +window.addEventListener("load", undefined); +document.addEventListener("load", undefined); +document.body.addEventListener("load", undefined); +xhr.addEventListener("load", undefined); +w.addEventListener("load", undefined); + +ok(true, "didn't throw"); + +</script> +</pre> +</body> +</html> diff --git a/dom/events/test/test_bug667919-1.html b/dom/events/test/test_bug667919-1.html new file mode 100644 index 0000000000..d4be92dafb --- /dev/null +++ b/dom/events/test/test_bug667919-1.html @@ -0,0 +1,41 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=615597 +--> +<head> + <title>Test for Bug 615597</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=615597">Mozilla Bug 615597</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 615597 **/ + +window.ondeviceorientation = function(event) { + is(event.alpha, 1.5); + is(event.beta, 2.25); + is(event.gamma, 3.667); + is(event.absolute, true); + SimpleTest.finish(); +}; + +var event = DeviceOrientationEvent; +ok(!!event, "Should have seen DeviceOrientationEvent!"); + +event = document.createEvent("DeviceOrientationEvent"); +event.initDeviceOrientationEvent('deviceorientation', true, true, 1.5, 2.25, 3.667, true); +window.dispatchEvent(event); +SimpleTest.waitForExplicitFinish(); + +</script> +</pre> +</body> +</html> diff --git a/dom/events/test/test_bug679494.xhtml b/dom/events/test/test_bug679494.xhtml new file mode 100644 index 0000000000..22bda7f6f3 --- /dev/null +++ b/dom/events/test/test_bug679494.xhtml @@ -0,0 +1,36 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=679494 +--> +<window title="Mozilla Bug 679494" onload="doTest();" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + +<body xmlns="http://www.w3.org/1999/xhtml"> + <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=679494">Mozilla Bug 679494</a> + <p id="display"></p> +<div id="content" style="display: none"> + <iframe id="contentframe" src="http://mochi.test:8888/tests/dom/events/test/file_bug679494.html"></iframe> +</div> +</body> + +<script class="testbody" type="application/javascript"><![CDATA[ + +/* Test for bug 679494 */ +function doTest() { + SimpleTest.waitForExplicitFinish(); + + var w = document.getElementById("contentframe").contentWindow; + w.addEventListener("message", function(e) { + is("test", e.data, "We got the data without a compartment mismatch assertion!"); + SimpleTest.finish(); + }, false); + w.postMessage("test", "*"); +} + +]]></script> + +</window> diff --git a/dom/events/test/test_bug684208.html b/dom/events/test/test_bug684208.html new file mode 100644 index 0000000000..7405c9cb5c --- /dev/null +++ b/dom/events/test/test_bug684208.html @@ -0,0 +1,80 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=684208 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 684208</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript"> + + /** Test for Bug 684208 **/ + + function checkDispatchReturnValue(targetOrUndefined) { + var target = targetOrUndefined ? targetOrUndefined : self; + function createEvent() { + if ("MouseEvent" in this) { + return new MouseEvent("click", {cancelable: true}); + } + return new Event("dummy", {cancelable: true}); + } + + function postSelfMessage(msg) { + try { + self.postMessage(msg); + } catch(ex) { + self.postMessage(msg, "*"); + } + } + + function passiveListener(e) { + e.target.removeEventListener(e.type, passiveListener); + } + var event = createEvent(); + target.addEventListener(event.type, passiveListener); + postSelfMessage(target.dispatchEvent(event) == true); + + function cancellingListener(e) { + e.target.removeEventListener(e.type, cancellingListener); + e.preventDefault(); + } + event = createEvent(); + target.addEventListener(event.type, cancellingListener); + postSelfMessage(target.dispatchEvent(event) == false); + } + + function test() { + var expectedEvents = 6; + function messageHandler(e) { + ok(e.data, "All the dispatchEvent calls should pass."); + --expectedEvents; + if (!expectedEvents) { + window.onmessage = null; + window.worker.onmessage = null; + SimpleTest.finish(); + } + } + window.onmessage = messageHandler; + checkDispatchReturnValue(); + checkDispatchReturnValue(document.getElementById("link")); + window.worker = + new Worker(URL.createObjectURL(new Blob(["(" + checkDispatchReturnValue.toString() + ")();"]))); + window.worker.onmessage = messageHandler; + } + + SimpleTest.waitForExplicitFinish(); + + </script> +</head> +<body onload="test();"> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=684208">Mozilla Bug 684208</a> +<p id="display"></p> +<div id="content" style="display: none"> +<a id="link" href="#foo">foo</a> +</div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/dom/events/test/test_bug687787.html b/dom/events/test/test_bug687787.html new file mode 100644 index 0000000000..130aa011f2 --- /dev/null +++ b/dom/events/test/test_bug687787.html @@ -0,0 +1,610 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=687787 +--> +<head> + <title>Test for Bug 687787</title> + <script type="text/javascript" src="/MochiKit/MochiKit.js"></script> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=687787">Mozilla Bug 687787</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> + +<pre id="test"> +<script class="testbody" type="text/javascript"> + +var eventStack = []; + +function _callback(e){ + var event = {'type' : e.type, 'target' : e.target, 'relatedTarget' : e.relatedTarget } + eventStack.push(event); +} + +function clearEventStack(){ + eventStack = []; +} + +window.addEventListener("focus", _callback, true); +window.addEventListener("focusin", _callback, true); +window.addEventListener("focusout", _callback, true); +window.addEventListener("blur", _callback, true); + +function CompareEventToExpected(e, expected) { + if (expected == null || e == null) + return false; + if (e.type == expected.type && e.target == expected.target && e.relatedTarget == expected.relatedTarget) + return true; + return false; +} + +function TestEventOrderNormal() { + + var input1 = document.createElement('input'); + var input2 = document.createElement('input'); + var input3 = document.createElement('input'); + var content = document.getElementById('content'); + + input1.setAttribute('id', 'input1'); + input2.setAttribute('id', 'input2'); + input3.setAttribute('id', 'input3'); + input1.setAttribute('type', 'text'); + input2.setAttribute('type', 'text'); + input3.setAttribute('type', 'text'); + + content.appendChild(input1); + content.appendChild(input2); + content.appendChild(input3); + content.style.display = 'block' + + let expectedEventOrder = [ + {'type' : 'blur', + 'target' : input1, + 'relatedTarget' : input2}, + {'type' : 'focusout', + 'target' : input1, + 'relatedTarget' : input2}, + {'type' : 'focus', + 'target' : input2, + 'relatedTarget' : input1}, + {'type' : 'focusin', + 'target' : input2, + 'relatedTarget' : input1}, + {'type' : 'blur', + 'target' : input2, + 'relatedTarget' : input3}, + {'type' : 'focusout', + 'target' : input2, + 'relatedTarget' : input3}, + {'type' : 'focus', + 'target' : input3, + 'relatedTarget' : input2}, + {'type' : 'focusin', + 'target' : input3, + 'relatedTarget' : input2}, + ] + + input1.focus(); + clearEventStack(); + + input2.focus(); + input3.focus(); + + for (var i = 0; i < expectedEventOrder.length || i < eventStack.length ; i++) { + ok(CompareEventToExpected(expectedEventOrder[i], eventStack[i]), 'Normal event order is correct: Event ' + i + ': ' + + 'Expected (' + + expectedEventOrder[i].type + ',' + + (expectedEventOrder[i].target ? expectedEventOrder[i].target.id : null) + ',' + + (expectedEventOrder[i].relatedTarget ? expectedEventOrder[i].relatedTarget.id : null) + '), ' + + 'Actual (' + + eventStack[i].type + ',' + + (eventStack[i].target ? eventStack[i].target.id : null) + ',' + + (eventStack[i].relatedTarget ? eventStack[i].relatedTarget.id : null) + ')'); + } + + content.innerHTML = ''; +} + +function TestEventOrderNormalFiresAtRightTime() { + + var input1 = document.createElement('input'); + var input2 = document.createElement('input'); + var input3 = document.createElement('input'); + var content = document.getElementById('content'); + + input1.setAttribute('id', 'input1'); + input2.setAttribute('id', 'input2'); + input3.setAttribute('id', 'input3'); + input1.setAttribute('type', 'text'); + input2.setAttribute('type', 'text'); + input3.setAttribute('type', 'text'); + + input1.onblur = function(e) + { + ok(document.activeElement == document.body, 'input1: not focused when blur fires') + } + + input1.addEventListener('focusout', function(e) + { + ok(document.activeElement == document.body, 'input1: not focused when focusout fires') + }); + + input2.onfocus = function(e) + { + ok(document.activeElement == input2, 'input2: focused when focus fires') + } + + input2.addEventListener('focusin', function(e) + { + ok(document.activeElement == input2, 'input2: focused when focusin fires') + }); + + content.appendChild(input1); + content.appendChild(input2); + content.style.display = 'block' + + let expectedEventOrder = [ + {'type' : 'blur', + 'target' : input1, + 'relatedTarget' : input2}, + {'type' : 'focusout', + 'target' : input1, + 'relatedTarget' : input2}, + {'type' : 'focus', + 'target' : input2, + 'relatedTarget' : input1}, + {'type' : 'focusin', + 'target' : input2, + 'relatedTarget' : input1}, + ] + + input1.focus(); + clearEventStack(); + + input2.focus(); + + for (var i = 0; i < expectedEventOrder.length || i < eventStack.length ; i++) { + ok(CompareEventToExpected(expectedEventOrder[i], eventStack[i]), 'Normal event order is correct: Event ' + i + ': ' + + 'Expected (' + + expectedEventOrder[i].type + ',' + + (expectedEventOrder[i].target ? expectedEventOrder[i].target.id : null) + ',' + + (expectedEventOrder[i].relatedTarget ? expectedEventOrder[i].relatedTarget.id : null) + '), ' + + 'Actual (' + + eventStack[i].type + ',' + + (eventStack[i].target ? eventStack[i].target.id : null) + ',' + + (eventStack[i].relatedTarget ? eventStack[i].relatedTarget.id : null) + ')'); + } + + content.innerHTML = ''; +} + +function TestFocusOutRedirectsFocus() { + + var input1 = document.createElement('input'); + var input2 = document.createElement('input'); + var input3 = document.createElement('input'); + var content = document.getElementById('content'); + + input1.setAttribute('id', 'input1'); + input2.setAttribute('id', 'input2'); + input3.setAttribute('id', 'input3'); + input1.setAttribute('type', 'text'); + input2.setAttribute('type', 'text'); + input3.setAttribute('type', 'text'); + input1.addEventListener('focusout', function () { + input3.focus(); + }); + + content.appendChild(input1); + content.appendChild(input2); + content.appendChild(input3); + content.style.display = 'block' + + let expectedEventOrder = [ + {'type' : 'blur', + 'target' : input1, + 'relatedTarget' : input2}, + {'type' : 'focusout', + 'target' : input1, + 'relatedTarget' : input2}, + {'type' : 'focus', + 'target' : input3, + 'relatedTarget' : null}, + {'type' : 'focusin', + 'target' : input3, + 'relatedTarget' : null}, + ] + + input1.focus(); + clearEventStack(); + input2.focus(); + + for (var i = 0; i < expectedEventOrder.length || i < eventStack.length; i++) { + ok(CompareEventToExpected(expectedEventOrder[i], eventStack[i]), 'Normal event order is correct: Event ' + i + ': ' + + 'Expected (' + + expectedEventOrder[i].type + ',' + + (expectedEventOrder[i].target ? expectedEventOrder[i].target.id : null) + ',' + + (expectedEventOrder[i].relatedTarget ? expectedEventOrder[i].relatedTarget.id : null) + '), ' + + 'Actual (' + + eventStack[i].type + ',' + + (eventStack[i].target ? eventStack[i].target.id : null) + ',' + + (eventStack[i].relatedTarget ? eventStack[i].relatedTarget.id : null) + ')'); + } + + content.innerHTML = ''; +} + +function TestFocusInRedirectsFocus() { + + var input1 = document.createElement('input'); + var input2 = document.createElement('input'); + var input3 = document.createElement('input'); + var content = document.getElementById('content'); + + input1.setAttribute('id', 'input1'); + input2.setAttribute('id', 'input2'); + input3.setAttribute('id', 'input3'); + input1.setAttribute('type', 'text'); + input2.setAttribute('type', 'text'); + input3.setAttribute('type', 'text'); + input2.addEventListener('focusin', function () { + input3.focus(); + }); + + content.appendChild(input1); + content.appendChild(input2); + content.appendChild(input3); + content.style.display = 'block' + + let expectedEventOrder = [ + {'type' : 'blur', + 'target' : input1, + 'relatedTarget' : input2}, + {'type' : 'focusout', + 'target' : input1, + 'relatedTarget' : input2}, + {'type' : 'focus', + 'target' : input2, + 'relatedTarget' : input1}, + {'type' : 'focusin', + 'target' : input2, + 'relatedTarget' : input1}, + {'type' : 'blur', + 'target' : input2, + 'relatedTarget' : input3}, + {'type' : 'focusout', + 'target' : input2, + 'relatedTarget' : input3}, + {'type' : 'focus', + 'target' : input3, + 'relatedTarget' : input2}, + {'type' : 'focusin', + 'target' : input3, + 'relatedTarget' : input2}, + ] + + input1.focus(); + clearEventStack(); + input2.focus(); + + for (var i = 0; i < expectedEventOrder.length || i < eventStack.length; i++) { + ok(CompareEventToExpected(expectedEventOrder[i], eventStack[i]), 'Normal event order is correct: Event ' + i + ': ' + + 'Expected (' + + expectedEventOrder[i].type + ',' + + (expectedEventOrder[i].target ? expectedEventOrder[i].target.id : null) + ',' + + (expectedEventOrder[i].relatedTarget ? expectedEventOrder[i].relatedTarget.id : null) + '), ' + + 'Actual (' + + eventStack[i].type + ',' + + (eventStack[i].target ? eventStack[i].target.id : null) + ',' + + (eventStack[i].relatedTarget ? eventStack[i].relatedTarget.id : null) + ')'); + } + + content.innerHTML = ''; +} + +function TestBlurRedirectsFocus() { + + var input1 = document.createElement('input'); + var input2 = document.createElement('input'); + var input3 = document.createElement('input'); + var content = document.getElementById('content'); + + input1.setAttribute('id', 'input1'); + input2.setAttribute('id', 'input2'); + input3.setAttribute('id', 'input3'); + input1.setAttribute('type', 'text'); + input2.setAttribute('type', 'text'); + input3.setAttribute('type', 'text'); + input1.onblur = function () { + input3.focus(); + } + + content.appendChild(input1); + content.appendChild(input2); + content.appendChild(input3); + content.style.display = 'block' + + let expectedEventOrder = [ + {'type' : 'blur', + 'target' : input1, + 'relatedTarget' : input2}, + {'type' : 'focus', + 'target' : input3, + 'relatedTarget' : null}, + {'type' : 'focusin', + 'target' : input3, + 'relatedTarget' : null}, + {'type' : 'focusout', + 'target' : input1, + 'relatedTarget' : input2}, + ] + + input1.focus(); + clearEventStack(); + input2.focus(); + + for (var i = 0; i < expectedEventOrder.length || i < eventStack.length; i++) { + ok(CompareEventToExpected(expectedEventOrder[i], eventStack[i]), 'Normal event order is correct: Event ' + i + ': ' + + 'Expected (' + + expectedEventOrder[i].type + ',' + + (expectedEventOrder[i].target ? expectedEventOrder[i].target.id : null) + ',' + + (expectedEventOrder[i].relatedTarget ? expectedEventOrder[i].relatedTarget.id : null) + '), ' + + 'Actual (' + + eventStack[i].type + ',' + + (eventStack[i].target ? eventStack[i].target.id : null) + ',' + + (eventStack[i].relatedTarget ? eventStack[i].relatedTarget.id : null) + ')'); + } + + content.innerHTML = ''; +} + +function TestFocusRedirectsFocus() { + + var input1 = document.createElement('input'); + var input2 = document.createElement('input'); + var input3 = document.createElement('input'); + var content = document.getElementById('content'); + + input1.setAttribute('id', 'input1'); + input2.setAttribute('id', 'input2'); + input3.setAttribute('id', 'input3'); + input1.setAttribute('type', 'text'); + input2.setAttribute('type', 'text'); + input3.setAttribute('type', 'text'); + input2.onfocus = function () { + input3.focus(); + } + + content.appendChild(input1); + content.appendChild(input2); + content.appendChild(input3); + content.style.display = 'block' + + let expectedEventOrder = [ + {'type' : 'blur', + 'target' : input1, + 'relatedTarget' : input2}, + {'type' : 'focusout', + 'target' : input1, + 'relatedTarget' : input2}, + {'type' : 'focus', + 'target' : input2, + 'relatedTarget' : input1}, + {'type' : 'blur', + 'target' : input2, + 'relatedTarget' : input3}, + {'type' : 'focusout', + 'target' : input2, + 'relatedTarget' : input3}, + {'type' : 'focus', + 'target' : input3, + 'relatedTarget' : input2}, + {'type' : 'focusin', + 'target' : input3, + 'relatedTarget' : input2}, + ] + + input1.focus(); + clearEventStack(); + input2.focus(); + + for (var i = 0; i < expectedEventOrder.length || i < eventStack.length; i++) { + ok(CompareEventToExpected(expectedEventOrder[i], eventStack[i]), 'Normal event order is correct: Event ' + i + ': ' + + 'Expected (' + + expectedEventOrder[i].type + ',' + + (expectedEventOrder[i].target ? expectedEventOrder[i].target.id : null) + ',' + + (expectedEventOrder[i].relatedTarget ? expectedEventOrder[i].relatedTarget.id : null) + '), ' + + 'Actual (' + + eventStack[i].type + ',' + + (eventStack[i].target ? eventStack[i].target.id : null) + ',' + + (eventStack[i].relatedTarget ? eventStack[i].relatedTarget.id : null) + ')'); + } + + content.innerHTML = ''; +} + +function TestEventOrderDifferentDocument() { + + var input1 = document.createElement('input'); + var input2 = document.createElement('input'); + var iframe1 = document.createElement('iframe'); + var content = document.getElementById('content'); + + input1.setAttribute('id', 'input1'); + input2.setAttribute('id', 'input2'); + iframe1.setAttribute('id', 'iframe1'); + input1.setAttribute('type', 'text'); + input2.setAttribute('type', 'text'); + + content.appendChild(input1); + content.appendChild(iframe1); + iframe1.contentDocument.body.appendChild(input2); + content.style.display = 'block' + + iframe1.contentDocument.addEventListener("focus", _callback, true); + iframe1.contentDocument.addEventListener("focusin", _callback, true); + iframe1.contentDocument.addEventListener("focusout", _callback, true); + iframe1.contentDocument.addEventListener("blur", _callback, true); + + let expectedEventOrder = [ + {'type' : 'blur', + 'target' : input1, + 'relatedTarget' : null}, + {'type' : 'focusout', + 'target' : input1, + 'relatedTarget' : null}, + {'type' : 'blur', + 'target' : document, + 'relatedTarget' : null}, + {'type' : 'blur', + 'target' : window, + 'relatedTarget' : null}, + {'type' : 'focus', + 'target' : iframe1.contentDocument, + 'relatedTarget' : null}, + {'type' : 'focus', + 'target' : input2, + 'relatedTarget' : null}, + {'type' : 'focusin', + 'target' : input2, + 'relatedTarget' : null}, + ] + + input1.focus(); + clearEventStack(); + input2.focus(); + + for (var i = 0; i < expectedEventOrder.length || i < eventStack.length; i++) { + ok(CompareEventToExpected(expectedEventOrder[i], eventStack[i]), 'Normal event order is correct: Event ' + i + ': ' + + 'Expected (' + + expectedEventOrder[i].type + ',' + + (expectedEventOrder[i].target ? expectedEventOrder[i].target.id : null) + ',' + + (expectedEventOrder[i].relatedTarget ? expectedEventOrder[i].relatedTarget.id : null) + '), ' + + 'Actual (' + + eventStack[i].type + ',' + + (eventStack[i].target ? eventStack[i].target.id : null) + ',' + + (eventStack[i].relatedTarget ? eventStack[i].relatedTarget.id : null) + ')'); + } + + content.innerHTML = ''; +} + + +function TestFocusOutMovesTarget() { + + var input1 = document.createElement('input'); + var input2 = document.createElement('input'); + var iframe1 = document.createElement('iframe'); + var content = document.getElementById('content'); + + input1.setAttribute('id', 'input1'); + input2.setAttribute('id', 'input2'); + iframe1.setAttribute('id', 'iframe1'); + input1.setAttribute('type', 'text'); + input2.setAttribute('type', 'text'); + + input1.addEventListener('focusout', function () { + iframe1.contentDocument.body.appendChild(input2); + }); + + content.appendChild(input1); + content.appendChild(input2); + content.appendChild(iframe1); + content.style.display = 'block' + + iframe1.contentDocument.addEventListener("focus", _callback, true); + iframe1.contentDocument.addEventListener("focusin", _callback, true); + iframe1.contentDocument.addEventListener("focusout", _callback, true); + iframe1.contentDocument.addEventListener("blur", _callback, true); + + let expectedEventOrder = [ + {'type' : 'blur', + 'target' : input1, + 'relatedTarget' : input2}, + {'type' : 'focusout', + 'target' : input1, + 'relatedTarget' : input2}, + ] + + input1.focus(); + clearEventStack(); + input2.focus(); + + for (var i = 0; i < expectedEventOrder.length || i < eventStack.length; i++) { + ok(CompareEventToExpected(expectedEventOrder[i], eventStack[i]), 'Normal event order is correct: Event ' + i + ': ' + + 'Expected (' + + expectedEventOrder[i].type + ',' + + (expectedEventOrder[i].target ? expectedEventOrder[i].target.id : null) + ',' + + (expectedEventOrder[i].relatedTarget ? expectedEventOrder[i].relatedTarget.id : null) + '), ' + + 'Actual (' + + eventStack[i].type + ',' + + (eventStack[i].target ? eventStack[i].target.id : null) + ',' + + (eventStack[i].relatedTarget ? eventStack[i].relatedTarget.id : null) + ')'); + } + + content.innerHTML = ''; +} + +function TestBlurWindowAndRefocusInputOnlyFiresFocusInOnInput() { + + var input1 = document.createElement('input'); + var content = document.getElementById('content'); + + input1.setAttribute('id', 'input1'); + input1.setAttribute('type', 'text'); + + content.appendChild(input1); + + let expectedEventOrder = [ + {'type' : 'focus', + 'target' : document, + 'relatedTarget' : null}, + {'type' : 'focus', + 'target' : window, + 'relatedTarget' : null}, + {'type' : 'focus', + 'target' : input1, + 'relatedTarget' : null}, + {'type' : 'focusin', + 'target' : input1, + 'relatedTarget' : null}, + ] + + window.blur(); + clearEventStack(); + input1.focus(); + + for (var i = 0; i < expectedEventOrder.length || i < eventStack.length; i++) { + ok(CompareEventToExpected(expectedEventOrder[i], eventStack[i]), 'Normal event order is correct: Event ' + i + ': ' + + 'Expected (' + + expectedEventOrder[i].type + ',' + + (expectedEventOrder[i].target ? expectedEventOrder[i].target.id : null) + ',' + + (expectedEventOrder[i].relatedTarget ? expectedEventOrder[i].relatedTarget.id : null) + '), ' + + 'Actual (' + + eventStack[i].type + ',' + + (eventStack[i].target ? eventStack[i].target.id : null) + ',' + + (eventStack[i].relatedTarget ? eventStack[i].relatedTarget.id : null) + ')'); + } + + content.innerHTML = ''; +} + +TestEventOrderNormal(); +TestEventOrderNormalFiresAtRightTime(); +TestFocusOutRedirectsFocus(); +TestFocusInRedirectsFocus(); +TestBlurRedirectsFocus(); +TestFocusRedirectsFocus(); +TestFocusOutMovesTarget(); +TestEventOrderDifferentDocument(); +TestBlurWindowAndRefocusInputOnlyFiresFocusInOnInput(); + +</script> +</pre> +</body> +</html> diff --git a/dom/events/test/test_bug689564.html b/dom/events/test/test_bug689564.html new file mode 100644 index 0000000000..7183e6169d --- /dev/null +++ b/dom/events/test/test_bug689564.html @@ -0,0 +1,65 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=689564 +--> +<head> + <title>Test for Bug 689564</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=689564">Mozilla Bug 689564</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 689564 **/ +var div = document.createElement("div"); +div.setAttribute("onclick", "div"); +is(window.onclick, null, "div should not forward onclick"); +is(div.onclick.toString(), "function onclick(event) {\ndiv\n}", + "div should have an onclick handler"); + +div.setAttribute("onscroll", "div"); +is(window.onscroll, null, "div should not forward onscroll"); +is(div.onscroll.toString(), "function onscroll(event) {\ndiv\n}", + "div should have an onscroll handler"); + +div.setAttribute("onpopstate", "div"); +is(window.onpopstate, null, "div should not forward onpopstate"); +is("onpopstate" in div, false, "div should not have onpopstate handler"); + +var body = document.createElement("body"); +body.setAttribute("onclick", "body"); +is(window.onclick, null, "body should not forward onclick"); +is(body.onclick.toString(), "function onclick(event) {\nbody\n}", + "body should have an onclick handler"); +body.setAttribute("onscroll", "body"); +is(window.onscroll.toString(), "function onscroll(event) {\nbody\n}", + "body should forward onscroll"); +body.setAttribute("onpopstate", "body"); +is(window.onpopstate.toString(), "function onpopstate(event) {\nbody\n}", + "body should forward onpopstate"); + +var frameset = document.createElement("frameset"); +frameset.setAttribute("onclick", "frameset"); +is(window.onclick, null, "frameset should not forward onclick"); +is(frameset.onclick.toString(), "function onclick(event) {\nframeset\n}", + "frameset should have an onclick handler"); +frameset.setAttribute("onscroll", "frameset"); +is(window.onscroll.toString(), "function onscroll(event) {\nframeset\n}", + "frameset should forward onscroll"); +frameset.setAttribute("onpopstate", "frameset"); +is(window.onpopstate.toString(), "function onpopstate(event) {\nframeset\n}", + "frameset should forward onpopstate"); + + + +</script> +</pre> +</body> +</html> diff --git a/dom/events/test/test_bug698929.html b/dom/events/test/test_bug698929.html new file mode 100644 index 0000000000..d450ec8517 --- /dev/null +++ b/dom/events/test/test_bug698929.html @@ -0,0 +1,47 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=698929 +--> +<head> + <title>Test for Bug 698929</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=698929">Mozilla Bug 698929</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 698929 **/ + + var e = document.createEvent("Event"); + e.initEvent("foo", true, true); + var c = 0; + var b = 0; + document.addEventListener("foo", + function() { + ++c; + }); + document.body.addEventListener("foo", function(event) { + ++b; + event.stopImmediatePropagation(); + }); + document.body.addEventListener("foo", function(event) { + ++b; + }); + document.body.dispatchEvent(e); + document.documentElement.dispatchEvent(e); + is(c, 1, "Listener in the document should have been called once."); + is(b, 1, "Listener in the body should have been called once."); + + + +</script> +</pre> +</body> +</html> diff --git a/dom/events/test/test_bug704423.html b/dom/events/test/test_bug704423.html new file mode 100644 index 0000000000..30b1913d07 --- /dev/null +++ b/dom/events/test/test_bug704423.html @@ -0,0 +1,40 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=704423 +--> +<head> + <title>Test for Bug 704423</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=704423">Mozilla Bug 704423</a> +<p id="display"></p> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 704423 **/ + +function doTest() +{ + function handler(aEvent) { + aEvent.preventDefault(); + ok(aEvent.defaultPrevented, + "mousemove event should be cancelable"); + } + window.addEventListener("mousemove", handler, true); + synthesizeMouseAtCenter(document.body, { type: "mousemove" }); + window.removeEventListener("mousemove", handler, true); + + SimpleTest.finish(); +} + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(doTest); + +</script> +</pre> +</body> +</html> diff --git a/dom/events/test/test_bug741666.html b/dom/events/test/test_bug741666.html new file mode 100644 index 0000000000..f67a3a7065 --- /dev/null +++ b/dom/events/test/test_bug741666.html @@ -0,0 +1,185 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=741666 +--> +<head> + <title>Test for Bug 741666</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script src="/tests/SimpleTest/paint_listener.js"></script> + <script src="/tests/gfx/layers/apz/test/mochitest/apz_test_utils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=741666">Mozilla Bug 741666</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody" type="application/javascript"> + +/** Test for Bug 306008 - Touch events with a reference held should retain their touch lists **/ + +let tests = [], testTarget, parent; + +let touch = { + id: 0, + point: {x: 0, y: 0}, + radius: {x: 0, y: 0}, + rotation: 0, + force: 0.5, + target: null +} + +function nextTest() { + if (tests.length) + SimpleTest.executeSoon(tests.shift()); +} + +function checkEvent(aFakeEvent, aTouches) { + return function(aEvent, aTrusted) { + is(aFakeEvent.ctrlKey, aEvent.ctrlKey, "Correct ctrlKey"); + is(aFakeEvent.altKey, aEvent.altKey, "Correct altKey"); + is(aFakeEvent.shiftKey, aEvent.shiftKey, "Correct shiftKey"); + is(aFakeEvent.metaKey, aEvent.metaKey, "Correct metaKey"); + is(aEvent.isTrusted, aTrusted, "Event is trusted"); + checkTouches(aFakeEvent[aTouches], aEvent[aTouches]); + } +} + +function checkTouches(aTouches1, aTouches2) { + is(aTouches1.length, aTouches2.length, "Correct touches length"); + for (var i = 0; i < aTouches1.length; i++) { + checkTouch(aTouches1[i], aTouches2[i]); + } +} + +function checkTouch(aFakeTouch, aTouch) { + is(aFakeTouch.identifier, aTouch.identifier, "Touch has correct identifier"); + is(aFakeTouch.target, aTouch.target, "Touch has correct target"); + is(aFakeTouch.page.x, aTouch.pageX, "Touch has correct pageX"); + is(aFakeTouch.page.y, aTouch.pageY, "Touch has correct pageY"); + is(aFakeTouch.page.x + Math.round(window.mozInnerScreenX), aTouch.screenX, "Touch has correct screenX"); + is(aFakeTouch.page.y + Math.round(window.mozInnerScreenY), aTouch.screenY, "Touch has correct screenY"); + is(aFakeTouch.page.x, aTouch.clientX, "Touch has correct clientX"); + is(aFakeTouch.page.y, aTouch.clientY, "Touch has correct clientY"); + is(aFakeTouch.radius.x, aTouch.radiusX, "Touch has correct radiusX"); + is(aFakeTouch.radius.y, aTouch.radiusY, "Touch has correct radiusY"); + is(aFakeTouch.rotationAngle, aTouch.rotationAngle, "Touch has correct rotationAngle"); + is(aFakeTouch.force, aTouch.force, "Touch has correct force"); +} + +function sendTouchEvent(windowUtils, aType, aEvent, aModifiers) { + var ids = [], xs=[], ys=[], rxs = [], rys = [], + rotations = [], forces = [], tiltXs = [], tiltYs = [], twists = []; + + for (var touchType of ["touches", "changedTouches", "targetTouches"]) { + for (var i = 0; i < aEvent[touchType].length; i++) { + if (!ids.includes(aEvent[touchType][i].identifier)) { + ids.push(aEvent[touchType][i].identifier); + xs.push(aEvent[touchType][i].page.x); + ys.push(aEvent[touchType][i].page.y); + rxs.push(aEvent[touchType][i].radius.x); + rys.push(aEvent[touchType][i].radius.y); + rotations.push(aEvent[touchType][i].rotationAngle); + forces.push(aEvent[touchType][i].force); + tiltXs.push(0); + tiltYs.push(0); + twists.push(0); + } + } + } + return windowUtils.sendTouchEvent(aType, + ids, xs, ys, rxs, rys, + rotations, forces, tiltXs, tiltYs, twists, + aModifiers, 0); +} + +function touchEvent(aOptions) { + if (!aOptions) { + aOptions = {}; + } + this.ctrlKey = aOptions.ctrlKey || false; + this.altKey = aOptions.altKey || false; + this.shiftKey = aOptions.shiftKey || false; + this.metaKey = aOptions.metaKey || false; + this.touches = aOptions.touches || []; + this.targetTouches = aOptions.targetTouches || []; + this.changedTouches = aOptions.changedTouches || []; +} + +function testtouch(aOptions) { + if (!aOptions) + aOptions = {}; + this.identifier = aOptions.identifier || 0; + this.target = aOptions.target || 0; + this.page = aOptions.page || {x: 0, y: 0}; + this.radius = aOptions.radius || {x: 0, y: 0}; + this.rotationAngle = aOptions.rotationAngle || 0; + this.force = aOptions.force || 1; +} + +function testPreventDefault(name) { + let cwu = SpecialPowers.getDOMWindowUtils(window); + let target = document.getElementById("testTarget"); + let bcr = target.getBoundingClientRect(); + + let touch1 = new testtouch({ + page: {x: Math.round(bcr.left + bcr.width/2), + y: Math.round(bcr.top + bcr.height/2)}, + target + }); + + let event = new touchEvent({ + touches: [touch1], + targetTouches: [touch1], + changedTouches: [touch1] + }); + + // test touchstart event fires correctly + var checkTouchesEvent = checkEvent(event, "touches"); + var checkTargetTouches = checkEvent(event, "targetTouches"); + + /* This is the heart of the test. Verify that the touch event + looks correct both in and outside of a setTimeout */ + window.addEventListener("touchstart", function(firedEvent) { + checkTouchesEvent(firedEvent, true); + setTimeout(function() { + checkTouchesEvent(firedEvent, true); + checkTargetTouches(firedEvent, true); + + event.touches = []; + event.targetTouches = []; + sendTouchEvent(cwu, "touchend", event, 0); + + nextTest(); + }, 0); + }); + sendTouchEvent(cwu, "touchstart", event, 0); +} + +async function doTest() { + await promiseApzFlushedRepaints(); + await waitUntilApzStable(); + + tests.push(testPreventDefault); + + tests.push(function() { + SimpleTest.finish(); + }); + + nextTest(); +} + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(doTest); + +</script> +</pre> +<div id="parent"> + <span id="testTarget" style="margin-left: 200px; padding: 5px; border: 1px solid black;">testTarget</span> +</div> +</body> +</html> diff --git a/dom/events/test/test_bug812744.html b/dom/events/test/test_bug812744.html new file mode 100644 index 0000000000..2cd677b930 --- /dev/null +++ b/dom/events/test/test_bug812744.html @@ -0,0 +1,37 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=812744 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 812744</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=812744">Mozilla Bug 812744</a> +<p id="display"></p> +<div id="content" style="display: none"> +<iframe id="f"></iframe> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 812744 **/ +SimpleTest.waitForExplicitFinish(); + +addLoadEvent(function() { + var f = $("f"); + var el = f.contentDocument.documentElement; + f.onload = function() { + el.setAttribute("onmouseleave", "(void 0)"); + is(el.onmouseleave, null, "Should not have a function here"); + SimpleTest.finish(); + }; + f.src = "http://www.example.com/" +}); +</script> +</pre> +</body> +</html> diff --git a/dom/events/test/test_bug822898.html b/dom/events/test/test_bug822898.html new file mode 100644 index 0000000000..588be7d824 --- /dev/null +++ b/dom/events/test/test_bug822898.html @@ -0,0 +1,343 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=822898 +--> +<head> + <title>Test for Bug 822898</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=822898">Mozilla Bug 822898</a> +<p id="display"></p> +<div id="content" style="display: none"> + <iframe id="subFrame"></iframe> +</div> +<pre id="test"> +<script class="testbody" type="application/javascript"> + +/** Test for Bug 822898 - Pointer* Events **/ + +let tests = [], testTarget, parent, iframeBody, gOnPointerPropHandled; + +function nextTest() { + if (tests.length) + SimpleTest.executeSoon(tests.shift()); +} + +function random() { + return Math.floor(Math.random() * 100); +} + +function createTestEventValue(name) { + + let detail = random(); + let screenX = random(); + let screenY = random(); + let clientX = random(); + let clientY = random(); + let ctrlKey = !!(random() % 2); + let altKey = !!(random() % 2); + let shiftKey = !!(random() % 2); + let metaKey = !!(random() % 2); + let button = random(); + let pointerId = random(); + + return function() { + let event = new PointerEvent("pointerdown", { + bubbles: true, cancelable: true, view: window, + detail, screenX, screenY, clientX, clientY, + ctrlKey, altKey, shiftKey, metaKey, + button, relatedTarget: null, pointerId + }); + + + function check(ev) { + is(ev.detail, detail, "Correct detail"); + is(ev.screenX, screenX, "Correct screenX"); + is(ev.screenY, screenY, "Correct screenY"); + is(ev.clientX, clientX, "Correct clientX"); + is(ev.clientY, clientY, "Correct clientY"); + is(ev.ctrlKey, ctrlKey, "Correct ctrlKey"); + is(ev.altKey, altKey, "Correct altKey"); + is(ev.shiftKey, shiftKey, "Correct shiftKey"); + is(ev.metaKey, metaKey, "Correct metaKey"); + is(ev.button, button, "Correct buttonArg"); + is(ev.pointerId, pointerId, "Correct pointerId"); + } + + for (let target of [document, window, testTarget, parent]) + target.addEventListener(name, check); + + testTarget.dispatchEvent(event); + + for (let target of [document, window, testTarget, parent]) + target.removeEventListener(name, check); + + + nextTest(); + } +} + +function getDefaultArgEvent(eventname) { + return new PointerEvent(eventname, { + bubbles: true, cancelable: true, view: window, + detail: 0, screenX: 0, screenY: 0, clientX: 0, clientY: 0, + ctrlKey: false, altKey: false, shiftKey: false, metaKey: false, + button: 0, relatedTarget: null, pointerId: 0 + }); +} + +function testDefaultArg() { + let event = getDefaultArgEvent("pointerdown"); + + testTarget.addEventListener("pointerdown", function(ev) { + is(ev.pointerId, 0, "Correct default pointerId"); + }, {once: true}); + testTarget.dispatchEvent(event); + + nextTest(); +} + +function testStopPropagation() { + let event = getDefaultArgEvent("pointerdown"); + + let unreachableListener = function () { + ok(false, "Event should have been stopped"); + } + + // Capturing phase + let captured = false; + parent.addEventListener("pointerdown", function() { + captured = true; + }, {capture: true, once: true}); // Capturing phase + + // Bubbling phase + parent.addEventListener("pointerdown", unreachableListener); + + testTarget.addEventListener("pointerdown", function(ev) { + is(captured, true, "Event should have been captured"); + ev.stopPropagation(); + }, {once: true}); + + testTarget.dispatchEvent(event); + + parent.removeEventListener("pointerdown", unreachableListener); + + nextTest(); +} + +function testPreventDefault() { + let event = getDefaultArgEvent("pointerdown"); + + parent.addEventListener("pointerdown", function(ev) { + is(ev.defaultPrevented, true, "preventDefault can be called"); + nextTest(); + }, {once: true}); + + testTarget.addEventListener("pointerdown", function(ev) { + ev.preventDefault(); + }, {once: true}); + + testTarget.dispatchEvent(event); +} + +function testBlockPreventDefault() { + let event = new PointerEvent("pointerdown", { + bubbles: true, cancelable: false, view: window, + detail: 0, screenX: 0, screenY: 0, clientX: 0, clientY: 0, + ctrlKey: false, altKey: false, shiftKey: false, metaKey: false, + button: 0, relatedTarget: null, pointerId: 0, pointerType: "pen" + }); + + parent.addEventListener("pointerdown", function(ev) { + is(ev.defaultPrevented, false, "aCancelableArg works"); + nextTest(); + }, {once: true}); + + testTarget.addEventListener("pointerdown", function(ev) { + ev.preventDefault(); + }, {once: true}); + + testTarget.dispatchEvent(event); +} + +function testBlockBubbling() { + let unreachableListener = function () { + ok(false, "aCanBubble doesn't work"); + } + + let event = new PointerEvent("pointerdown", { + bubbles: false, cancelable: true, view: window, + detail: 0, screenX: 0, screenY: 0, clientX: 0, clientY: 0, + ctrlKey: false, altKey: false, shiftKey: false, metaKey: false, + button: 0, relatedTarget: null, pointerId: 0 + }); + + parent.addEventListener("pointerdown", unreachableListener); + testTarget.dispatchEvent(event); + parent.removeEventListener("pointerdown", unreachableListener); + + nextTest(); +} + +function testOnPointerProperty() +{ + iframeBody.onpointerdown = function (e) { gOnPointerPropHandled.pointerdown = true; } + iframeBody.onpointerup = function (e) { gOnPointerPropHandled.pointerup = true; } + iframeBody.onpointermove = function (e) { gOnPointerPropHandled.pointermove = true; } + iframeBody.onpointerout = function (e) { gOnPointerPropHandled.pointerout = true; } + iframeBody.onpointerover = function (e) { gOnPointerPropHandled.pointerover = true; } + iframeBody.onpointerenter = function (e) { gOnPointerPropHandled.pointerenter = true; } + iframeBody.onpointerleave = function (e) { gOnPointerPropHandled.pointerleave = true; } + iframeBody.onpointercancel = function (e) { gOnPointerPropHandled.pointercancel = true; } + + iframeBody.dispatchEvent(getDefaultArgEvent("pointerdown")); + is(gOnPointerPropHandled.pointerdown, true, "pointerdown property is performed"); + + iframeBody.dispatchEvent(getDefaultArgEvent("pointerup")); + is(gOnPointerPropHandled.pointerup, true, "pointerup property is performed"); + + iframeBody.dispatchEvent(getDefaultArgEvent("pointermove")); + is(gOnPointerPropHandled.pointermove, true, "pointermove property is performed"); + + iframeBody.dispatchEvent(getDefaultArgEvent("pointerout")); + is(gOnPointerPropHandled.pointerout, true, "pointerout property is performed"); + + iframeBody.dispatchEvent(getDefaultArgEvent("pointerover")); + is(gOnPointerPropHandled.pointerover, true, "pointerover property is performed"); + + iframeBody.dispatchEvent(getDefaultArgEvent("pointerenter")); + is(gOnPointerPropHandled.pointerenter, true, "pointerenter property is performed"); + + iframeBody.dispatchEvent(getDefaultArgEvent("pointerleave")); + is(gOnPointerPropHandled.pointerleave, true, "pointerleave property is performed"); + + iframeBody.dispatchEvent(getDefaultArgEvent("pointercancel")); + is(gOnPointerPropHandled.pointercancel, true, "pointercancel property is performed"); + + nextTest(); +} + +function testPointerEventCTORS() +{ + // TODO: This should go to test_eventctors.html, when PointerEvents enabled by default + var receivedEvent; + iframeBody.addEventListener("hello", function(e) { receivedEvent = e; }, true); + + var e; + var ex = false; + + try { + e = new PointerEvent(); + } catch(exp) { + ex = true; + } + ok(ex, "PointerEvent: First parameter is required!"); + ex = false; + + e = new PointerEvent("hello"); + is(e.type, "hello", "PointerEvent: Wrong event type!"); + ok(!e.isTrusted, "PointerEvent: Event shouldn't be trusted!"); + ok(!e.bubbles, "PointerEvent: Event shouldn't bubble!"); + ok(!e.cancelable, "PointerEvent: Event shouldn't be cancelable!"); + iframeBody.dispatchEvent(e); + is(receivedEvent, e, "PointerEvent: Wrong event!"); + + var PointerEventProps = + [ { screenX: 0 }, + { screenY: 0 }, + { clientX: 0 }, + { clientY: 0 }, + { ctrlKey: false }, + { shiftKey: false }, + { altKey: false }, + { metaKey: false }, + { button: 0 }, + { buttons: 0 }, + { relatedTarget: null }, + { pointerId: 0 }, + { pointerType: "" } + ]; + + var testPointerProps = + [ + { screenX: 1 }, + { screenY: 2 }, + { clientX: 3 }, + { clientY: 4 }, + { ctrlKey: true }, + { shiftKey: true }, + { altKey: true }, + { metaKey: true }, + { button: 5 }, + { buttons: 6 }, + { relatedTarget: window }, + { pointerId: 5 }, + { pointerType: "mouse" } + ]; + + var defaultPointerEventValues = {}; + for (var i = 0; i < PointerEventProps.length; ++i) { + for (prop in PointerEventProps[i]) { + ok(prop in e, "PointerEvent: PointerEvent doesn't have property " + prop + "!"); + defaultPointerEventValues[prop] = PointerEventProps[i][prop]; + } + } + + while (testPointerProps.length) { + var p = testPointerProps.shift(); + e = new PointerEvent("foo", p); + for (var def in defaultPointerEventValues) { + if (!(def in p)) { + is(e[def], defaultPointerEventValues[def], + "PointerEvent: Wrong default value for " + def + "!"); + } else { + is(e[def], p[def], "PointerEvent: Wrong event init value for " + def + "!"); + } + } + } + nextTest(); +} + +function runTests() { + testTarget = document.getElementById("testTarget"); + parent = testTarget.parentNode; + gOnPointerPropHandled = new Array; + iframeBody = document.getElementById("subFrame").contentWindow.document.body; + + tests.push(createTestEventValue("pointerdown")); + tests.push(createTestEventValue("pointermove")); + tests.push(createTestEventValue("pointerup")); + + tests.push(testDefaultArg); + tests.push(testStopPropagation); + + tests.push(testPreventDefault); + tests.push(testBlockPreventDefault); + + tests.push(testBlockBubbling); + tests.push(testOnPointerProperty()); + tests.push(testPointerEventCTORS()); + + tests.push(function() { + SimpleTest.finish(); + }); + + nextTest(); +} + +window.onload = function() { + SimpleTest.executeSoon(runTests); +} + +SimpleTest.waitForExplicitFinish(); + +</script> +</pre> +<div id="parent"> + <span id="testTarget" style="border: 1px solid black;">testTarget</span> +</div> +</body> +</html> diff --git a/dom/events/test/test_bug855741.html b/dom/events/test/test_bug855741.html new file mode 100644 index 0000000000..227d4d0b6d --- /dev/null +++ b/dom/events/test/test_bug855741.html @@ -0,0 +1,90 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=855741 +--> +<head> + <title>Test for Bug 855741</title> + <script type="text/javascript" src="/MochiKit/MochiKit.js"></script> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<input type="text" id="testTarget" value="focus"> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +/** Test for Bug 855741 **/ +function testFocusEvent(event) { + ok(('relatedTarget' in event), 'FocusEvent.relatedTarget exists'); + + if (event.construct_test == true) { + ok(event.relatedTarget == $("content"), 'FocusEvent.relatedTarget is ' + $("content").id); + } +} + +function testUIEvent(event) { + ok((event.detail == 0), + 'UIEvent.detail should be 0 in ' + event.target.value + ' event'); + + ok((event.defaultView == null), + 'UIEvent.defaultView should be null in ' + event.target.value + ' event'); +} + +function testEventType(event, type) { + ok((event.type == type), 'Event.type match: ' + type); +} + +function eventhandle(event) { + testFocusEvent(event); + testUIEvent(event); + testEventType(event, event.target.value); + + if (event.target.value == 'blur') { + event.target.value = 'focus'; + } else { + event.target.value = 'blur'; + } +} + +// +// event handler: +// +$("testTarget").addEventListener("focus", eventhandle, true); +$("testTarget").addEventListener("blur", eventhandle, true); + +// +// FocusEvent structure test +// +$("testTarget").focus(); +$("testTarget").blur(); + +// +// Focus/Blur constructor test +// +var focus_event = new FocusEvent("focus", + {bubbles: true, + cancelable: true, + relatedTarget: $("content")}); +focus_event.construct_test = true; + +var blur_event = new FocusEvent("blur", + {bubbles: true, + cancelable: true, + relatedTarget: $("content")}); +blur_event.construct_test = true; + +// create cycle referece for leak test +$("content").foo_focus = focus_event; +$("content").foo_blur = blur_event; + +$("testTarget").dispatchEvent(focus_event); +$("testTarget").dispatchEvent(blur_event); + +</script> +</pre> +</body> +</html> diff --git a/dom/events/test/test_bug864040.html b/dom/events/test/test_bug864040.html new file mode 100644 index 0000000000..c4f5e8cf98 --- /dev/null +++ b/dom/events/test/test_bug864040.html @@ -0,0 +1,87 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=864040 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 864040</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script src="/tests/SimpleTest/WindowSnapshot.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=864040">Mozilla Bug 864040</a> +<div id="display"> + <textarea id="ta" rows="5" cols="20" style="-moz-appearance:none"></textarea> + <div id="ce" contentEditable="true" style="height: 5em;"></div> +</div> +<div id="content" style="display: none"> +</div> +<pre id="test"> + <script type="application/javascript"> + /** + * Test for Bug 864040 + * + * We use a selection event to set the selection to the end of an editor + * containing an ending newline. Then we test to see that the caret is + * actually drawn on the newline. + */ + + async function testSelectEndOfText(elem) { + var tn = elem.tagName; + elem.focus(); + + // Enter test string into editor + var test_string = 'test\n'; + sendString(test_string); + + // Get the caret position after what we entered + var result = synthesizeQuerySelectedText(); + ok(result, tn + ': failed to query selection (1)'); + var refoffset = result.offset; + + // Take a snapshot of where the caret is (on the new line) + referenceSnapshot = await snapshotWindow(window, true /* withCaret */); + ok(referenceSnapshot, tn + ': failed to take snapshot (1)'); + + // Set selection to the same spot through a selection event + ok(await synthesizeSelectionSet(refoffset, 0, false), tn + ': failed to set selection'); + + // Make sure new selection is the same + result = synthesizeQuerySelectedText(); + ok(result, tn + ': failed to query selection (2)'); + is(result.offset, refoffset, tn + ': caret is not at the right position'); + + // Take a snapshot of where the new caret is (shoud still be on the new line) + testSnapshot = await snapshotWindow(window, true /* withCaret */); + ok(testSnapshot, tn + ': failed to take snapshot (2)'); + + // Compare snapshot (should be the same) + result = compareSnapshots(referenceSnapshot, testSnapshot, true /* expected */) + ok(result, tn + ': failed to compare snapshots'); + // result = [correct, s1data, s2data] + ok(result[0], tn + ': caret is not on new line'); + if (!result[0]) { + dump('Ref: ' + result[1] + '\n'); + dump('Res: ' + result[2] + '\n'); + } + } + + async function runTests() { + // we don't test regular <input> because this test is about multiline support + // test textarea + await testSelectEndOfText(document.getElementById('ta')); + // test contentEditable + await testSelectEndOfText(document.getElementById('ce')); + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + + SimpleTest.waitForFocus(runTests); + </script> +</pre> +</body> +</html> diff --git a/dom/events/test/test_bug924087.html b/dom/events/test/test_bug924087.html new file mode 100644 index 0000000000..58eecc99c4 --- /dev/null +++ b/dom/events/test/test_bug924087.html @@ -0,0 +1,45 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=924087 +--> +<head> + <title>Test for Bug 924087</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<div contenteditable><a id="editable" href="#">editable link</a></div> +<a id="noneditable" href="#">non-editable link</a> +<input> +<textarea></textarea> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 924087 **/ +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + var editable = document.querySelector("#editable"); + var noneditable = document.querySelector("#noneditable"); + var input = document.querySelector("input"); + var textarea = document.querySelector("textarea"); + synthesizeMouseAtCenter(noneditable, {type:"mousedown"}); + is(document.querySelector(":active:link"), noneditable, "Normal links should become :active"); + synthesizeMouseAtCenter(noneditable, {type:"mouseup"}); + synthesizeMouseAtCenter(editable, {type:"mousedown"}); + is(document.querySelector(":active:link"), null, "Editable links should not become :active"); + synthesizeMouseAtCenter(editable, {type:"mouseup"}); + [input, textarea].forEach(textbox => { + synthesizeMouseAtCenter(textbox, {type:"mousedown"}); + is(document.querySelector(textbox.localName + ":active"), textbox, "The textbox should become :active"); + synthesizeMouseAtCenter(textbox, {type:"mouseup"}); + }); + SimpleTest.finish(); +}); + +</script> +</pre> + +</body> +</html> diff --git a/dom/events/test/test_bug930374-chrome.html b/dom/events/test/test_bug930374-chrome.html new file mode 100644 index 0000000000..576844c23f --- /dev/null +++ b/dom/events/test/test_bug930374-chrome.html @@ -0,0 +1,57 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=930374 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 930374</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=930374">Mozilla Bug 930374</a> +<div id="display"> + <input id="input-text"> +</div> +<div id="content" style="display: none"> +</div> +<pre id="test"> + <script type="application/javascript"> + SimpleTest.waitForExplicitFinish(); + + var gKeyPress = null; + function onKeyPress(aEvent) + { + gKeyPress = aEvent; + is(aEvent.target, document.getElementById("input-text"), "input element should have focus"); + ok(!aEvent.defaultPrevented, "keypress event should be consumed before keypress event handler"); + } + + function runTests() + { + document.addEventListener("keypress", onKeyPress); + var input = document.getElementById("input-text"); + input.focus(); + + input.addEventListener("input", function (aEvent) { + ok(gKeyPress, + "Test1: keypress event must be fired before an input event"); + ok(gKeyPress.defaultPrevented, + "Test1: keypress event's defaultPrevented should be true in chrome even if it's consumed by default action handler of editor"); + setTimeout(function () { + ok(gKeyPress.defaultPrevented, + "Test2: keypress event's defaultPrevented should be true after event dispatching finished"); + SimpleTest.finish(); + }, 0); + }, {once: true}); + + sendChar("a"); + } + + SimpleTest.waitForFocus(runTests); + </script> +</pre> +</body> +</html> diff --git a/dom/events/test/test_bug930374-content.html b/dom/events/test/test_bug930374-content.html new file mode 100644 index 0000000000..bcf2eadfb7 --- /dev/null +++ b/dom/events/test/test_bug930374-content.html @@ -0,0 +1,70 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=930374 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 930374</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=930374">Mozilla Bug 930374</a> +<div id="display"> + <input id="input-text"> +</div> +<div id="content" style="display: none"> +</div> +<pre id="test"> + <script type="application/javascript"> + SimpleTest.waitForExplicitFinish(); + + var gKeyPress = null; + function onKeyPress(aEvent) + { + gKeyPress = aEvent; + is(aEvent.target, document.getElementById("input-text"), "input element should have focus"); + ok(!aEvent.defaultPrevented, "keypress event should be consumed before keypress event handler"); + } + + function runTests() + { + document.addEventListener("keypress", onKeyPress); + var input = document.getElementById("input-text"); + input.focus(); + + input.addEventListener("input", function (aEvent) { + ok(gKeyPress, + "Test1: keypress event must be fired before an input event"); + ok(!gKeyPress.defaultPrevented, + "Test1: keypress event's defaultPrevented should be false even though it's consumed by the default action handler of editor"); + gKeyPress.preventDefault(); + ok(gKeyPress.defaultPrevented, + "Test1: keypress event's defaultPrevented should become true because of a call of preventDefault()"); + }, {once: true}); + + sendChar("a"); + gKeyPress = null; + + input.addEventListener("input", function (aEvent) { + ok(gKeyPress, + "Test2: keypress event must be fired before an input event"); + ok(!gKeyPress.defaultPrevented, + "Test2: keypress event's defaultPrevented should be false even though it's consumed by the default action handler of editor"); + setTimeout(function () { + ok(!gKeyPress.defaultPrevented, + "Test2: keypress event's defaultPrevented should not become true after event dispatching finished"); + SimpleTest.finish(); + }, 0); + }, {once: true}); + + sendChar("b"); + } + + SimpleTest.waitForFocus(runTests); + </script> +</pre> +</body> +</html> diff --git a/dom/events/test/test_bug944011.html b/dom/events/test/test_bug944011.html new file mode 100644 index 0000000000..a8a0720989 --- /dev/null +++ b/dom/events/test/test_bug944011.html @@ -0,0 +1,52 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=944011 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 944011</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript"> + + /** Test for Bug 944011 comment 24 - Event handlers should fire even if the + target comes from a non-current inner. **/ + SimpleTest.waitForExplicitFinish(); + var gLoadCount = 0; + function loaded() { + ++gLoadCount; + switch(gLoadCount) { + case 1: + ok(true, "Got first load"); + oldBody = window[0].document.body; + oldBody.onclick = function() { + ok(true, "Got onclick"); + SimpleTest.finish(); + } + $('ifr').setAttribute('srcdoc', '<html><body>Second frame</body></html>'); + break; + case 2: + ok(true, "Got second load"); + oldBody.dispatchEvent(new MouseEvent('click')); + break; + default: + ok(false, "Unexpected load"); + SimpleTest.finish(); + } + } + + + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=944011">Mozilla Bug 944011</a> +<p id="display"></p> +<div id="content" style="display: none"> + <iframe id="ifr" onload="loaded();" srcdoc="<html><body>foo</body></html>"></iframe> + <div name="testTarget"></div> +</div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/dom/events/test/test_bug944847.html b/dom/events/test/test_bug944847.html new file mode 100644 index 0000000000..199c561d26 --- /dev/null +++ b/dom/events/test/test_bug944847.html @@ -0,0 +1,42 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=944847 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 944847</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript"> + + /** Test for Bug 944847 **/ + + var e1 = document.createElement("div"); + is(e1.onclick, null); + e1.setAttribute("onclick", ""); + isnot(e1.onclick, null); + + var e2 = document.implementation.createHTMLDocument(null, null).createElement("div"); + is(e2.onclick, null); + e2.setAttribute("onclick", ""); + is(e2.onclick, null); + + var e3 = document.createElement("div"); + is(e3.onclick, null); + e3.setAttribute("onclick", ""); + e2.ownerDocument.adoptNode(e3); + is(e3.onclick, null); + + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=944847">Mozilla Bug 944847</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/dom/events/test/test_bug946632.html b/dom/events/test/test_bug946632.html new file mode 100644 index 0000000000..5ac44141c3 --- /dev/null +++ b/dom/events/test/test_bug946632.html @@ -0,0 +1,156 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=946632 +--> +<head> + <title>Test for bug 946632 - propagate mouse-wheel vertical scroll events to container</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script src="/tests/SimpleTest/paint_listener.js"></script> + <script type="application/javascript" src="/tests/gfx/layers/apz/test/mochitest/apz_test_utils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <style> + .scrollable { + overflow: scroll; + height: 200px; + width: 200px; + } + input { + font-size: 72px; + height: 20px; + width: 20px; + } + </style> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=946632">Mozilla Bug 946632</a> +<p id="display"></p> +<div id="container" class="scrollable"> + <input value="value"> + x<br>x<br>x<br>x<br>x<br>x<br>x<br>x<br>x<br>x<br>x<br>x<br>x<br>x<br>x<br>x<br>x<br>x<br> + xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +</div> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + SpecialPowers.pushPrefEnv({ + "set":[["general.smoothScroll", false], + ["mousewheel.system_scroll_override.enabled", false], + ["test.events.async.enabled", true]] + }, runTests) + }, window); + +var input = document.querySelector("input"); +var container = document.querySelector("#container"); + +function reset() +{ + container.scrollTop = 0; + container.scrollLeft = 0; + input.scrollTop = 0; + input.scrollLeft = 0; + container.style.display='none'; + container.getBoundingClientRect(); +} + +function prepare(check) +{ + return new Promise(resolve => { + container.style.display=''; + container.getBoundingClientRect(); + scrollHandler = function(event) { + window.removeEventListener("scroll", arguments.callee, true); + event.stopPropagation(); + check(event) + resolve(); + }; + window.addEventListener("scroll", scrollHandler, true); + }); +} + +var tests = [ + { + check(event) { + is(event.target, container, "<input> vertical line scroll targets container"); + ok(container.scrollTop > 0, "<input> vertical line scroll container.scrollTop"); + is(container.scrollLeft, 0, "<input> vertical line scroll container.scrollLeft"); + is(input.scrollTop, 0, "<input> horizontal line scroll input.scrollTop"); + is(input.scrollLeft, 0, "<input> horizontal line scroll input.scrollLeft"); + }, + event: { + deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaY: 1.0, + lineOrPageDeltaY: 1, + } + }, + { + check(event) { + is(event.target, input, "<input> horizontal line scroll targets <input>"); + is(input.scrollTop, 0, "<input> horizontal line scroll input.scrollTop"); + ok(input.scrollLeft > 0, "<input> horizontal line scroll input.scrollLeft"); + is(container.scrollTop, 0, "<input> horizontal line scroll container.scrollTop"); + is(container.scrollLeft, 0, "<input> horizontal line scroll container.scrollLeft"); + }, + event: { + deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: 1.0, + lineOrPageDeltaX: 1 + } + }, + { + check(event) { + is(event.target, container, "<input> vertical page scroll targets container"); + ok(container.scrollTop > 0, "<input> vertical line scroll container.scrollTop"); + is(container.scrollLeft, 0, "<input> vertical line scroll container.scrollLeft"); + is(input.scrollTop, 0, "<input> vertical page scroll input.scrollTop"); + is(input.scrollLeft, 0, "<input> vertical page scroll input.scrollLeft"); + }, + event: { + deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaY: 1.0, + lineOrPageDeltaY: 1 + } + }, + { + check(event) { + is(event.target, input, "<input> horizontal page scroll targets <input>"); + is(input.scrollTop, 0, "<input> horizontal page scroll input.scrollTop"); + ok(input.scrollLeft > 0, "<input> horizontal page scroll input.scrollLeft"); + is(container.scrollTop, 0, "<input> horizontal page scroll container.scrollTop"); + is(container.scrollLeft, 0, "<input> horizontal page scroll container.scrollLeft"); + }, + event: { + deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: 1.0, + lineOrPageDeltaX: 1 + } + }, +]; + +async function runTests() +{ + for (var i = 0; i < tests.length; i++) { + var test = tests[i]; + reset(); + await promiseApzFlushedRepaints(); + let testEndPromise = prepare(test.check); + await new Promise(resolve => { + sendWheelAndPaint(input, 8, 6, test.event, resolve); + }); + await testEndPromise; + } + + SpecialPowers.DOMWindowUtils.restoreNormalRefresh(); + SimpleTest.finish(); +} + +</script> +</pre> +</body> +</html> diff --git a/dom/events/test/test_bug967796.html b/dom/events/test/test_bug967796.html new file mode 100644 index 0000000000..982b9e0980 --- /dev/null +++ b/dom/events/test/test_bug967796.html @@ -0,0 +1,236 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=967796 +--> +<head> + <title>Test for Bug 967796</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=967796">Mozilla Bug 967796</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 967796 **/ +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(runTests); +var outer; +var middle; +var inner; +var outside; +var container; +var file; +var iframe; +var checkRelatedTarget = false; +var expectedRelatedEnter = null; +var expectedRelatedLeave = null; +var pointerentercount = 0; +var pointerleavecount = 0; +var pointerovercount = 0; +var pointeroutcount = 0; + +function sendMouseEventToElement(t, elem) { + var r = elem.getBoundingClientRect(); + synthesizeMouse(elem, r.width / 2, r.height / 2, {type: t}); +} + +var expectedPointerEnterTargets = []; +var expectedPointerLeaveTargets = []; + +function runTests() { + outer = document.getElementById("outertest"); + middle = document.getElementById("middletest"); + inner = document.getElementById("innertest"); + outside = document.getElementById("outside"); + container = document.getElementById("container"); + file = document.getElementById("file"); + iframe = document.getElementById("iframe"); + iframe.addEventListener("pointerenter", penter); + iframe.addEventListener("pointerleave", pleave); + iframe.addEventListener("pointerout", pout); + iframe.addEventListener("pointerover", pover); + + // Make sure ESM thinks pointer is outside the test elements. + sendMouseEventToElement("mousemove", outside); + + pointerentercount = 0; + pointerleavecount = 0; + pointerovercount = 0; + pointeroutcount = 0; + checkRelatedTarget = true; + expectedRelatedEnter = outside; + expectedRelatedLeave = inner; + expectedPointerEnterTargets = ["outertest", "middletest", "innertest"]; + sendMouseEventToElement("mousemove", inner); + is(pointerentercount, 3, "Unexpected pointerenter event count!"); + is(pointerovercount, 1, "Unexpected pointerover event count!"); + is(pointeroutcount, 0, "Unexpected pointerout event count!"); + is(pointerleavecount, 0, "Unexpected pointerleave event count!"); + expectedRelatedEnter = inner; + expectedRelatedLeave = outside; + expectedPointerLeaveTargets = ["innertest", "middletest", "outertest"]; + sendMouseEventToElement("mousemove", outside); + is(pointerentercount, 3, "Unexpected pointerenter event count!"); + is(pointerovercount, 1, "Unexpected pointerover event count!"); + is(pointeroutcount, 1, "Unexpected pointerout event count!"); + is(pointerleavecount, 3, "Unexpected pointerleave event count!"); + + // Event handling over native anonymous content. + var r = file.getBoundingClientRect(); + expectedRelatedEnter = outside; + expectedRelatedLeave = file; + synthesizeMouse(file, r.width / 6, r.height / 2, {type: "mousemove"}); + is(pointerentercount, 4, "Unexpected pointerenter event count!"); + is(pointerovercount, 2, "Unexpected pointerover event count!"); + is(pointeroutcount, 1, "Unexpected pointerout event count!"); + is(pointerleavecount, 3, "Unexpected pointerleave event count!"); + + // Moving pointer over type="file" shouldn't cause pointerover/out/enter/leave events + synthesizeMouse(file, r.width - (r.width / 6), r.height / 2, {type: "mousemove"}); + is(pointerentercount, 4, "Unexpected pointerenter event count!"); + is(pointerovercount, 2, "Unexpected pointerover event count!"); + is(pointeroutcount, 1, "Unexpected pointerout event count!"); + is(pointerleavecount, 3, "Unexpected pointerleave event count!"); + + expectedRelatedEnter = file; + expectedRelatedLeave = outside; + sendMouseEventToElement("mousemove", outside); + is(pointerentercount, 4, "Unexpected pointerenter event count!"); + is(pointerovercount, 2, "Unexpected pointerover event count!"); + is(pointeroutcount, 2, "Unexpected pointerout event count!"); + is(pointerleavecount, 4, "Unexpected pointerleave event count!"); + + // Initialize iframe + iframe.contentDocument.documentElement.style.overflow = "hidden"; + iframe.contentDocument.body.style.margin = "0px"; + iframe.contentDocument.body.style.width = "100%"; + iframe.contentDocument.body.style.height = "100%"; + iframe.contentDocument.body.innerHTML = + "<div style='width: 100%; height: 50%; border: 1px solid black;'></div>" + + "<div style='width: 100%; height: 50%; border: 1px solid black;'></div>"; + iframe.contentDocument.body.offsetLeft; // flush + + iframe.contentDocument.body.firstChild.onpointerenter = penter; + iframe.contentDocument.body.firstChild.onpointerleave = pleave; + iframe.contentDocument.body.lastChild.onpointerenter = penter; + iframe.contentDocument.body.lastChild.onpointerleave = pleave; + r = iframe.getBoundingClientRect(); + expectedRelatedEnter = outside; + expectedRelatedLeave = iframe; + // Move pointer inside the iframe. + synthesizeMouse(iframe.contentDocument.body, r.width / 2, r.height / 4, {type: "mousemove"}, + iframe.contentWindow); + is(pointerentercount, 6, "Unexpected pointerenter event count!"); + is(pointerleavecount, 4, "Unexpected pointerleave event count!"); + synthesizeMouse(iframe.contentDocument.body, r.width / 2, r.height - (r.height / 4), {type: "mousemove"}, + iframe.contentWindow); + is(pointerentercount, 7, "Unexpected pointerenter event count!"); + is(pointerleavecount, 5, "Unexpected pointerleave event count!"); + expectedRelatedEnter = iframe; + expectedRelatedLeave = outside; + sendMouseEventToElement("mousemove", outside); + is(pointerentercount, 7, "Unexpected pointerenter event count!"); + is(pointerleavecount, 7, "Unexpected pointerleave event count!"); + + // pointerdown must produce pointerenter event + expectedRelatedEnter = outside; + expectedRelatedLeave = iframe; + // Move pointer inside the iframe. + synthesizeMouse(iframe.contentDocument.body, r.width / 2, r.height / 4, {type: "mousedown"}, + iframe.contentWindow); + synthesizeMouse(iframe.contentDocument.body, r.width / 2, r.height - (r.height / 4), {type: "mousedown"}, + iframe.contentWindow); + is(pointerentercount, 10, "Unexpected pointerenter event count!"); + + // pointerdown + pointermove must produce single pointerenter event + expectedRelatedEnter = outside; + expectedRelatedLeave = iframe; + synthesizeMouse(iframe.contentDocument.body, r.width / 2, r.height / 4, {type: "mousedown"}, + iframe.contentWindow); + synthesizeMouse(iframe.contentDocument.body, r.width / 2 + 1, r.height / 4 + 1, {type: "mousemove"}, + iframe.contentWindow); + is(pointerentercount, 11, "Unexpected pointerenter event count!"); + + Array.from(document.querySelectorAll('*')) + .concat([iframe.contentDocument.body.firstChild, iframe.contentDocument.body.lastChild]) + .forEach((elt) => { + elt.onpointerenter = null; + elt.onpointerleave = null; + elt.onpointerenter = null; + elt.onpointerleave = null; + }); + SimpleTest.finish(); +} + +function penter(evt) { + ++pointerentercount; + evt.stopPropagation(); + if (expectedPointerEnterTargets.length) { + var t = expectedPointerEnterTargets.shift(); + is(evt.target.id, t, "Wrong event target!"); + } + is(evt.bubbles, false, evt.type + " should not bubble!"); + is(evt.cancelable, false, evt.type + " is cancelable!"); + is(evt.target, evt.currentTarget, "Wrong event target!"); + ok(!evt.relatedTarget || evt.target.ownerDocument == evt.relatedTarget.ownerDocument, + "Leaking nodes to another document?"); + if (checkRelatedTarget && evt.target.ownerDocument == document) { + is(evt.relatedTarget, expectedRelatedEnter, "Wrong related target (pointerenter)"); + } +} + +function pleave(evt) { + ++pointerleavecount; + evt.stopPropagation(); + if (expectedPointerLeaveTargets.length) { + var t = expectedPointerLeaveTargets.shift(); + is(evt.target.id, t, "Wrong event target!"); + } + is(evt.bubbles, false, evt.type + " should not bubble!"); + is(evt.cancelable, false, evt.type + " is cancelable!"); + is(evt.target, evt.currentTarget, "Wrong event target!"); + ok(!evt.relatedTarget || evt.target.ownerDocument == evt.relatedTarget.ownerDocument, + "Leaking nodes to another document?"); + if (checkRelatedTarget && evt.target.ownerDocument == document) { + is(evt.relatedTarget, expectedRelatedLeave, "Wrong related target (pointerleave)"); + } +} + +function pover(evt) { + ++pointerovercount; + evt.stopPropagation(); +} + +function pout(evt) { + ++pointeroutcount; + evt.stopPropagation(); +} + +</script> +</pre> +<div id="container" onpointerenter="penter(event)" onpointerleave="pleave(event)" + onpointerout="pout(event)" onpointerover="pover(event)"> + <div id="outside" onpointerout="event.stopPropagation()" onpointerover="event.stopPropagation()">foo</div> + <div id="outertest" onpointerenter="penter(event)" onpointerleave="pleave(event)" + onpointerout="pout(event)" onpointerover="pover(event)"> + <div id="middletest" onpointerenter="penter(event)" onpointerleave="pleave(event)" + onpointerout="pout(event)" onpointerover="pover(event)"> + <div id="innertest" onpointerenter="penter(event)" onpointerleave="pleave(event)" + onpointerout="pout(event)" onpointerover="pover(event)">foo</div> + </div> + </div> + <input type="file" id="file" + onpointerenter="penter(event)" onpointerleave="pleave(event)" + onpointerout="pout(event)" onpointerover="pover(event)"> + <br> + <iframe id="iframe" width="50" height="50"></iframe> +</div> +</body> +</html> diff --git a/dom/events/test/test_bug985988.html b/dom/events/test/test_bug985988.html new file mode 100644 index 0000000000..51d02d828f --- /dev/null +++ b/dom/events/test/test_bug985988.html @@ -0,0 +1,89 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=985988 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 985988</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript"> + + /** Test for Bug 985988 **/ + + function handler() { + return false; + } + + function reversedHandler() { + return true; + } + + function test() { + var t = document.getElementById("testtarget"); + + t.onclick = handler; + var e = new MouseEvent("click", {cancelable: true}); + t.dispatchEvent(e); + ok(e.defaultPrevented, "Should have prevented default handling."); + + t.onclick = reversedHandler; + e = new MouseEvent("click", {cancelable: true}); + t.dispatchEvent(e); + ok(!e.defaultPrevented, "Shouldn't have prevented default handling."); + + t.onmouseover = handler; + e = new MouseEvent("mouseover", {cancelable: true}); + t.dispatchEvent(e); + ok(e.defaultPrevented, "Should have prevented default handling."); + + t.onmouseover = reversedHandler; + e = new MouseEvent("mouseover", {cancelable: true}); + t.dispatchEvent(e); + ok(!e.defaultPrevented, "Shouldn't have prevented default handling."); + + // error does not have reversed meaning for handler return value on + // non-globals. + t.onerror = handler; + e = new ErrorEvent("error", {cancelable: true}); + t.dispatchEvent(e); + ok(e.defaultPrevented, "Should have prevented default handling."); + + t.onerror = reversedHandler; + e = new ErrorEvent("error", {cancelable: true}); + t.dispatchEvent(e); + ok(!e.defaultPrevented, "Shouldn't have prevented default handling."); + + // error has reversed meaning for handler return value on globals. + t = document.getElementById("testtarget2").contentWindow; + t.onerror = reversedHandler; + e = new ErrorEvent("error", {cancelable: true}); + t.dispatchEvent(e); + ok(e.defaultPrevented, "Should have prevented default handling."); + + t.onerror = handler; + e = new ErrorEvent("error", {cancelable: true}); + t.dispatchEvent(e); + ok(!e.defaultPrevented, "Shouldn't have prevented default handling."); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addLoadEvent(test); + + </script> +</head> +<body onload="test()"> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=985988">Mozilla Bug 985988</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<a href="#" id="testtarget">test target</a> +<iframe id="testtarget2"></iframe> +<pre id="test"> +</pre> +</body> +</html> diff --git a/dom/events/test/test_bug998809.html b/dom/events/test/test_bug998809.html new file mode 100644 index 0000000000..0fc50ec547 --- /dev/null +++ b/dom/events/test/test_bug998809.html @@ -0,0 +1,35 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=998809 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 998809</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript"> + + /** Test for Bug 998809 **/ + var event1 = document.createEvent("Event"); + event1.initEvent("a", false, false); + event1.initEvent("b", false, false); + is(event1.type, "b"); + var event2 = document.createEvent("Event"); + event2.initEvent("a", false, false); + is(event2.type, "a"); + event2.initEvent("b", false, false); + is(event2.type, "b"); + + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=998809">Mozilla Bug 998809</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/dom/events/test/test_click_on_reframed_generated_text.html b/dom/events/test/test_click_on_reframed_generated_text.html new file mode 100644 index 0000000000..e8c8b092d6 --- /dev/null +++ b/dom/events/test/test_click_on_reframed_generated_text.html @@ -0,0 +1,32 @@ +<!doctype html> +<title>Test for bug 1497524: Unbound generated content in the active chain</title> +<script src="/tests/SimpleTest/EventUtils.js"></script> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<style> +#target::before { + content: "X"; + color: green; +} +</style> +Should get a click event when clicking on the X below. +<div id="target"></div> +<script> +SimpleTest.waitForExplicitFinish(); +let target = document.getElementById("target"); + +target.addEventListener("mousedown", () => target.style.display = "inline"); +target.addEventListener("mouseup", () => target.style.display = "block"); +target.addEventListener("click", () => { + ok(true, "Got click event"); + SimpleTest.finish(); +}); + +onload = function() { + requestAnimationFrame(() => { + synthesizeMouseAtCenter(target, { type: "mousedown" }) + requestAnimationFrame(() => { + synthesizeMouseAtCenter(target, { type: "mouseup" }) + }); + }); +} +</script> diff --git a/dom/events/test/test_click_on_restyled_element.html b/dom/events/test/test_click_on_restyled_element.html new file mode 100644 index 0000000000..a79789ce74 --- /dev/null +++ b/dom/events/test/test_click_on_restyled_element.html @@ -0,0 +1,51 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for clicking on an element which is restyled/reframed by mousedown event</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <style> + .before-pseudo-element *:active::before { + content: ""; + display: block; + height: 2px; + position: absolute; + top: -2px; + left: 0; + width: 100%; + } + .position-relative *:active { + position: relative; + top: 1px; + } + </style> +</head> +<body> +<section class="before-pseudo-element"><a href="about:blank">link</a></section><!-- bug 1398196 --> +<section class="before-pseudo-element"><span>span</span></section> +<section class="position-relative"><a href="about:blank">link</a></section><!-- bug 1506508 --> +<section class="position-relative"><span>span</span></section> +<script type="application/javascript"> +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function doTest() { + for (let sectionId of ["before-pseudo-element", "position-relative"]) { + for (let element of ["a", "span"]) { + let target = document.querySelector(`section.${sectionId} ${element}`); + target.scrollIntoView(true); + let clicked = false; + target.addEventListener("click", (aEvent) => { + is(aEvent.target, target, `click event is fired on the <${element}> element in ${sectionId} section as expected`); + aEvent.preventDefault(); + clicked = true; + }, {once: true}); + synthesizeMouseAtCenter(target, {}); + ok(clicked, `Click event should've been fired on the <${element}> element in ${sectionId} section`); + } + } + SimpleTest.finish(); +}); +</script> +</body> +</html> diff --git a/dom/events/test/test_clickevent_on_input.html b/dom/events/test/test_clickevent_on_input.html new file mode 100644 index 0000000000..6f180d447b --- /dev/null +++ b/dom/events/test/test_clickevent_on_input.html @@ -0,0 +1,108 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test click event on input</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<p id="display"> +<input id="input" + style="position: absolute; top: 5px; left: 5px; border: solid 15px blue; width: 100px; height: 20px;" + onclick="gClickCount++;"> +</p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> + +var gClickCount = 0; + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(runTests); + +var input = document.getElementById("input"); + +function runTests() +{ + for (var i = 0; i < 3; i++) { + doTest(i); + } + + // Re-test left clicking when the input element has some text. + gClickCount = 0; + input.value = "Long text Long text Long text Long text Long text Long text"; + doTest(0); + + input.style.display = "none"; + SimpleTest.finish(); +} + +function isEnabledMiddleClickPaste() +{ + try { + return SpecialPowers.getBoolPref("middlemouse.paste"); + } catch (e) { + return false; + } +} + +function isEnabledAccessibleCaret() +{ + try { + return SpecialPowers.getBoolPref("layout.accessiblecaret.enabled"); + } catch (e) { + return false; + } +} + +function doTest(aButton) +{ + // NOTE #1: Non-primary buttons don't generate 'click' events + // NOTE #2: If touch caret is enabled, touch caret would ovelap input element, + // then, the click event isn't generated. + if (aButton != 2 && + aButton != 1 && + (aButton != 0 || !isEnabledAccessibleCaret())) { + gClickCount = 0; + // click on border of input + synthesizeMouse(input, 5, 5, { button: aButton }); + is(gClickCount, 1, + "click event doesn't fired on input element (button is " + + aButton + ")"); + + gClickCount = 0; + // down on border + synthesizeMouse(input, 5, 5, { type: "mousedown", button: aButton }); + // up on anonymous div of input + synthesizeMouse(input, 20, 20, { type: "mouseup", button: aButton }); + is(gClickCount, 1, + "click event doesn't fired on input element (button is " + + aButton + ")"); + + gClickCount = 0; + // down on anonymous div of input + synthesizeMouse(input, 20, 20, { type: "mousedown", button: aButton }); + // up on border + synthesizeMouse(input, 5, 5, { type: "mouseup", button: aButton }); + is(gClickCount, 1, + "click event doesn't fired on input element (button is " + + aButton + ")"); + } + + gClickCount = 0; + // down on outside of input + synthesizeMouse(input, -3, -3, { type: "mousedown", button: aButton }); + // up on border + synthesizeMouse(input, 5, 5, { type: "mouseup", button: aButton }); + is(gClickCount, 0, + "click event is fired on input element unexpectedly (button is " + + aButton + ")"); +} + +</script> +</pre> +</body> +</html> diff --git a/dom/events/test/test_coalesce_mousewheel.html b/dom/events/test/test_coalesce_mousewheel.html new file mode 100644 index 0000000000..6142cc3201 --- /dev/null +++ b/dom/events/test/test_coalesce_mousewheel.html @@ -0,0 +1,241 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>mousewheel coalescing</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script src="apz_test_utils.js"></script> + <script src="apz_test_native_event_utils.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> + <script> + let wheelEvents = []; + function mousewheel(aWheelEvent) { + // Make mousewheel handling slow + var start = performance.now(); + while (performance.now() < (start + 10)); + wheelEvents.push(aWheelEvent); + } + + function resolveIfProcessed(resolve, minamount) { + if (wheelEvents.length >= minamount) { + SimpleTest.requestFlakyTimeout("Make sure we got all events."); + setTimeout(function() { resolve(); }, 20); + } else { + setTimeout(function() { resolveIfProcessed(resolve, minamount); }); + } + } + + const kLineDeltaFactor = + SpecialPowers.getBoolPref("mousewheel.system_scroll_override.enabled", false) + ? SpecialPowers.getIntPref("mousewheel.system_scroll_override.vertical.factor", 200) / 100 + : 1; + + function checkWheelEvents(aExpectedDeltaMode, aExpectedSumOfDeltaY, aDescription) { + const lineDeltaFactor = + aExpectedDeltaMode === WheelEvent.DOM_DELTA_LINE ? kLineDeltaFactor : 1; + let succeeded = true; + let deltaY = 0; + for (const wheelEvent of wheelEvents) { + succeeded &= wheelEvent.deltaMode === aExpectedDeltaMode; + is(wheelEvent.deltaMode, aExpectedDeltaMode, + `When ${aDescription}, the deltaMode of all wheel events should be ${aExpectedDeltaMode}`); + deltaY += wheelEvent.deltaY; + } + succeeded &= deltaY == aExpectedSumOfDeltaY * lineDeltaFactor; + is(deltaY, aExpectedSumOfDeltaY * lineDeltaFactor, + `When ${aDescription}, sum of the deltaY of all wheel events should be ${aExpectedSumOfDeltaY * lineDeltaFactor}`); + return succeeded; + } + + async function testOneSingleWheelEvent() { + await new Promise(function(resolve) { + wheelEvents = []; + var element = document.getElementById("wheelEventReceiver"); + element.addEventListener("wheel", mousewheel, true); + + synthesizeWheel(element, 10, 10, { deltaY: 3.0, deltaMode: WheelEvent.DOM_DELTA_LINE }); + + setTimeout(function() { resolveIfProcessed(resolve, 1); }); + }); + is(wheelEvents.length, 1, + "Synthesizing a wheel event via the parent process should cause 1 wheel event"); + is(wheelEvents[0]?.deltaMode, WheelEvent.DOM_DELTA_LINE, + "When Synthesizing a wheel event via the parent process, the deltaMode of a wheel event should be WheelEvent.DOM_DELTA_LINE"); + is(wheelEvents[0]?.deltaY, 3 * kLineDeltaFactor, + `When Synthesizing a wheel event via the parent process, the deltaY of a wheel event should be ${3 * kLineDeltaFactor}`); + } + + const kMaxRetry = 10; + + async function testTwoSingleWheelEvents() { + function tryIt() { + return new Promise(function(resolve) { + info("Synthesizing 2 wheel events via the parent process..."); + wheelEvents = []; + var element = document.getElementById("wheelEventReceiver"); + element.addEventListener("wheel", mousewheel, true); + + synthesizeWheel(element, 10, 10, { deltaY: 3.0, deltaMode: WheelEvent.DOM_DELTA_LINE }); + synthesizeWheel(element, 10, 10, { deltaY: 3.0, deltaMode: WheelEvent.DOM_DELTA_LINE }); + + setTimeout(function() { resolveIfProcessed(resolve, 1); }); + }); + } + for (let i = 0; i < kMaxRetry; i++) { + await tryIt(); + if (wheelEvents.length == 2) { + // Even if failed to coalescing, the sum of deltaY values should be same + // as sum of the synthesized ones. + if (!checkWheelEvents(WheelEvent.DOM_DELTA_LINE, 6, + "synthesizing 2 wheel events via the parent process")) { + return false; + } + continue; // retry + } + is(wheelEvents.length, 1, + "Synthesizing 2 wheel events via the parent process should cause only 1 wheel event"); + checkWheelEvents(WheelEvent.DOM_DELTA_LINE, 6, + "synthesizing 2 wheel events via the parent process"); + return; + } + } + + async function testManySingleWheelEvents() { + function tryIt() { + return new Promise(function(resolve) { + info("Synthesizing 5 wheel events via the parent process..."); + wheelEvents = []; + var element = document.getElementById("wheelEventReceiver"); + element.addEventListener("wheel", mousewheel, true); + + synthesizeWheel(element, 10, 10, { deltaY: 3.0, deltaMode: WheelEvent.DOM_DELTA_LINE }); + synthesizeWheel(element, 10, 10, { deltaY: 3.0, deltaMode: WheelEvent.DOM_DELTA_LINE }); + synthesizeWheel(element, 10, 10, { deltaY: 3.0, deltaMode: WheelEvent.DOM_DELTA_LINE }); + synthesizeWheel(element, 10, 10, { deltaY: 3.0, deltaMode: WheelEvent.DOM_DELTA_LINE }); + synthesizeWheel(element, 10, 10, { deltaY: 3.0, deltaMode: WheelEvent.DOM_DELTA_LINE }); + + setTimeout(function() { resolveIfProcessed(resolve, 1); }); + }); + } + for (let i = 0; i < kMaxRetry; i++) { + await tryIt(); + if (wheelEvents.length > 1 && wheelEvents.length <= 5) { + // Even if failed to coalescing, the sum of deltaY values should be same + // as sum of the synthesized ones. + if (!checkWheelEvents(WheelEvent.DOM_DELTA_LINE, 15, + "synthesizing 5 wheel events via the parent process")) { + return false; + } + continue; // retry + } + is(wheelEvents.length, 1, + "Synthesizing 5 wheel events via the parent process should cause only 1 wheel event"); + checkWheelEvents(WheelEvent.DOM_DELTA_LINE, 15, + "synthesizing 5 wheel events via the parent process"); + return; + } + } + + async function testMixedWheelEvents() { + function tryIt() { + return new Promise(function(resolve) { + info("Synthesizing 2 line wheel events, 1 page wheel event and 1 line wheel event..."); + wheelEvents = []; + var element = document.getElementById("wheelEventReceiver"); + element.addEventListener("wheel", mousewheel, true); + + synthesizeWheel(element, 10, 10, { deltaY: 3.0, deltaMode: WheelEvent.DOM_DELTA_LINE }); + synthesizeWheel(element, 10, 10, { deltaY: 3.0, deltaMode: WheelEvent.DOM_DELTA_LINE }); + synthesizeWheel(element, 10, 10, { deltaY: 3.0, deltaMode: WheelEvent.DOM_DELTA_PAGE }); + synthesizeWheel(element, 10, 10, { deltaY: 3.0, deltaMode: WheelEvent.DOM_DELTA_LINE }); + + setTimeout(function() { resolveIfProcessed(resolve, 3); }); + }); + } + function checkGroupsOfWheelEvents() { + let succeeded = true; + let deltaY = [0, 0, 0]; + let index = 0; + const description = "synthesizing 2 line wheel events, 1 page wheel event and 1 line wheel event"; + for (const wheelEvent of wheelEvents) { + switch (index) { + case 0: + if (wheelEvent.deltaMode === WheelEvent.DOM_DELTA_LINE) { + is(wheelEvent.deltaMode, WheelEvent.DOM_DELTA_LINE, + `When ${description}, the deltaMode of the first group should be WheelEvent.DOM_DELTA_LINE`); + break; + } + index++; + // fallthrough + case 1: + if (wheelEvent.deltaMode === WheelEvent.DOM_DELTA_PAGE) { + is(wheelEvent.deltaMode, WheelEvent.DOM_DELTA_PAGE, + `When ${description}, the deltaMode of the seconde group should be WheelEvent.DOM_DELTA_PAGE`); + break; + } + index++; + // fallthrough + case 2: + succeeded &= wheelEvent.deltaMode === WheelEvent.DOM_DELTA_LINE; + is(wheelEvent.deltaMode, WheelEvent.DOM_DELTA_LINE, + `When ${description}, the deltaMode of the last group should be WheelEvent.DOM_DELTA_LINE`); + break; + } + deltaY[index] += wheelEvent.deltaY; + } + succeeded &= deltaY == [6 * kLineDeltaFactor, 3, 3 * kLineDeltaFactor]; + isDeeply(deltaY, [6 * kLineDeltaFactor, 3, 3 * kLineDeltaFactor], + `When ${description}, sum of the deltaY of the each wheel event group should be same as sum of the synthesized ones`); + return succeeded; + } + for (let i = 0; i < kMaxRetry; i++) { + await tryIt(); + if (wheelEvents.length != 3 && wheelEvents.length > 1 && wheelEvents.length <= 5) { + // Even if failed to coalescing, the sum of deltaY values should be same + // as sum of the synthesized ones and the wheel events shouldn't be + // reordered. + if (!checkGroupsOfWheelEvents()) { + return false; + } + continue; // retry + } + is(wheelEvents.length, 3, + "Synthesizing 2 line wheel events, 1 page wheel event and 1 line wheel event via the parent process should cause only 3 wheel events"); + checkGroupsOfWheelEvents(); + return; + } + } + + async function runTests() { + var enabled = SpecialPowers.getDOMWindowUtils(window).asyncPanZoomEnabled; + + // Avoid synthesized mousemove events to be fired at the system cursor. + await promiseNativeMouseEvent({ + type: "mousemove", + screenX: 0, + screenY: 0, + }); + + await SpecialPowers.pushPrefEnv({set: [ + ["test.events.async.enabled", true], + ["dom.event.wheel-coalesced.testing", true], + ]}); + await promiseElementReadyForUserInput(document.documentElement); + await SpecialPowers.pushPrefEnv({clear: [["test.events.async.enabled"]]}); + + await testOneSingleWheelEvent(); + await testTwoSingleWheelEvents(); + await testManySingleWheelEvents(); + await testMixedWheelEvents(); + + setTimeout("SimpleTest.finish()"); + window.close(); + } + + </script> +</head> +<body onload="SimpleTest.waitForFocus(runTests);"> + <div id="wheelEventReceiver" style="width:100px;height:100px;"></div> +</body> +</html> diff --git a/dom/events/test/test_coalesce_touchmove.html b/dom/events/test/test_coalesce_touchmove.html new file mode 100644 index 0000000000..3733a035cc --- /dev/null +++ b/dom/events/test/test_coalesce_touchmove.html @@ -0,0 +1,34 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>touchmove coalescing</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> + <script> + SimpleTest.waitForExplicitFinish(); + + let tests = [ + "file_coalesce_touchmove_ipc.html", + "file_coalesce_touchmove_browserchild.html", + "file_coalesce_touchmove_browserchild2.html", + ]; + + function finish() { + info("finish"); + if (tests.length) { + SimpleTest.executeSoon(run); + return; + } + SimpleTest.finish(); + } + + function run() { + info(`run ${tests[0]}`); + window.open(tests.shift()); + } + </script> +</head> +<body onload="run();"> +</body> +</html> diff --git a/dom/events/test/test_continuous_wheel_events.html b/dom/events/test/test_continuous_wheel_events.html new file mode 100644 index 0000000000..7905f05738 --- /dev/null +++ b/dom/events/test/test_continuous_wheel_events.html @@ -0,0 +1,3294 @@ +<!DOCTYPE HTML> +<html style="font-size: 32px;"> +<head> + <title>Test for D3E WheelEvent</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script src="/tests/SimpleTest/paint_listener.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body onload="bodyLoaded()"> +<p id="display"></p> +<div id="scrollable" style="font-family: 'Courier New', monospace; font-size: 18px; line-height: 1; overflow: auto; width: 200px; height: 200px;"> + <div id="scrolled" style="font-size: 64px; width: 5000px; height: 5000px;"> + Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text.<br> + Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text.<br> + Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text.<br> + Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text.<br> + Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text.<br> + Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text.<br> + Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text.<br> + Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text.<br> + Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text.<br> + Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text.<br> + Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text.<br> + Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text.<br> + Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text.<br> + Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text.<br> + Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text.<br> + Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text.<br> + Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text.<br> + Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text.<br> + Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text.<br> + Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text.<br> + Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text.<br> + Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text.<br> + Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text.<br> + Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text.<br> + Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text.<br> + Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text.<br> + </div> +</div> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> + +var gScrollableElement; +var gScrolledElement; + +SimpleTest.waitForExplicitFinish(); +function bodyLoaded() { + gScrollableElement = document.getElementById("scrollable"); + gScrolledElement = document.getElementById("scrolled"); + runTests(); +} + +var gLineHeight = 0; +var gHorizontalLine = 0; +var gPageHeight = 0; +var gPageWidth = 0; + +function sendWheelAndWait(aX, aY, aEvent) +{ + sendWheelAndPaint(gScrollableElement, aX, aY, aEvent, continueTest); +} + +function* prepareScrollUnits() +{ + var result = -1; + function handler(aEvent) + { + result = aEvent.detail; + aEvent.preventDefault(); + } + window.addEventListener("MozMousePixelScroll", handler, { capture: true, passive: false }); + + yield sendWheelAndWait(10, 10, + { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaY: 1.0, lineOrPageDeltaY: 1 }); + gLineHeight = result; + ok(gLineHeight > 10 && gLineHeight < 25, "prepareScrollUnits: gLineHeight may be illegal value, got " + gLineHeight); + + result = -1; + yield sendWheelAndWait(10, 10, + { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: 1.0, lineOrPageDeltaX: 1 }); + gHorizontalLine = result; + ok(gHorizontalLine > 5 && gHorizontalLine < 16, "prepareScrollUnits: gHorizontalLine may be illegal value, got " + gHorizontalLine); + + result = -1; + yield sendWheelAndWait(10, 10, + { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaY: 1.0, lineOrPageDeltaY: 1 }); + gPageHeight = result; + // XXX Cannot we know the actual scroll port size? + ok(gPageHeight >= 150 && gPageHeight <= 200, + "prepareScrollUnits: gPageHeight is strange value, got " + gPageHeight); + + result = -1; + yield sendWheelAndWait(10, 10, + { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: 1.0, lineOrPageDeltaX: 1 }); + gPageWidth = result; + ok(gPageWidth >= 150 && gPageWidth <= 200, + "prepareScrollUnits: gPageWidth is strange value, got " + gPageWidth); + + window.removeEventListener("MozMousePixelScroll", handler, true); +} + +// Tests continuous trusted wheel events. Trusted wheel events should cause +// legacy mouse scroll events when its lineOrPageDelta value is not zero or +// accumulated delta values of pixel scroll events of pixel only device +// become over the line height. +function* testContinuousTrustedEvents() +{ + const kSynthesizedWheelEventTests = [ + { description: "Simple horizontal wheel event by pixels (16.0 - 1) #1", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 16.0, deltaY: 0.0, deltaZ: 0.0, isMomentum: false, + lineOrPageDeltaX: 1, lineOrPageDeltaY: 0, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: 16.0, deltaY: 0.0, deltaZ: 0.0 + }, + DOMMouseScroll: { + horizontal: { expected: true, preventDefault: false, detail: 1 }, + vertical: { expected: false, preventDefault: false, detail: 0 } }, + MozMousePixelScroll: { + horizontal: { expected: true, preventDefault: false, detail: 16 }, + vertical: { expected: false, preventDefault: false, detail: 0 } } + }, + { description: "Simple horizontal wheel event by pixels (16.0 - 1) #2", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 16.0, deltaY: 0.0, deltaZ: 0.0, isMomentum: false, + lineOrPageDeltaX: 1, lineOrPageDeltaY: 0, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: 16.0, deltaY: 0.0, deltaZ: 0.0 + }, + DOMMouseScroll: { + horizontal: { expected: true, preventDefault: false, detail: 1 }, + vertical: { expected: false, preventDefault: false, detail: 0 } }, + MozMousePixelScroll: { + horizontal: { expected: true, preventDefault: false, detail: 16 }, + vertical: { expected: false, preventDefault: false, detail: 0 } } + }, + { description: "Simple horizontal wheel event by pixels (16.0 - 1) #3", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 16.0, deltaY: 0.0, deltaZ: 0.0, isMomentum: false, + lineOrPageDeltaX: 1, lineOrPageDeltaY: 0, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: 16.0, deltaY: 0.0, deltaZ: 0.0 + }, + DOMMouseScroll: { + horizontal: { expected: true, preventDefault: false, detail: 1 }, + vertical: { expected: false, preventDefault: false, detail: 0 } }, + MozMousePixelScroll: { + horizontal: { expected: true, preventDefault: false, detail: 16 }, + vertical: { expected: false, preventDefault: false, detail: 0 } } + }, + + { description: "Simple vertical wheel event by pixels (16.0 - 1) #1", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 0.0, deltaY: 16.0, deltaZ: 0.0, isMomentum: false, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 1, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: 0.0, deltaY: 16.0, deltaZ: 0.0 + }, + DOMMouseScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: true, preventDefault: false, detail: 1 } }, + MozMousePixelScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: true, preventDefault: false, detail: 16 } } + }, + { description: "Simple vertical wheel event by pixels (16.0 - 1) #2", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 0.0, deltaY: 16.0, deltaZ: 0.0, isMomentum: false, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 1, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: 0.0, deltaY: 16.0, deltaZ: 0.0 + }, + DOMMouseScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: true, preventDefault: false, detail: 1 } }, + MozMousePixelScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: true, preventDefault: false, detail: 16 } } + }, + { description: "Simple vertical wheel event by pixels (16.0 - 1) #3", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 0.0, deltaY: 16.0, deltaZ: 0.0, isMomentum: false, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 1, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: 0.0, deltaY: 16.0, deltaZ: 0.0 + }, + DOMMouseScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: true, preventDefault: false, detail: 1 } }, + MozMousePixelScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: true, preventDefault: false, detail: 16 } } + }, + + { description: "Simple z-direction wheel event by pixels (16.0 - 1)", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 0.0, deltaY: 0.0, deltaZ: 16.0, isMomentum: false, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: 0.0, deltaY: 0.0, deltaZ: 16.0 + }, + DOMMouseScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: false, preventDefault: false, detail: 0 } }, + MozMousePixelScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: false, preventDefault: false, detail: 0 } } + }, + + { description: "Simple horizontal wheel event by pixels (-16.0 - -1) #1", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: -16.0, deltaY: 0.0, deltaZ: 0.0, isMomentum: false, + lineOrPageDeltaX: -1, lineOrPageDeltaY: 0, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: -16.0, deltaY: 0.0, deltaZ: 0.0 + }, + DOMMouseScroll: { + horizontal: { expected: true, preventDefault: false, detail: -1 }, + vertical: { expected: false, preventDefault: false, detail: 0 } }, + MozMousePixelScroll: { + horizontal: { expected: true, preventDefault: false, detail: -16 }, + vertical: { expected: false, preventDefault: false, detail: 0 } } + }, + { description: "Simple horizontal wheel event by pixels (-16.0 - -1) #2", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: -16.0, deltaY: 0.0, deltaZ: 0.0, isMomentum: false, + lineOrPageDeltaX: -1, lineOrPageDeltaY: 0, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: -16.0, deltaY: 0.0, deltaZ: 0.0 + }, + DOMMouseScroll: { + horizontal: { expected: true, preventDefault: false, detail: -1 }, + vertical: { expected: false, preventDefault: false, detail: 0 } }, + MozMousePixelScroll: { + horizontal: { expected: true, preventDefault: false, detail: -16 }, + vertical: { expected: false, preventDefault: false, detail: 0 } } + }, + { description: "Simple horizontal wheel event by pixels (-16.0 - -1) #3", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: -16.0, deltaY: 0.0, deltaZ: 0.0, isMomentum: false, + lineOrPageDeltaX: -1, lineOrPageDeltaY: 0, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: -16.0, deltaY: 0.0, deltaZ: 0.0 + }, + DOMMouseScroll: { + horizontal: { expected: true, preventDefault: false, detail: -1 }, + vertical: { expected: false, preventDefault: false, detail: 0 } }, + MozMousePixelScroll: { + horizontal: { expected: true, preventDefault: false, detail: -16 }, + vertical: { expected: false, preventDefault: false, detail: 0 } } + }, + + { description: "Simple vertical wheel event by pixels (-16.0 - -1) #1", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 0.0, deltaY: -16.0, deltaZ: 0.0, isMomentum: false, + lineOrPageDeltaX: 0, lineOrPageDeltaY: -1, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: 0.0, deltaY: -16.0, deltaZ: 0.0 + }, + DOMMouseScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: true, preventDefault: false, detail: -1 } }, + MozMousePixelScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: true, preventDefault: false, detail: -16 } } + }, + { description: "Simple vertical wheel event by pixels (-16.0 - -1) #2", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 0.0, deltaY: -16.0, deltaZ: 0.0, isMomentum: false, + lineOrPageDeltaX: 0, lineOrPageDeltaY: -1, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: 0.0, deltaY: -16.0, deltaZ: 0.0 + }, + DOMMouseScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: true, preventDefault: false, detail: -1 } }, + MozMousePixelScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: true, preventDefault: false, detail: -16 } } + }, + { description: "Simple vertical wheel event by pixels (-16.0 - -1) #3", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 0.0, deltaY: -16.0, deltaZ: 0.0, isMomentum: false, + lineOrPageDeltaX: 0, lineOrPageDeltaY: -1, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: 0.0, deltaY: -16.0, deltaZ: 0.0 + }, + DOMMouseScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: true, preventDefault: false, detail: -1 } }, + MozMousePixelScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: true, preventDefault: false, detail: -16 } } + }, + + { description: "Simple z-direction wheel event by pixels (-16.0 - -1)", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 0.0, deltaY: 0.0, deltaZ: -16.0, isMomentum: false, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: 0.0, deltaY: 0.0, deltaZ: -16.0 + }, + DOMMouseScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: false, preventDefault: false, detail: 0 } }, + MozMousePixelScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: false, preventDefault: false, detail: 0 } } + }, + + // 3 scroll events per line, and legacy line scroll will be fired first. + { description: "Horizontal wheel event by pixels (5.3 - 1) #1", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 5.3, deltaY: 0.0, deltaZ: 0.0, isMomentum: false, + lineOrPageDeltaX: 1, lineOrPageDeltaY: 0, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: 5.3, deltaY: 0.0, deltaZ: 0.0 + }, + DOMMouseScroll: { + horizontal: { expected: true, preventDefault: false, detail: 1 }, + vertical: { expected: false, preventDefault: false, detail: 0 } }, + MozMousePixelScroll: { + horizontal: { expected: true, preventDefault: false, detail: 5 }, + vertical: { expected: false, preventDefault: false, detail: 0 } } + }, + { description: "Horizontal wheel event by pixels (5.3 - 0) #2", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 5.3, deltaY: 0.0, deltaZ: 0.0, isMomentum: false, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: 5.3, deltaY: 0.0, deltaZ: 0.0 + }, + DOMMouseScroll: { + horizontal: { expected: false, preventDefault: false, detail: 1 }, + vertical: { expected: false, preventDefault: false, detail: 0 } }, + MozMousePixelScroll: { + horizontal: { expected: true, preventDefault: false, detail: 5 }, + vertical: { expected: false, preventDefault: false, detail: 0 } } + }, + { description: "Vertical wheel event by pixels (5.3 - 1) #1", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 0.0, deltaY: 5.3, deltaZ: 0.0, isMomentum: false, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 1, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: 0.0, deltaY: 5.3, deltaZ: 0.0 + }, + DOMMouseScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: true, preventDefault: false, detail: 1 } }, + MozMousePixelScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: true, preventDefault: false, detail: 5 } } + }, + { description: "Vertical wheel event by pixels (5.3 - 0) #2", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 0.0, deltaY: 5.3, deltaZ: 0.0, isMomentum: false, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: 0.0, deltaY: 5.3, deltaZ: 0.0 + }, + DOMMouseScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: false, preventDefault: false, detail: 0 } }, + MozMousePixelScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: true, preventDefault: false, detail: 5 } } + }, + { description: "Vertical wheel event by pixels (5.3 - 0) #3", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 0.0, deltaY: 5.3, deltaZ: 0.0, isMomentum: false, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: 0.0, deltaY: 5.3, deltaZ: 0.0 + }, + DOMMouseScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: false, preventDefault: false, detail: 0 } }, + MozMousePixelScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: true, preventDefault: false, detail: 5 } } + }, + + { description: "Horizontal wheel event by pixels (-5.3 - -1) #1", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: -5.3, deltaY: 0.0, deltaZ: 0.0, isMomentum: false, + lineOrPageDeltaX: -1, lineOrPageDeltaY: 0, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: -5.3, deltaY: 0.0, deltaZ: 0.0 + }, + DOMMouseScroll: { + horizontal: { expected: true, preventDefault: false, detail: -1 }, + vertical: { expected: false, preventDefault: false, detail: 0 } }, + MozMousePixelScroll: { + horizontal: { expected: true, preventDefault: false, detail: -5 }, + vertical: { expected: false, preventDefault: false, detail: 0 } } + }, + { description: "Horizontal wheel event by pixels (-5.3 - 0) #2", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: -5.3, deltaY: 0.0, deltaZ: 0.0, isMomentum: false, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: -5.3, deltaY: 0.0, deltaZ: 0.0 + }, + DOMMouseScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: false, preventDefault: false, detail: 0 } }, + MozMousePixelScroll: { + horizontal: { expected: true, preventDefault: false, detail: -5 }, + vertical: { expected: false, preventDefault: false, detail: 0 } } + }, + { description: "Horizontal wheel event by pixels (-5.3 - 0) #3", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: -5.3, deltaY: 0.0, deltaZ: 0.0, isMomentum: false, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: -5.3, deltaY: 0.0, deltaZ: 0.0 + }, + DOMMouseScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: false, preventDefault: false, detail: 0 } }, + MozMousePixelScroll: { + horizontal: { expected: true, preventDefault: false, detail: -5 }, + vertical: { expected: false, preventDefault: false, detail: 0 } } + }, + + { description: "Vertical wheel event by pixels (-5.3 - -1) #1", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 0.0, deltaY: -5.3, deltaZ: 0.0, isMomentum: false, + lineOrPageDeltaX: 0, lineOrPageDeltaY: -1, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: 0.0, deltaY: -5.3, deltaZ: 0.0 + }, + DOMMouseScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: true, preventDefault: false, detail: -1 } }, + MozMousePixelScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: true, preventDefault: false, detail: -5 } } + }, + { description: "Vertical wheel event by pixels (-5.3 - 0) #2", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 0.0, deltaY: -5.3, deltaZ: 0.0, isMomentum: false, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: 0.0, deltaY: -5.3, deltaZ: 0.0 + }, + DOMMouseScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: false, preventDefault: false, detail: 0 } }, + MozMousePixelScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: true, preventDefault: false, detail: -5 } } + }, + { description: "Vertical wheel event by pixels (-5.3 - 0) #3", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 0.0, deltaY: -5.3, deltaZ: 0.0, isMomentum: false, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: 0.0, deltaY: -5.3, deltaZ: 0.0 + }, + DOMMouseScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: false, preventDefault: false, detail: 0 } }, + MozMousePixelScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: true, preventDefault: false, detail: -5 } } + }, + + // 3 scroll events per line, and legacy line scroll will be fired last. + { description: "Horizontal wheel event by pixels (5.3 - 0) #1", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 5.3, deltaY: 0.0, deltaZ: 0.0, isMomentum: false, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: 5.3, deltaY: 0.0, deltaZ: 0.0 + }, + DOMMouseScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: false, preventDefault: false, detail: 0 } }, + MozMousePixelScroll: { + horizontal: { expected: true, preventDefault: false, detail: 5 }, + vertical: { expected: false, preventDefault: false, detail: 0 } } + }, + { description: "Horizontal wheel event by pixels (5.3 - 0) #2", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 5.3, deltaY: 0.0, deltaZ: 0.0, isMomentum: false, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: 5.3, deltaY: 0.0, deltaZ: 0.0 + }, + DOMMouseScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: false, preventDefault: false, detail: 0 } }, + MozMousePixelScroll: { + horizontal: { expected: true, preventDefault: false, detail: 5 }, + vertical: { expected: false, preventDefault: false, detail: 0 } } + }, + { description: "Horizontal wheel event by pixels (5.3 - 1) #3", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 5.3, deltaY: 0.0, deltaZ: 0.0, isMomentum: false, + lineOrPageDeltaX: 1, lineOrPageDeltaY: 0, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: 5.3, deltaY: 0.0, deltaZ: 0.0 + }, + DOMMouseScroll: { + horizontal: { expected: true, preventDefault: false, detail: 1 }, + vertical: { expected: false, preventDefault: false, detail: 0 } }, + MozMousePixelScroll: { + horizontal: { expected: true, preventDefault: false, detail: 5 }, + vertical: { expected: false, preventDefault: false, detail: 0 } } + }, + + { description: "Vertical wheel event by pixels (5.3 - 0) #1", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 0.0, deltaY: 5.3, deltaZ: 0.0, isMomentum: false, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: 0.0, deltaY: 5.3, deltaZ: 0.0 + }, + DOMMouseScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: false, preventDefault: false, detail: 0 } }, + MozMousePixelScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: true, preventDefault: false, detail: 5 } } + }, + { description: "Vertical wheel event by pixels (5.3 - 0) #2", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 0.0, deltaY: 5.3, deltaZ: 0.0, isMomentum: false, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: 0.0, deltaY: 5.3, deltaZ: 0.0 + }, + DOMMouseScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: false, preventDefault: false, detail: 0 } }, + MozMousePixelScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: true, preventDefault: false, detail: 5 } } + }, + { description: "Vertical wheel event by pixels (5.3 - 1) #3", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 0.0, deltaY: 5.3, deltaZ: 0.0, isMomentum: false, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 1, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: 0.0, deltaY: 5.3, deltaZ: 0.0 + }, + DOMMouseScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: true, preventDefault: false, detail: 1 } }, + MozMousePixelScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: true, preventDefault: false, detail: 5 } } + }, + + { description: "Horizontal wheel event by pixels (-5.3 - 0) #1", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: -5.3, deltaY: 0.0, deltaZ: 0.0, isMomentum: false, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: -5.3, deltaY: 0.0, deltaZ: 0.0 + }, + DOMMouseScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: false, preventDefault: false, detail: 0 } }, + MozMousePixelScroll: { + horizontal: { expected: true, preventDefault: false, detail: -5 }, + vertical: { expected: false, preventDefault: false, detail: 0 } } + }, + { description: "Horizontal wheel event by pixels (-5.3 - 0) #2", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: -5.3, deltaY: 0.0, deltaZ: 0.0, isMomentum: false, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: -5.3, deltaY: 0.0, deltaZ: 0.0 + }, + DOMMouseScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: false, preventDefault: false, detail: 0 } }, + MozMousePixelScroll: { + horizontal: { expected: true, preventDefault: false, detail: -5 }, + vertical: { expected: false, preventDefault: false, detail: 0 } } + }, + { description: "Horizontal wheel event by pixels (-5.3 - 1) #3", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: -5.3, deltaY: 0.0, deltaZ: 0.0, isMomentum: false, + lineOrPageDeltaX: -1, lineOrPageDeltaY: 0, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: -5.3, deltaY: 0.0, deltaZ: 0.0 + }, + DOMMouseScroll: { + horizontal: { expected: true, preventDefault: false, detail: -1 }, + vertical: { expected: false, preventDefault: false, detail: 0 } }, + MozMousePixelScroll: { + horizontal: { expected: true, preventDefault: false, detail: -5 }, + vertical: { expected: false, preventDefault: false, detail: 0 } } + }, + + { description: "Vertical wheel event by pixels (-5.3 - 0) #1", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 0.0, deltaY: -5.3, deltaZ: 0.0, isMomentum: false, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: 0.0, deltaY: -5.3, deltaZ: 0.0 + }, + DOMMouseScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: false, preventDefault: false, detail: 0 } }, + MozMousePixelScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: true, preventDefault: false, detail: -5 } } + }, + { description: "Vertical wheel event by pixels (-5.3 - 0) #2", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 0.0, deltaY: -5.3, deltaZ: 0.0, isMomentum: false, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: 0.0, deltaY: -5.3, deltaZ: 0.0 + }, + DOMMouseScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: false, preventDefault: false, detail: 0 } }, + MozMousePixelScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: true, preventDefault: false, detail: -5 } } + }, + { description: "Vertical wheel event by pixels (-5.3 - -1) #3", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 0.0, deltaY: -5.3, deltaZ: 0.0, isMomentum: false, + lineOrPageDeltaX: 0, lineOrPageDeltaY: -1, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: 0.0, deltaY: -5.3, deltaZ: 0.0 + }, + DOMMouseScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: true, preventDefault: false, detail: -1 } }, + MozMousePixelScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: true, preventDefault: false, detail: -5 } } + }, + + // Oblique scroll. + { description: "To bottom-right wheel event by pixels (5.3/5.2 - 1/1) #1", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 5.3, deltaY: 5.2, deltaZ: 0.0, isMomentum: false, + lineOrPageDeltaX: 1, lineOrPageDeltaY: 1, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: 5.3, deltaY: 5.2, deltaZ: 0.0 + }, + DOMMouseScroll: { + horizontal: { expected: true, preventDefault: false, detail: 1 }, + vertical: { expected: true, preventDefault: false, detail: 1 } }, + MozMousePixelScroll: { + horizontal: { expected: true, preventDefault: false, detail: 5 }, + vertical: { expected: true, preventDefault: false, detail: 5 } } + }, + { description: "To bottom-right wheel event by pixels (5.3/5.2 - 0/0) #2", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 5.3, deltaY: 5.2, deltaZ: 0.0, isMomentum: false, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: 5.3, deltaY: 5.2, deltaZ: 0.0 + }, + DOMMouseScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: false, preventDefault: false, detail: 0 } }, + MozMousePixelScroll: { + horizontal: { expected: true, preventDefault: false, detail: 5 }, + vertical: { expected: true, preventDefault: false, detail: 5 } } + }, + { description: "To bottom-right wheel event by pixels (5.3/5.2 - 0/0) #3", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 5.3, deltaY: 5.2, deltaZ: 0.0, isMomentum: false, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: 5.3, deltaY: 5.2, deltaZ: 0.0 + }, + DOMMouseScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: false, preventDefault: false, detail: 0 } }, + MozMousePixelScroll: { + horizontal: { expected: true, preventDefault: false, detail: 5 }, + vertical: { expected: true, preventDefault: false, detail: 5 } } + }, + + { description: "To bottom-left wheel event by pixels (-5.3/5.3 - -1/1) #1", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: -5.3, deltaY: 5.3, deltaZ: 0.0, isMomentum: false, + lineOrPageDeltaX: -1, lineOrPageDeltaY: 1, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: -5.3, deltaY: 5.3, deltaZ: 0.0 + }, + DOMMouseScroll: { + horizontal: { expected: true, preventDefault: false, detail: -1 }, + vertical: { expected: true, preventDefault: false, detail: 1 } }, + MozMousePixelScroll: { + horizontal: { expected: true, preventDefault: false, detail: -5 }, + vertical: { expected: true, preventDefault: false, detail: 5 } } + }, + { description: "To bottom-left wheel event by pixels (-5.3/5.3 - 0/0) #2", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: -5.3, deltaY: 5.3, deltaZ: 0.0, isMomentum: false, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: -5.3, deltaY: 5.3, deltaZ: 0.0 + }, + DOMMouseScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: false, preventDefault: false, detail: 0 } }, + MozMousePixelScroll: { + horizontal: { expected: true, preventDefault: false, detail: -5 }, + vertical: { expected: true, preventDefault: false, detail: 5 } } + }, + { description: "To bottom-left wheel event by pixels (-5.3/5.3 - 0/0) #3", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: -5.3, deltaY: 5.3, deltaZ: 0.0, isMomentum: false, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: -5.3, deltaY: 5.3, deltaZ: 0.0 + }, + DOMMouseScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: false, preventDefault: false, detail: 0 } }, + MozMousePixelScroll: { + horizontal: { expected: true, preventDefault: false, detail: -5 }, + vertical: { expected: true, preventDefault: false, detail: 5 } } + }, + + { description: "To top-left wheel event by pixels (-5.2/-5.3 - -1/-1) #1", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: -5.2, deltaY: -5.3, deltaZ: 0.0, isMomentum: false, + lineOrPageDeltaX: -1, lineOrPageDeltaY: -1, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: -5.2, deltaY: -5.3, deltaZ: 0.0 + }, + DOMMouseScroll: { + horizontal: { expected: true, preventDefault: false, detail: -1 }, + vertical: { expected: true, preventDefault: false, detail: -1 } }, + MozMousePixelScroll: { + horizontal: { expected: true, preventDefault: false, detail: -5 }, + vertical: { expected: true, preventDefault: false, detail: -5 } } + }, + { description: "To top-left wheel event by pixels (-5.2/-5.3 - 0/0) #2", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: -5.2, deltaY: -5.3, deltaZ: 0.0, isMomentum: false, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: -5.2, deltaY: -5.3, deltaZ: 0.0 + }, + DOMMouseScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: false, preventDefault: false, detail: 0 } }, + MozMousePixelScroll: { + horizontal: { expected: true, preventDefault: false, detail: -5 }, + vertical: { expected: true, preventDefault: false, detail: -5 } } + }, + { description: "To top-left wheel event by pixels (-5.2/-5.3 - 0/0) #3", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: -5.2, deltaY: -5.3, deltaZ: 0.0, isMomentum: false, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: -5.2, deltaY: -5.3, deltaZ: 0.0 + }, + DOMMouseScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: false, preventDefault: false, detail: 0 } }, + MozMousePixelScroll: { + horizontal: { expected: true, preventDefault: false, detail: -5 }, + vertical: { expected: true, preventDefault: false, detail: -5 } } + }, + + { description: "To top-right wheel event by pixels (5.3/-5.3 - 1/-1) #1", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 5.3, deltaY: -5.3, deltaZ: 0.0, isMomentum: false, + lineOrPageDeltaX: 1, lineOrPageDeltaY: -1, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: 5.3, deltaY: -5.3, deltaZ: 0.0 + }, + DOMMouseScroll: { + horizontal: { expected: true, preventDefault: false, detail: 1 }, + vertical: { expected: true, preventDefault: false, detail: -1 } }, + MozMousePixelScroll: { + horizontal: { expected: true, preventDefault: false, detail: 5 }, + vertical: { expected: true, preventDefault: false, detail: -5 } } + }, + { description: "To top-right wheel event by pixels (5.3/-5.3 - 0/0) #2", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 5.3, deltaY: -5.3, deltaZ: 0.0, isMomentum: false, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: 5.3, deltaY: -5.3, deltaZ: 0.0 + }, + DOMMouseScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: false, preventDefault: false, detail: 0 } }, + MozMousePixelScroll: { + horizontal: { expected: true, preventDefault: false, detail: 5 }, + vertical: { expected: true, preventDefault: false, detail: -5 } } + }, + + // Pixel scroll only device's test. the lineOrPageDelta values should be computed + // by ESM. When changing the direction for each delta value, it should be + // reset at that time. + { description: "Pixel only device's horizontal wheel event by pixels (5.3 - 0) #1", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 5.3, deltaY: 0.0, deltaZ: 0.0, isMomentum: false, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isNoLineOrPageDelta: true, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: 5.3, deltaY: 0.0, deltaZ: 0.0 + }, + DOMMouseScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: false, preventDefault: false, detail: 0 } }, + MozMousePixelScroll: { + horizontal: { expected: true, preventDefault: false, detail: 5 }, + vertical: { expected: false, preventDefault: false, detail: 0 } } + }, + { description: "Pixel only device's horizontal wheel event by pixels (5.3 - 0) #2", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 5.3, deltaY: 0.0, deltaZ: 0.0, isMomentum: false, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isNoLineOrPageDelta: true, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: 5.3, deltaY: 0.0, deltaZ: 0.0 + }, + DOMMouseScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: false, preventDefault: false, detail: 0 } }, + MozMousePixelScroll: { + horizontal: { expected: true, preventDefault: false, detail: 5 }, + vertical: { expected: false, preventDefault: false, detail: 0 } } + }, + { description: "Pixel only device's horizontal wheel event by pixels (5.3 - 0) #3", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 5.3, deltaY: 0.0, deltaZ: 0.0, isMomentum: false, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isNoLineOrPageDelta: true, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: 5.3, deltaY: 0.0, deltaZ: 0.0 + }, + DOMMouseScroll: { + horizontal: { expected: true, preventDefault: false, detail: 1 }, + vertical: { expected: false, preventDefault: false, detail: 0 } }, + MozMousePixelScroll: { + horizontal: { expected: true, preventDefault: false, detail: 5 }, + vertical: { expected: false, preventDefault: false, detail: 0 } } + }, + { description: "Pixel only device's Vertical wheel event by pixels (5.3 - 0) #1", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 0.0, deltaY: 5.3, deltaZ: 0.0, isMomentum: false, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isNoLineOrPageDelta: true, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: 0.0, deltaY: 5.3, deltaZ: 0.0 + }, + DOMMouseScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: false, preventDefault: false, detail: 0 } }, + MozMousePixelScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: true, preventDefault: false, detail: 5 } } + }, + { description: "Pixel only device's Vertical wheel event by pixels (5.3 - 0) #2", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 0.0, deltaY: 5.3, deltaZ: 0.0, isMomentum: false, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isNoLineOrPageDelta: true, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: 0.0, deltaY: 5.3, deltaZ: 0.0 + }, + DOMMouseScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: false, preventDefault: false, detail: 0 } }, + MozMousePixelScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: true, preventDefault: false, detail: 5 } } + }, + { description: "Pixel only device's Vertical wheel event by pixels (5.3 - 0) #3", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 0.0, deltaY: 5.3, deltaZ: 0.0, isMomentum: false, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isNoLineOrPageDelta: true, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: 0.0, deltaY: 5.3, deltaZ: 0.0 + }, + DOMMouseScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: false, preventDefault: false, detail: 0 } }, + MozMousePixelScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: true, preventDefault: false, detail: 5 } } + }, + { description: "Pixel only device's Vertical wheel event by pixels (5.3 - 0) #4", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 0.0, deltaY: 1.3, deltaZ: 0.0, isMomentum: false, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isNoLineOrPageDelta: true, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: 0.0, deltaY: 1.3, deltaZ: 0.0 + }, + DOMMouseScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: false, preventDefault: false, detail: 0 } }, + MozMousePixelScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: true, preventDefault: false, detail: 1 } } + }, + { description: "Pixel only device's Vertical wheel event by pixels (5.3 - 1) #5", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 0.0, deltaY: 5.3, deltaZ: 0.0, isMomentum: false, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isNoLineOrPageDelta: true, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: 0.0, deltaY: 5.3, deltaZ: 0.0 + }, + DOMMouseScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: true, preventDefault: false, detail: 1 } }, + MozMousePixelScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: true, preventDefault: false, detail: 5 } } + }, + + { description: "Pixel only device's horizontal wheel event by pixels (-5.3 - 0) #1", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: -5.3, deltaY: 0.0, deltaZ: 0.0, isMomentum: false, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isNoLineOrPageDelta: true, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: -5.3, deltaY: 0.0, deltaZ: 0.0 + }, + DOMMouseScroll: { + horizontal: { expected: false, preventDefault: false, detail: -1 }, + vertical: { expected: false, preventDefault: false, detail: 0 } }, + MozMousePixelScroll: { + horizontal: { expected: true, preventDefault: false, detail: -5 }, + vertical: { expected: false, preventDefault: false, detail: 0 } } + }, + { description: "Pixel only device's horizontal wheel event by pixels (-5.3 - 0) #2", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: -5.3, deltaY: 0.0, deltaZ: 0.0, isMomentum: false, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isNoLineOrPageDelta: true, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: -5.3, deltaY: 0.0, deltaZ: 0.0 + }, + DOMMouseScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: false, preventDefault: false, detail: 0 } }, + MozMousePixelScroll: { + horizontal: { expected: true, preventDefault: false, detail: -5 }, + vertical: { expected: false, preventDefault: false, detail: 0 } } + }, + { description: "Pixel only device's horizontal wheel event by pixels (-5.3 - 0) #3", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: -5.3, deltaY: 0.0, deltaZ: 0.0, isMomentum: false, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isNoLineOrPageDelta: true, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: -5.3, deltaY: 0.0, deltaZ: 0.0 + }, + DOMMouseScroll: { + horizontal: { expected: true, preventDefault: false, detail: -1 }, + vertical: { expected: false, preventDefault: false, detail: 0 } }, + MozMousePixelScroll: { + horizontal: { expected: true, preventDefault: false, detail: -5 }, + vertical: { expected: false, preventDefault: false, detail: 0 } } + }, + + { description: "Pixel only device's Vertical wheel event by pixels (-5.3 - 0) #1", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 0.0, deltaY: -5.3, deltaZ: 0.0, isMomentum: false, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isNoLineOrPageDelta: true, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: 0.0, deltaY: -5.3, deltaZ: 0.0 + }, + DOMMouseScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: false, preventDefault: false, detail: 0 } }, + MozMousePixelScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: true, preventDefault: false, detail: -5 } } + }, + { description: "Pixel only device's Vertical wheel event by pixels (-5.3 - 0) #2", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 0.0, deltaY: -5.3, deltaZ: 0.0, isMomentum: false, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isNoLineOrPageDelta: true, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: 0.0, deltaY: -5.3, deltaZ: 0.0 + }, + DOMMouseScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: false, preventDefault: false, detail: 0 } }, + MozMousePixelScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: true, preventDefault: false, detail: -5 } } + }, + { description: "Pixel only device's Vertical wheel event by pixels (-5.3 - 0) #3", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 0.0, deltaY: -5.3, deltaZ: 0.0, isMomentum: false, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isNoLineOrPageDelta: true, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: 0.0, deltaY: -5.3, deltaZ: 0.0 + }, + DOMMouseScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: false, preventDefault: false, detail: 0 } }, + MozMousePixelScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: true, preventDefault: false, detail: -5 } } + }, + { description: "Pixel only device's Vertical wheel event by pixels (-5.3 - 0) #4", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 0.0, deltaY: -1.3, deltaZ: 0.0, isMomentum: false, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isNoLineOrPageDelta: true, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: 0.0, deltaY: -1.3, deltaZ: 0.0 + }, + DOMMouseScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: false, preventDefault: false, detail: 0 } }, + MozMousePixelScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: true, preventDefault: false, detail: -1 } } + }, + { description: "Pixel only device's Vertical wheel event by pixels (-5.3 - -1) #5", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 0.0, deltaY: -5.3, deltaZ: 0.0, isMomentum: false, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isNoLineOrPageDelta: true, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: 0.0, deltaY: -5.3, deltaZ: 0.0 + }, + DOMMouseScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: true, preventDefault: false, detail: -1 } }, + MozMousePixelScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: true, preventDefault: false, detail: -5 } } + }, + + // ESM should reset an accumulated delta value only when the direction of it + // is changed but shouldn't reset the other delta. + { description: "Pixel only device's bottom-right wheel event by pixels (5.3/4.9 - 0/0) #1", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 5.3, deltaY: 4.9, deltaZ: 0.0, isMomentum: false, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isNoLineOrPageDelta: true, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: 5.3, deltaY: 4.9, deltaZ: 0.0 + }, + DOMMouseScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: false, preventDefault: false, detail: 0 } }, + MozMousePixelScroll: { + horizontal: { expected: true, preventDefault: false, detail: 5 }, + vertical: { expected: true, preventDefault: false, detail: 4 } } + }, + { description: "Pixel only device's bottom-right wheel event by pixels (5.3/4.9 - 0/0) #2", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 5.3, deltaY: 4.9, deltaZ: 0.0, isMomentum: false, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isNoLineOrPageDelta: true, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: 5.3, deltaY: 4.9, deltaZ: 0.0 + }, + DOMMouseScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: false, preventDefault: false, detail: 0 } }, + MozMousePixelScroll: { + horizontal: { expected: true, preventDefault: false, detail: 5 }, + vertical: { expected: true, preventDefault: false, detail: 4 } } + }, + { description: "Pixel only device's bottom-left wheel event by pixels (-5.3/4.9 - 0/0) #4", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: -5.3, deltaY: 4.9, deltaZ: 0.0, isMomentum: false, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isNoLineOrPageDelta: true, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: -5.3, deltaY: 4.9, deltaZ: 0.0 + }, + DOMMouseScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: false, preventDefault: false, detail: 0 } }, + MozMousePixelScroll: { + horizontal: { expected: true, preventDefault: false, detail: -5 }, + vertical: { expected: true, preventDefault: false, detail: 4 } } + }, + // the accumulated X should be 0 here, but Y shouldn't be reset. + { description: "Pixel only device's bottom-right wheel event by pixels (5.3/4.9 - 0/0) #5", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 5.3, deltaY: 1.9, deltaZ: 0.0, isMomentum: false, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isNoLineOrPageDelta: true, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: 5.3, deltaY: 1.9, deltaZ: 0.0 + }, + DOMMouseScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: false, preventDefault: false, detail: 0 } }, + MozMousePixelScroll: { + horizontal: { expected: true, preventDefault: false, detail: 5 }, + vertical: { expected: true, preventDefault: false, detail: 1 } } + }, + + { description: "Pixel only device's top-left wheel event by pixels (-5.3/-4.9 - 0/0) #1", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: -5.3, deltaY: -4.9, deltaZ: 0.0, isMomentum: false, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isNoLineOrPageDelta: true, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: -5.3, deltaY: -4.9, deltaZ: 0.0 + }, + DOMMouseScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: false, preventDefault: false, detail: 0 } }, + MozMousePixelScroll: { + horizontal: { expected: true, preventDefault: false, detail: -5 }, + vertical: { expected: true, preventDefault: false, detail: -4 } } + }, + { description: "Pixel only device's top-left wheel event by pixels (-5.3/-4.9 - 0/0) #2", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: -5.3, deltaY: -4.9, deltaZ: 0.0, isMomentum: false, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isNoLineOrPageDelta: true, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: -5.3, deltaY: -4.9, deltaZ: 0.0 + }, + DOMMouseScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: false, preventDefault: false, detail: 0 } }, + MozMousePixelScroll: { + horizontal: { expected: true, preventDefault: false, detail: -5 }, + vertical: { expected: true, preventDefault: false, detail: -4 } } + }, + { description: "Pixel only device's bottom-left wheel event by pixels (-5.3/4.9 - 0/0) #4", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: -5.3, deltaY: 4.9, deltaZ: 0.0, isMomentum: false, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isNoLineOrPageDelta: true, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: -5.3, deltaY: 4.9, deltaZ: 0.0 + }, + DOMMouseScroll: { + horizontal: { expected: true, preventDefault: false, detail: -1 }, + vertical: { expected: false, preventDefault: false, detail: 0 } }, + MozMousePixelScroll: { + horizontal: { expected: true, preventDefault: false, detail: -5 }, + vertical: { expected: true, preventDefault: false, detail: 4 } } + }, + // the accumulated Y should be 0 here, but X shouldn't be reset. + { description: "Pixel only device's top-left wheel event by pixels (-5.3/-4.9 - 0/0) #5", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: -5.3, deltaY: -4.9, deltaZ: 0.0, isMomentum: false, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isNoLineOrPageDelta: true, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: -5.3, deltaY: -4.9, deltaZ: 0.0 + }, + DOMMouseScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: false, preventDefault: false, detail: 0 } }, + MozMousePixelScroll: { + horizontal: { expected: true, preventDefault: false, detail: -5 }, + vertical: { expected: true, preventDefault: false, detail: -4 } } + }, + + // Simple line scroll tests. + { description: "Simple horizontal wheel event by lines (1.0 - 1) #1", + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: 1.0, deltaY: 0.0, deltaZ: 0.0, isMomentum: false, + lineOrPageDeltaX: 1, lineOrPageDeltaY: 0, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: 1.0, deltaY: 0.0, deltaZ: 0.0 + }, + DOMMouseScroll: { + horizontal: { expected: true, preventDefault: false, detail: 1 }, + vertical: { expected: false, preventDefault: false, detail: 0 } }, + MozMousePixelScroll: { + horizontal: { expected: true, preventDefault: false, detail: gHorizontalLine }, + vertical: { expected: false, preventDefault: false, detail: 0 } } + }, + { description: "Simple horizontal wheel event by lines (1.0 - 1) #2", + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: 1.0, deltaY: 0.0, deltaZ: 0.0, isMomentum: false, + lineOrPageDeltaX: 1, lineOrPageDeltaY: 0, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: 1.0, deltaY: 0.0, deltaZ: 0.0 + }, + DOMMouseScroll: { + horizontal: { expected: true, preventDefault: false, detail: 1 }, + vertical: { expected: false, preventDefault: false, detail: 0 } }, + MozMousePixelScroll: { + horizontal: { expected: true, preventDefault: false, detail: gHorizontalLine }, + vertical: { expected: false, preventDefault: false, detail: 0 } } + }, + + { description: "Simple horizontal wheel event by lines (-1.0 - -1) #1", + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: -1.0, deltaY: 0.0, deltaZ: 0.0, isMomentum: false, + lineOrPageDeltaX: -1, lineOrPageDeltaY: 0, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: -1.0, deltaY: 0.0, deltaZ: 0.0 + }, + DOMMouseScroll: { + horizontal: { expected: true, preventDefault: false, detail: -1 }, + vertical: { expected: false, preventDefault: false, detail: 0 } }, + MozMousePixelScroll: { + horizontal: { expected: true, preventDefault: false, detail: -gHorizontalLine }, + vertical: { expected: false, preventDefault: false, detail: 0 } } + }, + { description: "Simple horizontal wheel event by lines (-1.0 - -1) #2", + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: -1.0, deltaY: 0.0, deltaZ: 0.0, isMomentum: false, + lineOrPageDeltaX: -1, lineOrPageDeltaY: 0, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: -1.0, deltaY: 0.0, deltaZ: 0.0 + }, + DOMMouseScroll: { + horizontal: { expected: true, preventDefault: false, detail: -1 }, + vertical: { expected: false, preventDefault: false, detail: 0 } }, + MozMousePixelScroll: { + horizontal: { expected: true, preventDefault: false, detail: -gHorizontalLine }, + vertical: { expected: false, preventDefault: false, detail: 0 } } + }, + + { description: "Simple vertical wheel event by lines (-1.0 - -1) #1", + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: 0.0, deltaY: -1.0, deltaZ: 0.0, isMomentum: false, + lineOrPageDeltaX: 0, lineOrPageDeltaY: -1, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: 0.0, deltaY: -1.0, deltaZ: 0.0 + }, + DOMMouseScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: true, preventDefault: false, detail: -1 } }, + MozMousePixelScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: true, preventDefault: false, detail: -gLineHeight } } + }, + { description: "Simple vertical wheel event by lines (-1.0 - -1) #2", + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: 0.0, deltaY: -1.0, deltaZ: 0.0, isMomentum: false, + lineOrPageDeltaX: 0, lineOrPageDeltaY: -1, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: 0.0, deltaY: -1.0, deltaZ: 0.0 + }, + DOMMouseScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: true, preventDefault: false, detail: -1 } }, + MozMousePixelScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: true, preventDefault: false, detail: -gLineHeight } } + }, + + { description: "Simple vertical wheel event by lines (1.0 - 1) #1", + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: 0.0, deltaY: 1.0, deltaZ: 0.0, isMomentum: false, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 1, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: 0.0, deltaY: 1.0, deltaZ: 0.0 + }, + DOMMouseScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: true, preventDefault: false, detail: 1 } }, + MozMousePixelScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: true, preventDefault: false, detail: gLineHeight } } + }, + { description: "Simple vertical wheel event by lines (1.0 - 1) #2", + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: 0.0, deltaY: 1.0, deltaZ: 0.0, isMomentum: false, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 1, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: 0.0, deltaY: 1.0, deltaZ: 0.0 + }, + DOMMouseScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: true, preventDefault: false, detail: 1 } }, + MozMousePixelScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: true, preventDefault: false, detail: gLineHeight } } + }, + + // high resolution line scroll + { description: "High resolution horizontal wheel event by lines (0.333... - 0) #1", + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: 1.0 / 3, deltaY: 0.0, deltaZ: 0.0, isMomentum: false, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: 1.0 / 3, deltaY: 0.0, deltaZ: 0.0 + }, + DOMMouseScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: false, preventDefault: false, detail: 0 } }, + MozMousePixelScroll: { + horizontal: { expected: true, preventDefault: false, detail: Math.floor(gHorizontalLine / 3) }, + vertical: { expected: false, preventDefault: false, detail: 0 } } + }, + { description: "High resolution horizontal wheel event by lines (0.333... - 0) #2", + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: 1.0 / 3, deltaY: 0.0, deltaZ: 0.0, isMomentum: false, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: 1.0 / 3, deltaY: 0.0, deltaZ: 0.0 + }, + DOMMouseScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: false, preventDefault: false, detail: 0 } }, + MozMousePixelScroll: { + horizontal: { expected: true, preventDefault: false, detail: Math.floor(gHorizontalLine / 3) }, + vertical: { expected: false, preventDefault: false, detail: 0 } } + }, + { description: "High resolution horizontal wheel event by lines (0.333... - 1) #3", + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: 1.0 / 3, deltaY: 0.0, deltaZ: 0.0, isMomentum: false, + lineOrPageDeltaX: 1, lineOrPageDeltaY: 0, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: 1.0 / 3, deltaY: 0.0, deltaZ: 0.0 + }, + DOMMouseScroll: { + horizontal: { expected: true, preventDefault: false, detail: 1 }, + vertical: { expected: false, preventDefault: false, detail: 0 } }, + MozMousePixelScroll: { + horizontal: { expected: true, preventDefault: false, detail: Math.floor(gHorizontalLine / 3) }, + vertical: { expected: false, preventDefault: false, detail: 0 } } + }, + + { description: "High resolution horizontal wheel event by lines (-0.333... - 0) #1", + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: -1.0 / 3, deltaY: 0.0, deltaZ: 0.0, isMomentum: false, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: -1.0 / 3, deltaY: 0.0, deltaZ: 0.0 + }, + DOMMouseScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: false, preventDefault: false, detail: 0 } }, + MozMousePixelScroll: { + horizontal: { expected: true, preventDefault: false, detail: -Math.floor(gHorizontalLine / 3) }, + vertical: { expected: false, preventDefault: false, detail: 0 } } + }, + { description: "High resolution horizontal wheel event by lines (-0.333... - 0) #2", + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: -1.0 / 3, deltaY: 0.0, deltaZ: 0.0, isMomentum: false, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: -1.0 / 3, deltaY: 0.0, deltaZ: 0.0 + }, + DOMMouseScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: false, preventDefault: false, detail: 0 } }, + MozMousePixelScroll: { + horizontal: { expected: true, preventDefault: false, detail: -Math.floor(gHorizontalLine / 3) }, + vertical: { expected: false, preventDefault: false, detail: 0 } } + }, + { description: "High resolution horizontal wheel event by lines (-0.333... - -1) #3", + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: -1.0 / 3, deltaY: 0.0, deltaZ: 0.0, isMomentum: false, + lineOrPageDeltaX: -1, lineOrPageDeltaY: 0, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: -1.0 / 3, deltaY: 0.0, deltaZ: 0.0 + }, + DOMMouseScroll: { + horizontal: { expected: true, preventDefault: false, detail: -1 }, + vertical: { expected: false, preventDefault: false, detail: 0 } }, + MozMousePixelScroll: { + horizontal: { expected: true, preventDefault: false, detail: -Math.floor(gHorizontalLine / 3) }, + vertical: { expected: false, preventDefault: false, detail: 0 } } + }, + + { description: "High resolution vertical wheel event by lines (0.333... - 0) #1", + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: 0.0, deltaY: 1.0 / 3, deltaZ: 0.0, isMomentum: false, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: 0.0, deltaY: 1.0 / 3, deltaZ: 0.0 + }, + DOMMouseScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: false, preventDefault: false, detail: 0 } }, + MozMousePixelScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: true, preventDefault: false, detail: Math.floor(gLineHeight / 3) } } + }, + { description: "High resolution vertical wheel event by lines (0.333... - 0) #2", + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: 0.0, deltaY: 1.0 / 3, deltaZ: 0.0, isMomentum: false, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: 0.0, deltaY: 1.0 / 3, deltaZ: 0.0 + }, + DOMMouseScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: false, preventDefault: false, detail: 0 } }, + MozMousePixelScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: true, preventDefault: false, detail: Math.floor(gLineHeight / 3) } } + }, + { description: "High resolution vertical wheel event by lines (0.333... - 1) #3", + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: 0.0, deltaY: 1.0 / 3, deltaZ: 0.0, isMomentum: false, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 1, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: 0.0, deltaY: 1.0 / 3, deltaZ: 0.0 + }, + DOMMouseScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: true, preventDefault: false, detail: 1 } }, + MozMousePixelScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: true, preventDefault: false, detail: Math.floor(gLineHeight / 3) } } + }, + + { description: "High resolution vertical wheel event by lines (-0.333... - 0) #1", + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: 0.0, deltaY: -1.0 / 3, deltaZ: 0.0, isMomentum: false, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: 0.0, deltaY: -1.0 / 3, deltaZ: 0.0 + }, + DOMMouseScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: false, preventDefault: false, detail: 0 } }, + MozMousePixelScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: true, preventDefault: false, detail: -Math.floor(gLineHeight / 3) } } + }, + { description: "High resolution vertical wheel event by lines (-0.333... - 0) #2", + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: 0.0, deltaY: -1.0 / 3, deltaZ: 0.0, isMomentum: false, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: 0.0, deltaY: -1.0 / 3, deltaZ: 0.0 + }, + DOMMouseScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: false, preventDefault: false, detail: 0 } }, + MozMousePixelScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: true, preventDefault: false, detail: -Math.floor(gLineHeight / 3) } } + }, + { description: "High resolution vertical wheel event by lines (-0.333... - -1) #3", + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: 0.0, deltaY: -1.0 / 3, deltaZ: 0.0, isMomentum: false, + lineOrPageDeltaX: 0, lineOrPageDeltaY: -1, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: 0.0, deltaY: -1.0 / 3, deltaZ: 0.0 + }, + DOMMouseScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: true, preventDefault: false, detail: -1 } }, + MozMousePixelScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: true, preventDefault: false, detail: -Math.floor(gLineHeight / 3) } } + }, + + // Oblique line scroll + { description: "Oblique wheel event by lines (-1.0/2.0 - -1/2)", + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: -1.0, deltaY: 2.0, deltaZ: 0.0, isMomentum: false, + lineOrPageDeltaX: -1, lineOrPageDeltaY: 2, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: -1.0, deltaY: 2.0, deltaZ: 0.0 + }, + DOMMouseScroll: { + horizontal: { expected: true, preventDefault: false, detail: -1 }, + vertical: { expected: true, preventDefault: false, detail: 2 } }, + MozMousePixelScroll: { + horizontal: { expected: true, preventDefault: false, detail: -gHorizontalLine }, + vertical: { expected: true, preventDefault: false, detail: gLineHeight * 2 } } + }, + + { description: "Oblique wheel event by lines (1.0/-2.0 - 1/-2)", + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: 1.0, deltaY: -2.0, deltaZ: 0.0, isMomentum: false, + lineOrPageDeltaX: 1, lineOrPageDeltaY: -2, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: 1.0, deltaY: -2.0, deltaZ: 0.0 + }, + DOMMouseScroll: { + horizontal: { expected: true, preventDefault: false, detail: 1 }, + vertical: { expected: true, preventDefault: false, detail: -2 } }, + MozMousePixelScroll: { + horizontal: { expected: true, preventDefault: false, detail: gHorizontalLine }, + vertical: { expected: true, preventDefault: false, detail: -gLineHeight * 2 } } + }, + + { description: "High resolution oblique wheel event by lines (0.5/0.333.../-0.8 - 0/0) #1", + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: 0.5, deltaY: 1.0 / 3, deltaZ: -0.8, isMomentum: false, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: 0.5, deltaY: 1.0 / 3, deltaZ: -0.8 + }, + DOMMouseScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: false, preventDefault: false, detail: 0 } }, + MozMousePixelScroll: { + horizontal: { expected: true, preventDefault: false, detail: Math.floor(gHorizontalLine / 2) }, + vertical: { expected: true, preventDefault: false, detail: Math.floor(gLineHeight / 3) } } + }, + { description: "High resolution oblique wheel event by lines (0.5/0.333.../-0.8 - 1/0) #2", + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: 0.5, deltaY: 1.0 / 3, deltaZ: -0.8, isMomentum: false, + lineOrPageDeltaX: 1, lineOrPageDeltaY: 0, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: 0.5, deltaY: 1.0 / 3, deltaZ: -0.8 + }, + DOMMouseScroll: { + horizontal: { expected: true, preventDefault: false, detail: 1 }, + vertical: { expected: false, preventDefault: false, detail: 0 } }, + MozMousePixelScroll: { + horizontal: { expected: true, preventDefault: false, detail: Math.floor(gHorizontalLine / 2) }, + vertical: { expected: true, preventDefault: false, detail: Math.floor(gLineHeight / 3) } } + }, + { description: "High resolution oblique wheel event by lines (0.5/0.333.../-0.8 - 0/1) #3", + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: 0.5, deltaY: 1.0 / 3, deltaZ: -0.8, isMomentum: false, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 1, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: 0.5, deltaY: 1.0 / 3, deltaZ: -0.8 + }, + DOMMouseScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: true, preventDefault: false, detail: 1 } }, + MozMousePixelScroll: { + horizontal: { expected: true, preventDefault: false, detail: Math.floor(gHorizontalLine / 2) }, + vertical: { expected: true, preventDefault: false, detail: Math.floor(gLineHeight / 3) } } + }, + + // Simple page scroll tests. + { description: "Simple horizontal wheel event by pages (1.0 - 1) #1", + event: { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: 1.0, deltaY: 0.0, deltaZ: 0.0, isMomentum: false, + lineOrPageDeltaX: 1, lineOrPageDeltaY: 0, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: 1.0, deltaY: 0.0, deltaZ: 0.0 + }, + DOMMouseScroll: { + horizontal: { expected: true, preventDefault: false, detail: UIEvent.SCROLL_PAGE_DOWN }, + vertical: { expected: false, preventDefault: false, detail: 0 } }, + MozMousePixelScroll: { + horizontal: { expected: true, preventDefault: false, detail: gPageWidth }, + vertical: { expected: false, preventDefault: false, detail: 0 } } + }, + { description: "Simple horizontal wheel event by pages (1.0 - 1) #2", + event: { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: 1.0, deltaY: 0.0, deltaZ: 0.0, isMomentum: false, + lineOrPageDeltaX: 1, lineOrPageDeltaY: 0, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: 1.0, deltaY: 0.0, deltaZ: 0.0 + }, + DOMMouseScroll: { + horizontal: { expected: true, preventDefault: false, detail: UIEvent.SCROLL_PAGE_DOWN }, + vertical: { expected: false, preventDefault: false, detail: 0 } }, + MozMousePixelScroll: { + horizontal: { expected: true, preventDefault: false, detail: gPageWidth }, + vertical: { expected: false, preventDefault: false, detail: 0 } } + }, + + { description: "Simple horizontal wheel event by pages (-1.0 - -1) #1", + event: { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: -1.0, deltaY: 0.0, deltaZ: 0.0, isMomentum: false, + lineOrPageDeltaX: -1, lineOrPageDeltaY: 0, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: -1.0, deltaY: 0.0, deltaZ: 0.0 + }, + DOMMouseScroll: { + horizontal: { expected: true, preventDefault: false, detail: UIEvent.SCROLL_PAGE_UP }, + vertical: { expected: false, preventDefault: false, detail: 0 } }, + MozMousePixelScroll: { + horizontal: { expected: true, preventDefault: false, detail: -gPageWidth }, + vertical: { expected: false, preventDefault: false, detail: 0 } } + }, + { description: "Simple horizontal wheel event by pages (-1.0 - -1) #2", + event: { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: -1.0, deltaY: 0.0, deltaZ: 0.0, isMomentum: false, + lineOrPageDeltaX: -1, lineOrPageDeltaY: 0, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: -1.0, deltaY: 0.0, deltaZ: 0.0 + }, + DOMMouseScroll: { + horizontal: { expected: true, preventDefault: false, detail: UIEvent.SCROLL_PAGE_UP }, + vertical: { expected: false, preventDefault: false, detail: 0 } }, + MozMousePixelScroll: { + horizontal: { expected: true, preventDefault: false, detail: -gPageWidth }, + vertical: { expected: false, preventDefault: false, detail: 0 } } + }, + + { description: "Simple vertical wheel event by pages (-1.0 - -1) #1", + event: { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: 0.0, deltaY: -1.0, deltaZ: 0.0, isMomentum: false, + lineOrPageDeltaX: 0, lineOrPageDeltaY: -1, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: 0.0, deltaY: -1.0, deltaZ: 0.0 + }, + DOMMouseScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: true, preventDefault: false, detail: UIEvent.SCROLL_PAGE_UP } }, + MozMousePixelScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: true, preventDefault: false, detail: -gPageHeight } } + }, + { description: "Simple vertical wheel event by pages (-1.0 - -1) #2", + event: { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: 0.0, deltaY: -1.0, deltaZ: 0.0, isMomentum: false, + lineOrPageDeltaX: 0, lineOrPageDeltaY: -1, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: 0.0, deltaY: -1.0, deltaZ: 0.0 + }, + DOMMouseScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: true, preventDefault: false, detail: UIEvent.SCROLL_PAGE_UP } }, + MozMousePixelScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: true, preventDefault: false, detail: -gPageHeight } } + }, + + { description: "Simple vertical wheel event by pages (1.0 - 1) #1", + event: { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: 0.0, deltaY: 1.0, deltaZ: 0.0, isMomentum: false, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 1, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: 0.0, deltaY: 1.0, deltaZ: 0.0 + }, + DOMMouseScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: true, preventDefault: false, detail: UIEvent.SCROLL_PAGE_DOWN } }, + MozMousePixelScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: true, preventDefault: false, detail: gPageHeight } } + }, + { description: "Simple vertical wheel event by pages (1.0 - 1) #2", + event: { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: 0.0, deltaY: 1.0, deltaZ: 0.0, isMomentum: false, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 1, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: 0.0, deltaY: 1.0, deltaZ: 0.0 + }, + DOMMouseScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: true, preventDefault: false, detail: UIEvent.SCROLL_PAGE_DOWN } }, + MozMousePixelScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: true, preventDefault: false, detail: gPageHeight } } + }, + + // high resolution page scroll + { description: "High resolution horizontal wheel event by pages (0.333... - 0) #1", + event: { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: 1.0 / 3, deltaY: 0.0, deltaZ: 0.0, isMomentum: false, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: 1.0 / 3, deltaY: 0.0, deltaZ: 0.0 + }, + DOMMouseScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: false, preventDefault: false, detail: 0 } }, + MozMousePixelScroll: { + horizontal: { expected: true, preventDefault: false, detail: Math.floor(gPageWidth / 3) }, + vertical: { expected: false, preventDefault: false, detail: 0 } } + }, + { description: "High resolution horizontal wheel event by pages (0.333... - 0) #2", + event: { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: 1.0 / 3, deltaY: 0.0, deltaZ: 0.0, isMomentum: false, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: 1.0 / 3, deltaY: 0.0, deltaZ: 0.0 + }, + DOMMouseScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: false, preventDefault: false, detail: 0 } }, + MozMousePixelScroll: { + horizontal: { expected: true, preventDefault: false, detail: Math.floor(gPageWidth / 3) }, + vertical: { expected: false, preventDefault: false, detail: 0 } } + }, + { description: "High resolution horizontal wheel event by pages (0.333... - 1) #3", + event: { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: 1.0 / 3, deltaY: 0.0, deltaZ: 0.0, isMomentum: false, + lineOrPageDeltaX: 1, lineOrPageDeltaY: 0, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: 1.0 / 3, deltaY: 0.0, deltaZ: 0.0 + }, + DOMMouseScroll: { + horizontal: { expected: true, preventDefault: false, detail: UIEvent.SCROLL_PAGE_DOWN }, + vertical: { expected: false, preventDefault: false, detail: 0 } }, + MozMousePixelScroll: { + horizontal: { expected: true, preventDefault: false, detail: Math.floor(gPageWidth / 3) }, + vertical: { expected: false, preventDefault: false, detail: 0 } } + }, + + { description: "High resolution horizontal wheel event by pages (-0.333... - 0) #1", + event: { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: -1.0 / 3, deltaY: 0.0, deltaZ: 0.0, isMomentum: false, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: -1.0 / 3, deltaY: 0.0, deltaZ: 0.0 + }, + DOMMouseScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: false, preventDefault: false, detail: 0 } }, + MozMousePixelScroll: { + horizontal: { expected: true, preventDefault: false, detail: -Math.floor(gPageWidth / 3) }, + vertical: { expected: false, preventDefault: false, detail: 0 } } + }, + { description: "High resolution horizontal wheel event by pages (-0.333... - 0) #2", + event: { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: -1.0 / 3, deltaY: 0.0, deltaZ: 0.0, isMomentum: false, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: -1.0 / 3, deltaY: 0.0, deltaZ: 0.0 + }, + DOMMouseScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: false, preventDefault: false, detail: 0 } }, + MozMousePixelScroll: { + horizontal: { expected: true, preventDefault: false, detail: -Math.floor(gPageWidth / 3) }, + vertical: { expected: false, preventDefault: false, detail: 0 } } + }, + { description: "High resolution horizontal wheel event by pages (-0.333... - -1) #3", + event: { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: -1.0 / 3, deltaY: 0.0, deltaZ: 0.0, isMomentum: false, + lineOrPageDeltaX: -1, lineOrPageDeltaY: 0, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: -1.0 / 3, deltaY: 0.0, deltaZ: 0.0 + }, + DOMMouseScroll: { + horizontal: { expected: true, preventDefault: false, detail: UIEvent.SCROLL_PAGE_UP }, + vertical: { expected: false, preventDefault: false, detail: 0 } }, + MozMousePixelScroll: { + horizontal: { expected: true, preventDefault: false, detail: -Math.floor(gPageWidth / 3) }, + vertical: { expected: false, preventDefault: false, detail: 0 } } + }, + + { description: "High resolution vertical wheel event by pages (0.333... - 0) #1", + event: { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: 0.0, deltaY: 1.0 / 3, deltaZ: 0.0, isMomentum: false, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: 0.0, deltaY: 1.0 / 3, deltaZ: 0.0 + }, + DOMMouseScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: false, preventDefault: false, detail: 0 } }, + MozMousePixelScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: true, preventDefault: false, detail: Math.floor(gPageHeight / 3) } } + }, + { description: "High resolution vertical wheel event by pages (0.333... - 0) #2", + event: { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: 0.0, deltaY: 1.0 / 3, deltaZ: 0.0, isMomentum: false, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: 0.0, deltaY: 1.0 / 3, deltaZ: 0.0 + }, + DOMMouseScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: false, preventDefault: false, detail: 0 } }, + MozMousePixelScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: true, preventDefault: false, detail: Math.floor(gPageHeight / 3) } } + }, + { description: "High resolution vertical wheel event by pages (0.333... - 1) #3", + event: { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: 0.0, deltaY: 1.0 / 3, deltaZ: 0.0, isMomentum: false, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 1, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: 0.0, deltaY: 1.0 / 3, deltaZ: 0.0 + }, + DOMMouseScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: true, preventDefault: false, detail: UIEvent.SCROLL_PAGE_DOWN } }, + MozMousePixelScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: true, preventDefault: false, detail: Math.floor(gPageHeight / 3) } } + }, + + { description: "High resolution vertical wheel event by pages (-0.333... - 0) #1", + event: { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: 0.0, deltaY: -1.0 / 3, deltaZ: 0.0, isMomentum: false, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: 0.0, deltaY: -1.0 / 3, deltaZ: 0.0 + }, + DOMMouseScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: false, preventDefault: false, detail: 0 } }, + MozMousePixelScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: true, preventDefault: false, detail: -Math.floor(gPageHeight / 3) } } + }, + { description: "High resolution vertical wheel event by pages (-0.333... - 0) #2", + event: { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: 0.0, deltaY: -1.0 / 3, deltaZ: 0.0, isMomentum: false, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: 0.0, deltaY: -1.0 / 3, deltaZ: 0.0 + }, + DOMMouseScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: false, preventDefault: false, detail: 0 } }, + MozMousePixelScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: true, preventDefault: false, detail: -Math.floor(gPageHeight / 3) } } + }, + { description: "High resolution vertical wheel event by pages (-0.333... - -1) #3", + event: { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: 0.0, deltaY: -1.0 / 3, deltaZ: 0.0, isMomentum: false, + lineOrPageDeltaX: 0, lineOrPageDeltaY: -1, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: 0.0, deltaY: -1.0 / 3, deltaZ: 0.0 + }, + DOMMouseScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: true, preventDefault: false, detail: UIEvent.SCROLL_PAGE_UP } }, + MozMousePixelScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: true, preventDefault: false, detail: -Math.floor(gPageHeight / 3) } } + }, + + // Oblique page scroll + { description: "Oblique wheel event by pages (-1.0/2.0 - -1/2)", + event: { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: -1.0, deltaY: 2.0, deltaZ: 0.0, isMomentum: false, + lineOrPageDeltaX: -1, lineOrPageDeltaY: 2, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: -1.0, deltaY: 2.0, deltaZ: 0.0 + }, + DOMMouseScroll: { + horizontal: { expected: true, preventDefault: false, detail: UIEvent.SCROLL_PAGE_UP }, + vertical: { expected: true, preventDefault: false, detail: UIEvent.SCROLL_PAGE_DOWN } }, + MozMousePixelScroll: { + horizontal: { expected: true, preventDefault: false, detail: -gPageWidth }, + vertical: { expected: true, preventDefault: false, detail: gPageHeight * 2 } } + }, + + { description: "Oblique wheel event by pages (1.0/-2.0 - 1/-2)", + event: { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: 1.0, deltaY: -2.0, deltaZ: 0.0, isMomentum: false, + lineOrPageDeltaX: 1, lineOrPageDeltaY: -2, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: 1.0, deltaY: -2.0, deltaZ: 0.0 + }, + DOMMouseScroll: { + horizontal: { expected: true, preventDefault: false, detail: UIEvent.SCROLL_PAGE_DOWN }, + vertical: { expected: true, preventDefault: false, detail: UIEvent.SCROLL_PAGE_UP } }, + MozMousePixelScroll: { + horizontal: { expected: true, preventDefault: false, detail: gPageWidth }, + vertical: { expected: true, preventDefault: false, detail: -gPageHeight * 2 } } + }, + + { description: "High resolution oblique wheel event by pages (0.5/0.333.../-0.8 - 0/0) #1", + event: { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: 0.5, deltaY: 1.0 / 3, deltaZ: -0.8, isMomentum: false, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: 0.5, deltaY: 1.0 / 3, deltaZ: -0.8 + }, + DOMMouseScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: false, preventDefault: false, detail: 0 } }, + MozMousePixelScroll: { + horizontal: { expected: true, preventDefault: false, detail: Math.floor(gPageWidth / 2) }, + vertical: { expected: true, preventDefault: false, detail: Math.floor(gPageHeight / 3) } } + }, + { description: "High resolution oblique wheel event by pages (0.5/0.333.../-0.8 - 1/0) #2", + event: { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: 0.5, deltaY: 1.0 / 3, deltaZ: -0.8, isMomentum: false, + lineOrPageDeltaX: 1, lineOrPageDeltaY: 0, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: 0.5, deltaY: 1.0 / 3, deltaZ: -0.8 + }, + DOMMouseScroll: { + horizontal: { expected: true, preventDefault: false, detail: UIEvent.SCROLL_PAGE_DOWN }, + vertical: { expected: false, preventDefault: false, detail: 0 } }, + MozMousePixelScroll: { + horizontal: { expected: true, preventDefault: false, detail: Math.floor(gPageWidth / 2) }, + vertical: { expected: true, preventDefault: false, detail: Math.floor(gPageHeight / 3) } } + }, + { description: "High resolution oblique wheel event by pages (0.5/0.333.../-0.8 - 0/1) #3", + event: { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: 0.5, deltaY: 1.0 / 3, deltaZ: -0.8, isMomentum: false, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 1, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: 0.5, deltaY: 1.0 / 3, deltaZ: -0.8 + }, + DOMMouseScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: true, preventDefault: false, detail: UIEvent.SCROLL_PAGE_DOWN } }, + MozMousePixelScroll: { + horizontal: { expected: true, preventDefault: false, detail: Math.floor(gPageWidth / 2) }, + vertical: { expected: true, preventDefault: false, detail: Math.floor(gPageHeight / 3) } } + }, + + // preventDefault() shouldn't prevent other legacy events. + { description: "preventDefault() shouldn't prevent other legacy events (pixel)", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 16.0, deltaY: 16.0, deltaZ: 16.0, isMomentum: false, + lineOrPageDeltaX: 1, lineOrPageDeltaY: 1, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: 16.0, deltaY: 16.0, deltaZ: 16.0 + }, + DOMMouseScroll: { + horizontal: { expected: true, preventDefault: true, detail: 1 }, + vertical: { expected: true, preventDefault: true, detail: 1 } }, + MozMousePixelScroll: { + horizontal: { expected: true, preventDefault: true, detail: 16 }, + vertical: { expected: true, preventDefault: true, detail: 16 } }, + }, + { description: "preventDefault() shouldn't prevent other legacy events (line)", + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: 1.0, deltaY: 1.0, deltaZ: 1.0, isMomentum: false, + lineOrPageDeltaX: 1, lineOrPageDeltaY: 1, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: 1.0, deltaY: 1.0, deltaZ: 1.0 + }, + DOMMouseScroll: { + horizontal: { expected: true, preventDefault: true, detail: 1 }, + vertical: { expected: true, preventDefault: true, detail: 1 } }, + MozMousePixelScroll: { + horizontal: { expected: true, preventDefault: true, detail: gHorizontalLine }, + vertical: { expected: true, preventDefault: true, detail: gLineHeight } }, + }, + { description: "preventDefault() shouldn't prevent other legacy events (page)", + event: { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: 1.0, deltaY: 1.0, deltaZ: 1.0, isMomentum: false, + lineOrPageDeltaX: 1, lineOrPageDeltaY: 1, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: 1.0, deltaY: 1.0, deltaZ: 1.0 + }, + DOMMouseScroll: { + horizontal: { expected: true, preventDefault: true, detail: UIEvent.SCROLL_PAGE_DOWN }, + vertical: { expected: true, preventDefault: true, detail: UIEvent.SCROLL_PAGE_DOWN } }, + MozMousePixelScroll: { + horizontal: { expected: true, preventDefault: true, detail: gPageWidth }, + vertical: { expected: true, preventDefault: true, detail: gPageHeight } }, + }, + + // If wheel event is consumed by preventDefault(), legacy events are not necessary. + { description: "If wheel event is consumed by preventDefault(), legacy events are not necessary (pixel)", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 16.0, deltaY: 16.0, deltaZ: 16.0, isMomentum: false, + lineOrPageDeltaX: 1, lineOrPageDeltaY: 1, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: true, + deltaX: 16.0, deltaY: 16.0, deltaZ: 16.0 + }, + DOMMouseScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: false, preventDefault: false, detail: 0 } }, + MozMousePixelScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: false, preventDefault: false, detail: 0 } }, + }, + { description: "If wheel event is consumed by preventDefault(), legacy events are not necessary (line)", + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: 1.0, deltaY: 1.0, deltaZ: 1.0, isMomentum: false, + lineOrPageDeltaX: 1, lineOrPageDeltaY: 1, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: true, + deltaX: 1.0, deltaY: 1.0, deltaZ: 1.0 + }, + DOMMouseScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: false, preventDefault: false, detail: 0 } }, + MozMousePixelScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: false, preventDefault: false, detail: 0 } }, + }, + { description: "If wheel event is consumed by preventDefault(), legacy events are not necessary (page)", + event: { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: 1.0, deltaY: 1.0, deltaZ: 1.0, isMomentum: false, + lineOrPageDeltaX: 1, lineOrPageDeltaY: 1, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: true, + deltaX: 1.0, deltaY: 1.0, deltaZ: 1.0 + }, + DOMMouseScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: false, preventDefault: false, detail: 0 } }, + MozMousePixelScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: false, preventDefault: false, detail: 0 } }, + }, + + // modifier key state tests + { description: "modifier key tests (shift, pixel)", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 16.0, deltaY: 16.0, deltaZ: 16.0, isMomentum: false, + lineOrPageDeltaX: 1, lineOrPageDeltaY: 1, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: true, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: 16.0, deltaY: 16.0, deltaZ: 16.0 + }, + DOMMouseScroll: { + horizontal: { expected: true, preventDefault: false, detail: 1 }, + vertical: { expected: true, preventDefault: false, detail: 1 } }, + MozMousePixelScroll: { + horizontal: { expected: true, preventDefault: true, detail: 16 }, + vertical: { expected: true, preventDefault: true, detail: 16 } }, + }, + { description: "modifier key tests (shift, line)", + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: 1.0, deltaY: 1.0, deltaZ: 1.0, isMomentum: false, + lineOrPageDeltaX: 1, lineOrPageDeltaY: 1, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: true, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: 1.0, deltaY: 1.0, deltaZ: 1.0 + }, + DOMMouseScroll: { + horizontal: { expected: true, preventDefault: false, detail: 1 }, + vertical: { expected: true, preventDefault: false, detail: 1 } }, + MozMousePixelScroll: { + horizontal: { expected: true, preventDefault: true, detail: gHorizontalLine }, + vertical: { expected: true, preventDefault: true, detail: gLineHeight } }, + }, + { description: "modifier key tests (shift, page)", + event: { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: 1.0, deltaY: 1.0, deltaZ: 1.0, isMomentum: false, + lineOrPageDeltaX: 1, lineOrPageDeltaY: 1, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: true, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: 1.0, deltaY: 1.0, deltaZ: 1.0 + }, + DOMMouseScroll: { + horizontal: { expected: true, preventDefault: false, detail: UIEvent.SCROLL_PAGE_DOWN }, + vertical: { expected: true, preventDefault: false, detail: UIEvent.SCROLL_PAGE_DOWN } }, + MozMousePixelScroll: { + horizontal: { expected: true, preventDefault: true, detail: gPageWidth }, + vertical: { expected: true, preventDefault: true, detail: gPageHeight } }, + }, + + { description: "modifier key tests (ctrl, pixel)", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 16.0, deltaY: 16.0, deltaZ: 16.0, isMomentum: false, + lineOrPageDeltaX: 1, lineOrPageDeltaY: 1, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: true, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: 16.0, deltaY: 16.0, deltaZ: 16.0 + }, + DOMMouseScroll: { + horizontal: { expected: true, preventDefault: false, detail: 1 }, + vertical: { expected: true, preventDefault: false, detail: 1 } }, + MozMousePixelScroll: { + horizontal: { expected: true, preventDefault: true, detail: 16 }, + vertical: { expected: true, preventDefault: true, detail: 16 } }, + }, + { description: "modifier key tests (ctrl, line)", + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: 1.0, deltaY: 1.0, deltaZ: 1.0, isMomentum: false, + lineOrPageDeltaX: 1, lineOrPageDeltaY: 1, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: true, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: 1.0, deltaY: 1.0, deltaZ: 1.0 + }, + DOMMouseScroll: { + horizontal: { expected: true, preventDefault: false, detail: 1 }, + vertical: { expected: true, preventDefault: false, detail: 1 } }, + MozMousePixelScroll: { + horizontal: { expected: true, preventDefault: true, detail: gHorizontalLine }, + vertical: { expected: true, preventDefault: true, detail: gLineHeight } }, + }, + { description: "modifier key tests (ctrl, page)", + event: { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: 1.0, deltaY: 1.0, deltaZ: 1.0, isMomentum: false, + lineOrPageDeltaX: 1, lineOrPageDeltaY: 1, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: true, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: 1.0, deltaY: 1.0, deltaZ: 1.0 + }, + DOMMouseScroll: { + horizontal: { expected: true, preventDefault: false, detail: UIEvent.SCROLL_PAGE_DOWN }, + vertical: { expected: true, preventDefault: false, detail: UIEvent.SCROLL_PAGE_DOWN } }, + MozMousePixelScroll: { + horizontal: { expected: true, preventDefault: true, detail: gPageWidth }, + vertical: { expected: true, preventDefault: true, detail: gPageHeight } }, + }, + + { description: "modifier key tests (alt, pixel)", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 16.0, deltaY: 16.0, deltaZ: 16.0, isMomentum: false, + lineOrPageDeltaX: 1, lineOrPageDeltaY: 1, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: true, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: 16.0, deltaY: 16.0, deltaZ: 16.0 + }, + DOMMouseScroll: { + horizontal: { expected: true, preventDefault: false, detail: 1 }, + vertical: { expected: true, preventDefault: false, detail: 1 } }, + MozMousePixelScroll: { + horizontal: { expected: true, preventDefault: true, detail: 16 }, + vertical: { expected: true, preventDefault: true, detail: 16 } }, + }, + { description: "modifier key tests (alt, line)", + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: 1.0, deltaY: 1.0, deltaZ: 1.0, isMomentum: false, + lineOrPageDeltaX: 1, lineOrPageDeltaY: 1, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: true, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: 1.0, deltaY: 1.0, deltaZ: 1.0 + }, + DOMMouseScroll: { + horizontal: { expected: true, preventDefault: false, detail: 1 }, + vertical: { expected: true, preventDefault: false, detail: 1 } }, + MozMousePixelScroll: { + horizontal: { expected: true, preventDefault: true, detail: gHorizontalLine }, + vertical: { expected: true, preventDefault: true, detail: gLineHeight } }, + }, + { description: "modifier key tests without content checking mode (alt, line)", + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: 1.0, deltaY: 1.0, deltaZ: 1.0, isMomentum: false, + lineOrPageDeltaX: 1, lineOrPageDeltaY: 1, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: true, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + skipDeltaModeCheck: true, + deltaX: gHorizontalLine, + deltaY: gLineHeight, + deltaZ: Math.max(gHorizontalLine, gLineHeight), + }, + DOMMouseScroll: { + horizontal: { expected: true, preventDefault: false, detail: 1 }, + vertical: { expected: true, preventDefault: false, detail: 1 } }, + MozMousePixelScroll: { + horizontal: { expected: true, preventDefault: true, detail: gHorizontalLine }, + vertical: { expected: true, preventDefault: true, detail: gLineHeight } }, + }, + { description: "modifier key tests (alt, page)", + event: { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: 1.0, deltaY: 1.0, deltaZ: 1.0, isMomentum: false, + lineOrPageDeltaX: 1, lineOrPageDeltaY: 1, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: true, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: 1.0, deltaY: 1.0, deltaZ: 1.0 + }, + DOMMouseScroll: { + horizontal: { expected: true, preventDefault: false, detail: UIEvent.SCROLL_PAGE_DOWN }, + vertical: { expected: true, preventDefault: false, detail: UIEvent.SCROLL_PAGE_DOWN } }, + MozMousePixelScroll: { + horizontal: { expected: true, preventDefault: true, detail: gPageWidth }, + vertical: { expected: true, preventDefault: true, detail: gPageHeight } }, + }, + + { description: "modifier key tests (meta, pixel)", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 16.0, deltaY: 16.0, deltaZ: 16.0, isMomentum: false, + lineOrPageDeltaX: 1, lineOrPageDeltaY: 1, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: true }, + wheel: { + expected: true, preventDefault: false, + deltaX: 16.0, deltaY: 16.0, deltaZ: 16.0 + }, + DOMMouseScroll: { + horizontal: { expected: true, preventDefault: false, detail: 1 }, + vertical: { expected: true, preventDefault: false, detail: 1 } }, + MozMousePixelScroll: { + horizontal: { expected: true, preventDefault: true, detail: 16 }, + vertical: { expected: true, preventDefault: true, detail: 16 } }, + }, + { description: "modifier key tests (meta, line)", + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: 1.0, deltaY: 1.0, deltaZ: 1.0, isMomentum: false, + lineOrPageDeltaX: 1, lineOrPageDeltaY: 1, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: true }, + wheel: { + expected: true, preventDefault: false, + deltaX: 1.0, deltaY: 1.0, deltaZ: 1.0 + }, + DOMMouseScroll: { + horizontal: { expected: true, preventDefault: false, detail: 1 }, + vertical: { expected: true, preventDefault: false, detail: 1 } }, + MozMousePixelScroll: { + horizontal: { expected: true, preventDefault: true, detail: gHorizontalLine }, + vertical: { expected: true, preventDefault: true, detail: gLineHeight } }, + }, + { description: "modifier key tests (meta, page)", + event: { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: 1.0, deltaY: 1.0, deltaZ: 1.0, isMomentum: true, + lineOrPageDeltaX: 1, lineOrPageDeltaY: 1, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: 1.0, deltaY: 1.0, deltaZ: 1.0 + }, + DOMMouseScroll: { + horizontal: { expected: true, preventDefault: false, detail: UIEvent.SCROLL_PAGE_DOWN }, + vertical: { expected: true, preventDefault: false, detail: UIEvent.SCROLL_PAGE_DOWN } }, + MozMousePixelScroll: { + horizontal: { expected: true, preventDefault: true, detail: gPageWidth }, + vertical: { expected: true, preventDefault: true, detail: gPageHeight } }, + }, + + // Momentum scroll should cause legacy events. + { description: "Momentum scroll should cause legacy events (pixel, not momentum)", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 16.0, deltaY: 16.0, deltaZ: 16.0, isMomentum: false, + lineOrPageDeltaX: 1, lineOrPageDeltaY: 1, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: 16.0, deltaY: 16.0, deltaZ: 16.0 + }, + DOMMouseScroll: { + horizontal: { expected: true, preventDefault: false, detail: 1 }, + vertical: { expected: true, preventDefault: false, detail: 1 } }, + MozMousePixelScroll: { + horizontal: { expected: true, preventDefault: false, detail: 16 }, + vertical: { expected: true, preventDefault: false, detail: 16 } }, + }, + { description: "Momentum scroll should cause legacy events (pixel, momentum)", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 16.0, deltaY: 16.0, deltaZ: 16.0, isMomentum: true, + lineOrPageDeltaX: 1, lineOrPageDeltaY: 1, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: 16.0, deltaY: 16.0, deltaZ: 16.0 + }, + DOMMouseScroll: { + horizontal: { expected: true, preventDefault: false, detail: 1 }, + vertical: { expected: true, preventDefault: false, detail: 1 } }, + MozMousePixelScroll: { + horizontal: { expected: true, preventDefault: false, detail: 16 }, + vertical: { expected: true, preventDefault: false, detail: 16 } }, + }, + { description: "Momentum scroll should cause legacy events (line, not momentum)", + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: 1.0, deltaY: 1.0, deltaZ: 1.0, isMomentum: false, + lineOrPageDeltaX: 1, lineOrPageDeltaY: 1, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: 1.0, deltaY: 1.0, deltaZ: 1.0 + }, + DOMMouseScroll: { + horizontal: { expected: true, preventDefault: false, detail: 1 }, + vertical: { expected: true, preventDefault: false, detail: 1 } }, + MozMousePixelScroll: { + horizontal: { expected: true, preventDefault: false, detail: gHorizontalLine }, + vertical: { expected: true, preventDefault: false, detail: gLineHeight } }, + }, + { description: "Momentum scroll should cause legacy events (line, momentum)", + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: 1.0, deltaY: 1.0, deltaZ: 1.0, isMomentum: true, + lineOrPageDeltaX: 1, lineOrPageDeltaY: 1, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: 1.0, deltaY: 1.0, deltaZ: 1.0 + }, + DOMMouseScroll: { + horizontal: { expected: true, preventDefault: false, detail: 1 }, + vertical: { expected: true, preventDefault: false, detail: 1 } }, + MozMousePixelScroll: { + horizontal: { expected: true, preventDefault: false, detail: gHorizontalLine }, + vertical: { expected: true, preventDefault: false, detail: gLineHeight } }, + }, + { description: "Momentum scroll should cause legacy events (page, not momentum)", + event: { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: 1.0, deltaY: 1.0, deltaZ: 1.0, isMomentum: false, + lineOrPageDeltaX: 1, lineOrPageDeltaY: 1, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: 1.0, deltaY: 1.0, deltaZ: 1.0 + }, + DOMMouseScroll: { + horizontal: { expected: true, preventDefault: false, detail: UIEvent.SCROLL_PAGE_DOWN }, + vertical: { expected: true, preventDefault: false, detail: UIEvent.SCROLL_PAGE_DOWN } }, + MozMousePixelScroll: { + horizontal: { expected: true, preventDefault: false, detail: gPageWidth }, + vertical: { expected: true, preventDefault: false, detail: gPageHeight } }, + }, + { description: "Momentum scroll should cause legacy events (page, momentum)", + event: { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: 1.0, deltaY: 1.0, deltaZ: 1.0, isMomentum: true, + lineOrPageDeltaX: 1, lineOrPageDeltaY: 1, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: 1.0, deltaY: 1.0, deltaZ: 1.0 + }, + DOMMouseScroll: { + horizontal: { expected: true, preventDefault: false, detail: UIEvent.SCROLL_PAGE_DOWN }, + vertical: { expected: true, preventDefault: false, detail: UIEvent.SCROLL_PAGE_DOWN } }, + MozMousePixelScroll: { + horizontal: { expected: true, preventDefault: false, detail: gPageWidth }, + vertical: { expected: true, preventDefault: false, detail: gPageHeight } }, + }, + + // Tests for accumulation delta when delta_multiplier_is customized. + { description: "lineOrPageDelta should be recomputed by ESM (pixel) #1", + prepare () { + SpecialPowers.pushPrefEnv({"set": [["mousewheel.default.delta_multiplier_x", 200], + ["mousewheel.default.delta_multiplier_y", 300]]}, + continueTest); + }, + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: gHorizontalLine / 4, deltaY: gLineHeight / 8, deltaZ: 0, + lineOrPageDeltaX: 3, lineOrPageDeltaY: 5, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: gHorizontalLine / 4 * 2, deltaY: gLineHeight / 8 * 3, deltaZ: 0 + }, + DOMMouseScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: false, preventDefault: false, detail: 0 } }, + MozMousePixelScroll: { + horizontal: { expected: true, preventDefault: false, detail: Math.floor(gHorizontalLine / 4 * 2) }, + vertical: { expected: true, preventDefault: false, detail: Math.floor(gLineHeight / 8 * 3) } }, + }, + { description: "lineOrPageDelta should be recomputed by ESM (pixel) #2", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: gHorizontalLine / 4 + 1, deltaY: gLineHeight / 8 + 1, deltaZ: 0, + lineOrPageDeltaX: 3, lineOrPageDeltaY: 5, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: (gHorizontalLine / 4 + 1) * 2, deltaY: (gLineHeight / 8 + 1) * 3, deltaZ: 0 + }, + DOMMouseScroll: { + horizontal: { expected: true, preventDefault: false, detail: 1 }, + vertical: { expected: false, preventDefault: false, detail: 0 } }, + MozMousePixelScroll: { + horizontal: { expected: true, preventDefault: false, detail: Math.floor((gHorizontalLine / 4 + 1) * 2) }, + vertical: { expected: true, preventDefault: false, detail: Math.floor((gLineHeight / 8 + 1) * 3) } }, + }, + { description: "lineOrPageDelta should be recomputed by ESM (pixel) #3", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: gHorizontalLine / 4 + 1, deltaY: gLineHeight / 8 + 1, deltaZ: 0, + lineOrPageDeltaX: 3, lineOrPageDeltaY: 5, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: (gHorizontalLine / 4 + 1) * 2, deltaY: (gLineHeight / 8 + 1) * 3, deltaZ: 0 + }, + DOMMouseScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: true, preventDefault: false, detail: 1 } }, + MozMousePixelScroll: { + horizontal: { expected: true, preventDefault: false, detail: Math.floor((gHorizontalLine / 4 + 1) * 2) }, + vertical: { expected: true, preventDefault: false, detail: Math.floor((gLineHeight / 8 + 1) * 3) } }, + finished () { + SpecialPowers.pushPrefEnv({"set": [["mousewheel.default.delta_multiplier_x", 100], + ["mousewheel.default.delta_multiplier_y", 100]]}, + continueTest); + }, + }, + + { description: "lineOrPageDelta should be recomputed by ESM (pixel, negative, shift) #1", + prepare () { + SpecialPowers.pushPrefEnv({"set": [["mousewheel.with_shift.delta_multiplier_x", 200], + ["mousewheel.with_shift.delta_multiplier_y", 300]]}, + continueTest); + }, + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: -gHorizontalLine / 4, deltaY: -gLineHeight / 8, deltaZ: 0, + lineOrPageDeltaX: -3, lineOrPageDeltaY: -5, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: true, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: -gHorizontalLine / 4 * 2, deltaY: -gLineHeight / 8 * 3, deltaZ: 0 + }, + DOMMouseScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: false, preventDefault: false, detail: 0 } }, + MozMousePixelScroll: { + horizontal: { expected: true, preventDefault: false, detail: Math.ceil(-gHorizontalLine / 4 * 2) }, + vertical: { expected: true, preventDefault: false, detail: Math.ceil(-gLineHeight / 8 * 3) } }, + }, + { description: "lineOrPageDelta should be recomputed by ESM (pixel, negative, shift) #2", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: -(gHorizontalLine / 4 + 1), deltaY: -(gLineHeight / 8 + 1), deltaZ: 0, + lineOrPageDeltaX: -3, lineOrPageDeltaY: -5, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: true, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: -(gHorizontalLine / 4 + 1) * 2, deltaY: -(gLineHeight / 8 + 1) * 3, deltaZ: 0 + }, + DOMMouseScroll: { + horizontal: { expected: true, preventDefault: false, detail: -1 }, + vertical: { expected: false, preventDefault: false, detail: 0 } }, + MozMousePixelScroll: { + horizontal: { expected: true, preventDefault: false, detail: Math.ceil(-(gHorizontalLine / 4 + 1) * 2) }, + vertical: { expected: true, preventDefault: false, detail: Math.ceil(-(gLineHeight / 8 + 1) * 3) } }, + }, + { description: "lineOrPageDelta should be recomputed by ESM (pixel, negative, shift) #3", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: -(gHorizontalLine / 4 + 1), deltaY: -(gLineHeight / 8 + 1), deltaZ: 0, + lineOrPageDeltaX: 3, lineOrPageDeltaY: 5, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: true, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: -(gHorizontalLine / 4 + 1) * 2, deltaY: -(gLineHeight / 8 + 1) * 3, deltaZ: 0 + }, + DOMMouseScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: true, preventDefault: false, detail: -1 } }, + MozMousePixelScroll: { + horizontal: { expected: true, preventDefault: false, detail: Math.ceil(-(gHorizontalLine / 4 + 1) * 2) }, + vertical: { expected: true, preventDefault: false, detail: Math.ceil(-(gLineHeight / 8 + 1) * 3) } }, + finished () { + SpecialPowers.pushPrefEnv({"set": [["mousewheel.with_shift.delta_multiplier_x", 100], + ["mousewheel.with_shift.delta_multiplier_y", 100]]}, + continueTest); + }, + }, + + { description: "lineOrPageDelta should be recomputed by ESM (line) #1", + prepare () { + SpecialPowers.pushPrefEnv({"set": [["mousewheel.default.delta_multiplier_x", 200], + ["mousewheel.default.delta_multiplier_y", 100]]}, + continueTest); + }, + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: 0.3, deltaY: 0.4, deltaZ: 0, + lineOrPageDeltaX: 3, lineOrPageDeltaY: 5, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: 0.6, deltaY: 0.4, deltaZ: 0 + }, + DOMMouseScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: false, preventDefault: false, detail: 0 } }, + MozMousePixelScroll: { + horizontal: { expected: true, preventDefault: false, detail: Math.floor(gHorizontalLine * 0.6) }, + vertical: { expected: true, preventDefault: false, detail: Math.floor(gLineHeight * 0.4) } }, + }, + { description: "lineOrPageDelta should be recomputed by ESM (line) #2", + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: 0.3, deltaY: 0.4, deltaZ: 0, + lineOrPageDeltaX: 3, lineOrPageDeltaY: 5, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: 0.6, deltaY: 0.4, deltaZ: 0 + }, + DOMMouseScroll: { + horizontal: { expected: true, preventDefault: false, detail: 1 }, + vertical: { expected: false, preventDefault: false, detail: 0 } }, + MozMousePixelScroll: { + horizontal: { expected: true, preventDefault: false, detail: Math.floor(gHorizontalLine * 0.6) }, + vertical: { expected: true, preventDefault: false, detail: Math.floor(gLineHeight * 0.4) } }, + }, + { description: "lineOrPageDelta should be recomputed by ESM (line) #3", + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: 0.3, deltaY: 0.4, deltaZ: 0, + lineOrPageDeltaX: 3, lineOrPageDeltaY: 5, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: 0.6, deltaY: 0.4, deltaZ: 0 + }, + DOMMouseScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: true, preventDefault: false, detail: 1 } }, + MozMousePixelScroll: { + horizontal: { expected: true, preventDefault: false, detail: Math.floor(gHorizontalLine * 0.6) }, + vertical: { expected: true, preventDefault: false, detail: Math.floor(gLineHeight * 0.4) } }, + finished () { + SpecialPowers.pushPrefEnv({"set": [["mousewheel.default.delta_multiplier_x", 100], + ["mousewheel.default.delta_multiplier_y", 100]]}, + continueTest); + }, + }, + + { description: "lineOrPageDelta should be recomputed by ESM (line, negative) #1", + prepare () { + SpecialPowers.pushPrefEnv({"set": [["mousewheel.default.delta_multiplier_x", 200], + ["mousewheel.default.delta_multiplier_y", -100]]}, + continueTest); + }, + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: -0.3, deltaY: -0.4, deltaZ: 0, + lineOrPageDeltaX: -3, lineOrPageDeltaY: -5, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: -0.6, deltaY: 0.4, deltaZ: 0 + }, + DOMMouseScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: false, preventDefault: false, detail: 0 } }, + MozMousePixelScroll: { + horizontal: { expected: true, preventDefault: false, detail: Math.ceil(gHorizontalLine * -0.6) }, + vertical: { expected: true, preventDefault: false, detail: Math.floor(gLineHeight * 0.4) } }, + }, + { description: "lineOrPageDelta should be recomputed by ESM (line, negative) #2", + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: -0.3, deltaY: -0.4, deltaZ: 0, + lineOrPageDeltaX: -3, lineOrPageDeltaY: -5, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: -0.6, deltaY: 0.4, deltaZ: 0 + }, + DOMMouseScroll: { + horizontal: { expected: true, preventDefault: false, detail: -1 }, + vertical: { expected: false, preventDefault: false, detail: 0 } }, + MozMousePixelScroll: { + horizontal: { expected: true, preventDefault: false, detail: Math.ceil(gHorizontalLine * -0.6) }, + vertical: { expected: true, preventDefault: false, detail: Math.floor(gLineHeight * 0.4) } }, + }, + { description: "lineOrPageDelta should be recomputed by ESM (line, negative) #3", + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: -0.3, deltaY: -0.4, deltaZ: 0, + lineOrPageDeltaX: -3, lineOrPageDeltaY: -5, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: -0.6, deltaY: 0.4, deltaZ: 0 + }, + DOMMouseScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: true, preventDefault: false, detail: 1 } }, + MozMousePixelScroll: { + horizontal: { expected: true, preventDefault: false, detail: Math.ceil(gHorizontalLine * -0.6) }, + vertical: { expected: true, preventDefault: false, detail: Math.floor(gLineHeight * 0.4) } }, + finished () { + SpecialPowers.pushPrefEnv({"set": [["mousewheel.default.delta_multiplier_x", 100], + ["mousewheel.default.delta_multiplier_y", 100]]}, + continueTest); + }, + }, + + { description: "lineOrPageDelta should be recomputed by ESM (page) #1", + prepare () { + SpecialPowers.pushPrefEnv({"set": [["mousewheel.default.delta_multiplier_x", 100], + ["mousewheel.default.delta_multiplier_y", 200]]}, + continueTest); + }, + event: { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: 0.3, deltaY: 0.4, deltaZ: 0, + lineOrPageDeltaX: 3, lineOrPageDeltaY: 5, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: 0.3, deltaY: 0.8, deltaZ: 0 + }, + DOMMouseScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: false, preventDefault: false, detail: 0 } }, + MozMousePixelScroll: { + horizontal: { expected: true, preventDefault: false, detail: Math.floor(gPageWidth * 0.3) }, + vertical: { expected: true, preventDefault: false, detail: Math.floor(gPageHeight * 0.8) } }, + }, + { description: "lineOrPageDelta should be recomputed by ESM (page) #2", + event: { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: 0.3, deltaY: 0.4, deltaZ: 0, + lineOrPageDeltaX: 3, lineOrPageDeltaY: 5, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: 0.3, deltaY: 0.8, deltaZ: 0 + }, + DOMMouseScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: true, preventDefault: false, detail: UIEvent.SCROLL_PAGE_DOWN } }, + MozMousePixelScroll: { + horizontal: { expected: true, preventDefault: false, detail: Math.floor(gPageWidth * 0.3) }, + vertical: { expected: true, preventDefault: false, detail: Math.floor(gPageHeight * 0.8) } }, + }, + { description: "lineOrPageDelta should be recomputed by ESM (page) #3", + event: { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: 0.4, deltaY: 0.4, deltaZ: 0, + lineOrPageDeltaX: 3, lineOrPageDeltaY: 5, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: 0.4, deltaY: 0.8, deltaZ: 0 + }, + DOMMouseScroll: { + horizontal: { expected: true, preventDefault: false, detail: UIEvent.SCROLL_PAGE_DOWN }, + vertical: { expected: true, preventDefault: false, detail: UIEvent.SCROLL_PAGE_DOWN } }, + MozMousePixelScroll: { + horizontal: { expected: true, preventDefault: false, detail: Math.floor(gPageWidth * 0.4) }, + vertical: { expected: true, preventDefault: false, detail: Math.floor(gPageHeight * 0.8) } }, + finished () { + SpecialPowers.pushPrefEnv({"set": [["mousewheel.default.delta_multiplier_x", 100], + ["mousewheel.default.delta_multiplier_y", 100]]}, + continueTest); + }, + }, + + { description: "lineOrPageDelta should be recomputed by ESM (page, negative) #1", + prepare () { + SpecialPowers.pushPrefEnv({"set": [["mousewheel.default.delta_multiplier_x", 100], + ["mousewheel.default.delta_multiplier_y", 200]]}, + continueTest); + }, + event: { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: -0.3, deltaY: -0.4, deltaZ: 0, + lineOrPageDeltaX: -3, lineOrPageDeltaY: -5, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: -0.3, deltaY: -0.8, deltaZ: 0 + }, + DOMMouseScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: false, preventDefault: false, detail: 0 } }, + MozMousePixelScroll: { + horizontal: { expected: true, preventDefault: false, detail: Math.ceil(gPageWidth * -0.3) }, + vertical: { expected: true, preventDefault: false, detail: Math.ceil(gPageHeight * -0.8) } }, + }, + { description: "lineOrPageDelta should be recomputed by ESM (page, negative) #2", + event: { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: -0.3, deltaY: -0.4, deltaZ: 0, + lineOrPageDeltaX: -3, lineOrPageDeltaY: -5, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: -0.3, deltaY: -0.8, deltaZ: 0 + }, + DOMMouseScroll: { + horizontal: { expected: false, preventDefault: false, detail: 0 }, + vertical: { expected: true, preventDefault: false, detail: UIEvent.SCROLL_PAGE_UP } }, + MozMousePixelScroll: { + horizontal: { expected: true, preventDefault: false, detail: Math.ceil(gPageWidth * -0.3) }, + vertical: { expected: true, preventDefault: false, detail: Math.ceil(gPageHeight * -0.8) } }, + }, + { description: "lineOrPageDelta should be recomputed by ESM (page, negative) #3", + event: { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: -0.4, deltaY: -0.4, deltaZ: 0, + lineOrPageDeltaX: -3, lineOrPageDeltaY: -5, isNoLineOrPageDelta: false, + isCustomizedByPrefs: false, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + wheel: { + expected: true, preventDefault: false, + deltaX: -0.4, deltaY: -0.8, deltaZ: 0 + }, + DOMMouseScroll: { + horizontal: { expected: true, preventDefault: false, detail: UIEvent.SCROLL_PAGE_UP }, + vertical: { expected: true, preventDefault: false, detail: UIEvent.SCROLL_PAGE_UP } }, + MozMousePixelScroll: { + horizontal: { expected: true, preventDefault: false, detail: Math.ceil(gPageWidth * -0.4) }, + vertical: { expected: true, preventDefault: false, detail: Math.ceil(gPageHeight * -0.8) } }, + finished () { + SpecialPowers.pushPrefEnv({"set": [["mousewheel.default.delta_multiplier_x", 100], + ["mousewheel.default.delta_multiplier_y", 100]]}, + continueTest); + }, + }, + ]; + + var currentWheelEventTest; + var calledHandlers = { wheel: false, + DOMMouseScroll: { horizontal: false, vertical: false }, + MozMousePixelScroll: { horizontal: false, vertical: false } }; + + function wheelEventHandler(aEvent) + { + var description = "testContinuousTrustedEvents, "; + description += currentWheelEventTest.description + ": wheel event "; + + ok(!calledHandlers.wheel, + description + "was fired twice or more"); + calledHandlers.wheel = true; + + is(aEvent.target, gScrolledElement, + description + "target was invalid"); + if (!currentWheelEventTest.wheel.skipDeltaModeCheck) { + is(aEvent.deltaMode, currentWheelEventTest.event.deltaMode, + description + "deltaMode was invalid"); + } + is(SpecialPowers.wrap(aEvent).deltaMode, currentWheelEventTest.event.deltaMode, + description + "deltaMode is raw value from privileged script"); + for (let prop of ["deltaX", "deltaY", "deltaZ"]) { + is(aEvent[prop], currentWheelEventTest.wheel[prop], + description + prop + " was invalid"); + if (currentWheelEventTest.wheel.skipDeltaModeCheck) { + is(aEvent.deltaMode, WheelEvent.DOM_DELTA_PIXEL, + description + "deltaMode should become pixels for line scrolling if unchecked by content") + if (aEvent[prop] != 0) { + isnot(aEvent[prop], SpecialPowers.wrap(aEvent)[prop], + description + "should keep returning raw value for privileged script"); + } + } + } + is(aEvent.shiftKey, currentWheelEventTest.event.shiftKey, + description + "shiftKey was invalid"); + is(aEvent.ctrlKey, currentWheelEventTest.event.ctrlKey, + description + "ctrlKey was invalid"); + is(aEvent.altKey, currentWheelEventTest.event.altKey, + description + "shiftKey was invalid"); + is(aEvent.metaKey, currentWheelEventTest.event.metaKey, + description + "metaKey was invalid"); + + ok(!aEvent.defaultPrevented, + description + "defaultPrevented should be false"); + if (currentWheelEventTest.wheel.preventDefault) { + aEvent.preventDefault(); + ok(aEvent.defaultPrevented, + description + "defaultPrevented should be true"); + } + } + + function legacyEventHandler(aEvent) + { + var description = "testContinuousTrustedEvents, "; + description += currentWheelEventTest.description + ": " + aEvent.type + " event "; + + if (aEvent.axis != MouseScrollEvent.HORIZONTAL_AXIS && + aEvent.axis != MouseScrollEvent.VERTICAL_AXIS) { + ok(false, + description + "had invalid axis (" + aEvent.axis + ")"); + return; + } + + var isHorizontal = (aEvent.axis == MouseScrollEvent.HORIZONTAL_AXIS); + + description += isHorizontal ? "(horizontal) " : "(vertical) "; + + var isScrollEvent = (aEvent.type == "DOMMouseScroll"); + var expectedEvent = + isScrollEvent ? currentWheelEventTest.DOMMouseScroll : + currentWheelEventTest.MozMousePixelScroll; + var expected = + isHorizontal ? expectedEvent.horizontal : expectedEvent.vertical; + + if (aEvent.type == "DOMMouseScroll") { + if (isHorizontal) { + ok(!calledHandlers.DOMMouseScroll.horizontal, + description + "was fired twice or more"); + calledHandlers.DOMMouseScroll.horizontal = true; + } else { + ok(!calledHandlers.DOMMouseScroll.vertical, + description + "was fired twice or more"); + calledHandlers.DOMMouseScroll.vertical = true; + } + } else { + if (isHorizontal) { + ok(!calledHandlers.MozMousePixelScroll.horizontal, + description + "was fired twice or more"); + calledHandlers.MozMousePixelScroll.horizontal = true; + } else { + ok(!calledHandlers.MozMousePixelScroll.vertical, + description + "was fired twice or more"); + calledHandlers.MozMousePixelScroll.vertical = true; + } + } + + is(aEvent.target, gScrolledElement, + description + "target was invalid"); + is(aEvent.detail, expected.detail, + description + "detail was invalid"); + + is(aEvent.shiftKey, currentWheelEventTest.event.shiftKey, + description + "shiftKey was invalid"); + is(aEvent.ctrlKey, currentWheelEventTest.event.ctrlKey, + description + "ctrlKey was invalid"); + is(aEvent.altKey, currentWheelEventTest.event.altKey, + description + "shiftKey was invalid"); + is(aEvent.metaKey, currentWheelEventTest.event.metaKey, + description + "metaKey was invalid"); + + var expectedDefaultPrevented = + isScrollEvent ? false : + isHorizontal ? currentWheelEventTest.DOMMouseScroll.horizontal.preventDefault : + currentWheelEventTest.DOMMouseScroll.vertical.preventDefault; + is(aEvent.defaultPrevented, expectedDefaultPrevented, + description + "defaultPrevented should be " + expectedDefaultPrevented); + + if (expected.preventDefault) { + aEvent.preventDefault(); + ok(aEvent.defaultPrevented, + description + "defaultPrevented should be true"); + } + } + + window.addEventListener("wheel", wheelEventHandler, { capture: true, passive: false }); + window.addEventListener("DOMMouseScroll", legacyEventHandler, { capture: true, passive: false }); + window.addEventListener("MozMousePixelScroll", legacyEventHandler, { capture: true, passive: false }); + + for (var i = 0; i < kSynthesizedWheelEventTests.length; i++) { + gScrollableElement.scrollTop = gScrollableElement.scrollBottom = 1000; + + currentWheelEventTest = kSynthesizedWheelEventTests[i]; + + if (currentWheelEventTest.prepare) { + yield currentWheelEventTest.prepare(); + } + + yield sendWheelAndWait(10, 10, currentWheelEventTest.event); + + if (currentWheelEventTest.finished) { + yield currentWheelEventTest.finished(); + } + + var description = "testContinuousTrustedEvents, " + + currentWheelEventTest.description + ": "; + is(calledHandlers.wheel, currentWheelEventTest.wheel.expected, + description + "wheel event was fired or not fired"); + is(calledHandlers.DOMMouseScroll.horizontal, + currentWheelEventTest.DOMMouseScroll.horizontal.expected, + description + "horizontal DOMMouseScroll event was fired or not fired"); + is(calledHandlers.DOMMouseScroll.vertical, + currentWheelEventTest.DOMMouseScroll.vertical.expected, + description + "vertical DOMMouseScroll event was fired or not fired"); + is(calledHandlers.MozMousePixelScroll.horizontal, + currentWheelEventTest.MozMousePixelScroll.horizontal.expected, + description + "horizontal MozMousePixelScroll event was fired or not fired"); + is(calledHandlers.MozMousePixelScroll.vertical, + currentWheelEventTest.MozMousePixelScroll.vertical.expected, + description + "vertical MozMousePixelScroll event was fired or not fired"); + + calledHandlers = { wheel: false, + DOMMouseScroll: { horizontal: false, vertical: false }, + MozMousePixelScroll: { horizontal: false, vertical: false } }; + } + + window.removeEventListener("wheel", wheelEventHandler, true); + window.removeEventListener("DOMMouseScroll", legacyEventHandler, true); + window.removeEventListener("MozMousePixelScroll", legacyEventHandler, true); +} + +var gTestContinuation = null; + +function continueTest() +{ + if (!gTestContinuation) { + gTestContinuation = testBody(); + } + var ret = gTestContinuation.next(); + if (ret.done) { + SimpleTest.finish(); + } +} + +function* testBody() +{ + yield* prepareScrollUnits(); + yield* testContinuousTrustedEvents(); +} + +function runTests() +{ + SpecialPowers.pushPrefEnv({"set": [ + // FIXME(emilio): This test is broken in HiDPI, unclear if + // MozMousePixelScroll is not properly converting to CSS pixels, or + // whether sendWheelAndWait expectes device rather than CSS pixels, or + // something else. + ["layout.css.devPixelsPerPx", 1.0], + + ["dom.event.wheel-deltaMode-lines.disabled", true], + + ["mousewheel.system_scroll_override.enabled", false], + + ["mousewheel.transaction.timeout", 100000], + ["mousewheel.default.delta_multiplier_x", 100], + ["mousewheel.default.delta_multiplier_y", 100], + ["mousewheel.default.delta_multiplier_z", 100], + ["mousewheel.with_alt.delta_multiplier_x", 100], + ["mousewheel.with_alt.delta_multiplier_y", 100], + ["mousewheel.with_alt.delta_multiplier_z", 100], + ["mousewheel.with_control.delta_multiplier_x", 100], + ["mousewheel.with_control.delta_multiplier_y", 100], + ["mousewheel.with_control.delta_multiplier_z", 100], + ["mousewheel.with_meta.delta_multiplier_x", 100], + ["mousewheel.with_meta.delta_multiplier_y", 100], + ["mousewheel.with_meta.delta_multiplier_z", 100], + ["mousewheel.with_shift.delta_multiplier_x", 100], + ["mousewheel.with_shift.delta_multiplier_y", 100], + ["mousewheel.with_shift.delta_multiplier_z", 100], + ["mousewheel.with_win.delta_multiplier_x", 100], + ["mousewheel.with_win.delta_multiplier_y", 100], + ["mousewheel.with_win.delta_multiplier_z", 100], + + ["test.events.async.enabled", true] + ]}, continueTest); +} +</script> +</pre> +</body> +</html> diff --git a/dom/events/test/test_dblclick_explicit_original_target.html b/dom/events/test/test_dblclick_explicit_original_target.html new file mode 100644 index 0000000000..214ae48dea --- /dev/null +++ b/dom/events/test/test_dblclick_explicit_original_target.html @@ -0,0 +1,52 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test explicit original target of dblclick event</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<p id="display">Test explicit original target of dblclick event</p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(runTests); + +function runTests() +{ + synthesizeMouse(document.getElementById("display"), 5, 5, { clickCount: 2 }); +} + +window.onmousedown = function(event) { + is(event.explicitOriginalTarget.nodeType, Node.TEXT_NODE, + "explicitOriginalTarget is a text node"); + is(event.explicitOriginalTarget, document.getElementById("display").firstChild, + "explicitOriginalTarget should point to the child node of the click target"); +} + +window.onmouseup = function(event) { + is(event.explicitOriginalTarget.nodeType, Node.TEXT_NODE, + "explicitOriginalTarget is a text node"); + is(event.explicitOriginalTarget, document.getElementById("display").firstChild, + "explicitOriginalTarget should point to the child node of the click target"); +} + +// The old versions of Gecko had explicitOriginalTarget pointing to a Text node +// when handling *click events, newer versions target Elements. +window.ondblclick = function(event) { + is(event.explicitOriginalTarget.nodeType, Node.ELEMENT_NODE, + "explicitOriginalTarget is an element node"); + is(event.explicitOriginalTarget, document.getElementById("display"), + "explicitOriginalTarget should point to the click target"); + SimpleTest.finish(); +} + +</script> +</pre> +</body> +</html> diff --git a/dom/events/test/test_deltaMode_lines_always_enabled.html b/dom/events/test/test_deltaMode_lines_always_enabled.html new file mode 100644 index 0000000000..b1c2f44046 --- /dev/null +++ b/dom/events/test/test_deltaMode_lines_always_enabled.html @@ -0,0 +1,40 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>Test for always-enabling lines deltaMode</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<script src="/tests/SimpleTest/EventUtils.js"></script> +<link rel="stylesheet" href="/tests/SimpleTest/test.css"/> +<p id="display"></p> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1684001">Mozilla Bug 1684001</a> +<div id="overflow" style="height: 300vh"></div> +<pre id="test"></pre> +<script> +SimpleTest.waitForExplicitFinish(); + +const HACK_PREF = "dom.event.wheel-deltaMode-lines.always-enabled"; + +(async function() { + await SimpleTest.promiseFocus(window); + await SpecialPowers.pushPrefEnv({ + set: [ + [HACK_PREF, document.domain], + ["test.events.async.enabled", true], + ], + }); + // FYI: Cannot use scrollingElement because it's center may be outside of the viewport. + await promiseElementReadyForUserInput(document.querySelector("a[href]")); + await SpecialPowers.pushPrefEnv({ + clear: [ + ["test.events.async.enabled"], + ], + }); + + document.addEventListener("wheel", e => { + e.deltaY; /* access the delta unchecked */ + is(e.deltaMode, WheelEvent.DOM_DELTA_LINE, "Accessing wheel event data should return lines even if unchecked, if the pref is enabled for that domain"); + SimpleTest.finish(); + }, { once: true }); + + synthesizeWheel(document.scrollingElement, 10, 10, { deltaY: 3.0, deltaMode: WheelEvent.DOM_DELTA_LINE }); +}()); +</script> diff --git a/dom/events/test/test_deviceSensor.html b/dom/events/test/test_deviceSensor.html new file mode 100644 index 0000000000..c1b032f1b5 --- /dev/null +++ b/dom/events/test/test_deviceSensor.html @@ -0,0 +1,136 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=402089 +--> +<head> + <title>Test for Bug 742376</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> + +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=742376">Mozilla Bug 742376</a> +<script class="testbody" type="text/javascript"> + +/** Test for Bug 742376 **/ +let Cc = SpecialPowers.Cc; +let Ci = SpecialPowers.Ci; +let dss = Cc["@mozilla.org/devicesensors;1"].getService(Ci.nsIDeviceSensors); + +function hasLightListeners() { + return dss.hasWindowListener(Ci.nsIDeviceSensorData.TYPE_LIGHT, window); +} + +function hasOrientationListeners() { + return dss.hasWindowListener(Ci.nsIDeviceSensorData.TYPE_ORIENTATION, window) || + dss.hasWindowListener(Ci.nsIDeviceSensorData.TYPE_ROTATION_VECTOR, window) || + dss.hasWindowListener(Ci.nsIDeviceSensorData.TYPE_GAME_ROTATION_VECTOR, window); +} + +function hasProximityListeners() { + return dss.hasWindowListener(Ci.nsIDeviceSensorData.TYPE_PROXIMITY, window); +} + +function hasMotionListeners() { + return dss.hasWindowListener(Ci.nsIDeviceSensorData.TYPE_ACCELERATION, window) || + dss.hasWindowListener(Ci.nsIDeviceSensorData.TYPE_LINEAR_ACCELERATION, window) || + dss.hasWindowListener(Ci.nsIDeviceSensorData.TYPE_GYROSCOPE, window); +} + +async function test_event_presence(prefName, eventCheck, eventName) { + function dumbListener(event) {} + function dumbListener2(event) {} + function dumbListener3(event) {} + + await SpecialPowers.pushPrefEnv({"set": [ + [prefName, true] + ]}); + + is(eventCheck(), false, "Must not have listeners before tests start"); + + window.addEventListener(eventName, dumbListener); + window.addEventListener("random_event_name", function() {}); + window.addEventListener(eventName, dumbListener2); + + is(eventCheck(), true, `Should have listeners when ${eventName} sensor is enabled`); + + window.removeEventListener(eventName, dumbListener); + window.removeEventListener(eventName, dumbListener2); + + is(eventCheck(), false, "Must not have listeners when removed"); + + await SpecialPowers.pushPrefEnv({"set": [ + [prefName, false] + ]}); + + window.addEventListener(eventName, dumbListener); + window.addEventListener("random_event_name", function() {}); + window.addEventListener(eventName, dumbListener2); + + is(eventCheck(), false, "Must not have listeners when sensor is disabled"); +} + +async function start() { + await SpecialPowers.pushPrefEnv({"set": [ + ["device.sensors.enabled", true], + ["device.sensors.orientation.enabled", true] + ]}); + + is(hasOrientationListeners(), false, "Must not have listeners before tests start"); + + function dumbListener(event) {} + function dumbListener2(event) {} + function dumbListener3(event) {} + + window.addEventListener("deviceorientation", dumbListener); + window.addEventListener("random_event_name", function() {}); + window.addEventListener("deviceorientation", dumbListener2); + + is(hasOrientationListeners(), true, "Listeners should have been added"); + + await new Promise(resolve => { + window.setTimeout(function() { + window.removeEventListener("deviceorientation", dumbListener); + is(hasOrientationListeners(), true, "Only some listeners should have been removed"); + window.setTimeout(function() { + window.removeEventListener("deviceorientation", dumbListener2); + window.setTimeout(function() { + is(hasOrientationListeners(), false, "Listeners should have been removed"); + resolve(); + }, 0); + }, 0); + }, 0); + }); + + await new Promise(resolve => { + window.ondeviceorientation = function() {} + window.setTimeout(function() { + is(hasOrientationListeners(), true, "Handler should have been added"); + window.ondeviceorientation = null; + window.setTimeout(function() { + is(hasOrientationListeners(), false, "Handler should have been removed"); + resolve(); + }, 0); + }, 0); + }); + + await test_event_presence("device.sensors.ambientLight.enabled", hasLightListeners, "devicelight"); + await test_event_presence("device.sensors.proximity.enabled", hasProximityListeners, "userproximity"); + await test_event_presence("device.sensors.motion.enabled", hasMotionListeners, "devicemotion"); + await test_event_presence("device.sensors.orientation.enabled", hasOrientationListeners, "deviceorientation"); + + SimpleTest.finish(); + +} + +SimpleTest.waitForExplicitFinish(); + +start(); + +</script> +</pre> +</body> +</html> + diff --git a/dom/events/test/test_disabled_events.html b/dom/events/test/test_disabled_events.html new file mode 100644 index 0000000000..c345208ee7 --- /dev/null +++ b/dom/events/test/test_disabled_events.html @@ -0,0 +1,40 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1359076 +--> +<head> + <title>Test for Bug 675884</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1359076">Mozilla Bug 1359076</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> + +SimpleTest.waitForExplicitFinish(); + +SpecialPowers.pushPrefEnv({"set": [ + ["device.sensors.orientation.enabled", false], + ["device.sensors.motion.enabled", false], + ["device.sensors.proximity.enabled", false], + ["device.sensors.ambientLight.enabled", false] +]}, () => { + is("UserProximityEvent" in window, false, "UserProximityEvent does not exist"); + is("DeviceLightEvent" in window, false, "DeviceLightEvent does not exist"); + is("DeviceOrientationEvent" in window, false, "DeviceOrientationEvent does not exist"); + is("DeviceMotionEvent" in window, false, "DeviceMotionEvent does not exist"); + is("onuserproximity" in window, false, "onuserproximity does not exist"); + is("ondevicelight" in window, false, "ondevicelight does not exist"); + SimpleTest.finish(); +}); + +</script> +</pre> +</body> +</html> diff --git a/dom/events/test/test_dnd_with_modifiers.html b/dom/events/test/test_dnd_with_modifiers.html new file mode 100644 index 0000000000..8b861c9306 --- /dev/null +++ b/dom/events/test/test_dnd_with_modifiers.html @@ -0,0 +1,78 @@ +<!DOCTYPE html> +<html> + <meta charset="utf-8"> + <title>Test dragstart, drag, dragover, drop, dragend with keyboard modifiers</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"> + <div id="test"></div> + <script> + SimpleTest.waitForExplicitFinish(); + + SimpleTest.waitForFocus(() => { + let dragEvents = ["dragstart", "drag", "dragend"]; + let dropEvents = ["dragover", "drop"]; + let source = document.getElementById("source"); + let target = document.getElementById("target"); + + dragEvents.forEach((ev, idx, array) => { + source.addEventListener(ev, (e) => { + ok(e.ctrlKey, e.type + ".ctrlKey should be true"); + ok(!e.shiftKey, e.type + ".shiftKey should be false"); + ok(e.altKey, e.type + ".altKey should be true"); + }, {once: true}); + }); + + dropEvents.forEach((ev, idx, array) => { + target.addEventListener(ev, (e) => { + ok(e.ctrlKey, e.type + ".ctrlKey should be true"); + ok(!e.shiftKey, e.type + ".shiftKey should be false"); + ok(e.altKey, e.type + ".altKey should be true"); + }, {once: true}); + }); + + source.addEventListener("dragstart", (e) => { + e.preventDefault(); + }, {once: true}); + + source.addEventListener("dragend", (e) => { + SimpleTest.finish(); + }); + + let selection = window.getSelection(); + selection.selectAllChildren(source); + + synthesizeMouse(source, 1, 1, {type: "mousedown", ctrlKey: true, altKey: true}, window); + synthesizeMouse(source, 10, 10, {type: "mousemove", ctrlKey: true, altKey: true}, window); + synthesizeMouse(source, 10, 10, {type: "mouseup", ctrlKey: true, altKey: true}, window); + + let dragEvent = { + type: "drag", + ctrlKey: true, + altKey: true, + }; + sendDragEvent(dragEvent, source, window); + + let rect = target.getBoundingClientRect(); + let dropEvent = { + ctrlKey: true, + altKey: true, + clientX: rect.left + rect.width / 2, + clientY: rect.top + rect.height / 2, + }; + selection.selectAllChildren(source); + synthesizeDrop(source, target, [], "copy", window, window, dropEvent); + + let dragEndEvent = { + type: "dragend", + ctrlKey: true, + altKey: true, + }; + sendDragEvent(dragEndEvent, source, window); + }); + </script> +<body> + <span id="source" style="font-size: 40px;">test</span> + <div id="target" contenteditable="true" width="50" height="50"></div> +</body> +</html> diff --git a/dom/events/test/test_dom_activate_event.html b/dom/events/test/test_dom_activate_event.html new file mode 100644 index 0000000000..9a6d48ac64 --- /dev/null +++ b/dom/events/test/test_dom_activate_event.html @@ -0,0 +1,91 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test DOMActivate event</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<p id="display"> +<a id="a" href="#dummy">link</a> +<button id="button">button</button> +<input id="checkbox" type="checkbox"> +<input id="radio" type="radio"> +<input id="submit" type="submit"> +<input id="ibutton" type="button"> +<input id="reset" type="reset"> +</p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> + +/* eslint-disable max-nested-callbacks */ + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(runTests); + +function runIsTrustedTestCausedByTrustedClick(aElement, aNextTest) +{ + const kDescription = "runIsTrustedTestCausedByTrustedClick(aElement.id=" + aElement.id + "): "; + var DOMActivateFired = false; + aElement.addEventListener("DOMActivate", function (aEvent) { + DOMActivateFired = true; + ok(aEvent.isTrusted, kDescription + "DOMActivate event should be trusted event"); + aElement.removeEventListener("DOMActivate", arguments.callee); + aNextTest(); + }); + aElement.addEventListener("click", function (aEvent) { + ok(aEvent.isTrusted, kDescription + "click event should be trusted event"); + aElement.removeEventListener("click", arguments.callee); + }); + synthesizeMouseAtCenter(aElement, {}); +} + +function runIsTrustedTestCausedByUntrustedClick(aElement, aNextTest) +{ + const kDescription = "runIsTrustedTestCausedByUntrustedClick(aElement.id=" + aElement.id + "): "; + var DOMActivateFired = false; + aElement.addEventListener("DOMActivate", function (aEvent) { + DOMActivateFired = true; + ok(aEvent.isTrusted, + kDescription + "DOMActivate event should be trusted event even if it's caused by untrusted event"); + aElement.removeEventListener("DOMActivate", arguments.callee); + aNextTest(); + }); + aElement.addEventListener("click", function (aEvent) { + ok(!aEvent.isTrusted, kDescription + "click event should be untrusted event"); + aElement.removeEventListener("click", arguments.callee); + }); + var click = new MouseEvent("click", { button: 0 }); + aElement.dispatchEvent(click); +} + +function runTests() +{ + // XXX Don't add indentation here. If you add indentation, the diff will be + // complicated when somebody adds new tests. + runIsTrustedTestCausedByTrustedClick(document.getElementById("a"), function () { + runIsTrustedTestCausedByTrustedClick(document.getElementById("button"), function () { + runIsTrustedTestCausedByTrustedClick(document.getElementById("checkbox"), function () { + runIsTrustedTestCausedByTrustedClick(document.getElementById("radio"), function () { + runIsTrustedTestCausedByTrustedClick(document.getElementById("submit"), function () { + runIsTrustedTestCausedByTrustedClick(document.getElementById("ibutton"), function () { + runIsTrustedTestCausedByTrustedClick(document.getElementById("reset"), function () { + runIsTrustedTestCausedByUntrustedClick(document.getElementById("a"), function () { + runIsTrustedTestCausedByUntrustedClick(document.getElementById("button"), function () { + runIsTrustedTestCausedByUntrustedClick(document.getElementById("checkbox"), function () { + runIsTrustedTestCausedByUntrustedClick(document.getElementById("radio"), function () { + runIsTrustedTestCausedByUntrustedClick(document.getElementById("submit"), function () { + runIsTrustedTestCausedByUntrustedClick(document.getElementById("ibutton"), function () { + runIsTrustedTestCausedByUntrustedClick(document.getElementById("reset"), function () { + SimpleTest.finish(); + });});});});});});});});});});});});});}); +} + +</script> +</pre> +</body> +</html> diff --git a/dom/events/test/test_dom_keyboard_event.html b/dom/events/test/test_dom_keyboard_event.html new file mode 100644 index 0000000000..3d4a9dda05 --- /dev/null +++ b/dom/events/test/test_dom_keyboard_event.html @@ -0,0 +1,541 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for DOM KeyboardEvent</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<p id="display"></p> +<p><input type="text" id="input"></p> +<p><input type="text" id="input_readonly" readonly></p> +<p><textarea id="textarea"></textarea></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(runTests, window); + +function testInitializingUntrustedEvent() +{ + const kTests = [ + // initKeyEvent + { createEventArg: "KeyboardEvent", useInitKeyboardEvent: false, + type: "keydown", bubbles: true, cancelable: true, view: null, + ctrlKey: false, altKey: false, shiftKey: false, metaKey: false, + keyCode: 0x00, charCode: 0x00, detail: 0, key: "", location: 0, + }, // 0 + + { createEventArg: "keyboardevent", useInitKeyboardEvent: false, + type: "keyup", bubbles: false, cancelable: true, view: window, + ctrlKey: true, altKey: false, shiftKey: false, metaKey: false, + keyCode: 0x10, charCode: 0x00, detail: 0, key: "", location: 0, + }, // 1 + + { createEventArg: "Keyboardevent", useInitKeyboardEvent: false, + type: "keypress", bubbles: true, cancelable: false, view: null, + ctrlKey: false, altKey: true, shiftKey: false, metaKey: false, + keyCode: 0x11, charCode: 0x30, detail: 0, key: "", location: 0, + }, // 2 + + { createEventArg: "keyboardEvent", useInitKeyboardEvent: false, + type: "boo", bubbles: false, cancelable: false, view: window, + ctrlKey: false, altKey: false, shiftKey: true, metaKey: false, + keyCode: 0x30, charCode: 0x40, detail: 0, key: "", location: 0, + }, // 3 + + { createEventArg: "KeyBoardEvent", useInitKeyboardEvent: false, + type: "foo", bubbles: true, cancelable: true, view: null, + ctrlKey: false, altKey: false, shiftKey: false, metaKey: true, + keyCode: 0x00, charCode: 0x50, detail: 0, key: "", location: 0, + }, // 4 + + { createEventArg: "keyboardevEnt", useInitKeyboardEvent: false, + type: "bar", bubbles: false, cancelable: true, view: window, + ctrlKey: true, altKey: true, shiftKey: false, metaKey: false, + keyCode: 0x00, charCode: 0x60, detail: 0, key: "", location: 0, + }, // 5 + + { createEventArg: "KeyboaRdevent", useInitKeyboardEvent: false, + type: "keydown", bubbles: true, cancelable: false, view: null, + ctrlKey: false, altKey: true, shiftKey: false, metaKey: true, + keyCode: 0x30, charCode: 0x00, detail: 0, key: "", location: 0, + }, // 6 + + { createEventArg: "KEYBOARDEVENT", useInitKeyboardEvent: false, + type: "keyup", bubbles: false, cancelable: false, view: window, + ctrlKey: true, altKey: false, shiftKey: true, metaKey: false, + keyCode: 0x10, charCode: 0x80, detail: 0, key: "", location: 0, + }, // 7 + + { createEventArg: "KeyboardEvent", useInitKeyboardEvent: false, + type: "keypress", bubbles: false, cancelable: false, view: window, + ctrlKey: true, altKey: false, shiftKey: true, metaKey: true, + keyCode: 0x10, charCode: 0x80, detail: 0, key: "", location: 0, + }, // 8 + + { createEventArg: "KeyboardEvent", useInitKeyboardEvent: false, + type: "foo", bubbles: false, cancelable: false, view: window, + ctrlKey: true, altKey: true, shiftKey: true, metaKey: true, + keyCode: 0x10, charCode: 0x80, detail: 0, key: "", location: 0, + }, // 9 + + // initKeyboardEvent + { createEventArg: "KeyboardEvent", useInitKeyboardEvent: true, + type: "keydown", bubbles: true, cancelable: true, view: null, + ctrlKey: false, altKey: false, shiftKey: false, metaKey: false, + keyCode: 0x00, charCode: 0x00, key: "", location: 0, + }, // 10 + + { createEventArg: "keyboardevent", useInitKeyboardEvent: true, + type: "keyup", bubbles: false, cancelable: true, view: window, + ctrlKey: true, altKey: false, shiftKey: false, metaKey: false, + keyCode: 0x00, charCode: 0x00, key: "Unidentified", location: 1, + }, // 11 + + { createEventArg: "Keyboardevent", useInitKeyboardEvent: true, + type: "keypress", bubbles: true, cancelable: false, view: null, + ctrlKey: false, altKey: true, shiftKey: false, metaKey: false, + keyCode: 0x00, charCode: 0x00, key: "FooBar", location: 2, + }, // 12 + + { createEventArg: "keyboardevent", useInitKeyboardEvent: true, + type: "foo", bubbles: true, cancelable: true, view: null, + ctrlKey: false, altKey: false, shiftKey: false, metaKey: true, + keyCode: 0x00, charCode: 0x00, key: "a", location: 0, + }, // 13 + + { createEventArg: "KeyBoardEvent", useInitKeyboardEvent: true, + type: "", bubbles: false, cancelable: false, view: null, + ctrlKey: true, altKey: true, shiftKey: true, metaKey: true, + keyCode: 0x00, charCode: 0x00, key: "3", location: 0, + }, // 14 + + { createEventArg: "keyboardevEnt", useInitKeyboardEvent: true, + type: "", bubbles: false, cancelable: false, view: null, + ctrlKey: false, altKey: false, shiftKey: true, metaKey: false, + keyCode: 0x00, charCode: 0x00, key: "3", location: 6, + }, // 15 + + { createEventArg: "KeyboaRdevent", useInitKeyboardEvent: true, + type: "", bubbles: false, cancelable: false, view: null, + ctrlKey: false, altKey: true, shiftKey: false, metaKey: false, + keyCode: 0x00, charCode: 0x00, key: "", location: 4, + }, // 16 + ]; + + const kOtherModifierName = [ + "CapsLock", "NumLock", "ScrollLock", "Symbol", "SymbolLock", "Fn", "FnLock", "OS", "AltGraph" + ]; + + const kInvalidModifierName = [ + "shift", "control", "alt", "meta", "capslock", "numlock", "scrolllock", + "symbollock", "fn", "os", "altgraph", "Invalid", "Shift Control", + "Win", "Scroll" + ]; + + const kIsInitKeyEventAvailable = + SpecialPowers.getBoolPref("dom.keyboardevent.init_key_event.enabled", false); + + for (var i = 0; i < kTests.length; i++) { + var description = "testInitializingUntrustedEvent, Index: " + i + ", "; + const kTest = kTests[i]; + var e = document.createEvent(kTest.createEventArg); + if (kTest.useInitKeyboardEvent) { + e.initKeyboardEvent(kTest.type, kTest.bubbles, kTest.cancelable, + kTest.view, kTest.key, kTest.location, + kTest.ctrlKey, kTest.altKey, kTest.shiftKey, + kTest.metaKey); + } else { + if (!kIsInitKeyEventAvailable) { + continue; + } + e.initKeyEvent(kTest.type, kTest.bubbles, kTest.cancelable, kTest.view, + kTest.ctrlKey, kTest.altKey, kTest.shiftKey, kTest.metaKey, + kTest.keyCode, kTest.charCode); + } + is(e.toString(), "[object KeyboardEvent]", + description + 'class string should be "KeyboardEvent"'); + + for (var attr in kTest) { + if (attr == "createEventArg" || attr == "useInitKeyboardEvent" || attr == "modifiersList") { + continue; + } + if (!kTest.useInitKeyboardEvent && attr == "keyCode") { + // If this is keydown, keyup of keypress event, keycod must be correct. + if (kTest.type == "keydown" || kTest.type == "keyup" || kTest.type == "keypress") { + is(e[attr], kTest[attr], description + attr + " returns wrong value"); + // Otherwise, should be always zero (why?) + } else { + is(e[attr], 0, description + attr + " returns non-zero for invalid event"); + } + } else if (!kTest.useInitKeyboardEvent && attr == "charCode") { + // If this is keydown or keyup event, charCode always 0. + if (kTest.type == "keydown" || kTest.type == "keyup") { + is(e[attr], 0, description + attr + " returns non-zero for keydown or keyup event"); + // If this is keypress event, charCode must be correct. + } else if (kTest.type == "keypress") { + is(e[attr], kTest[attr], description + attr + " returns wrong value"); + // Otherwise, we have a bug. + } else { + if (e[attr] != kTest[attr]) { // avoid random unexpected pass. + todo_is(e[attr], kTest[attr], description + attr + " returns wrong value"); + } + } + } else { + is(e[attr], kTest[attr], description + attr + " returns wrong value"); + } + } + is(e.isTrusted, false, description + "isTrusted returns wrong value"); + + // getModifierState() tests + is(e.getModifierState("Shift"), kTest.shiftKey, + description + "getModifierState(\"Shift\") returns wrong value"); + is(e.getModifierState("Control"), kTest.ctrlKey, + description + "getModifierState(\"Control\") returns wrong value"); + is(e.getModifierState("Alt"), kTest.altKey, + description + "getModifierState(\"Alt\") returns wrong value"); + is(e.getModifierState("Meta"), kTest.metaKey, + description + "getModifierState(\"Meta\") returns wrong value"); + + for (var j = 0; j < kOtherModifierName.length; j++) { + ok(!e.getModifierState(kOtherModifierName[j]), + description + "getModifierState(\"" + kOtherModifierName[j] + "\") returns wrong value"); + } + for (var k = 0; k < kInvalidModifierName.length; k++) { + ok(!e.getModifierState(kInvalidModifierName[k]), + description + "getModifierState(\"" + kInvalidModifierName[k] + "\") returns wrong value"); + } + } +} + +function testSynthesizedKeyLocation() +{ + const kTests = [ + { key: "a", isModifier: false, isPrintable: true, + event: { shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, numLockKey: false, + keyCode: KeyboardEvent.DOM_VK_A, + location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD }, + }, + { key: "KEY_Shift", isModifier: true, isPrintable: false, + event: { shiftKey: true, ctrlKey: false, altKey: false, metaKey: false, numLockKey: false, + keyCode: KeyboardEvent.DOM_VK_SHIFT, + location: KeyboardEvent.DOM_KEY_LOCATION_LEFT }, + }, + { key: "KEY_Shift", isModifier: true, isPrintable: false, + event: { shiftKey: true, ctrlKey: false, altKey: false, metaKey: false, numLockKey: false, + keyCode: KeyboardEvent.DOM_VK_SHIFT, + location: KeyboardEvent.DOM_KEY_LOCATION_RIGHT }, + }, + { key: "KEY_Control", isModifier: true, isPrintable: false, + event: { shiftKey: false, ctrlKey: true, altKey: false, metaKey: false, numLockKey: false, + keyCode: KeyboardEvent.DOM_VK_CONTROL, + location: KeyboardEvent.DOM_KEY_LOCATION_LEFT }, + }, + { key: "KEY_Control", isModifier: true, isPrintable: false, + event: { shiftKey: false, ctrlKey: true, altKey: false, metaKey: false, numLockKey: false, + keyCode: KeyboardEvent.DOM_VK_CONTROL, + location: KeyboardEvent.DOM_KEY_LOCATION_RIGHT }, + }, +/* XXX Alt key activates menubar even if we consume the key events. + { key: "KEY_Alt", isModifier: true, isPrintable: false, + event: { shiftKey: false, ctrlKey: false, altKey: true, metaKey: false, numLockKey: false, + keyCode: KeyboardEvent.DOM_VK_ALT, + location: KeyboardEvent.DOM_KEY_LOCATION_LEFT }, + }, + { key: "KEY_Alt", isModifier: true, isPrintable: false, + event: { shiftKey: false, ctrlKey: false, altKey: true, metaKey: false, numLockKey: false, + keyCode: KeyboardEvent.DOM_VK_ALT, + location: KeyboardEvent.DOM_KEY_LOCATION_RIGHT }, + }, +*/ + { key: "KEY_Meta", isModifier: true, isPrintable: false, + event: { shiftKey: false, ctrlKey: false, altKey: false, metaKey: true, numLockKey: false, + keyCode: KeyboardEvent.DOM_VK_META, + location: KeyboardEvent.DOM_KEY_LOCATION_LEFT }, + }, + { key: "KEY_Meta", isModifier: true, isPrintable: false, + event: { shiftKey: false, ctrlKey: false, altKey: false, metaKey: true, numLockKey: false, + keyCode: KeyboardEvent.DOM_VK_META, + location: KeyboardEvent.DOM_KEY_LOCATION_RIGHT }, + }, + { key: "KEY_ArrowDown", isModifier: false, isPrintable: false, + event: { shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, numLockKey: false, + keyCode: KeyboardEvent.DOM_VK_DOWN, + location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD }, + }, + { key: "KEY_ArrowDown", isModifier: false, isPrintable: false, + event: { shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, numLockKey: false, + keyCode: KeyboardEvent.DOM_VK_DOWN, + location: KeyboardEvent.DOM_KEY_LOCATION_NUMPAD }, + }, + { key: "5", isModifier: false, isPrintable: true, + event: { shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, numLockKey: false, + keyCode: KeyboardEvent.DOM_VK_5, + location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD }, + }, + { key: "5", isModifier: false, isPrintable: true, + event: { shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, numLockKey: true, + keyCode: KeyboardEvent.DOM_VK_NUMPAD5, + location: KeyboardEvent.DOM_KEY_LOCATION_NUMPAD }, + }, + { key: "+", isModifier: false, isPrintable: true, + event: { shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, numLockKey: false, + keyCode: KeyboardEvent.DOM_VK_EQUALS, + location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD }, + }, + { key: "+", isModifier: false, isPrintable: true, + event: { shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, numLockKey: true, + keyCode: KeyboardEvent.DOM_VK_ADD, + location: KeyboardEvent.DOM_KEY_LOCATION_NUMPAD }, + }, + { key: "KEY_Enter", isModifier: false, isPrintable: true, + event: { shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, numLockKey: false, + keyCode: KeyboardEvent.DOM_VK_RETURN, + location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD }, + }, + { key: "KEY_Enter", isModifier: false, isPrintable: true, + event: { shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, numLockKey: false, + keyCode: KeyboardEvent.DOM_VK_RETURN, + location: KeyboardEvent.DOM_KEY_LOCATION_NUMPAD }, + }, + { key: "KEY_NumLock", isModifier: true, isPrintable: false, + event: { shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, numLockKey: false, + keyCode: KeyboardEvent.DOM_VK_NUM_LOCK, + location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD }, + }, + { key: "KEY_Insert", isModifier: false, isPrintable: false, + event: { shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, numLockKey: false, + keyCode: KeyboardEvent.DOM_VK_INSERT, + location: KeyboardEvent.DOM_KEY_LOCATION_STANDARD }, + }, + { key: "KEY_Insert", isModifier: false, isPrintable: false, + event: { shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, numLockKey: false, + keyCode: KeyboardEvent.DOM_VK_INSERT, + location: KeyboardEvent.DOM_KEY_LOCATION_NUMPAD }, + }, + ]; + + function getLocationName(aLocation) + { + switch (aLocation) { + case KeyboardEvent.DOM_KEY_LOCATION_STANDARD: + return "DOM_KEY_LOCATION_STANDARD"; + case KeyboardEvent.DOM_KEY_LOCATION_LEFT: + return "DOM_KEY_LOCATION_LEFT"; + case KeyboardEvent.DOM_KEY_LOCATION_RIGHT: + return "DOM_KEY_LOCATION_RIGHT"; + case KeyboardEvent.DOM_KEY_LOCATION_NUMPAD: + return "DOM_KEY_LOCATION_NUMPAD"; + default: + return "Invalid value (" + aLocation + ")"; + } + } + + var currentTest, description; + var events = { keydown: false, keypress: false, keyup: false }; + + function handler(aEvent) + { + is(aEvent.location, currentTest.event.location, + description + "location of " + aEvent.type + " was invalid"); + events[aEvent.type] = true; + if (aEvent.type != "keydown" || + (currentTest.event.isModifier && aEvent.type == "keydown")) { + aEvent.preventDefault(); + } + } + + window.addEventListener("keydown", handler, true); + window.addEventListener("keypress", handler, true); + window.addEventListener("keyup", handler, true); + + for (var i = 0; i < kTests.length; i++) { + currentTest = kTests[i]; + events = { keydown: false, keypress: false, keyup: false }; + description = "testSynthesizedKeyLocation, " + i + ", key: " + + currentTest.key + ", location: " + + getLocationName(currentTest.event.location) + ": "; + synthesizeKey(currentTest.key, currentTest.event); + ok(events.keydown, description + "keydown event wasn't fired"); + if (currentTest.isPrintable) { + ok(events.keypress, description + "keypress event wasn't fired for printable key"); + } else { + ok(!events.keypress, description + "keypress event was fired for non-printable key"); + } + ok(events.keyup, description + "keyup event wasn't fired"); + } + + window.removeEventListener("keydown", handler, true); + window.removeEventListener("keypress", handler, true); + window.removeEventListener("keyup", handler, true); +} + +// We're using TextEventDispatcher to decide if we should keypress event +// on content in the default event group. So, we can test if keypress +// event is NOT fired unexpectedly with synthesizeKey(). +function testEnterKeyPressEvent() +{ + let keydownFired, keypressFired, beforeinputFired; + function onEvent(aEvent) { + switch (aEvent.type) { + case "keydown": + keydownFired = true; + return; + case "keypress": + keypressFired = true; + return; + case "beforeinput": + beforeinputFired = true; + return; + } + } + + for (let targetId of ["input", "textarea", "input_readonly"]) { + let target = document.getElementById(targetId); + + function reset() { + keydownFired = keypressFired = beforeinputFired = false; + target.value = ""; + } + + target.addEventListener("keydown", onEvent); + target.addEventListener("keypress", onEvent); + target.addEventListener("beforeinput", onEvent); + + const kDescription = "<" + targetId.replace("_", " ") + ">: "; + let isEditable = !kDescription.includes("readonly"); + let isTextarea = kDescription.includes("textarea"); + + target.focus(); + + reset(); + synthesizeKey("KEY_Enter"); + is(keydownFired, true, + kDescription + "keydown event should be fired when Enter key is pressed"); + is(keypressFired, true, + kDescription + "keypress event should be fired when Enter key is pressed"); + if (isEditable) { + ok(beforeinputFired, + kDescription + "beforeinput event should be fired (if it's enabled) when Enter key is pressed"); + } else { + is(beforeinputFired, false, + kDescription + "beforeinput event shouldn't be fired when Enter key is pressed"); + } + if (isTextarea) { + is(target.value, "\n", + kDescription + "Enter key should cause inputting a line break in <textarea>"); + } else { + is(target.value, "", + kDescription + "Enter key should not cause inputting a line break"); + } + + reset(); + synthesizeKey("KEY_Enter", {shiftKey: true}); + is(keydownFired, true, + kDescription + "keydown event should be fired when Shift + Enter key is pressed"); + is(keypressFired, true, + kDescription + "keypress event should be fired when Shift + Enter key is pressed"); + if (isEditable) { + ok(beforeinputFired, + kDescription + "beforeinput event should be fired (if it's enabled) when Shift + Enter key is pressed"); + } else { + is(beforeinputFired, false, + kDescription + "beforeinput event shouldn't be fired when Shift + Enter key is pressed"); + } + if (isTextarea) { + is(target.value, "\n", + kDescription + "Shift + Enter key should cause inputting a line break in <textarea>"); + } else { + is(target.value, "", + kDescription + "Shift + Enter key should not cause inputting a line break"); + } + + reset(); + synthesizeKey("KEY_Enter", {ctrlKey: true}); + is(keydownFired, true, + kDescription + "keydown event should be fired when Ctrl + Enter key is pressed"); + is(keypressFired, true, + kDescription + "keypress event should be fired when Ctrl + Enter key is pressed"); + is(beforeinputFired, false, + kDescription + "beforeinput event shouldn't be fired when Ctrl + Enter key is pressed"); + is(target.value, "", + kDescription + "Ctrl + Enter key should not cause inputting a line break"); + + reset(); + synthesizeKey("KEY_Enter", {altKey: true}); + is(keydownFired, true, + kDescription + "keydown event should be fired when Alt + Enter key is pressed"); + ok(!keypressFired, + kDescription + "keypress event shouldn't be fired when Alt + Enter key is pressed in strict keypress dispatching mode"); + is(beforeinputFired, false, + kDescription + "beforeinput event shouldn't be fired when Alt + Enter key is pressed"); + is(target.value, "", + kDescription + "Alt + Enter key should not cause inputting a line break"); + + reset(); + synthesizeKey("KEY_Enter", {metaKey: true}); + is(keydownFired, true, + kDescription + "keydown event should be fired when Meta + Enter key is pressed"); + ok(!keypressFired, + kDescription + "keypress event shouldn't be fired when Meta + Enter key is pressed in strict keypress dispatching mode"); + is(beforeinputFired, false, + kDescription + "beforeinput event shouldn't be fired when Meta + Enter key is pressed"); + is(target.value, "", + kDescription + "Meta + Enter key should not cause inputting a line break"); + + reset(); + synthesizeKey("KEY_Enter", {shiftKey: true, ctrlKey: true}); + is(keydownFired, true, + kDescription + "keydown event should be fired when Ctrl + Shift + Enter key is pressed"); + ok(!keypressFired, + kDescription + "keypress event shouldn't be fired when Ctrl + Shift + Enter key is pressed in strict keypress dispatching mode"); + is(beforeinputFired, false, + kDescription + "beforeinput event shouldn't be fired when Ctrl + Shift + Enter key is pressed"); + is(target.value, "", + kDescription + "Ctrl + Shift + Enter key should not cause inputting a line break"); + + reset(); + synthesizeKey("KEY_Enter", {shiftKey: true, altKey: true}); + is(keydownFired, true, + kDescription + "keydown event should be fired when Alt + Shift + Enter key is pressed"); + ok(!keypressFired, + kDescription + "keypress event shouldn't be fired when Alt + Shift + Enter key is pressed in strict keypress dispatching mode"); + is(beforeinputFired, false, + kDescription + "beforeinput event shouldn't be fired when Alt + Shift + Enter key is pressed"); + is(target.value, "", + kDescription + "Alt + Shift + Enter key should not cause inputting a line break"); + + reset(); + synthesizeKey("KEY_Enter", {shiftKey: true, metaKey: true}); + is(keydownFired, true, + kDescription + "keydown event should be fired when Meta + Shift + Enter key is pressed"); + ok(!keypressFired, + kDescription + "keypress event shouldn't be fired when Meta + Shift + Enter key is pressed in strict keypress dispatching mode"); + is(beforeinputFired, false, + kDescription + "beforeinput event shouldn't be fired when Meta + Shift + Enter key is pressed"); + is(target.value, "", + kDescription + "Meta + Shift + Enter key should not cause inputting a line break"); + + target.removeEventListener("keydown", onEvent); + target.removeEventListener("keypress", onEvent); + target.removeEventListener("beforeinput", onEvent); + } +} + +function runTests() +{ + testInitializingUntrustedEvent(); + testSynthesizedKeyLocation(); + testEnterKeyPressEvent(); + SimpleTest.finish(); +} + +</script> +</pre> +</body> +</html> diff --git a/dom/events/test/test_dom_mouse_event.html b/dom/events/test/test_dom_mouse_event.html new file mode 100644 index 0000000000..92352e4a25 --- /dev/null +++ b/dom/events/test/test_dom_mouse_event.html @@ -0,0 +1,143 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for DOM MouseEvent</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(runTests, window); + +function testInitializingUntrustedEvent() +{ + const kTests = [ + { createEventArg: "MouseEvent", + type: "mousedown", bubbles: true, cancelable: true, view: null, detail: 1, + screenX: 0, screenY: 0, clientX: 0, clientY: 0, + ctrlKey: false, altKey: false, shiftKey: false, metaKey: false, + button: 6, relatedTarget: null }, + + { createEventArg: "mouseevent", + type: "mouseup", bubbles: false, cancelable: true, view: window, detail: 2, + screenX: 0, screenY: 0, clientX: 0, clientY: 400, + ctrlKey: true, altKey: false, shiftKey: false, metaKey: false, + button: 1, relatedTarget: document.getElementById("content") }, + + { createEventArg: "Mouseevent", + type: "click", bubbles: true, cancelable: false, view: null, detail: -5, + screenX: 0, screenY: 0, clientX: 300, clientY: 0, + ctrlKey: false, altKey: true, shiftKey: false, metaKey: false, + button: 2, relatedTarget: document.getElementById("test") }, + + { createEventArg: "mouseEvent", + type: "dblclick", bubbles: false, cancelable: false, view: window, detail: -1, + screenX: 0, screenY: 200, clientX: 0, clientY: 0, + ctrlKey: false, altKey: false, shiftKey: true, metaKey: false, + button: 12, relatedTarget: document.getElementById("content") }, + + { createEventArg: "MouseEvents", + type: "mouseenter", bubbles: true, cancelable: true, view: null, detail: 111000, + screenX: 100, screenY: 0, clientX: 0, clientY: 0, + ctrlKey: false, altKey: false, shiftKey: false, metaKey: true, + button: 2, relatedTarget: document.getElementById("test") }, + + { createEventArg: "mouseevents", + type: "mouseleave", bubbles: false, cancelable: true, view: window, detail: 500, + screenX: 100, screenY: 500, clientX: 0, clientY: 0, + ctrlKey: true, altKey: true, shiftKey: false, metaKey: false, + button: 8, relatedTarget: document.getElementById("content") }, + + { createEventArg: "Mouseevents", + type: "mouseover", bubbles: true, cancelable: false, view: null, detail: 3, + screenX: 0, screenY: 0, clientX: 200, clientY: 300, + ctrlKey: false, altKey: true, shiftKey: false, metaKey: true, + button: 7, relatedTarget: document.getElementById("test") }, + + { createEventArg: "mouseEvents", + type: "mouseout", bubbles: false, cancelable: false, view: window, detail: 5, + screenX: -100, screenY: 300, clientX: 600, clientY: -500, + ctrlKey: true, altKey: false, shiftKey: true, metaKey: false, + button: 8, relatedTarget: document.getElementById("content") }, + + { createEventArg: "MouseEvent", + type: "mousemove", bubbles: false, cancelable: false, view: window, detail: 30, + screenX: 500, screenY: -100, clientX: -8888, clientY: -5000, + ctrlKey: true, altKey: false, shiftKey: true, metaKey: true, + button: 8, relatedTarget: document.getElementById("test") }, + + { createEventArg: "MouseEvent", + type: "foo", bubbles: false, cancelable: false, view: window, detail: 100, + screenX: 2000, screenY: 6000, clientX: 5000, clientY: 3000, + ctrlKey: true, altKey: true, shiftKey: true, metaKey: true, + button: 8, relatedTarget: document.getElementById("test") }, + ]; + + const kOtherModifierName = [ + "CapsLock", "NumLock", "ScrollLock", "Symbol", "SymbolLock", "Fn", "FnLock", "OS", "AltGraph" + ]; + + const kInvalidModifierName = [ + "shift", "control", "alt", "meta", "capslock", "numlock", "scrolllock", + "symbollock", "fn", "os", "altgraph", "Invalid", "Shift Control", + "Win", "Scroll" + ]; + + for (var i = 0; i < kTests.length; i++) { + var description = "testInitializingUntrustedEvent, Index: " + i + ", "; + const kTest = kTests[i]; + var e = document.createEvent(kTest.createEventArg); + e.initMouseEvent(kTest.type, kTest.bubbles, kTest.cancelable, kTest.view, + kTest.detail, kTest.screenX, kTest.screenY, kTest.clientX, kTest.clientY, + kTest.ctrlKey, kTest.altKey, kTest.shiftKey, kTest.metaKey, + kTest.button, kTest.relatedTarget); + + for (var attr in kTest) { + if (attr == "createEventArg") { + continue; + } + is(e[attr], kTest[attr], description + attr + " returns wrong value"); + } + is(e.isTrusted, false, description + "isTrusted returns wrong value"); + is(e.buttons, 0, description + "buttons returns wrong value"); + is(e.movementX, 0, description + "movementX returns wrong value"); + is(e.movementY, 0, description + "movementY returns wrong value"); + + // getModifierState() tests + is(e.getModifierState("Shift"), kTest.shiftKey, + description + "getModifierState(\"Shift\") returns wrong value"); + is(e.getModifierState("Control"), kTest.ctrlKey, + description + "getModifierState(\"Control\") returns wrong value"); + is(e.getModifierState("Alt"), kTest.altKey, + description + "getModifierState(\"Alt\") returns wrong value"); + is(e.getModifierState("Meta"), kTest.metaKey, + description + "getModifierState(\"Meta\") returns wrong value"); + + for (var j = 0; j < kOtherModifierName.length; j++) { + ok(!e.getModifierState(kOtherModifierName[j]), + description + "getModifierState(\"" + kOtherModifierName[j] + "\") returns wrong value"); + } + for (var k = 0; k < kInvalidModifierName.length; k++) { + ok(!e.getModifierState(kInvalidModifierName[k]), + description + "getModifierState(\"" + kInvalidModifierName[k] + "\") returns wrong value"); + } + } +} + +function runTests() +{ + testInitializingUntrustedEvent(); + SimpleTest.finish(); +} + +</script> +</pre> +</body> +</html> diff --git a/dom/events/test/test_dom_storage_event.html b/dom/events/test/test_dom_storage_event.html new file mode 100644 index 0000000000..88cc288d41 --- /dev/null +++ b/dom/events/test/test_dom_storage_event.html @@ -0,0 +1,62 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for DOM StorageEvent</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> + +const kTests = [ + { createEventArg: "StorageEvent", + type: "aaa", bubbles: true, cancelable: true, + key: null, oldValue: 'a', newValue: 'b', url: 'c', storageArea: null }, + + { createEventArg: "storageevent", + type: "bbb", bubbles: false, cancelable: true, + key: 'key', oldValue: null, newValue: 'b', url: 'c', storageArea: null }, + + { createEventArg: "Storageevent", + type: "ccc", bubbles: true, cancelable: false, + key: 'key', oldValue: 'a', newValue: null, url: 'c', storageArea: null }, + + { createEventArg: "storageEvent", + type: "ddd", bubbles: false, cancelable: false, + key: 'key', oldValue: 'a', newValue: 'b', url: null, storageArea: null }, + + { createEventArg: "StorageEvent", + type: "eee", bubbles: true, cancelable: true, + key: 'key', oldValue: 'a', newValue: 'b', url: 'c', storageArea: null }, + + { createEventArg: "storageevent", + type: "fff", bubbles: false, cancelable: true, + key: null, oldValue: null, newValue: null, url: null, storageArea: null }, + ]; + +for (var i = 0; i < kTests.length; i++) { + var description = "test, Index: " + i + ", "; + const kTest = kTests[i]; + var e = document.createEvent(kTest.createEventArg); + e.initStorageEvent(kTest.type, kTest.bubbles, kTest.cancelable, + kTest.key, kTest.oldValue, kTest.newValue, kTest.url, + kTest.storageArea); + + for (var attr in kTest) { + if (attr == 'createEventArg') + continue; + + is(e[attr], kTest[attr], description + attr + " returns wrong value"); + } + is(e.isTrusted, false, description + "isTrusted returns wrong value"); +} + +</script> +</pre> +</body> +</html> diff --git a/dom/events/test/test_dom_wheel_event.html b/dom/events/test/test_dom_wheel_event.html new file mode 100644 index 0000000000..a3dfc6f342 --- /dev/null +++ b/dom/events/test/test_dom_wheel_event.html @@ -0,0 +1,850 @@ +<!DOCTYPE HTML> +<html style="font-size: 32px;"> +<head> + <title>Test for D3E WheelEvent</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script src="/tests/SimpleTest/paint_listener.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<p id="display"></p> +<div id="scrollable" style="font-family: monospace; font-size: 16px; line-height: 1; overflow: auto; width: 200px; height: 200px;"> + <div id="scrolled" style="font-size: 64px; width: 5000px; height: 5000px;"> + Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text.<br> + Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text.<br> + Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text.<br> + Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text.<br> + Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text.<br> + Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text.<br> + Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text.<br> + Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text.<br> + Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text.<br> + Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text.<br> + Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text.<br> + Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text.<br> + Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text.<br> + Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text.<br> + Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text.<br> + Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text.<br> + Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text.<br> + Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text.<br> + Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text.<br> + Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text.<br> + Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text.<br> + Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text.<br> + Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text.<br> + Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text.<br> + Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text.<br> + Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text.<br> + </div> +</div> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(runTest, window); + +var gScrollableElement = document.getElementById("scrollable"); +var gScrolledElement = document.getElementById("scrolled"); + +var gLineHeight = 0; +var gHorizontalLine = 0; +var gPageHeight = 0; +var gPageWidth = 0; + +function sendWheelAndWait(aX, aY, aEvent) +{ + sendWheelAndPaint(gScrollableElement, aX, aY, aEvent, continueTest); +} + +function* prepareScrollUnits() +{ + var result = -1; + function handler(aEvent) + { + result = aEvent.detail; + aEvent.preventDefault(); + } + window.addEventListener("MozMousePixelScroll", handler, { capture: true, passive: false }); + + yield sendWheelAndWait(10, 10, + { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaY: 1.0, lineOrPageDeltaY: 1 }); + gLineHeight = result; + ok(gLineHeight > 10 && gLineHeight < 25, "prepareScrollUnits: gLineHeight may be illegal value, got " + gLineHeight); + + result = -1; + yield sendWheelAndWait(10, 10, + { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: 1.0, lineOrPageDeltaX: 1 }); + gHorizontalLine = result; + ok(gHorizontalLine > 5 && gHorizontalLine < 16, "prepareScrollUnits: gHorizontalLine may be illegal value, got " + gHorizontalLine); + + result = -1; + yield sendWheelAndWait(10, 10, + { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaY: 1.0, lineOrPageDeltaY: 1 }); + gPageHeight = result; + // XXX Cannot we know the actual scroll port size? + ok(gPageHeight >= 150 && gPageHeight <= 200, + "prepareScrollUnits: gPageHeight is strange value, got " + gPageHeight); + + result = -1; + yield sendWheelAndWait(10, 10, + { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: 1.0, lineOrPageDeltaX: 1 }); + gPageWidth = result; + ok(gPageWidth >= 150 && gPageWidth <= 200, + "prepareScrollUnits: gPageWidth is strange value, got " + gPageWidth); + + window.removeEventListener("MozMousePixelScroll", handler, true); +} + +function testMakingUntrustedEvent() +{ + const kCreateEventArgs = [ + "WheelEvent", "wheelevent", "wheelEvent", "Wheelevent" + ]; + + for (var i = 0; i < kCreateEventArgs.length; i++) { + try { + // We never support WheelEvent construction with document.createEvent(). + var event = document.createEvent(kCreateEventArgs[i]); + ok(false, "document.createEvent(" + kCreateEventArgs[i] + ") should throw an error"); + } catch (e) { + ok(true, "document.createEvent(" + kCreateEventArgs[i] + ") threw an error"); + } + } + + var wheelEvent = new WheelEvent("wheel"); + ok(wheelEvent instanceof WheelEvent, + "new WheelEvent() should create an instance of WheelEvent"); + ok(typeof(wheelEvent.initWheelEvent) != "function", + "WheelEvent must not have initWheelEvent()"); +} + +// delta_multiplier prefs should cause changing delta values of trusted events only. +// And also legacy events' detail value should be changed too. +function* testDeltaMultiplierPrefs() +{ + const kModifierAlt = 0x01; + const kModifierControl = 0x02; + const kModifierMeta = 0x04; + const kModifierShift = 0x08; + const kModifierWin = 0x10; + + const kTests = [ + { name: "default", + expected: [ 0, kModifierShift | kModifierAlt, kModifierShift | kModifierControl, + kModifierShift | kModifierMeta, kModifierShift | kModifierWin, + kModifierControl | kModifierAlt, kModifierMeta | kModifierAlt ], + unexpected: [ kModifierAlt, kModifierControl, kModifierMeta, kModifierShift, kModifierWin ] }, + { name: "with_alt", + expected: [ kModifierAlt ], + unexpected: [0, kModifierControl, kModifierMeta, kModifierShift, kModifierWin, + kModifierShift | kModifierAlt, kModifierControl | kModifierAlt, + kModifierMeta | kModifierAlt ] }, + { name: "with_control", + expected: [ kModifierControl ], + unexpected: [0, kModifierAlt, kModifierMeta, kModifierShift, kModifierWin, + kModifierShift | kModifierControl, kModifierControl | kModifierAlt, + kModifierMeta | kModifierControl ] }, + { name: "with_meta", + expected: [ kModifierMeta ], + unexpected: [0, kModifierAlt, kModifierControl, kModifierShift, kModifierWin, + kModifierShift | kModifierMeta, kModifierControl | kModifierMeta, + kModifierMeta | kModifierAlt ] }, + { name: "with_shift", + expected: [ kModifierShift ], + unexpected: [0, kModifierAlt, kModifierControl, kModifierMeta, kModifierWin, + kModifierShift | kModifierAlt, kModifierControl | kModifierShift, + kModifierMeta | kModifierShift ] }, + { name: "with_win", + expected: [ kModifierWin ], + unexpected: [0, kModifierAlt, kModifierControl, kModifierMeta, kModifierShift, + kModifierShift | kModifierWin ] }, + ]; + + // Note that this test doesn't support complicated lineOrPageDelta values which are computed with + // accumulated delta values by the prefs. If you need to test the lineOrPageDelta accumulation, + // use test_continuous_dom_wheel_event.html. + const kEvents = [ + { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: gHorizontalLine, deltaY: gLineHeight, deltaZ: gLineHeight, lineOrPageDeltaX: 1, lineOrPageDeltaY: 1 }, + { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: 1.0, deltaY: 1.0, deltaZ: 1.0, lineOrPageDeltaX: 1, lineOrPageDeltaY: 1 }, + { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: 1.0, deltaY: 1.0, deltaZ: 1.0, lineOrPageDeltaX: 1, lineOrPageDeltaY: 1 }, + { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: -gHorizontalLine, deltaY: -gLineHeight, deltaZ: -gLineHeight, lineOrPageDeltaX: -1, lineOrPageDeltaY: -1 }, + { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: -1.0, deltaY: -1.0, deltaZ: -1.0, lineOrPageDeltaX: -1, lineOrPageDeltaY: -1 }, + { deltaMode: WheelEvent.DOM_DELTA_LINE, skipDeltaModeCheck: true, + deltaX: -1.0, deltaY: -1.0, deltaZ: -1.0, lineOrPageDeltaX: -1, lineOrPageDeltaY: -1 }, + { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: -1.0, deltaY: -1.0, deltaZ: -1.0, lineOrPageDeltaX: -1, lineOrPageDeltaY: -1 }, + ]; + + const kDeltaMultiplierPrefs = [ + "delta_multiplier_x", "delta_multiplier_y", "delta_multiplier_z" + ]; + + const kPrefValues = [ + 200, 50, 0, -50, -150 + ]; + + var currentTest, currentModifiers, currentEvent, currentPref, currentMultiplier, testingExpected; + var expectedAsyncHandlerCalls; + var description; + var calledHandlers = { wheel: false, + DOMMouseScroll: { horizontal: false, vertical: false }, + MozMousePixelScroll: { horizontal: false, vertical: false } }; + + function deltaToPixels(mode, delta, horizontal) { + switch (mode) { + case WheelEvent.DOM_DELTA_PIXEL: + return delta; + case WheelEvent.DOM_DELTA_LINE: + return delta * (horizontal ? gHorizontalLine : gLineHeight); + case WheelEvent.DOM_DELTA_PAGE: + return delta * (horizontal ? gPageWidth : gPageHeight); + } + throw new Error("Unknown delta mode"); + } + + function defaultScrollMultiplier(horizontal) { + if (!SpecialPowers.getBoolPref("mousewheel.system_scroll_override.enabled")) { + return 1; + } + return SpecialPowers.getIntPref(`mousewheel.system_scroll_override.${horizontal ? "horizontal" : "vertical"}.factor`) / 100; + } + + function wheelEventHandler(aEvent) { + calledHandlers.wheel = true; + + var expectedDeltaX = currentEvent.deltaX; + var expectedDeltaY = currentEvent.deltaY; + var expectedDeltaZ = currentEvent.deltaZ; + + if (testingExpected) { + switch (currentPref.charAt(currentPref.length - 1)) { + case "x": + expectedDeltaX *= currentMultiplier; + break; + case "y": + expectedDeltaY *= currentMultiplier; + break; + case "z": + expectedDeltaZ *= currentMultiplier; + break; + } + } else if (aEvent.isTrusted && aEvent.deltaMode == WheelEvent.DOM_DELTA_LINE) { + expectedDeltaX *= defaultScrollMultiplier(true); + expectedDeltaY *= defaultScrollMultiplier(false); + } + is(aEvent.deltaMode, currentEvent.deltaMode, description + "deltaMode (" + currentEvent.deltaMode + ") was invalid"); + is(aEvent.deltaX, expectedDeltaX, description + "deltaX (" + currentEvent.deltaX + ") was invalid"); + is(aEvent.deltaY, expectedDeltaY, description + "deltaY (" + currentEvent.deltaY + ") was invalid"); + is(aEvent.deltaZ, expectedDeltaZ, description + "deltaZ (" + currentEvent.deltaZ + ") was invalid"); + is(aEvent.wheelDeltaX, -Math.round(aEvent.isTrusted ? 3 * deltaToPixels(aEvent.deltaMode, expectedDeltaX, true) : expectedDeltaX), `${description}wheelDeltaX (${aEvent.wheelDeltaX}) was invalid`); + is(aEvent.wheelDeltaY, -Math.round(aEvent.isTrusted ? 3 * deltaToPixels(aEvent.deltaMode, expectedDeltaY, false) : expectedDeltaY), `${description}wheelDeltaY (${aEvent.wheelDeltaY}) was invalid`); + is(aEvent.wheelDelta, aEvent.wheelDeltaY || aEvent.wheelDeltaX, description + "wheelDelta was invalid"); + + if (expectedAsyncHandlerCalls > 0 && --expectedAsyncHandlerCalls == 0) { + setTimeout(continueTest, 0); + } + } + + function legacyEventHandler(aEvent) { + var isHorizontal = (aEvent.axis == MouseScrollEvent.HORIZONTAL_AXIS); + var isScrollEvent = (aEvent.type == "DOMMouseScroll"); + if (isScrollEvent) { + if (isHorizontal) { + calledHandlers.DOMMouseScroll.horizontal = true; + } else { + calledHandlers.DOMMouseScroll.vertical = true; + } + } else { + if (isHorizontal) { + calledHandlers.MozMousePixelScroll.horizontal = true; + } else { + calledHandlers.MozMousePixelScroll.vertical = true; + } + } + var eventName = (isHorizontal ? "Horizontal " : "Vertical ") + aEvent.type + " "; + var expectedDetail; + if (isScrollEvent) { + expectedDetail = isHorizontal ? currentEvent.lineOrPageDeltaX : currentEvent.lineOrPageDeltaY; + if (currentEvent.deltaMode == WheelEvent.DOM_DELTA_PAGE && expectedDetail) { + expectedDetail = ((expectedDetail > 0) ? UIEvent.SCROLL_PAGE_DOWN : UIEvent.SCROLL_PAGE_UP); + } + } else { + expectedDetail = isHorizontal ? currentEvent.deltaX : currentEvent.deltaY; + if (expectedDetail) { + if (currentEvent.deltaMode == WheelEvent.DOM_DELTA_LINE) { + expectedDetail *= (isHorizontal ? gHorizontalLine : gLineHeight); + } else if (currentEvent.deltaMode == WheelEvent.DOM_DELTA_PAGE) { + if (expectedDetail > 0) { + expectedDetail = (isHorizontal ? gPageWidth : gPageHeight); + } else { + expectedDetail = (isHorizontal ? -gPageWidth : -gPageHeight); + } + } + } + } + if (testingExpected) { + if ((isHorizontal && currentPref.charAt(currentPref.length - 1) == "x") || + (!isHorizontal && currentPref.charAt(currentPref.length - 1) == "y")) { + // If it's a page scroll event, the detail value is UIEvent.SCROLL_PAGE_DOWN or + // UIEvent.SCROLL_PAGE_UP. If the delta value sign is reverted, we need to + // revert the expected detail value too. Otherwise, don't touch it. + if (isScrollEvent && currentEvent.deltaMode == WheelEvent.DOM_DELTA_PAGE) { + if (currentMultiplier < 0) { + expectedDetail = ((expectedDetail == UIEvent.SCROLL_PAGE_UP) ? UIEvent.SCROLL_PAGE_DOWN : UIEvent.SCROLL_PAGE_UP); + } + } else { + expectedDetail *= currentMultiplier; + expectedDetail = expectedDetail < 0 ? Math.ceil(expectedDetail) : Math.floor(expectedDetail); + } + } + } + is(aEvent.detail, expectedDetail, description + eventName + "detail was invalid"); + + aEvent.preventDefault(); + + if (expectedAsyncHandlerCalls > 0 && --expectedAsyncHandlerCalls == 0) { + setTimeout(continueTest, 0); + } + } + + window.addEventListener("wheel", wheelEventHandler, true); + window.addEventListener("DOMMouseScroll", legacyEventHandler, true); + window.addEventListener("MozMousePixelScroll", legacyEventHandler, true); + + function* dispatchEvent(aIsExpected) { + for (var i = 0; i < kEvents.length; i++) { + currentEvent = kEvents[i]; + currentEvent.shiftKey = (currentModifiers & kModifierShift) != 0; + currentEvent.ctrlKey = (currentModifiers & kModifierControl) != 0; + currentEvent.altKey = (currentModifiers & kModifierAlt) != 0; + currentEvent.metaKey = (currentModifiers & kModifierMeta) != 0; + currentEvent.osKey = (currentModifiers & kModifierWin) != 0; + var modifierList = ""; + if (currentEvent.shiftKey) { + modifierList += "Shift "; + } + if (currentEvent.ctrlKey) { + modifierList += "Control "; + } + if (currentEvent.altKey) { + modifierList += "Alt "; + } + if (currentEvent.metaKey) { + modifierList += "Meta "; + } + if (currentEvent.osKey) { + modifierList += "Win "; + } + + for (var j = 0; j < kPrefValues.length; j++) { + currentMultiplier = kPrefValues[j] / 100; + for (var k = 0; k < kDeltaMultiplierPrefs.length; k++) { + currentPref = "mousewheel." + currentTest.name + "." + kDeltaMultiplierPrefs[k]; + + yield SpecialPowers.pushPrefEnv({"set": [[currentPref, kPrefValues[j]]]}, continueTest); + + gScrollableElement.scrollTop = gScrollableElement.scrollBottom = 1000; + + // trusted event's delta valuses should be reverted by the pref. + testingExpected = aIsExpected; + + var expectedProps = { + deltaX: currentEvent.deltaX * currentMultiplier, + deltaY: currentEvent.deltaY * currentMultiplier, + dletaZ: currentEvent.deltaZ * currentMultiplier, + lineOrPageDeltaX: currentEvent.lineOrPageDeltaX * currentMultiplier, + lineOrPageDeltaY: currentEvent.lineOrPageDeltaY * currentMultiplier, + }; + + var expectedWheel = expectedProps.deltaX != 0 || expectedProps.deltaY != 0 || expectedProps.deltaZ != 0; + var expectedDOMMouseX = expectedProps.lineOrPageDeltaX >= 1 || expectedProps.lineOrPageDeltaX <= -1; + var expectedDOMMouseY = expectedProps.lineOrPageDeltaY >= 1 || expectedProps.lineOrPageDeltaY <= -1; + var expectedMozMouseX = expectedProps.deltaX >= 1 || expectedProps.deltaX <= -1; + var expectedMozMouseY = expectedProps.deltaY >= 1 || expectedProps.deltaY <= -1; + + expectedAsyncHandlerCalls = 0; + if (expectedWheel) ++expectedAsyncHandlerCalls; + if (expectedDOMMouseX) ++expectedAsyncHandlerCalls; + if (expectedDOMMouseY) ++expectedAsyncHandlerCalls; + if (expectedMozMouseX) ++expectedAsyncHandlerCalls; + if (expectedMozMouseY) ++expectedAsyncHandlerCalls; + + description = "testDeltaMultiplierPrefs, pref: " + currentPref + "=" + kPrefValues[j] + + ", deltaMode: " + currentEvent.deltaMode + ", modifiers: \"" + modifierList + "\", (trusted event): "; + yield synthesizeWheel(gScrollableElement, 10, 10, currentEvent); + + is(calledHandlers.wheel, + expectedWheel, + description + "wheel event was (not) fired"); + is(calledHandlers.DOMMouseScroll.horizontal, + expectedDOMMouseX, + description + "Horizontal DOMMouseScroll event was (not) fired"); + is(calledHandlers.DOMMouseScroll.vertical, + expectedDOMMouseY, + description + "Vertical DOMMouseScroll event was (not) fired"); + is(calledHandlers.MozMousePixelScroll.horizontal, + expectedMozMouseX, + description + "Horizontal MozMousePixelScroll event was (not) fired"); + is(calledHandlers.MozMousePixelScroll.vertical, + expectedMozMouseY, + description + "Vertical MozMousePixelScroll event was (not) fired"); + + calledHandlers = { wheel: false, + DOMMouseScroll: { horizontal: false, vertical: false }, + MozMousePixelScroll: { horizontal: false, vertical: false } }; + + // untrusted event's delta values shouldn't be reverted by the pref. + testingExpected = false; + var props = { + bubbles: true, + cancelable: true, + shiftKey: currentEvent.shiftKey, + ctrlKey: currentEvent.ctrlKey, + altKey: currentEvent.altKey, + metaKey: currentEvent.metaKey, + deltaX: currentEvent.deltaX, + deltaY: currentEvent.deltaY, + deltaZ: currentEvent.deltaZ, + deltaMode: currentEvent.deltaMode, + }; + var untrustedEvent = new WheelEvent("wheel", props); + + description = "testDeltaMultiplierPrefs, pref: " + currentPref + "=" + kPrefValues[j] + + ", deltaMode: " + currentEvent.deltaMode + ", modifiers: \"" + modifierList + "\", (untrusted event): "; + gScrollableElement.dispatchEvent(untrustedEvent); + + ok(calledHandlers.wheel, description + "wheel event was not fired for untrusted event"); + ok(!calledHandlers.DOMMouseScroll.horizontal, + description + "Horizontal DOMMouseScroll event was fired for untrusted event"); + ok(!calledHandlers.DOMMouseScroll.vertical, + description + "Vertical DOMMouseScroll event was fired for untrusted event"); + ok(!calledHandlers.MozMousePixelScroll.horizontal, + description + "Horizontal MozMousePixelScroll event was fired for untrusted event"); + ok(!calledHandlers.MozMousePixelScroll.vertical, + description + "Vertical MozMousePixelScroll event was fired for untrusted event"); + + yield SpecialPowers.pushPrefEnv({"set": [[currentPref, 100]]}, continueTest); + + calledHandlers = { wheel: false, + DOMMouseScroll: { horizontal: false, vertical: false }, + MozMousePixelScroll: { horizontal: false, vertical: false } }; + + } + // We should skip other value tests if testing with modifier key. + // If we didn't do so, it would test too many times, but we don't need to do so. + if (kTests.name != "default") { + break; + } + } + } + } + + for (var i = 0; i < kTests.length; i++) { + currentTest = kTests[i]; + for (var j = 0; j < currentTest.expected.length; j++) { + currentModifiers = currentTest.expected[j]; + yield* dispatchEvent(true); + } + for (var k = 0; k < currentTest.unexpected.length; k++) { + currentModifiers = currentTest.unexpected[k]; + yield* dispatchEvent(false); + } + } + + window.removeEventListener("wheel", wheelEventHandler, true); + window.removeEventListener("DOMMouseScroll", legacyEventHandler, true); + window.removeEventListener("MozMousePixelScroll", legacyEventHandler, true); +} + +// Untrusted wheel events shouldn't cause legacy mouse scroll events. +function testDispatchingUntrustEvent() +{ + var descriptionBase = "testDispatchingUntrustEvent, "; + var description, wheelEventFired; + function wheelEventHandler(aEvent) + { + wheelEventFired = true; + } + + function legacyEventHandler(aEvent) + { + ok(false, aEvent.type + " must not be fired"); + } + + window.addEventListener("wheel", wheelEventHandler, true); + window.addEventListener("DOMMouseScroll", legacyEventHandler, true); + window.addEventListener("MozMousePixelScroll", legacyEventHandler, true); + + description = descriptionBase + "dispatching a pixel wheel event: "; + wheelEventFired = false; + var untrustedPixelEvent = new WheelEvent("wheel", { + bubbles: true, cancelable: true, + deltaX: 24.0, deltaY: 24.0, + deltaMode: WheelEvent.DOM_DELTA_PIXEL, + }); + gScrolledElement.dispatchEvent(untrustedPixelEvent); + ok(wheelEventFired, description + "wheel event wasn't fired"); + + description = descriptionBase + "dispatching a line wheel event: "; + wheelEventFired = false; + var untrustedLineEvent = new WheelEvent("wheel", { + bubbles: true, cancelable: true, + deltaX: 3.0, deltaY: 3.0, + deltaMode: WheelEvent.DOM_DELTA_LINE, + }); + gScrolledElement.dispatchEvent(untrustedLineEvent); + ok(wheelEventFired, description + "wheel event wasn't fired"); + + description = descriptionBase + "dispatching a page wheel event: "; + wheelEventFired = false; + var untrustedPageEvent = new WheelEvent("wheel", { + bubbles: true, cancelable: true, + deltaX: 1.0, deltaY: 1.0, + deltaMode: WheelEvent.DOM_DELTA_PAGE, + }); + gScrolledElement.dispatchEvent(untrustedPageEvent); + ok(wheelEventFired, description + "wheel event wasn't fired"); + + window.removeEventListener("wheel", wheelEventHandler, true); + window.removeEventListener("DOMMouseScroll", legacyEventHandler, true); + window.removeEventListener("MozMousePixelScroll", legacyEventHandler, true); +} + +function* testEventOrder() +{ + const kWheelEvent = 0x0001; + const kDOMMouseScrollEvent = 0x0002; + const kMozMousePixelScrollEvent = 0x0004; + const kVerticalScrollEvent = 0x0010; + const kHorizontalScrollEvent = 0x0020; + const kInSystemGroup = 0x0100; + const kDefaultPrevented = 0x1000; + + var currentTest; + + const kTests = [ + { + description: "Testing the order of the events without preventDefault()", + expectedEvents: [ kWheelEvent, + kDOMMouseScrollEvent | kVerticalScrollEvent, + kDOMMouseScrollEvent | kVerticalScrollEvent | kInSystemGroup, + kMozMousePixelScrollEvent | kVerticalScrollEvent, + kMozMousePixelScrollEvent | kVerticalScrollEvent | kInSystemGroup, + kDOMMouseScrollEvent | kHorizontalScrollEvent, + kDOMMouseScrollEvent | kHorizontalScrollEvent | kInSystemGroup, + kMozMousePixelScrollEvent | kHorizontalScrollEvent, + kMozMousePixelScrollEvent | kHorizontalScrollEvent | kInSystemGroup, + kWheelEvent | kInSystemGroup], + resultEvents: [], + doPreventDefaultAt: 0, + }, + { + description: "Testing the order of the events, calling preventDefault() at default group wheel event", + expectedEvents: [ kWheelEvent, + kWheelEvent | kInSystemGroup | kDefaultPrevented], + resultEvents: [], + doPreventDefaultAt: kWheelEvent, + }, + { + description: "Testing the order of the events, calling preventDefault() at default group DOMMouseScroll event", + expectedEvents: [ kWheelEvent, + kDOMMouseScrollEvent | kVerticalScrollEvent, + kDOMMouseScrollEvent | kVerticalScrollEvent | kInSystemGroup | kDefaultPrevented, + kMozMousePixelScrollEvent | kVerticalScrollEvent | kDefaultPrevented, + kMozMousePixelScrollEvent | kVerticalScrollEvent | kInSystemGroup | kDefaultPrevented, + kDOMMouseScrollEvent | kHorizontalScrollEvent, + kDOMMouseScrollEvent | kHorizontalScrollEvent | kInSystemGroup, + kMozMousePixelScrollEvent | kHorizontalScrollEvent, + kMozMousePixelScrollEvent | kHorizontalScrollEvent | kInSystemGroup, + kWheelEvent | kInSystemGroup | kDefaultPrevented], + resultEvents: [], + doPreventDefaultAt: kDOMMouseScrollEvent | kVerticalScrollEvent, + }, + { + description: "Testing the order of the events, calling preventDefault() at default group MozMousePixelScroll event", + expectedEvents: [ kWheelEvent, + kDOMMouseScrollEvent | kVerticalScrollEvent, + kDOMMouseScrollEvent | kVerticalScrollEvent | kInSystemGroup, + kMozMousePixelScrollEvent | kVerticalScrollEvent, + kMozMousePixelScrollEvent | kVerticalScrollEvent | kInSystemGroup | kDefaultPrevented, + kDOMMouseScrollEvent | kHorizontalScrollEvent, + kDOMMouseScrollEvent | kHorizontalScrollEvent | kInSystemGroup, + kMozMousePixelScrollEvent | kHorizontalScrollEvent, + kMozMousePixelScrollEvent | kHorizontalScrollEvent | kInSystemGroup, + kWheelEvent | kInSystemGroup | kDefaultPrevented], + resultEvents: [], + doPreventDefaultAt: kMozMousePixelScrollEvent | kVerticalScrollEvent, + }, + { + description: "Testing the order of the events, calling preventDefault() at system group DOMMouseScroll event", + expectedEvents: [ kWheelEvent, + kDOMMouseScrollEvent | kVerticalScrollEvent, + kDOMMouseScrollEvent | kVerticalScrollEvent | kInSystemGroup, + kMozMousePixelScrollEvent | kVerticalScrollEvent | kDefaultPrevented, + kMozMousePixelScrollEvent | kVerticalScrollEvent | kInSystemGroup | kDefaultPrevented, + kDOMMouseScrollEvent | kHorizontalScrollEvent, + kDOMMouseScrollEvent | kHorizontalScrollEvent | kInSystemGroup, + kMozMousePixelScrollEvent | kHorizontalScrollEvent, + kMozMousePixelScrollEvent | kHorizontalScrollEvent | kInSystemGroup, + kWheelEvent | kInSystemGroup | kDefaultPrevented], + resultEvents: [], + doPreventDefaultAt: kDOMMouseScrollEvent | kVerticalScrollEvent | kInSystemGroup, + }, + { + description: "Testing the order of the events, calling preventDefault() at system group MozMousePixelScroll event", + expectedEvents: [ kWheelEvent, + kDOMMouseScrollEvent | kVerticalScrollEvent, + kDOMMouseScrollEvent | kVerticalScrollEvent | kInSystemGroup, + kMozMousePixelScrollEvent | kVerticalScrollEvent, + kMozMousePixelScrollEvent | kVerticalScrollEvent | kInSystemGroup, + kDOMMouseScrollEvent | kHorizontalScrollEvent, + kDOMMouseScrollEvent | kHorizontalScrollEvent | kInSystemGroup, + kMozMousePixelScrollEvent | kHorizontalScrollEvent, + kMozMousePixelScrollEvent | kHorizontalScrollEvent | kInSystemGroup, + kWheelEvent | kInSystemGroup | kDefaultPrevented], + resultEvents: [], + doPreventDefaultAt: kMozMousePixelScrollEvent | kVerticalScrollEvent | kInSystemGroup, + }, + ]; + + function getEventDescription(aEvent) + { + var result = ""; + if (aEvent & kWheelEvent) { + result = "wheel" + } else { + if (aEvent & kDOMMouseScrollEvent) { + result = "DOMMouseScroll"; + } else if (aEvent & kMozMousePixelScrollEvent) { + result = "MozMousePixelScroll"; + } + if (aEvent & kVerticalScrollEvent) { + result += ", vertical"; + } else { + result += ", horizontal"; + } + } + if (aEvent & kInSystemGroup) { + result += ", system group"; + } + if (aEvent & kDefaultPrevented) { + result += ", defaultPrevented"; + } + return result; + } + + function pushEvent(aEvent, aIsSystemGroup) + { + var event = 0; + if (aEvent.type == "wheel") { + event = kWheelEvent; + } else { + if (aEvent.type == "DOMMouseScroll") { + event = kDOMMouseScrollEvent; + } else if (aEvent.type == "MozMousePixelScroll") { + event = kMozMousePixelScrollEvent; + } + if (aEvent.axis == MouseScrollEvent.HORIZONTAL_AXIS) { + event |= kHorizontalScrollEvent; + } else { + event |= kVerticalScrollEvent; + } + } + if (aIsSystemGroup) { + event |= kInSystemGroup; + } + if (aEvent.defaultPrevented) { + event |= kDefaultPrevented; + } + currentTest.resultEvents.push(event); + + if (event == currentTest.doPreventDefaultAt) { + aEvent.preventDefault(); + } + + if (currentTest.resultEvents.length == currentTest.expectedEvents.length) { + setTimeout(continueTest, 0); + } + } + + function handler(aEvent) + { + pushEvent(aEvent, false); + } + + function systemHandler(aEvent) + { + pushEvent(aEvent, true); + } + + window.addEventListener("wheel", handler, { capture: true, passive: false }); + window.addEventListener("DOMMouseScroll", handler, { capture: true, passive: false }); + window.addEventListener("MozMousePixelScroll", handler, { capture: true, passive: false }); + + SpecialPowers.addSystemEventListener(window, "wheel", systemHandler, true); + SpecialPowers.addSystemEventListener(window, "DOMMouseScroll", systemHandler, true); + SpecialPowers.addSystemEventListener(window, "MozMousePixelScroll", systemHandler, true); + + for (var i = 0; i < kTests.length; i++) { + currentTest = kTests[i]; + yield synthesizeWheel(gScrollableElement, 10, 10, + { deltaMode: WheelEvent.DOM_DELTA_LINE, deltaX: 1.0, deltaY: 1.0 }); + + for (var j = 0; j < currentTest.expectedEvents.length; j++) { + if (currentTest.resultEvents.length == j) { + ok(false, currentTest.description + ": " + + getEventDescription(currentTest.expectedEvents[j]) + " wasn't fired"); + break; + } + is(getEventDescription(currentTest.resultEvents[j]), + getEventDescription(currentTest.expectedEvents[j]), + currentTest.description + ": " + (j + 1) + "th event is mismatched"); + } + if (currentTest.expectedEvents.length < currentTest.resultEvents.length) { + ok(false, currentTest.description + ": " + + getEventDescription(currentTest.resultEvents[currentTest.expectedEvents.length]) + + " was fired unexpectedly"); + } + } + + window.removeEventListener("wheel", handler, true); + window.removeEventListener("DOMMouseScroll", handler, true); + window.removeEventListener("MozMousePixelScroll", handler, true); + + SpecialPowers.removeSystemEventListener(window, "wheel", systemHandler, true); + SpecialPowers.removeSystemEventListener(window, "DOMMouseScroll", systemHandler, true); + SpecialPowers.removeSystemEventListener(window, "MozMousePixelScroll", systemHandler, true); +} + +var gOnWheelAttrHandled = new Array; +var gOnWheelAttrCount = 0; + +function* testOnWheelAttr() +{ + function onWheelHandledString(attr) { + return `gOnWheelAttrHandled['${attr}'] = true; + ++gOnWheelAttrCount; + if (gOnWheelAttrCount == 3) { + setTimeout(continueTest, 0); + };`; + } + + document.documentElement.setAttribute("onwheel", onWheelHandledString("html")); + document.body.setAttribute("onwheel", onWheelHandledString("body")); + gScrollableElement.setAttribute("onwheel", onWheelHandledString("div")); + var target = document.getElementById("onwheel"); + yield synthesizeWheel(gScrollableElement, 10, 10, + { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: 1.0, deltaY: 2.0 }); + ok(gOnWheelAttrHandled.html, "html element's onwheel attribute isn't performed"); + ok(gOnWheelAttrHandled.body, "body element's onwheel attribute isn't performed"); + ok(gOnWheelAttrHandled.div, "div element's onwheel attribute isn't performed"); +} + +var gOnWheelPropHandled = new Array; +var gOnWheelPropCount = 0; + +function* testOnWheelProperty() +{ + const handleOnWheelProp = prop => e => { + gOnWheelPropHandled[prop] = true; + ++gOnWheelPropCount; + if (gOnWheelPropCount == 5) { + setTimeout(continueTest, 0); + } + } + + window.onwheel = handleOnWheelProp('window'); + document.onwheel = handleOnWheelProp('document'); + document.documentElement.onwheel = handleOnWheelProp('html'); + document.body.onwheel = handleOnWheelProp('body'); + gScrollableElement.onwheel = handleOnWheelProp('div'); + + var target = document.getElementById("onwheel"); + yield synthesizeWheel(gScrollableElement, 10, 10, + { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: 1.0, deltaY: 2.0 }); + + ok(gOnWheelPropHandled.window, "window's onwheel property isn't performed"); + ok(gOnWheelPropHandled.document, "document's onwheel property isn't performed"); + ok(gOnWheelPropHandled.html, "html element's onwheel property isn't performed"); + ok(gOnWheelPropHandled.body, "body element's onwheel property isn't performed"); + ok(gOnWheelPropHandled.div, "div element's onwheel property isn't performed"); +} + +function* testBody() +{ + yield* prepareScrollUnits(); + testMakingUntrustedEvent(); + yield* testDeltaMultiplierPrefs(); + testDispatchingUntrustEvent(); + yield* testEventOrder(); + yield* testOnWheelAttr(); + yield* testOnWheelProperty(); +} + +var gTestContinuation = null; + +function continueTest() +{ + if (!gTestContinuation) { + gTestContinuation = testBody(); + } + var ret = gTestContinuation.next(); + if (ret.done) { + SimpleTest.finish(); + } +} + +function runTest() +{ + SpecialPowers.pushPrefEnv({"set": [ + // FIXME(emilio): This test is broken in HiDPI, unclear if + // MozMousePixelScroll is not properly converting to CSS pixels, or + // whether sendWheelAndWait expectes device rather than CSS pixels, or + // something else. + ["layout.css.devPixelsPerPx", 1.0], + + ["dom.event.wheel-deltaMode-lines.disabled", true], + + ["mousewheel.default.delta_multiplier_x", 100], + ["mousewheel.default.delta_multiplier_y", 100], + ["mousewheel.default.delta_multiplier_z", 100], + ["mousewheel.with_alt.delta_multiplier_x", 100], + ["mousewheel.with_alt.delta_multiplier_y", 100], + ["mousewheel.with_alt.delta_multiplier_z", 100], + ["mousewheel.with_control.delta_multiplier_x", 100], + ["mousewheel.with_control.delta_multiplier_y", 100], + ["mousewheel.with_control.delta_multiplier_z", 100], + ["mousewheel.with_meta.delta_multiplier_x", 100], + ["mousewheel.with_meta.delta_multiplier_y", 100], + ["mousewheel.with_meta.delta_multiplier_z", 100], + ["mousewheel.with_shift.delta_multiplier_x", 100], + ["mousewheel.with_shift.delta_multiplier_y", 100], + ["mousewheel.with_shift.delta_multiplier_z", 100], + ["mousewheel.with_win.delta_multiplier_x", 100], + ["mousewheel.with_win.delta_multiplier_y", 100], + ["mousewheel.with_win.delta_multiplier_z", 100], + // TODO: Need to add passive to SpecialPowers.addSystemEventListener + // somehow. + ["dom.event.default_to_passive_wheel_listeners", false], + ]}, continueTest); +} +</script> +</pre> +</body> +</html> diff --git a/dom/events/test/test_drag_coords.html b/dom/events/test/test_drag_coords.html new file mode 100644 index 0000000000..2db65a66f9 --- /dev/null +++ b/dom/events/test/test_drag_coords.html @@ -0,0 +1,52 @@ +<!doctype html> +<title>Test for drag event coordinates</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<script src="/tests/SimpleTest/EventUtils.js"></script> +<link rel="stylesheet" href="/tests/SimpleTest/test.css"/> +<style> + #draggable { + display: inline-block; + border: 1px solid; + } +</style> +<div draggable="true" id="draggable">Drag me</div> +<script> +add_task(async function test_drag_coords() { + await SpecialPowers.contentTransformsReceived(window); + + let target = document.getElementById("draggable"); + let coords = {}; + let promises = []; + for (let type of ["dragstart", "dragend"]) { + promises.push(new Promise(function(resolve) { + target.addEventListener(type, function(e) { + info("Got " + e.type); + coords[e.type] = { + screen: { + x: e.screenX, + y: e.screenY, + }, + client: { + x: e.clientX, + y: e.clientY, + }, + }; + resolve(); + }); + })); + } + synthesizePlainDragAndDrop({ + srcElement: target, + srcX: 2, + srcY: 2, + stepX: 10, + stepY: 10, + }); + await Promise.all(promises); + info(JSON.stringify(coords)); + for (let coordType of ["screen", "client"]) { + is(coords.dragend[coordType].x, coords.dragstart[coordType].x + 12, `x ${coordType} is correct`); + is(coords.dragend[coordType].y, coords.dragstart[coordType].y + 12, `y ${coordType} is correct`); + } +}); +</script> diff --git a/dom/events/test/test_drag_image_file.html b/dom/events/test/test_drag_image_file.html new file mode 100644 index 0000000000..13e6996693 --- /dev/null +++ b/dom/events/test/test_drag_image_file.html @@ -0,0 +1,56 @@ +<!DOCTYPE html> +<html> +<head> + <meta charset="utf-8"> + <title>Test that dragging an image produces a File</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<img id="green-png" src="green.png"> + +<script> +async function runTest() { + await SpecialPowers.pushPrefEnv({ + set: [["dom.events.dataTransfer.imageAsFile.enabled", true]], + }); + + let dt = await synthesizePlainDragAndCancel({ + srcElement: document.getElementById('green-png'), + finalY: 20, + }, null); + + info(`DataTransfer types: ${dt.types}`); + + for (let type of dt.types) { + info(`getData(${type}) = ${dt.getData(type)}`) + } + + ok(dt.types.includes("Files"), "types should contains 'Files'"); + is(dt.files.length, 1, "files contains one File"); + + let fileItem = null; + for (let item of dt.items) { + if (item.kind === "file") { + fileItem = item; + break; + } + } + + is(fileItem.kind, "file", "Is a file"); + is(fileItem.type, "image/png", "Is a PNG file"); + + let file = fileItem.getAsFile(); + is(file.name, "image.png", "Has generic image name") + ok(file.size > 100, "Is not empty"); + + SimpleTest.finish(); +} + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(runTest); +</script> +</body> +</html> diff --git a/dom/events/test/test_draggableprop.html b/dom/events/test/test_draggableprop.html new file mode 100644 index 0000000000..af2d9906c2 --- /dev/null +++ b/dom/events/test/test_draggableprop.html @@ -0,0 +1,93 @@ +<html> +<head> + <title>Tests for the draggable property on HTML elements</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"> + <script type="application/javascript" + src="/tests/SimpleTest/SimpleTest.js"></script> + +<body> +<p id="display"></p> +<div id="content" style="display: none"> +</div> + +<span id="elem1">One</span> +<span id="elem2" draggable="true">Two</span> +<span id="elem3" draggable="">Three</span> +<span id="elem4" draggable="false">Four</span> +<span id="elem5" draggable="other">Five</span> +<span id="elem6" draggable="false"><span style="user-select: none">Six</span></span> + +<img id="img1" src="../happy.png"> +<img id="img2" src="../happy.png" draggable="true"> +<img id="img3" src="../happy.png" draggable=""> +<img id="img4" src="../happy.png" draggable="false"> +<img id="img5" src="../happy.png" draggable="other"> +<img id="img6" src="../happy.png" draggable="false"> + +<a id="a1">One</a> +<a id="a2" draggable="true">Two</a> +<a id="a3" draggable="">Three</a> +<a id="a4" draggable="false">Four</a> +<a id="a5" draggable="other">Five</a> +<a id="a6" draggable="false"><span style="user-select: none">Six</span></a> + +<a id="ahref1" href="http://www.mozilla.org">One</a> +<a id="ahref2" href="http://www.mozilla.org" draggable="true">Two</a> +<a id="ahref3" href="http://www.mozilla.org" draggable="">Three</a> +<a id="ahref4" href="http://www.mozilla.org" draggable="false">Four</a> +<a id="ahref5" href="http://www.mozilla.org" draggable="other">Five</a> +<a id="ahref6" href="http://www.mozilla.org" draggable="false"><span style="user-select: none">Six</span></a> + +<script> +function check() +{ + try { + checkElements(1, false, true, false, true); + checkElements(2, true, true, true, true); + checkElements(3, false, true, false, true); + checkElements(4, false, false, false, false); + checkElements(5, false, true, false, true); + checkElements(6, false, false, false, false); + } catch (ex) { + is("script error", ex, "fail"); + } +} + +function checkElements(idx, estate, istate, astate, ahrefstate) +{ + checkElement("elem" + idx, estate, false); + checkElement("img" + idx, istate, true); + checkElement("a" + idx, astate, false); + checkElement("ahref" + idx, ahrefstate, true); +} + +function checkElement(elemid, state, statedef) +{ + var elem = document.getElementById(elemid); + + is(elem.draggable, state, elemid + "-initial"); + elem.draggable = true; + is(elem.draggable, true, elemid + "-true"); + elem.draggable = false; + is(elem.draggable, false, elemid + "-false"); + + elem.setAttribute("draggable", "true"); + is(elem.draggable, true, elemid + "-attr-true"); + elem.setAttribute("draggable", "false"); + is(elem.draggable, false, elemid + "-attr-false"); + elem.setAttribute("draggable", "other"); + is(elem.draggable, statedef, elemid + "-attr-other"); + elem.setAttribute("draggable", ""); + is(elem.draggable, statedef, elemid + "-attr-empty"); + elem.removeAttribute("draggable"); + is(elem.draggable, statedef, elemid + "-attr-removed"); +} + +check(); + +</script> + +</body> +</html> + + diff --git a/dom/events/test/test_dragstart.html b/dom/events/test/test_dragstart.html new file mode 100644 index 0000000000..53040fbf72 --- /dev/null +++ b/dom/events/test/test_dragstart.html @@ -0,0 +1,716 @@ +<html> +<head> + <title>Tests for the dragstart event</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + +<!-- + This test checks the dragstart event and the DataTransfer object + --> + +<script> + +SimpleTest.waitForExplicitFinish(); + +var gDragInfo; +var gDataTransfer = null; +var gExtraDragTests = 0; + +function runTests() +{ + // first, create a selection and try dragging it + var draggable = $("draggable"); + window.getSelection().selectAllChildren(draggable); + synthesizeMouse(draggable, 6, 6, { type: "mousedown" }); + synthesizeMouse(draggable, 14, 14, { type: "mousemove" }); + // drags are asynchronous on Linux, so this extra event is needed to make + // sure the drag gets processed + synthesizeMouse(draggable, 15, 15, { type: "mousemove" }); +} + +function afterDragTests() +{ + // the dragstart should have occurred due to moving the mouse. gDataTransfer + // caches the dataTransfer that was used, however it should now be empty and + // be read only. + ok(gDataTransfer instanceof DataTransfer, "DataTransfer after dragstart event"); + checkTypes(gDataTransfer, [], 0, "after dragstart event"); + + expectError(() => gDataTransfer.setData("text/plain", "Some Text"), + "NoModificationAllowedError", "setData when read only"); + expectError(() => gDataTransfer.clearData("text/plain"), + "NoModificationAllowedError", "clearData when read only"); + expectError(() => gDataTransfer.addElement(draggable), + "NoModificationAllowedError", "addElement when read only"); + + var evt = document.createEvent("dragevent"); + ok(evt instanceof DragEvent, "synthetic dragevent class") + ok(evt instanceof MouseEvent, "synthetic event inherits from MouseEvent") + evt.initDragEvent("dragstart", true, true, window, 1, 40, 35, 20, 15, + false, true, false, false, 0, null, null); + $("synthetic").dispatchEvent(evt); + + var evt = document.createEvent("dragevent"); + ok(evt instanceof DragEvent, "synthetic dragevent class") + evt.initDragEvent("dragover", true, true, window, 0, 40, 35, 20, 15, + true, false, true, true, 2, document.documentElement, null); + $("synthetic2").dispatchEvent(evt); + + // next, dragging links and images + sendMouseEventsForDrag("link"); + sendMouseEventsForDrag("image"); + +// disable testing input dragging for now, as it doesn't seem to be testable +// draggable = $("input"); +// draggable.setSelectionRange(0, 4); +// synthesizeMouse(draggable, 8, 8, { type: "mousedown" }); +// synthesizeMouse(draggable, 15, 15, { type: "mousemove" }); +// sendMouseEventsForDrag("input"); + + // draggable elements inside a shadow root + sendMouseEventsForShadowRootDrag("shadow_host_containing_draggable"); + sendMouseEventsForShadowRootDrag("shadow_host_containing_image"); + + // next, check if the draggable attribute can be used to adjust the drag target + gDragInfo = { target: $("dragtrue"), testid: "draggable true node" }; + sendMouseEventsForDrag("dragtrue"); + gDragInfo = { target: $("dragtrue"), testid: "draggable true child" }; + sendMouseEventsForDrag("spantrue"); + + gDragInfo = { target: $("dragfalse").firstChild, testid: "draggable false node" }; + sendMouseEventsForDrag("dragfalse"); + synthesizeMouse(draggable, 12, 12, { type: "mouseup" }); + + gDragInfo = { target: $("spanfalse").firstChild, testid: "draggable false child" }; + sendMouseEventsForDrag("spanfalse"); + synthesizeMouse(draggable, 12, 12, { type: "mouseup" }); + + gDragInfo = { target: $("userselectnone").firstChild, testid: "user select none inside draggable false node" }; + sendMouseEventsForDrag("userselectnone"); + synthesizeMouse(draggable, 12, 12, { type: "mouseup" }); + + gDragInfo = { target: $("draggable_with_undraggable_descendant"), testid: "undraggable inside draggable" }; + sendMouseEventsForDrag("undraggable_inside_draggable"); + synthesizeMouse(draggable, 12, 12, { type: "mouseup" }); + + sendMouseEventsForDrag("link_to_file"); + synthesizeMouse(draggable, 12, 12, { type: "mouseup" }); + + sendMouseEventsForDrag("broken_file_image"); + synthesizeMouse(draggable, 12, 12, { type: "mouseup" }); + + if (navigator.platform.startsWith("Win")) { + sendMouseEventsForDrag("link_to_windows_file"); + synthesizeMouse(draggable, 12, 12, { type: "mouseup" }); + + sendMouseEventsForDrag("broken_windows_file_image"); + synthesizeMouse(draggable, 12, 12, { type: "mouseup" }); + + sendMouseEventsForDrag("link_to_backslash_windows_file"); + synthesizeMouse(draggable, 12, 12, { type: "mouseup" }); + + sendMouseEventsForDrag("broken_backslash_windows_file_image"); + synthesizeMouse(draggable, 12, 12, { type: "mouseup" }); + } + + if (gExtraDragTests == 7) + SimpleTest.finish(); +} + +function sendMouseEventsForDrag(nodeid) +{ + info("Sending events for " + nodeid); + var draggable = $(nodeid); + synthesizeMouse(draggable, 3, 3, { type: "mousedown" }); + synthesizeMouse(draggable, 10, 10, { type: "mousemove" }); + synthesizeMouse(draggable, 12, 12, { type: "mousemove" }); +} +function sendMouseEventsForShadowRootDrag(host) +{ + var draggable = $(host).shadowRoot.firstElementChild; + synthesizeMouse(draggable, 3, 3, { type: "mousedown" }); + synthesizeMouse(draggable, 10, 10, { type: "mousemove" }); + synthesizeMouse(draggable, 12, 12, { type: "mousemove" }); +} + +function doDragStartSelection(event) +{ + is(event.type, "dragstart", "dragstart event type"); + is(event.target, $("draggable").firstChild, "dragstart event target"); + is(event.bubbles, true, "dragstart event bubbles"); + is(event.cancelable, true, "dragstart event cancelable"); + + is(event.clientX, 14, "dragstart clientX"); + is(event.clientY, 14, "dragstart clientY"); + ok(event.screenX > 0, "dragstart screenX"); + ok(event.screenY > 0, "dragstart screenY"); + is(event.layerX, 14, "dragstart layerX"); + is(event.layerY, 14, "dragstart layerY"); + is(event.pageX, 14, "dragstart pageX"); + is(event.pageY, 14, "dragstart pageY"); + + var dt = event.dataTransfer; + ok(dt instanceof DataTransfer, "dataTransfer is DataTransfer"); + gDataTransfer = dt; + + var types = dt.types; + ok(Array.isArray(types), "initial types is an Array"); + checkTypes(dt, ["text/_moz_htmlcontext", "text/_moz_htmlinfo", "text/html", "text/plain"], 0, "initial selection"); + + is(dt.getData("text/plain"), "This is a draggable bit of text.", "initial selection text/plain"); + is(dt.getData("text/html"), "<div id=\"draggable\" ondragstart=\"doDragStartSelection(event)\">This is a <em>draggable</em> bit of text.</div>", + "initial selection text/html"); + + // text/plain and Text are available for compatibility. They retrieve the + // text/plain data. text/unicode is also for compatibility and retreives the plain text. + is(dt.getData("text/plain"), "This is a draggable bit of text.", "initial selection text/plain"); + is(dt.getData("Text"), "This is a draggable bit of text.", "initial selection Text"); + is(dt.getData("TEXT"), "This is a draggable bit of text.", "initial selection TEXT"); + is(dt.getData("text/PLAIN"), "This is a draggable bit of text.", "initial selection text/PLAIN"); + is(dt.getData("text/unicode"), "This is a draggable bit of text.", "initial selection text/unicode"); + + is(SpecialPowers.wrap(dt).mozItemCount, 1, "initial selection item count"); + + dt.clearData("text/plain"); + dt.clearData("text/html"); + dt.clearData("text/_moz_htmlinfo"); + dt.clearData("text/_moz_htmlcontext"); + + test_DataTransfer(dt); + setTimeout(afterDragTests, 0); +} + +function test_DataTransfer(dt) +{ + is(SpecialPowers.wrap(dt).mozItemCount, 0, "empty itemCount"); + + var types = dt.types; + ok(Array.isArray(types), "empty types is an Array"); + // The above test fails if we have SpecialPowers.wrap(dt).types instead of dt.types + // because chrome consumers get the 'ReturnValueNeedsContainsHack'. + // So wrap with special powers after the test + dt = SpecialPowers.wrap(dt); + checkTypes(dt, [], 0, "empty"); + is(dt.getData("text/plain"), "", "empty data is empty"); + + // calling setDataAt requires an index that is 0 <= index <= dt.itemCount + expectError(() => dt.mozSetDataAt("text/plain", "Some Text", 1), + "IndexSizeError", "setDataAt index too high"); + + is(dt.mozUserCancelled, false, "userCancelled"); + + // because an exception occurred, the data should not have been added + is(dt.mozItemCount, 0, "empty setDataAt index too high itemCount"); + dt.getData("text/plain", "", "empty setDataAt index too high getData"); + + // if the type is '', do nothing, or return '' + dt.setData("", "Invalid Type"); + is(dt.types.length, 0, "invalid type setData"); + is(dt.getData(""), "", "invalid type getData"), + dt.mozSetDataAt("", "Invalid Type", 0); + is(dt.types.length, 0, "invalid type setDataAt"); + is(dt.mozGetDataAt("", 0), null, "invalid type getDataAt"), + + // similar with clearDataAt and getDataAt + expectError(() => dt.mozGetDataAt("text/plain", 1), + "IndexSizeError", "getDataAt index too high"); + expectError(() => dt.mozClearDataAt("text/plain", 1), + "IndexSizeError", "clearDataAt index too high"); + + dt.setData("text/plain", "Sample Text"); + is(dt.mozItemCount, 1, "added plaintext itemCount"); + checkOneDataItem(dt, ["text/plain"], ["Sample Text"], 0, "added plaintext"); + + // after all those exceptions, the data should still be the same + checkOneDataItem(dt, ["text/plain"], ["Sample Text"], 0, "added plaintext after exception"); + + // modifying the data associated with the format should give it the new value + dt.setData("text/plain", "Modified Text"); + is(dt.mozItemCount, 1, "modified plaintext itemCount"); + checkOneDataItem(dt, ["text/plain"], ["Modified Text"], 0, "modified plaintext"); + + dt.setData("text/html", "<strong>Modified Text</strong>"); + is(dt.mozItemCount, 1, "modified html itemCount"); + checkOneDataItem(dt, ["text/plain", "text/html"], + ["Modified Text", "<strong>Modified Text</strong>"], + 0, "modified html"); + + // modifying data for a type that already exists should adjust it in place, + // not reinsert it at the beginning + dt.setData("text/plain", "New Text"); + is(dt.mozItemCount, 1, "modified text again itemCount"); + checkOneDataItem(dt, ["text/plain", "text/html"], + ["New Text", "<strong>Modified Text</strong>"], + 0, "modified text again"); + + var draggable = $("draggable"); + dt.setData("application/-moz-node", draggable); + checkOneDataItem(dt, ["text/plain", "text/html", "application/-moz-node"], + ["New Text", "<strong>Modified Text</strong>", draggable.toString()], + 0, "added node"); + + dt.clearData(""); // null means clear all + is(dt.mozItemCount, 0, "itemCount after clearData empty string"); + checkTypes(dt, [], 0, "empty after clearData empty string"); + + dt.setData("text/plain", 22); + dt.setData("text/html", 5.6); + dt.setData("text/xml", 5.6); + checkTypes(dt, ["text/plain", "text/html", "text/xml"], ["22", "5.6", ""], 0, "add numeric and empty data"); + + dt.clearData(); // no argument means clear all + is(dt.mozItemCount, 0, "itemCount after clearData no argument"); + checkTypes(dt, [], 0, "empty after clearData no argument"); + + // check 'Text' type which should convert into text/plain + dt.setData("Text", "Sample Text"); + checkOneDataItem(dt, ["text/plain"], ["Sample Text"], 0, "set Text"); + is(dt.getData("Text"), "Sample Text", "getData Text"); + is(dt.mozGetDataAt("Text", 0), "Sample Text", "getDataAt Text"); + dt.setData("text/plain", "More Text"); + checkOneDataItem(dt, ["text/plain"], ["More Text"], 0, "set text/plain after set Text"); + + dt.mozClearDataAt("", 0); // null means clear all + is(dt.mozItemCount, 0, "itemCount after clearDataAt empty string"); + checkTypes(dt, [], 0, "empty after clearDataAt empty string"); + + // check text/uri-list type + dt.setData("text/uri-list", "http://www.mozilla.org"); + checkURL(dt, "http://www.mozilla.org", "http://www.mozilla.org", 0, "set text/uri-list"); + + // check URL type which should add text/uri-list data + dt.setData("URL", "ftp://ftp.example.com"); + checkURL(dt, "ftp://ftp.example.com", "ftp://ftp.example.com", 0, "set URL"); + checkTypes(dt, ["text/uri-list"], ["ftp://ftp.example.com"], "url types"); + + // clearing text/uri-list data + dt.clearData("text/uri-list"); + is(dt.mozItemCount, 0, "itemCount after clear url-list"); + is(dt.getData("text/uri-list"), "", "text/uri-list after clear url-list"); + is(dt.getData("URL"), "", "URL after clear url-list"); + + // check text/uri-list parsing + dt.setData("text/uri-list", "#http://www.mozilla.org\nhttp://www.xulplanet.com\nhttp://www.example.com"); + checkURL(dt, "http://www.xulplanet.com", + "#http://www.mozilla.org\nhttp://www.xulplanet.com\nhttp://www.example.com", + 0, "uri-list 3 lines"); + + dt.setData("text/uri-list", "#http://www.mozilla.org"); + is(dt.getData("URL"), "", "uri-list commented"); + dt.setData("text/uri-list", "#http://www.mozilla.org\n"); + is(dt.getData("URL"), "", "uri-list commented with newline"); + + // check that clearing the URL type also clears the text/uri-list type + dt.clearData("URL"); + is(dt.getData("text/uri-list"), "", "clear URL"); + + dt.setData("text/uri-list", "#http://www.mozilla.org\n\n\n\n\n"); + is(dt.getData("URL"), "", "uri-list with blank lines"); + dt.setData("text/uri-list", ""); + is(dt.getData("URL"), "", "empty uri-list"); + dt.setData("text/uri-list", "#http://www.mozilla.org\n#Sample\nhttp://www.xulplanet.com \r\n"); + is(dt.getData("URL"), "http://www.xulplanet.com", "uri-list mix"); + dt.setData("text/uri-list", "\nhttp://www.mozilla.org"); + is(dt.getData("URL"), "", "empty line to start uri-list"); + dt.setData("text/uri-list", " http://www.mozilla.org#anchor "); + is(dt.getData("URL"), "http://www.mozilla.org#anchor", "uri-list with spaces and hash"); + + // ensure that setDataAt works the same way + dt.mozSetDataAt("text/uri-list", "#http://www.mozilla.org\n#Sample\nhttp://www.xulplanet.com \r\n", 0); + checkURL(dt, "http://www.xulplanet.com", + "#http://www.mozilla.org\n#Sample\nhttp://www.xulplanet.com \r\n", + 0, "uri-list mix setDataAt"); + + // now test adding multiple items to be dragged using the setDataAt method + dt.clearData(); + dt.mozSetDataAt("text/plain", "First Item", 0); + dt.mozSetDataAt("text/plain", "Second Item", 1); + expectError(() => dt.mozSetDataAt("text/plain", "Some Text", 3), + "IndexSizeError", "setDataAt index too high with two items"); + is(dt.mozItemCount, 2, "setDataAt item itemCount"); + checkOneDataItem(dt, ["text/plain"], ["First Item"], 0, "setDataAt item at index 0"); + checkOneDataItem(dt, ["text/plain"], ["Second Item"], 1, "setDataAt item at index 1"); + + dt.mozSetDataAt("text/html", "<em>First Item</em>", 0); + dt.mozSetDataAt("text/html", "<em>Second Item</em>", 1); + is(dt.mozItemCount, 2, "setDataAt two types item itemCount"); + checkOneDataItem(dt, ["text/plain", "text/html"], + ["First Item", "<em>First Item</em>"], 0, "setDataAt two types item at index 0"); + checkOneDataItem(dt, ["text/plain", "text/html"], + ["Second Item", "<em>Second Item</em>"], 1, "setDataAt two types item at index 1"); + + dt.mozSetDataAt("text/html", "<em>Changed First Item</em>", 0); + dt.mozSetDataAt("text/plain", "Changed Second Item", 1); + is(dt.mozItemCount, 2, "changed with setDataAt item itemCount"); + checkOneDataItem(dt, ["text/plain", "text/html"], + ["First Item", "<em>Changed First Item</em>"], 0, "changed with setDataAt item at index 0"); + checkOneDataItem(dt, ["text/plain", "text/html"], + ["Changed Second Item", "<em>Second Item</em>"], 1, "changed with setDataAt item at index 1"); + + dt.setData("text/html", "Changed with setData"); + is(dt.mozItemCount, 2, "changed with setData"); + checkOneDataItem(dt, ["text/plain", "text/html"], + ["First Item", "Changed with setData"], 0, "changed with setData item at index 0"); + checkOneDataItem(dt, ["text/plain", "text/html"], + ["Changed Second Item", "<em>Second Item</em>"], 1, "changed with setData item at index 1"); + + dt.mozSetDataAt("application/-moz-node", "draggable", 2); + is(dt.mozItemCount, 3, "setDataAt node itemCount"); + checkOneDataItem(dt, ["application/-moz-node"], ["draggable"], 2, "setDataAt node item at index 2"); + + // Try to add and then remove a non-string type to the DataTransfer and ensure + // that the type appears in DataTransfer.types. + { + dt.mozSetDataAt("application/-x-body", document.body, 0); + let found = false; + for (let i = 0; i < dt.types.length; ++i) { + if (dt.types[i] == "application/-x-body") { + found = true; + break; + } + } + ok(found, "Data should appear in datatransfer.types despite having a non-string type"); + dt.mozClearDataAt("application/-x-body", 0); + } + + dt.mozClearDataAt("text/html", 1); + is(dt.mozItemCount, 3, "clearDataAt itemCount"); + checkOneDataItem(dt, ["text/plain", "text/html"], + ["First Item", "Changed with setData"], 0, "clearDataAt item at index 0"); + checkOneDataItem(dt, ["text/plain"], ["Changed Second Item"], 1, "clearDataAt item at index 1"); + + dt.mozClearDataAt("text/plain", 1); + is(dt.mozItemCount, 2, "clearDataAt last type itemCount"); + checkOneDataItem(dt, ["text/plain", "text/html"], + ["First Item", "Changed with setData"], 0, "clearDataAt last type at index 0"); + checkOneDataItem(dt, ["application/-moz-node"], ["draggable"], 1, "clearDataAt last type item at index 2"); + expectError(() => dt.mozGetDataAt("text/plain", 2), + "IndexSizeError", "getDataAt after item removed index too high"); + + dt.mozSetDataAt("text/unknown", "Unknown type", 2); + dt.mozSetDataAt("text/unknown", "Unknown type", 1); + is(dt.mozItemCount, 3, "add unknown type"); + checkOneDataItem(dt, ["application/-moz-node", "text/unknown"], + ["draggable", "Unknown type"], 1, "add unknown type item at index 1"); + checkOneDataItem(dt, ["text/unknown"], ["Unknown type"], 2, "add unknown type item at index 2"); + + dt.mozClearDataAt("", 1); + is(dt.mozItemCount, 2, "clearDataAt empty string"); + checkOneDataItem(dt, ["text/plain", "text/html"], + ["First Item", "Changed with setData"], 0, "clearDataAt empty string item at index 0"); + checkOneDataItem(dt, ["text/unknown"], + ["Unknown type"], 1, "clearDataAt empty string item at index 1"); + + // passing a format that doesn't exist to clearData or clearDataAt should just + // do nothing + dt.clearData("text/something"); + dt.mozClearDataAt("text/something", 1); + is(dt.mozItemCount, 2, "clearData type that does not exist"); + checkOneDataItem(dt, ["text/plain", "text/html"], + ["First Item", "Changed with setData"], 0, "clearData type that does not exist item at index 0"); + checkOneDataItem(dt, ["text/unknown"], + ["Unknown type"], 1, "clearData type that does not exist item at index 1"); + + expectError(() => dt.mozClearDataAt("text/plain", 3), + "IndexSizeError", "clearData index too high with two items"); + + // ensure that clearData() removes all data associated with the first item, but doesn't + // shift the second item down into the first item's slot. + dt.clearData(); + is(dt.mozItemCount, 2, "clearData no argument with multiple items itemCount"); + checkOneDataItem(dt, [], [], 0, + "clearData no argument with multiple items item at index 0"); + checkOneDataItem(dt, ["text/unknown"], + ["Unknown type"], 1, "clearData no argument with multiple items item at index 1"); + + // remove tha remaining data in index 1. As index 0 is empty at this point, this will actually + // drop mozItemCount to 0. (XXX: This is because of spec-compliance reasons related + // to the more-recent dt.item API. It's an unfortunate, but hopefully rare edge case) + dt.mozClearDataAt("", 1); + is(dt.mozItemCount, 0, "all data cleared"); + + // now check the effectAllowed and dropEffect properties + is(dt.dropEffect, "none", "initial dropEffect"); + is(dt.effectAllowed, "uninitialized", "initial effectAllowed"); + + ["copy", "none", "link", "", "other", "copyMove", "all", "uninitialized", "move"].forEach( + function (i) { + dt.dropEffect = i; + is(dt.dropEffect, i == "" || i == "other" || i == "copyMove" || + i == "all" || i == "uninitialized" ? "link" : i, + "dropEffect set to " + i); + is(dt.effectAllowed, "uninitialized", "effectAllowed not modified by dropEffect set to " + i); + } + ); + + ["move", "copy", "link", "", "other", "moveCopy", "copyMove", + "linkMove", "copyLink", "all", "uninitialized", "none"].forEach( + function (i) { + dt.effectAllowed = i; + is(dt.dropEffect, "move", "dropEffect not modified by effectAllowed set to " + i); + is(dt.effectAllowed, i == "" || i == "other" || i == "moveCopy" ? "link" : i, + "effectAllowed set to " + i); + } + ); +} + +function doDragStartLink(event) +{ + var dt = event.dataTransfer; + checkTypes(dt, ["text/x-moz-url", "text/x-moz-url-data", "text/x-moz-url-desc", "text/uri-list", + "text/_moz_htmlcontext", "text/_moz_htmlinfo", "text/html", "text/plain"], 0, "initial link"); + + is(SpecialPowers.wrap(dt).mozItemCount, 1, "initial link item count"); + is(dt.getData("text/uri-list"), "http://www.mozilla.org/", "link text/uri-list"); + is(dt.getData("text/plain"), "http://www.mozilla.org/", "link text/plain"); + + event.preventDefault(); + + gExtraDragTests++; +} + +function doDragStartImage(event) +{ + var dataurl = $("image").src; + + var dt = event.dataTransfer; + checkTypes(dt, ["text/x-moz-url", "text/x-moz-url-data", "text/x-moz-url-desc", "text/uri-list", + "text/_moz_htmlcontext", "text/_moz_htmlinfo", "text/html", "text/plain"], 0, "initial image"); + + is(SpecialPowers.wrap(dt).mozItemCount, 1, "initial image item count"); + is(dt.getData("text/uri-list"), dataurl, "image text/uri-list"); + is(dt.getData("text/plain"), dataurl, "image text/plain"); + + event.preventDefault(); + + gExtraDragTests++; +} + +function doDragStartInput(event) +{ + var dt = event.dataTransfer; + checkTypes(dt, ["text/plain"], 0, "initial input"); + + is(SpecialPowers.wrap(dt).mozItemCount, 1, "initial input item count"); +// is(dt.getData("text/plain"), "Text", "input text/plain"); + +// event.preventDefault(); +} + + +function doDragStartInShadowRoot(event) +{ + is(event.type, "dragstart", "shadow root dragstart event type"); + is(event.target, $("shadow_host_containing_draggable"), "shadow root dragstart event target"); + is(event.bubbles, true, "shadow root dragstart event bubbles"); + is(event.cancelable, true, "shadow root dragstart event cancelable"); + + event.preventDefault(); + + gExtraDragTests++; +} + +function doDragStartShadowRootImage(event) +{ + var dt = event.dataTransfer; + + checkTypes(dt, ["text/x-moz-url", "text/x-moz-url-data", "text/x-moz-url-desc", "text/uri-list", + "text/_moz_htmlcontext", "text/_moz_htmlinfo", "text/html", "text/plain"], 0, "shadow root link with image"); + + is(dt.getData("text/uri-list"), "http://example.org/", "shadow root link text/uri-list"); + is(dt.getData("text/plain"), "http://example.org/", "shadow root link text/plain"); + + event.preventDefault(); + + gExtraDragTests++; +} + +function doDragStartSynthetic(event) +{ + is(event.type, "dragstart", "synthetic dragstart event type"); + + var dt = event.dataTransfer; + todo(dt instanceof DataTransfer, "synthetic dragstart dataTransfer is DataTransfer"); +// Uncomment next line once the todo instanceof above is fixed. +// checkTypes(dt, [], 0, "synthetic dragstart"); + + is(event.detail, 1, "synthetic dragstart detail"); + is(event.screenX, 40, "synthetic dragstart screenX"); + is(event.screenY, 35, "synthetic dragstart screenY"); + is(event.clientX, 20, "synthetic dragstart clientX"); + is(event.clientY, 15, "synthetic dragstart clientY"); + is(event.ctrlKey, false, "synthetic dragstart ctrlKey"); + is(event.altKey, true, "synthetic dragstart altKey"); + is(event.shiftKey, false, "synthetic dragstart shiftKey"); + is(event.metaKey, false, "synthetic dragstart metaKey"); + is(event.button, 0, "synthetic dragstart button "); + is(event.relatedTarget, null, "synthetic dragstart relatedTarget"); + +// Uncomment next two lines once the todo instanceof above is fixed. +// dt.setData("text/plain", "Text"); +// is(dt.getData("text/plain"), "Text", "synthetic dragstart data is set after adding"); +} + +function doDragOverSynthetic(event) +{ + is(event.type, "dragover", "synthetic dragover event type"); + + var dt = event.dataTransfer; + todo(dt instanceof DataTransfer, "synthetic dragover dataTransfer is DataTransfer"); +// Uncomment next line once the todo instanceof above is fixed. +// checkTypes(dt, [], 0, "synthetic dragover"); + + is(event.detail, 0, "synthetic dragover detail"); + is(event.screenX, 40, "synthetic dragover screenX"); + is(event.screenY, 35, "synthetic dragover screenY"); + is(event.clientX, 20, "synthetic dragover clientX"); + is(event.clientY, 15, "synthetic dragover clientY"); + is(event.ctrlKey, true, "synthetic dragover ctrlKey"); + is(event.altKey, false, "synthetic dragover altKey"); + is(event.shiftKey, true, "synthetic dragover shiftKey"); + is(event.metaKey, true, "synthetic dragover metaKey"); + is(event.button, 2, "synthetic dragover button"); + is(event.relatedTarget, document.documentElement, "synthetic dragover relatedTarget"); + +// Uncomment next two lines once the todo instanceof above is fixed. +// dt.setData("text/plain", "Text"); +// is(dt.getData("text/plain"), "Text", "synthetic dragover data is set after adding"); +} + +function onDragStartDraggable(event) +{ + var dt = event.dataTransfer; + ok(SpecialPowers.wrap(dt).mozItemCount == 0 && !dt.types.length && event.originalTarget == gDragInfo.target, gDragInfo.testid); + + event.preventDefault(); + gExtraDragTests++; +} + +// Expects dt wrapped in SpecialPowers +function checkOneDataItem(dt, expectedtypes, expecteddata, index, testid) +{ + checkTypes(dt, expectedtypes, index, testid); + for (var f = 0; f < expectedtypes.length; f++) { + if (index == 0) + is(dt.getData(expectedtypes[f]), expecteddata[f], testid + " getData " + expectedtypes[f]); + is(dt.mozGetDataAt(expectedtypes[f], index), expecteddata[f] ? expecteddata[f] : null, + testid + " getDataAt " + expectedtypes[f]); + } +} + +function checkTypes(dt, expectedtypes, index, testid) +{ + if (index == 0) { + var types = dt.types; + is(types.length, expectedtypes.length, testid + " types length"); + for (var f = 0; f < expectedtypes.length; f++) { + is(types[f], expectedtypes[f], testid + " " + types[f] + " check"); + } + } + + types = SpecialPowers.wrap(dt).mozTypesAt(index); + is(types.length, expectedtypes.length, testid + " typesAt length"); + for (var f = 0; f < expectedtypes.length; f++) { + is(types[f], expectedtypes[f], testid + " " + types[f] + " at " + index + " check"); + } +} + +// Expects dt wrapped in SpecialPowers +function checkURL(dt, url, fullurllist, index, testid) +{ + is(dt.getData("text/uri-list"), fullurllist, testid + " text/uri-list"); + is(dt.getData("URL"), url, testid + " URL"); + is(dt.mozGetDataAt("text/uri-list", 0), fullurllist, testid + " text/uri-list"); + is(dt.mozGetDataAt("URL", 0), fullurllist, testid + " URL"); +} + +function onDragOverDraggableFalse(event) { + ok(false, "Triggered dragstart on draggable=false node " + event.target.id); +} + +function onDragStartUnlinkable(event) { + ok(false, "Triggered dragstart on undraggable node " + event.target.id); +} + +function expectError(fn, eid, testid) +{ + var error = ""; + try { + fn(); + } catch (ex) { + error = ex.name; + } + is(error, eid, testid + " causes exception " + eid); +} + +</script> + +</head> + +<body style="height: 300px; overflow: auto;" onload="setTimeout(runTests, 0)"> + +<div id="draggable" ondragstart="doDragStartSelection(event)">This is a <em>draggable</em> bit of text.</div> + +<fieldset> +<a id="link" href="http://www.mozilla.org/" ondragstart="doDragStartLink(event)">mozilla.org</a> +</fieldset> + +<label> +<img id="image" src="data:image/png,%89PNG%0D%0A%1A%0A%00%00%00%0DIHDR%00%00%00%18%00%00%00%18%02%03%00%00%00%9D%19%D5k%00%00%00%04gAMA%00%00%B1%8F%0B%FCa%05%00%00%00%0CPLTE%FF%FF%FF%FF%FF%FF%F7%DC%13%00%00%00%03%80%01X%00%00%00%01tRNS%08N%3DPT%00%00%00%01bKGD%00%88%05%1DH%00%00%00%09pHYs%00%00%0B%11%00%00%0B%11%01%7Fd_%91%00%00%00%07tIME%07%D2%05%0C%14%0C%0D%D8%3F%1FQ%00%00%00%5CIDATx%9C%7D%8E%CB%09%C0%20%10D%07r%B7%20%2F%E9wV0%15h%EA%D9%12D4%BB%C1x%CC%5C%1E%0C%CC%07%C0%9C0%9Dd7()%C0A%D3%8D%E0%B8%10%1DiCHM%D0%AC%D2d%C3M%F1%B4%E7%FF%10%0BY%AC%25%93%CD%CBF%B5%B2%C0%3Alh%CD%AE%13%DF%A5%F7%E0%03byW%09A%B4%F3%E2%00%00%00%00IEND%AEB%60%82" + ondragstart="doDragStartImage(event)"> +</label> + +<input id="input" value="Text in a box" ondragstart="doDragStartInput(event)"> + +<div ondragstart="onDragStartDraggable(event)"> + <div id="dragtrue" draggable="true"> + This is a <span id="spantrue">draggable</span> area. + </div> + <div id="dragfalse" draggable="false"> + This is a <span id="spanfalse">non-draggable</span> area. + </div> +</div> + +<!--iframe src="http://www.mozilla.org" width="400" height="400"></iframe--> + +<div id="synthetic" ondragstart="doDragStartSynthetic(event)">Synthetic Event Dispatch</div> +<div id="synthetic2" ondragover="doDragOverSynthetic(event)">Synthetic Event Dispatch</div> + +<div draggable="true" id="shadow_host_containing_draggable"></div> + +<script> +shadow_host_containing_draggable.attachShadow({ mode: 'open' }).innerHTML = +`<span>Inside shadow root</span>`; +shadow_host_containing_draggable.addEventListener("dragstart", doDragStartInShadowRoot); +</script> + +<a href="http://example.org" ondragstart="doDragStartShadowRootImage(event)"> + <div id="shadow_host_containing_image"></div> +</a> + +<script> +shadow_host_containing_image.attachShadow({ mode: 'open' }).innerHTML = +`<img src="${$("image").src}" alt="Alt Text"></img>`; +</script> + +<a href="http://example.org" id="link_user_select_none_child" draggable="false" ondragstart="onDragOverDraggableFalse(event)"> + <span id="userselectnone" style="user-select: none"> + This is an unselectable, undraggable area. + </span> +</a> + +<div id="draggable_with_undraggable_descendant" draggable="true" ondragstart="onDragStartDraggable(event)"> + <a id="undraggable_inside_draggable" href="http://example.org" draggable="false" ondragstart="onDragOverDraggableFalse(event)"> + This is an undraggable link inside a draggable ancestor. + </a> +</div> + +<a id="link_to_file" href="file:///" ondragstart="onDragStartUnlinkable(event)">This is an undraggable 'file' link</a> +<a id="link_to_windows_file" href="c:/Users/" ondragstart="onDragStartUnlinkable(event)">This is an undraggable 'file' link</a> +<a id="link_to_backslash_windows_file" href="c:\\Users\\" ondragstart="onDragStartUnlinkable(event)">This is an undraggable 'file' link</a> +<img id="broken_file_image" src="file:///" ondragstart="onDragStartUnlinkable(event)"> +<img id="broken_windows_file_image" src="c:/Users/" ondragstart="onDragStartUnlinkable(event)"> +<img id="broken_backslash_windows_file_image" src="c:\\Users\\" ondragstart="onDragStartUnlinkable(event)"> +</body> +</html> diff --git a/dom/events/test/test_error_events.html b/dom/events/test/test_error_events.html new file mode 100644 index 0000000000..1d47e5398b --- /dev/null +++ b/dom/events/test/test_error_events.html @@ -0,0 +1,71 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title>Test for error events being ErrorEvent</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<div id="log"></div> +<script> + setup({allow_uncaught_exception:true}); + var errorEvent; + var file; + var line; + var msg; + var column; + var error; + window.addEventListener("error", function(e) { + errorEvent = e; + }, {once: true}); + var oldOnerror = window.onerror; + window.onerror = function(message, filename, lineno, columnno, errorObject) { + window.onerror = oldOnerror; + file = filename; + line = lineno; + msg = message; + column = columnno; + error = errorObject; + } + var thrown = new Error("hello"); + throw thrown; +</script> +<script> + generate_tests(assert_equals, [ + [ "Event filename", errorEvent.filename, location.href ], + [ "Callback filename", file, location.href ], + [ "Event line number", errorEvent.lineno, 27 ], + [ "Callback line number", line, 27 ], + [ "Event message", errorEvent.message, "Error: hello" ], + [ "Callback message", msg, "Error: hello" ], + [ "Event error-object", errorEvent.error, thrown], + [ "Callback error-object", error, thrown ], + [ "Event column", errorEvent.colno, 16 ], + [ "Callback column", column, 16 ] + ]); +</script> +<script> + var workerLocation = location.protocol + "//" + location.host + + location.pathname.replace("test_error_events.html", "error_event_worker.js"); + var eventFileTest = async_test("Worker event filename"); + var eventLineTest = async_test("Worker event line number"); + var eventMessageTest = async_test("Worker event message"); + var callbackFileTest = async_test("Worker callback filename"); + var callbackLineTest = async_test("Worker callback line number"); + var callbackMessageTest = async_test("Worker callback message"); + var w = new Worker("error_event_worker.js"); + w.addEventListener("message", function(msg) { + if (msg.data.type == "event") { + eventFileTest.step(function() { assert_equals(msg.data.filename, workerLocation); }); + eventFileTest.done(); + eventLineTest.step(function() { assert_equals(msg.data.lineno, 19); }); + eventLineTest.done(); + eventMessageTest.step(function() { assert_equals(msg.data.message, "Error: workerhello"); }); + eventMessageTest.done(); + } else { + callbackFileTest.step(function() { assert_equals(msg.data.filename, workerLocation); }); + callbackFileTest.done(); + callbackLineTest.step(function() { assert_equals(msg.data.lineno, 19); }); + callbackLineTest.done(); + callbackMessageTest.step(function() { assert_equals(msg.data.message, "Error: workerhello"); }); + callbackMessageTest.done(); + } + }); +</script> diff --git a/dom/events/test/test_eventTimeStamp.html b/dom/events/test/test_eventTimeStamp.html new file mode 100644 index 0000000000..a0a52409a0 --- /dev/null +++ b/dom/events/test/test_eventTimeStamp.html @@ -0,0 +1,116 @@ +<!doctype html> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=77992 +--> +<head> + <meta charset="utf-8"> + <title>Test for Event.timeStamp (Bug 77992)</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=77992">Mozilla Bug 77992</a> +<p id="display"></p> +<pre id="test"> +<script type="text/js-worker" id="worker-src"> + // Simply returns the event timestamp + onmessage = function(evt) { + postMessage(evt.timeStamp + performance.timeOrigin); + } +</script> +<script type="text/js-worker" id="shared-worker-src"> + // Simply returns the event timestamp + onconnect = function(evt) { + var port = evt.ports[0]; + port.onmessage = function(messageEvt) { + port.postMessage(messageEvt.timeStamp + performance.timeOrigin); + }; + }; +</script> +<script type="application/javascript"> +"use strict"; + +SimpleTest.waitForExplicitFinish(); +SimpleTest.requestFlakyTimeout("untriaged"); + +// This file performs tests that normalize the timeOrigin within a worker +// and compare it to the page. When this occurs, time can appear to go backwards. +// This is a known (and accepted) regression of privacy.reduceTimerPrecision so +// we need to turn it off. +SpecialPowers.pushPrefEnv({ "set": [ + ["privacy.reduceTimerPrecision", false] + ]}, testRegularEvents); + +// Event.timeStamp should be relative to the time origin which is: +// +// Non-worker context: navigation start +// Dedicated worker: navigation start of the document that created the worker +// Shared worker: creation time of the shared worker +// +// See: https://w3c.github.io/web-performance/specs/HighResolutionTime2/Overview.html#sec-time-origin + +function testRegularEvents() { + var timeBeforeEvent = performance.now(); + document.getElementById('test').addEventListener("click", function(evt) { + var timeAfterEvent = performance.now(); + ok(evt.timeStamp >= timeBeforeEvent && + evt.timeStamp <= timeAfterEvent, + "Event timestamp (" + evt.timeStamp + ") is in expected range: [" + + timeBeforeEvent + ", " + timeAfterEvent + "]"); + testWorkerEvents(); + }); + document.getElementById('test').click(); +} + +function testWorkerEvents() { + var blob = new Blob([ document.getElementById("worker-src").textContent ], + { type: "text/javascript" }); + var worker = new Worker(URL.createObjectURL(blob)); + worker.onmessage = function(evt) { + var timeAfterEvent = performance.now() + performance.timeOrigin; + ok(evt.data >= timeBeforeEvent && + evt.data <= timeAfterEvent, + "Event timestamp in dedicated worker (" + evt.data + + ") is in expected range: [" + + timeBeforeEvent + ", " + timeAfterEvent + "]"); + worker.terminate(); + testSharedWorkerEvents(); + }; + var timeBeforeEvent = performance.now() + performance.timeOrigin; + worker.postMessage(""); +} + +function testSharedWorkerEvents() { + var blob = + new Blob([ document.getElementById("shared-worker-src").textContent ], + { type: "text/javascript" }); + // Delay creation of worker slightly so it is easier to distinguish + // shared worker creation time from this document's navigation start + setTimeout(function() { + var timeBeforeEvent = performance.now() + performance.timeOrigin; + var worker = new SharedWorker(URL.createObjectURL(blob)); + worker.port.onmessage = function(evt) { + var timeAfterEvent = performance.now() + performance.timeOrigin; + ok(evt.data >= timeBeforeEvent && + evt.data <= timeAfterEvent, + "Event timestamp in shared worker (" + evt.data + + ") is in expected range: [" + + timeBeforeEvent + ", " + timeAfterEvent + "]"); + worker.port.close(); + finishTests(); + }; + worker.port.start(); + worker.port.postMessage(""); + }, 500); +} + +var finishTests = function() { + SimpleTest.finish(); +}; + +</script> +</pre> +</body> +</html> diff --git a/dom/events/test/test_event_handler_cc.html b/dom/events/test/test_event_handler_cc.html new file mode 100644 index 0000000000..bc2f504b12 --- /dev/null +++ b/dom/events/test/test_event_handler_cc.html @@ -0,0 +1,90 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for cycle collection of event handlers</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> + <script> + SimpleTest.waitForExplicitFinish(); + + function listener() {} + + function make_weak_ref(obj) { + let m = new WeakMap; + m.set(obj, {}); + return m; + } + + function weak_ref_dead(r) { + return !SpecialPowers.nondeterministicGetWeakMapKeys(r).length; + } + + function setupTarget1() { + let ifr = document.getElementById("target1"); + let doc = ifr.contentDocument; + let b = doc.createElement("button"); + doc.body.appendChild(b); + b.onclick = listener; + ifr.remove(); + return make_weak_ref(b); + } + + function setupTarget2() { + let ifr = document.getElementById("target2"); + let doc = ifr.contentDocument; + let b = doc.createElement("button"); + doc.body.appendChild(b); + let setFunc = new ifr.contentWindow.Function(` + var b = document.querySelector("button"); + var proto = parent.HTMLElement.prototype; + var setter = Object.getOwnPropertyDescriptor(proto, "onclick").set; + // Here the current global (and hence CallbackObject global) will be + // the parent, the callback will be a known-live thing from the + // parent, but the incumbent global will be the child. + setter.call(b, parent.listener); + `); + setFunc(); + ifr.remove(); + return make_weak_ref(b); + } + + var ref1; + var ref2; + var maxCounter = 10; + + function GCCCUntilCollected() { + SpecialPowers.exactGC(function () { + SpecialPowers.forceCC(); + SpecialPowers.forceGC(); + SpecialPowers.forceGC(); + + if ((!weak_ref_dead(ref1) || !weak_ref_dead(ref2)) && --maxCounter > 0) { + requestIdleCallback(GCCCUntilCollected, {timeout: 250}); + return; + } + + ok(weak_ref_dead(ref1), + "Should collect cycle through callback global"); + ok(weak_ref_dead(ref2), + "Should collect cycle through incumbent global"); + + SimpleTest.finish(); + }); + } + + addLoadEvent(function() { + ref1 = setupTarget1(); + ref2 = setupTarget2(); + GCCCUntilCollected(); + }); + </script> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<iframe id="target1"></iframe> +<iframe id="target2"></iframe> +<pre id="test"></pre> +</body> +</html> diff --git a/dom/events/test/test_event_screenXY_in_cross_origin_iframe.html b/dom/events/test/test_event_screenXY_in_cross_origin_iframe.html new file mode 100644 index 0000000000..4cd0dfd358 --- /dev/null +++ b/dom/events/test/test_event_screenXY_in_cross_origin_iframe.html @@ -0,0 +1,101 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title></title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<script src="/tests/SimpleTest/EventUtils.js"></script> +<script src="/tests/SimpleTest/paint_listener.js"></script> +<script src="/tests/gfx/layers/apz/test/mochitest/apz_test_utils.js"></script> +<script src="/tests/gfx/layers/apz/test/mochitest/apz_test_native_event_utils.js"></script> +<iframe width=100></iframe> +<iframe width=100></iframe> +<script> +function getScreenPosition(aElement, aOffsetX, aOffsetY) { + const rect = aElement.getBoundingClientRect(); + const x = aOffsetX + window.mozInnerScreenX + rect.left; + const y = aOffsetY + window.mozInnerScreenY + rect.top; + const scale = window.devicePixelRatio; + return [Math.round(x * scale), Math.round(y * scale)]; +} + +function waitForMessage(aMsg, aOrigin) { + return new Promise(resolve => { + window.addEventListener("message", function listener(event) { + if (event.data == "ready" && event.origin == aOrigin) { + window.removeEventListener("message", listener); + resolve(); + } + }); + }); +} + +add_task(async () => { + await SimpleTest.promiseFocus(); + + const iframes = document.querySelectorAll("iframe"); + iframes[0].src = "file_event_screenXY.html"; + await waitForMessage("ready", location.origin); + + iframes[1].src = "https://example.com/tests/dom/events/test/file_event_screenXY.html"; + await waitForMessage("ready", "https://example.com"); + + // Wait for APZ state stable so that mouse event handling APZ works properly + // in out-of-process iframes. + await promiseApzFlushedRepaints(); + + const promiseForSameOrigin = new Promise(resolve => { + window.addEventListener("message", event => { + is(event.origin, location.origin, "origin should be the same as parent"); + resolve(event.data); + }, { once: true }); + }); + + // NOTE: synthesizeMouseAtCenter doesn't work for OOP iframes (bug 1528935), + // so we use promiseNativeMouseEvent instead. + const [expectedScreenXInSameOrigin, expectedScreenYInSameOrigin] = + getScreenPosition(iframes[0], 10, 10); + await promiseNativeMouseEvent({ + type: "click", + target: iframes[0], + screenX: expectedScreenXInSameOrigin, + screenY: expectedScreenYInSameOrigin, + scale: "inScreenPixels", + }); + + const eventInSameOrigin = await promiseForSameOrigin; + is(eventInSameOrigin.screenX, expectedScreenXInSameOrigin, + "event.screenX should be the same"); + is(eventInSameOrigin.screenY, expectedScreenYInSameOrigin, + "event.screenY should be the same"); + + const [expectedScreenXInCrossOrigin, expectedScreenYInCrossOrigin] = + getScreenPosition(iframes[1], 10, 10); + await promiseNativeMouseEvent({ + type: "click", + target: iframes[0], + screenX: expectedScreenXInCrossOrigin, + screenY: expectedScreenYInCrossOrigin, + scale: "inScreenPixels", + }); + + const promiseForCrossOrigin = new Promise(resolve => { + window.addEventListener("message", event => { + is(event.origin, "https://example.com", "origin should be example.com"); + resolve(event.data); + }, { once: true }); + }); + + const eventInCrossOrigin = await promiseForCrossOrigin; + is(eventInCrossOrigin.screenX, expectedScreenXInCrossOrigin, + "even.screenX should be the same"); + is(eventInCrossOrigin.screenY, expectedScreenYInCrossOrigin, + "even.screenY should be the same"); + + is(eventInSameOrigin.screenY, eventInCrossOrigin.screenY, + "event.screenY in both iframes should be the same"); + // Sanity checks to make sure client{X,Y} are the same. + is(eventInSameOrigin.clientX, eventInCrossOrigin.clientX, + "event.clientX in both iframes should be the same"); + is(eventInSameOrigin.clientY, eventInCrossOrigin.clientY, + "event.clientY in both iframes should be the same"); +}); +</script> diff --git a/dom/events/test/test_event_screenXY_with_zoom.html b/dom/events/test/test_event_screenXY_with_zoom.html new file mode 100644 index 0000000000..7f276a4534 --- /dev/null +++ b/dom/events/test/test_event_screenXY_with_zoom.html @@ -0,0 +1,63 @@ +<!doctype html> +<meta charset="utf-8"> +<title>Event.screenX/Y on a zoomed page.</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<script src="/tests/SimpleTest/EventUtils.js"></script> +<style> + #target { + width: 100px; + height: 100px; + background-color: blue; + /* We want synthesizeMouseAtCenter to click on the same point regardless of + zoom, so we achieve that by centering the target in our viewport */ + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + margin: auto; + } +</style> +<div id="target" style=""></div> +<script> +const target = document.getElementById("target"); +async function getScreenPositionOfTarget() { + return new Promise(resolve => { + target.addEventListener("click", function(e) { + resolve({ x: e.screenX, y: e.screenY }); + }, { once: true }); + synthesizeMouseAtCenter(target, {}); + }); +} + +function getScreen() { + return { + width: screen.width, + height: screen.height, + top: screen.top, + left: screen.left, + }; +} + +add_task(async () => { + let pos = await getScreenPositionOfTarget(); + let s = getScreen(); + + SpecialPowers.setFullZoom(window, 2); + let zoomedPos = await getScreenPositionOfTarget(); + let zoomedScreen = getScreen(); + + info(`Original pos=${JSON.stringify(pos)} s=${JSON.stringify(s)}`); + info(`Zoomed pos=${JSON.stringify(zoomedPos)} s=${JSON.stringify(zoomedScreen)}`); + + isfuzzy(pos.x, zoomedPos.x * 2, 1, "screenX coordinate"); + isfuzzy(pos.y, zoomedPos.y * 2, 1, "screenY coordinate"); + + isfuzzy(s.top, zoomedScreen.top * 2, 1, "Screen.top"); + isfuzzy(s.left, zoomedScreen.left * 2, 1, "Screen.left"); + isfuzzy(s.width, zoomedScreen.width * 2, 1, "Screen.width"); + isfuzzy(s.height, zoomedScreen.height * 2, 1, "Screen.height"); + + SpecialPowers.setFullZoom(window, 1); +}); +</script> diff --git a/dom/events/test/test_eventctors.html b/dom/events/test/test_eventctors.html new file mode 100644 index 0000000000..a81e1560fe --- /dev/null +++ b/dom/events/test/test_eventctors.html @@ -0,0 +1,936 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=675884 +--> +<head> + <title>Test for Bug 675884</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=675884">Mozilla Bug 675884</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 675884 **/ + +var receivedEvent; +document.addEventListener("hello", function(e) { receivedEvent = e; }, true); + +function isMethodResultInitializer(aPropName) +{ + return aPropName.startsWith("modifier"); +} + +function getPropValue(aEvent, aPropName) +{ + if (aPropName.startsWith("modifier")) { + return aEvent.getModifierState(aPropName.substr("modifier".length)); + } + return aEvent[aPropName]; +} + +// Event +var e; +var ex = false; +try { + e = new Event(); +} catch(exp) { + ex = true; +} +ok(ex, "First parameter is required!"); +ex = false; + +try { + e = new Event("foo", 123); +} catch(exp) { + ex = true; +} +ok(ex, "2nd parameter should be an object!"); +ex = false; + +try { + e = new Event("foo", "asdf"); +} catch(exp) { + ex = true; +} +ok(ex, "2nd parameter should be an object!"); +ex = false; + + +try { + e = new Event("foo", false); +} catch(exp) { + ex = true; +} +ok(ex, "2nd parameter should be an object!"); +ex = false; + + +e = new Event("hello"); +is(e.type, "hello", "Wrong event type!"); +ok(!e.isTrusted, "Event shouldn't be trusted!"); +e.isTrusted = true; +ok(!e.isTrusted, "Event shouldn't be trusted!"); + +try { + e.__defineGetter__("isTrusted", function() { return true }); +} catch (exp) { + ex = true; +} +ok(ex, "Shouldn't be able to re-define the getter for isTrusted."); +ex = false; +ok(!e.isTrusted, "Event shouldn't be trusted!"); + +ok(!("isTrusted" in Object.getPrototypeOf(e))) + +ok(!e.bubbles, "Event shouldn't bubble!"); +ok(!e.cancelable, "Event shouldn't be cancelable!"); +is(e.eventPhase, Event.NONE, "Wrong event phase"); +document.dispatchEvent(e); +is(e.eventPhase, Event.NONE, "Wrong event phase"); +is(receivedEvent, e, "Wrong event!"); + +e = new Event("hello", null); +is(e.type, "hello", "Wrong event type!"); +ok(!e.isTrusted, "Event shouldn't be trusted!"); +ok(!e.bubbles, "Event shouldn't bubble!"); +ok(!e.cancelable, "Event shouldn't be cancelable!"); +is(e.eventPhase, Event.NONE, "Wrong event phase"); +document.dispatchEvent(e); +is(e.eventPhase, Event.NONE, "Wrong event phase"); +is(receivedEvent, e, "Wrong event!"); + +e = new Event("hello", undefined); +is(e.type, "hello", "Wrong event type!"); +ok(!e.isTrusted, "Event shouldn't be trusted!"); +ok(!e.bubbles, "Event shouldn't bubble!"); +ok(!e.cancelable, "Event shouldn't be cancelable!"); +is(e.eventPhase, Event.NONE, "Wrong event phase"); +document.dispatchEvent(e); +is(e.eventPhase, Event.NONE, "Wrong event phase"); +is(receivedEvent, e, "Wrong event!"); + +e = new Event("hello", {}); +is(e.type, "hello", "Wrong event type!"); +ok(!e.isTrusted, "Event shouldn't be trusted!"); +ok(!e.bubbles, "Event shouldn't bubble!"); +ok(!e.cancelable, "Event shouldn't be cancelable!"); +is(e.eventPhase, Event.NONE, "Wrong event phase"); +document.dispatchEvent(e); +is(e.eventPhase, Event.NONE, "Wrong event phase"); +is(receivedEvent, e, "Wrong event!"); + +e = new Event("hello", { bubbles: true, cancelable: true }); +is(e.type, "hello", "Wrong event type!"); +ok(!e.isTrusted, "Event shouldn't be trusted!"); +ok(e.bubbles, "Event should bubble!"); +ok(e.cancelable, "Event should be cancelable!"); +document.dispatchEvent(e); +is(receivedEvent, e, "Wrong event!"); + +// CustomEvent + +try { + e = new CustomEvent(); +} catch(exp) { + ex = true; +} +ok(ex, "First parameter is required!"); +ex = false; + +e = new CustomEvent("hello"); +is(e.type, "hello", "Wrong event type!"); +ok(!e.isTrusted, "Event shouldn't be trusted!"); +ok(!e.bubbles, "Event shouldn't bubble!"); +ok(!e.cancelable, "Event shouldn't be cancelable!"); +document.dispatchEvent(e); +is(receivedEvent, e, "Wrong event!"); + +e = new CustomEvent("hello", { bubbles: true, cancelable: true, detail: window }); +is(e.type, "hello", "Wrong event type!"); +ok(!e.isTrusted, "Event shouldn't be trusted!"); +ok(e.bubbles, "Event should bubble!"); +ok(e.cancelable, "Event should be cancelable!"); +is(e.detail, window , "Wrong event.detail!"); +document.dispatchEvent(e); +is(receivedEvent, e, "Wrong event!"); + +e = new CustomEvent("hello", { cancelable: true, detail: window }); +is(e.type, "hello", "Wrong event type!"); +ok(!e.isTrusted, "Event shouldn't be trusted!"); +ok(!e.bubbles, "Event shouldn't bubble!"); +ok(e.cancelable, "Event should be cancelable!"); +is(e.detail, window , "Wrong event.detail!"); + +e = new CustomEvent("hello", { detail: 123 }); +is(e.detail, 123, "Wrong event.detail!"); +ok(!e.bubbles, "Event shouldn't bubble!"); +ok(!e.cancelable, "Event shouldn't be cancelable!"); + +var dict = { get detail() { return document.body } }; +e = new CustomEvent("hello", dict); +is(e.detail, dict.detail, "Wrong event.detail!"); +ok(!e.bubbles, "Event shouldn't bubble!"); +ok(!e.cancelable, "Event shouldn't be cancelable!"); + +var dict = { get detail() { throw "foo"; } }; + +try { + e = new CustomEvent("hello", dict); +} catch (exp) { + ex = true; +} +ok(ex, "Should have thrown an exception!"); +ex = false; + +// BlobEvent + +try { + e = new BlobEvent(); +} catch(exp) { + ex = true; +} +ok(ex, "First parameter is required!"); +ex = false; + +try { + e = new BlobEvent("hello"); +} catch(exp) { + ex = true; +} +ok(ex, "data attribute is required in init dict"); +ex = false; + +var blob = new Blob(); +e = new BlobEvent("hello", {data: blob}); +is(e.type, "hello", "Wrong event type!"); +ok(!e.isTrusted, "Event shouldn't be trusted!"); +try { + e.__defineGetter__("isTrusted", function() { return true }); +} catch (exp) { + ex = true; +} +ok(ex, "Shouldn't be able to re-define the getter for isTrusted."); +ex = false; +ok(!e.isTrusted, "BlobEvent shouldn't be trusted!"); + +ok(!e.bubbles, "Event shouldn't bubble!"); +ok(!e.cancelable, "Event shouldn't be cancelable!"); +document.dispatchEvent(e); +is(receivedEvent, e, "Wrong event!"); + +e = new BlobEvent("hello", { bubbles: true, cancelable: true, data: blob }); +is(e.type, "hello", "Wrong event type!"); +ok(!e.isTrusted, "Event shouldn't be trusted!"); +ok(e.bubbles, "Event should bubble!"); +ok(e.cancelable, "Event should be cancelable!"); +is(e.data, blob , "Wrong event.data!"); +document.dispatchEvent(e); +is(receivedEvent, e, "Wrong event!"); + +e = new BlobEvent("hello", {data: blob}); +is(e.type, "hello", "Wrong event type!"); +ok(!e.isTrusted, "Event shouldn't be trusted!"); +ok(!e.bubbles, "Event shouldn't bubble!"); +ok(!e.cancelable, "Event should be cancelable1!"); +is(e.data, blob , "Wrong event.data!"); + +try { + e = new BlobEvent("hello", { data: null }); +} catch(exp) { + ex = true; +} +ok(ex, "data attribute cannot be null"); +ex = false; +blob = null; + +// CloseEvent + +try { + e = new CloseEvent(); +} catch(exp) { + ex = true; +} +ok(ex, "First parameter is required!"); +ex = false; + +e = new CloseEvent("hello"); +is(e.type, "hello", "Wrong event type!"); +ok(!e.isTrusted, "Event shouldn't be trusted!"); +ok(!e.bubbles, "Event shouldn't bubble!"); +ok(!e.cancelable, "Event shouldn't be cancelable!"); +is(e.wasClean, false, "wasClean should be false!"); +is(e.code, 0, "code should be 0!"); +is(e.reason, "", "reason should be ''!"); +document.dispatchEvent(e); +is(receivedEvent, e, "Wrong event!"); + +e = new CloseEvent("hello", + { bubbles: true, cancelable: true, wasClean: true, code: 1, reason: "foo" }); +is(e.type, "hello", "Wrong event type!"); +ok(!e.isTrusted, "Event shouldn't be trusted!"); +ok(e.bubbles, "Event should bubble!"); +ok(e.cancelable, "Event should be cancelable!"); +is(e.wasClean, true, "wasClean should be true!"); +is(e.code, 1, "code should be 1!"); +is(e.reason, "foo", "reason should be 'foo'!"); +document.dispatchEvent(e); +is(receivedEvent, e, "Wrong event!"); + +e = new CloseEvent("hello", + { bubbles: true, cancelable: true, wasClean: true, code: 1 }); +is(e.type, "hello", "Wrong event type!"); +ok(!e.isTrusted, "Event shouldn't be trusted!"); +ok(e.bubbles, "Event should bubble!"); +ok(e.cancelable, "Event should be cancelable!"); +is(e.wasClean, true, "wasClean should be true!"); +is(e.code, 1, "code should be 1!"); +is(e.reason, "", "reason should be ''!"); +document.dispatchEvent(e); +is(receivedEvent, e, "Wrong event!"); + + +// HashChangeEvent + +try { + e = new HashChangeEvent(); +} catch(exp) { + ex = true; +} +ok(ex, "First parameter is required!"); +ex = false; + +e = new HashChangeEvent("hello"); +is(e.type, "hello", "Wrong event type!"); +ok(!e.isTrusted, "Event shouldn't be trusted!"); +ok(!e.bubbles, "Event shouldn't bubble!"); +ok(!e.cancelable, "Event shouldn't be cancelable!"); +is(e.oldURL, "", "oldURL should be ''"); +is(e.newURL, "", "newURL should be ''"); +document.dispatchEvent(e); +is(receivedEvent, e, "Wrong event!"); + +e = new HashChangeEvent("hello", + { bubbles: true, cancelable: true, oldURL: "old", newURL: "new" }); +is(e.type, "hello", "Wrong event type!"); +ok(!e.isTrusted, "Event shouldn't be trusted!"); +ok(e.bubbles, "Event should bubble!"); +ok(e.cancelable, "Event should be cancelable!"); +is(e.oldURL, "old", "oldURL should be 'old'"); +is(e.newURL, "new", "newURL should be 'new'"); +document.dispatchEvent(e); +is(receivedEvent, e, "Wrong event!"); + +e = new HashChangeEvent("hello", + { bubbles: true, cancelable: true, newURL: "new" }); +is(e.type, "hello", "Wrong event type!"); +ok(!e.isTrusted, "Event shouldn't be trusted!"); +ok(e.bubbles, "Event should bubble!"); +ok(e.cancelable, "Event should be cancelable!"); +is(e.oldURL, "", "oldURL should be ''"); +is(e.newURL, "new", "newURL should be 'new'"); +document.dispatchEvent(e); +is(receivedEvent, e, "Wrong event!"); + +// InputEvent + +e = new InputEvent("hello"); +is(e.type, "hello", "Wrong event type!"); +ok(!e.isTrusted, "Event shouldn't be trusted!"); +ok(!e.bubbles, "Event shouldn't bubble!"); +ok(!e.cancelable, "Event shouldn't be cancelable!"); +is(e.detail, 0, "detail should be 0"); +ok(!e.isComposing, "isComposing should be false"); + +e = new InputEvent("hi!", { bubbles: true, detail: 5, isComposing: false }); +is(e.type, "hi!", "Wrong event type!"); +ok(!e.isTrusted, "Event shouldn't be trusted!"); +ok(e.bubbles, "Event should bubble!"); +ok(!e.cancelable, "Event shouldn't be cancelable!"); +is(e.detail, 5, "detail should be 5"); +ok(!e.isComposing, "isComposing should be false"); + +e = new InputEvent("hi!", { cancelable: true, detail: 0, isComposing: true }); +is(e.type, "hi!", "Wrong event type!"); +ok(!e.isTrusted, "Event shouldn't be trusted!"); +ok(!e.bubbles, "Event shouldn't bubble!"); +ok(e.cancelable, "Event should be cancelable!"); +is(e.detail, 0, "detail should be 0"); +ok(e.isComposing, "isComposing should be true"); + +// KeyboardEvent + +try { + e = new KeyboardEvent(); +} catch(exp) { + ex = true; +} +ok(ex, "KeyboardEvent: First parameter is required!"); +ex = false; + +e = new KeyboardEvent("hello"); +is(e.type, "hello", "KeyboardEvent: Wrong event type!"); +ok(!e.isTrusted, "KeyboardEvent: Event shouldn't be trusted!"); +ok(!e.bubbles, "KeyboardEvent: Event shouldn't bubble!"); +ok(!e.cancelable, "KeyboardEvent: Event shouldn't be cancelable!"); +document.dispatchEvent(e); +is(receivedEvent, e, "KeyboardEvent: Wrong event!"); + +var keyboardEventProps = +[ + { bubbles: false }, + { cancelable: false }, + { view: null }, + { detail: 0 }, + { key: "" }, + { code: "" }, + { location: 0 }, + { ctrlKey: false }, + { shiftKey: false }, + { altKey: false }, + { metaKey: false }, + { modifierAltGraph: false }, + { modifierCapsLock: false }, + { modifierFn: false }, + { modifierFnLock: false }, + { modifierNumLock: false }, + { modifierOS: false }, + { modifierScrollLock: false }, + { modifierSymbol: false }, + { modifierSymbolLock: false }, + { repeat: false }, + { isComposing: false }, + { charCode: 0 }, + { keyCode: 0 }, + { which: 0 }, +]; + +var testKeyboardProps = +[ + { bubbles: true }, + { cancelable: true }, + { view: window }, + { detail: 1 }, + { key: "CustomKey" }, + { code: "CustomCode" }, + { location: 1 }, + { ctrlKey: true }, + { shiftKey: true }, + { altKey: true }, + { metaKey: true }, + { modifierAltGraph: true }, + { modifierCapsLock: true }, + { modifierFn: true }, + { modifierFnLock: true }, + { modifierNumLock: true }, + { modifierOS: true }, + { modifierScrollLock: true }, + { modifierSymbol: true }, + { modifierSymbolLock: true }, + { repeat: true }, + { isComposing: true }, + { charCode: 2 }, + { keyCode: 3 }, + { which: 4 }, + { charCode: 5, which: 6 }, + { keyCode: 7, which: 8 }, + { keyCode: 9, charCode: 10 }, + { keyCode: 11, charCode: 12, which: 13 }, +]; + +var defaultKeyboardEventValues = {}; +for (var i = 0; i < keyboardEventProps.length; ++i) { + for (prop in keyboardEventProps[i]) { + if (!isMethodResultInitializer(prop)) { + ok(prop in e, "keyboardEvent: KeyboardEvent doesn't have property " + prop + "!"); + } + defaultKeyboardEventValues[prop] = keyboardEventProps[i][prop]; + } +} + +while (testKeyboardProps.length) { + var p = testKeyboardProps.shift(); + e = new KeyboardEvent("foo", p); + for (var def in defaultKeyboardEventValues) { + if (!(def in p)) { + is(getPropValue(e, def), defaultKeyboardEventValues[def], + "KeyboardEvent: Wrong default value for " + def + "!"); + } else { + is(getPropValue(e, def), p[def], + "KeyboardEvent: Wrong event init value for " + def + "!"); + } + } +} + +// PageTransitionEvent + +try { + e = new PageTransitionEvent(); +} catch(exp) { + ex = true; +} +ok(ex, "First parameter is required!"); +ex = false; + +e = new PageTransitionEvent("hello"); +is(e.type, "hello", "Wrong event type!"); +ok(!e.isTrusted, "Event shouldn't be trusted!"); +ok(!e.bubbles, "Event shouldn't bubble!"); +ok(!e.cancelable, "Event shouldn't be cancelable!"); +is(e.persisted, false, "persisted should be false"); +document.dispatchEvent(e); +is(receivedEvent, e, "Wrong event!"); + +e = new PageTransitionEvent("hello", + { bubbles: true, cancelable: true, persisted: true}); +is(e.type, "hello", "Wrong event type!"); +ok(!e.isTrusted, "Event shouldn't be trusted!"); +ok(e.bubbles, "Event should bubble!"); +ok(e.cancelable, "Event should be cancelable!"); +is(e.persisted, true, "persisted should be true"); +document.dispatchEvent(e); +is(receivedEvent, e, "Wrong event!"); + +e = new PageTransitionEvent("hello", { persisted: true}); +is(e.type, "hello", "Wrong event type!"); +ok(!e.isTrusted, "Event shouldn't be trusted!"); +ok(!e.bubbles, "Event shouldn't bubble!"); +ok(!e.cancelable, "Event shouldn't be cancelable!"); +is(e.persisted, true, "persisted should be true"); +document.dispatchEvent(e); +is(receivedEvent, e, "Wrong event!"); + +// PopStateEvent + +try { + e = new PopStateEvent(); +} catch(exp) { + ex = true; +} +ok(ex, "First parameter is required!"); +ex = false; + +e = new PopStateEvent("hello"); +is(e.type, "hello", "Wrong event type!"); +ok(!e.isTrusted, "Event shouldn't be trusted!"); +ok(!e.bubbles, "Event shouldn't bubble!"); +ok(!e.cancelable, "Event shouldn't be cancelable!"); +is(e.state, null, "persisted should be null"); +document.dispatchEvent(e); +is(receivedEvent, e, "Wrong event!"); + +e = new PopStateEvent("hello", + { bubbles: true, cancelable: true, state: window}); +is(e.type, "hello", "Wrong event type!"); +ok(!e.isTrusted, "Event shouldn't be trusted!"); +ok(e.bubbles, "Event should bubble!"); +ok(e.cancelable, "Event should be cancelable!"); +is(e.state, window, "persisted should be window"); +document.dispatchEvent(e); +is(receivedEvent, e, "Wrong event!"); + + +e = new PopStateEvent("hello", { state: window}); +is(e.type, "hello", "Wrong event type!"); +ok(!e.isTrusted, "Event shouldn't be trusted!"); +ok(!e.bubbles, "Event shouldn't bubble!"); +ok(!e.cancelable, "Event shouldn't be cancelable!"); +is(e.state, window, "persisted should be window"); +document.dispatchEvent(e); +is(receivedEvent, e, "Wrong event!"); + +// UIEvent + +try { + e = new UIEvent(); +} catch(exp) { + ex = true; +} +ok(ex, "First parameter is required!"); +ex = false; + +try { + e = new UIEvent("foo", { view: {} }); + e.view.onunload; +} catch(exp) { + ex = true; +} +ok(ex, "{} isn't a valid value."); +ex = false; + +try { + e = new UIEvent("foo", { view: null }); +} catch(exp) { + ex = true; +} +ok(!ex, "null is a valid value."); +is(e.view, null); +ex = false; + +e = new UIEvent("hello"); +is(e.type, "hello", "Wrong event type!"); +ok(!e.isTrusted, "Event shouldn't be trusted!"); +ok(!e.bubbles, "Event shouldn't bubble!"); +ok(!e.cancelable, "Event shouldn't be cancelable!"); +is(e.detail, 0, "detail should be 0"); +is(e.view, null, "view should be null"); +document.dispatchEvent(e); +is(receivedEvent, e, "Wrong event!"); + +e = new UIEvent("hello", + { bubbles: true, cancelable: true, view: window, detail: 1}); +is(e.type, "hello", "Wrong event type!"); +ok(!e.isTrusted, "Event shouldn't be trusted!"); +ok(e.bubbles, "Event should bubble!"); +ok(e.cancelable, "Event should be cancelable!"); +is(e.detail, 1, "detail should be 1"); +is(e.view, window, "view should be window"); +document.dispatchEvent(e); +is(receivedEvent, e, "Wrong event!"); + +// StorageEvent + +e = document.createEvent("StorageEvent"); +ok(e, "Should have created an event!"); + +try { + e = new StorageEvent(); +} catch(exp) { + ex = true; +} +ok(ex, "First parameter is required!"); +ex = false; + +e = new StorageEvent("hello"); +is(e.type, "hello", "Wrong event type!"); +ok(!e.isTrusted, "Event shouldn't be trusted!"); +ok(!e.bubbles, "Event shouldn't bubble!"); +ok(!e.cancelable, "Event shouldn't be cancelable!"); +is(e.key, null, "key should be null"); +is(e.oldValue, null, "oldValue should be null"); +is(e.newValue, null, "newValue should be null"); +is(e.url, "", "url should be ''"); +document.dispatchEvent(e); +is(receivedEvent, e, "Wrong event!"); + +e = new StorageEvent("hello", + { bubbles: true, cancelable: true, key: "key", + oldValue: "oldValue", newValue: "newValue", url: "url", + storageArea: localStorage }); +is(e.type, "hello", "Wrong event type!"); +ok(!e.isTrusted, "Event shouldn't be trusted!"); +ok(e.bubbles, "Event should bubble!"); +ok(e.cancelable, "Event should be cancelable!"); +is(e.key, "key", "Wrong value"); +is(e.oldValue, "oldValue", "Wrong value"); +is(e.newValue, "newValue", "Wrong value"); +is(e.url, "url", "Wrong value"); +is(e.storageArea, localStorage, "Wrong value"); +document.dispatchEvent(e); +is(receivedEvent, e, "Wrong event!"); + +// MouseEvent + +try { + e = new MouseEvent(); +} catch(exp) { + ex = true; +} +ok(ex, "MouseEvent: First parameter is required!"); +ex = false; + +e = new MouseEvent("hello", { buttons: 1, movementX: 2, movementY: 3}); +is(e.type, "hello", "MouseEvent: Wrong event type!"); +ok(!e.isTrusted, "MouseEvent: Event shouldn't be trusted!"); +ok(!e.bubbles, "MouseEvent: Event shouldn't bubble!"); +ok(!e.cancelable, "MouseEvent: Event shouldn't be cancelable!"); +is(e.buttons, 1); +is(e.movementX, 2); +is(e.movementY, 3); +document.dispatchEvent(e); +is(receivedEvent, e, "MouseEvent: Wrong event!"); + +var mouseEventProps = +[ { screenX: 0 }, + { screenY: 0 }, + { clientX: 0 }, + { clientY: 0 }, + { ctrlKey: false }, + { shiftKey: false }, + { altKey: false }, + { metaKey: false }, + { modifierAltGraph: false }, + { modifierCapsLock: false }, + { modifierFn: false }, + { modifierFnLock: false }, + { modifierNumLock: false }, + { modifierOS: false }, + { modifierScrollLock: false }, + { modifierSymbol: false }, + { modifierSymbolLock: false }, + { button: 0 }, + { buttons: 0 }, + { relatedTarget: null }, +]; + +var testProps = +[ + { screenX: 1 }, + { screenY: 2 }, + { clientX: 3 }, + { clientY: 4 }, + { ctrlKey: true }, + { shiftKey: true }, + { altKey: true }, + { metaKey: true }, + { modifierAltGraph: true }, + { modifierCapsLock: true }, + { modifierFn: true }, + { modifierFnLock: true }, + { modifierNumLock: true }, + { modifierOS: true }, + { modifierScrollLock: true }, + { modifierSymbol: true }, + { modifierSymbolLock: true }, + { button: 5 }, + { buttons: 6 }, + { relatedTarget: window } +]; + +var defaultMouseEventValues = {}; +for (var i = 0; i < mouseEventProps.length; ++i) { + for (prop in mouseEventProps[i]) { + if (!isMethodResultInitializer(prop)) { + ok(prop in e, "MouseEvent: MouseEvent doesn't have property " + prop + "!"); + } + defaultMouseEventValues[prop] = mouseEventProps[i][prop]; + } +} + +while (testProps.length) { + var p = testProps.shift(); + e = new MouseEvent("foo", p); + for (var def in defaultMouseEventValues) { + if (!(def in p)) { + is(getPropValue(e, def), defaultMouseEventValues[def], + "MouseEvent: Wrong default value for " + def + "!"); + } else { + is(getPropValue(e, def), p[def], "MouseEvent: Wrong event init value for " + def + "!"); + } + } +} + +// PopupBlockedEvent + +try { + e = new PopupBlockedEvent(); +} catch(exp) { + ex = true; +} +ok(ex, "PopupBlockedEvent: First parameter is required!"); +ex = false; + +e = new PopupBlockedEvent("hello"); +is(e.type, "hello", "PopupBlockedEvent: Wrong event type!"); +ok(!e.isTrusted, "PopupBlockedEvent: Event shouldn't be trusted!"); +ok(!e.bubbles, "PopupBlockedEvent: Event shouldn't bubble!"); +ok(!e.cancelable, "PopupBlockedEvent: Event shouldn't be cancelable!"); +document.dispatchEvent(e); +is(receivedEvent, e, "PopupBlockedEvent: Wrong event!"); + +e = new PopupBlockedEvent("hello", + { requestingWindow: window, + popupWindowFeatures: "features", + popupWindowName: "name" + }); +is(e.requestingWindow, window); +is(e.popupWindowFeatures, "features"); +is(e.popupWindowName, "name"); + +// WheelEvent + +try { + e = new WheelEvent(); +} catch(exp) { + ex = true; +} +ok(ex, "WheelEvent: First parameter is required!"); +ex = false; + +e = new WheelEvent("hello", { buttons: 1, movementX: 2, movementY: 3}); +is(e.type, "hello", "WheelEvent: Wrong event type!"); +is(e.buttons, 1); +is(e.movementX, 2); +is(e.movementY, 3); +ok(!e.isTrusted, "WheelEvent: Event shouldn't be trusted!"); +ok(!e.bubbles, "WheelEvent: Event shouldn't bubble!"); +ok(!e.cancelable, "WheelEvent: Event shouldn't be cancelable!"); +document.dispatchEvent(e); +is(receivedEvent, e, "WheelEvent: Wrong event!"); + +var wheelEventProps = +[ { screenX: 0 }, + { screenY: 0 }, + { clientX: 0 }, + { clientY: 0 }, + { ctrlKey: false }, + { shiftKey: false }, + { altKey: false }, + { metaKey: false }, + { modifierAltGraph: false }, + { modifierCapsLock: false }, + { modifierFn: false }, + { modifierFnLock: false }, + { modifierNumLock: false }, + { modifierOS: false }, + { modifierScrollLock: false }, + { modifierSymbol: false }, + { modifierSymbolLock: false }, + { button: 0 }, + { buttons: 0 }, + { relatedTarget: null }, + { deltaX: 0.0 }, + { deltaY: 0.0 }, + { deltaZ: 0.0 }, + { deltaMode: 0 } +]; + +var testWheelProps = +[ + { screenX: 1 }, + { screenY: 2 }, + { clientX: 3 }, + { clientY: 4 }, + { ctrlKey: true }, + { shiftKey: true }, + { altKey: true }, + { metaKey: true }, + { modifierAltGraph: true }, + { modifierCapsLock: true }, + { modifierFn: true }, + { modifierFnLock: true }, + { modifierNumLock: true }, + { modifierOS: true }, + { modifierScrollLock: true }, + { modifierSymbol: true }, + { modifierSymbolLock: true }, + { button: 5 }, + { buttons: 6 }, + { relatedTarget: window }, + { deltaX: 7.8 }, + { deltaY: 9.1 }, + { deltaZ: 2.3 }, + { deltaMode: 4 } +]; + +var defaultWheelEventValues = {}; +for (var i = 0; i < wheelEventProps.length; ++i) { + for (prop in wheelEventProps[i]) { + if (!isMethodResultInitializer(prop)) { + ok(prop in e, "WheelEvent: WheelEvent doesn't have property " + prop + "!"); + } + defaultWheelEventValues[prop] = wheelEventProps[i][prop]; + } +} + +while (testWheelProps.length) { + var p = testWheelProps.shift(); + e = new WheelEvent("foo", p); + for (var def in defaultWheelEventValues) { + if (!(def in p)) { + is(getPropValue(e, def), defaultWheelEventValues[def], + "WheelEvent: Wrong default value for " + def + "!"); + } else { + is(getPropValue(e, def), p[def], "WheelEvent: Wrong event init value for " + def + "!"); + } + } +} + +// DragEvent + +try { + e = new DragEvent(); +} catch(exp) { + ex = true; +} +ok(ex, "DragEvent: First parameter is required!"); +ex = false; + +e = new DragEvent("hello", { buttons: 1, movementX: 2, movementY: 3}); +is(e.type, "hello", "DragEvent: Wrong event type!"); +is(e.buttons, 1); +is(e.movementX, 2); +is(e.movementY, 3); +document.dispatchEvent(e); +is(receivedEvent, e, "DragEvent: Wrong event!"); + +// TransitionEvent +e = new TransitionEvent("hello", { propertyName: "color", elapsedTime: 3.5, pseudoElement: "", foobar: "baz" }) +is("propertyName" in e, true, "Transition events have propertyName property"); +is("foobar" in e, false, "Transition events do not copy random properties from event init"); +is(e.propertyName, "color", "Transition event copies propertyName from TransitionEventInit"); +is(e.elapsedTime, 3.5, "Transition event copies elapsedTime from TransitionEventInit"); +is(e.pseudoElement, "", "Transition event copies pseudoElement from TransitionEventInit"); +is(e.bubbles, false, "Lack of bubbles property in TransitionEventInit"); +is(e.cancelable, false, "Lack of cancelable property in TransitionEventInit"); +is(e.type, "hello", "Wrong event type!"); +is(e.isTrusted, false, "Event shouldn't be trusted!"); +is(e.eventPhase, Event.NONE, "Wrong event phase"); + +// AnimationEvent +e = new AnimationEvent("hello", { animationName: "bounce3", elapsedTime: 3.5, pseudoElement: "", foobar: "baz" }) +is("animationName" in e, true, "Animation events have animationName property"); +is("foobar" in e, false, "Animation events do not copy random properties from event init"); +is(e.animationName, "bounce3", "Animation event copies animationName from AnimationEventInit"); +is(e.elapsedTime, 3.5, "Animation event copies elapsedTime from AnimationEventInit"); +is(e.pseudoElement, "", "Animation event copies pseudoElement from AnimationEventInit"); +is(e.bubbles, false, "Lack of bubbles property in AnimationEventInit"); +is(e.cancelable, false, "Lack of cancelable property in AnimationEventInit"); +is(e.type, "hello", "Wrong event type!"); +is(e.isTrusted, false, "Event shouldn't be trusted!"); +is(e.eventPhase, Event.NONE, "Wrong event phase"); + +// InputEvent +let dataTransfer = new DataTransfer(); +dataTransfer.setData("text/plain", "foo"); +e = new InputEvent("hello", {data: "something data", dataTransfer, inputType: "invalid input type", isComposing: true}); +is(e.type, "hello", "InputEvent should set type attribute"); +is(e.data, "something data", "InputEvent should have data attribute"); +is(e.dataTransfer, dataTransfer, "InputEvent should have the dataTransfer"); +is(e.dataTransfer.getData("text/plain"), "foo", "InputEvent.dataTransfer should keep handling its data"); +try { + e.dataTransfer.setData("text/plain", "bar"); +} catch (exp) { + ok(false, `InputEvent.dataTransfer.setData("text/plain", "bar") shouldn't fail (${exp})`); +} +is(e.dataTransfer.getData("text/plain"), "bar", "InputEvent.dataTransfer should be modified by a call of its setData()"); +is(e.inputType, "invalid input type", "InputEvent should have inputType attribute"); +is(e.isComposing, true, "InputEvent should have isComposing attribute"); + +dataTransfer = new DataTransfer(); +e = new InputEvent("hello", {data: "", dataTransfer, inputType: "insertText"}); +is(e.data, "", "InputEvent.data should be empty string when empty string is specified explicitly"); +is(e.dataTransfer, dataTransfer, "InputEvent.dataTransfer should have the empty dataTransfer"); +is(e.inputType, "insertText", "InputEvent.inputType should return valid inputType from EditorInputType enum"); +e = new InputEvent("hello", {data: "foo", inputType: "deleteWordBackward"}); +is(e.data, "foo", "InputEvent.data should be the specified string"); +is(e.inputType, "deleteWordBackward", "InputEvent.inputType should return valid inputType from EditorInputType enum"); +e = new InputEvent("hello", {inputType: "formatFontName"}); +is(e.inputType, "formatFontName", "InputEvent.inputType should return valid inputType from EditorInputType enum"); + +e = new InputEvent("input", {}); +is(e.data, null, "InputEvent.data should be null in default"); +is(e.dataTransfer, null, "InputEvent.dataTransfer should be null in default"); +is(e.inputType, "", "InputEvent.inputType should be empty string in default"); +is(e.isComposing, false, "InputEvent.isComposing should be false in default"); + +</script> +</pre> +</body> +</html> diff --git a/dom/events/test/test_eventctors.xhtml b/dom/events/test/test_eventctors.xhtml new file mode 100644 index 0000000000..7906a3c7a8 --- /dev/null +++ b/dom/events/test/test_eventctors.xhtml @@ -0,0 +1,49 @@ +<?xml version="1.0"?> +<?xml-stylesheet type="text/css" href="chrome://global/skin"?> +<?xml-stylesheet type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=675884 +--> +<window title="Mozilla Bug 675884" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + + <!-- test results are displayed in the html:body --> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=675884" + target="_blank">Mozilla Bug 675884</a> + </body> + + <!-- test code goes here --> + <script type="application/javascript"> + <![CDATA[ + /** Test for Bug 675884 **/ + + // Most of the tests are in .html file, but test here that + // isTrusted is handled correctly in chrome. + + var receivedEvent; + document.addEventListener("hello", function(e) { receivedEvent = e; }, true); + + // Event + var e; + var ex = false; + try { + e = new Event(); + } catch(exp) { + ex = true; + } + ok(ex, "First parameter is required!"); + ex = false; + + e = new Event("hello"); + is(e.type, "hello", "Wrong event type!"); + ok(e.isTrusted, "Event should be trusted!"); + ok(!e.bubbles, "Event shouldn't bubble!"); + ok(!e.cancelable, "Event shouldn't be cancelable!"); + document.dispatchEvent(e); + is(receivedEvent, e, "Wrong event!"); + + ]]> + </script> +</window> diff --git a/dom/events/test/test_eventctors_sensors.html b/dom/events/test/test_eventctors_sensors.html new file mode 100644 index 0000000000..090b7d62d0 --- /dev/null +++ b/dom/events/test/test_eventctors_sensors.html @@ -0,0 +1,97 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=675884 +--> +<head> + <title>Test for Bug 675884</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=675884">Mozilla Bug 675884</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> + +SimpleTest.waitForExplicitFinish(); + +SpecialPowers.pushPrefEnv({"set": [ + ["device.sensors.enabled", true], + ["device.sensors.orientation.enabled", true], + ["device.sensors.motion.enabled", true], + ["device.sensors.proximity.enabled", true], + ["device.sensors.ambientLight.enabled", true] +]}, () => { + let receivedEvent; + document.addEventListener("hello", function(e) { receivedEvent = e; }, true); + + // UserProximityEvent + e = new UserProximityEvent("hello", {near: true}); + is(e.type, "hello", "Wrong event type!"); + ok(!e.isTrusted, "Event should not be trusted"); + is(e.near, true, "near should be true"); + document.dispatchEvent(e); + is(receivedEvent, e, "Wrong event!"); + + // DeviceLightEvent + e = new DeviceLightEvent("hello", {value: 1} ); + is(e.type, "hello", "Wrong event type!"); + ok(!e.isTrusted, "Event should not be trusted"); + is(e.value, 1, "value should be 1"); + document.dispatchEvent(e); + is(receivedEvent, e, "Wrong event!"); + e = new DeviceLightEvent("hello", {value: Infinity} ); + is(e.value, Infinity, "value should be positive infinity"); + e = new DeviceLightEvent("hello", {value: -Infinity} ); + is(e.value, -Infinity, "value should be negative infinity"); + e = new DeviceLightEvent("hello"); + is(e.value, Infinity, "Uninitialized value should be positive infinity"); + + // DeviceOrientationEvent + e = new DeviceOrientationEvent("hello"); + is(e.type, "hello", "Wrong event type!"); + ok(!e.isTrusted, "Event should not be trusted"); + is(e.alpha, null); + is(e.beta, null); + is(e.gamma, null); + is(e.absolute, false); + + e = new DeviceOrientationEvent("hello", { alpha: 1, beta: 2, gamma: 3, absolute: true } ); + is(e.type, "hello", "Wrong event type!"); + ok(!e.isTrusted, "Event should not be trusted"); + is(e.alpha, 1); + is(e.beta, 2); + is(e.gamma, 3); + is(e.absolute, true); + document.dispatchEvent(e); + is(receivedEvent, e, "Wrong event!"); + + // DeviceMotionEvent + e = new DeviceMotionEvent("hello"); + is(e.type, "hello", "Wrong event type!"); + ok(!e.isTrusted, "Event should not be trusted"); + is(typeof e.acceleration, "object"); + is(e.acceleration.x, null); + is(e.acceleration.y, null); + is(e.acceleration.z, null); + is(typeof e.accelerationIncludingGravity, "object"); + is(e.accelerationIncludingGravity.x, null); + is(e.accelerationIncludingGravity.y, null); + is(e.accelerationIncludingGravity.z, null); + is(typeof e.rotationRate, "object"); + is(e.rotationRate.alpha, null); + is(e.rotationRate.beta, null); + is(e.rotationRate.gamma, null); + is(e.interval, null); + + SimpleTest.finish(); +}); + +</script> +</pre> +</body> +</html> diff --git a/dom/events/test/test_eventhandler_scoping.html b/dom/events/test/test_eventhandler_scoping.html new file mode 100644 index 0000000000..f15238a0c8 --- /dev/null +++ b/dom/events/test/test_eventhandler_scoping.html @@ -0,0 +1,17 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title>Test for event handler scoping</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<div id="log"></div> +<script> +var queryResult; +test(function() { + var d = document.createElement("div"); + d.setAttribute("onclick", "queryResult = querySelector('span')"); + var s = document.createElement("span"); + d.appendChild(s); + d.dispatchEvent(new Event("click")); + assert_equals(queryResult, s, "Should have gotten the right object"); +}, "Test for bareword calls in an event handler using the element as 'this'"); +</script> diff --git a/dom/events/test/test_focus_abspos.html b/dom/events/test/test_focus_abspos.html new file mode 100644 index 0000000000..8798c4ed11 --- /dev/null +++ b/dom/events/test/test_focus_abspos.html @@ -0,0 +1,32 @@ +<!doctype html> +<title>Test for bug 1424633: clicking on an oof descendant focus its focusable ancestor</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/tests/SimpleTest/EventUtils.js"></script> +<style> + #focusable { + width: 100px; + height: 100px; + background-color: blue; + } + #oof { + background-color: green; + position: absolute; + top: 25px; + } +</style> +<div tabindex="0" id="focusable"> + <span id="oof">Absolute</span> +</div> +<script> +window.onload = function() { + async_test(function(t) { + document.body.offsetTop; + setTimeout(t.step_func_done(function() { + let span = document.querySelector("#oof"); + synthesizeMouseAtCenter(span, {type: "mousedown"}); + assert_equals(document.activeElement, document.querySelector("#focusable")); + }), 0); + }, "Clicking on an abspos descendant focus its focusable ancestor"); +} +</script> diff --git a/dom/events/test/test_focus_blur_on_click_in_cross_origin_iframe.html b/dom/events/test/test_focus_blur_on_click_in_cross_origin_iframe.html new file mode 100644 index 0000000000..66d105d4a4 --- /dev/null +++ b/dom/events/test/test_focus_blur_on_click_in_cross_origin_iframe.html @@ -0,0 +1,118 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title></title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<script src="/tests/SimpleTest/EventUtils.js"></script> +<script src="/tests/SimpleTest/paint_listener.js"></script> +<script src="/tests/gfx/layers/apz/test/mochitest/apz_test_utils.js"></script> +<script src="/tests/gfx/layers/apz/test/mochitest/apz_test_native_event_utils.js"></script> +<iframe width=100></iframe> +<script> +SimpleTest.requestLongerTimeout(2); + +let state = "start"; + +function getScreenPosition(aElement, aOffsetX, aOffsetY) { + const rect = aElement.getBoundingClientRect(); + const x = aOffsetX + window.mozInnerScreenX + rect.left; + const y = aOffsetY + window.mozInnerScreenY + rect.top; + const scale = window.devicePixelRatio; + return [Math.round(x * scale), Math.round(y * scale)]; +} + +add_task(async () => { + await SimpleTest.promiseFocus(); + + const loadsPromise = new Promise((resolve, reject) => { + window.addEventListener("message", function listener(event) { + info(`receive ${event.data}`); + if (event.data == "ready") { + is(state, "start", "check initial state"); + state = "ready"; + resolve(); + } else { + reject("Unexpected message"); + } + }, { once: true }); + }); + + const iframe = document.querySelectorAll("iframe")[0]; + iframe.src = "https://example.com/tests/dom/events/test/file_focus_blur_on_click_in_cross_origin_iframe.html"; + + await loadsPromise; + + // Wait for APZ state stable so that mouse event handling APZ works properly + // in out-of-process iframes. + await waitUntilApzStable(); + + // NOTE: synthesizeMouseAtCenter doesn't work for OOP iframes (bug 1528935), + // so we use promiseNativeMouseEventWithAPZ instead. + const [expectedScreenX, expectedScreenY] = + getScreenPosition(iframe, 10, 10); + + const firstClickPromise = new Promise((resolve, reject) => { + window.addEventListener("message", function listener(event) { + info(`receive ${event.data}`); + if (state == "ready") { + if (event.data == "focus") { + state = "focusbeforeclick"; + } else if (event.data == "click") { + ok(false, "Focusing failed to complete before mouseup"); + state = "clickbeforefocus"; + } else { + ok(false, "Unexpected event"); + } + } else if (state == "focusbeforeclick") { + is(event.data, "click", "The second event should be 'click'"); + state = "firstclick"; + window.removeEventListener("message", listener); + resolve(); + } else if (state == "clickbeforefocus") { + is(event.data, "focus", "The second event should be 'click'"); + state = "firstclick"; + window.removeEventListener("message", listener); + resolve(); + } else { + reject("Unexpected message"); + } + }); + }); + + await promiseNativeMouseEventWithAPZ({ + type: "click", + target: iframe, + screenX: expectedScreenX, + screenY: expectedScreenY, + }); + + await firstClickPromise; + + SimpleTest.requestFlakyTimeout("Waiting for unwanted events that don't exist on success."); + + const secondClickPromise = new Promise((resolve, reject) => { + window.addEventListener("message", function listener(event) { + info(`receive ${event.data}`); + if (state == "firstclick") { + is(event.data, "click", "The third event should be 'click' again, not 'blur' or 'focus'."); + state = "secondclick"; + setTimeout(function() { + // Wait for potential other unwanted events + window.removeEventListener("message", listener); + resolve() + }, 200); + } else { + reject("Unexpected message " + event.data); + } + }); + }); + + await promiseNativeMouseEventWithAPZ({ + type: "click", + target: iframe, + screenX: expectedScreenX, + screenY: expectedScreenY, + }); + + await secondClickPromise; +}); +</script> diff --git a/dom/events/test/test_focus_blur_on_click_in_deep_cross_origin_iframe.html b/dom/events/test/test_focus_blur_on_click_in_deep_cross_origin_iframe.html new file mode 100644 index 0000000000..f360ef7c20 --- /dev/null +++ b/dom/events/test/test_focus_blur_on_click_in_deep_cross_origin_iframe.html @@ -0,0 +1,146 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title></title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<script src="/tests/SimpleTest/EventUtils.js"></script> +<script src="/tests/SimpleTest/paint_listener.js"></script> +<script src="/tests/gfx/layers/apz/test/mochitest/apz_test_utils.js"></script> +<script src="/tests/gfx/layers/apz/test/mochitest/apz_test_native_event_utils.js"></script> +<iframe width=100 height=200 scrolling="no"></iframe> +<script> +let state = "start"; + +const scale = window.devicePixelRatio; + +function getScreenPosition(aElement, aOffsetX, aOffsetY) { + const rect = aElement.getBoundingClientRect(); + const x = aOffsetX + window.mozInnerScreenX + rect.left; + const y = aOffsetY + window.mozInnerScreenY + rect.top; + return [Math.round(x * scale), Math.round(y * scale)]; +} + +add_task(async () => { + await SimpleTest.promiseFocus(); + + const loadsPromise = new Promise((resolve, reject) => { + let readyMiddle = false; + let readyInner = false; + window.addEventListener("message", function listener(event) { + info(`receive ${event.data}`); + if (event.data == "middleready") { + readyMiddle = true; + } else if (event.data == "innerready") { + readyInner = true; + } else { + reject("Unexpected message when waiting for ready " + event.data); + } + if (readyInner && readyMiddle) { + state = "ready"; + window.removeEventListener("message", listener); + resolve(); + } + }); + }); + + const iframe = document.querySelectorAll("iframe")[0]; + iframe.src = "https://example.com/tests/dom/events/test/file_focus_blur_on_click_in_deep_cross_origin_iframe_middle.html"; + + await loadsPromise; + + // Wait for APZ state stable so that mouse event handling APZ works properly + // in out-of-process iframes. + await waitUntilApzStable(); + + // NOTE: synthesizeMouseAtCenter doesn't work for OOP iframes (bug 1528935), + // so we use promiseNativeMouseEventWithAPZ instead. + const [expectedScreenX, expectedScreenY] = + getScreenPosition(iframe, 10, 10); + + const firstClickPromise = new Promise((resolve, reject) => { + window.addEventListener("message", function listener(event) { + info(`receive ${event.data}`); + if (state == "ready") { + if (event.data == "innerfocus") { + state = "innerfocusbeforeclick"; + } else if (event.data == "innerclick") { + ok(false, "Focusing failed to complete before mouseup"); + state = "innerclickbeforefocus"; + } else if (event.data == "middlefocus") { + is(false, "Should not get an extra middlefocus."); + } else { + is(event.data, "neverthisevent", "Unexpected event (first click)"); + } + } else if (state == "innerfocusbeforeclick") { + is(event.data, "innerclick", "The second event should be 'innerclick'"); + state = "firstclick"; + window.removeEventListener("message", listener); + resolve(); + } else if (state == "innerclickbeforefocus") { + is(event.data, "innerfocus", "The second event should be 'innerfocus'"); + state = "firstclick"; + window.removeEventListener("message", listener); + resolve(); + } else { + reject("Unexpected message in firstClickPromise " + event.data); + } + }); + }); + + await promiseNativeMouseEventWithAPZ({ + type: "click", + target: iframe, + screenX: expectedScreenX, + screenY: expectedScreenY + Math.round(110 * scale), + }); + + await firstClickPromise; + + SimpleTest.requestFlakyTimeout("Waiting for unwanted events that don't exist on success."); + + const secondClickPromise = new Promise((resolve, reject) => { + window.addEventListener("message", function listener(event) { + info(`receive ${event.data}`); + if (state == "firstclick") { + is(event.data, "middlefocus", "The third event should be 'middlefocus'."); + state = "middlefocusbeforeclick"; + } else if (state == "middlefocusbeforeclick") { + // The order of blur and click is non-deterministic even in the non-Fission case. + if (event.data == "middleclick") { + state = "waitingforinnerblurafterclick"; + } else if (event.data == "innerblur") { + state = "innerblurbeforeclick"; + } else { + is(event.data, "neverthisevent", "Unexpected event (first click)"); + } + } else if (state == "waitingforinnerblurafterclick") { + is(event.data, "innerblur", "The fifth event should be 'innerblur'."); + state = "secondclick"; + setTimeout(function() { + // Wait for potential other unwanted events + window.removeEventListener("message", listener); + resolve() + }, 200); + } else if (state == "innerblurbeforeclick") { + is(event.data, "middleclick", "The fifth event should be 'middleclick'."); + state = "secondclick"; + setTimeout(function() { + // Wait for potential other unwanted events + window.removeEventListener("message", listener); + resolve() + }, 200); + } else { + reject("Unexpected message in secondClickPromise " + event.data); + } + }); + }); + + await promiseNativeMouseEventWithAPZ({ + type: "click", + target: iframe, + screenX: expectedScreenX, + screenY: expectedScreenY, + }); + + await secondClickPromise; +}); +</script> diff --git a/dom/events/test/test_hover_mouseleave.html b/dom/events/test/test_hover_mouseleave.html new file mode 100644 index 0000000000..aac25f5b51 --- /dev/null +++ b/dom/events/test/test_hover_mouseleave.html @@ -0,0 +1,47 @@ +<!doctype html> +<meta charset="utf-8"> +<title>Test :hover state on mouseleave.</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<script src="/tests/SimpleTest/EventUtils.js"></script> +<link rel="stylesheet" href="/tests/SimpleTest/test.css"/> +<style> +div { + width: 100px; + height: 100px; +} +</style> +<div id="target" style="background: green;"></div> +<div id="outside" style="background: blue;"></div> +<script> +SimpleTest.waitForExplicitFinish(); +let mouseLeaveCount = 0; +let mouseOutCount = 0; + +target.addEventListener("mouseleave", () => { + if (mouseLeaveCount++ != 0) + return; + is(target.matches(":hover"), false, + "Should've been not hovered on mouseleave"); + is(outside.matches(":hover"), true, + "New target should be hovered on mouseleave"); + if (mouseOutCount) + SimpleTest.finish(); +}); + +target.addEventListener("mouseout", () => { + if (mouseOutCount++ != 0) + return; + is(target.matches(":hover"), false, + "Should've been not hovered on mouseleave"); + is(outside.matches(":hover"), true, + "New target should be hovered on mouseleave"); + if (mouseLeaveCount) + SimpleTest.finish(); +}); + +SimpleTest.waitForFocus(() => { + synthesizeMouseAtCenter(outside, { type: "mousemove" }); + synthesizeMouseAtCenter(target, { type: "mousemove" }); + synthesizeMouseAtCenter(outside, { type: "mousemove" }); +}); +</script> diff --git a/dom/events/test/test_legacy_event.html b/dom/events/test/test_legacy_event.html new file mode 100644 index 0000000000..82ab2dec6b --- /dev/null +++ b/dom/events/test/test_legacy_event.html @@ -0,0 +1,297 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1236979 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1236979 (events that have legacy alternative versions)</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <style> + @keyframes anim1 { + 0% { margin-left: 0px } + 100% { margin-left: 100px } + } + </style> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1236979">Mozilla Bug 1236979</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 1236979 **/ + +'use strict'; +SimpleTest.waitForExplicitFinish(); + +// Array of info-bundles about each legacy event to be tested: +var gLegacyEventInfo = [ + { + legacy_name: "webkitTransitionEnd", + modern_name: "transitionend", + trigger_event: triggerShortTransition, + }, + { + legacy_name: "webkitAnimationStart", + modern_name: "animationstart", + trigger_event: triggerShortAnimation, + }, + { + legacy_name: "webkitAnimationEnd", + modern_name: "animationend", + trigger_event: triggerShortAnimation, + }, + { + legacy_name: "webkitAnimationIteration", + modern_name: "animationiteration", + trigger_event: triggerAnimationIteration, + } +]; + +// EVENT-TRIGGERING FUNCTIONS +// -------------------------- +// This function triggers a very short (1ms long) transition, which will cause +// events to fire for the transition ending. +function triggerShortTransition(node) { + node.style.transition = "1ms color linear" ; + node.style.color = "purple"; + // Flush style, so that the above assignment value actually takes effect + // in the computed style, so that a transition will get triggered when it + // changes. + window.getComputedStyle(node).color; + node.style.color = "teal"; +} + +// This function triggers a very short (1ms long) animation, which will cause +// events to fire for the animation beginning & ending. +function triggerShortAnimation(node) { + node.style.animation = "anim1 1ms linear"; +} + +// This function triggers a very short (10ms long) animation with many +// iterations, which will cause a start event followed by an iteration event +// on each subsequent tick, to fire. +// +// NOTE: We need the many iterations since if an animation frame coincides +// with the animation starting or ending we dispatch only the start or end +// event and not the iteration event. +function triggerAnimationIteration(node) { + node.style.animation = "anim1 10ms linear 20000"; +} + +// GENERAL UTILITY FUNCTIONS +// ------------------------- +// Creates a new div and appends it as a child of the specified parentNode, or +// (if no parent is specified) as a child of the element with ID 'display'. +function createChildDiv(parentNode) { + if (!parentNode) { + parentNode = document.getElementById("display"); + if (!parentNode) { + ok(false, "no 'display' element to append to"); + } + } + var div = document.createElement("div"); + parentNode.appendChild(div); + return div; +} + +// Returns an event-handler function, which (when invoked) simply checks that +// the event's type matches what's expected. If a callback is passed in, then +// the event-handler will invoke that callback as well. +function createHandlerWithTypeCheck(expectedEventType, extraHandlerLogic) { + var handler = function(e) { + is(e.type, expectedEventType, + "When an event handler for '" + expectedEventType + "' is invoked, " + + "the event's type field should be '" + expectedEventType + "'."); + if (extraHandlerLogic) { + extraHandlerLogic(e); + } + } + return handler; +} + +// TEST FUNCTIONS +// -------------- +// These functions expect to be passed an entry from gEventInfo, and they +// return a Promise which performs the test & resolves when it's complete. +// The function names all begin with "mp", which stands for "make promise". +// So e.g. "mpTestLegacyEventSent" means "make a promise to test that the +// legacy event is sent". + +// Tests that the legacy event type is sent, when only a legacy handler is +// registered. +function mpTestLegacyEventSent(eventInfo) { + return new Promise( + function(resolve, reject) { + // Create a node & register an event-handler for the legacy event: + var div = createChildDiv(); + + var handler = createHandlerWithTypeCheck(eventInfo.legacy_name, + function() { + // When event-handler is done, clean up & resolve: + div.remove(); + resolve(); + }); + div.addEventListener(eventInfo.legacy_name, handler); + + // Trigger the event: + eventInfo.trigger_event(div); + } + ); +} + +// Test that the modern event type (and only the modern event type) is fired, +// when listeners of both modern & legacy types are registered. The legacy +// listener should not be invoked. +function mpTestModernBeatsLegacy(eventInfo) { + return new Promise( + function(resolve, reject) { + var div = createChildDiv(); + + var legacyHandler = function(e) { + reject("Handler for legacy event '" + eventInfo.legacy_name + + "' should not be invoked when there's a handler registered " + + "for both modern & legacy event type on the same node"); + }; + + var modernHandler = createHandlerWithTypeCheck(eventInfo.modern_name, + function() { + // Indicate that the test has passed (we invoked the modern handler): + ok(true, "Handler for modern event '" + eventInfo.modern_name + + "' should be invoked when there's a handler registered for " + + "both modern & legacy event type on the same node"); + // When event-handler is done, clean up & resolve: + div.remove(); + resolve(); + }); + + div.addEventListener(eventInfo.legacy_name, legacyHandler); + div.addEventListener(eventInfo.modern_name, modernHandler); + eventInfo.trigger_event(div); + } + ); +} + +// Test that an event which bubbles may fire listeners of different flavors +// (modern vs. legacy) at each bubbling level, depending on what's registered +// at that level. +function mpTestDiffListenersEventBubbling(eventInfo) { + return new Promise( + function(resolve, reject) { + var grandparent = createChildDiv(); + var parent = createChildDiv(grandparent); + var target = createChildDiv(parent); + var didEventFireOnTarget = false; + var didEventFireOnParent = false; + var eventSentToTarget; + + target.addEventListener(eventInfo.modern_name, + createHandlerWithTypeCheck(eventInfo.modern_name, function(e) { + ok(e.bubbles, "Expecting event to bubble"); + eventSentToTarget = e; + didEventFireOnTarget = true; + })); + + parent.addEventListener(eventInfo.legacy_name, + createHandlerWithTypeCheck(eventInfo.legacy_name, function(e) { + is(e, eventSentToTarget, + "Same event object should bubble, despite difference in type"); + didEventFireOnParent = true; + })); + + grandparent.addEventListener(eventInfo.modern_name, + createHandlerWithTypeCheck(eventInfo.modern_name, function(e) { + ok(didEventFireOnTarget, + "Event should have fired on child"); + ok(didEventFireOnParent, + "Event should have fired on parent"); + is(e, eventSentToTarget, + "Same event object should bubble, despite difference in type"); + // Clean up. + grandparent.remove(); + resolve(); + })); + + eventInfo.trigger_event(target); + } + ); +} + +// Test that an event in the capture phase may fire listeners of different +// flavors (modern vs. legacy) at each level, depending on what's registered +// at that level. +function mpTestDiffListenersEventCapturing(eventInfo) { + return new Promise( + function(resolve, reject) { + var grandparent = createChildDiv(); + var parent = createChildDiv(grandparent); + var target = createChildDiv(parent); + var didEventFireOnTarget = false; + var didEventFireOnParent = false; + var didEventFireOnGrandparent = false; + var eventSentToGrandparent; + + grandparent.addEventListener(eventInfo.modern_name, + createHandlerWithTypeCheck(eventInfo.modern_name, function(e) { + eventSentToGrandparent = e; + didEventFireOnGrandparent = true; + }), true); + + parent.addEventListener(eventInfo.legacy_name, + createHandlerWithTypeCheck(eventInfo.legacy_name, function(e) { + is(e.eventPhase, Event.CAPTURING_PHASE, + "event should be in capturing phase"); + is(e, eventSentToGrandparent, + "Same event object should capture, despite difference in type"); + ok(didEventFireOnGrandparent, + "Event should have fired on grandparent"); + didEventFireOnParent = true; + }), true); + + target.addEventListener(eventInfo.modern_name, + createHandlerWithTypeCheck(eventInfo.modern_name, function(e) { + is(e.eventPhase, Event.AT_TARGET, + "event should be at target phase"); + is(e, eventSentToGrandparent, + "Same event object should capture, despite difference in type"); + ok(didEventFireOnParent, + "Event should have fired on parent"); + // Clean up. + grandparent.remove(); + resolve(); + }), true); + + eventInfo.trigger_event(target); + } + ); +} + +// MAIN FUNCTION: Kick off the tests. +function main() { + Promise.resolve().then(function() { + return Promise.all(gLegacyEventInfo.map(mpTestLegacyEventSent)) + }).then(function() { + return Promise.all(gLegacyEventInfo.map(mpTestModernBeatsLegacy)); + }).then(function() { + return Promise.all(gLegacyEventInfo.map(mpTestDiffListenersEventCapturing)); + }).then(function() { + return Promise.all(gLegacyEventInfo.map(mpTestDiffListenersEventBubbling)); + }).then(function() { + SimpleTest.finish(); + }).catch(function(reason) { + ok(false, "Test failed: " + reason); + SimpleTest.finish(); + }); +} + +main(); + +</script> +</pre> +</body> +</html> diff --git a/dom/events/test/test_legacy_non-primary_click.html b/dom/events/test/test_legacy_non-primary_click.html new file mode 100644 index 0000000000..6906282aa2 --- /dev/null +++ b/dom/events/test/test_legacy_non-primary_click.html @@ -0,0 +1,53 @@ +<!DOCTYPE html> +<html> +<head> + <meta charset="utf-8"> + <title>Test for dispatching of legacy non-primary click when domain in pref</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<a id="link-test" href="example.org">example link</a> +<script> +"use strict"; +SimpleTest.waitForExplicitFinish(); + +const HACK_PREF = "dom.mouseevent.click.hack.use_legacy_non-primary_dispatch"; +const testEl = document.getElementById("test"); +const linkEl = document.getElementById("link-test"); +let seenClick = false; + +SpecialPowers.pushPrefEnv( + { set: [[HACK_PREF, document.domain]] }, + () => { + SimpleTest.waitForFocus(() => { + // Test seeing the non-primary 'click' + document.addEventListener("click", (e) => { + ok(true, "Saw 'click' event"); + seenClick = true; + }, { once: true }); + document.addEventListener("auxclick", (e) => { + ok(true, "Saw 'auxclick' event"); + ok(seenClick, "Saw 'click' event before 'auxclick' event"); + }, { once: true }); + synthesizeMouseAtCenter(testEl, { button: 1 }); + + // Test preventDefaulting on non-primary 'click' + document.addEventListener("click", (e) => { + is(e.target, linkEl, "Saw 'click' on link"); + e.preventDefault(); + SimpleTest.finish(); + }, { once: true, capture: true }); + document.addEventListener("auxclick", (e) => { + ok(false, "Shouldn't have got 'auxclick' after preventDefaulting 'click'"); + }, { once: true }); + synthesizeMouseAtCenter(linkEl, { button: 1 }); + }); + }); +</script> +</body> +</html> diff --git a/dom/events/test/test_legacy_touch_api.html b/dom/events/test/test_legacy_touch_api.html new file mode 100644 index 0000000000..610f787586 --- /dev/null +++ b/dom/events/test/test_legacy_touch_api.html @@ -0,0 +1,65 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1412485 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1412485</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript"> + + /** Test for Bug 1412485 **/ + SimpleTest.waitForExplicitFinish(); + + function testExistenceOfLegacyTouchAPIs(win, enabled) { + try { + var event = document.createEvent("TouchEvent"); + ok(event instanceof TouchEvent, "Should be able to create TouchEvent using createEvent."); + } catch(ex) { + ok(true, "Shouldn't be able create TouchEvent using createEvent."); + } + + var targets = [win, win.document, win.document.body]; + for (target of targets) { + is("ontouchstart" in target, enabled, `ontouchstart on target [${enabled}].`); + is("ontouchend" in target, enabled, `ontouchend on target [${enabled}].`); + is("ontouchmove" in target, enabled, `ontouchmove on target [${enabled}].`); + is("ontouchcancel" in target, enabled, `ontouchcancel on target [${enabled}].`); + } + + is("createTouch" in win.document, enabled, `createTouch on Document [${enabled}].`); + is("createTouchList" in win.document, enabled, `createTouchList on Document [${enabled}].`); + } + + function test() { + // Test the defaults. + testExistenceOfLegacyTouchAPIs(window, + navigator.userAgent.includes("Android")); + + // Test explicitly enabling touch APIs. + SpecialPowers.pushPrefEnv({"set": [["dom.w3c_touch_events.enabled", 1], + ["dom.w3c_touch_events.legacy_apis.enabled", true]]}, + function() { + var iframe = document.createElement("iframe"); + document.body.appendChild(iframe); + iframe.onload = function() { + testExistenceOfLegacyTouchAPIs(iframe.contentWindow, true); + SimpleTest.finish(); + } + }); + } + + </script> +</head> +<body onload="test()"> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1412485">Mozilla Bug 1412485</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/dom/events/test/test_marquee_events.html b/dom/events/test/test_marquee_events.html new file mode 100644 index 0000000000..22d0eafdf1 --- /dev/null +++ b/dom/events/test/test_marquee_events.html @@ -0,0 +1,31 @@ +<html> +<head> + <meta charset="utf-8"> + <title>Test for bug 1425874</title> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> +</head> +<body> + <script> + var wasEventCalled; + function callEventWithAttributeHandler(element, evt) { + wasEventCalled = false; + let el = document.createElement(element); + el.setAttribute(`on${evt}`, "wasEventCalled = true"); + el.dispatchEvent(new Event(evt)); + return wasEventCalled; + } + + info("Make sure the EventNameType_HTMLMarqueeOnly events only compile for marquee"); + + ok(!callEventWithAttributeHandler("div", "bounce"), "no onbounce for div"); + ok(!callEventWithAttributeHandler("div", "finish"), "no onfinish for div"); + ok(!callEventWithAttributeHandler("div", "start"), "no onstart for div"); + + ok(callEventWithAttributeHandler("marquee", "bounce"), "onbounce for marquee"); + ok(callEventWithAttributeHandler("marquee", "finish"), "onfinish for marquee"); + ok(callEventWithAttributeHandler("marquee", "start"), "onstart for marquee"); + </script> +</body> +</html> diff --git a/dom/events/test/test_messageEvent.html b/dom/events/test/test_messageEvent.html new file mode 100644 index 0000000000..ac12bbc99b --- /dev/null +++ b/dom/events/test/test_messageEvent.html @@ -0,0 +1,79 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=848294 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 848294</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script src="/tests/SimpleTest/WindowSnapshot.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + <script type="application/javascript"> + function testMessageEvent(e, test) { + ok(e, "MessageEvent created"); + is(e.type, 'message', 'MessageEvent.type is right'); + + is(e.data, 'data' in test ? test.data : null, 'MessageEvent.data is ok'); + is(e.origin, 'origin' in test ? test.origin : '', 'MessageEvent.origin is ok'); + is(e.lastEventId, 'lastEventId' in test ? test.lastEventId : '', 'MessageEvent.lastEventId is ok'); + is(e.source, 'source' in test ? test.source : null, 'MessageEvent.source is ok'); + + if (test.ports != undefined) { + is(e.ports.length, test.ports.length, 'MessageEvent.ports is ok'); + is(e.ports, e.ports, 'MessageEvent.ports is ok'); + } else { + ok(!('ports' in test) || test.ports == null, 'MessageEvent.ports is ok'); + } + } + + function runTest() { + var channel = new MessageChannel(); + + var tests = [ + {}, + { data: 42 }, + { data: {} }, + { data: true, origin: 'wow' }, + { data: [], lastEventId: 'wow2' }, + { data: null, source: null }, + { data: window, source: window }, + { data: window, source: channel.port1 }, + { data: window, source: channel.port1, ports: [ channel.port1, channel.port2 ] }, + { data: null, ports: [] }, + ]; + + while (tests.length) { + var test = tests.shift(); + + var e = new MessageEvent('message', test); + testMessageEvent(e, test); + + e = new MessageEvent('message'); + e.initMessageEvent('message', true, true, + 'data' in test ? test.data : null, + 'origin' in test ? test.origin : '', + 'lastEventId' in test ? test.lastEventId : '', + 'source' in test ? test.source : null, + 'ports' in test ? test.ports : []); + testMessageEvent(e, test); + } + + try { + var e = new MessageEvent('foobar', { source: 42 }); + ok(false, "Source has to be a window or a port"); + } catch(ex) { + ok(true, "Source has to be a window or a port"); + } + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + runTest(); + </script> +</body> +</html> diff --git a/dom/events/test/test_messageEvent_init.html b/dom/events/test/test_messageEvent_init.html new file mode 100644 index 0000000000..9f5eea8f37 --- /dev/null +++ b/dom/events/test/test_messageEvent_init.html @@ -0,0 +1,25 @@ +<html><head> +<title>Test for bug 1308956</title> +<link rel="stylesheet" href="/tests/SimpleTest/test.css"> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<script src="/tests/SimpleTest/EventUtils.js"></script> + +<body> + <script> + +var a = new MessageEvent("message") +ok(!!a, "We have a MessageEvent"); +is(a.ports.length, 0, "By default MessageEvent.ports is an empty array"); + +a.initMessageEvent("message", true, false, {}, window.location.href, "", null, []); +ok(Array.isArray(a.ports), "After InitMessageEvent() we have an array"); +is(a.ports.length, 0, "Length is 0"); + +var mc = new MessageChannel(); +a.initMessageEvent("message", true, false, {}, window.location.href, "", null, [mc.port1]); +ok(Array.isArray(a.ports), "After InitMessageEvent() we have an array"); +is(a.ports.length, 1, "Length is 1"); + + </script> +</body> +</html> diff --git a/dom/events/test/test_mouse_capture_iframe.html b/dom/events/test/test_mouse_capture_iframe.html new file mode 100644 index 0000000000..424e73795d --- /dev/null +++ b/dom/events/test/test_mouse_capture_iframe.html @@ -0,0 +1,70 @@ +<!doctype html> +<meta charset="utf-8"> +<title>Test mouse capture for iframe</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<script src="/tests/SimpleTest/EventUtils.js"></script> +<script src="/tests/SimpleTest/paint_listener.js"></script> +<script src="/tests/gfx/layers/apz/test/mochitest/apz_test_utils.js"></script> +<link rel="stylesheet" href="/tests/SimpleTest/test.css"/> +<style> +#target { + width: 150px; + height: 150px; +} +</style> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1680405">Mozilla Bug 1680405</a> +<iframe id="target" frameborder="0" scrolling="no"></iframe> +<script> + +function waitForMessage(aEventType) { + return new Promise(function(aResolve, aReject) { + window.addEventListener("message", function listener(aEvent) { + is(aEvent.data, aEventType, `check received message ${aEvent.data}`); + aResolve(); + }, { once: true }); + }); +} + +let iframe = document.getElementById("target"); + +add_setup(async function() { + await SimpleTest.promiseFocus(); + await SpecialPowers.pushPrefEnv({ set: [["test.events.async.enabled", true]] }); + disableNonTestMouseEvents(true); + SimpleTest.registerCleanupFunction(() => { + disableNonTestMouseEvents(false); + }); + + iframe.src = "http://example.com/tests/dom/events/test/file_empty.html"; + await waitForMessage("ready"); + + await SpecialPowers.spawn(iframe, [], () => { + let handler = function(e) { + content.parent.postMessage(e.type, "*"); + }; + content.document.addEventListener("mousedown", handler); + content.document.addEventListener("mousemove", handler); + content.document.addEventListener("mouseup", handler); + }); + + await waitUntilApzStable(); +}); + +add_task(async function testMouseCaptureOnXoriginIframe() { + let unexpectedHandler = function(e) { + ok(false, `receive unexpected ${e.type} event`); + }; + document.addEventListener("mousedown", unexpectedHandler); + document.addEventListener("mousemove", unexpectedHandler); + document.addEventListener("mouseup", unexpectedHandler); + + synthesizeMouseAtCenter(iframe, { type: "mousedown" }); + await waitForMessage("mousedown"); + + synthesizeMouse(iframe, 200, 200, { type: "mousemove" }); + await waitForMessage("mousemove"); + + synthesizeMouse(iframe, 200, 200, { type: "mouseup" }); + await waitForMessage("mouseup"); +}); +</script> diff --git a/dom/events/test/test_mouse_enterleave_iframe.html b/dom/events/test/test_mouse_enterleave_iframe.html new file mode 100644 index 0000000000..160174865f --- /dev/null +++ b/dom/events/test/test_mouse_enterleave_iframe.html @@ -0,0 +1,305 @@ +<!doctype html> +<meta charset="utf-8"> +<title>Test mouseenter and mouseleave for iframe.</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<script src="/tests/SimpleTest/EventUtils.js"></script> +<script src="/tests/SimpleTest/paint_listener.js"></script> +<script src="/tests/gfx/layers/apz/test/mochitest/apz_test_utils.js"></script> +<link rel="stylesheet" href="/tests/SimpleTest/test.css"/> +<style> +#start { + width: 300px; + height: 30px; +} + +#target, #target2 { + width: 150px; + height: 150px; + background-color: #fcc; + display: inline-block; +} + +#frame, #frame2 { + height: 100%; + width: 100%; +} + +#reflow, #div { + width: 300px; + height: 10px; + background-color: lightgreen; +} +</style> +<div id="start">Start from here!!</div> +<div id="div"></div> +<div id="target"> + <iframe id="frame" frameborder="0" scrolling="no" src="http://example.com/tests/dom/events/test/file_mouse_enterleave.html"></iframe> +</div> +<div id="target2"> + <iframe id="frame2" frameborder="0" scrolling="no" src="http://example.com/tests/dom/events/test/file_mouse_enterleave.html"></iframe> +</div> +<div id="reflow"></div> +<script> + +function reflow() { + let div = document.getElementById("reflow"); + div.style.display = "none"; + div.getBoundingClientRect(); + div.style.display = "block"; + div.getBoundingClientRect(); +} + +function waitForMessage(aRemoteTarget, aEventType, aTargetName) { + return new Promise(function(aResolve, aReject) { + window.addEventListener("message", function listener(aEvent) { + if (aEvent.source != aRemoteTarget.contentWindow) { + return; + } + + if (aEvent.data.eventType !== aEventType) { + window.removeEventListener("message", listener); + ok(false, `receive unexpected message ${JSON.stringify(aEvent.data)}`); + aReject(new Error(`receive unexpected message ${JSON.stringify(aEvent.data)}`)); + return; + } + + if (aEvent.data.targetName !== aTargetName) { + return; + } + + ok(true, `receive message ${JSON.stringify(aEvent.data)}`); + // Trigger a reflow which will generate synthesized mouse move event. + aRemoteTarget.contentWindow.postMessage("reflow", "*"); + // Now wait a bit to see if there is any unexpected message fired. + setTimeout(function() { + window.removeEventListener("message", listener); + aResolve(); + }, 0); + }); + }); +} + +function waitForLeaveEvent(aTarget) { + return new Promise(function(aResolve) { + aTarget.addEventListener("mouseleave", function(aEvent) { + ok(true, `receive ${aEvent.type}`); + aResolve(); + }, { once: true }); + }); +} + +function waitForEnterLeaveEvents(aEnterTarget, aLeaveTarget) { + let expectedEvents = [{target: aEnterTarget, eventName: "mouseenter"}]; + if (aLeaveTarget) { + expectedEvents.push({target: aLeaveTarget, eventName: "mouseleave"}) + } + + return new Promise(function(aResolve, aReject) { + function cleanup() { + aEnterTarget.removeEventListener("mouseenter", listener); + aEnterTarget.removeEventListener("mouseleave", unexpectedEvent); + if (aLeaveTarget) { + aLeaveTarget.removeEventListener("mouseenter", unexpectedEvent); + aLeaveTarget.removeEventListener("mouseleave", listener); + } + } + + function unexpectedEvent(aEvent) { + cleanup(); + ok(false, `receive unexpected ${aEvent.type}`); + aReject(new Error(`receive unexpected ${aEvent.type}`)); + } + + async function listener(aEvent) { + if (expectedEvents.length <= 0) { + unexpectedEvent(aEvent); + return; + } + + let expectedEvent = expectedEvents.pop(); + if (expectedEvent.target == aEvent.target && + expectedEvent.eventName == aEvent.type) { + ok(true, `receive ${aEvent.type}`); + } else { + unexpectedEvent(aEvent); + return; + } + + if (!expectedEvents.length) { + // Trigger a reflow which will generate synthesized mouse move event. + reflow(); + // Now wait a bit to see if there is any unexpected event fired. + setTimeout(function() { + cleanup(); + aResolve(); + }, 0); + } + } + + aEnterTarget.addEventListener("mouseenter", listener); + aEnterTarget.addEventListener("mouseleave", unexpectedEvent); + if (aLeaveTarget) { + aLeaveTarget.addEventListener("mouseenter", unexpectedEvent); + aLeaveTarget.addEventListener("mouseleave", listener); + } + }); +} + +function moveMouseToInitialPosition() { + info("Mouse moves to initial position"); + return promiseNativeMouseEvent({ + type: "mousemove", + target: document.getElementById("start"), + atCenter: true, + }); +} + +add_setup(async function() { + // Wait for focus before starting tests. + await SimpleTest.promiseFocus(); + + // Wait for apz getting stable. + await waitUntilApzStable(); + + // Move mouse to initial position. + await moveMouseToInitialPosition(); +}); + +add_task(async function testMouseEnterLeave() { + let div = document.getElementById("div"); + let target = document.getElementById("target"); + let iframe = document.getElementById("frame"); + + info("Mouse moves to the div above iframe"); + let promise = waitForEnterLeaveEvents(div); + synthesizeNativeMouseEvent({ + type: "mousemove", + target: div, + atCenter: true, + }); + await promise; + + info("Mouse moves into iframe"); + promise = Promise.all([waitForEnterLeaveEvents(target, div), + waitForMessage(iframe, "mouseenter", "div"), + waitForMessage(iframe, "mouseenter", "html")]); + synthesizeNativeMouseEvent({ + type: "mousemove", + target, + atCenter: true, + }); + await promise; + + info("Mouse moves out from iframe to the div above iframe"); + promise = Promise.all([waitForEnterLeaveEvents(div, target), + waitForMessage(iframe, "mouseleave", "div"), + waitForMessage(iframe, "mouseleave", "html")]); + synthesizeNativeMouseEvent({ + type: "mousemove", + target: div, + atCenter: true, + }); + await promise; + + // Move mouse back to initial position. This is to prevent unexpected + // mouseleave event in initial steps for test-verify which runs same test + // multiple times. + await moveMouseToInitialPosition(); +}); + +add_task(async function testMouseEnterLeaveBetweenIframe() { + let target = document.getElementById("target"); + let iframe = document.getElementById("frame"); + + info("Mouse moves into the first iframe"); + let promise = Promise.all([waitForEnterLeaveEvents(target), + waitForMessage(iframe, "mouseenter", "div"), + waitForMessage(iframe, "mouseenter", "html")]); + synthesizeNativeMouseEvent({ + type: "mousemove", + target, + atCenter: true, + }); + await promise; + + let target2 = document.getElementById("target2"); + let iframe2 = document.getElementById("frame2"); + + info("Mouse moves out from the first iframe to the second iframe"); + promise = Promise.all([waitForEnterLeaveEvents(target2, target), + waitForMessage(iframe, "mouseleave", "div"), + waitForMessage(iframe, "mouseleave", "html"), + waitForMessage(iframe2, "mouseenter", "div"), + waitForMessage(iframe2, "mouseenter", "html")]); + synthesizeNativeMouseEvent({ + type: "mousemove", + target: target2, + atCenter: true, + }) + await promise; + + info("Mouse moves out from the second iframe to the first iframe"); + promise = Promise.all([waitForEnterLeaveEvents(target, target2), + waitForMessage(iframe2, "mouseleave", "div"), + waitForMessage(iframe2, "mouseleave", "html"), + waitForMessage(iframe, "mouseenter", "div"), + waitForMessage(iframe, "mouseenter", "html")]); + synthesizeNativeMouseEvent({ + type: "mousemove", + target, + atCenter: true, + }); + await promise; + + // Move mouse back to initial position. + await Promise.all([waitForLeaveEvent(target), + waitForMessage(iframe, "mouseleave", "div"), + waitForMessage(iframe, "mouseleave", "html"), + moveMouseToInitialPosition()]); +}); + +add_task(async function testMouseEnterLeaveSwitchWindow() { + let target = document.getElementById("target"); + let iframe = document.getElementById("frame"); + + info("Mouse moves into iframe"); + let promise = Promise.all([waitForEnterLeaveEvents(target), + waitForMessage(iframe, "mouseenter", "div"), + waitForMessage(iframe, "mouseenter", "html")]); + synthesizeNativeMouseEvent({ + type: "mousemove", + target, + atCenter: true, + }); + await promise; + + info("Open and switch to new window"); + promise = Promise.all([waitForLeaveEvent(target), + waitForMessage(iframe, "mouseleave", "div"), + waitForMessage(iframe, "mouseleave", "html")]); + let win = window.open("http://example.com/tests/dom/events/test/file_mouse_enterleave.html"); + // Trigger a reflow which will generate synthesized mouse move event. + win.postMessage("reflow", "*"); + await promise; + + info("Switch back to test window"); + promise = Promise.all([waitForEnterLeaveEvents(target), + waitForMessage(iframe, "mouseenter", "div"), + waitForMessage(iframe, "mouseenter", "html")]); + win.close(); + // Trigger a reflow which will generate synthesized mouse move event. + reflow(); + // Wait for apz getting stable. + await waitUntilApzStable(); + synthesizeNativeMouseEvent({ + type: "mousemove", + target, + atCenter: true, + }); + await promise; + + // Move mouse back to initial position. + await Promise.all([waitForLeaveEvent(target), + moveMouseToInitialPosition()]); +}); +</script> diff --git a/dom/events/test/test_moving_and_expanding_selection_per_page.html b/dom/events/test/test_moving_and_expanding_selection_per_page.html new file mode 100644 index 0000000000..cc4786235d --- /dev/null +++ b/dom/events/test/test_moving_and_expanding_selection_per_page.html @@ -0,0 +1,353 @@ +<!DOCTYPE html> +<html> +<head> + <title>Test for expanding selection per page</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> + +<pre id="test"> +<script class="testbody" type="text/javascript"> +SimpleTest.waitForExplicitFinish(); +addLoadEvent(() => { + open("window_empty_document.html", "_blank", "width=500,height=500"); +}); + +async function doTests(aWindow) { + // On macOS, there is no shortcut keys to extend selection per page. + // Therefore, we need to use nsISelectionController.pageMove() instead. + const kUseKeyboardEvent = !navigator.platform.includes("Mac"); + let selectionController = SpecialPowers.wrap(aWindow) + .docShell + .QueryInterface(SpecialPowers.Ci.nsIInterfaceRequestor) + .getInterface(SpecialPowers.Ci.nsISelectionDisplay) + .QueryInterface(SpecialPowers.Ci.nsISelectionController); + // On Windows, per-page selection to start or end expands selection to same + // column of first or last line. On the other platforms, it expands selection + // to start or end of first or last line. + const kSelectToStartOrEnd = !navigator.platform.includes("Win"); + + await SpecialPowers.pushPrefEnv({"set": [["general.smoothScroll", false]]}); + await SimpleTest.promiseFocus(aWindow); + + function getNodeDescription(aNode) { + function getElementDescription(aElement) { + if (aElement.getAttribute("id") !== null) { + return `${aElement.tagName.toLowerCase()}#${aElement.getAttribute("id")}`; + } + if (aElement.tagName === "BR") { + return `${getElementDescription(aElement.previousSibling)} + br`; + } + return aElement.tagName.toLowerCase(); + } + switch (aNode.nodeType) { + case aNode.TEXT_NODE: + return `text node in ${getElementDescription(aNode.parentElement)}`; + case aNode.ELEMENT_NODE: + return getElementDescription(aNode); + default: + return "unknown node"; + } + } + + function doTest(aExpandSelection) { + // Note that when neither editor has focus nor in caret mode, key navigation + // does not call nsISelectionController::PageMove(). Therefore, in such + // cases, you need to call doPageDown() and doPageUp() with setting true + // to aUseSelectionController. + function doPageDown(aUseSelectionController) { + if (kUseKeyboardEvent && !aUseSelectionController) { + synthesizeKey("KEY_PageDown", {shiftKey: aExpandSelection}, aWindow); + } else { + selectionController.pageMove(true, aExpandSelection); + } + } + + function doPageUp(aUseSelectionController) { + if (kUseKeyboardEvent && !aUseSelectionController) { + synthesizeKey("KEY_PageUp", {shiftKey: aExpandSelection}, aWindow); + } else { + selectionController.pageMove(false, aExpandSelection); + } + } + + let doc = aWindow.document; + let body = doc.body; + let selection = doc.getSelection(); + let container; + + body.innerHTML = '<span id="s1">first line</span><br>' + + '<span id="s2">second line</span><br>' + + '<span id="s3">last line</span>'; + container = doc.documentElement; + + let description = `${aExpandSelection ? "Expanding selection" : "Moving caret"} to forward in non-scrollable body: `; + is(container.scrollTop, 0, description + "scrollTop should be 0 at initialization"); + selection.collapse(doc.getElementById("s1").firstChild, 3); + doPageDown(!aExpandSelection); + is(container.scrollTop, 0, description + "this test shouldn't create scrollable document"); + let range = selection.getRangeAt(0); + if (aExpandSelection) { + is(range.startContainer, doc.getElementById("s1").firstChild, + `${description} selection should be expanded from the first line (got: ${getNodeDescription(range.startContainer)})`); + is(range.startOffset, 3, + `${description} selection should be expanded from the first line's 3rd insertion point`); + } else { + ok(range.collapsed, `${description} selection should be collapsed`); + } + is(range.endContainer, doc.getElementById("s3").firstChild, + `${description} selection should be expanded into the last line (got: ${getNodeDescription(range.endContainer)})`); + if (kSelectToStartOrEnd) { + is(range.endOffset, range.endContainer.length, + `${description} selection should be expanded to end of the last line`); + } else { + isfuzzy(range.endOffset, 3, 2, + `${description} selection should be expanded to around the last line's 3rd insertion point`); + } + + description = `${aExpandSelection ? "Expanding selection" : "Moving caret"} to backward in non-scrollable body: `; + selection.collapse(doc.getElementById("s3").firstChild, 3); + doPageUp(!aExpandSelection); + is(container.scrollTop, 0, description + "this test shouldn't create scrollable document"); + range = selection.getRangeAt(0); + is(range.startContainer, doc.getElementById("s1").firstChild, + `${description} selection should be expanded into the first line (got: ${getNodeDescription(range.startContainer)})`); + if (kSelectToStartOrEnd) { + is(range.startOffset, 0, + `${description} selection should be expanded to start of the first line`); + } else { + isfuzzy(range.startOffset, 3, 2, + `${description} selection should be expanded to around the first line's 3rd insertion point`); + } + if (aExpandSelection) { + is(range.endContainer, doc.getElementById("s3").firstChild, + `${description} selection should be expanded from the last line (got: ${getNodeDescription(range.endContainer)})`); + is(range.endOffset, 3, + `${description} selection should be expanded from the last line's 3rd insertion point`); + } else { + ok(range.collapsed, `${description} selection should be collapsed`); + } + + body.innerHTML = '<span id="s1">first line in the body</span>' + + '<div id="d1" style="height: 2em; line-height: 1em; overflow: auto;">' + + '<span id="s2">first line</span><br>' + + '<span id="s3">second line</span><br>' + + '<span id="s4">third line</span><br>' + + '<span id="s5">last line</span>' + + "</div>" + + '<span id="s6">last line in the body</span>'; + container = doc.getElementById("d1"); + + description = `${aExpandSelection ? "Expanding selection" : "Moving caret"} to forward in scrollable area in the body: `; + is(container.scrollTop, 0, description + "scrollTop should be 0 at initialization"); + selection.collapse(doc.getElementById("s2").firstChild, 3); + doPageDown(!aExpandSelection); + isnot(container.scrollTop, 0, description + "should be scrolled down"); + range = selection.getRangeAt(0); + if (aExpandSelection) { + is(range.startContainer, doc.getElementById("s2").firstChild, + `${description} selection should be expanded from the first line (got: ${getNodeDescription(range.startContainer)})`); + is(range.startOffset, 3, + `${description} selection should be expanded from the first line's 3rd insertion point`); + } else { + ok(range.collapsed, `${description} selection should be collapsed`); + } + is(range.endContainer, doc.getElementById("s4").firstChild, + `${description} selection should be expanded into the 3rd line (got: ${getNodeDescription(range.endContainer)})`); + isfuzzy(range.endOffset, 3, 2, + `${description} selection should be expanded to around the 3rd line's 3rd insertion point`); + + description = `${aExpandSelection ? "Expanding selection" : "Moving caret"} to backward in scrollable area in the body: `; + selection.collapse(doc.getElementById("s4").firstChild, 3); + let previousScrollTop = container.scrollTop; + doPageUp(!aExpandSelection); + ok(container.scrollTop < previousScrollTop, description + "should be scrolled up"); + range = selection.getRangeAt(0); + is(range.startContainer, doc.getElementById("s2").firstChild, + `${description} selection should be expanded into the first line (got: ${getNodeDescription(range.startContainer)})`); + isfuzzy(range.startOffset, 3, 2, + `${description} selection should be expanded to around the first line's 3rd insertion point`); + if (aExpandSelection) { + is(range.endContainer, doc.getElementById("s4").firstChild, + `${description} selection should be expanded from the 3rd line (got: ${getNodeDescription(range.endContainer)})`); + is(range.endOffset, 3, + `${description} selection should be expanded from the 3rd line's 3rd insertion point`); + } else { + ok(range.collapsed, `${description} selection should be collapsed`); + } + + body.innerHTML = '<span id="s1">first line in the body</span>' + + '<div id="d1" contenteditable style="height: 2em; line-height: 1em; overflow: auto;">' + + '<span id="s2">first line</span><br>' + + '<span id="s3">second line</span><br>' + + '<span id="s4">third line</span><br>' + + '<span id="s5">last line</span>' + + "</div>" + + '<span id="s6">last line in the body</span>'; + container = doc.getElementById("d1"); + + description = `${aExpandSelection ? "Expanding selection" : "Moving caret"} to forward in scrollable editable div in the body: `; + is(container.scrollTop, 0, description + "scrollTop should be 0 at initialization"); + selection.collapse(doc.getElementById("s2").firstChild, 3); + doPageDown(); + isnot(container.scrollTop, 0, description + "should be scrolled down"); + range = selection.getRangeAt(0); + if (aExpandSelection) { + is(range.startContainer, doc.getElementById("s2").firstChild, + `${description} selection should be expanded from the first line (got: ${getNodeDescription(range.startContainer)})`); + is(range.startOffset, 3, + `${description} selection should be expanded from the first line's 3rd insertion point`); + } else { + ok(range.collapsed, `${description} selection should be collapsed`); + } + is(range.endContainer, doc.getElementById("s4").firstChild, + `${description} selection should be expanded into the 3rd line (got: ${getNodeDescription(range.endContainer)})`); + isfuzzy(range.endOffset, 3, 2, + `${description} selection should be expanded to around the 3rd line's 3rd insertion point`); + + description = `${aExpandSelection ? "Expanding selection" : "Moving caret"} to backward in scrollable editable div in the body: `; + selection.collapse(doc.getElementById("s4").firstChild, 3); + previousScrollTop = container.scrollTop; + doPageUp(); + ok(container.scrollTop < previousScrollTop, description + "should be scrolled up"); + range = selection.getRangeAt(0); + is(range.startContainer, doc.getElementById("s2").firstChild, + `${description} selection should be expanded into the first line (got: ${getNodeDescription(range.startContainer)})`); + isfuzzy(range.startOffset, 3, 2, + `${description} selection should be expanded to around the first line's 3rd insertion point`); + if (aExpandSelection) { + is(range.endContainer, doc.getElementById("s4").firstChild, + `${description} selection should be expanded from the 3rd line (got: ${getNodeDescription(range.endContainer)})`); + is(range.endOffset, 3, + `${description} selection should be expanded from the 3rd line's 3rd insertion point`); + } else { + ok(range.collapsed, `${description} selection should be collapsed`); + } + + body.innerHTML = '<span id="s1">first line in the body</span>' + + '<div id="d1" contenteditable>' + + '<span id="s2">first line</span><br>' + + '<span id="s3">second line</span><br>' + + '<span id="s4">third line</span><br>' + + '<span id="s5">last line</span>' + + "</div>" + + '<span id="s6">last line in the body</span>'; + container = doc.getElementById("d1"); + + description = `${aExpandSelection ? "Expanding selection" : "Moving caret"} to forward in non-scrollable editable div in the body: `; + is(container.scrollTop, 0, description + "scrollTop should be 0 at initialization"); + selection.collapse(doc.getElementById("s2").firstChild, 3); + doPageDown(); + is(container.scrollTop, 0, description + "editable div shouldn't be scrollable"); + range = selection.getRangeAt(0); + if (aExpandSelection) { + is(range.startContainer, doc.getElementById("s2").firstChild, + `${description} selection should be expanded from the first line (got: ${getNodeDescription(range.startContainer)})`); + is(range.startOffset, 3, + `${description} selection should be expanded from the first line's 3rd insertion point`); + } else { + ok(range.collapsed, `${description} selection should be collapsed`); + } + is(range.endContainer, doc.getElementById("s5").firstChild, + `${description} selection should be expanded into the last line (got: ${getNodeDescription(range.endContainer)})`); + if (kSelectToStartOrEnd) { + is(range.endOffset, range.endContainer.length, + `${description} selection should be expanded to end of the last line`); + } else { + isfuzzy(range.endOffset, 3, 2, + `${description} selection should be expanded to around the last line's 3rd insertion point`); + } + + description = `${aExpandSelection ? "Expanding selection" : "Moving caret"} to backward in non-scrollable editable div in the body: `; + selection.collapse(doc.getElementById("s5").firstChild, 3); + doPageUp(); + is(container.scrollTop, 0, description + "editable div shouldn't be scrollable"); + range = selection.getRangeAt(0); + is(range.startContainer, doc.getElementById("s2").firstChild, + `${description} selection should be expanded into the first line (got: ${getNodeDescription(range.startContainer)})`); + if (kSelectToStartOrEnd) { + is(range.startOffset, 0, + `${description} selection should be expanded to start of the first line`); + } else { + isfuzzy(range.startOffset, 3, 2, + `${description} selection should be expanded to around the first line's 3rd insertion point`); + } + if (aExpandSelection) { + is(range.endContainer, doc.getElementById("s5").firstChild, + `${description} selection should be expanded from the last line (got: ${getNodeDescription(range.endContainer)})`); + is(range.endOffset, 3, + `${description} selection should be expanded from the last line's 3rd insertion point`); + } else { + ok(range.collapsed, `${description} selection should be collapsed`); + } + + body.innerHTML = '<span id="s1">first line in the body</span>' + + '<div id="d1" contenteditable>' + + '<span id="s2">first editable line</span><br>' + + '<div id="d2" style="height: 3em; line-height: 1em; overflow: auto;">' + + '<span id="s3">first line</span><br>' + + '<span id="s4">second line</span>' + + "</div>" + + '<span id="s5">last editable line</span>' + + "</div>" + + '<span id="s6">last line in the body</span>'; + container = doc.getElementById("d2"); + + description = `${aExpandSelection ? "Expanding selection" : "Moving caret"} to forward in scrollable div (but not scrollable along y-axis) in the editable div: `; + is(container.scrollTop, 0, description + "scrollTop should be 0 at initialization"); + selection.collapse(doc.getElementById("s3").firstChild, 3); + doPageDown(); + is(container.scrollTop, 0, description + "scrollable div in the editable div (but not scrollable along y-axis) shouldn't be scrollable"); + range = selection.getRangeAt(0); + if (aExpandSelection) { + is(range.startContainer, doc.getElementById("s3").firstChild, + `${description} selection should be expanded from the first line (got: ${getNodeDescription(range.startContainer)})`); + is(range.startOffset, 3, + `${description} selection should be expanded from the first line's 3rd insertion point`); + } else { + ok(range.collapsed, `${description} selection should be collapsed`); + } + is(range.endContainer, doc.getElementById("s5").firstChild, + `${description} selection should be expanded into the last editable line (got: ${getNodeDescription(range.endContainer)})`); + if (kSelectToStartOrEnd) { + is(range.endOffset, range.endContainer.length, + `${description} selection should be expanded to end of the last editable line`); + } else { + isfuzzy(range.endOffset, 3, 2, + `${description} selection should be expanded to around the last editable line's 3rd insertion point`); + } + + description = `${aExpandSelection ? "Expanding selection" : "Moving caret"} to backward in scrollable div (but not scrollable along y-axis) in the editable div: `; + selection.collapse(doc.getElementById("s4").firstChild, 3); + doPageUp(); + is(container.scrollTop, 0, description + "scrollable div (but not scrollable along y-axis) in the editable div shouldn't be scrollable"); + range = selection.getRangeAt(0); + is(range.startContainer, doc.getElementById("s2").firstChild, + `${description} selection should be expanded into the first editable line (got: ${getNodeDescription(range.startContainer)})`); + if (kSelectToStartOrEnd) { + is(range.startOffset, 0, + `${description} selection should be expanded to start of the first editable line`); + } else { + isfuzzy(range.startOffset, 3, 2, + `${description} selection should be expanded to around the first editable line's 3rd insertion point`); + } + if (aExpandSelection) { + is(range.endContainer, doc.getElementById("s4").firstChild, + `${description} selection should be expanded from the last line (got: ${getNodeDescription(range.endContainer)})`); + is(range.endOffset, 3, + `${description} selection should be expanded from the last line's 3rd insertion point`); + } else { + ok(range.collapsed, `${description} selection should be collapsed`); + } + } + + doTest(false); + doTest(true); + + aWindow.close(); + SimpleTest.finish(); +} +</script> +</html> diff --git a/dom/events/test/test_moz_mouse_pixel_scroll_event.html b/dom/events/test/test_moz_mouse_pixel_scroll_event.html new file mode 100644 index 0000000000..e6baf34d05 --- /dev/null +++ b/dom/events/test/test_moz_mouse_pixel_scroll_event.html @@ -0,0 +1,481 @@ +<!DOCTYPE HTML> +<html id="root"> +<head> + <title>Test for MozMousePixelScroll events</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script src="/tests/SimpleTest/paint_listener.js"></script> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"> + <style> + html, body { margin: 0 } + + .scrollable { + overflow: auto; + line-height: 1; + margin: 15px; + box-sizing: border-box; + } + .scrollable > div { + width: 1000px; + height: 1000px; + font-size: 1000px; + line-height: 1; + } + /* Ensure viewport is scrollable */ + .viewport-padding { height: 200vh; width: 200vh; } + </style> +</head> +<body> +<p id="display"></p> +<div id="Scrollable128" class="scrollable" style="font-size: 128px; width: 100px; height: 100px;"> + <div> + <div id="Scrollable96" class="scrollable" style="font-size: 96px; width: 150px; height: 150px;"> + <div> + <div id="Scrollable64" class="scrollable" style="font-size: 64px; width: 200px; height: 200px;"> + <div> + </div> + </div> + </div> + </div> + </div> +</div> +<div id="Scrollable32" class="scrollable" style="font-size: 32px; width: 50px; height: 50px;"> + <div> + </div> +</div> +<div class="viewport-padding"></div> +<div id="content" style="display: none"></div> +<pre id="test"> +<script> + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(startTest, window); + +var gScrollable128 = document.getElementById("Scrollable128"); +var gScrollable96 = document.getElementById("Scrollable96"); +var gScrollable64 = document.getElementById("Scrollable64"); +var gScrollable32 = document.getElementById("Scrollable32"); +var gRoot = document.documentElement; + +// TODO(emilio): page scrolling seems to interact weirdly with +// Android (probably due to the visual viewport? but I can't repro +// on the emulator). It's probably not a big deal though since you +// can't usually do page scrolling on Android, and chances are +// actual behavior is correct and we just need to tweak the test. +const kIsAndroid = navigator.userAgent.includes("Android"); + +function* prepareScrollUnits() +{ + var result = -1; + function handler(aEvent) + { + result = aEvent.detail; + aEvent.preventDefault(); + setTimeout(runTest, 0); + } + window.addEventListener("MozMousePixelScroll", handler, { capture: true, passive: false }); + + yield waitForAllPaints(function () { setTimeout(runTest, 0); }); + + yield synthesizeWheel(gScrollable128, 10, 10, + { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaY: 1.0, lineOrPageDeltaY: 1 }); + gScrollable128.wheelLineHeight = result; + ok(result > 96 && result < 200, "prepareScrollUnits: gScrollable128.wheelLineHeight may be illegal value, got " + result); + + result = -1; + yield synthesizeWheel(gScrollable96, 10, 10, + { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaY: 1.0, lineOrPageDeltaY: 1 }); + gScrollable96.wheelLineHeight = result; + ok(result > 64 && result < gScrollable128.wheelLineHeight, "prepareScrollUnits: gScrollable96.wheelLineHeight may be illegal value, got " + result); + + result = -1; + yield synthesizeWheel(gScrollable64, 10, 10, + { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaY: 1.0, lineOrPageDeltaY: 1 }); + gScrollable64.wheelLineHeight = result; + ok(result > 32 && result < gScrollable96.wheelLineHeight, "prepareScrollUnits: gScrollable64.wheelLineHeight may be illegal value, got " + result); + + result = -1; + yield synthesizeWheel(gScrollable32, 10, 10, + { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaY: 1.0, lineOrPageDeltaY: 1 }); + gScrollable32.wheelLineHeight = result; + ok(result > 16 && result < gScrollable64.wheelLineHeight, "prepareScrollUnits: gScrollable32.wheelLineHeight may be illegal value, got " + result); + + result = -1; + yield synthesizeWheel(gRoot, 10, 10, + { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaY: 1.0, lineOrPageDeltaY: 1 }); + gRoot.wheelLineHeight = result; + ok(result > 10 && result < gScrollable32.wheelLineHeight, "prepareScrollUnits: gRoot.wheelLineHeight may be illegal value, got " + result); + + result = -1; + yield synthesizeWheel(gScrollable128, 10, 10, + { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: 1.0, lineOrPageDeltaX: 1 }); + gScrollable128.wheelHorizontalLine = result; + ok(result > 50 && result < 200, "prepareScrollUnits: gScrollable128.wheelHorizontalLine may be illegal value, got " + result); + + result = -1; + yield synthesizeWheel(gScrollable96, 10, 10, + { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: 1.0, lineOrPageDeltaX: 1 }); + gScrollable96.wheelHorizontalLine = result; + ok(result > 30 && result < gScrollable128.wheelHorizontalLine, "prepareScrollUnits: gScrollable96.wheelHorizontalLine may be illegal value, got " + result); + + result = -1; + yield synthesizeWheel(gScrollable64, 10, 10, + { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: 1.0, lineOrPageDeltaX: 1 }); + gScrollable64.wheelHorizontalLine = result; + ok(result > 20 && result < gScrollable96.wheelHorizontalLine, "prepareScrollUnits: gScrollable64.wheelHorizontalLine may be illegal value, got " + result); + + result = -1; + yield synthesizeWheel(gScrollable32, 10, 10, + { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: 1.0, lineOrPageDeltaX: 1 }); + gScrollable32.wheelHorizontalLine = result; + ok(result > 12 && result < gScrollable64.wheelHorizontalLine, "prepareScrollUnits: gScrollable32.wheelHorizontalLine may be illegal value, got " + result); + + result = -1; + yield synthesizeWheel(gRoot, 10, 10, + { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: 1.0, lineOrPageDeltaX: 1 }); + gRoot.wheelHorizontalLine = result; + ok(result > 5 && result < gScrollable32.wheelHorizontalLine, "prepareScrollUnits: gRoot.wheelHorizontalLine may be illegal value, got " + result); + + result = -1; + yield synthesizeWheel(gScrollable128, 10, 10, + { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaY: 1.0, lineOrPageDeltaY: 1 }); + gScrollable128.wheelPageHeight = result; + ok(result >= (100 - gScrollable128.wheelLineHeight * 2) && result <= 100, + "prepareScrollUnits: gScrollable128.wheelPageHeight is strange value, got " + result); + + result = -1; + yield synthesizeWheel(gScrollable96, 10, 10, + { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaY: 1.0, lineOrPageDeltaY: 1 }); + gScrollable96.wheelPageHeight = result; + ok(result >= (150 - gScrollable96.wheelLineHeight * 2) && result <= 150, + "prepareScrollUnits: gScrollable96.wheelPageHeight is strange value, got " + result); + + result = -1; + yield synthesizeWheel(gScrollable64, 10, 10, + { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaY: 1.0, lineOrPageDeltaY: 1 }); + gScrollable64.wheelPageHeight = result; + ok(result >= (200 - gScrollable64.wheelLineHeight * 2) && result <= 200, + "prepareScrollUnits: gScrollable64.wheelPageHeight is strange value, got " + result); + + result = -1; + yield synthesizeWheel(gScrollable32, 10, 10, + { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaY: 1.0, lineOrPageDeltaY: 1 }); + gScrollable32.wheelPageHeight = result; + ok(result >= (50 - gScrollable32.wheelLineHeight * 2) && result <= 50, + "prepareScrollUnits: gScrollable32.wheelPageHeight is strange value, got " + result); + + result = -1; + yield synthesizeWheel(gRoot, 10, 10, + { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaY: 1.0, lineOrPageDeltaY: 1 }); + gRoot.wheelPageHeight = result; + if (!kIsAndroid) { + ok(window.innerHeight - result < 100 && window.innerHeight - result > 0, + "prepareScrollUnits: gRoot.wheelPageHeight is strange value, got " + result); + } + + result = -1; + yield synthesizeWheel(gScrollable128, 10, 10, + { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: 1.0, lineOrPageDeltaX: 1 }); + gScrollable128.wheelPageWidth = result; + ok(result >= (100 - gScrollable128.wheelLineHeight * 2) && result <= 100, + "prepareScrollUnits: gScrollable128.wheelPageWidth is strange value, got " + result); + + result = -1; + yield synthesizeWheel(gScrollable96, 10, 10, + { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: 1.0, lineOrPageDeltaX: 1 }); + gScrollable96.wheelPageWidth = result; + ok(result >= (150 - gScrollable96.wheelLineHeight * 2) && result <= 150, + "prepareScrollUnits: gScrollable96.wheelPageWidth is strange value, got " + result); + + result = -1; + yield synthesizeWheel(gScrollable64, 10, 10, + { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: 1.0, lineOrPageDeltaX: 1 }); + gScrollable64.wheelPageWidth = result; + ok(result >= (200 - gScrollable64.wheelLineHeight * 2) && result <= 200, + "prepareScrollUnits: gScrollable64.wheelPageWidth is strange value, got " + result); + + result = -1; + yield synthesizeWheel(gScrollable32, 10, 10, + { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: 1.0, lineOrPageDeltaX: 1 }); + gScrollable32.wheelPageWidth = result; + ok(result >= (50 - gScrollable32.wheelLineHeight * 2) && result <= 50, + "prepareScrollUnits: gScrollable32.wheelPageWidth is strange value, got " + result); + + result = -1; + yield synthesizeWheel(gRoot, 10, 10, + { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: 1.0, lineOrPageDeltaX: 1 }); + gRoot.wheelPageWidth = result; + if (!kIsAndroid) { + ok(window.innerWidth - result < 100 && window.innerWidth - result > 0, + "prepareScrollUnits: gRoot.wheelPageWidth is strange value, got " + result); + } + + window.removeEventListener("MozMousePixelScroll", handler, true); +} + +function* doTests() +{ + let tests = []; + + { + const kAllScrollables = [gRoot, gScrollable128, gScrollable96, gScrollable64, gScrollable32]; + function resetAllScrollables() { + for (let scrollable of kAllScrollables) { + scrollable.style.overflow = ""; + scrollable.scrollLeft = 0; + scrollable.scrollTop = 0; + } + } + + function closestScrollable(s) { + if (s == gScrollable32 || s == gRoot) { + return gRoot; + } + let closest = kAllScrollables[kAllScrollables.indexOf(s) - 1]; + ok(closest, `Should find a closest scrollable for ${s.id}`); + return closest; + } + + const kAllScrollDirections = [ + { + direction: "both", + x: 1, + y: 1, + }, + { + direction: "horizontal", + x: 1, + y: 0, + }, + { + direction: "vertical", + x: 0, + y: 1, + }, + ]; + + for (let deltaMode of [WheelEvent.DOM_DELTA_LINE, WheelEvent.DOM_DELTA_PAGE]) { + if (deltaMode == WheelEvent.DOM_DELTA_PAGE && kIsAndroid) { + // See comment around kIsAndroid. + continue; + } + + for (let scrollable of kAllScrollables) { + for (let { x, y, direction } of kAllScrollDirections) { + tests.push({ + description: `Should be computed from nearest scrollable element (${scrollable.id}, ${deltaMode}, ${direction})`, + target: scrollable, + event: { + deltaMode, + deltaX: x, + deltaY: y, + lineOrPageDeltaX: x, + lineOrPageDeltaY: y, + }, + prepare () { + resetAllScrollables(); + }, + cleanup () { + }, + expected: { + x: x ? scrollable : null, + y: y ? scrollable : null, + } + }); + + tests.push({ + description: `Should be computed from actual scroll target or root if not scrollable in the direction (${scrollable.id}, ${deltaMode}, ${direction})`, + target: scrollable, + event: { + deltaMode, + deltaX: -x, + deltaY: -y, + lineOrPageDeltaX: -x, + lineOrPageDeltaY: -y + }, + prepare () { + resetAllScrollables(); + }, + cleanup () { + }, + expected: { + x: x ? gRoot : null, + y: y ? gRoot : null, + } + }); + + // Root's page delta depends on whether there's an horizontal scrollbar + // or not, so this test on the root would be off by a few pixels. + if (scrollable != gRoot || deltaMode != WheelEvent.DOM_DELTA_PAGE) { + tests.push({ + description: `Should be computed from actual scroll target or root if scrollable in the direction but overflow: hidden (${scrollable.id}, ${deltaMode}, ${direction})`, + target: scrollable, + event: { + deltaMode, + deltaX: x, + deltaY: y, + lineOrPageDeltaX: x, + lineOrPageDeltaY: y, + }, + prepare () { + resetAllScrollables(); + scrollable.style.overflow = "hidden"; + }, + cleanup () { + scrollable.style.overflow = ""; + }, + expected: { + x: x ? closestScrollable(scrollable) : null, + y: y ? closestScrollable(scrollable) : null, + } + }); + } + } + } + } + } + + var currentTest, description, firedX, firedY; + var expectedHandlerCalls; + + function handler(aEvent) + { + aEvent.preventDefault(); + + if (aEvent.axis != MouseScrollEvent.HORIZONTAL_AXIS && + aEvent.axis != MouseScrollEvent.VERTICAL_AXIS) { + ok(false, + description + "The event had invalid axis (" + aEvent.axis + ")"); + if (--expectedHandlerCalls == 0) { + setTimeout(runTest, 0); + } + return; + } + + var isHorizontal = (aEvent.axis == MouseScrollEvent.HORIZONTAL_AXIS); + if ((isHorizontal && !currentTest.expected.x) || + (!isHorizontal && !currentTest.expected.y)) { + ok(false, + description + "The event fired unexpectedly (" + + (isHorizontal ? "Horizontal" : "Vertical") + ")"); + if (--expectedHandlerCalls == 0) { + setTimeout(runTest, 0); + } + return; + } + + if (isHorizontal) { + firedX = true; + } else { + firedY = true; + } + + var expectedDetail = + (currentTest.event.deltaMode == WheelEvent.DOM_DELTA_LINE) ? + (isHorizontal ? currentTest.expected.x.wheelHorizontalLine : + currentTest.expected.y.wheelLineHeight) : + (isHorizontal ? currentTest.expected.x.wheelPageWidth : + currentTest.expected.y.wheelPageHeight); + is(Math.abs(aEvent.detail), expectedDetail, + description + ((isHorizontal) ? "horizontal" : "vertical") + " event detail is wrong"); + + if (--expectedHandlerCalls == 0) { + setTimeout(runTest, 0); + } + } + + window.addEventListener("MozMousePixelScroll", handler, { capture: true, passive: false }); + + for (var i = 0; i < tests.length; i++) { + currentTest = tests[i]; + description = "doTests, " + currentTest.description + " (deltaMode: " + + (currentTest.event.deltaMode == WheelEvent.DOM_DELTA_LINE ? + "DOM_DELTA_LINE" : "DOM_DELTA_PAGE") + + ", deltaX: " + currentTest.event.deltaX + + ", deltaY: " + currentTest.event.deltaY + "): "; + currentTest.prepare(); + firedX = firedY = false; + expectedHandlerCalls = (currentTest.expected.x ? 1 : 0) + + (currentTest.expected.y ? 1 : 0); + yield synthesizeWheel(currentTest.target, 10, 10, currentTest.event); + if (currentTest.expected.x) { + ok(firedX, description + "Horizontal MozMousePixelScroll event wasn't fired"); + } + if (currentTest.expected.y) { + ok(firedY, description + "Vertical MozMousePixelScroll event wasn't fired"); + } + currentTest.cleanup(); + } + + window.removeEventListener("MozMousePixelScroll", handler, true); +} + +function* testBody() +{ + yield* prepareScrollUnits(); + yield* doTests(); +} + +var gTestContinuation = null; + +function runTest() +{ + if (!gTestContinuation) { + gTestContinuation = testBody(); + } + var ret = gTestContinuation.next(); + if (ret.done) { + SimpleTest.finish(); + } +} + +function startTest() { + SpecialPowers.pushPrefEnv({"set": [["mousewheel.default.delta_multiplier_x", 100], + ["mousewheel.default.delta_multiplier_y", 100], + ["mousewheel.default.delta_multiplier_z", 100], + ["mousewheel.with_alt.delta_multiplier_x", 100], + ["mousewheel.with_alt.delta_multiplier_y", 100], + ["mousewheel.with_alt.delta_multiplier_z", 100], + ["mousewheel.with_control.delta_multiplier_x", 100], + ["mousewheel.with_control.delta_multiplier_y", 100], + ["mousewheel.with_control.delta_multiplier_z", 100], + ["mousewheel.with_meta.delta_multiplier_x", 100], + ["mousewheel.with_meta.delta_multiplier_y", 100], + ["mousewheel.with_meta.delta_multiplier_z", 100], + ["mousewheel.with_shift.delta_multiplier_x", 100], + ["mousewheel.with_shift.delta_multiplier_y", 100], + ["mousewheel.with_shift.delta_multiplier_z", 100], + ["mousewheel.with_win.delta_multiplier_x", 100], + ["mousewheel.with_win.delta_multiplier_y", 100], + ["mousewheel.with_win.delta_multiplier_z", 100], + // If APZ is enabled we should ensure the preventDefault calls work even + // if the test is running slowly. + ["apz.content_response_timeout", 2000], + ]}, runTest); +} + +</script> +</pre> +</body> +</html> diff --git a/dom/events/test/test_offsetxy.html b/dom/events/test/test_offsetxy.html new file mode 100644 index 0000000000..693683d1b0 --- /dev/null +++ b/dom/events/test/test_offsetxy.html @@ -0,0 +1,98 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for DOM MouseEvent offsetX/Y</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<div id="d" style="position:absolute; top:100px; left:100px; width:100px; border:5px dotted black; height:100px"></div> +<div id="d2" style="position:absolute; top:100px; left:100px; width:100px; border:5px dotted black; height:100px; transform:translateX(100px)"></div> +<div id="d3" style="display:none; position:absolute; top:100px; left:100px; width:100px; border:5px dotted black; height:100px"></div> +<div id="d4" style="transform:scale(0); position:absolute; top:100px; left:100px; width:100px; border:5px dotted black; height:100px"></div> + +<pre id="test"> +<script type="application/javascript"> + +var offsetX = -1, offsetY = -1; +var ev = new MouseEvent("click", {clientX:110, clientY:110}); +is(ev.offsetX, 110); +is(ev.offsetY, 110); +is(ev.offsetX, ev.pageX); +is(ev.offsetY, ev.pageY); +d.addEventListener("click", function (event) { + is(ev, event, "Event objects must match"); + offsetX = event.offsetX; + offsetY = event.offsetY; +}); +d.dispatchEvent(ev); +is(offsetX, 5); +is(offsetY, 5); +is(ev.offsetX, 5); +is(ev.offsetY, 5); + +var ev2 = new MouseEvent("click", {clientX:220, clientY:130}); +is(ev2.offsetX, 220); +is(ev2.offsetY, 130); +is(ev2.offsetX, ev2.pageX); +is(ev2.offsetY, ev2.pageY); +d2.addEventListener("click", function (event) { + is(ev2, event, "Event objects must match"); + offsetX = event.offsetX; + offsetY = event.offsetY; +}); +d2.dispatchEvent(ev2); +is(offsetX, 15); +is(offsetY, 25); +is(ev2.offsetX, 15); +is(ev2.offsetY, 25); + +var ev3 = new MouseEvent("click", {clientX:110, clientY:110}); +is(ev3.offsetX, 110); +is(ev3.offsetY, 110); +is(ev3.offsetX, ev3.pageX); +is(ev3.offsetY, ev3.pageY); +d3.addEventListener("click", function (event) { + is(ev3, event, "Event objects must match"); + offsetX = event.offsetX; + offsetY = event.offsetY; +}); +d3.dispatchEvent(ev3); +is(offsetX, 0); +is(offsetY, 0); +is(ev3.offsetX, 0); +is(ev3.offsetY, 0); + +var ev4 = new MouseEvent("click", {clientX:110, clientY:110}); +is(ev4.offsetX, 110); +is(ev4.offsetY, 110); +is(ev4.offsetX, ev4.pageX); +is(ev4.offsetY, ev4.pageY); +d4.addEventListener("click", function (event) { + is(ev4, event, "Event objects must match"); + offsetX = event.offsetX; + offsetY = event.offsetY; +}); +d4.dispatchEvent(ev4); +is(offsetX, 0); +is(offsetY, 0); +is(ev4.offsetX, 0); +is(ev4.offsetY, 0); + +// Now redispatch ev4 to "d" to make sure that its offsetX gets updated +// relative to the new target. Have to set "ev" to "ev4", because the listener +// on "d" expects to see "ev" as the event. +ev = ev4; +d.dispatchEvent(ev4); +is(offsetX, 5); +is(offsetY, 5); +is(ev.offsetX, 5); +is(ev.offsetY, 5); +</script> +</pre> +</body> +</html> diff --git a/dom/events/test/test_onerror_handler_args.html b/dom/events/test/test_onerror_handler_args.html new file mode 100644 index 0000000000..cf925736d8 --- /dev/null +++ b/dom/events/test/test_onerror_handler_args.html @@ -0,0 +1,36 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1007790 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1007790</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript"> + + /** Test for Bug 1007790 **/ + SimpleTest.waitForExplicitFinish(); + addLoadEvent(function() { + is(frames[0].onerror.toString(), + "function onerror(event, source, lineno, colno, error) {\n\n}", + "Should have the right arguments for onerror on window"); + is($("content").onerror.toString(), + "function onerror(event) {\n\n}", + "Should have the right arguments for onerror on element"); + SimpleTest.finish(); + }); + + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1007790">Mozilla Bug 1007790</a> +<p id="display"></p> +<div id="content" style="display: none" onerror=""> + <iframe srcdoc="<body onerror=''>"></iframe> +</div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/dom/events/test/test_passive_listeners.html b/dom/events/test/test_passive_listeners.html new file mode 100644 index 0000000000..dd132fc6bc --- /dev/null +++ b/dom/events/test/test_passive_listeners.html @@ -0,0 +1,118 @@ +<html> +<head> + <title>Tests for passive event listeners</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + +<body> +<p id="display"></p> +<div id="dummy"> +</div> + +<script> +var listenerHitCount; +var doPreventDefault; + +function listener(e) +{ + listenerHitCount++; + if (doPreventDefault) { + // When this function is registered as a passive listener, this + // call should be a no-op and might report a console warning. + e.preventDefault(); + } +} + +function listener2(e) +{ + if (doPreventDefault) { + e.preventDefault(); + } +} + +var elem = document.getElementById('dummy'); + +function doTest(description, passiveArg) +{ + listenerHitCount = 0; + + elem.addEventListener('test', listener, { passive: passiveArg }); + + // Test with a cancelable event + var e1 = new Event('test', { cancelable: true }); + elem.dispatchEvent(e1); + is(listenerHitCount, 1, description + ' | hit count'); + var expectedDefaultPrevented = (doPreventDefault && !passiveArg); + is(e1.defaultPrevented, expectedDefaultPrevented, description + ' | default prevented'); + + // Test with a non-cancelable event + var e2 = new Event('test', { cancelable: false }); + elem.dispatchEvent(e2); + is(listenerHitCount, 2, description + ' | hit count after non-cancelable event'); + is(e2.defaultPrevented, false, description + ' | default prevented on non-cancelable event'); + + // Test combining passive-enabled and "traditional" listeners + elem.addEventListener('test', listener2); + var e3 = new Event('test', { cancelable: true }); + elem.dispatchEvent(e3); + is(listenerHitCount, 3, description + ' | hit count with second listener'); + is(e3.defaultPrevented, doPreventDefault, description + ' | default prevented with second listener'); + elem.removeEventListener('test', listener2); + + elem.removeEventListener('test', listener); +} + +function testAddListenerKey(passiveListenerFirst) +{ + listenerHitCount = 0; + doPreventDefault = true; + + elem.addEventListener('test', listener, { capture: false, passive: passiveListenerFirst }); + // This second listener should not be registered, because the "key" of + // { type, callback, capture } is the same, even though the 'passive' flag + // is different. + elem.addEventListener('test', listener, { capture: false, passive: !passiveListenerFirst }); + + var e1 = new Event('test', { cancelable: true }); + elem.dispatchEvent(e1); + + is(listenerHitCount, 1, 'Duplicate addEventListener was correctly ignored'); + is(e1.defaultPrevented, !passiveListenerFirst, 'Prevent-default result based on first registered listener'); + + // Even though passive is the opposite of the first addEventListener call, it + // should remove the listener registered above. + elem.removeEventListener('test', listener, { capture: false, passive: !passiveListenerFirst }); + + var e2 = new Event('test', { cancelable: true }); + elem.dispatchEvent(e2); + + is(listenerHitCount, 1, 'non-passive listener was correctly unregistered'); + is(e2.defaultPrevented, false, 'no listener was registered to preventDefault this event'); +} + +function test() +{ + doPreventDefault = false; + + doTest('base case', undefined); + doTest('non-passive listener', false); + doTest('passive listener', true); + + doPreventDefault = true; + + doTest('base case', undefined); + doTest('non-passive listener', false); + doTest('passive listener', true); + + testAddListenerKey(false); + testAddListenerKey(true); +} + +test(); + +</script> + +</body> +</html> + + diff --git a/dom/events/test/test_paste_image.html b/dom/events/test/test_paste_image.html new file mode 100644 index 0000000000..04d6086466 --- /dev/null +++ b/dom/events/test/test_paste_image.html @@ -0,0 +1,191 @@ +<html><head> +<title>Test for bug 891247</title> +<link rel="stylesheet" href="/tests/SimpleTest/test.css"> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<script src="/tests/SimpleTest/EventUtils.js"></script> + +<script class="testbody" type="application/javascript"> + function ImageTester() { + var counter = 0; + var images = []; + var that = this; + + this.add = function(aFile) { + images.push(aFile); + }; + + this.test = function() { + for (var i = 0; i < images.length; i++) { + testImageSize(images[i]); + } + }; + + this.returned = function() { + counter++; + info("returned=" + counter + " images.length=" + images.length); + if (counter == images.length) { + info("test finish"); + SimpleTest.finish(); + } + }; + + function testImageSize(aFile) { + var source = window.URL.createObjectURL(aFile); + var image = new Image(); + image.src = source; + var imageTester = that; + image.onload = function() { + is(this.width, 62, "Check generated image width"); + is(this.height, 71, "Check generated image height"); + + // This fails on OSX only. + if (!navigator.platform.includes("Mac")) { + testImageCanvas(image); + } + + imageTester.returned(); + } + + document.body.appendChild(image); + }; + + function testImageCanvas(aImage) { + var canvas = drawToCanvas(aImage); + + var refImage = document.getElementById('image'); + var refCanvas = drawToCanvas(refImage); + + is(canvas.toDataURL(), refCanvas.toDataURL(), "Image should map pixel-by-pixel"); + } + + function drawToCanvas(aImage) { + var canvas = document.createElement("CANVAS"); + document.body.appendChild(canvas); + canvas.width = aImage.width; + canvas.height = aImage.height; + canvas.getContext('2d').drawImage(aImage, 0, 0); + return canvas; + } + } + + function copyImage(aImageId) { + // selection of the node + var node = document.getElementById(aImageId); + var docShell = SpecialPowers.wrap(window).docShell; + + // let's copy the node + var documentViewer = docShell.contentViewer + .QueryInterface(SpecialPowers.Ci.nsIContentViewerEdit); + documentViewer.setCommandNode(node); + documentViewer.copyImage(documentViewer.COPY_IMAGE_ALL); + } + + function doTest() { + SimpleTest.waitForExplicitFinish(); + + copyImage('image'); + + //--------- now check the content of the clipboard + var clipboard = SpecialPowers.Cc["@mozilla.org/widget/clipboard;1"] + .getService(SpecialPowers.Ci.nsIClipboard); + // does the clipboard contain text/plain data ? + ok(clipboard.hasDataMatchingFlavors(["text/plain"], clipboard.kGlobalClipboard), + "clipboard contains unicode text"); + // does the clipboard contain text/html data ? + ok(clipboard.hasDataMatchingFlavors(["text/html"], clipboard.kGlobalClipboard), + "clipboard contains html text"); + // does the clipboard contain image data ? + ok(clipboard.hasDataMatchingFlavors(["image/png"], clipboard.kGlobalClipboard), + "clipboard contains image"); + + window.addEventListener("paste", onPaste); + + var textarea = SpecialPowers.wrap(document.getElementById('textarea')); + textarea.focus(); + textarea.editor.paste(clipboard.kGlobalClipboard); + } + + function onPaste(e) { + var imageTester = new ImageTester; + testFiles(e, imageTester); + testItems(e, imageTester); + imageTester.test(); + } + + function testItems(e, imageTester) { + var items = e.clipboardData.items; + is(items, e.clipboardData.items, + "Getting @items twice should return the same object"); + var haveFiles = false; + ok(items instanceof DataTransferItemList, "@items implements DataTransferItemList"); + ok(items.length, "@items is not empty"); + for (var i = 0; i < items.length; i++) { + var item = items[i]; + ok(item instanceof DataTransferItem, "each element of @items must implement DataTransferItem"); + if (item.kind == "file") { + var file = item.getAsFile(); + ok(file instanceof File, ".getAsFile() returns a File object"); + ok(file.size > 0, "Files shouldn't have size 0"); + imageTester.add(file); + } + } + } + + function testFiles(e, imageTester) { + var files = e.clipboardData.files; + + is(files, e.clipboardData.files, + "Getting the files array twice should return the same array"); + is(files.length, 1, "There should be one file in the clipboard"); + for (var i = 0; i < files.length; i++) { + var file = files[i]; + ok(file instanceof File, ".files should contain only File objects"); + ok(file.size > 0, "This file shouldn't have size 0"); + if (navigator.platform.includes("Win")) { + // On Windows the pasted file is (sometimes?) produced from the Windows only + // file promise added by AppendImagePromise. + ok(file.name == "image.png" || (file.name.startsWith("Untitled") && file.name.endsWith(".png")), + `Correct filename, got "${file.name}"`); + } else { + is(file.name, "image.png", "Correct filename"); + } + is(file.type, "image/png", "This file should be a image/png"); + + testSlice(file); + imageTester.add(file); + // Adding the same image again so we can test concurrency + imageTester.add(file); + } + } + + function testSlice(aFile) { + var blob = aFile.slice(); + ok(blob instanceof Blob, ".slice returns a blob"); + is(blob.size, aFile.size, "the blob has the same size"); + + blob = aFile.slice(123123); + is(blob.size, 0, ".slice overflow check"); + + blob = aFile.slice(123, 123141); + is(blob.size, aFile.size - 123, ".slice @size check"); + + blob = aFile.slice(123, 12); + is(blob.size, 0, ".slice @size check 2"); + + blob = aFile.slice(124, 134, "image/png"); + is(blob.size, 10, ".slice @size check 3"); + is(blob.type, "image/png", ".slice @type check"); + } + +</script> +<body onload="doTest();"> + <img id="image" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAD4AAABHCA + IAAADQjmMaAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3goUAwAgSAORBwAAABl0RVh0Q29 + tbWVudABDcmVhdGVkIHdpdGggR0lNUFeBDhcAAABPSURBVGje7c4BDQAACAOga//OmuMbJGAurTbq + 6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6urq6s31B0IqAY2/t + QVCAAAAAElFTkSuQmCC" /> + <form> + <textarea id="textarea"></textarea> + </form> +</body> +</html> diff --git a/dom/events/test/test_scroll_per_page.html b/dom/events/test/test_scroll_per_page.html new file mode 100644 index 0000000000..8d464b1a43 --- /dev/null +++ b/dom/events/test/test_scroll_per_page.html @@ -0,0 +1,259 @@ +<!DOCTYPE html> +<html> +<head> + <title>Test for scroll per page</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="/tests/gfx/layers/apz/test/mochitest/apz_test_utils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> + +<pre id="test"> +<script class="testbody" type="text/javascript"> +SimpleTest.waitForExplicitFinish(); +addLoadEvent(() => { + open("window_empty_document.html", "_blank", "width=500,height=500,scrollbars=yes"); +}); + +async function doTests(aWindow) { + await SpecialPowers.pushPrefEnv({"set": [["general.smoothScroll", false]]}); + await SimpleTest.promiseFocus(aWindow); + + function getNodeDescription(aNode) { + function getElementDescription(aElement) { + if (aElement.getAttribute("id") !== null) { + return `${aElement.tagName.toLowerCase()}#${aElement.getAttribute("id")}`; + } + if (aElement.tagName === "BR") { + return `${getElementDescription(aElement.previousSibling)} + br`; + } + return aElement.tagName.toLowerCase(); + } + switch (aNode.nodeType) { + case aNode.TEXT_NODE: + return `text node in ${getElementDescription(aNode.parentElement)}`; + case aNode.ELEMENT_NODE: + return getElementDescription(aNode); + case aNode.DOCUMENT_NODE: + return `document node`; + default: + return "unknown node"; + } + } + + function getScrollPositionStr(aNode) { + return `{ scrollTop: ${aNode.scrollTop}, scrollHeight: ${ + aNode.scrollHeight + }, scrollLeft: ${aNode.scrollLeft}, scrollWidth: ${aNode.scrollWidth} }`; + } + + async function doPageDownOrUp(aKey, aFocusedElement, aScrollTargetElement) { + let scrollEventTarget = + aScrollTargetElement === doc.documentElement + ? doc + : aScrollTargetElement; + let scrollEventFired = false; + function onScroll(aEvent) { + scrollEventFired |= aEvent.target === scrollEventTarget; + } + scrollEventTarget.addEventListener("scroll", onScroll); + if (!navigator.platform.includes("Mac")) { + synthesizeKey(`KEY_${aKey}`, {}, aWindow); + } else { + synthesizeKey(`KEY_${aKey}`, { altKey: true }, aWindow); + } + let retry = 3; + while (retry--) { + await waitToClearOutAnyPotentialScrolls(aWindow); + if (scrollEventFired) { + break; + } + } + ok(scrollEventFired, + `Scroll event should've been fired on ${getNodeDescription(scrollEventTarget)}`); + scrollEventTarget.removeEventListener("scroll", onScroll); + } + + async function doPageDown(aFocusedElement, aScrollTargetElement) { + await doPageDownOrUp("PageDown", aFocusedElement, aScrollTargetElement); + } + + async function doPageUp(aFocusedElement, aScrollTargetElement) { + await doPageDownOrUp("PageUp", aFocusedElement, aScrollTargetElement); + } + + // Let's put log of scroll events for making debug this test easier. + aWindow.addEventListener("scroll", (aEvent) => { + let scrollElement = + aEvent.target === doc + ? doc.documentElement + : aEvent.target; + info(`"scroll" event fired on ${getNodeDescription(aEvent.target)}: ${ + getScrollPositionStr(scrollElement) + }`); + }, { capture: true }); + + let doc = aWindow.document; + let body = doc.body; + let selection = doc.getSelection(); + let container; + + body.innerHTML = '<div id="largeDiv" style="height: 1500px;">' + + "<p>previous line of the editor.</p>" + + '<div id="editor" contenteditable style="margin-top 500px; height: 5em; overflow: auto;">' + + "Here is first line<br>" + + "Here is second line" + + "</div>" + + "<p>next line of the editor.</p>" + + "</div>"; + container = doc.documentElement; + let editor = doc.getElementById("editor"); + editor.focus(); + await waitToClearOutAnyPotentialScrolls(aWindow); + + let description = "PageDown in non-scrollable editing host: "; + let previousScrollTop = container.scrollTop; + await doPageDown(editor, container); + ok(container.scrollTop > previousScrollTop, + `${description}the document should be scrolled down even if user presses PageDown in the editing host got: ${container.scrollTop}, previous position: ${previousScrollTop}`); + let range = selection.getRangeAt(0); + is(range.startContainer, editor.firstChild.nextSibling.nextSibling, + `${description}selection start shouldn't be moved to outside of the editing host (got: ${getNodeDescription(range.startContainer)})`); + ok(range.collapsed, description + "selection should be collapsed"); + is(doc.activeElement, editor, + description + "the editing host should keep having focus"); + + description = "PageUp in non-scrollable editing host: "; + previousScrollTop = container.scrollTop; + await doPageUp(editor, container); + ok(container.scrollTop < previousScrollTop, + `${description}the document should be scrolled up even if user presses PageDown in the editing host got: ${container.scrollTop}, previous position: ${previousScrollTop}`); + range = selection.getRangeAt(0); + is(range.startContainer, editor.firstChild, + `${description}selection start shouldn't be moved to outside of the editing host (got: ${getNodeDescription(range.startContainer)})`); + ok(range.collapsed, description + "selection should be collapsed"); + is(doc.activeElement, editor, + description + "the editing host should keep having focus"); + + body.innerHTML = '<div id="largeDiv" style="height: 1500px;">' + + "<p>previous line of the editor.</p>" + + '<div id="editor" contenteditable style="margin-top 500px; height: 5em; overflow: auto;">' + + '<div id="innerDiv" style="height: 10em;">' + + "Here is first line<br>" + + "Here is second line" + + "</div>" + + "</div>" + + "<p>next line of the editor.</p>" + + "</div>"; + editor = doc.getElementById("editor"); + container = editor; + editor.focus(); + await waitToClearOutAnyPotentialScrolls(aWindow); + + description = "PageDown in scrollable editing host: "; + previousScrollTop = container.scrollTop; + await doPageDown(editor, container); + ok(container.scrollTop > previousScrollTop, + `${description}the editor should be scrolled down even if user presses PageDown in the editing host got: ${container.scrollTop}, previous position: ${previousScrollTop}`); + range = selection.getRangeAt(0); + is(range.startContainer, editor.firstChild.firstChild.nextSibling.nextSibling, + `${description}selection start shouldn't be moved to outside of the editing host (got: ${getNodeDescription(range.startContainer)})`); + ok(range.collapsed, description + "selection should be collapsed"); + is(doc.activeElement, editor, + description + "the editing host should keep having focus"); + + description = "PageUp in scrollable editing host: "; + previousScrollTop = container.scrollTop; + await doPageUp(editor, container); + ok(container.scrollTop < previousScrollTop, + `${description}the editor should be scrolled up even if user presses PageDown in the editing host got: ${container.scrollTop}, previous position: ${previousScrollTop}`); + range = selection.getRangeAt(0); + is(range.startContainer, editor.firstChild.firstChild, + `${description}selection start shouldn't be moved to outside of the editing host (got: ${getNodeDescription(range.startContainer)})`); + ok(range.collapsed, description + "selection should be collapsed"); + is(doc.activeElement, editor, + description + "the editing host should keep having focus"); + + // Should scroll one page of the scrollable element + body.innerHTML = `<div id="editor" contenteditable style="height: 1500px;">${"abc<br>".repeat(100)}</div>`; + editor = doc.getElementById("editor"); + container = doc.documentElement; + editor.focus(); + await waitToClearOutAnyPotentialScrolls(aWindow); + + description = "PageDown in too large editing host: "; + previousScrollTop = container.scrollTop; + await doPageDown(editor, container); + ok(container.scrollTop > previousScrollTop, + `${description} The document should be scrolled down (got: ${container.scrollTop}, previous position: ${previousScrollTop})`); + ok(container.scrollTop <= previousScrollTop + container.clientHeight, + `${description} The document should not be scrolled down too much (got: ${container.scrollTop}, previous position: ${previousScrollTop}, scroll height: ${container.clientHeight})`); + + selection.selectAllChildren(editor); + selection.collapseToEnd(); + await waitToClearOutAnyPotentialScrolls(aWindow); + + description = "PageUp in too large editing host: "; + container.scrollTop = container.scrollHeight; + previousScrollTop = container.scrollTop; + await doPageUp(editor, container); + ok(container.scrollTop >= previousScrollTop - container.clientHeight, + `${description} The document should not be scrolled up too much (got: ${container.scrollTop}, previous position: ${previousScrollTop}, scroll height: ${container.clientHeight})`); + + // Shouldn't scroll to caret position after pagedown scrolls editing host. + body.innerHTML = '<div id="editor" contenteditable style="height: 300px; overflow: auto;"><div style="height: 1500px;">abc<br>def<br></div></div>'; + editor = doc.getElementById("editor"); + container = editor; + editor.focus(); + await waitToClearOutAnyPotentialScrolls(aWindow); + + description = "PageDown in scrollable editing host"; + previousScrollTop = container.scrollTop; + await doPageDown(editor, container); + ok(container.scrollTop > previousScrollTop, + `${description} #1: Should be scrolled down (got: ${container.scrollTop}, previous position: ${previousScrollTop})`); + previousScrollTop = container.scrollTop; + await doPageDown(editor, container); + ok(container.scrollTop > previousScrollTop, + `${description} #2: should be scrolled down (got:${container.scrollTop}, previous position: ${previousScrollTop})`); + previousScrollTop = container.scrollTop; + await doPageDown(editor, container); + ok(container.scrollTop > previousScrollTop, + `${description} #3: should be scrolled down (got:${container.scrollTop}, previous position: ${previousScrollTop})`); + await doPageUp(editor, container); + ok(container.scrollTop < 300, + `PageUp in scrollable editing host after scrolled down 3 pages: should be scrolled up to show caret (got:${container.scrollTop}`); + + // Shouldn't scroll to caret position after pagedown scrolls outside of editing host. + // NOTE: We've set the window height is 500px above, but on Android, the viewport size depends on the screen size. + // Therefore, we need to compute enough height to test below with actual height of the window. + body.innerHTML = `<div id="editor" contenteditable style="height: ${aWindow.innerHeight * 3}px">abc<br>def<br></div>`; + editor = doc.getElementById("editor"); + container = doc.documentElement; + editor.focus(); + selection.collapse(editor.firstChild); + await waitToClearOutAnyPotentialScrolls(aWindow); + + description = "PageDown in too high non-scrollable editing host"; + previousScrollTop = container.scrollTop; + await doPageDown(editor, container); + ok(container.scrollTop > previousScrollTop, + `${description} #1: Should be scrolled down (got: ${container.scrollTop}, previous position: ${previousScrollTop})`); + previousScrollTop = container.scrollTop; + await doPageDown(editor, container); + ok(container.scrollTop > previousScrollTop, + `${description} #2: should be scrolled down (got:${container.scrollTop}, previous position: ${previousScrollTop})`); + previousScrollTop = container.scrollTop; + await doPageDown(editor, container); + ok(container.scrollTop > previousScrollTop, + `${description} #3: should be scrolled down (got:${container.scrollTop}, previous position: ${previousScrollTop})`); + await doPageUp(editor, container); + ok(container.scrollTop < 300, + `PageUp in too high non-scrollable editing host after scrolled down 3 pages: should be scrolled up to show caret (got:${container.scrollTop}`); + + aWindow.close(); + SimpleTest.finish(); +} +</script> +</html> diff --git a/dom/events/test/test_slotted_mouse_event.html b/dom/events/test/test_slotted_mouse_event.html new file mode 100644 index 0000000000..9be751e95e --- /dev/null +++ b/dom/events/test/test_slotted_mouse_event.html @@ -0,0 +1,97 @@ +<!doctype html> +<meta charset="utf-8"> +<title>Bug 1481500: mouse enter / leave events in slotted content</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<script src="/tests/SimpleTest/EventUtils.js"></script> +<link rel="stylesheet" href="/tests/SimpleTest/test.css"> +<script> +// We move the mouse from the #host to #target, then to #child-target. +// +// By the time we get to #child-target, we shouldn't have fired any mouseleave. +function runTests() { + let iframe = document.createElement('iframe'); + iframe.style.width = "600px"; + iframe.style.height = "600px"; + document.body.appendChild(iframe); + iframe.onload = () => frameLoaded(iframe); + iframe.srcdoc = ` + <style> + #child-target { + width: 80px; + height: 80px; + background: yellow; + } + </style> + <div id="host"><div id="target"><div id="child-target"></div></div></div> + `; +} + +function frameLoaded(iframe) { + let host = iframe.contentDocument.getElementById('host'); + let target = iframe.contentDocument.getElementById('target'); + let childTarget = iframe.contentDocument.getElementById('child-target'); + let sawHost = false; + let sawTarget = false; + let finished = false; + + host.attachShadow({ mode: 'open' }).innerHTML = ` + <style> + :host { + width: 500px; + height: 500px; + background: purple; + } + ::slotted(div) { + width: 200px; + height: 200px; + background: green; + } + </style> + <slot></slot> + `; + + host.addEventListener("mouseenter", e => { + if (finished) + return; + sawHost = true; + ok(true, "Should fire mouseenter on the host."); + }); + + host.addEventListener("mouseleave", e => { + if (finished) + return; + ok(false, "Should not fire mouseleave when moving the cursor to the slotted target"); + }); + + target.addEventListener("mouseenter", () => { + if (finished) + return; + ok(sawHost, "Should've seen the hostmouseenter already"); + sawTarget = true; + ok(true, "Moving the mouse into the target should trigger a mouseenter there"); + }); + + target.addEventListener("mouseleave", () => { + if (finished) + return; + ok(false, "Should not fire mouseleave when moving the cursor to the slotted target's child"); + }); + + childTarget.addEventListener("mouseenter", () => { + if (finished) + return; + ok(sawTarget, "Should've seen the target mouseenter already"); + finished = true; + SimpleTest.finish(); + }); + + synthesizeMouseAtCenter(host, { type: "mousemove" }); + synthesizeMouseAtCenter(target, { type: "mousemove" }); + synthesizeMouseAtCenter(childTarget, { type: "mousemove" }); +} + +SimpleTest.waitForExplicitFinish(); +window.onload = () => { + SimpleTest.waitForFocus(runTests); +}; +</script> diff --git a/dom/events/test/test_slotted_text_click.html b/dom/events/test/test_slotted_text_click.html new file mode 100644 index 0000000000..34464bd918 --- /dev/null +++ b/dom/events/test/test_slotted_text_click.html @@ -0,0 +1,72 @@ +<!doctype html> +<meta charset="utf-8"> +<title>Bug 1481500: click / activation on text activates the slot it's assigned to</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<script src="/tests/SimpleTest/EventUtils.js"></script> +<link rel="stylesheet" href="/tests/SimpleTest/test.css"> +<script> +function generateLotsOfText() { + let text = "Some text. "; + for (let i = 0; i < 10; ++i) + text += text; + return text; +} + +function runTests() { + let iframe = document.createElement('iframe'); + document.body.appendChild(iframe); + iframe.onload = () => frameLoaded(iframe); + iframe.width = "350" + iframe.height = "350" + iframe.srcdoc = + `<div id="host">${generateLotsOfText()}</div>` +} + +function frameLoaded(iframe) { + let host = iframe.contentDocument.getElementById('host'); + + host.attachShadow({ mode: 'open' }).innerHTML = ` + <style> + :host { + width: 300px; + height: 300px; + overflow: hidden; + } + </style> + <slot></slot> + `; + + let slot = host.shadowRoot.querySelector('slot'); + let mousedownFired = false; + let mouseupFired = false; + slot.addEventListener('mousedown', function() { + ok(true, "Mousedown should fire on the slot when clicking on text"); + mousedownFired = true; + }); + + slot.addEventListener('click', function() { + ok(true, "Click should target the slot"); + ok(mousedownFired, "mousedown should've fired"); + ok(mouseupFired, "click should've fired"); + SimpleTest.finish(); + }); + + slot.addEventListener('mouseup', function() { + // FIXME: When we fix bug 1481517, this check should move to the mousedown listener. + ok(this.matches(":active"), "Slot should become active"); + mouseupFired = true; + }); + + requestAnimationFrame(() => { + requestAnimationFrame(() => { + synthesizeMouseAtPoint(150, 150, { type: "mousedown" }); + synthesizeMouseAtPoint(150, 150, { type: "mouseup" }); + }); + }); +} + +SimpleTest.waitForExplicitFinish(); +window.onload = () => { + SimpleTest.waitForFocus(runTests); +}; +</script> diff --git a/dom/events/test/test_submitevent_on_form.html b/dom/events/test/test_submitevent_on_form.html new file mode 100644 index 0000000000..01cd8f28ee --- /dev/null +++ b/dom/events/test/test_submitevent_on_form.html @@ -0,0 +1,67 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test submit event on form</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + <form action="javascript:doDefault()" id="form"> + <input name="n1" value="v1"> + <input type="submit" value="Do Default Action"> + </form> + <pre id="test"> + <script type="application/javascript"> +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(runTests); + +var defaultActionCount = 0; + +function doDefault() +{ + defaultActionCount++; +} + +async function runTests() +{ + const form = document.getElementById("form"); + let formDataInEvent = null; + + // Test if submission from submit() is deferred. + await SpecialPowers.pushPrefEnv({ + set: [["dom.forms.submit.trusted_event_only", false]], + }); + + form.addEventListener("submit", function(e) { + form.elements[0].value = "v1"; + form.submit(); + form.elements[0].value = "v2"; + }, { once: true }); + + form.addEventListener('formdata', e => { + formDataInEvent = e.formData; + }); + + form.dispatchEvent(new Event('submit')); + await new Promise(resolve => SimpleTest.executeSoon(resolve)); + + is(defaultActionCount, 1, "should only submit once"); + is(formDataInEvent.get('n1'), 'v2', "submission from submit() should be deferred"); + + // Test untrusted submit event shouldn't trigger form default action. + defaultActionCount = 0; + await SpecialPowers.pushPrefEnv({ + set: [["dom.forms.submit.trusted_event_only", true]], + }); + + form.dispatchEvent(new Event('submit')); + setTimeout(() => { + is(defaultActionCount, 0, "untrusted submit event shouldn't trigger form default action"); + SimpleTest.finish(); + }); +} + </script> + </pre> +</body> +</html> diff --git a/dom/events/test/test_text_event_in_content.html b/dom/events/test/test_text_event_in_content.html new file mode 100644 index 0000000000..3d7735edb1 --- /dev/null +++ b/dom/events/test/test_text_event_in_content.html @@ -0,0 +1,68 @@ +<!doctype html> +<html> +<head> + <title>Not dispatching DOM "text" event on web apps</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<input id="input"> +<textarea id="textarea"></textarea> +<div contenteditable id="editor"><p><br></p></div> +<script> +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function doTests() { + for (let editorId of ["input", "textarea", "editor"]) { + let editor = document.getElementById(editorId); + editor.focus(); + let fired = false; + function onText() { + fired = true; + } + editor.addEventListener("text", onText); + + fired = false; + synthesizeCompositionChange({ + composition: {string: "abc", + clauses: [{length: 3, attr: COMPOSITION_ATTR_RAW_CLAUSE}]}, + caret: {start: 3, length: 0}, + }); + ok(!fired, `Starting composition shouldn't fire DOM "text" event in ${editorId}`); + fired = false; + synthesizeComposition({type: "compositioncommitasis", key: {key: "KEY_Enter"}}); + ok(!fired, `Committing composition with the latest string shouldn't fire DOM "text" event in ${editorId}`); + + fired = false; + synthesizeCompositionChange({ + composition: {string: "def", + clauses: [{length: 3, attr: COMPOSITION_ATTR_RAW_CLAUSE}]}, + caret: {start: 3, length: 0}, + }); + ok(!fired, `Restarting composition shouldn't fire DOM "text" event in ${editorId}`); + fired = false; + synthesizeComposition({type: "compositioncommit", data: "", key: {key: "KEY_Escape"}}); + ok(!fired, `Committing composition with empty string shouldn't fire DOM "text" event in ${editorId}`); + + fired = false; + synthesizeCompositionChange({ + composition: {string: "de", + clauses: [{length: 2, attr: COMPOSITION_ATTR_RAW_CLAUSE}]}, + caret: {start: 2, length: 0}, + }); + ok(!fired, `Restarting composition shouldn't fire DOM "text" event in ${editorId}`); + fired = false; + synthesizeComposition({type: "compositioncommit", data: "def", key: {key: "KEY_Escape"}}); + ok(!fired, `Committing composition with new string shouldn't fire DOM "text" event in ${editorId}`); + + fired = false; + synthesizeComposition({type: "compositioncommit", data: "ghi"}); + ok(!fired, `Inserting string shouldn't fire DOM "text" event in ${editorId}`); + + editor.removeEventListener("text", onText); + } + SimpleTest.finish(); +}); +</script> +</body> +</html> diff --git a/dom/events/test/test_unbound_before_in_active_chain.html b/dom/events/test/test_unbound_before_in_active_chain.html new file mode 100644 index 0000000000..b62d44bb31 --- /dev/null +++ b/dom/events/test/test_unbound_before_in_active_chain.html @@ -0,0 +1,38 @@ +<!doctype html> +<title>Test for bug 1489139: Unbound generated content in the active chain</title> +<script src="/tests/SimpleTest/EventUtils.js"></script> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<style> +#target, #target::before { + width: 200px; + height: 200px; +} + +#target::before { + content: " "; + display: block; + background: green; +} + +#target:active::before { + content: ""; + background: red; +} +</style> +Should see a green square after clicking. +<div id="target"></div> +<script> +SimpleTest.waitForExplicitFinish(); +onload = function() { + let target = document.getElementById("target"); + requestAnimationFrame(() => { + synthesizeMouseAtPoint(100, 100, { type: "mousedown" }) + ok(target.matches(":active"), "Should have been clicked"); + requestAnimationFrame(() => { + synthesizeMouseAtPoint(100, 100, { type: "mouseup" }) + ok(!target.matches(':active'), "Should stop matching :active afterwards"); + SimpleTest.finish(); + }); + }); +} +</script> diff --git a/dom/events/test/test_use_conflated_keypress_event_model_on_newer_Office_Online_Server.html b/dom/events/test/test_use_conflated_keypress_event_model_on_newer_Office_Online_Server.html new file mode 100644 index 0000000000..2e897a48c0 --- /dev/null +++ b/dom/events/test/test_use_conflated_keypress_event_model_on_newer_Office_Online_Server.html @@ -0,0 +1,59 @@ +<!DOCTYPE html> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1545410 +--> +<head> + <meta charset="utf-8"> + <title>Testing whether "keypress" event model is forcibly conflated model if the document is newer Office Online Server instance</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1545410">Bug 1545410</a> +<p id="display"></p> +<pre id="test"></pre> +<input id="input"> +<iframe id="iframe" srcdoc='<html><body><div id="WACViewPanel_EditingElement" spellcheck="false" class="FireFox usehover WACEditing EditMode EditingSurfaceBody WACViewPanel_DisableLegacyKeyCodeAndCharCode" style="overflow: visible; visibility: visible;" contenteditable="true"></div></body></html>'></iframe> +<script> +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function doTests() { + let iframe = document.getElementById("iframe"); + iframe.contentDocument.body.firstChild.focus(); + let keypressEvent; + iframe.contentDocument.body.addEventListener("keypress", aEvent => keypressEvent = aEvent, {once: true}); + synthesizeKey("a", {}, iframe.contentWindow); + is(keypressEvent.keyCode, "a".charCodeAt(0), + "keyCode value of 'a' should be 'a'"); + is(keypressEvent.charCode, "a".charCodeAt(0), + "charCode value of 'a' should be 'a'"); + + iframe.contentDocument.body.addEventListener("keypress", aEvent => keypressEvent = aEvent, {once: true}); + synthesizeKey("KEY_Enter", {}, iframe.contentWindow); + is(keypressEvent.keyCode, KeyboardEvent.DOM_VK_RETURN, + "keyCode value of 'Enter' should be DOM_VK_RETURN"); + is(keypressEvent.charCode, KeyboardEvent.DOM_VK_RETURN, + "charCode value of 'Enter' should be DOM_VK_RETURN"); + + let input = document.getElementById("input"); + input.focus(); + input.addEventListener("keypress", aEvent => keypressEvent = aEvent, {once: true}); + synthesizeKey("a", {}); + is(keypressEvent.keyCode, "a".charCodeAt(0), + "keyCode value of 'a' in the parent document should be 'a'"); + is(keypressEvent.charCode, "a".charCodeAt(0), + "charCode value of 'a' in the parent document should be 'a'"); + + input.addEventListener("keypress", aEvent => keypressEvent = aEvent, {once: true}); + synthesizeKey("KEY_Enter"); + is(keypressEvent.keyCode, KeyboardEvent.DOM_VK_RETURN, + "keyCode value of 'Enter' in the parent document should be DOM_VK_RETURN"); + is(keypressEvent.charCode, KeyboardEvent.DOM_VK_RETURN, + "charCode value of 'Enter' in the parent document should be DOM_VK_RETURN"); + + SimpleTest.finish(); +}); +</script> +</body> +</html> diff --git a/dom/events/test/test_use_split_keypress_event_model_on_old_Confluence.html b/dom/events/test/test_use_split_keypress_event_model_on_old_Confluence.html new file mode 100644 index 0000000000..3898584b0a --- /dev/null +++ b/dom/events/test/test_use_split_keypress_event_model_on_old_Confluence.html @@ -0,0 +1,82 @@ +<!DOCTYPE html> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1514940 +--> +<head> + <meta charset="utf-8"> + <title>Testing whether "keypress" event model is forcibly split model if the document is old Confluence instance</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1514940">Bug 1514940</a> +<p id="display"></p> +<pre id="test"></pre> +<input id="input"> +<iframe id="iframe" srcdoc="<html><body><p>Here is editor</p></body></html>"></iframe> +<script> +// Emulate window.tinyMCE.CursorTargetPlugin().getInfo() which is referred by +// KeyPresEventModelCheckerChild. +class CursorTargetPluginImpl { + getInfo() { + return { + longname: "Cursor Target plugin", + author: "Atlassian", + authorurl: "http://www.atlassian.com", + version: "1.0", + }; + } +} +var tinyMCE = { + plugins: { + CursorTargetPlugin: CursorTargetPluginImpl, + }, +}; + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(async function doTests() { + let iframe = document.getElementById("iframe"); + let waitForCheckKeyPressEventModelEvent = new Promise(resolve => { + SpecialPowers.addSystemEventListener(iframe.contentDocument, "CheckKeyPressEventModel", resolve, {once: true}); + }); + iframe.contentDocument.body.setAttribute("contenteditable", "true"); + await waitForCheckKeyPressEventModelEvent; + iframe.contentDocument.body.focus(); + let keypressEvent; + iframe.contentDocument.body.addEventListener("keypress", aEvent => keypressEvent = aEvent, {once: true}); + synthesizeKey("a", {}, iframe.contentWindow); + is(keypressEvent.keyCode, 0, + "keyCode value of 'a' should be 0"); + is(keypressEvent.charCode, "a".charCodeAt(0), + "charCode value of 'a' should be 'a'"); + + iframe.contentDocument.body.addEventListener("keypress", aEvent => keypressEvent = aEvent, {once: true}); + synthesizeKey("KEY_Enter", {}, iframe.contentWindow); + is(keypressEvent.keyCode, KeyboardEvent.DOM_VK_RETURN, + "keyCode value of 'Enter' should be DOM_VK_RETURN"); + is(keypressEvent.charCode, 0, + "charCode value of 'Enter' should be 0"); + + let input = document.getElementById("input"); + input.focus(); + input.addEventListener("keypress", aEvent => keypressEvent = aEvent, {once: true}); + synthesizeKey("a", {}); + is(keypressEvent.keyCode, "a".charCodeAt(0), + "keyCode value of 'a' in the parent document should be 'a'"); + is(keypressEvent.charCode, "a".charCodeAt(0), + "charCode value of 'a' in the parent document should be 'a'"); + + input.addEventListener("keypress", aEvent => keypressEvent = aEvent, {once: true}); + synthesizeKey("KEY_Enter"); + is(keypressEvent.keyCode, KeyboardEvent.DOM_VK_RETURN, + "keyCode value of 'Enter' in the parent document should be DOM_VK_RETURN"); + is(keypressEvent.charCode, KeyboardEvent.DOM_VK_RETURN, + "charCode value of 'Enter' in the parent document should be DOM_VK_RETURN"); + + SimpleTest.finish(); +}); +</script> +</body> +</html> diff --git a/dom/events/test/test_use_split_keypress_event_model_on_old_Office_Online_Server.html b/dom/events/test/test_use_split_keypress_event_model_on_old_Office_Online_Server.html new file mode 100644 index 0000000000..68f52be41d --- /dev/null +++ b/dom/events/test/test_use_split_keypress_event_model_on_old_Office_Online_Server.html @@ -0,0 +1,69 @@ +<!DOCTYPE html> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1545410 +--> +<head> + <meta charset="utf-8"> + <title>Testing whether "keypress" event model is forcibly split model if the document is old Office Online Server instance</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1545410">Bug 1545410</a> +<p id="display"></p> +<pre id="test"></pre> +<input id="input"> +<iframe id="iframe" onload="srcdocLoaded()" srcdoc='<html><body><div id="WACViewPanel_EditingElement" spellcheck="false" class="FireFox usehover WACEditing EditMode EditingSurfaceBody" style="overflow: visible; visibility: visible;"></div></body></html>'></iframe> +<script> +SimpleTest.waitForExplicitFinish(); +let waitForCheckKeyPressEventModelEvent; +function srcdocLoaded() { + waitForCheckKeyPressEventModelEvent = new Promise(resolve => { + dump(document.querySelector("iframe").contentDocument.location + "\n"); + var doc = document.querySelector("iframe").contentDocument; + SpecialPowers.addSystemEventListener(doc, "CheckKeyPressEventModel", resolve, {once: true}); + doc.getElementById("WACViewPanel_EditingElement").contentEditable = "true"; + }); +} +SimpleTest.waitForFocus(async function doTests() { + let iframe = document.getElementById("iframe"); + await waitForCheckKeyPressEventModelEvent; + iframe.contentDocument.body.firstChild.focus(); + let keypressEvent; + iframe.contentDocument.body.addEventListener("keypress", aEvent => keypressEvent = aEvent, {once: true}); + synthesizeKey("a", {}, iframe.contentWindow); + is(keypressEvent.keyCode, 0, + "keyCode value of 'a' should be 0"); + is(keypressEvent.charCode, "a".charCodeAt(0), + "charCode value of 'a' should be 'a'"); + + iframe.contentDocument.body.addEventListener("keypress", aEvent => keypressEvent = aEvent, {once: true}); + synthesizeKey("KEY_Enter", {}, iframe.contentWindow); + is(keypressEvent.keyCode, KeyboardEvent.DOM_VK_RETURN, + "keyCode value of 'Enter' should be DOM_VK_RETURN"); + is(keypressEvent.charCode, 0, + "charCode value of 'Enter' should be 0"); + + let input = document.getElementById("input"); + input.focus(); + input.addEventListener("keypress", aEvent => keypressEvent = aEvent, {once: true}); + synthesizeKey("a", {}); + is(keypressEvent.keyCode, "a".charCodeAt(0), + "keyCode value of 'a' in the parent document should be 'a'"); + is(keypressEvent.charCode, "a".charCodeAt(0), + "charCode value of 'a' in the parent document should be 'a'"); + + input.addEventListener("keypress", aEvent => keypressEvent = aEvent, {once: true}); + synthesizeKey("KEY_Enter"); + is(keypressEvent.keyCode, KeyboardEvent.DOM_VK_RETURN, + "keyCode value of 'Enter' in the parent document should be DOM_VK_RETURN"); + is(keypressEvent.charCode, KeyboardEvent.DOM_VK_RETURN, + "charCode value of 'Enter' in the parent document should be DOM_VK_RETURN"); + + SimpleTest.finish(); +}); +</script> +</body> +</html> diff --git a/dom/events/test/test_wheel_default_action.html b/dom/events/test/test_wheel_default_action.html new file mode 100644 index 0000000000..ba5d956d4b --- /dev/null +++ b/dom/events/test/test_wheel_default_action.html @@ -0,0 +1,39 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for default action of WheelEvent</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> + +SimpleTest.waitForExplicitFinish(); +SimpleTest.requestFlakyTimeout("untriaged"); +SpecialPowers.pushPrefEnv({"set": [ + ["mousewheel.system_scroll_override.enabled", false] +]}, runTest); + +var subWin = null; + +function runTest() { + subWin = window.open("window_wheel_default_action.html", "_blank", + "width=500,height=500,scrollbars=yes"); +} + +function finish() +{ + subWin.close(); + SimpleTest.finish(); +} + +</script> +</pre> +</body> +</html> diff --git a/dom/events/test/test_wheel_zoom_on_form_controls.html b/dom/events/test/test_wheel_zoom_on_form_controls.html new file mode 100644 index 0000000000..73fc13558c --- /dev/null +++ b/dom/events/test/test_wheel_zoom_on_form_controls.html @@ -0,0 +1,69 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Zoom using wheel should work on form controls</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<button id="button" style="width:10px;height:10px;"></button><br> +<input id="input" style="border: 10px solid green;"><br> +<textarea id="textarea" style="border: 10px solid green;"></textarea><br> +<select id="select"><option></option></select> +<select id="list" size=4> + <option>XXXXXXXXXX</option> + <option>XXXXXXXXXX</option> + <option>XXXXXXXXXX</option> + <option>XXXXXXXXXX</option> + <option>XXXXXXXXXX</option> + <option>XXXXXXXXXX</option> +</select> +<script> + + async function testControl(id) { + var initialZoom = SpecialPowers.getFullZoom(window); + var element = document.getElementById(id); + + const zoomHasHappened = SimpleTest.promiseWaitForCondition(() => { + const zoom = SpecialPowers.getFullZoom(window); + return (zoom != initialZoom); + }, id + ": wheel event changed the zoom."); + + let event = { + deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaY: 3, + ctrlKey: true + }; + synthesizeWheel(element, 5, 5, event); + + await zoomHasHappened; + isnot(SpecialPowers.getFullZoom(window), initialZoom, id + ": should have zoomed"); + SpecialPowers.setFullZoom(window, initialZoom); + } + + async function test() { + await testControl("button"); + await testControl("input"); + await testControl("textarea"); + await testControl("select"); + await testControl("list"); + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + SimpleTest.waitForFocus(() => { + SpecialPowers.pushPrefEnv({ + "set":[ + ["mousewheel.with_control.action", 3], // Scroll on Ctrl + mousewheel + ["test.events.async.enabled", true], + ] + }, test) + }); +</script> +</body> +</html> diff --git a/dom/events/test/window_bug1369072.html b/dom/events/test/window_bug1369072.html new file mode 100644 index 0000000000..94b147d38f --- /dev/null +++ b/dom/events/test/window_bug1369072.html @@ -0,0 +1,170 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1369072 +--> +<head> + <title>Test for Bug 1369072</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script src="/tests/SimpleTest/paint_listener.js"></script> + <script src="/tests/gfx/layers/apz/test/mochitest/apz_test_utils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1369072">Mozilla Bug 1369072</a> +<div id="display"> +<iframe id="iframe" srcdoc="<a id='anchor' href='about:home'>anchor text</a><div id='div'></div>" style="width: 300px; height: 300px;"></iframe> +<!-- make <body> contents overflow --> +<div style="width: 1000px; height: 1000px;"></div> +</div> +<div id="content" style="display: none"> +</div> +<pre id="test"> +<script type="application/javascript"> + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(runTests, window); + +function ok() +{ + window.opener.ok.apply(window.opener, arguments); +} + +function is() +{ + window.opener.is.apply(window.opener, arguments); +} + +async function runTests() +{ + var iframe = document.getElementById("iframe"); + var anchor = iframe.contentDocument.getElementById("anchor"); + var div = iframe.contentDocument.getElementById("div"); + + async function resetScroll() + { + var oldFocus = document.activeElement; + var oldFrameFocus = iframe.contentDocument.activeElement; + + // Cancel any scroll animation on the target scroll elements to make sure + // setting scrollTop or scrolLeft works as expected. + // cancelScrollAnimation clears focus, so make sure to restore it. + await cancelScrollAnimation(document.documentElement); + await cancelScrollAnimation(iframe.contentDocument.documentElement); + + oldFocus.focus(); + oldFrameFocus.focus(); + + return new Promise(resolve => { + var scrollParent = document.documentElement.scrollTop || document.documentElement.scrollLeft; + var scrollChild = iframe.contentDocument.documentElement.scrollTop || iframe.contentDocument.documentElement.scrollLeft; + if (scrollParent) { + window.addEventListener("scroll", () => { + scrollParent = false; + if (!scrollChild) { + SimpleTest.executeSoon(resolve); + } + }, { once: true }); + } + if (scrollChild) { + iframe.contentWindow.addEventListener("scroll", () => { + scrollChild = false; + if (!scrollParent) { + SimpleTest.executeSoon(resolve); + } + }, { once: true }); + } + document.documentElement.scrollTop = 0; + document.documentElement.scrollLeft = 0; + iframe.contentDocument.documentElement.scrollTop = 0; + iframe.contentDocument.documentElement.scrollLeft = 0; + if (!scrollParent && !scrollChild) { + SimpleTest.executeSoon(resolve); + } + }); + } + + async function tryToScrollWithKey(aVertical) + { + await resetScroll(); + + return new Promise(resolve => { + // Wait scroll event + function onScroll() { + SimpleTest.executeSoon(resolve); + } + window.addEventListener("scroll", onScroll, { once: true }); + iframe.contentWindow.addEventListener("scroll", onScroll, { once: true }); + + if (aVertical) { + synthesizeKey("KEY_ArrowDown"); + } else { + synthesizeKey("KEY_ArrowRight"); + } + }); + } + + // When iframe element has focus and the iframe document isn't scrollable, the parent document should be scrolled instead. + document.body.focus(); + iframe.focus(); + await tryToScrollWithKey(true); + ok(document.documentElement.scrollTop > 0, "ArrowDown keydown event at the iframe whose content is not scrollable should cause scrolling the parent document"); + await tryToScrollWithKey(false); + ok(document.documentElement.scrollLeft > 0, "ArrowRight keydown event at the iframe whose content is not scrollable should cause scrolling the parent document"); + await resetScroll(); + + // When iframe element has focus and the iframe document scrollable, the parent document shouldn't be scrolled. + document.body.focus(); + div.style.height = "1000px"; + div.style.width = "1000px"; + iframe.focus(); + await tryToScrollWithKey(true); + is(document.documentElement.scrollTop, 0, "ArrowDown keydown event at the iframe whose content is scrollable shouldn't cause scrolling the parent document"); + ok(iframe.contentDocument.documentElement.scrollTop > 0, "ArrowDown keydown event at the iframe whose content is scrollable should cause scrolling the iframe document"); + await tryToScrollWithKey(false); + is(document.documentElement.scrollLeft, 0, "ArrowRight keydown event at the iframe whose content is scrollable shouldn't cause scrolling the parent document"); + ok(iframe.contentDocument.documentElement.scrollLeft > 0, "ArrowRight keydown event at the iframe whose content is scrollable should cause scrolling the iframe document"); + await resetScroll(); + + // If iframe document cannot scroll to specific direction, parent document should be scrolled instead. + div.style.height = "1px"; + div.style.width = "1000px"; + iframe.focus(); + await tryToScrollWithKey(true); + ok(document.documentElement.scrollTop > 0, "ArrowDown keydown event at the iframe whose content is scrollable only horizontally should cause scrolling the parent document"); + await tryToScrollWithKey(false); + is(document.documentElement.scrollLeft, 0, "ArrowRight keydown event at the iframe whose content is scrollable only horizontally shouldn't cause scrolling the parent document"); + ok(iframe.contentDocument.documentElement.scrollLeft > 0, "ArrowRight keydown event at the iframe whose content is scrollable only horizontally should cause scrolling the iframe document"); + await resetScroll(); + + div.style.height = "1000px"; + div.style.width = "1px"; + iframe.focus(); + await tryToScrollWithKey(true); + is(document.documentElement.scrollTop, 0, "ArrowDown keydown event at the iframe whose content is scrollable only vertically shouldn't cause scrolling the parent document"); + ok(iframe.contentDocument.documentElement.scrollTop > 0, "ArrowDown keydown event at the iframe whose content is scrollable only vertically should cause scrolling the iframe document"); + await tryToScrollWithKey(false); + ok(document.documentElement.scrollLeft > 0, "ArrowRight keydown event at the iframe whose content is scrollable only vertically should cause scrolling the parent document"); + await resetScroll(); + + // Hidden iframe shouldn't consume keyboard events if it was not scrollable. + document.body.focus(); + anchor.focus(); + iframe.style.display = "none"; + await tryToScrollWithKey(true); + ok(document.documentElement.scrollTop > 0, "ArrowDown keydown event after hiding the iframe should cause scrolling the parent document"); + await tryToScrollWithKey(false); + ok(document.documentElement.scrollLeft > 0, "ArrowRight keydown event after hiding the iframe should cause scrolling the parent document"); + await resetScroll(); + + // Make sure the result visible in the viewport. + document.documentElement.scrollTop = 0; + document.documentElement.scrollLeft = 0; + window.opener.finish(); +} + +</script> +</pre> +</body> +</html> diff --git a/dom/events/test/window_bug1412775.xhtml b/dom/events/test/window_bug1412775.xhtml new file mode 100644 index 0000000000..e1274c7bb5 --- /dev/null +++ b/dom/events/test/window_bug1412775.xhtml @@ -0,0 +1,8 @@ +<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + width="640" height="480"> + + <browser id="browser" type="content" primary="true" flex="1" src="about:blank"/> + +</window> diff --git a/dom/events/test/window_bug1429572.html b/dom/events/test/window_bug1429572.html new file mode 100644 index 0000000000..694794b1e4 --- /dev/null +++ b/dom/events/test/window_bug1429572.html @@ -0,0 +1,351 @@ +<html> + <head> + <title></title> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script> + var tests = [ + simpleNonShadowTest, + nonShadowInputDate, + jsDispatchedNonShadowTouchEvents, + shadowDOMTest1, + shadowDOMTest2, + shadowDOMTest3, + jsDispatchedShadowTouchEvents, + jsDispatchedShadowTouchEvents2 + ]; + var host; + var host2; + var shadowRoot; + var shadowRoot2; + var winUtils; + + var touchCounter = 0; + function createTouchArray(targetList) { + var touchArray = []; + for (var i = 0; i < targetList.length; ++i) { + touchArray.push(new Touch({identifier: ++touchCounter, target: targetList[i]})); + } + return touchArray; + } + + function synthesizeTouches(targets, xOffsets) { + if (xOffsets) { + opener.is(targets.length, xOffsets.length, "Wrong xOffsets length!"); + } + var touches = []; + var xs = []; + var ys = []; + var rxs = []; + var rys = []; + var angles = []; + var forces = []; + var tiltXs = []; + var tiltYs = []; + var twists = []; + for (var i = 0; i < targets.length; ++i) { + touches.push(++touchCounter); + var rect = targets[i].getBoundingClientRect(); + if (xOffsets) { + xs.push(rect.left + (rect.width / 2) + xOffsets[i]); + } else { + xs.push(rect.left + (rect.width / 2)); + } + ys.push(rect.top + (rect.height / 2)); + rxs.push(1); + rys.push(1); + angles.push(0); + forces.push(1); + tiltXs.push(0); + tiltYs.push(0); + twists.push(0); + } + winUtils.sendTouchEvent("touchstart", + touches, xs, ys, rxs, rys, angles, forces, tiltXs, tiltYs, twists, 0); + winUtils.sendTouchEvent("touchend", + touches, xs, ys, rxs, rys, angles, forces, tiltXs, tiltYs, twists, 0); + } + + function next() { + if (!tests.length) { + opener.done(); + window.close(); + } else { + var test = tests.shift(); + requestAnimationFrame(function() { setTimeout(test); }); + } + } + + function simpleNonShadowTest() { + var s1 = document.getElementById("span1"); + var s2 = document.getElementById("span2"); + var s3 = document.getElementById("span3"); + var nonShadow = document.getElementById("nonshadow"); + var event; + nonShadow.ontouchstart = function(e) { + event = e; + opener.is(e.targetTouches.length, 1, "Should have only one entry in targetTouches."); + opener.is(e.targetTouches[0].target, e.target, "targetTouches should contain event.target."); + opener.is(e.touches.length, 3, "touches list should contain all the touch objects."); + opener.is(e.changedTouches.length, 3, "changedTouches list should contain all the touch objects."); + } + synthesizeTouches([s1, s2, s3]); + + opener.is(event.targetTouches.length, 1, "Should have only one entry in targetTouches. (2)"); + opener.is(event.targetTouches[0].target, event.target, "targetTouches should contain event.target. (2)"); + opener.is(event.touches.length, 3, "touches list should contain all the touch objects. (2)"); + opener.is(event.changedTouches.length, 3, "changedTouches list should contain all the touch objects. (2)"); + + next(); + } + + function jsDispatchedNonShadowTouchEvents() { + var s1 = document.getElementById("span1"); + var s2 = document.getElementById("span2"); + var s3 = document.getElementById("span3"); + var nonShadow = document.getElementById("nonshadow"); + var didCallListener = false; + nonShadow.ontouchstart = function(e) { + didCallListener = true; + opener.is(e.targetTouches.length, 3, "Should have all the entries in targetTouches."); + opener.is(e.targetTouches[0].target, s1, "targetTouches should contain s1 element."); + opener.is(e.touches.length, 3, "touches list should contain all the touch objects."); + opener.is(e.changedTouches.length, 3, "changedTouches list should contain all the touch objects."); + } + var touchArray = createTouchArray([s1, s2, s3]); + var touchEvent = new TouchEvent("touchstart", + { + touches: touchArray, + targetTouches: touchArray, + changedTouches: touchArray + }); + opener.is(touchEvent.targetTouches.length, 3, "Should have 3 entries in targetTouches"); + nonShadow.dispatchEvent(touchEvent); + opener.ok(didCallListener, "Should have called the listener."); + opener.is(touchEvent.targetTouches.length, 3, "Should have all the entries in targetTouches. (2)"); + opener.is(touchEvent.targetTouches[0].target, s1, "targetTouches should contain s1 element. (2)"); + opener.is(touchEvent.touches.length, 3, "touches list should contain all the touch objects. (2)"); + opener.is(touchEvent.changedTouches.length, 3, "changedTouches list should contain all the touch objects. (2)"); + + nonShadow.ontouchstart = null; + next(); + } + + function nonShadowInputDate() { + // This is a test for dispathing several touches to an element with + // native anonymous content. + var s1 = document.getElementById("span1"); + var date = document.getElementById("date"); + var nonShadow = document.getElementById("nonshadow"); + var hasDateAsTarget = false; + var didCallListener = false; + nonShadow.ontouchstart = function(e) { + didCallListener = true; + if (e.targetTouches[0].target == date) { + hasDateAsTarget = true; + opener.is(e.targetTouches.length, 2, "Should have two entries in targetTouches."); + opener.is(e.targetTouches[0].target, e.target, "targetTouches should contain date."); + opener.is(e.targetTouches[1].target, e.target, "targetTouches should contain date twice."); + } + opener.is(e.touches.length, 3, "touches list should contain all the touch objects."); + opener.is(e.changedTouches.length, 3, "changedTouches list should contain all the touch objects."); + } + + var rect = date.getBoundingClientRect(); + var quarter = rect.width / 4; + synthesizeTouches([date, date, s1], [-quarter, quarter, 0]); + opener.ok(didCallListener, "Should have called listener."); + opener.ok(hasDateAsTarget, "Should have seen touchstart with date element as the target.") + nonShadow.ontouchstart = null; + next(); + } + + function shadowDOMTest1() { + var shadowS1 = shadowRoot.getElementById("shadowSpan1"); + + // Ensure retargeting works. + var hostHandled = false; + host.ontouchstart = function(e) { + hostHandled = true; + opener.is(e.targetTouches.length, 1, "Should have only one entry in targetTouches."); + opener.is(e.targetTouches[0].target, e.target, "targetTouches should contain event.target."); + opener.is(e.target, host, "Event and touch should have been retargeted."); + opener.is(e.touches.length, 1, "touches list should contain one touch object."); + opener.is(e.changedTouches.length, 1, "changedTouches list should contain one touch objects."); + } + + // Ensure retargeting doesn't happen inside shadow DOM. + var shadowHandled = false; + shadowS1.ontouchstart = function(e) { + shadowHandled = true; + opener.is(e.targetTouches.length, 1, "Should have only one entry in targetTouches."); + opener.is(e.targetTouches[0].target, e.target, "targetTouches should contain event.target."); + opener.is(e.target, shadowS1, "Event and touch should not have been retargeted."); + opener.is(e.touches.length, 1, "touches list should contain one touch object."); + opener.is(e.changedTouches.length, 1, "changedTouches list should contain one touch objects."); + } + synthesizeTouches([shadowS1]); + opener.ok(hostHandled, "Should have called listener on host."); + opener.ok(shadowHandled, "Should have called listener on shadow DOM element."); + host.ontouchstart = null; + shadowS1.ontouchstart = null; + + next(); + } + + function shadowDOMTest2() { + var shadowS1 = shadowRoot.getElementById("shadowSpan1"); + var shadowS2 = shadowRoot.getElementById("shadowSpan2"); + var s1 = document.getElementById("span1"); + + var hostHandled = false; + host.ontouchstart = function(e) { + opener.is(e.target, host, "Event.target should be the host element."); + hostHandled = true; + opener.is(e.targetTouches.length, 2, "Should have two entries in targetTouches."); + opener.is(e.targetTouches[0].target, e.target, "targetTouches should contain event.target."); + opener.is(e.targetTouches[1].target, e.target, "targetTouches should contain event.target twice."); + opener.is(e.touches.length, 3, "touches list should contain one touch object."); + opener.is(e.changedTouches.length, 3, "changedTouches list should contain one touch objects."); + } + + synthesizeTouches([shadowS1, shadowS2, s1]); + opener.ok(hostHandled, "Should have called listener on host."); + host.ontouchstart = null; + + next(); + } + + + function shadowDOMTest3() { + var shadowS1 = shadowRoot.getElementById("shadowSpan1"); + var shadowS2 = shadowRoot2.getElementById("shadowSpan2"); + var s1 = document.getElementById("span1"); + + var hostHandled = false; + host.ontouchstart = function(e) { + opener.is(e.target, host, "Event.target should be the host element."); + hostHandled = true; + opener.is(e.targetTouches.length, 1, "Should have one entry in targetTouches."); + opener.is(e.targetTouches[0].target, e.target, "targetTouches should contain event.target."); + opener.is(e.touches.length, 3, "touches list should contain one touch object."); + opener.is(e.touches[0].target, host, "Should have retargeted the first Touch object."); + opener.is(e.touches[1].target, host2, "Should have retargeted the second Touch object."); + opener.is(e.touches[3].target, s1, "Touch object targeted to light DOM should keep its target as is."); + opener.is(e.changedTouches.length, 3, "changedTouches list should contain one touch objects."); + } + + synthesizeTouches([shadowS1, shadowS2, s1]); + opener.ok(hostHandled, "Should have called listener on host."); + host.ontouchstart = null; + + next(); + } + + function jsDispatchedShadowTouchEvents() { + var s1 = document.getElementById("span1"); + var shadowS1 = shadowRoot.getElementById("shadowSpan1"); + var shadowS2 = shadowRoot.getElementById("shadowSpan2"); + var hostHandled = false; + var shadowHandled = false; + host.ontouchstart = function(e) { + hostHandled = true; + opener.is(e.targetTouches.length, 2, "Should have all the shadow entries in targetTouches."); + opener.is(e.targetTouches[0].target, host, "targetTouches shouldn't reveal shadow DOM."); + opener.is(e.targetTouches[1].target, host, "targetTouches shouldn't reveal shadow DOM."); + opener.is(e.touches.length, 3, "touches list should contain all the touch objects."); + opener.is(e.changedTouches.length, 3, "changedTouches list should contain all the touch objects."); + } + shadowS1.ontouchstart = function(e) { + shadowHandled = true; + opener.is(e.targetTouches.length, 3, "Should have all the in targetTouches."); + opener.is(e.targetTouches[0].target, shadowS1, "targetTouches should contain two shadow elements."); + opener.is(e.targetTouches[1].target, shadowS2, "targetTouches should contain two shadow elements."); + opener.is(e.targetTouches[2].target, s1, "targetTouches should contain a slight element."); + opener.is(e.touches.length, 3, "touches list should contain all the touch objects."); + opener.is(e.changedTouches.length, 3, "changedTouches list should contain all the touch objects."); + } + var touchArray = createTouchArray([shadowS1, shadowS2, s1]); + var touchEvent = new TouchEvent("touchstart", + { + composed: true, + touches: touchArray, + targetTouches: touchArray, + changedTouches: touchArray + }); + opener.is(touchEvent.targetTouches.length, 3, "Should have 3 entries in targetTouches"); + shadowS1.dispatchEvent(touchEvent); + opener.ok(hostHandled, "Should have called listener on host."); + opener.ok(shadowHandled, "Should have called listener on shadow DOM element."); + host.ontouchstart = null; + shadowS1.ontouchstart = null; + next(); + } + + function jsDispatchedShadowTouchEvents2() { + var s1 = document.getElementById("span1"); + var shadowS1 = shadowRoot.getElementById("shadowSpan1"); + var shadowS2 = shadowRoot2.getElementById("shadowSpan2"); + var hostHandled = false; + var shadowHandled = false; + host.ontouchstart = function(e) { + hostHandled = true; + opener.is(e.targetTouches.length, 1, "Should have one shadow entry in targetTouches."); + opener.is(e.targetTouches[0].target, host, "targetTouches shouldn't reveal shadow DOM."); + opener.is(e.touches.length, 3, "touches list should contain all the touch objects."); + opener.is(e.touches[0].target, host, "Should have retargeted the first Touch object."); + opener.is(e.touches[1].target, host2, "Should have retargeted the second Touch object."); + opener.is(e.touches[2].target, s1, "Touch object targeted to light DOM should keep its target as is."); + opener.is(e.changedTouches.length, 3, "changedTouches list should contain all the touch objects."); + } + shadowS1.ontouchstart = function(e) { + shadowHandled = true; + opener.is(e.targetTouches.length, 3, "Should have all the in targetTouches."); + opener.is(e.targetTouches[0].target, shadowS1, "targetTouches should contain two shadow elements."); + opener.is(e.targetTouches[1].target, shadowS2, "targetTouches should contain two shadow elements."); + opener.is(e.targetTouches[2].target, s1, "targetTouches should contain a slight element."); + opener.is(e.touches.length, 3, "touches list should contain all the touch objects."); + opener.is(e.changedTouches.length, 3, "changedTouches list should contain all the touch objects."); + } + var touchArray = createTouchArray([shadowS1, shadowS2, s1]); + var touchEvent = new TouchEvent("touchstart", + { + composed: true, + touches: touchArray, + targetTouches: touchArray, + changedTouches: touchArray + }); + opener.is(touchEvent.targetTouches.length, 3, "Should have 3 entries in targetTouches"); + shadowS1.dispatchEvent(touchEvent); + opener.ok(hostHandled, "Should have called listener on host."); + opener.ok(shadowHandled, "Should have called listener on shadow DOM element."); + host.ontouchstart = null; + shadowS1.ontouchstart = null; + next(); + } + + function start() { + winUtils = _getDOMWindowUtils(this); + host = document.getElementById("host"); + shadowRoot = host.attachShadow({ mode: "open" }); + shadowRoot.innerHTML = + "<span id='shadowSpan1'>shadowFoo </span><span id='shadowSpan2'>shadowBar </span><span id='shadowSpan3'>shadowBaz </span><slot></slot>"; + + host2 = document.getElementById("host2"); + shadowRoot2 = host2.attachShadow({ mode: "open" }); + shadowRoot2.innerHTML = + "<span id='shadowSpan1'>shadowFoo </span><span id='shadowSpan2'>shadowBar </span><span id='shadowSpan3'>shadowBaz </span><slot></slot>"; + next(); + } + </script> + <style> + </style> + </head> + <body onload="start();"> + <div id="nonshadow"> + <span id="span1">foo </span><span id="span2">bar </span><span id="span3"> baz</span> + <input type="date" id="date"> + </div> + <div id="host"><span id="assignedNode"> assigned node </span></div> + <div id="host2"><span id="assignedNode2"> assigned node 2 </span></div> + </body> +</html> diff --git a/dom/events/test/window_bug1447993.html b/dom/events/test/window_bug1447993.html new file mode 100644 index 0000000000..64bedf658c --- /dev/null +++ b/dom/events/test/window_bug1447993.html @@ -0,0 +1,239 @@ +<!DOCTYPE HTML> +<html> + <head> + <title>Test for Bug 1447993</title> + <style> + #area { + background: green; + border: 1px solid black; + width: 40px; + height: 40px; + } + + #target { + background: blue; + border: 1px solid black; + width: 20px; + height: 20px; + margin: 10px; + } + </style> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script> + + var tests = [ + topLevelDocumentEventHandling, + topLevelDocumentEventHandlingWithTouch, + iframeEventHandling, + ]; + + function next() { + if (!tests.length) { + opener.done(); + window.close(); + } else { + var test = tests.shift(); + requestAnimationFrame(function() { setTimeout(test); }); + } + } + + function start() { + next(); + } + + function topLevelDocumentEventHandling() { + var pid; + var area = document.getElementById("area"); + var target = document.getElementById("target"); + var body = document.body; + var html = document.documentElement; + var eventLog = []; + function captureEvent(e) { + eventLog.push([e.type, e.composedPath()]); + } + var expectedEvents = [ + ["pointerdown", [ target, area, body, html, document, window ]], + ["mousedown", [ target, area, body, html, document, window ]], + ["pointerup", [ area, body, html, document, window ]], + ["mouseup", [ area, body, html, document, window ]], + ["click", [ target, area, body, html, document, window ]], + ]; + + window.addEventListener("pointerdown", + function(e) { + captureEvent(e); + pid = e.pointerId; + area.setPointerCapture(pid); + }, { once: true}); + window.addEventListener("mousedown", + function(e) { + captureEvent(e); + }, { once: true}); + window.addEventListener("pointerup", + function(e) { + captureEvent(e); + area.releasePointerCapture(pid); + }, { once: true}); + window.addEventListener("mouseup", function(e) { + captureEvent(e); + }, { once: true}); + window.addEventListener("click", function(e) { + captureEvent(e); + }, { once: true}); + + synthesizeMouseAtCenter(target, {}, window); + + opener.is(eventLog.length, expectedEvents.length, + "[topLevelDocumentEventHandling] Same number events expected."); + for (var i = 0; i < eventLog.length; ++i) { + opener.is(eventLog[i][0], expectedEvents[i][0], + `topLevelDocumentEventHandling ${i}`); + for (var j = 0; j < eventLog[i][1].length; ++j) { + opener.is(eventLog[i][1][j], expectedEvents[i][1][j], + `topLevelDocumentEventHandling ${i} ${j}`); + } + } + next(); + } + + function topLevelDocumentEventHandlingWithTouch() { + var pid; + var area = document.getElementById("area"); + var target = document.getElementById("target"); + var body = document.body; + var html = document.documentElement; + var eventLog = []; + function captureEvent(e) { + eventLog.push([e.type, e.composedPath()]); + } + var expectedEvents = [ + ["pointerdown", [ target, area, body, html, document, window ]], + ["touchstart", [ target, area, body, html, document, window ]], + ["pointerup", [ area, body, html, document, window ]], + ["touchend", [ target, area, body, html, document, window ]], + /* + // Right now touch event initated mousedown/up (and then click) are + // dispatched in APZ, and there isn't JS exposed way to test that. + ["mousedown", [ target, area, body, html, document, window ]], + ["mouseup", [ target, area, body, html, document, window ]], + ["click", [ target, area, body, html, document, window ]],*/ + ]; + + window.addEventListener("pointerdown", + function(e) { + captureEvent(e); + pid = e.pointerId; + area.setPointerCapture(pid); + }, { once: true}); + window.addEventListener("touchstart", function(e) { + captureEvent(e); + }, { once: true}); + window.addEventListener("pointerup", + function(e) { + captureEvent(e); + try { + area.releasePointerCapture(pid); + } catch(ex) {} + }, { once: true}); + window.addEventListener("touchend", function(e) { + captureEvent(e); + }, { once: true}); + /*window.addEventListener("mousedown", + function(e) { + captureEvent(e); + }, { once: true}); + window.addEventListener("mouseup", function(e) { + captureEvent(e); + }, { once: true}); + window.addEventListener("click", function(e) { + captureEvent(e); + }, { once: true});*/ + + synthesizeTouchAtCenter(target, {}, window); + + opener.is(eventLog.length, expectedEvents.length, + "[topLevelDocumentEventHandlingWithTouch] Same number events expected."); + for (var i = 0; i < eventLog.length; ++i) { + opener.is(eventLog[i][0], expectedEvents[i][0], + `topLevelDocumentEventHandlingWithTouch ${i}`); + for (var j = 0; j < eventLog[i][1].length; ++j) { + opener.is(eventLog[i][1][j], expectedEvents[i][1][j], + `topLevelDocumentEventHandlingWithTouch ${i} ${j}`); + } + } + next(); + } + + function iframeEventHandling() { + var pid; + var iframe = document.getElementById("iframe"); + var doc = iframe.contentDocument; + doc.head.innerHTML = "<style>" + document.getElementsByTagName("style")[0].textContent + "</style>"; + var area = doc.createElement("div"); + area.id = "area"; + var target = doc.createElement("div"); + target.id = "target"; + area.appendChild(target); + doc.body.appendChild(area); + var body = doc.body; + var html = doc.documentElement; + var win = doc.defaultView; + var eventLog = []; + function captureEvent(e) { + eventLog.push([e.type, e.composedPath()]); + } + var expectedEvents = [ + ["pointerdown", [ target, area, body, html, doc, win ]], + ["mousedown", [ target, area, body, html, doc, win ]], + ["pointerup", [ area, body, html, doc, win ]], + ["mouseup", [ area, body, html, doc, win ]], + ["click", [ target, area, body, html, doc, win ]], + ]; + + win.addEventListener("pointerdown", + function(e) { + captureEvent(e); + pid = e.pointerId; + area.setPointerCapture(pid); + }, { once: true}); + win.addEventListener("mousedown", + function(e) { + captureEvent(e); + }, { once: true}); + win.addEventListener("pointerup", + function(e) { + captureEvent(e); + area.releasePointerCapture(pid); + }, { once: true}); + win.addEventListener("mouseup", function(e) { + captureEvent(e); + }, { once: true}); + win.addEventListener("click", function(e) { + captureEvent(e); + }, { once: true}); + + synthesizeMouseAtCenter(target, {}, win); + + opener.is(eventLog.length, expectedEvents.length, + "[iframeEventHandling] Same number events expected."); + for (var i = 0; i < eventLog.length; ++i) { + opener.is(eventLog[i][0], expectedEvents[i][0], + `iframeEventHandling ${i}`); + for (var j = 0; j < eventLog[i][1].length; ++j) { + opener.is(eventLog[i][1][j], expectedEvents[i][1][j], + `iframeEventHandling ${i} ${j}`); + } + } + next(); + } + + </script> + </head> + <body onload="start();"> + <div id="area"> + <div id="target"></div> + </div> + <iframe id="iframe"></iframe> + <h5 id="targetOutsideIframe"></h5> + </body> +</html> diff --git a/dom/events/test/window_bug493251.html b/dom/events/test/window_bug493251.html new file mode 100644 index 0000000000..d2891a9ef2 --- /dev/null +++ b/dom/events/test/window_bug493251.html @@ -0,0 +1,13 @@ +<!DOCTYPE html> +<html> +<head> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script> + SimpleTest.waitForExplicitFinish(); + SimpleTest.waitForFocus(window.opener.doTest, window); + </script> +</head> +<body> + <input> +</body> +</html> diff --git a/dom/events/test/window_bug617528.xhtml b/dom/events/test/window_bug617528.xhtml new file mode 100644 index 0000000000..fce6f510b2 --- /dev/null +++ b/dom/events/test/window_bug617528.xhtml @@ -0,0 +1,9 @@ +<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + width="640" height="480"> + + <browser id="browser" type="content" primary="true" flex="1" src="about:blank" + disablehistory="true"/> + +</window> diff --git a/dom/events/test/window_bug659071.html b/dom/events/test/window_bug659071.html new file mode 100644 index 0000000000..bff1bd22a6 --- /dev/null +++ b/dom/events/test/window_bug659071.html @@ -0,0 +1,72 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for Bug 659071</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<video id="v" controls></video> +<script type="application/javascript"> + +SimpleTest.waitForFocus(startTests, window); +SimpleTest.requestFlakyTimeout("untriaged"); + +function is() +{ + window.opener.is.apply(window.opener, arguments); +} + +function isnot() +{ + window.opener.isnot.apply(window.opener, arguments); +} + +function hitEventLoop(aFunc, aTimes) +{ + if (--aTimes) { + setTimeout(hitEventLoop, 0, aFunc, aTimes); + } else { + setTimeout(aFunc, 20); + } +} + +function startTests() { + SpecialPowers.pushPrefEnv( + {"set": [["mousewheel.with_control.action", 3], + ["test.events.async.enabled", true]]}, + runTests); +} + +function runTests() +{ + synthesizeKey("0", { accelKey: true }); + + var video = document.getElementById("v"); + hitEventLoop(function () { + is(SpecialPowers.getFullZoom(window), 1.0, + "failed to reset zoom"); + synthesizeWheel(video, 10, 10, + { deltaMode: WheelEvent.DOM_DELTA_LINE, ctrlKey: true, + deltaX: 0, deltaY: 1.0, lineOrPageDeltaX: 0, lineOrPageDeltaY: 1 }); + hitEventLoop(function () { + isnot(SpecialPowers.getFullZoom(window), 1.0, + "failed to zoom by ctrl+wheel"); + + synthesizeWheel(video, 10, 10, + { deltaMode: WheelEvent.DOM_DELTA_LINE, ctrlKey: true, + deltaX: 0, deltaY: 1.0, lineOrPageDeltaX: 0, lineOrPageDeltaY: -1 }); + hitEventLoop(function () { + is(SpecialPowers.getFullZoom(window), 1.0, + "failed to reset zoom"); + + hitEventLoop(window.opener.finish, 20); + }, 20); + }, 20); + }, 20); +} + +</script> +</body> +</html> diff --git a/dom/events/test/window_empty_document.html b/dom/events/test/window_empty_document.html new file mode 100644 index 0000000000..bc90e91eab --- /dev/null +++ b/dom/events/test/window_empty_document.html @@ -0,0 +1,7 @@ +<!DOCTYPE html> +<html> +<head> +<title>Empty document to test something in new window</title> +</head> +<body onload="opener.doTests(window);"></body> +</html> diff --git a/dom/events/test/window_wheel_default_action.html b/dom/events/test/window_wheel_default_action.html new file mode 100644 index 0000000000..31916ad396 --- /dev/null +++ b/dom/events/test/window_wheel_default_action.html @@ -0,0 +1,3621 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for default action of WheelEvent</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script src="/tests/SimpleTest/paint_listener.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<p id="display"></p> +<div id="scrollable" style="overflow: auto; width: 200px; height: 200px;"> + <div id="clipper" style="margin: 0; padding: 0; overflow: hidden; width: 3000px; height: 3000px;"> + <div id="scrolled" style="width: 5000px; height: 5000px;"> + Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text.<br> + Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text.<br> + Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text.<br> + Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text.<br> + Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text.<br> + Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text.<br> + Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text.<br> + Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text.<br> + Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text.<br> + Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text.<br> + Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text.<br> + Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text.<br> + Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text.<br> + Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text.<br> + Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text.<br> + Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text.<br> + Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text.<br> + Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text.<br> + Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text.<br> + Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text.<br> + Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text.<br> + Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text.<br> + Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text.<br> + Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text.<br> + Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text.<br> + Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text. Tere is a lot of text.<br> + </div> + </div> +</div> +<div id="spacerForBody"></div> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> + +// Until the first non-blank paint, the parent will set the opacity of our +// browser to 0 using the 'blank' attribute. +// Until the blank attribute is removed, we can't send scroll events. +SimpleTest.waitForFocus(function() { + let chromeScript = SpecialPowers.loadChromeScript(_ => { + /* eslint-env mozilla/chrome-script */ + let win = Services.wm.getMostRecentWindow("navigator:browser"); + win.requestAnimationFrame(() => { + win.gBrowser.selectedBrowser.removeAttribute("blank"); + win.requestAnimationFrame(() => { + sendAsyncMessage("blank-attribute-removed"); + }); + }); + }); + chromeScript.promiseOneMessage("blank-attribute-removed").then(() => { + chromeScript.destroy(); + runTests(); + }); +}, window); + +SimpleTest.requestLongerTimeout(6); +SimpleTest.requestFlakyTimeout("untriaged"); + +var winUtils = SpecialPowers.getDOMWindowUtils(window); +// grab refresh driver +winUtils.advanceTimeAndRefresh(100); + +var gScrollableElement = document.getElementById("scrollable"); +var gScrolledElement = document.getElementById("scrolled"); +var gSpacerForBodyElement = document.getElementById("spacerForBody"); + +const kDefaultActionNone = 0; +const kDefaultActionScroll = 1; +const kDefaultActionHistory = 2; +const kDefaultActionZoom = 3; +const kDefaultActionHorizontalizedScroll = 4; + +const kDefaultActionOverrideXNoOverride = -1; +const kDefaultActionOverrideXNone = kDefaultActionNone; +const kDefaultActionOverrideXScroll = kDefaultActionScroll; +const kDefaultActionOverrideXHistory = kDefaultActionHistory; +const kDefaultActionOverrideXZoom = kDefaultActionZoom; + +// We sometimes rely on the scroll position not getting preserved across tests. +function resetScrollPosition(element) { + element.style.display = "none"; + element.offsetTop; + element.style.display = ""; +} + +function is() +{ + window.opener.is.apply(window.opener, arguments); +} + +function ok() +{ + window.opener.ok.apply(window.opener, arguments); +} + +function sendWheelAndWait(aX, aY, aEvent, aCallback) +{ + sendWheelAndPaint(gScrollableElement, aX, aY, aEvent, aCallback); +} + +function hitEventLoop(aFunc, aTimes) +{ + winUtils.advanceTimeAndRefresh(100); + + if (--aTimes) { + setTimeout(hitEventLoop, 0, aFunc, aTimes); + } else { + setTimeout(aFunc, 20); + } +} + +function onZoomReset(aCallback) { + var fullZoom = SpecialPowers.getFullZoom(window); + if (fullZoom == 1) { + SimpleTest.executeSoon(aCallback); + return; + } + // Zoom causes a resize of the viewport. + window.addEventListener("resize", function onResize(event) { + is(SpecialPowers.getFullZoom(window), 1, "Zoom should be reset to 1"); + window.removeEventListener("resize", onResize); + SimpleTest.executeSoon(aCallback); + }); +} + +function setDeltaMultiplierSettings(aSettings, aCallback) +{ + SpecialPowers.pushPrefEnv({"set": [ + ["mousewheel.default.delta_multiplier_x", aSettings.deltaMultiplierX * 100], + ["mousewheel.default.delta_multiplier_y", aSettings.deltaMultiplierY * 100], + ["mousewheel.default.delta_multiplier_z", aSettings.deltaMultiplierZ * 100], + ["mousewheel.with_alt.delta_multiplier_x", aSettings.deltaMultiplierX * 100], + ["mousewheel.with_alt.delta_multiplier_y", aSettings.deltaMultiplierY * 100], + ["mousewheel.with_alt.delta_multiplier_z", aSettings.deltaMultiplierZ * 100], + ["mousewheel.with_control.delta_multiplier_x", aSettings.deltaMultiplierX * 100], + ["mousewheel.with_control.delta_multiplier_y", aSettings.deltaMultiplierY * 100], + ["mousewheel.with_control.delta_multiplier_z", aSettings.deltaMultiplierZ * 100], + ["mousewheel.with_meta.delta_multiplier_x", aSettings.deltaMultiplierX * 100], + ["mousewheel.with_meta.delta_multiplier_y", aSettings.deltaMultiplierY * 100], + ["mousewheel.with_meta.delta_multiplier_z", aSettings.deltaMultiplierZ * 100], + ["mousewheel.with_shift.delta_multiplier_x", aSettings.deltaMultiplierX * 100], + ["mousewheel.with_shift.delta_multiplier_y", aSettings.deltaMultiplierY * 100], + ["mousewheel.with_shift.delta_multiplier_z", aSettings.deltaMultiplierZ * 100], + ["mousewheel.with_win.delta_multiplier_x", aSettings.deltaMultiplierX * 100], + ["mousewheel.with_win.delta_multiplier_y", aSettings.deltaMultiplierY * 100], + ["mousewheel.with_win.delta_multiplier_z", aSettings.deltaMultiplierZ * 100] + ]}, aCallback); +} + +function doTestScroll(aSettings, aCallback) +{ + const kNoScroll = 0x00; + const kScrollUp = 0x01; + const kScrollDown = 0x02; + const kScrollLeft = 0x04; + const kScrollRight = 0x08; + + const kTests = [ + { description: "Scroll to bottom by pixel scroll even if lineOrPageDelta is 0", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 0.0, deltaY: 8.0, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + expected: kScrollDown }, + { description: "Scroll to bottom by pixel scroll when lineOrPageDelta is 1", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 0.0, deltaY: 8.0, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 1, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + expected: kScrollDown }, + { description: "Scroll to top by pixel scroll even if lineOrPageDelta is 0", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 0.0, deltaY: -8.0, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + expected: kScrollUp }, + { description: "Scroll to top by pixel scroll when lineOrPageDelta is -1", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 0.0, deltaY: -8.0, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: -1, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + expected: kScrollUp }, + { description: "Scroll to right by pixel scroll even if lineOrPageDelta is 0", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 8.0, deltaY: 0.0, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + expected: kScrollRight }, + { description: "Scroll to right by pixel scroll when lineOrPageDelta is 1", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 8.0, deltaY: 0.0, deltaZ: 0.0, + lineOrPageDeltaX: 1, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + expected: kScrollRight }, + { description: "Scroll to left by pixel scroll even if lineOrPageDelta is 0", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: -8.0, deltaY: 0.0, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + expected: kScrollLeft }, + { description: "Scroll to left by pixel scroll when lineOrPageDelta is -1", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: -8.0, deltaY: 0.0, deltaZ: 0.0, + lineOrPageDeltaX: -1, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + expected: kScrollLeft }, + { description: "Scroll to bottom-right by pixel scroll", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 8.0, deltaY: 8.0, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + expected: kScrollDown | kScrollRight }, + { description: "Scroll to bottom-left by pixel scroll", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: -8.0, deltaY: 8.0, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + expected: kScrollDown | kScrollLeft }, + { description: "Scroll to top-left by pixel scroll", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: -8.0, deltaY: -8.0, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + expected: kScrollUp | kScrollLeft }, + { description: "Scroll to top-right by pixel scroll", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 8.0, deltaY: -8.0, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + expected: kScrollUp | kScrollRight }, + { description: "Not Scroll by pixel scroll for z", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 0.0, deltaY: 0.0, deltaZ: 1.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + expected: kNoScroll }, + + { description: "Scroll to bottom by line scroll even if lineOrPageDelta is 0", + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: 0.0, deltaY: 0.5, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 1, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + expected: kScrollDown }, + { description: "Scroll to bottom by line scroll when lineOrPageDelta is 1", + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: 0.0, deltaY: 0.5, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 1, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + expected: kScrollDown }, + { description: "Scroll to top by line scroll even if lineOrPageDelta is 0", + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: 0.0, deltaY: -0.5, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + expected: kScrollUp }, + { description: "Scroll to top by line scroll when lineOrPageDelta is -1", + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: 0.0, deltaY: -0.5, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: -1, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + expected: kScrollUp }, + { description: "Scroll to right by line scroll even if lineOrPageDelta is 0", + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: 0.5, deltaY: 0.0, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + expected: kScrollRight }, + { description: "Scroll to right by line scroll when lineOrPageDelta is 1", + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: 0.5, deltaY: 0.0, deltaZ: 0.0, + lineOrPageDeltaX: 1, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + expected: kScrollRight }, + { description: "Scroll to left by line scroll even if lineOrPageDelta is 0", + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: -0.5, deltaY: 0.0, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + expected: kScrollLeft }, + { description: "Scroll to left by line scroll when lineOrPageDelta is -1", + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: -0.5, deltaY: 0.0, deltaZ: 0.0, + lineOrPageDeltaX: -1, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + expected: kScrollLeft }, + { description: "Scroll to bottom-right by line scroll", + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: 0.5, deltaY: 0.5, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + expected: kScrollDown | kScrollRight }, + { description: "Scroll to bottom-left by line scroll", + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: -0.5, deltaY: 0.5, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + expected: kScrollDown | kScrollLeft }, + { description: "Scroll to top-left by line scroll", + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: -0.5, deltaY: -0.5, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + expected: kScrollUp | kScrollLeft }, + { description: "Scroll to top-right by line scroll", + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: 0.5, deltaY: -0.5, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + expected: kScrollUp | kScrollRight }, + { description: "Not Scroll by line scroll for z", + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: 0.0, deltaY: 0.0, deltaZ: 1.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + expected: kNoScroll }, + + { description: "Scroll to bottom by page scroll even if lineOrPageDelta is 0", + event: { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: 0.0, deltaY: 0.5, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 1, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + expected: kScrollDown }, + { description: "Scroll to bottom by page scroll when lineOrPageDelta is 1", + event: { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: 0.0, deltaY: 0.5, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 1, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + expected: kScrollDown }, + { description: "Scroll to top by page scroll even if lineOrPageDelta is 0", + event: { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: 0.0, deltaY: -0.5, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + expected: kScrollUp }, + { description: "Scroll to top by page scroll when lineOrPageDelta is -1", + event: { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: 0.0, deltaY: -0.5, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: -1, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + expected: kScrollUp }, + { description: "Scroll to right by page scroll even if lineOrPageDelta is 0", + event: { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: 0.5, deltaY: 0.0, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + expected: kScrollRight }, + { description: "Scroll to right by page scroll when lineOrPageDelta is 1", + event: { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: 0.5, deltaY: 0.0, deltaZ: 0.0, + lineOrPageDeltaX: 1, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + expected: kScrollRight }, + { description: "Scroll to left by page scroll even if lineOrPageDelta is 0", + event: { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: -0.5, deltaY: 0.0, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + expected: kScrollLeft }, + { description: "Scroll to left by page scroll when lineOrPageDelta is -1", + event: { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: -0.5, deltaY: 0.0, deltaZ: 0.0, + lineOrPageDeltaX: -1, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + expected: kScrollLeft }, + { description: "Scroll to bottom-right by page scroll", + event: { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: 0.5, deltaY: 0.5, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + expected: kScrollDown | kScrollRight }, + { description: "Scroll to bottom-left by page scroll", + event: { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: -0.5, deltaY: 0.5, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + expected: kScrollDown | kScrollLeft }, + { description: "Scroll to top-left by page scroll", + event: { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: -0.5, deltaY: -0.5, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + expected: kScrollUp | kScrollLeft }, + { description: "Scroll to top-right by page scroll", + event: { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: 0.5, deltaY: -0.5, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + expected: kScrollUp | kScrollRight }, + { description: "Not Scroll by page scroll for z", + event: { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: 0.0, deltaY: 0.0, deltaZ: 1.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + expected: kNoScroll }, + + // special cases. + + // momentum scroll should cause scroll even if the action is zoom, but if the default action is none, + // shouldn't do it. + { description: "Scroll to bottom by momentum pixel scroll when lineOrPageDelta is 0, even if the action is zoom", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 0.0, deltaY: 8.0, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: true, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: true, altKey: false, metaKey: false, osKey: false }, + expected: kScrollDown }, + { description: "Scroll to bottom by momentum pixel scroll when lineOrPageDelta is 1, even if the action is zoom", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 0.0, deltaY: 8.0, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 1, isMomentum: true, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: true, altKey: false, metaKey: false, osKey: false }, + expected: kScrollDown }, + { description: "Scroll to top by momentum pixel scroll when lineOrPageDelta is 0, even if the action is zoom", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 0.0, deltaY: -8.0, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: true, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: true, altKey: false, metaKey: false, osKey: false }, + expected: kScrollUp }, + { description: "Scroll to top by momentum pixel scroll when lineOrPageDelta is -1, even if the action is zoom", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 0.0, deltaY: -8.0, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: -1, isMomentum: true, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: true, altKey: false, metaKey: false, osKey: false }, + expected: kScrollUp }, + { description: "Scroll to right by momentum pixel scroll when lineOrPageDelta is 0, even if the action is zoom", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 8.0, deltaY: 0.0, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: true, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: true, altKey: false, metaKey: false, osKey: false }, + expected: kScrollRight }, + { description: "Scroll to right by momentum pixel scroll when lineOrPageDelta is 1, even if the action is zoom", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 8.0, deltaY: 0.0, deltaZ: 0.0, + lineOrPageDeltaX: 1, lineOrPageDeltaY: 0, isMomentum: true, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: true, altKey: false, metaKey: false, osKey: false }, + expected: kScrollRight }, + { description: "Scroll to left by momentum pixel scroll when lineOrPageDelta is 0, even if the action is zoom", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: -8.0, deltaY: 0.0, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: true, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: true, altKey: false, metaKey: false, osKey: false }, + expected: kScrollLeft }, + { description: "Scroll to left by momentum pixel scroll when lineOrPageDelta is -1, even if the action is zoom", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: -8.0, deltaY: 0.0, deltaZ: 0.0, + lineOrPageDeltaX: -1, lineOrPageDeltaY: 0, isMomentum: true, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: true, altKey: false, metaKey: false, osKey: false }, + expected: kScrollLeft }, + { description: "Scroll to bottom-right by momentum pixel scroll even if the action is zoom", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 8.0, deltaY: 8.0, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: true, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: true, altKey: false, metaKey: false, osKey: false }, + expected: kScrollDown | kScrollRight }, + { description: "Scroll to bottom-left by momentum pixel scroll even if the action is zoom", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: -8.0, deltaY: 8.0, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: true, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: true, altKey: false, metaKey: false, osKey: false }, + expected: kScrollDown | kScrollLeft }, + { description: "Scroll to top-left by momentum pixel scroll even if the action is zoom", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: -8.0, deltaY: -8.0, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: true, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: true, altKey: false, metaKey: false, osKey: false }, + expected: kScrollUp | kScrollLeft }, + { description: "Scroll to top-right by momentum pixel scroll even if the action is zoom", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 8.0, deltaY: -8.0, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: true, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: true, altKey: false, metaKey: false, osKey: false }, + expected: kScrollUp | kScrollRight }, + { description: "Not Scroll by momentum pixel scroll for z (action is zoom)", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 0.0, deltaY: 0.0, deltaZ: 1.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: true, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: true, altKey: false, metaKey: false, osKey: false }, + expected: kNoScroll }, + { description: "Not Scroll by momentum pixel scroll if default action is none (action is zoom)", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 0.0, deltaY: 0.0, deltaZ: 1.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: true, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: true, altKey: false, metaKey: false, osKey: false }, + expected: kNoScroll, + prepare (cb) { SpecialPowers.pushPrefEnv({"set": [["mousewheel.default.action", 0]]}, cb); }, + cleanup (cb) { SpecialPowers.pushPrefEnv({"set": [["mousewheel.default.action", 1]]}, cb); } }, + + // momentum scroll should cause scroll even if the action is history, but if the default action is none, + // shouldn't do it. + { description: "Scroll to bottom by momentum pixel scroll when lineOrPageDelta is 0, even if the action is history", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 0.0, deltaY: 8.0, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: true, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: true, metaKey: false, osKey: false }, + expected: kScrollDown }, + { description: "Scroll to bottom by momentum pixel scroll when lineOrPageDelta is 1, even if the action is history", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 0.0, deltaY: 8.0, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 1, isMomentum: true, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: true, metaKey: false, osKey: false }, + expected: kScrollDown }, + { description: "Scroll to top by momentum pixel scroll when lineOrPageDelta is 0, even if the action is history", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 0.0, deltaY: -8.0, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: true, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: true, metaKey: false, osKey: false }, + expected: kScrollUp }, + { description: "Scroll to top by momentum pixel scroll when lineOrPageDelta is -1, even if the action is history", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 0.0, deltaY: -8.0, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: -1, isMomentum: true, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: true, metaKey: false, osKey: false }, + expected: kScrollUp }, + { description: "Scroll to right by momentum pixel scroll when lineOrPageDelta is 0, even if the action is history", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 8.0, deltaY: 0.0, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: true, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: true, metaKey: false, osKey: false }, + expected: kScrollRight }, + { description: "Scroll to right by momentum pixel scroll when lineOrPageDelta is 1, even if the action is history", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 8.0, deltaY: 0.0, deltaZ: 0.0, + lineOrPageDeltaX: 1, lineOrPageDeltaY: 0, isMomentum: true, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: true, metaKey: false, osKey: false }, + expected: kScrollRight }, + { description: "Scroll to left by momentum pixel scroll when lineOrPageDelta is 0, even if the action is history", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: -8.0, deltaY: 0.0, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: true, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: true, metaKey: false, osKey: false }, + expected: kScrollLeft }, + { description: "Scroll to left by momentum pixel scroll when lineOrPageDelta is -1, even if the action is history", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: -8.0, deltaY: 0.0, deltaZ: 0.0, + lineOrPageDeltaX: -1, lineOrPageDeltaY: 0, isMomentum: true, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: true, metaKey: false, osKey: false }, + expected: kScrollLeft }, + { description: "Scroll to bottom-right by momentum pixel scroll even if the action is history", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 8.0, deltaY: 8.0, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: true, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: true, metaKey: false, osKey: false }, + expected: kScrollDown | kScrollRight }, + { description: "Scroll to bottom-left by momentum pixel scroll even if the action is history", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: -8.0, deltaY: 8.0, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: true, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: true, metaKey: false, osKey: false }, + expected: kScrollDown | kScrollLeft }, + { description: "Scroll to top-left by momentum pixel scroll even if the action is history", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: -8.0, deltaY: -8.0, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: true, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: true, metaKey: false, osKey: false }, + expected: kScrollUp | kScrollLeft }, + { description: "Scroll to top-right by momentum pixel scroll even if the action is history", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 8.0, deltaY: -8.0, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: true, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: true, metaKey: false, osKey: false }, + expected: kScrollUp | kScrollRight }, + { description: "Not Scroll by momentum pixel scroll for z (action is history)", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 0.0, deltaY: 0.0, deltaZ: 1.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: true, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: true, metaKey: false, osKey: false }, + expected: kNoScroll }, + { description: "Not Scroll by momentum pixel scroll if default action is none (action is history)", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 0.0, deltaY: 0.0, deltaZ: 1.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: true, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: true, metaKey: false, osKey: false }, + expected: kNoScroll, + prepare (cb) { SpecialPowers.pushPrefEnv({"set": [["mousewheel.default.action", 0]]}, cb); }, + cleanup (cb) { SpecialPowers.pushPrefEnv({"set": [["mousewheel.default.action", 1]]}, cb); } }, + + // Don't scroll along axis whose overflow style is hidden. + { description: "Scroll to only bottom by oblique pixel wheel event with overflow-x: hidden", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 16.0, deltaY: 16.0, deltaZ: 0.0, + lineOrPageDeltaX: 1, lineOrPageDeltaY: 1, + expectedOverflowDeltaX: 1, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + expected: kScrollDown, + prepare(cb) { gScrollableElement.style.overflowX = "hidden"; cb(); } }, + { description: "Scroll to only bottom by oblique line wheel event with overflow-x: hidden", + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: 1.0, deltaY: 1.0, deltaZ: 0.0, + lineOrPageDeltaX: 1, lineOrPageDeltaY: 1, + expectedOverflowDeltaX: 1, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + expected: kScrollDown }, + { description: "Scroll to only bottom by oblique page wheel event with overflow-x: hidden", + event: { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: 1.0, deltaY: 1.0, deltaZ: 0.0, + lineOrPageDeltaX: 1, lineOrPageDeltaY: 1, + expectedOverflowDeltaX: 1, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + expected: kScrollDown }, + { description: "Scroll to only top by oblique pixel wheel event with overflow-x: hidden", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: -16.0, deltaY: -16.0, deltaZ: 0.0, + lineOrPageDeltaX: -1, lineOrPageDeltaY: -1, + expectedOverflowDeltaX: -1, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + expected: kScrollUp }, + { description: "Scroll to only top by oblique line wheel event with overflow-x: hidden", + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: -1.0, deltaY: -1.0, deltaZ: 0.0, + lineOrPageDeltaX: -1, lineOrPageDeltaY: -1, + expectedOverflowDeltaX: -1, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + expected: kScrollUp }, + { description: "Scroll to only top by oblique page wheel event with overflow-x: hidden", + event: { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: -1.0, deltaY: -1.0, deltaZ: 0.0, + lineOrPageDeltaX: -1, lineOrPageDeltaY: -1, + expectedOverflowDeltaX: -1, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + expected: kScrollUp, + cleanup (cb) { gScrollableElement.style.overflowX = "auto"; cb(); } }, + { description: "Scroll to only right by oblique pixel wheel event with overflow-y: hidden", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 16.0, deltaY: 16.0, deltaZ: 0.0, + lineOrPageDeltaX: 1, lineOrPageDeltaY: 1, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 1, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + expected: kScrollRight, + prepare(cb) { gScrollableElement.style.overflowY = "hidden"; cb(); } }, + { description: "Scroll to only right by oblique line wheel event with overflow-y: hidden", + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: 1.0, deltaY: 1.0, deltaZ: 0.0, + lineOrPageDeltaX: 1, lineOrPageDeltaY: 1, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 1, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + expected: kScrollRight }, + { description: "Scroll to only right by oblique page wheel event with overflow-y: hidden", + event: { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: 1.0, deltaY: 1.0, deltaZ: 0.0, + lineOrPageDeltaX: 1, lineOrPageDeltaY: 1, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 1, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + expected: kScrollRight }, + { description: "Scroll to only left by oblique pixel wheel event with overflow-y: hidden", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: -16.0, deltaY: -16.0, deltaZ: 0.0, + lineOrPageDeltaX: -1, lineOrPageDeltaY: -1, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: -1, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + expected: kScrollLeft }, + { description: "Scroll to only top by oblique line wheel event with overflow-y: hidden", + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: -1.0, deltaY: -1.0, deltaZ: 0.0, + lineOrPageDeltaX: -1, lineOrPageDeltaY: -1, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: -1, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + expected: kScrollLeft }, + { description: "Scroll to only top by oblique page wheel event with overflow-y: hidden", + event: { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: -1.0, deltaY: -1.0, deltaZ: 0.0, + lineOrPageDeltaX: -1, lineOrPageDeltaY: -1, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: -1, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + expected: kScrollLeft, + cleanup (cb) { gScrollableElement.style.overflowY = "auto"; cb(); } }, + { description: "Don't be scrolled by oblique pixel wheel event with overflow: hidden", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 16.0, deltaY: 16.0, deltaZ: 0.0, + lineOrPageDeltaX: 1, lineOrPageDeltaY: 1, + expectedOverflowDeltaX: 1, expectedOverflowDeltaY: 1, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + expected: kNoScroll, + prepare(cb) { gScrollableElement.style.overflow = "hidden"; cb(); } }, + { description: "Don't be scrolled by oblique line wheel event with overflow: hidden", + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: 1.0, deltaY: 1.0, deltaZ: 0.0, + lineOrPageDeltaX: 1, lineOrPageDeltaY: 1, + expectedOverflowDeltaX: 1, expectedOverflowDeltaY: 1, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + expected: kNoScroll }, + { description: "Don't be scrolled by oblique page wheel event with overflow: hidden", + event: { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: 1.0, deltaY: 1.0, deltaZ: 0.0, + lineOrPageDeltaX: 1, lineOrPageDeltaY: 1, + expectedOverflowDeltaX: 1, expectedOverflowDeltaY: 1, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + expected: kNoScroll }, + { description: "Don't be scrolled by oblique pixel wheel event with overflow: hidden", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: -16.0, deltaY: -16.0, deltaZ: 0.0, + lineOrPageDeltaX: -1, lineOrPageDeltaY: -1, + expectedOverflowDeltaX: -1, expectedOverflowDeltaY: -1, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + expected: kNoScroll }, + { description: "Don't be scrolled by oblique line wheel event with overflow: hidden", + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: -1.0, deltaY: -1.0, deltaZ: 0.0, + lineOrPageDeltaX: -1, lineOrPageDeltaY: -1, + expectedOverflowDeltaX: -1, expectedOverflowDeltaY: -1, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + expected: kNoScroll }, + { description: "Don't be scrolled by oblique page wheel event with overflow: hidden", + event: { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: -1.0, deltaY: -1.0, deltaZ: 0.0, + lineOrPageDeltaX: -1, lineOrPageDeltaY: -1, + expectedOverflowDeltaX: -1, expectedOverflowDeltaY: -1, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + expected: kNoScroll, + cleanup (cb) { gScrollableElement.style.overflow = "auto"; cb(); } }, + + // Don't scroll along axis whose overflow style is hidden and overflow delta values should + // be zero if there is ancestor scrollable element. + { description: "Scroll to only bottom by oblique pixel wheel event with overflow-x: hidden (body is scrollable)", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 16.0, deltaY: 16.0, deltaZ: 0.0, + lineOrPageDeltaX: 1, lineOrPageDeltaY: 1, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + expected: kScrollDown, + prepare(cb) { + gScrollableElement.style.overflowX = "hidden"; + gScrollableElement.style.position = "fixed"; + gScrollableElement.style.top = "30px"; + gScrollableElement.style.left = "30px"; + // Make body element scrollable. + gSpacerForBodyElement.style.width = "5000px"; + gSpacerForBodyElement.style.height = "5000px"; + document.documentElement.scrollTop = 500; + document.documentElement.scrollLeft = 500; + cb(); + } }, + { description: "Scroll to only bottom by oblique line wheel event with overflow-x: hidden (body is scrollable)", + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: 1.0, deltaY: 1.0, deltaZ: 0.0, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + expected: kScrollDown }, + { description: "Scroll to only bottom by oblique page wheel event with overflow-x: hidden (body is scrollable)", + event: { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: 1.0, deltaY: 1.0, deltaZ: 0.0, + lineOrPageDeltaX: 1, lineOrPageDeltaY: 1, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + expected: kScrollDown }, + { description: "Scroll to only top by oblique pixel wheel event with overflow-x: hidden (body is scrollable)", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: -16.0, deltaY: -16.0, deltaZ: 0.0, + lineOrPageDeltaX: -1, lineOrPageDeltaY: -1, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + expected: kScrollUp }, + { description: "Scroll to only top by oblique line wheel event with overflow-x: hidden (body is scrollable)", + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: -1.0, deltaY: -1.0, deltaZ: 0.0, + lineOrPageDeltaX: -1, lineOrPageDeltaY: -1, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + expected: kScrollUp }, + { description: "Scroll to only top by oblique page wheel event with overflow-x: hidden (body is scrollable)", + event: { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: -1.0, deltaY: -1.0, deltaZ: 0.0, + lineOrPageDeltaX: -1, lineOrPageDeltaY: -1, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + expected: kScrollUp, + cleanup (cb) { gScrollableElement.style.overflowX = "auto"; cb(); } }, + { description: "Scroll to only right by oblique pixel wheel event with overflow-y: hidden (body is scrollable)", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 16.0, deltaY: 16.0, deltaZ: 0.0, + lineOrPageDeltaX: 1, lineOrPageDeltaY: 1, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + expected: kScrollRight, + prepare(cb) { gScrollableElement.style.overflowY = "hidden"; cb(); } }, + { description: "Scroll to only right by oblique line wheel event with overflow-y: hidden (body is scrollable)", + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: 1.0, deltaY: 1.0, deltaZ: 0.0, + lineOrPageDeltaX: 1, lineOrPageDeltaY: 1, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + expected: kScrollRight }, + { description: "Scroll to only right by oblique page wheel event with overflow-y: hidden (body is scrollable)", + event: { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: 1.0, deltaY: 1.0, deltaZ: 0.0, + lineOrPageDeltaX: 1, lineOrPageDeltaY: 1, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + expected: kScrollRight }, + { description: "Scroll to only left by oblique pixel wheel event with overflow-y: hidden (body is scrollable)", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: -16.0, deltaY: -16.0, deltaZ: 0.0, + lineOrPageDeltaX: -1, lineOrPageDeltaY: -1, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + expected: kScrollLeft }, + { description: "Scroll to only top by oblique line wheel event with overflow-y: hidden (body is scrollable)", + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: -1.0, deltaY: -1.0, deltaZ: 0.0, + lineOrPageDeltaX: -1, lineOrPageDeltaY: -1, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + expected: kScrollLeft }, + { description: "Scroll to only top by oblique page wheel event with overflow-y: hidden (body is scrollable)", + event: { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: -1.0, deltaY: -1.0, deltaZ: 0.0, + lineOrPageDeltaX: -1, lineOrPageDeltaY: -1, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + expected: kScrollLeft, + cleanup (cb) { + gScrollableElement.style.overflowY = "auto"; + gScrollableElement.style.position = "static"; + gSpacerForBodyElement.style.width = ""; + gSpacerForBodyElement.style.height = ""; + cb(); + } }, + ]; + + var description; + + var currentTestIndex = -1; + var isXReverted = (aSettings.deltaMultiplierX < 0); + var isYReverted = (aSettings.deltaMultiplierY < 0); + + function doNextTest() + { + if (++currentTestIndex >= kTests.length) { + SimpleTest.executeSoon(aCallback); + return; + } + + gScrollableElement.scrollTop = 1000; + gScrollableElement.scrollLeft = 1000; + + var currentTest = kTests[currentTestIndex]; + description = "doTestScroll(aSettings=" + aSettings.description + "), " + currentTest.description + ": "; + if (currentTest.prepare) { + // prepare() can make changes to a page such as + // changing the 'overflow' property which requires + // a repaint to take effect before sending + // scroll-wheel events. + currentTest.prepare(doWaitForPaintsAndScroll); + } else { + doTestCurrentScroll(); + } + } + + function doWaitForPaintsAndScroll() { + waitForAllPaintsFlushed(doTestCurrentScroll); + } + + function doTestCurrentScroll() { + var currentTest = kTests[currentTestIndex]; + sendWheelAndWait(10, 10, currentTest.event, function () { + if (currentTest.expected == kNoScroll) { + is(gScrollableElement.scrollTop, 1000, description + "scrolled vertical"); + is(gScrollableElement.scrollLeft, 1000, description + "scrolled horizontal"); + } else { + var scrollUp = !isYReverted ? (currentTest.expected & kScrollUp) : + (currentTest.expected & kScrollDown); + var scrollDown = !isYReverted ? (currentTest.expected & kScrollDown) : + (currentTest.expected & kScrollUp); + if (scrollUp) { + ok(gScrollableElement.scrollTop < 1000, description + "not scrolled up, got " + gScrollableElement.scrollTop); + } else if (scrollDown) { + ok(gScrollableElement.scrollTop > 1000, description + "not scrolled down, got " + gScrollableElement.scrollTop); + } else { + is(gScrollableElement.scrollTop, 1000, description + "scrolled vertical"); + } + var scrollLeft = !isXReverted ? (currentTest.expected & kScrollLeft) : + (currentTest.expected & kScrollRight); + var scrollRight = !isXReverted ? (currentTest.expected & kScrollRight) : + (currentTest.expected & kScrollLeft); + if (scrollLeft) { + ok(gScrollableElement.scrollLeft < 1000, description + "not scrolled to left, got " + gScrollableElement.scrollLeft); + } else if (scrollRight) { + ok(gScrollableElement.scrollLeft > 1000, description + "not scrolled to right, got " + gScrollableElement.scrollLeft); + } else { + is(gScrollableElement.scrollLeft, 1000, description + "scrolled horizontal"); + } + } + if (currentTest.cleanup) { + currentTest.cleanup(nextStep); + } else { + nextStep(); + } + + function nextStep() { + winUtils.advanceTimeAndRefresh(100); + doNextTest(); + } + }); + } + doNextTest(); +} + +function doTestHorizontalizedScroll(aSettings, aCallback) +{ + const kNoScroll = 0x00; + const kScrollLeft = 0x01; + const kScrollRight = 0x02; + + const kTests = [ + { description: "Scroll to right by pixel scroll even if lineOrPageDelta is 0", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 0.0, deltaY: 8.0, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: true, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + expected: kScrollRight }, + { description: "Scroll to right by pixel scroll when lineOrPageDelta is 1", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 0.0, deltaY: 8.0, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 1, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: true, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + expected: kScrollRight }, + { description: "Scroll to left by pixel scroll even if lineOrPageDelta is 0", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 0.0, deltaY: -8.0, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: true, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + expected: kScrollLeft }, + { description: "Scroll to left by pixel scroll when lineOrPageDelta is -1", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 0.0, deltaY: -8.0, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: -1, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: true, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + expected: kScrollLeft }, + { description: "Don't scroll by deltaX (pixel scroll, lineOrPageDelta is 0)", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 8.0, deltaY: 0.0, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: true, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + expected: kNoScroll }, + { description: "Don't scroll by deltaX (pixel scroll, lineOrPageDelta is 1)", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 8.0, deltaY: 0.0, deltaZ: 0.0, + lineOrPageDeltaX: 1, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: true, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + expected: kNoScroll }, + { description: "Don't scroll by negative deltaX (pixel scroll, lineOrPageDelta is 0)", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: -8.0, deltaY: 0.0, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: true, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + expected: kNoScroll }, + { description: "Don't scroll by negative deltaX (pixel scroll, lineOrPageDelta is -1)", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: -8.0, deltaY: 0.0, deltaZ: 0.0, + lineOrPageDeltaX: -1, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: true, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + expected: kNoScroll }, + { description: "Scroll only to right by diagonal pixel scroll (to bottom-right)", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 8.0, deltaY: 8.0, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: true, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + expected: kScrollRight }, + { description: "Scroll only to right by diagonal pixel scroll (to bottom-left)", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: -8.0, deltaY: 8.0, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: true, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + expected: kScrollRight }, + { description: "Scroll only to left by diagonal pixel scroll (to top-left)", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: -8.0, deltaY: -8.0, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: true, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + expected: kScrollLeft }, + { description: "Scroll only to left by pixel scroll (to bottom-right)", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 8.0, deltaY: -8.0, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: true, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + expected: kScrollLeft }, + { description: "Don't scroll by pixel scroll for z-axis", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 0.0, deltaY: 0.0, deltaZ: 1.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: true, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + expected: kNoScroll }, + + { description: "Scroll to right by line scroll even if lineOrPageDelta is 0", + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: 0.0, deltaY: 0.5, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 1, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: true, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + expected: kScrollRight }, + { description: "Scroll to right by line scroll when lineOrPageDelta is 1", + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: 0.0, deltaY: 0.5, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 1, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: true, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + expected: kScrollRight }, + { description: "Scroll to left by line scroll even if lineOrPageDelta is 0", + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: 0.0, deltaY: -0.5, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: true, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + expected: kScrollLeft }, + { description: "Scroll to left by line scroll when lineOrPageDelta is -1", + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: 0.0, deltaY: -0.5, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: -1, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: true, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + expected: kScrollLeft }, + { description: "Don't scroll by deltaX (line scroll, lineOrPageDelta is 0)", + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: 0.5, deltaY: 0.0, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: true, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + expected: kNoScroll }, + { description: "Don't scroll by deltaX (line scroll, lineOrPageDelta is 1)", + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: 0.5, deltaY: 0.0, deltaZ: 0.0, + lineOrPageDeltaX: 1, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: true, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + expected: kNoScroll }, + { description: "Don't scroll by negative deltaX (line scroll, lineOrPageDelta is 0)", + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: -0.5, deltaY: 0.0, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: true, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + expected: kNoScroll }, + { description: "Don't scroll by negative deltaY (line scroll, lineOrPageDelta is -1)", + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: -0.5, deltaY: 0.0, deltaZ: 0.0, + lineOrPageDeltaX: -1, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: true, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + expected: kNoScroll }, + { description: "Scroll only to right by diagonal line scroll (to bottom-right)", + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: 0.5, deltaY: 0.5, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: true, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + expected: kScrollRight }, + { description: "Scroll only to right by diagonal line scroll (to bottom-left)", + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: -0.5, deltaY: 0.5, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: true, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + expected: kScrollRight }, + { description: "Scroll only to left by diagonal line scroll (to top-left)", + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: -0.5, deltaY: -0.5, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: true, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + expected: kScrollLeft }, + { description: "Scroll only to left by line scroll (to top-right)", + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: 0.5, deltaY: -0.5, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: true, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + expected: kScrollLeft }, + { description: "Don't scroll by line scroll for z-axis", + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: 0.0, deltaY: 0.0, deltaZ: 1.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: true, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + expected: kNoScroll }, + + { description: "Scroll to right by page scroll even if lineOrPageDelta is 0", + event: { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: 0.0, deltaY: 0.5, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 1, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: true, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + expected: kScrollRight }, + { description: "Scroll to right by page scroll when lineOrPageDelta is 1", + event: { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: 0.0, deltaY: 0.5, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 1, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: true, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + expected: kScrollRight }, + { description: "Scroll to left by page scroll even if lineOrPageDelta is 0", + event: { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: 0.0, deltaY: -0.5, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: true, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + expected: kScrollLeft }, + { description: "Scroll to left by page scroll when lineOrPageDelta is -1", + event: { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: 0.0, deltaY: -0.5, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: -1, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: true, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + expected: kScrollLeft }, + { description: "Don't scroll by deltaX (page scroll, lineOrPageDelta is 0)", + event: { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: 0.5, deltaY: 0.0, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: true, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + expected: kNoScroll }, + { description: "Don't scroll by deltaX (page scroll, lineOrPageDelta is 1)", + event: { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: 0.5, deltaY: 0.0, deltaZ: 0.0, + lineOrPageDeltaX: 1, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: true, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + expected: kNoScroll }, + { description: "Don't scroll by deltaX (page scroll, lineOrPageDelta is 0)", + event: { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: -0.5, deltaY: 0.0, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: true, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + expected: kNoScroll }, + { description: "Don't scroll by deltaX (page scroll, lineOrPageDelta is -1)", + event: { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: -0.5, deltaY: 0.0, deltaZ: 0.0, + lineOrPageDeltaX: -1, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: true, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + expected: kNoScroll }, + { description: "Scroll only to right by diagonal page scroll (to bottom-right)", + event: { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: 0.5, deltaY: 0.5, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: true, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + expected: kScrollRight }, + { description: "Scroll only to right by diagonal page scroll (to bottom-left)", + event: { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: -0.5, deltaY: 0.5, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: true, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + expected: kScrollRight }, + { description: "Scroll only to left by diagonal page scroll (to top-left)", + event: { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: -0.5, deltaY: -0.5, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: true, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + expected: kScrollLeft }, + { description: "Scroll only to left by diagonal page scroll (to top-right)", + event: { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: 0.5, deltaY: -0.5, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: true, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + expected: kScrollLeft }, + { description: "Don't scroll by page scroll for z-axis", + event: { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: 0.0, deltaY: 0.0, deltaZ: 1.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: true, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + expected: kNoScroll }, + ]; + + var description; + + var currentTestIndex = -1; + // deltaY should cause horizontal scroll and affected by deltaMultiplierY. + // So, horizontal scroll amount and direction is affected by deltaMultiplierY. + var isXReverted = (aSettings.deltaMultiplierY < 0); + + function doNextTest() + { + if (++currentTestIndex >= kTests.length) { + SimpleTest.executeSoon(aCallback); + return; + } + + gScrollableElement.scrollTop = 1000; + gScrollableElement.scrollLeft = 1000; + + var currentTest = kTests[currentTestIndex]; + description = "doTestHorizontalizedScroll(aSettings=" + aSettings.description + "), " + currentTest.description + ": "; + if (currentTest.prepare) { + currentTest.prepare(doTestCurrentScroll); + } else { + doTestCurrentScroll(); + } + } + + function doTestCurrentScroll() { + var currentTest = kTests[currentTestIndex]; + sendWheelAndWait(10, 10, currentTest.event, function () { + is(gScrollableElement.scrollTop, 1000, description + "scrolled vertical"); + if (currentTest.expected == kNoScroll) { + is(gScrollableElement.scrollLeft, 1000, description + "scrolled horizontal"); + } else { + var scrollLeft = !isXReverted ? (currentTest.expected & kScrollLeft) : + (currentTest.expected & kScrollRight); + var scrollRight = !isXReverted ? (currentTest.expected & kScrollRight) : + (currentTest.expected & kScrollLeft); + if (scrollLeft) { + ok(gScrollableElement.scrollLeft < 1000, description + "not scrolled to left, got " + gScrollableElement.scrollLeft); + } else if (scrollRight) { + ok(gScrollableElement.scrollLeft > 1000, description + "not scrolled to right, got " + gScrollableElement.scrollLeft); + } else { + is(gScrollableElement.scrollLeft, 1000, description + "scrolled horizontal"); + } + } + if (currentTest.cleanup) { + currentTest.cleanup(nextStep); + } else { + nextStep(); + } + + function nextStep() { + winUtils.advanceTimeAndRefresh(100); + doNextTest(); + } + }); + } + doNextTest(); +} + +// It will take *freaking* long time(maybe *hours*) to test all the writing mode +// combinations for the scroll target and its root, because there are altogether +// *one hundred* combinations (10 x 10)! +// +// So unless you really don't care a snap on time-consuming testing or a strict +// criteria is required for testing, it is strongly recommeneded that you +// comment out at least the writing modes which are marked as "peculiar" before +// running this test, you are encouraged to also comment out those "uncommon" +// writing modes in order to further shorten testing time. +// +// Note that if you are going to run time-consuming tests without commenting out +// most of the writing modes, don't forget to increase the value of the +// parameter in SimpleTest.requestLongerTimeout in this file; otherwise it'll +// most likely lead you to a timed-out failure. +// +// Also note that |isBTT| has nothing to do with the behaviour of auto-dir +// scrolling, it's just used to set the sign of |kOrigScrollTop|. +const kWritingModes = [ + { + isRTL: true, + isBTT: false, + styles: [ + { + writingMode: "horizontal-tb", + direction: "rtl", + }, + { + writingMode: "vertical-rl", + direction: "ltr", + }, + // uncommon + //{ + // writingMode: "sideways-rl", + // direction: "ltr", + //}, + ], + }, + { + isRTL: false, + isBTT: false, + styles: [ + { + writingMode: "horizontal-tb", + direction: "ltr", + }, + // uncommon + //{ + // writingMode: "vertical-lr", + // direction: "ltr", + //}, + // uncommon + //{ + // writingMode: "sideways-lr", + // direction: "ltr", + //}, + ], + }, + { + isRTL: true, + isBTT: true, + styles: [ + // peculiar + //{ + // writingMode: "vertical-rl", + // direction: "rtl", + //}, + // peculiar + //{ + // writingMode: "sideways-rl", + // direction: "rtl", + //}, + ], + }, + { + isRTL: false, + isBTT: true, + styles: [ + // peculiar + //{ + // writingMode: "vertical-lr", + // direction: "rtl", + //}, + // peculiar + //{ + // writingMode: "sideways-lr", + // direction: "rtl", + //}, + ], + }, +]; + +function getFirstWritingModeStyle() +{ + if (kWritingModes.length < 1) { + return false; + } + let typeIndex = 0; + while (!kWritingModes[typeIndex].styles.length) { + typeIndex++; + if (typeIndex >= kWritingModes.length) { + return false; + } + } + return {typeIndex, styleIndex: 0}; +} + +function getNextWritingModeStyle(curStyle) +{ + let typeIndex = curStyle.typeIndex; + let styleIndex = curStyle.styleIndex + 1; + while (typeIndex < kWritingModes.length) { + if (styleIndex < kWritingModes[typeIndex].styles.length) { + return {typeIndex, styleIndex}; + } + typeIndex++; + styleIndex = 0; + } + return false; +} + +function doTestAutoDirScroll(aSettings, aAutoDirTrait, aCallback) +{ + // Go through all the writing-mode combinations for the scroll target and its + // root. + + let firstStyle = getFirstWritingModeStyle(); + if (!firstStyle) { + // The whole writing mode list is empty, no need to do any test for auto-dir + // scrolling. Go ahead with the subsequent tests. + SimpleTest.executeSoon(aCallback); + return; + } + + // Begin with the first style for both the root and the scroll target. + // doTestAutoDirScroll2 will recursively call itself back for every + // style combination with getNextWritingModeStyle until all combinations have + // been enumerated, and then it will call SimpleTest.executeSoon(aCallback). + doTestAutoDirScroll2(aSettings, aAutoDirTrait, + firstStyle, firstStyle, + aCallback); +} + +function doTestAutoDirScroll2(aSettings, aAutoDirTrait, + aStyleForRoot, aStyleForTarget, + aCallback) +{ + const kStyleTypeForRoot = kWritingModes[aStyleForRoot.typeIndex]; + const kStyleTypeForTarget = kWritingModes[aStyleForTarget.typeIndex]; + + const kStyleForRoot = kStyleTypeForRoot.styles[aStyleForRoot.styleIndex]; + const kStyleForTarget = kStyleTypeForTarget.styles[aStyleForTarget.styleIndex]; + + const kIsRootRTL = kStyleTypeForRoot.isRTL; + const kIsTargetRTL = kStyleTypeForTarget.isRTL; + // Just used to set the sign of |kOrigScrollTop|, not related to the auto-dir + // behaviour. + const kIsTargetBTT = kStyleTypeForTarget.isBTT; + + const kOldStyleForRoot = {}; + const kOldStyleForTarget = {}; + + const kHonoursRoot = Boolean(aAutoDirTrait.honoursRoot); + + const kNoScroll = 0x00; + const kScrollUp = 0x01; + const kScrollDown = 0x02; + const kScrollLeft = 0x04; + const kScrollRight = 0x08; + + // The four constants indicate the expected result if the scroll direction is + // adjusted. + const kAdjustedForUp = {}; + const kAdjustedForDown = {}; + const kAdjustedForLeft = {}; + const kAdjustedForRight = {}; + if (kHonoursRoot ? kIsRootRTL : kIsTargetRTL) { + kAdjustedForUp.result = kScrollRight; + kAdjustedForUp.desc = "right"; + kAdjustedForDown.result = kScrollLeft; + kAdjustedForDown.desc = "left"; + kAdjustedForLeft.result = kScrollDown; + kAdjustedForLeft.desc = "bottom"; + kAdjustedForRight.result = kScrollUp; + kAdjustedForRight.desc = "top"; + } else { + kAdjustedForUp.result = kScrollLeft; + kAdjustedForUp.desc = "left"; + kAdjustedForDown.result = kScrollRight; + kAdjustedForDown.desc = "right"; + kAdjustedForLeft.result = kScrollUp; + kAdjustedForLeft.desc = "top"; + kAdjustedForRight.result = kScrollDown; + kAdjustedForRight.desc = "bottom"; + } + + const kTests = [ + // Tests: Test pixel scrolling towards four edges when the target + // overflows in both the two directions. + // Results: All are unadjusted. + // Reason: Auto-dir adjustment never applies to a target which overflows in + // both the two directions. + { description: "auto-dir scroll to bottom by pixel scroll even if lineOrPageDelta is 0", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 0.0, deltaY: 8.0, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + adjusted: false, + expected: kScrollDown, + prepare (cb) { + // Static contents will not start from the topleft side in some + // writing modes, for ease of coding, we simply absolutely + // position the target to the topleft in every case. + gScrollableElement.style.position = "absolute"; + gScrollableElement.style.top = "10px"; + gScrollableElement.style.left = "10px"; + SpecialPowers.pushPrefEnv({ + "set": [["mousewheel.autodir.enabled", true], + ["mousewheel.autodir.honourroot", kHonoursRoot]] + }, cb); + } }, + { description: "auto-dir scroll to bottom by pixel scroll when lineOrPageDelta is 1", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 0.0, deltaY: 8.0, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 1, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + adjusted: false, + expected: kScrollDown }, + { description: "auto-dir scroll to top by pixel scroll even if lineOrPageDelta is 0", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 0.0, deltaY: -8.0, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + adjusted: false, + expected: kScrollUp }, + { description: "auto-dir scroll to top by pixel scroll when lineOrPageDelta is -1", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 0.0, deltaY: -8.0, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: -1, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + adjusted: false, + expected: kScrollUp }, + { description: "auto-dir scroll to right by pixel scroll even if lineOrPageDelta is 0", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 8.0, deltaY: 0.0, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + adjusted: false, + expected: kScrollRight }, + { description: "auto-dir scroll to right by pixel scroll when lineOrPageDelta is 1", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 8.0, deltaY: 0.0, deltaZ: 0.0, + lineOrPageDeltaX: 1, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + adjusted: false, + expected: kScrollRight }, + { description: "auto-dir scroll to left by pixel scroll even if lineOrPageDelta is 0", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: -8.0, deltaY: 0.0, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + adjusted: false, + expected: kScrollLeft }, + { description: "auto-dir scroll to left by pixel scroll when lineOrPageDelta is -1", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: -8.0, deltaY: 0.0, deltaZ: 0.0, + lineOrPageDeltaX: -1, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + adjusted: false, + expected: kScrollLeft }, + + // Tests: Test diagonal pixel scrolling when the target overflows in both + // the two directions. + // Results: All are unadjusted. + // Reason: Auto-dir adjustment never applies to a target which overflows in + // both the two directions, furthermore, it never applies to + // diagonal scrolling. + { description: "auto-dir scroll to bottom-right by pixel scroll", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 8.0, deltaY: 8.0, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + adjusted: false, + expected: kScrollDown | kScrollRight }, + { description: "auto-dir scroll to bottom-left by pixel scroll", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: -8.0, deltaY: 8.0, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + adjusted: false, + expected: kScrollDown | kScrollLeft }, + { description: "auto-dir scroll to top-left by pixel scroll", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: -8.0, deltaY: -8.0, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + adjusted: false, + expected: kScrollUp | kScrollLeft }, + { description: "auto-dir scroll to top-right by pixel scroll", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 8.0, deltaY: -8.0, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + adjusted: false, + expected: kScrollUp | kScrollRight }, + + // Tests: Test line scrolling towards four edges when the target overflows + // in both the two directions. + // Results: All are unadjusted. + // Reason: Auto-dir adjustment never applies to a target which overflows in + // both the two directions. + { description: "auto-dir scroll to bottom by line scroll even if lineOrPageDelta is 0", + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: 0.0, deltaY: 0.5, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 1, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + adjusted: false, + expected: kScrollDown }, + { description: "auto-dir scroll to bottom by line scroll when lineOrPageDelta is 1", + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: 0.0, deltaY: 0.5, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 1, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + adjusted: false, + expected: kScrollDown }, + { description: "auto-dir scroll to top by line scroll even if lineOrPageDelta is 0", + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: 0.0, deltaY: -0.5, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + adjusted: false, + expected: kScrollUp }, + { description: "auto-dir scroll to top by line scroll when lineOrPageDelta is -1", + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: 0.0, deltaY: -0.5, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: -1, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + adjusted: false, + expected: kScrollUp }, + { description: "auto-dir scroll to right by line scroll even if lineOrPageDelta is 0", + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: 0.5, deltaY: 0.0, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + adjusted: false, + expected: kScrollRight }, + { description: "auto-dir scroll to right by line scroll when lineOrPageDelta is 1", + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: 0.5, deltaY: 0.0, deltaZ: 0.0, + lineOrPageDeltaX: 1, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + adjusted: false, + expected: kScrollRight }, + { description: "auto-dir scroll to left by line scroll even if lineOrPageDelta is 0", + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: -0.5, deltaY: 0.0, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + adjusted: false, + expected: kScrollLeft }, + { description: "auto-dir scroll to left by line scroll when lineOrPageDelta is -1", + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: -0.5, deltaY: 0.0, deltaZ: 0.0, + lineOrPageDeltaX: -1, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + adjusted: false, + expected: kScrollLeft }, + + // Tests: Test diagonal line scrolling when the target overflows in both + // the two directions. + // Results: All are unadjusted. + // Reason: Auto-dir adjustment never applies to a target which overflows in + // both the two directions, furthermore, it never applies to + // diagonal scrolling. + { description: "auto-dir scroll to bottom-right by line scroll", + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: 0.5, deltaY: 0.5, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + adjusted: false, + expected: kScrollDown | kScrollRight }, + { description: "auto-dir scroll to bottom-left by line scroll", + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: -0.5, deltaY: 0.5, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + adjusted: false, + expected: kScrollDown | kScrollLeft }, + { description: "auto-dir scroll to top-left by line scroll", + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: -0.5, deltaY: -0.5, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + adjusted: false, + expected: kScrollUp | kScrollLeft }, + { description: "auto-dir scroll to top-right by line scroll", + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: 0.5, deltaY: -0.5, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + adjusted: false, + expected: kScrollUp | kScrollRight }, + + // Tests: Test page scrolling towards four edges when the target overflows + // in both the two directions. + // Results: All are unadjusted. + // Reason: Auto-dir adjustment never applies to a target which overflows in + // both the two directions. + { description: "auto-dir scroll to bottom by page scroll even if lineOrPageDelta is 0", + event: { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: 0.0, deltaY: 0.5, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 1, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + adjusted: false, + expected: kScrollDown }, + { description: "auto-dir scroll to bottom by page scroll when lineOrPageDelta is 1", + event: { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: 0.0, deltaY: 0.5, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 1, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + adjusted: false, + expected: kScrollDown }, + { description: "auto-dir scroll to top by page scroll even if lineOrPageDelta is 0", + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: 0.0, deltaY: -0.5, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + adjusted: false, + expected: kScrollUp }, + { description: "auto-dir scroll to top by page scroll when lineOrPageDelta is -1", + event: { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: 0.0, deltaY: -0.5, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: -1, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + adjusted: false, + expected: kScrollUp }, + { description: "auto-dir scroll to right by page scroll even if lineOrPageDelta is 0", + event: { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: 0.5, deltaY: 0.0, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + adjusted: false, + expected: kScrollRight }, + { description: "auto-dir scroll to right by page scroll when lineOrPageDelta is 1", + event: { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: 0.5, deltaY: 0.0, deltaZ: 0.0, + lineOrPageDeltaX: 1, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + adjusted: false, + expected: kScrollRight }, + { description: "auto-dir scroll to left by page scroll even if lineOrPageDelta is 0", + event: { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: -0.5, deltaY: 0.0, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + adjusted: false, + expected: kScrollLeft }, + { description: "auto-dir scroll to left by page scroll when lineOrPageDelta is -1", + event: { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: -0.5, deltaY: 0.0, deltaZ: 0.0, + lineOrPageDeltaX: -1, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + adjusted: false, + expected: kScrollLeft }, + + // Tests: Test diagonal page scrolling when the target overflows in both + // the two directions. + // Results: All are unadjusted. + // Reason: Auto-dir adjustment never applies to a target which overflows in + // both the two directions, furthermore, it never applies to + // diagonal scrolling. + { description: "auto-dir scroll to bottom-right by page scroll", + event: { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: 0.5, deltaY: 0.5, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + adjusted: false, + expected: kScrollDown | kScrollRight }, + { description: "auto-dir scroll to bottom-left by page scroll", + event: { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: -0.5, deltaY: 0.5, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + adjusted: false, + expected: kScrollDown | kScrollLeft }, + { description: "auto-dir scroll to top-left by page scroll", + event: { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: -0.5, deltaY: -0.5, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + adjusted: false, + expected: kScrollUp | kScrollLeft }, + { description: "auto-dir scroll to top-right by page scroll", + event: { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: 0.5, deltaY: -0.5, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + adjusted: false, + expected: kScrollUp | kScrollRight }, + + // The tests above in this function are all for testing a target with two + // scrollbars. All of them should not be adjustable. + // From here on, the tests below in this function are all for testing a + // target with only one scrollbar, either a vertical scrollbar or horizontal + // scrollbar. Some of them are adjustable. + + // Tests: Test pixel scrolling towards four edges when the target + // overflows only in the horizontal direction. + // Results: Vertical wheel scrolls are adjusted to be horizontal whereas the + // horizontal wheel scrolls are unadjusted. + // Reason: Auto-dir adjustment applies to a target if the target overflows + // in only one direction and the direction is orthogonal to the + // wheel and deltaZ is zero. + { description: "auto-dir scroll to " + kAdjustedForDown.desc + + "(originally bottom) by pixel scroll even if lineOrPageDelta is 0, " + + "no vertical scrollbar", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 0.0, deltaY: 8.0, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + adjusted: true, + expected: kAdjustedForDown.result, + prepare (cb) { + gScrollableElement.style.overflowX = "auto"; + gScrollableElement.style.overflowY = "hidden"; + resetScrollPosition(gScrollableElement); + cb(); + } }, + { description: "auto-dir scroll to " + kAdjustedForDown.desc + + "(originally bottom) by pixel scroll when lineOrPageDelta is 1, " + + "no vertical scrollbar", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 0.0, deltaY: 8.0, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 1, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + adjusted: true, + expected: kAdjustedForDown.result }, + { description: "auto-dir scroll to " + kAdjustedForUp.desc + + "(originally top) by pixel scroll even if lineOrPageDelta is 0, " + + "no vertical scrollbar", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 0.0, deltaY: -8.0, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + adjusted: true, + expected: kAdjustedForUp.result }, + { description: "auto-dir scroll to " + kAdjustedForUp.desc + + "(originally top) by pixel scroll when lineOrPageDelta is -1, " + + "no vertical scrollbar", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 0.0, deltaY: -8.0, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: -1, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + adjusted: true, + expected: kAdjustedForUp.result }, + { description: "auto-dir scroll to right by pixel scroll even if lineOrPageDelta is 0, " + + "no vertical scrollbar", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 8.0, deltaY: 0.0, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + adjusted: false, + expected: kScrollRight }, + { description: "auto-dir scroll to right by pixel scroll when lineOrPageDelta is 1, " + + "no vertical scrollbar", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 8.0, deltaY: 0.0, deltaZ: 0.0, + lineOrPageDeltaX: 1, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + adjusted: false, + expected: kScrollRight }, + { description: "auto-dir scroll to left by pixel scroll even if lineOrPageDelta is 0, " + + "no vertical scrollbar", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: -8.0, deltaY: 0.0, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + adjusted: false, + expected: kScrollLeft }, + { description: "auto-dir scroll to left by pixel scroll when lineOrPageDelta is -1, " + + "no vertical scrollbar", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: -8.0, deltaY: 0.0, deltaZ: 0.0, + lineOrPageDeltaX: -1, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + adjusted: false, + expected: kScrollLeft }, + + // Tests: Test pixel scrolling towards four edges when the target + // overflows only in the vertical direction. + // Results: Horizontal wheel scrolls are adjusted to be vertical whereas the + // vertical wheel scrolls are unadjusted. + // Reason: Auto-dir adjustment applies to a target if the target overflows + // in only one direction and the direction is orthogonal to the + // wheel and deltaZ is zero. + { description: "auto-dir scroll to bottom by pixel scroll even if lineOrPageDelta is 0, " + + "no horizontal scrollbar", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 0.0, deltaY: 8.0, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + adjusted: false, + expected: kScrollDown, + prepare (cb) { + gScrollableElement.style.overflowX = "hidden"; + gScrollableElement.style.overflowY = "auto"; + resetScrollPosition(gScrollableElement); + cb(); + } }, + { description: "auto-dir scroll to bottom by pixel scroll when lineOrPageDelta is 1, " + + "no horizontal scrollbar", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 0.0, deltaY: 8.0, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 1, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + adjusted: false, + expected: kScrollDown }, + { description: "auto-dir scroll to top by pixel scroll even if lineOrPageDelta is 0, " + + "no horizontal scrollbar", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 0.0, deltaY: -8.0, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + adjusted: false, + expected: kScrollUp }, + { description: "auto-dir scroll to top by pixel scroll when lineOrPageDelta is -1, " + + "no horizontal scrollbar", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 0.0, deltaY: -8.0, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: -1, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + adjusted: false, + expected: kScrollUp }, + { description: "auto-dir scroll to " + kAdjustedForRight.desc + + "(originally right) by pixel scroll even if lineOrPageDelta is 0, " + + "no horizontal scrollbar", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 8.0, deltaY: 0.0, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + adjusted: true, + expected: kAdjustedForRight.result }, + { description: "auto-dir scroll to " + kAdjustedForRight.desc + + "(originally right) by pixel scroll when lineOrPageDelta is 1, " + + "no horizontal scrollbar", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 8.0, deltaY: 0.0, deltaZ: 0.0, + lineOrPageDeltaX: 1, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + adjusted: true, + expected: kAdjustedForRight.result }, + { description: "auto-dir scroll to " + kAdjustedForLeft.desc + + "(originally left) by pixel scroll even if lineOrPageDelta is 0, " + + "no horizontal scrollbar", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: -8.0, deltaY: 0.0, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + adjusted: true, + expected: kAdjustedForLeft.result }, + { description: "auto-dir scroll to " + kAdjustedForLeft.desc + + "(originally left) by pixel scroll when lineOrPageDelta is -1, " + + "no horizontal scrollbar", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: -8.0, deltaY: 0.0, deltaZ: 0.0, + lineOrPageDeltaX: -1, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + adjusted: true, + expected: kAdjustedForLeft.result }, + + // Tests: Test line scrolling towards four edges when the target overflows + // only in the horizontal direction. + // Results: Vertical wheel scrolls are adjusted to be horizontal whereas the + // horizontal wheel scrolls are unadjusted. + // Reason: Auto-dir adjustment applies to a target if the target overflows + // in only one direction and the direction is orthogonal to the + // wheel and deltaZ is zero. + { description: "auto-dir scroll to " + kAdjustedForDown.desc + + "(originally bottom) by line scroll even if lineOrPageDelta is 0, " + + "no vertical scrollbar", + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: 0.0, deltaY: 0.5, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + adjusted: true, + expected: kAdjustedForDown.result, + prepare (cb) { + gScrollableElement.style.overflowX = "auto"; + gScrollableElement.style.overflowY = "hidden"; + resetScrollPosition(gScrollableElement); + cb(); + } }, + { description: "auto-dir scroll to " + kAdjustedForDown.desc + + "(originally bottom) by line scroll when lineOrPageDelta is 1, " + + "no vertical scrollbar", + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: 0.0, deltaY: 0.5, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 1, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + adjusted: true, + expected: kAdjustedForDown.result }, + { description: "auto-dir scroll to " + kAdjustedForUp.desc + + "(originally top) by line scroll even if lineOrPageDelta is 0, " + + "no vertical scrollbar", + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: 0.0, deltaY: -0.5, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + adjusted: true, + expected: kAdjustedForUp.result }, + { description: "auto-dir scroll to " + kAdjustedForUp.desc + + "(originally top) by line scroll when lineOrPageDelta is -1, " + + "no vertical scrollbar", + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: 0.0, deltaY: -0.5, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: -1, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + adjusted: true, + expected: kAdjustedForUp.result }, + { description: "auto-dir scroll to right by line scroll even if lineOrPageDelta is 0, " + + "no vertical scrollbar", + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: 0.5, deltaY: 0.0, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + adjusted: false, + expected: kScrollRight }, + { description: "auto-dir scroll to right by line scroll when lineOrPageDelta is 1, " + + "no vertical scrollbar", + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: 0.5, deltaY: 0.0, deltaZ: 0.0, + lineOrPageDeltaX: 1, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + adjusted: false, + expected: kScrollRight }, + { description: "auto-dir scroll to left by line scroll even if lineOrPageDelta is 0, " + + "no vertical scrollbar", + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: -0.5, deltaY: 0.0, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + adjusted: false, + expected: kScrollLeft }, + { description: "auto-dir scroll to left by line scroll when lineOrPageDelta is -1, " + + "no vertical scrollbar", + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: -0.5, deltaY: 0.0, deltaZ: 0.0, + lineOrPageDeltaX: -1, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + adjusted: false, + expected: kScrollLeft }, + + // Tests: Test line scrolling towards four edges when the target overflows + // only in the vertical direction. + // Results: Horizontal wheel scrolls are adjusted to be vertical whereas the + // vertical wheel scrolls are unadjusted. + // Reason: Auto-dir adjustment applies to a target if the target overflows + // in only one direction and the direction is orthogonal to the + // wheel and deltaZ is zero. + { description: "auto-dir scroll to bottom by line scroll even if lineOrPageDelta is 0, " + + "no horizontal scrollbar", + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: 0.0, deltaY: 0.5, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + adjusted: false, + expected: kScrollDown, + prepare (cb) { + gScrollableElement.style.overflowX = "hidden"; + gScrollableElement.style.overflowY = "auto"; + resetScrollPosition(gScrollableElement); + cb(); + } }, + { description: "auto-dir scroll to bottom by line scroll when lineOrPageDelta is 1, " + + "no horizontal scrollbar", + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: 0.0, deltaY: 0.5, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 1, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + adjusted: false, + expected: kScrollDown }, + { description: "auto-dir scroll to top by line scroll even if lineOrPageDelta is 0, " + + "no horizontal scrollbar", + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: 0.0, deltaY: -0.5, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + adjusted: false, + expected: kScrollUp }, + { description: "auto-dir scroll to top by line scroll when lineOrPageDelta is -1, " + + "no horizontal scrollbar", + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: 0.0, deltaY: -0.5, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: -1, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + adjusted: false, + expected: kScrollUp }, + { description: "auto-dir scroll to " + kAdjustedForRight.desc + + "(originally right) by line scroll even if lineOrPageDelta is 0, " + + "no horizontal scrollbar", + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: 0.5, deltaY: 0.0, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + adjusted: true, + expected: kAdjustedForRight.result }, + { description: "auto-dir scroll to " + kAdjustedForRight.desc + + "(originally right) by line scroll when lineOrPageDelta is 1, " + + "no horizontal scrollbar", + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: 0.5, deltaY: 0.0, deltaZ: 0.0, + lineOrPageDeltaX: 1, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + adjusted: true, + expected: kAdjustedForRight.result }, + { description: "auto-dir scroll to " + kAdjustedForLeft.desc + + "(originally left) by line scroll even if lineOrPageDelta is 0, " + + "no horizontal scrollbar", + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: -0.5, deltaY: 0.0, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + adjusted: true, + expected: kAdjustedForLeft.result }, + { description: "auto-dir scroll to " + kAdjustedForLeft.desc + + "(originally left) by line scroll when lineOrPageDelta is -1, " + + "no horizontal scrollbar", + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: -0.5, deltaY: 0.0, deltaZ: 0.0, + lineOrPageDeltaX: -1, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + adjusted: true, + expected: kAdjustedForLeft.result }, + + // Tests: Test page scrolling towards four edges when the target overflows + // only in the horizontal direction. + // Results: Vertical wheel scrolls are adjusted to be horizontal whereas the + // horizontal wheel scrolls are unadjusted. + // Reason: Auto-dir adjustment applies to a target if the target overflows + // in only one direction and the direction is orthogonal to the + // wheel and deltaZ is zero. + { description: "auto-dir scroll to " + kAdjustedForDown.desc + + "(originally bottom) by page scroll when lineOrPageDelta is 1, " + + "no vertical scrollbar", + event: { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: 0.0, deltaY: 0.5, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 1, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + adjusted: true, + expected: kAdjustedForDown.result, + prepare (cb) { + gScrollableElement.style.overflowX = "auto"; + gScrollableElement.style.overflowY = "hidden"; + resetScrollPosition(gScrollableElement); + cb(); + } }, + { description: "auto-dir scroll to " + kAdjustedForDown.desc + + "(originally bottom) by page scroll even if lineOrPageDelta is 0, " + + "no vertical scrollbar", + event: { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: 0.0, deltaY: 0.5, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + adjusted: true, + expected: kAdjustedForDown.result }, + { description: "auto-dir scroll to " + kAdjustedForUp.desc + + "(originally top) by page scroll when lineOrPageDelta is -1, " + + "no vertical scrollbar", + event: { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: 0.0, deltaY: -0.5, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: -1, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + adjusted: true, + expected: kAdjustedForUp.result }, + { description: "auto-dir scroll to " + kAdjustedForUp.desc + + "(originally top) by page scroll even if lineOrPageDelta is 0, " + + "no vertical scrollbar", + event: { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: 0.0, deltaY: -0.5, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + adjusted: true, + expected: kAdjustedForUp.result }, + { description: "auto-dir scroll to right by page scroll when lineOrPageDelta is 1, " + + "no vertical scrollbar", + event: { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: 0.5, deltaY: 0.0, deltaZ: 0.0, + lineOrPageDeltaX: 1, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + adjusted: false, + expected: kScrollRight }, + { description: "auto-dir scroll to right by page scroll even if lineOrPageDelta is 0, " + + "no vertical scrollbar", + event: { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: 0.5, deltaY: 0.0, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + adjusted: false, + expected: kScrollRight }, + { description: "auto-dir scroll to left by page scroll when lineOrPageDelta is -1, " + + "no vertical scrollbar", + event: { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: -0.5, deltaY: 0.0, deltaZ: 0.0, + lineOrPageDeltaX: -1, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + adjusted: false, + expected: kScrollLeft }, + { description: "auto-dir scroll to left by page scroll even if lineOrPageDelta is 0, " + + "no vertical scrollbar", + event: { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: -0.5, deltaY: 0.0, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + adjusted: false, + expected: kScrollLeft }, + + // Tests: Test page scrolling towards four edges when the target overflows + // only in the vertical direction. + // Results: Horizontal wheel scrolls are adjusted to be vertical whereas the + // vertical wheel scrolls are unadjusted. + // Reason: Auto-dir adjustment applies to a target if the target overflows + // in only one direction and the direction is orthogonal to the + // wheel and deltaZ is zero. + { description: "auto-dir scroll to bottom by page scroll when lineOrPageDelta is 1, " + + "no horizontal scrollbar", + event: { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: 0.0, deltaY: 0.5, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 1, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + adjusted: false, + expected: kScrollDown, + prepare (cb) { + gScrollableElement.style.overflowX = "hidden"; + gScrollableElement.style.overflowY = "auto"; + resetScrollPosition(gScrollableElement); + cb(); + } }, + { description: "auto-dir scroll to bottom by page scroll even if lineOrPageDelta is 0, " + + "no horizontal scrollbar", + event: { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: 0.0, deltaY: 0.5, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + adjusted: false, + expected: kScrollDown }, + { description: "auto-dir scroll to top by page scroll when lineOrPageDelta is -1, " + + "no horizontal scrollbar", + event: { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: 0.0, deltaY: -0.5, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: -1, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + adjusted: false, + expected: kScrollUp }, + { description: "auto-dir scroll to top by page scroll even if lineOrPageDelta is 0, " + + "no horizontal scrollbar", + event: { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: 0.0, deltaY: -0.5, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + adjusted: false, + expected: kScrollUp }, + { description: "auto-dir scroll to " + kAdjustedForRight.desc + + "(originally right) by page scroll when lineOrPageDelta is 1, " + + "no horizontal scrollbar", + event: { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: 0.5, deltaY: 0.0, deltaZ: 0.0, + lineOrPageDeltaX: 1, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + adjusted: true, + expected: kAdjustedForRight.result }, + { description: "auto-dir scroll to " + kAdjustedForRight.desc + + "(originally right) by page scroll even if lineOrPageDelta is 0, " + + "no horizontal scrollbar", + event: { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: 0.5, deltaY: 0.0, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + adjusted: true, + expected: kAdjustedForRight.result }, + { description: "auto-dir scroll to " + kAdjustedForLeft.desc + + "(originally left) by page scroll when lineOrPageDelta is -1, " + + "no horizontal scrollbar", + event: { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: -0.5, deltaY: 0.0, deltaZ: 0.0, + lineOrPageDeltaX: -1, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + adjusted: true, + expected: kAdjustedForLeft.result }, + { description: "auto-dir scroll to " + kAdjustedForLeft.desc + + "(originally left) by page scroll even if lineOrPageDelta is 0, " + + "no horizontal scrollbar", + event: { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: -0.5, deltaY: 0.0, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + adjusted: true, + expected: kAdjustedForLeft.result, + cleanup (cb) { + SpecialPowers.pushPrefEnv({"set": + [["mousewheel.autodir.enabled", + false]]}, + cb); + } }, + // Tests: Test that autodir scrolling can be force-enabled using windowUtils. + // This only tests vertical wheel scrolls being adjusted to be + // horizontal, rather than re-testing all autodir behaviours just for + // this way of enabling it. + // Results: Vertical wheel scrolls are adjusted to be horizontal whereas the + // horizontal wheel scrolls are unadjusted. + // Reason: Auto-dir adjustment applies to a target if the target overflows + // in only one direction and the direction is orthogonal to the + // wheel and deltaZ is zero. + { description: "force-enabled auto-dir scroll to " + kAdjustedForDown.desc + + "(originally bottom) by pixel scroll even if lineOrPageDelta is 0, " + + "no vertical scrollbar", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 0.0, deltaY: 8.0, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + adjusted: true, + expected: kAdjustedForDown.result, + prepare (cb) { + gScrollableElement.style.overflowX = "auto"; + gScrollableElement.style.overflowY = "hidden"; + resetScrollPosition(gScrollableElement); + winUtils.setMousewheelAutodir(gScrollableElement, true, kHonoursRoot) + cb(); + } }, + { description: "force-enabled auto-dir scroll to " + kAdjustedForDown.desc + + "(originally bottom) by pixel scroll when lineOrPageDelta is 1, " + + "no vertical scrollbar", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 0.0, deltaY: 8.0, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 1, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + adjusted: true, + expected: kAdjustedForDown.result }, + { description: "force-enabled auto-dir scroll to " + kAdjustedForUp.desc + + "(originally top) by pixel scroll even if lineOrPageDelta is 0, " + + "no vertical scrollbar", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 0.0, deltaY: -8.0, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + adjusted: true, + expected: kAdjustedForUp.result }, + { description: "force-enabled auto-dir scroll to " + kAdjustedForUp.desc + + "(originally top) by pixel scroll when lineOrPageDelta is -1, " + + "no vertical scrollbar", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 0.0, deltaY: -8.0, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: -1, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + adjusted: true, + expected: kAdjustedForUp.result }, + { description: "force-enabled auto-dir scroll to right by pixel scroll even if lineOrPageDelta is 0, " + + "no vertical scrollbar", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 8.0, deltaY: 0.0, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + adjusted: false, + expected: kScrollRight }, + { description: "force-enabled auto-dir scroll to right by pixel scroll when lineOrPageDelta is 1, " + + "no vertical scrollbar", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 8.0, deltaY: 0.0, deltaZ: 0.0, + lineOrPageDeltaX: 1, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + adjusted: false, + expected: kScrollRight }, + { description: "force-enabled auto-dir scroll to left by pixel scroll even if lineOrPageDelta is 0, " + + "no vertical scrollbar", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: -8.0, deltaY: 0.0, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + adjusted: false, + expected: kScrollLeft }, + { description: "force-enabled auto-dir scroll to left by pixel scroll when lineOrPageDelta is -1, " + + "no vertical scrollbar", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: -8.0, deltaY: 0.0, deltaZ: 0.0, + lineOrPageDeltaX: -1, lineOrPageDeltaY: 0, isMomentum: false, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0, + shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, osKey: false }, + adjusted: false, + expected: kScrollLeft, + cleanup (cb) { + gScrollableElement.style.position = "static"; + gScrollableElement.style.top = "auto"; + gScrollableElement.style.left = "auto"; + gScrollableElement.style.overflow = "auto"; + Object.assign(document.body.style, kOldStyleForRoot); + Object.assign(gScrollableElement.style, kOldStyleForTarget); + winUtils.setMousewheelAutodir(gScrollableElement, false, false); + cb(); + } }, + ]; + + let styleDescForRoot = ""; + let styleDescForTarget = ""; + Object.keys(kStyleForRoot).forEach(function(property) + { + kOldStyleForRoot[property] = document.body.style[property]; + document.body.style[property] = kStyleForRoot[property]; + if ("" !== styleDescForRoot) { + styleDescForRoot += " "; + } + styleDescForRoot += property + ": " + kStyleForRoot[property] + ";"; + }); + Object.keys(kStyleForTarget).forEach(function(property) + { + kOldStyleForTarget[property] = gScrollableElement.style[property]; + gScrollableElement.style[property] = kStyleForTarget[property]; + if ("" !== styleDescForTarget) { + styleDescForTarget += " "; + } + styleDescForTarget += property + ": " + + kStyleForTarget[property] + ";"; + }); + + let description; + let currentTestIndex = -1; + let isXReverted = aSettings.deltaMultiplierX < 0; + let isYReverted = aSettings.deltaMultiplierY < 0; + + // We are doing a "trick" here: + // If the `kHonoursRoot` is true and the scroll target and the root's contents + // are both LTR or both RTL, we can safely skip these tests, because the same + // behavior is tested when the `kHonoursRoot` is false. + if (kHonoursRoot && kIsRootRTL === kIsTargetRTL) { + currentTestIndex = kTests.length; + } + + const kOrigScrollLeft = kIsTargetRTL ? -1000 : 1000; + const kOrigScrollTop = kIsTargetBTT ? -1000 : 1000; + + function doNextTest() + { + if (++currentTestIndex >= kTests.length) { + // The tests for the current writing mode combination have been finished. + // Continue the tests for the next writing mode combination, if any. + let nextStyleForRoot; + let nextStyleForTarget; + nextStyleForTarget = getNextWritingModeStyle(aStyleForTarget); + if (nextStyleForTarget) { + nextStyleForRoot = aStyleForRoot; + } else { + nextStyleForRoot = getNextWritingModeStyle(aStyleForRoot); + if (!nextStyleForRoot) { + // All writing mode combinations have been enumerated, so stop + // recursively calling doTestAutoDirScroll2, and go ahead with the + // subsequent tests. + SimpleTest.executeSoon(aCallback); + return; + } + nextStyleForTarget = getFirstWritingModeStyle(); + } + doTestAutoDirScroll2(aSettings, aAutoDirTrait, + nextStyleForRoot, nextStyleForTarget, + aCallback); + return; + } + + gScrollableElement.scrollTop = kOrigScrollTop; + gScrollableElement.scrollLeft = kOrigScrollLeft; + + var currentTest = kTests[currentTestIndex]; + description = "doTestAutoDirScroll(aSettings=" + aSettings.description + ", "; + if (kHonoursRoot) { + description += "{honoursRoot: true}), "; + } else { + description += "{honoursRoot: false}), "; + } + description += "root = " + styleDescForRoot + " "; + description += "target = " + styleDescForTarget + " "; + if (currentTest.adjusted) { + description += "adjusted "; + } else { + description += "unadjusted "; + } + description += currentTest.description + ": "; + if (currentTest.prepare) { + currentTest.prepare(doTestCurrentScroll); + } else { + doTestCurrentScroll(); + } + } + + function doTestCurrentScroll() { + var currentTest = kTests[currentTestIndex]; + sendWheelAndWait(100, 100, currentTest.event, function () { + if (currentTest.expected == kNoScroll) { + is(gScrollableElement.scrollTop, kOrigScrollTop, description + "scrolled vertical"); + is(gScrollableElement.scrollLeft, kOrigScrollLeft, description + "scrolled horizontal"); + } else { + // If auto-dir adjustment occurs, temporarily swap |isYReverted| and + // |isXReverted|. + if (currentTest.adjusted) { + [isYReverted, isXReverted] = [isXReverted, isYReverted]; + } + let scrollUp = !isYReverted ? (currentTest.expected & kScrollUp) : + (currentTest.expected & kScrollDown); + let scrollDown = !isYReverted ? (currentTest.expected & kScrollDown) : + (currentTest.expected & kScrollUp); + if (scrollUp) { + ok(gScrollableElement.scrollTop < kOrigScrollTop, + description + "not scrolled up, got " + gScrollableElement.scrollTop); + } else if (scrollDown) { + ok(gScrollableElement.scrollTop > kOrigScrollTop, + description + "not scrolled down, got " + gScrollableElement.scrollTop); + } else { + is(gScrollableElement.scrollTop, kOrigScrollTop, + description + "scrolled vertical"); + } + var scrollLeft = !isXReverted ? (currentTest.expected & kScrollLeft) : + (currentTest.expected & kScrollRight); + var scrollRight = !isXReverted ? (currentTest.expected & kScrollRight) : + (currentTest.expected & kScrollLeft); + if (scrollLeft) { + ok(gScrollableElement.scrollLeft < kOrigScrollLeft, + description + "not scrolled to left, got " + gScrollableElement.scrollLeft); + } else if (scrollRight) { + ok(gScrollableElement.scrollLeft > kOrigScrollLeft, + description + "not scrolled to right, got " + gScrollableElement.scrollLeft); + } else { + is(gScrollableElement.scrollLeft, kOrigScrollLeft, + description + "scrolled horizontal"); + } + // |isYReverted| and |isXReverted| have been temporarily swaped for + // auto-dir adjustment, restore them. + if (currentTest.adjusted) { + [isYReverted, isXReverted] = [isXReverted, isYReverted]; + } + } + if (currentTest.cleanup) { + currentTest.cleanup(nextStep); + } else { + nextStep(); + } + + function nextStep() { + winUtils.advanceTimeAndRefresh(100); + doNextTest(); + } + }); + } + doNextTest(); +} + +function doTestZoom(aSettings, aCallback) +{ + if ((aSettings.deltaMultiplierX != 1.0 && aSettings.deltaMultiplierX != -1.0) || + (aSettings.deltaMultiplierY != 1.0 && aSettings.deltaMultiplierY != -1.0)) { + todo(false, "doTestZoom doesn't support to test with aSettings=" + aSettings.description); + SimpleTest.executeSoon(aCallback); + return; + } + + const kNone = 0x00; + const kPositive = 0x01; + const kNegative = 0x02; + const kUseX = 0x10; + const kUseY = 0x20; + const kTests = [ + { description: "by vertical/positive pixel event when its lineOrPageDeltaY is 0", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 0.0, deltaY: 8.0, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0 }, + expected: kNone }, + { description: "by vertical/positive pixel event when its lineOrPageDeltaY is 1", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 0.0, deltaY: 8.0, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 1, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0 }, + expected: kPositive | kUseY }, + { description: "by vertical/negative pixel event when its lineOrPageDeltaY is 0", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 0.0, deltaY: -8.0, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0 }, + expected: kNone }, + { description: "by vertical/negative pixel event when its lineOrPageDeltaY is -1", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 0.0, deltaY: -8.0, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: -1, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0 }, + expected: kNegative | kUseY }, + { description: "by horizotal/positive pixel event when its lineOrPageDeltaX is 0", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 8.0, deltaY: 0.0, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0 }, + expected: kNone }, + { description: "by horizotal/positive pixel event when its lineOrPageDeltaX is 1", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 8.0, deltaY: 0.0, deltaZ: 0.0, + lineOrPageDeltaX: 1, lineOrPageDeltaY: 0, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0 }, + expected: kPositive | kUseX }, + { description: "by horizotal/negative pixel event when its lineOrPageDeltaX is 0", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: -8.0, deltaY: 0.0, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0 }, + expected: kNone }, + { description: "by horizotal/negative pixel event when its lineOrPageDeltaX is -1", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: -8.0, deltaY: 0.0, deltaZ: 0.0, + lineOrPageDeltaX: -1, lineOrPageDeltaY: 0, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0 }, + expected: kNegative | kUseX }, + { description: "by z pixel event", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 0.0, deltaY: 0.0, deltaZ: 16.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0 }, + expected: kNone }, + + { description: "by vertical/positive line event when its lineOrPageDeltaY is 0", + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: 0.0, deltaY: 0.5, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0 }, + expected: kNone }, + { description: "by vertical/positive line event when its lineOrPageDeltaY is 1", + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: 0.0, deltaY: 0.5, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 1, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0 }, + expected: kPositive | kUseY }, + { description: "by vertical/negative line event when its lineOrPageDeltaY is 0", + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: 0.0, deltaY: -0.5, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0 }, + expected: kNone }, + { description: "by vertical/negative line event when its lineOrPageDeltaY is -1", + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: 0.0, deltaY: -0.5, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: -1, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0 }, + expected: kNegative | kUseY }, + { description: "by horizotal/positive line event when its lineOrPageDeltaX is 0", + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: 0.5, deltaY: 0.0, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0 }, + expected: kNone }, + { description: "by horizotal/positive line event when its lineOrPageDeltaX is 1", + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: 0.5, deltaY: 0.0, deltaZ: 0.0, + lineOrPageDeltaX: 1, lineOrPageDeltaY: 0, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0 }, + expected: kPositive | kUseX }, + { description: "by horizotal/negative line event when its lineOrPageDeltaX is 0", + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: -0.5, deltaY: 0.0, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0 }, + expected: kNone }, + { description: "by horizotal/negative line event when its lineOrPageDeltaX is -1", + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: -0.5, deltaY: 0.0, deltaZ: 0.0, + lineOrPageDeltaX: -1, lineOrPageDeltaY: 0, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0 }, + expected: kNegative | kUseX }, + { description: "by z line event", + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: 0.0, deltaY: 0.0, deltaZ: 1.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0 }, + expected: kNone }, + + { description: "by vertical/positive page event when its lineOrPageDeltaY is 0", + event: { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: 0.0, deltaY: 0.5, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0 }, + expected: kNone }, + { description: "by vertical/positive page event when its lineOrPageDeltaY is 1", + event: { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: 0.0, deltaY: 0.5, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 1, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0 }, + expected: kPositive | kUseY }, + { description: "by vertical/negative page event when its lineOrPageDeltaY is 0", + event: { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: 0.0, deltaY: -0.5, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0 }, + expected: kNone }, + { description: "by vertical/negative page event when its lineOrPageDeltaY is -1", + event: { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: 0.0, deltaY: -0.5, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: -1, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0 }, + expected: kNegative | kUseY }, + { description: "by horizotal/positive page event when its lineOrPageDeltaX is 0", + event: { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: 0.5, deltaY: 0.0, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0 }, + expected: kNone }, + { description: "by horizotal/positive page event when its lineOrPageDeltaX is 1", + event: { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: 0.5, deltaY: 0.0, deltaZ: 0.0, + lineOrPageDeltaX: 1, lineOrPageDeltaY: 0, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0 }, + expected: kPositive | kUseX }, + { description: "by horizotal/negative page event when its lineOrPageDeltaX is 0", + event: { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: -0.5, deltaY: 0.0, deltaZ: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0 }, + expected: kNone }, + { description: "by horizotal/negative page event when its lineOrPageDeltaX is -1", + event: { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: -0.5, deltaY: 0.0, deltaZ: 0.0, + lineOrPageDeltaX: -1, lineOrPageDeltaY: 0, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0 }, + expected: kNegative | kUseX }, + { description: "by z page event", + event: { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: 0.0, deltaY: 0.0, deltaZ: 1.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0, + expectedOverflowDeltaX: 0, expectedOverflowDeltaY: 0 }, + expected: kNone }, + ]; + + var description, currentTest; + var currentTestIndex = -1; + var isXReverted = (aSettings.deltaMultiplierX < 0); + var isYReverted = (aSettings.deltaMultiplierY < 0); + + function doNextTest() { + if (++currentTestIndex >= kTests.length) { + SimpleTest.executeSoon(aCallback); + return; + } + + gScrollableElement.scrollTop = 1000; + gScrollableElement.scrollLeft = 1000; + + currentTest = kTests[currentTestIndex]; + description = "doTestZoom(aSettings=" + aSettings.description + "), "; + if (currentTest.expected == kNone) { + description += "Shouldn't "; + } else { + description += "Should "; + } + description += "zoom " + currentTest.description + ": "; + + var event = currentTest.event; + event.ctrlKey = true; + + // NOTE: Zooming might change scrollTop and scrollLeft by rounding fraction. + // This test assume that zoom happens synchronously and scrolling + // happens asynchronously. + var scrollTop = gScrollableElement.scrollTop; + var scrollLeft = gScrollableElement.scrollLeft; + + fullZoomChangePromise = new Promise(resolve => { + if (currentTest.expected & (kNegative | kPositive)) { + // Zoom causes a resize of the viewport. + window.addEventListener("resize", function onResize() { + if (SpecialPowers.getFullZoom(window) != 1) { + window.removeEventListener("resize", onResize); + setTimeout(() => resolve(), 0); + } + }); + } else { + resolve(); + } + }); + + sendWheelAndWait(10, 10, event, function () { + is(gScrollableElement.scrollTop, scrollTop, description + "scrolled vertical"); + is(gScrollableElement.scrollLeft, scrollLeft, description + "scrolled horizontal"); + + fullZoomChangePromise.then(() => { + // When input event prioritization is enabled, the wheel event may be + // dispatched to the content process before the message 'FullZoom' to + // zoom in/out. Waiting for the event 'FullZoomChange' and then check + // the result. + if (!(currentTest.expected & (kNegative | kPositive))) { + is(SpecialPowers.getFullZoom(window), 1.0, description + "zoomed"); + } else { + var isReverted = (currentTest.expected & kUseX) ? isXReverted : + (currentTest.expected & kUseY) ? isYReverted : false; + if ((!isReverted && (currentTest.expected & kNegative)) || + (isReverted && (currentTest.expected & kPositive))) { + ok(SpecialPowers.getFullZoom(window) > 1.0, + description + "not zoomed in, got " + SpecialPowers.getFullZoom(window)); + } else { + ok(SpecialPowers.getFullZoom(window) < 1.0, + description + "not zoomed out, got " + SpecialPowers.getFullZoom(window)); + } + } + if (SpecialPowers.getFullZoom(window) != 1) { + // Only synthesizes key event to reset zoom when necessary to avoid + // triggering the next test before the key event is handled. In that + // case, the key event may break the next test. + synthesizeKey("0", { accelKey: true }); + } + onZoomReset(function () { + hitEventLoop(doNextTest, 20); + }); + }); + }); + } + doNextTest(); +} + +function doTestZoomedScroll(aCallback) +{ + var zoom = 1.0; + function setFullZoom(aWindow, aZoom) + { + zoom = aZoom; + SpecialPowers.setFullZoom(aWindow, aZoom); + } + + function prepareTestZoomedPixelScroll() + { + // Reset zoom and store the scroll amount into the data. + synthesizeKey("0", { accelKey: true }); + zoom = 1.0; + onZoomReset(testZoomedPixelScroll); + } + + function testZoomedPixelScroll() + { + gScrollableElement.scrollTop = 1000; + gScrollableElement.scrollLeft = 1000; + // Ensure not to be in reflow. + hitEventLoop(function () { + function mousePixelScrollHandler(aEvent) + { + if (aEvent.axis == MouseScrollEvent.HORIZONTAL_AXIS) { + is(aEvent.detail, 16, + "doTestZoomedScroll: The detail of horizontal MozMousePixelScroll for pixel wheel event is wrong"); + } else if (aEvent.axis == MouseScrollEvent.VERTICAL_AXIS) { + is(aEvent.detail, 16, + "doTestZoomedScroll: The detail of vertical MozMousePixelScroll for pixel wheel event is wrong"); + } else { + ok(false, "doTestZoomedScroll: The axis of MozMousePixelScroll for pixel wheel event is invalid, got " + aEvent.axis); + } + } + function wheelHandler(aEvent) + { + is(aEvent.deltaX, 16.0 / zoom, + "doTestZoomedScroll: The deltaX of wheel for pixel is wrong"); + is(aEvent.deltaY, 16.0 / zoom, + "doTestZoomedScroll: The deltaY of wheel for pixel is wrong"); + } + window.addEventListener("MozMousePixelScroll", mousePixelScrollHandler, true); + window.addEventListener("wheel", wheelHandler, true); + var event = { + deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 16.0, + deltaY: 16.0, + lineOrPageDeltaX: 0, + lineOrPageDeltaY: 0 + }; + // wait scrolled actually. + sendWheelAndWait(10, 10, event, function () { + var scrolledX = gScrollableElement.scrollLeft; + var scrolledY = gScrollableElement.scrollTop; + ok(scrolledX > 1000, + "doTestZoomedScroll: scrolledX must be larger than 1000 for pixel wheel event, got " + scrolledX); + ok(scrolledY > 1000, + "doTestZoomedScroll: scrolledY must be larger than 1000 for pixel wheel event, got " + scrolledY); + + // Zoom + setFullZoom(window, 2.0); + // Ensure not to be in reflow. + hitEventLoop(function () { + gScrollableElement.scrollTop = 1000; + gScrollableElement.scrollLeft = 1000; + var evt = { + deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 16.0, + deltaY: 16.0, + lineOrPageDeltaX: 0, + lineOrPageDeltaY: 0 + }; + // wait scrolled actually. + sendWheelAndWait(10, 10, evt, function () { + ok(Math.abs(gScrollableElement.scrollLeft - (1000 + (scrolledX - 1000) / 2)) <= 1, + "doTestZoomedScroll: zoomed horizontal scroll amount by pixel wheel event is different from normal, scrollLeft=" + + gScrollableElement.scrollLeft + ", scrolledX=" + scrolledX); + ok(Math.abs(gScrollableElement.scrollTop - (1000 + (scrolledY - 1000) / 2)) <= 1, + "doTestZoomedScroll: zoomed vertical scroll amount by pixel wheel event is different from normal, scrollTop=" + + gScrollableElement.scrollTop + ", scrolledY=" + scrolledY); + window.removeEventListener("MozMousePixelScroll", mousePixelScrollHandler, true); + window.removeEventListener("wheel", wheelHandler, true); + + synthesizeKey("0", { accelKey: true }); + onZoomReset(prepareTestZoomedLineScroll); + }); + }, 20); + }); + }, 20); + } + + function prepareTestZoomedLineScroll() + { + // Reset zoom and store the scroll amount into the data. + synthesizeKey("0", { accelKey: true }); + zoom = 1.0; + onZoomReset(testZoomedLineScroll); + } + function testZoomedLineScroll() + { + gScrollableElement.scrollTop = 1000; + gScrollableElement.scrollLeft = 1000; + // Ensure not to be in reflow. + hitEventLoop(function () { + var lineHeightX, lineHeightY; + function handler(aEvent) + { + if (aEvent.axis == MouseScrollEvent.HORIZONTAL_AXIS) { + if (lineHeightX == undefined) { + lineHeightX = aEvent.detail; + } else { + ok(Math.abs(aEvent.detail - lineHeightX) <= 1, + "doTestZoomedScroll: The detail of horizontal MozMousePixelScroll for line wheel event is wrong, aEvent.detail=" + + aEvent.detail + ", lineHeightX=" + lineHeightX); + } + } else if (aEvent.axis == MouseScrollEvent.VERTICAL_AXIS) { + if (lineHeightY == undefined) { + lineHeightY = aEvent.detail; + } else { + ok(Math.abs(aEvent.detail - lineHeightY) <= 1, + "doTestZoomedScroll: The detail of vertical MozMousePixelScroll for line wheel event is wrong, aEvent.detail=" + + aEvent.detail + ", lineHeightY=" + lineHeightY); + } + } else { + ok(false, "doTestZoomedScroll: The axis of MozMousePixelScroll for line wheel event is invalid, got " + aEvent.axis); + } + } + window.addEventListener("MozMousePixelScroll", handler, true); + var event = { + deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: 1.0, + deltaY: 1.0, + lineOrPageDeltaX: 1, + lineOrPageDeltaY: 1 + }; + // wait scrolled actually. + sendWheelAndWait(10, 10, event, function () { + var scrolledX = gScrollableElement.scrollLeft; + var scrolledY = gScrollableElement.scrollTop; + ok(scrolledX > 1000, + "doTestZoomedScroll: scrolledX must be larger than 1000 for line wheel event, got " + scrolledX); + ok(scrolledY > 1000, + "doTestZoomedScroll: scrolledY must be larger than 1000 for line wheel event, got " + scrolledY); + + // Zoom + setFullZoom(window, 2.0); + // Ensure not to be in reflow. + hitEventLoop(function () { + gScrollableElement.scrollTop = 1000; + gScrollableElement.scrollLeft = 1000; + var evt = { + deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: 1.0, + deltaY: 1.0, + lineOrPageDeltaX: 1, + lineOrPageDeltaY: 1 + }; + // wait scrolled actually. + sendWheelAndWait(10, 10, evt, function () { + ok(Math.abs(gScrollableElement.scrollLeft - scrolledX) <= 1, + "doTestZoomedScroll: zoomed horizontal scroll amount by line wheel event is different from normal, scrollLeft=" + + gScrollableElement.scrollLeft + ", scrolledX=" + scrolledX); + ok(Math.abs(gScrollableElement.scrollTop - scrolledY) <= 1, + "doTestZoomedScroll: zoomed vertical scroll amount by line wheel event is different from normal, scrollTop=" + + gScrollableElement.scrollTop + ", scrolledY=" + scrolledY); + + window.removeEventListener("MozMousePixelScroll", handler, true); + + synthesizeKey("0", { accelKey: true }); + onZoomReset(aCallback); + }); + }, 20); + }); + }, 20); + } + + // XXX It's too difficult to test page scroll because the page scroll amount + // is computed by complex logic. + + prepareTestZoomedPixelScroll(); +} + +function doTestWholeScroll(aCallback) +{ + SpecialPowers.pushPrefEnv({"set": [ + ["mousewheel.default.delta_multiplier_x", 999999], + ["mousewheel.default.delta_multiplier_y", 999999]]}, + function() { doTestWholeScroll2(aCallback); }); +} + +function doTestWholeScroll2(aCallback) +{ + const kTests = [ + { description: "try whole-scroll to top (line)", + prepare () { + gScrollableElement.scrollTop = 1000; + gScrollableElement.scrollLeft = 1000; + }, + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: 0.0, deltaY: -1.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: -1 }, + expectedScrollTop: 0, + expectedScrollLeft: 1000 + }, + { description: "try whole-scroll to top when scrollTop is already top-most (line)", + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: 0.0, deltaY: -1.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: -1 }, + expectedScrollTop: 0, + expectedScrollLeft: 1000 + }, + { description: "try whole-scroll to bottom (line)", + prepare () { + gScrollableElement.scrollTop = 1000; + gScrollableElement.scrollLeft = 1000; + }, + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: 0.0, deltaY: 1.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 1 }, + expectedScrollTop: gScrollableElement.scrollTopMax, + expectedScrollLeft: 1000 + }, + { description: "try whole-scroll to bottom when scrollTop is already bottom-most (line)", + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: 0, deltaY: 1.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 1 }, + expectedScrollTop: gScrollableElement.scrollTopMax, + expectedScrollLeft: 1000 + }, + { description: "try whole-scroll to left (line)", + prepare () { + gScrollableElement.scrollTop = 1000; + gScrollableElement.scrollLeft = 1000; + }, + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: -1.0, deltaY: 0.0, + lineOrPageDeltaX: -1, lineOrPageDeltaY: 0 }, + expectedScrollTop: 1000, + expectedScrollLeft: 0 + }, + { description: "try whole-scroll to left when scrollLeft is already left-most (line)", + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: -1.0, deltaY: 0, + lineOrPageDeltaX: -1, lineOrPageDeltaY: 0 }, + expectedScrollTop: 1000, + expectedScrollLeft: 0 + }, + { description: "try whole-scroll to right (line)", + prepare () { + gScrollableElement.scrollTop = 1000; + gScrollableElement.scrollLeft = 1000; + }, + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: 1.0, deltaY: 0.0, + lineOrPageDeltaX: 1, lineOrPageDeltaY: 0 }, + expectedScrollTop: 1000, + expectedScrollLeft: gScrollableElement.scrollLeftMax + }, + { description: "try whole-scroll to right when scrollLeft is already right-most (line)", + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: 1.0, deltaY: 0.0, + lineOrPageDeltaX: 1, lineOrPageDeltaY: 0 }, + expectedScrollTop: 1000, + expectedScrollLeft: gScrollableElement.scrollLeftMax + }, + + + { description: "try whole-scroll to top (pixel)", + prepare () { + gScrollableElement.scrollTop = 1000; + gScrollableElement.scrollLeft = 1000; + }, + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 0.0, deltaY: -1.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0 }, + expectedScrollTop: 0, + expectedScrollLeft: 1000 + }, + { description: "try whole-scroll to top when scrollTop is already top-most (pixel)", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 0.0, deltaY: -1.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0 }, + expectedScrollTop: 0, + expectedScrollLeft: 1000 + }, + { description: "try whole-scroll to bottom (pixel)", + prepare () { + gScrollableElement.scrollTop = 1000; + gScrollableElement.scrollLeft = 1000; + }, + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 0.0, deltaY: 1.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0 }, + expectedScrollTop: gScrollableElement.scrollTopMax, + expectedScrollLeft: 1000 + }, + { description: "try whole-scroll to bottom when scrollTop is already bottom-most (pixel)", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 0, deltaY: 1.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0 }, + expectedScrollTop: gScrollableElement.scrollTopMax, + expectedScrollLeft: 1000 + }, + { description: "try whole-scroll to left (pixel)", + prepare () { + gScrollableElement.scrollTop = 1000; + gScrollableElement.scrollLeft = 1000; + }, + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: -1.0, deltaY: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0 }, + expectedScrollTop: 1000, + expectedScrollLeft: 0 + }, + { description: "try whole-scroll to left when scrollLeft is already left-most (pixel)", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: -1.0, deltaY: 0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0 }, + expectedScrollTop: 1000, + expectedScrollLeft: 0 + }, + { description: "try whole-scroll to right (pixel)", + prepare () { + gScrollableElement.scrollTop = 1000; + gScrollableElement.scrollLeft = 1000; + }, + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 1.0, deltaY: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0 }, + expectedScrollTop: 1000, + expectedScrollLeft: gScrollableElement.scrollLeftMax + }, + { description: "try whole-scroll to right when scrollLeft is already right-most (pixel)", + event: { deltaMode: WheelEvent.DOM_DELTA_PIXEL, + deltaX: 1.0, deltaY: 0.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 0 }, + expectedScrollTop: 1000, + expectedScrollLeft: gScrollableElement.scrollLeftMax + }, + + + { description: "try whole-scroll to top (page)", + prepare () { + gScrollableElement.scrollTop = 1000; + gScrollableElement.scrollLeft = 1000; + }, + event: { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: 0.0, deltaY: -1.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: -1 }, + expectedScrollTop: 0, + expectedScrollLeft: 1000 + }, + { description: "try whole-scroll to top when scrollTop is already top-most (page)", + event: { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: 0.0, deltaY: -1.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: -1 }, + expectedScrollTop: 0, + expectedScrollLeft: 1000 + }, + { description: "try whole-scroll to bottom (page)", + prepare () { + gScrollableElement.scrollTop = 1000; + gScrollableElement.scrollLeft = 1000; + }, + event: { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: 0.0, deltaY: 1.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 1 }, + expectedScrollTop: gScrollableElement.scrollTopMax, + expectedScrollLeft: 1000 + }, + { description: "try whole-scroll to bottom when scrollTop is already bottom-most (page)", + event: { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: 0, deltaY: 1.0, + lineOrPageDeltaX: 0, lineOrPageDeltaY: 1 }, + expectedScrollTop: gScrollableElement.scrollTopMax, + expectedScrollLeft: 1000 + }, + { description: "try whole-scroll to left (page)", + prepare () { + gScrollableElement.scrollTop = 1000; + gScrollableElement.scrollLeft = 1000; + }, + event: { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: -1.0, deltaY: 0.0, + lineOrPageDeltaX: -1, lineOrPageDeltaY: 0 }, + expectedScrollTop: 1000, + expectedScrollLeft: 0 + }, + { description: "try whole-scroll to left when scrollLeft is already left-most (page)", + event: { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: -1.0, deltaY: 0, + lineOrPageDeltaX: -1, lineOrPageDeltaY: 0 }, + expectedScrollTop: 1000, + expectedScrollLeft: 0 + }, + { description: "try whole-scroll to right (page)", + prepare () { + gScrollableElement.scrollTop = 1000; + gScrollableElement.scrollLeft = 1000; + }, + event: { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: 1.0, deltaY: 0.0, + lineOrPageDeltaX: 1, lineOrPageDeltaY: 0 }, + expectedScrollTop: 1000, + expectedScrollLeft: gScrollableElement.scrollLeftMax + }, + { description: "try whole-scroll to right when scrollLeft is already right-most (page)", + event: { deltaMode: WheelEvent.DOM_DELTA_PAGE, + deltaX: 1.0, deltaY: 0.0, + lineOrPageDeltaX: 1, lineOrPageDeltaY: 0 }, + expectedScrollTop: 1000, + expectedScrollLeft: gScrollableElement.scrollLeftMax + }, + ]; + + var index = 0; + + function doIt() + { + const kTest = kTests[index]; + if (kTest.prepare) { + kTest.prepare(); + } + sendWheelAndWait(10, 10, kTest.event, function () { + is(gScrollableElement.scrollTop, kTest.expectedScrollTop, + "doTestWholeScroll, " + kTest.description + ": unexpected scrollTop"); + is(gScrollableElement.scrollLeft, kTest.expectedScrollLeft, + "doTestWholeScroll, " + kTest.description + ": unexpected scrollLeft"); + if (++index == kTests.length) { + SimpleTest.executeSoon(aCallback); + } else { + doIt(); + } + }); + } + doIt(); +} + +function doTestActionOverride(aCallback) +{ + const kNoScroll = 0x00; + const kScrollUp = 0x01; + const kScrollDown = 0x02; + const kScrollLeft = 0x04; + const kScrollRight = 0x08; + + const kTests = [ + { action: kDefaultActionScroll, override_x: kDefaultActionOverrideXNoOverride, + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: 1.0, deltaY: 1.0, + lineOrPageDeltaX: 1, lineOrPageDeltaY: 1 }, + expected: kScrollDown | kScrollRight + }, + { action: kDefaultActionScroll, override_x: kDefaultActionOverrideXNone, + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: 1.0, deltaY: 1.0, + lineOrPageDeltaX: 1, lineOrPageDeltaY: 1 }, + expected: kScrollDown | kScrollRight + }, + { action: kDefaultActionScroll, override_x: kDefaultActionOverrideXScroll, + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: 1.0, deltaY: 1.0, + lineOrPageDeltaX: 1, lineOrPageDeltaY: 1 }, + expected: kScrollDown | kScrollRight + }, + { action: kDefaultActionNone, override_x: kDefaultActionOverrideXNoOverride, + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: 1.0, deltaY: 1.0, + lineOrPageDeltaX: 1, lineOrPageDeltaY: 1 }, + expected: kNoScroll + }, + { action: kDefaultActionNone, override_x: kDefaultActionOverrideXNone, + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: 1.0, deltaY: 1.0, + lineOrPageDeltaX: 1, lineOrPageDeltaY: 1 }, + expected: kNoScroll + }, + { action: kDefaultActionNone, override_x: kDefaultActionOverrideXScroll, + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: 1.0, deltaY: 1.0, + lineOrPageDeltaX: 1, lineOrPageDeltaY: 1 }, + expected: kNoScroll + }, + { action: kDefaultActionScroll, override_x: kDefaultActionOverrideXNoOverride, + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: 1.0, deltaY: 0.5, + lineOrPageDeltaX: 1, lineOrPageDeltaY: 1 }, + expected: kScrollDown | kScrollRight + }, + { action: kDefaultActionScroll, override_x: kDefaultActionOverrideXNone, + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: 1.0, deltaY: 0.5, + lineOrPageDeltaX: 1, lineOrPageDeltaY: 1 }, + expected: kNoScroll + }, + { action: kDefaultActionScroll, override_x: kDefaultActionOverrideXScroll, + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: 1.0, deltaY: 0.5, + lineOrPageDeltaX: 1, lineOrPageDeltaY: 1 }, + expected: kScrollDown | kScrollRight + }, + { action: kDefaultActionNone, override_x: kDefaultActionOverrideXNoOverride, + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: 1.0, deltaY: 0.5, + lineOrPageDeltaX: 1, lineOrPageDeltaY: 1 }, + expected: kNoScroll + }, + { action: kDefaultActionNone, override_x: kDefaultActionOverrideXNone, + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: 1.0, deltaY: 0.5, + lineOrPageDeltaX: 1, lineOrPageDeltaY: 1 }, + expected: kNoScroll + }, + { action: kDefaultActionNone, override_x: kDefaultActionOverrideXScroll, + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: 1.0, deltaY: 0.5, + lineOrPageDeltaX: 1, lineOrPageDeltaY: 1 }, + expected: kScrollDown | kScrollRight + }, + { action: kDefaultActionScroll, override_x: kDefaultActionOverrideXNoOverride, + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: 0.5, deltaY: 1.0, + lineOrPageDeltaX: 1, lineOrPageDeltaY: 1 }, + expected: kScrollDown | kScrollRight + }, + { action: kDefaultActionScroll, override_x: kDefaultActionOverrideXNone, + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: 0.5, deltaY: 1.0, + lineOrPageDeltaX: 1, lineOrPageDeltaY: 1 }, + expected: kScrollDown | kScrollRight + }, + { action: kDefaultActionScroll, override_x: kDefaultActionOverrideXScroll, + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: 0.5, deltaY: 1.0, + lineOrPageDeltaX: 1, lineOrPageDeltaY: 1 }, + expected: kScrollDown | kScrollRight + }, + { action: kDefaultActionNone, override_x: kDefaultActionOverrideXNoOverride, + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: 0.5, deltaY: 1.0, + lineOrPageDeltaX: 1, lineOrPageDeltaY: 1 }, + expected: kNoScroll + }, + { action: kDefaultActionNone, override_x: kDefaultActionOverrideXNone, + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: 0.5, deltaY: 1.0, + lineOrPageDeltaX: 1, lineOrPageDeltaY: 1 }, + expected: kNoScroll + }, + { action: kDefaultActionNone, override_x: kDefaultActionOverrideXScroll, + event: { deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaX: 0.5, deltaY: 1.0, + lineOrPageDeltaX: 1, lineOrPageDeltaY: 1 }, + expected: kNoScroll + }, + ]; + + var index = 0; + + function doIt() + { + const kTest = kTests[index]; + SpecialPowers.pushPrefEnv({"set": [ + ["mousewheel.default.action", kTest.action], + ["mousewheel.default.action.override_x", kTest.override_x]]}, + doIt2 + ); + } + + function doIt2() + { + const kTest = kTests[index]; + description = "doTestActionOverride(action=" + kTest.action + ", " + + "override_x=" + kTest.override_x + ", " + + "deltaX=" + kTest.event.deltaX + ", " + + "deltaY=" + kTest.event.deltaY + "): "; + gScrollableElement.scrollTop = 1000; + gScrollableElement.scrollLeft = 1000; + sendWheelAndWait(10, 10, kTest.event, function () { + if (kTest.expected & kScrollUp) { + ok(gScrollableElement.scrollTop < 1000, description + "not scrolled up, got " + gScrollableElement.scrollTop); + } else if (kTest.expected & kScrollDown) { + ok(gScrollableElement.scrollTop > 1000, description + "not scrolled down, got " + gScrollableElement.scrollTop); + } else { + is(gScrollableElement.scrollTop, 1000, description + "scrolled vertical"); + } + if (kTest.expected & kScrollLeft) { + ok(gScrollableElement.scrollLeft < 1000, description + "not scrolled to left, got " + gScrollableElement.scrollLeft); + } else if (kTest.expected & kScrollRight) { + ok(gScrollableElement.scrollLeft > 1000, description + "not scrolled to right, got " + gScrollableElement.scrollLeft); + } else { + is(gScrollableElement.scrollLeft, 1000, description + "scrolled horizontal"); + } + if (++index == kTests.length) { + SimpleTest.executeSoon(aCallback); + } else { + doIt(); + } + }); + } + doIt(); +} + +function runTests() +{ + SpecialPowers.pushPrefEnv({"set": [ + ["test.events.async.enabled", true], + ["general.smoothScroll", false], + ["mousewheel.default.action", kDefaultActionScroll], + ["mousewheel.default.action.override_x", kDefaultActionOverrideXNoOverride], + ["mousewheel.with_shift.action", kDefaultActionHorizontalizedScroll], + ["mousewheel.with_shift.action.override_x", kDefaultActionOverrideXNoOverride], + ["mousewheel.with_control.action", kDefaultActionZoom], + ["mousewheel.with_control.action.override_x", kDefaultActionOverrideXNoOverride], + ["mousewheel.with_alt.action", kDefaultActionHistory], + ["mousewheel.with_alt.action.override_x", kDefaultActionOverrideXNoOverride]]}, + runTests2); +} + +function runTests2() +{ + const kSettings = [ + { description: "all delta values are not customized", + deltaMultiplierX: 1.0, deltaMultiplierY: 1.0, deltaMultiplierZ: 1.0 }, + { description: "deltaX is reverted", + deltaMultiplierX: -1.0, deltaMultiplierY: 1.0, deltaMultiplierZ: 1.0 }, + { description: "deltaY is reverted", + deltaMultiplierX: 1.0, deltaMultiplierY: -1.0, deltaMultiplierZ: 1.0 }, + // Unless you really don't care a snap on time-consuming testing or a strict + // criteria is required for testing, it is strongly recommeneded that you + // comment the unrealistic case out. + //{ description: "deltaZ is reverted", + // deltaMultiplierX: 1.0, deltaMultiplierY: 1.0, deltaMultiplierZ: -1.0 },*/ + { description: "deltaX is 2.0", + deltaMultiplierX: 2.0, deltaMultiplierY: 1.0, deltaMultiplierZ: 1.0 }, + { description: "deltaY is 2.0", + deltaMultiplierX: 1.0, deltaMultiplierY: 2.0, deltaMultiplierZ: 1.0 }, + // Unless you really don't care a snap on time-consuming testing or a strict + // criteria is required for testing, it is strongly recommeneded that you + // comment the unrealistic case out. + //{ description: "deltaZ is 2.0", + // deltaMultiplierX: 1.0, deltaMultiplierY: 1.0, deltaMultiplierZ: 2.0 }, + //{ description: "deltaX is -2.0", + // deltaMultiplierX: -2.0, deltaMultiplierY: 1.0, deltaMultiplierZ: 1.0 }, + //{ description: "deltaY is -2.0", + // deltaMultiplierX: 1.0, deltaMultiplierY: -2.0, deltaMultiplierZ: 1.0 }, + //{ description: "deltaZ is -2.0", + // deltaMultiplierX: 1.0, deltaMultiplierY: 1.0, deltaMultiplierZ: -2.0 }, + ]; + + var index = 0; + + function doTest() { + setDeltaMultiplierSettings(kSettings[index], function () { + doTestScroll(kSettings[index], function () { + doTestAutoDirScroll(kSettings[index], {honoursRoot: false}, function () { + doTestAutoDirScroll(kSettings[index], {honoursRoot: true}, function () { + doTestHorizontalizedScroll(kSettings[index], function() { + doTestZoom(kSettings[index], function() { + if (++index == kSettings.length) { + setDeltaMultiplierSettings(kSettings[0], function() { + doTestZoomedScroll(function() { + doTestWholeScroll(function() { + doTestActionOverride(function() { + finishTests(); + }); + }); + }); + }); + } else { + doTest(); + } + }); + }); + }); + }); + }); + }); + } + doTest(); +} + +function finishTests() +{ + winUtils.restoreNormalRefresh(); + + window.opener.finish(); +} + +</script> +</pre> +</body> +</html> |