summaryrefslogtreecommitdiffstats
path: root/dom/inputmethod
diff options
context:
space:
mode:
Diffstat (limited to 'dom/inputmethod')
-rw-r--r--dom/inputmethod/HardwareKeyHandler.cpp562
-rw-r--r--dom/inputmethod/HardwareKeyHandler.h224
-rw-r--r--dom/inputmethod/InputMethod.manifest2
-rw-r--r--dom/inputmethod/Keyboard.jsm644
-rw-r--r--dom/inputmethod/MozKeyboard.js1255
-rw-r--r--dom/inputmethod/forms.js1561
-rw-r--r--dom/inputmethod/jar.mn6
-rw-r--r--dom/inputmethod/mochitest/bug1110030_helper.js267
-rw-r--r--dom/inputmethod/mochitest/chrome.ini52
-rw-r--r--dom/inputmethod/mochitest/file_blank.html4
-rw-r--r--dom/inputmethod/mochitest/file_inputmethod.html25
-rw-r--r--dom/inputmethod/mochitest/file_test_app.html11
-rw-r--r--dom/inputmethod/mochitest/file_test_bug1066515.html6
-rw-r--r--dom/inputmethod/mochitest/file_test_bug1137557.html6
-rw-r--r--dom/inputmethod/mochitest/file_test_bug1175399.html1
-rw-r--r--dom/inputmethod/mochitest/file_test_empty_app.html10
-rw-r--r--dom/inputmethod/mochitest/file_test_focus_blur_manage_events.html22
-rw-r--r--dom/inputmethod/mochitest/file_test_sendkey_cancel.html14
-rw-r--r--dom/inputmethod/mochitest/file_test_setSupportsSwitching.html5
-rw-r--r--dom/inputmethod/mochitest/file_test_simple_manage_events.html1
-rw-r--r--dom/inputmethod/mochitest/file_test_sms_app.html14
-rw-r--r--dom/inputmethod/mochitest/file_test_sms_app_1066515.html14
-rw-r--r--dom/inputmethod/mochitest/file_test_sync_edit.html1
-rw-r--r--dom/inputmethod/mochitest/file_test_two_inputs.html1
-rw-r--r--dom/inputmethod/mochitest/file_test_two_selects.html1
-rw-r--r--dom/inputmethod/mochitest/file_test_unload.html1
-rw-r--r--dom/inputmethod/mochitest/file_test_unload_action.html1
-rw-r--r--dom/inputmethod/mochitest/inputmethod_common.js24
-rw-r--r--dom/inputmethod/mochitest/test_basic.html212
-rw-r--r--dom/inputmethod/mochitest/test_bug1026997.html101
-rw-r--r--dom/inputmethod/mochitest/test_bug1043828.html183
-rw-r--r--dom/inputmethod/mochitest/test_bug1059163.html87
-rw-r--r--dom/inputmethod/mochitest/test_bug1066515.html93
-rw-r--r--dom/inputmethod/mochitest/test_bug1137557.html1799
-rw-r--r--dom/inputmethod/mochitest/test_bug1175399.html62
-rw-r--r--dom/inputmethod/mochitest/test_bug944397.html107
-rw-r--r--dom/inputmethod/mochitest/test_bug949059.html40
-rw-r--r--dom/inputmethod/mochitest/test_bug953044.html52
-rw-r--r--dom/inputmethod/mochitest/test_bug960946.html108
-rw-r--r--dom/inputmethod/mochitest/test_bug978918.html77
-rw-r--r--dom/inputmethod/mochitest/test_focus_blur_manage_events.html199
-rw-r--r--dom/inputmethod/mochitest/test_forward_hardware_key_to_ime.html149
-rw-r--r--dom/inputmethod/mochitest/test_input_registry_events.html251
-rw-r--r--dom/inputmethod/mochitest/test_sendkey_cancel.html67
-rw-r--r--dom/inputmethod/mochitest/test_setSupportsSwitching.html130
-rw-r--r--dom/inputmethod/mochitest/test_simple_manage_events.html154
-rw-r--r--dom/inputmethod/mochitest/test_sync_edit.html81
-rw-r--r--dom/inputmethod/mochitest/test_two_inputs.html184
-rw-r--r--dom/inputmethod/mochitest/test_two_selects.html182
-rw-r--r--dom/inputmethod/mochitest/test_unload.html167
-rw-r--r--dom/inputmethod/moz.build41
-rw-r--r--dom/inputmethod/nsIHardwareKeyHandler.idl142
52 files changed, 9403 insertions, 0 deletions
diff --git a/dom/inputmethod/HardwareKeyHandler.cpp b/dom/inputmethod/HardwareKeyHandler.cpp
new file mode 100644
index 000000000..737c30e5b
--- /dev/null
+++ b/dom/inputmethod/HardwareKeyHandler.cpp
@@ -0,0 +1,562 @@
+/* -*- 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 "HardwareKeyHandler.h"
+#include "mozilla/BasicEvents.h"
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/dom/KeyboardEvent.h"
+#include "mozilla/dom/TabParent.h"
+#include "mozilla/EventDispatcher.h"
+#include "mozilla/EventStateManager.h"
+#include "mozilla/TextEvents.h"
+#include "nsDeque.h"
+#include "nsFocusManager.h"
+#include "nsFrameLoader.h"
+#include "nsIContent.h"
+#include "nsIDOMHTMLDocument.h"
+#include "nsIDOMHTMLElement.h"
+#include "nsPIDOMWindow.h"
+#include "nsPresContext.h"
+#include "nsPresShell.h"
+
+namespace mozilla {
+
+using namespace dom;
+
+NS_IMPL_ISUPPORTS(HardwareKeyHandler, nsIHardwareKeyHandler)
+
+StaticRefPtr<HardwareKeyHandler> HardwareKeyHandler::sInstance;
+
+HardwareKeyHandler::HardwareKeyHandler()
+ : mInputMethodAppConnected(false)
+{
+}
+
+HardwareKeyHandler::~HardwareKeyHandler()
+{
+}
+
+NS_IMETHODIMP
+HardwareKeyHandler::OnInputMethodAppConnected()
+{
+ if (NS_WARN_IF(mInputMethodAppConnected)) {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ mInputMethodAppConnected = true;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+HardwareKeyHandler::OnInputMethodAppDisconnected()
+{
+ if (NS_WARN_IF(!mInputMethodAppConnected)) {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ mInputMethodAppConnected = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+HardwareKeyHandler::RegisterListener(nsIHardwareKeyEventListener* aListener)
+{
+ // Make sure the listener is not nullptr and there is no available
+ // hardwareKeyEventListener now
+ if (NS_WARN_IF(!aListener)) {
+ return NS_ERROR_NULL_POINTER;
+ }
+
+ if (NS_WARN_IF(mHardwareKeyEventListener)) {
+ return NS_ERROR_ALREADY_INITIALIZED;
+ }
+
+ mHardwareKeyEventListener = do_GetWeakReference(aListener);
+
+ if (NS_WARN_IF(!mHardwareKeyEventListener)) {
+ return NS_ERROR_NULL_POINTER;
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+HardwareKeyHandler::UnregisterListener()
+{
+ // Clear the HardwareKeyEventListener
+ mHardwareKeyEventListener = nullptr;
+ return NS_OK;
+}
+
+bool
+HardwareKeyHandler::ForwardKeyToInputMethodApp(nsINode* aTarget,
+ WidgetKeyboardEvent* aEvent,
+ nsEventStatus* aEventStatus)
+{
+ MOZ_ASSERT(aTarget, "No target provided");
+ MOZ_ASSERT(aEvent, "No event provided");
+
+ // No need to forward hardware key event to IME
+ // if key's defaultPrevented is true
+ if (aEvent->mFlags.mDefaultPrevented) {
+ return false;
+ }
+
+ // No need to forward hardware key event to IME if IME is disabled
+ if (!mInputMethodAppConnected) {
+ return false;
+ }
+
+ // No need to forward hardware key event to IME
+ // if this key event is generated by IME itself(from nsITextInputProcessor)
+ if (aEvent->mIsSynthesizedByTIP) {
+ return false;
+ }
+
+ // No need to forward hardware key event to IME
+ // if the key event is handling or already handled
+ if (aEvent->mInputMethodAppState != WidgetKeyboardEvent::eNotHandled) {
+ return false;
+ }
+
+ // No need to forward hardware key event to IME
+ // if there is no nsIHardwareKeyEventListener in use
+ nsCOMPtr<nsIHardwareKeyEventListener>
+ keyHandler(do_QueryReferent(mHardwareKeyEventListener));
+ if (!keyHandler) {
+ return false;
+ }
+
+ // Set the flags to specify the keyboard event is in forwarding phase.
+ aEvent->mInputMethodAppState = WidgetKeyboardEvent::eHandling;
+
+ // For those keypress events coming after their heading keydown's reply
+ // already arrives, they should be dispatched directly instead of
+ // being stored into the event queue. Otherwise, without the heading keydown
+ // in the event queue, the stored keypress will never be withdrawn to be fired.
+ if (aEvent->mMessage == eKeyPress && mEventQueue.IsEmpty()) {
+ DispatchKeyPress(aTarget, *aEvent, *aEventStatus);
+ return true;
+ }
+
+ // Push the key event into queue for reuse when its reply arrives.
+ KeyboardInfo* copiedInfo =
+ new KeyboardInfo(aTarget,
+ *aEvent,
+ aEventStatus ? *aEventStatus : nsEventStatus_eIgnore);
+
+ // No need to forward hardware key event to IME if the event queue is full
+ if (!mEventQueue.Push(copiedInfo)) {
+ delete copiedInfo;
+ return false;
+ }
+
+ // We only forward keydown and keyup event to input-method-app
+ // because input-method-app will generate keypress by itself.
+ if (aEvent->mMessage == eKeyPress) {
+ return true;
+ }
+
+ // Create a keyboard event to pass into
+ // nsIHardwareKeyEventListener.onHardwareKey
+ nsCOMPtr<EventTarget> eventTarget = do_QueryInterface(aTarget);
+ nsPresContext* presContext = GetPresContext(aTarget);
+ RefPtr<KeyboardEvent> keyboardEvent =
+ NS_NewDOMKeyboardEvent(eventTarget, presContext, aEvent->AsKeyboardEvent());
+ // Duplicate the internal event data in the heap for the keyboardEvent,
+ // or the internal data from |aEvent| in the stack may be destroyed by others.
+ keyboardEvent->DuplicatePrivateData();
+
+ // Forward the created keyboard event to input-method-app
+ bool isSent = false;
+ keyHandler->OnHardwareKey(keyboardEvent, &isSent);
+
+ // Pop the pending key event if it can't be forwarded
+ if (!isSent) {
+ mEventQueue.RemoveFront();
+ }
+
+ return isSent;
+}
+
+NS_IMETHODIMP
+HardwareKeyHandler::OnHandledByInputMethodApp(const nsAString& aType,
+ uint16_t aDefaultPrevented)
+{
+ // We can not handle this reply because the pending events had been already
+ // removed from the forwarding queue before this reply arrives.
+ if (mEventQueue.IsEmpty()) {
+ return NS_OK;
+ }
+
+ RefPtr<KeyboardInfo> keyInfo = mEventQueue.PopFront();
+
+ // Only allow keydown and keyup to call this method
+ if (NS_WARN_IF(aType.EqualsLiteral("keydown") &&
+ keyInfo->mEvent.mMessage != eKeyDown) ||
+ NS_WARN_IF(aType.EqualsLiteral("keyup") &&
+ keyInfo->mEvent.mMessage != eKeyUp)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ // The value of defaultPrevented depends on whether or not
+ // the key is consumed by input-method-app
+ SetDefaultPrevented(keyInfo->mEvent, aDefaultPrevented);
+
+ // Set the flag to specify the reply phase
+ keyInfo->mEvent.mInputMethodAppState = WidgetKeyboardEvent::eHandled;
+
+ // Check whether the event is still valid to be fired
+ if (CanDispatchEvent(keyInfo->mTarget, keyInfo->mEvent)) {
+ // If the key's defaultPrevented is true, it means that the
+ // input-method-app has already consumed this key,
+ // so we can dispatch |mozbrowserafterkey*| directly if
+ // preference "dom.beforeAfterKeyboardEvent.enabled" is enabled.
+ if (keyInfo->mEvent.mFlags.mDefaultPrevented) {
+ DispatchAfterKeyEvent(keyInfo->mTarget, keyInfo->mEvent);
+ // Otherwise, it means that input-method-app doesn't handle this key,
+ // so we need to dispatch it to its current event target.
+ } else {
+ DispatchToTargetApp(keyInfo->mTarget,
+ keyInfo->mEvent,
+ keyInfo->mStatus);
+ }
+ }
+
+ // No need to do further processing if the event is not keydown
+ if (keyInfo->mEvent.mMessage != eKeyDown) {
+ return NS_OK;
+ }
+
+ // Update the latest keydown data:
+ // Release the holding reference to the previous keydown's data and
+ // add a reference count to the current keydown's data.
+ mLatestKeyDownInfo = keyInfo;
+
+ // Handle the pending keypress event once keydown's reply arrives:
+ // It may have many keypress events per keydown on some platforms,
+ // so we use loop to dispatch keypress events.
+ // (But Gonk dispatch only one keypress per keydown)
+ // However, if there is no keypress after this keydown,
+ // then those following keypress will be handled in
+ // ForwardKeyToInputMethodApp directly.
+ for (KeyboardInfo* keypressInfo;
+ !mEventQueue.IsEmpty() &&
+ (keypressInfo = mEventQueue.PeekFront()) &&
+ keypressInfo->mEvent.mMessage == eKeyPress;
+ mEventQueue.RemoveFront()) {
+ DispatchKeyPress(keypressInfo->mTarget,
+ keypressInfo->mEvent,
+ keypressInfo->mStatus);
+ }
+
+ return NS_OK;
+}
+
+bool
+HardwareKeyHandler::DispatchKeyPress(nsINode* aTarget,
+ WidgetKeyboardEvent& aEvent,
+ nsEventStatus& aStatus)
+{
+ MOZ_ASSERT(aTarget, "No target provided");
+ MOZ_ASSERT(aEvent.mMessage == eKeyPress, "Event is not keypress");
+
+ // No need to dispatch keypress to the event target
+ // if the keydown event is consumed by the input-method-app.
+ if (mLatestKeyDownInfo &&
+ mLatestKeyDownInfo->mEvent.mFlags.mDefaultPrevented) {
+ return false;
+ }
+
+ // No need to dispatch keypress to the event target
+ // if the previous keydown event is modifier key's
+ if (mLatestKeyDownInfo &&
+ mLatestKeyDownInfo->mEvent.IsModifierKeyEvent()) {
+ return false;
+ }
+
+ // No need to dispatch keypress to the event target
+ // if it's invalid to be dispatched
+ if (!CanDispatchEvent(aTarget, aEvent)) {
+ return false;
+ }
+
+ // Set the flag to specify the reply phase.
+ aEvent.mInputMethodAppState = WidgetKeyboardEvent::eHandled;
+
+ // Dispatch the pending keypress event
+ bool ret = DispatchToTargetApp(aTarget, aEvent, aStatus);
+
+ // Re-trigger EventStateManager::PostHandleKeyboardEvent for keypress
+ PostHandleKeyboardEvent(aTarget, aEvent, aStatus);
+
+ return ret;
+}
+
+void
+HardwareKeyHandler::DispatchAfterKeyEvent(nsINode* aTarget,
+ WidgetKeyboardEvent& aEvent)
+{
+ MOZ_ASSERT(aTarget, "No target provided");
+
+ if (!PresShell::BeforeAfterKeyboardEventEnabled() ||
+ aEvent.mMessage == eKeyPress) {
+ return;
+ }
+
+ nsCOMPtr<nsIPresShell> presShell = GetPresShell(aTarget);
+ if (NS_WARN_IF(presShell)) {
+ presShell->DispatchAfterKeyboardEvent(aTarget,
+ aEvent,
+ aEvent.mFlags.mDefaultPrevented);
+ }
+}
+
+bool
+HardwareKeyHandler::DispatchToTargetApp(nsINode* aTarget,
+ WidgetKeyboardEvent& aEvent,
+ nsEventStatus& aStatus)
+{
+ MOZ_ASSERT(aTarget, "No target provided");
+
+ // Get current focused element as the event target
+ nsCOMPtr<nsIContent> currentTarget = GetCurrentTarget();
+ if (NS_WARN_IF(!currentTarget)) {
+ return false;
+ }
+
+ // The event target should be set to the current focused element.
+ // However, it might have security issue if the event is dispatched to
+ // the unexpected application, and it might cause unexpected operation
+ // in the new app.
+ nsCOMPtr<nsPIDOMWindowOuter> originalRootWindow = GetRootWindow(aTarget);
+ nsCOMPtr<nsPIDOMWindowOuter> currentRootWindow = GetRootWindow(currentTarget);
+ if (currentRootWindow != originalRootWindow) {
+ NS_WARNING("The root window is changed during the event is dispatching");
+ return false;
+ }
+
+ // If the current focused element is still in the same app,
+ // then we can use it as the current target to dispatch event.
+ nsCOMPtr<nsIPresShell> presShell = GetPresShell(currentTarget);
+ if (!presShell) {
+ return false;
+ }
+
+ if (!presShell->CanDispatchEvent(&aEvent)) {
+ return false;
+ }
+
+ // In-process case: the event target is in the current process
+ if (!PresShell::IsTargetIframe(currentTarget)) {
+ DispatchToCurrentProcess(presShell, currentTarget, aEvent, aStatus);
+
+ if (presShell->CanDispatchEvent(&aEvent)) {
+ DispatchAfterKeyEvent(aTarget, aEvent);
+ }
+
+ return true;
+ }
+
+ // OOP case: the event target is in the child process
+ return DispatchToCrossProcess(aTarget, aEvent);
+
+ // After the oop target receives the event from TabChild::RecvRealKeyEvent
+ // and return the result through TabChild::SendDispatchAfterKeyboardEvent,
+ // the |mozbrowserafterkey*| will be fired from
+ // TabParent::RecvDispatchAfterKeyboardEvent, so we don't need to dispatch
+ // |mozbrowserafterkey*| by ourselves in this module.
+}
+
+void
+HardwareKeyHandler::DispatchToCurrentProcess(nsIPresShell* presShell,
+ nsIContent* aTarget,
+ WidgetKeyboardEvent& aEvent,
+ nsEventStatus& aStatus)
+{
+ EventDispatcher::Dispatch(aTarget, presShell->GetPresContext(),
+ &aEvent, nullptr, &aStatus, nullptr);
+}
+
+bool
+HardwareKeyHandler::DispatchToCrossProcess(nsINode* aTarget,
+ WidgetKeyboardEvent& aEvent)
+{
+ nsCOMPtr<nsIFrameLoaderOwner> remoteLoaderOwner = do_QueryInterface(aTarget);
+ if (NS_WARN_IF(!remoteLoaderOwner)) {
+ return false;
+ }
+
+ RefPtr<nsFrameLoader> remoteFrameLoader =
+ remoteLoaderOwner->GetFrameLoader();
+ if (NS_WARN_IF(!remoteFrameLoader)) {
+ return false;
+ }
+
+ uint32_t eventMode;
+ remoteFrameLoader->GetEventMode(&eventMode);
+ if (eventMode == nsIFrameLoader::EVENT_MODE_DONT_FORWARD_TO_CHILD) {
+ return false;
+ }
+
+ PBrowserParent* remoteBrowser = remoteFrameLoader->GetRemoteBrowser();
+ TabParent* remote = static_cast<TabParent*>(remoteBrowser);
+ if (NS_WARN_IF(!remote)) {
+ return false;
+ }
+
+ return remote->SendRealKeyEvent(aEvent);
+}
+
+void
+HardwareKeyHandler::PostHandleKeyboardEvent(nsINode* aTarget,
+ WidgetKeyboardEvent& aEvent,
+ nsEventStatus& aStatus)
+{
+ MOZ_ASSERT(aTarget, "No target provided");
+
+ nsPresContext* presContext = GetPresContext(aTarget);
+
+ RefPtr<mozilla::EventStateManager> esm = presContext->EventStateManager();
+ bool dispatchedToChildProcess = PresShell::IsTargetIframe(aTarget);
+ esm->PostHandleKeyboardEvent(&aEvent, aStatus, dispatchedToChildProcess);
+}
+
+void
+HardwareKeyHandler::SetDefaultPrevented(WidgetKeyboardEvent& aEvent,
+ uint16_t aDefaultPrevented) {
+ if (aDefaultPrevented & DEFAULT_PREVENTED) {
+ aEvent.mFlags.mDefaultPrevented = true;
+ }
+
+ if (aDefaultPrevented & DEFAULT_PREVENTED_BY_CHROME) {
+ aEvent.mFlags.mDefaultPreventedByChrome = true;
+ }
+
+ if (aDefaultPrevented & DEFAULT_PREVENTED_BY_CONTENT) {
+ aEvent.mFlags.mDefaultPreventedByContent = true;
+ }
+}
+
+bool
+HardwareKeyHandler::CanDispatchEvent(nsINode* aTarget,
+ WidgetKeyboardEvent& aEvent)
+{
+ nsCOMPtr<nsIPresShell> presShell = GetPresShell(aTarget);
+ if (NS_WARN_IF(!presShell)) {
+ return false;
+ }
+ return presShell->CanDispatchEvent(&aEvent);
+}
+
+already_AddRefed<nsPIDOMWindowOuter>
+HardwareKeyHandler::GetRootWindow(nsINode* aNode)
+{
+ // Get nsIPresShell's pointer first
+ nsCOMPtr<nsIPresShell> presShell = GetPresShell(aNode);
+ if (NS_WARN_IF(!presShell)) {
+ return nullptr;
+ }
+ nsCOMPtr<nsPIDOMWindowOuter> rootWindow = presShell->GetRootWindow();
+ return rootWindow.forget();
+}
+
+already_AddRefed<nsIContent>
+HardwareKeyHandler::GetCurrentTarget()
+{
+ nsFocusManager* fm = nsFocusManager::GetFocusManager();
+ if (NS_WARN_IF(!fm)) {
+ return nullptr;
+ }
+
+ nsCOMPtr<mozIDOMWindowProxy> focusedWindow;
+ fm->GetFocusedWindow(getter_AddRefs(focusedWindow));
+ if (NS_WARN_IF(!focusedWindow)) {
+ return nullptr;
+ }
+
+ auto* ourWindow = nsPIDOMWindowOuter::From(focusedWindow);
+
+ nsCOMPtr<nsPIDOMWindowOuter> rootWindow = ourWindow->GetPrivateRoot();
+ if (NS_WARN_IF(!rootWindow)) {
+ return nullptr;
+ }
+
+ nsCOMPtr<nsPIDOMWindowOuter> focusedFrame;
+ nsCOMPtr<nsIContent> focusedContent =
+ fm->GetFocusedDescendant(rootWindow, true, getter_AddRefs(focusedFrame));
+
+ // If there is no focus, then we use document body instead
+ if (NS_WARN_IF(!focusedContent || !focusedContent->GetPrimaryFrame())) {
+ nsIDocument* document = ourWindow->GetExtantDoc();
+ if (NS_WARN_IF(!document)) {
+ return nullptr;
+ }
+
+ focusedContent = document->GetRootElement();
+
+ nsCOMPtr<nsIDOMHTMLDocument> htmlDocument = do_QueryInterface(document);
+ if (htmlDocument) {
+ nsCOMPtr<nsIDOMHTMLElement> body;
+ htmlDocument->GetBody(getter_AddRefs(body));
+ nsCOMPtr<nsIContent> bodyContent = do_QueryInterface(body);
+ if (bodyContent) {
+ focusedContent = bodyContent;
+ }
+ }
+ }
+
+ return focusedContent ? focusedContent.forget() : nullptr;
+}
+
+nsPresContext*
+HardwareKeyHandler::GetPresContext(nsINode* aNode)
+{
+ // Get nsIPresShell's pointer first
+ nsCOMPtr<nsIPresShell> presShell = GetPresShell(aNode);
+ if (NS_WARN_IF(!presShell)) {
+ return nullptr;
+ }
+
+ // then use nsIPresShell to get nsPresContext's pointer
+ return presShell->GetPresContext();
+}
+
+already_AddRefed<nsIPresShell>
+HardwareKeyHandler::GetPresShell(nsINode* aNode)
+{
+ nsIDocument* doc = aNode->OwnerDoc();
+ if (NS_WARN_IF(!doc)) {
+ return nullptr;
+ }
+
+ nsCOMPtr<nsIPresShell> presShell = doc->GetShell();
+ if (NS_WARN_IF(!presShell)) {
+ return nullptr;
+ }
+
+ return presShell.forget();
+}
+
+/* static */
+already_AddRefed<HardwareKeyHandler>
+HardwareKeyHandler::GetInstance()
+{
+ if (!XRE_IsParentProcess()) {
+ return nullptr;
+ }
+
+ if (!sInstance) {
+ sInstance = new HardwareKeyHandler();
+ ClearOnShutdown(&sInstance);
+ }
+
+ RefPtr<HardwareKeyHandler> service = sInstance.get();
+ return service.forget();
+}
+
+} // namespace mozilla
diff --git a/dom/inputmethod/HardwareKeyHandler.h b/dom/inputmethod/HardwareKeyHandler.h
new file mode 100644
index 000000000..7520c40cd
--- /dev/null
+++ b/dom/inputmethod/HardwareKeyHandler.h
@@ -0,0 +1,224 @@
+/* -*- 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_HardwareKeyHandler_h_
+#define mozilla_HardwareKeyHandler_h_
+
+#include "mozilla/EventForwards.h" // for nsEventStatus
+#include "mozilla/StaticPtr.h"
+#include "mozilla/TextEvents.h"
+#include "nsCOMPtr.h"
+#include "nsDeque.h"
+#include "nsIHardwareKeyHandler.h"
+#include "nsIWeakReferenceUtils.h" // for nsWeakPtr
+
+class nsIContent;
+class nsINode;
+class nsIPresShell;
+class nsPIDOMWindowOuter;
+class nsPresContext;
+
+namespace mozilla {
+
+// This module will copy the events' data into its event queue for reuse
+// after receiving input-method-app's reply, so we use the following struct
+// for storing these information.
+// RefCounted<T> is a helper class for adding reference counting mechanism.
+struct KeyboardInfo : public RefCounted<KeyboardInfo>
+{
+ MOZ_DECLARE_REFCOUNTED_TYPENAME(KeyboardInfo)
+
+ nsINode* mTarget;
+ WidgetKeyboardEvent mEvent;
+ nsEventStatus mStatus;
+
+ KeyboardInfo(nsINode* aTarget,
+ WidgetKeyboardEvent& aEvent,
+ nsEventStatus aStatus)
+ : mTarget(aTarget)
+ , mEvent(aEvent)
+ , mStatus(aStatus)
+ {
+ }
+};
+
+// The following is the type-safe wrapper around nsDeque
+// for storing events' data.
+// The T must be one class that supports reference counting mechanism.
+// The EventQueueDeallocator will be called in nsDeque::~nsDeque() or
+// nsDeque::Erase() to deallocate the objects. nsDeque::Erase() will remove
+// and delete all items in the queue. See more from nsDeque.h.
+template <class T>
+class EventQueueDeallocator : public nsDequeFunctor
+{
+ virtual void* operator() (void* aObject)
+ {
+ RefPtr<T> releaseMe = dont_AddRef(static_cast<T*>(aObject));
+ return nullptr;
+ }
+};
+
+// The type-safe queue to be used to store the KeyboardInfo data
+template <class T>
+class EventQueue : private nsDeque
+{
+public:
+ EventQueue()
+ : nsDeque(new EventQueueDeallocator<T>())
+ {
+ };
+
+ ~EventQueue()
+ {
+ Clear();
+ }
+
+ inline size_t GetSize()
+ {
+ return nsDeque::GetSize();
+ }
+
+ bool IsEmpty()
+ {
+ return !nsDeque::GetSize();
+ }
+
+ inline bool Push(T* aItem)
+ {
+ MOZ_ASSERT(aItem);
+ NS_ADDREF(aItem);
+ size_t sizeBefore = GetSize();
+ nsDeque::Push(aItem);
+ if (GetSize() != sizeBefore + 1) {
+ NS_RELEASE(aItem);
+ return false;
+ }
+ return true;
+ }
+
+ inline already_AddRefed<T> PopFront()
+ {
+ RefPtr<T> rv = dont_AddRef(static_cast<T*>(nsDeque::PopFront()));
+ return rv.forget();
+ }
+
+ inline void RemoveFront()
+ {
+ RefPtr<T> releaseMe = PopFront();
+ }
+
+ inline T* PeekFront()
+ {
+ return static_cast<T*>(nsDeque::PeekFront());
+ }
+
+ void Clear()
+ {
+ while (GetSize() > 0) {
+ RemoveFront();
+ }
+ }
+};
+
+class HardwareKeyHandler : public nsIHardwareKeyHandler
+{
+public:
+ HardwareKeyHandler();
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIHARDWAREKEYHANDLER
+
+ static already_AddRefed<HardwareKeyHandler> GetInstance();
+
+ virtual bool ForwardKeyToInputMethodApp(nsINode* aTarget,
+ WidgetKeyboardEvent* aEvent,
+ nsEventStatus* aEventStatus) override;
+
+private:
+ virtual ~HardwareKeyHandler();
+
+ // Return true if the keypress is successfully dispatched.
+ // Otherwise, return false.
+ bool DispatchKeyPress(nsINode* aTarget,
+ WidgetKeyboardEvent& aEvent,
+ nsEventStatus& aStatus);
+
+ void DispatchAfterKeyEvent(nsINode* aTarget, WidgetKeyboardEvent& aEvent);
+
+ void DispatchToCurrentProcess(nsIPresShell* aPresShell,
+ nsIContent* aTarget,
+ WidgetKeyboardEvent& aEvent,
+ nsEventStatus& aStatus);
+
+ bool DispatchToCrossProcess(nsINode* aTarget, WidgetKeyboardEvent& aEvent);
+
+ // This method will dispatch not only key* event to its event target,
+ // no mather it's in the current process or in its child process,
+ // but also mozbrowserafterkey* to the corresponding target if it needs.
+ // Return true if the key is successfully dispatched.
+ // Otherwise, return false.
+ bool DispatchToTargetApp(nsINode* aTarget,
+ WidgetKeyboardEvent& aEvent,
+ nsEventStatus& aStatus);
+
+ // This method will be called after dispatching keypress to its target,
+ // if the input-method-app doesn't handle the key.
+ // In normal dispatching path, EventStateManager::PostHandleKeyboardEvent
+ // will be called when event is keypress.
+ // However, the ::PostHandleKeyboardEvent mentioned above will be aborted
+ // when we try to forward key event to the input-method-app.
+ // If the input-method-app consumes the key, then we don't need to do anything
+ // because the input-method-app will generate a new key event by itself.
+ // On the other hand, if the input-method-app doesn't consume the key,
+ // then we need to dispatch the key event by ourselves
+ // and call ::PostHandleKeyboardEvent again after the event is forwarded.
+ // Note that the EventStateManager::PreHandleEvent is already called before
+ // forwarding, so we don't need to call it in this module.
+ void PostHandleKeyboardEvent(nsINode* aTarget,
+ WidgetKeyboardEvent& aEvent,
+ nsEventStatus& aStatus);
+
+ void SetDefaultPrevented(WidgetKeyboardEvent& aEvent,
+ uint16_t aDefaultPrevented);
+
+ // Check whether the event is valid to be fired.
+ // This method should be called every time before dispatching next event.
+ bool CanDispatchEvent(nsINode* aTarget,
+ WidgetKeyboardEvent& aEvent);
+
+ already_AddRefed<nsPIDOMWindowOuter> GetRootWindow(nsINode* aNode);
+
+ already_AddRefed<nsIContent> GetCurrentTarget();
+
+ nsPresContext* GetPresContext(nsINode* aNode);
+
+ already_AddRefed<nsIPresShell> GetPresShell(nsINode* aNode);
+
+ static StaticRefPtr<HardwareKeyHandler> sInstance;
+
+ // The event queue is used to store the forwarded keyboard events.
+ // Those stored events will be dispatched if input-method-app doesn't
+ // consume them.
+ EventQueue<KeyboardInfo> mEventQueue;
+
+ // Hold the pointer to the latest keydown's data
+ RefPtr<KeyboardInfo> mLatestKeyDownInfo;
+
+ // input-method-app needs to register a listener by
+ // |nsIHardwareKeyHandler.registerListener| to receive
+ // the hardware keyboard event, and |nsIHardwareKeyHandler.registerListener|
+ // will set an nsIHardwareKeyEventListener to mHardwareKeyEventListener.
+ // Then, mHardwareKeyEventListener is used to forward the event
+ // to the input-method-app.
+ nsWeakPtr mHardwareKeyEventListener;
+
+ // To keep tracking the input-method-app is active or disabled.
+ bool mInputMethodAppConnected;
+};
+
+} // namespace mozilla
+
+#endif // #ifndef mozilla_HardwareKeyHandler_h_
diff --git a/dom/inputmethod/InputMethod.manifest b/dom/inputmethod/InputMethod.manifest
new file mode 100644
index 000000000..5dc073508
--- /dev/null
+++ b/dom/inputmethod/InputMethod.manifest
@@ -0,0 +1,2 @@
+component {4607330d-e7d2-40a4-9eb8-43967eae0142} MozKeyboard.js
+contract @mozilla.org/b2g-inputmethod;1 {4607330d-e7d2-40a4-9eb8-43967eae0142}
diff --git a/dom/inputmethod/Keyboard.jsm b/dom/inputmethod/Keyboard.jsm
new file mode 100644
index 000000000..22f87ffbc
--- /dev/null
+++ b/dom/inputmethod/Keyboard.jsm
@@ -0,0 +1,644 @@
+/* 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';
+
+this.EXPORTED_SYMBOLS = ['Keyboard'];
+
+const Cu = Components.utils;
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+
+Cu.import('resource://gre/modules/Services.jsm');
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(this, "ppmm",
+ "@mozilla.org/parentprocessmessagemanager;1", "nsIMessageBroadcaster");
+
+XPCOMUtils.defineLazyModuleGetter(this, "SystemAppProxy",
+ "resource://gre/modules/SystemAppProxy.jsm");
+
+XPCOMUtils.defineLazyGetter(this, "appsService", function() {
+ return Cc["@mozilla.org/AppsService;1"].getService(Ci.nsIAppsService);
+});
+
+XPCOMUtils.defineLazyGetter(this, "hardwareKeyHandler", function() {
+#ifdef MOZ_B2G
+ return Cc["@mozilla.org/HardwareKeyHandler;1"]
+ .getService(Ci.nsIHardwareKeyHandler);
+#else
+ return null;
+#endif
+});
+
+var Utils = {
+ getMMFromMessage: function u_getMMFromMessage(msg) {
+ let mm;
+ try {
+ mm = msg.target.QueryInterface(Ci.nsIFrameLoaderOwner)
+ .frameLoader.messageManager;
+ } catch(e) {
+ mm = msg.target;
+ }
+
+ return mm;
+ },
+ checkPermissionForMM: function u_checkPermissionForMM(mm, permName) {
+ return mm.assertPermission(permName);
+ }
+};
+
+this.Keyboard = {
+#ifdef MOZ_B2G
+ // For receving keyboard event fired from hardware before it's dispatched,
+ // |this| object is used to be the listener to get the forwarded event.
+ // As the listener, |this| object must implement nsIHardwareKeyEventListener
+ // and nsSupportsWeakReference.
+ // Please see nsIHardwareKeyHandler.idl to get more information.
+ QueryInterface: XPCOMUtils.generateQI([
+ Ci.nsIHardwareKeyEventListener,
+ Ci.nsISupportsWeakReference
+ ]),
+#endif
+ _isConnectedToHardwareKeyHandler: false,
+ _formMM: null, // The current web page message manager.
+ _keyboardMM: null, // The keyboard app message manager.
+ _keyboardID: -1, // The keyboard app's ID number. -1 = invalid
+ _nextKeyboardID: 0, // The ID number counter.
+ _systemMMs: [], // The message managers registered to handle system async
+ // messages.
+ _supportsSwitchingTypes: [],
+ _systemMessageNames: [
+ 'SetValue', 'RemoveFocus', 'SetSelectedOption', 'SetSelectedOptions',
+ 'SetSupportsSwitchingTypes', 'RegisterSync', 'Unregister'
+ ],
+
+ _messageNames: [
+ 'RemoveFocus',
+ 'SetSelectionRange', 'ReplaceSurroundingText', 'ShowInputMethodPicker',
+ 'SwitchToNextInputMethod', 'HideInputMethod',
+ 'SendKey', 'GetContext',
+ 'SetComposition', 'EndComposition',
+ 'RegisterSync', 'Unregister',
+ 'ReplyHardwareKeyEvent'
+ ],
+
+ get formMM() {
+ if (this._formMM && !Cu.isDeadWrapper(this._formMM))
+ return this._formMM;
+
+ return null;
+ },
+
+ set formMM(mm) {
+ this._formMM = mm;
+ },
+
+ sendToForm: function(name, data) {
+ if (!this.formMM) {
+ dump("Keyboard.jsm: Attempt to send message " + name +
+ " to form but no message manager exists.\n");
+
+ return;
+ }
+ try {
+ this.formMM.sendAsyncMessage(name, data);
+ } catch(e) { }
+ },
+
+ sendToKeyboard: function(name, data) {
+ try {
+ this._keyboardMM.sendAsyncMessage(name, data);
+ } catch(e) {
+ return false;
+ }
+ return true;
+ },
+
+ sendToSystem: function(name, data) {
+ if (!this._systemMMs.length) {
+ dump("Keyboard.jsm: Attempt to send message " + name +
+ " to system but no message manager registered.\n");
+
+ return;
+ }
+
+ this._systemMMs.forEach((mm, i) => {
+ data.inputManageId = i;
+ mm.sendAsyncMessage(name, data);
+ });
+ },
+
+ init: function keyboardInit() {
+ Services.obs.addObserver(this, 'inprocess-browser-shown', false);
+ Services.obs.addObserver(this, 'remote-browser-shown', false);
+ Services.obs.addObserver(this, 'oop-frameloader-crashed', false);
+ Services.obs.addObserver(this, 'message-manager-close', false);
+
+ // For receiving the native hardware keyboard event
+ if (hardwareKeyHandler) {
+ hardwareKeyHandler.registerListener(this);
+ }
+
+ for (let name of this._messageNames) {
+ ppmm.addMessageListener('Keyboard:' + name, this);
+ }
+
+ for (let name of this._systemMessageNames) {
+ ppmm.addMessageListener('System:' + name, this);
+ }
+
+ this.inputRegistryGlue = new InputRegistryGlue();
+ },
+
+ // This method will be registered into nsIHardwareKeyHandler:
+ // Send the initialized dictionary retrieved from the native keyboard event
+ // to input-method-app for generating a new event.
+ onHardwareKey: function onHardwareKeyReceived(evt) {
+ return this.sendToKeyboard('Keyboard:ReceiveHardwareKeyEvent', {
+ type: evt.type,
+ keyDict: evt.initDict
+ });
+ },
+
+ observe: function keyboardObserve(subject, topic, data) {
+ let frameLoader = null;
+ let mm = null;
+
+ if (topic == 'message-manager-close') {
+ mm = subject;
+ } else {
+ frameLoader = subject.QueryInterface(Ci.nsIFrameLoader);
+ mm = frameLoader.messageManager;
+ }
+
+ if (topic == 'oop-frameloader-crashed' ||
+ topic == 'message-manager-close') {
+ if (this.formMM == mm) {
+ // The application has been closed unexpectingly. Let's tell the
+ // keyboard app that the focus has been lost.
+ this.sendToKeyboard('Keyboard:Blur', {});
+ // Notify system app to hide keyboard.
+ this.sendToSystem('System:Blur', {});
+ // XXX: To be removed when content migrate away from mozChromeEvents.
+ SystemAppProxy.dispatchEvent({
+ type: 'inputmethod-contextchange',
+ inputType: 'blur'
+ });
+
+ this.formMM = null;
+ }
+ } else {
+ // Ignore notifications that aren't from a BrowserOrApp
+ if (!frameLoader.ownerIsMozBrowserOrAppFrame) {
+ return;
+ }
+ this.initFormsFrameScript(mm);
+ }
+ },
+
+ initFormsFrameScript: function(mm) {
+ mm.addMessageListener('Forms:Focus', this);
+ mm.addMessageListener('Forms:Blur', this);
+ mm.addMessageListener('Forms:SelectionChange', this);
+ mm.addMessageListener('Forms:SetSelectionRange:Result:OK', this);
+ mm.addMessageListener('Forms:SetSelectionRange:Result:Error', this);
+ mm.addMessageListener('Forms:ReplaceSurroundingText:Result:OK', this);
+ mm.addMessageListener('Forms:ReplaceSurroundingText:Result:Error', this);
+ mm.addMessageListener('Forms:SendKey:Result:OK', this);
+ mm.addMessageListener('Forms:SendKey:Result:Error', this);
+ mm.addMessageListener('Forms:SequenceError', this);
+ mm.addMessageListener('Forms:GetContext:Result:OK', this);
+ mm.addMessageListener('Forms:SetComposition:Result:OK', this);
+ mm.addMessageListener('Forms:EndComposition:Result:OK', this);
+ },
+
+ receiveMessage: function keyboardReceiveMessage(msg) {
+ // If we get a 'Keyboard:XXX'/'System:XXX' message, check that the sender
+ // has the required permission.
+ let mm;
+
+ // Assert the permission based on the prefix of the message.
+ let permName;
+ if (msg.name.startsWith("Keyboard:")) {
+ permName = "input";
+ } else if (msg.name.startsWith("System:")) {
+ permName = "input-manage";
+ }
+
+ // There is no permission to check (nor we need to get the mm)
+ // for Form: messages.
+ if (permName) {
+ mm = Utils.getMMFromMessage(msg);
+ if (!mm) {
+ dump("Keyboard.jsm: Message " + msg.name + " has no message manager.");
+ return;
+ }
+ if (!Utils.checkPermissionForMM(mm, permName)) {
+ dump("Keyboard.jsm: Message " + msg.name +
+ " from a content process with no '" + permName + "' privileges.\n");
+ return;
+ }
+ }
+
+ // we don't process kb messages (other than register)
+ // if they come from a kb that we're currently not regsitered for.
+ // this decision is made with the kbID kept by us and kb app
+ let kbID = null;
+ if ('kbID' in msg.data) {
+ kbID = msg.data.kbID;
+ }
+
+ if (0 === msg.name.indexOf('Keyboard:') &&
+ ('Keyboard:RegisterSync' !== msg.name && this._keyboardID !== kbID)
+ ) {
+ return;
+ }
+
+ switch (msg.name) {
+ case 'Forms:Focus':
+ this.handleFocus(msg);
+ break;
+ case 'Forms:Blur':
+ this.handleBlur(msg);
+ break;
+ case 'Forms:SelectionChange':
+ case 'Forms:SetSelectionRange:Result:OK':
+ case 'Forms:ReplaceSurroundingText:Result:OK':
+ case 'Forms:SendKey:Result:OK':
+ case 'Forms:SendKey:Result:Error':
+ case 'Forms:SequenceError':
+ case 'Forms:GetContext:Result:OK':
+ case 'Forms:SetComposition:Result:OK':
+ case 'Forms:EndComposition:Result:OK':
+ case 'Forms:SetSelectionRange:Result:Error':
+ case 'Forms:ReplaceSurroundingText:Result:Error':
+ let name = msg.name.replace(/^Forms/, 'Keyboard');
+ this.forwardEvent(name, msg);
+ break;
+
+ case 'System:SetValue':
+ this.setValue(msg);
+ break;
+ case 'Keyboard:RemoveFocus':
+ case 'System:RemoveFocus':
+ this.removeFocus();
+ break;
+ case 'System:RegisterSync': {
+ if (this._systemMMs.length !== 0) {
+ dump('Keyboard.jsm Warning: There are more than one content page ' +
+ 'with input-manage permission. There will be undeterministic ' +
+ 'responses to addInput()/removeInput() if both content pages are ' +
+ 'trying to respond to the same request event.\n');
+ }
+
+ let id = this._systemMMs.length;
+ this._systemMMs.push(mm);
+
+ return id;
+ }
+
+ case 'System:Unregister':
+ this._systemMMs.splice(msg.data.id, 1);
+
+ break;
+ case 'System:SetSelectedOption':
+ this.setSelectedOption(msg);
+ break;
+ case 'System:SetSelectedOptions':
+ this.setSelectedOption(msg);
+ break;
+ case 'System:SetSupportsSwitchingTypes':
+ this.setSupportsSwitchingTypes(msg);
+ break;
+ case 'Keyboard:SetSelectionRange':
+ this.setSelectionRange(msg);
+ break;
+ case 'Keyboard:ReplaceSurroundingText':
+ this.replaceSurroundingText(msg);
+ break;
+ case 'Keyboard:SwitchToNextInputMethod':
+ this.switchToNextInputMethod();
+ break;
+ case 'Keyboard:ShowInputMethodPicker':
+ this.showInputMethodPicker();
+ break;
+ case 'Keyboard:SendKey':
+ this.sendKey(msg);
+ break;
+ case 'Keyboard:GetContext':
+ this.getContext(msg);
+ break;
+ case 'Keyboard:SetComposition':
+ this.setComposition(msg);
+ break;
+ case 'Keyboard:EndComposition':
+ this.endComposition(msg);
+ break;
+ case 'Keyboard:RegisterSync':
+ this._keyboardMM = mm;
+ if (kbID) {
+ // keyboard identifies itself, use its kbID
+ // this msg would be async, so no need to return
+ this._keyboardID = kbID;
+ }else{
+ // generate the id for the keyboard
+ this._keyboardID = this._nextKeyboardID;
+ this._nextKeyboardID++;
+ // this msg is sync,
+ // and we want to return the id back to inputmethod
+ return this._keyboardID;
+ }
+ break;
+ case 'Keyboard:Unregister':
+ this._keyboardMM = null;
+ this._keyboardID = -1;
+ break;
+ case 'Keyboard:ReplyHardwareKeyEvent':
+ if (hardwareKeyHandler) {
+ let reply = msg.data;
+ hardwareKeyHandler.onHandledByInputMethodApp(reply.type,
+ reply.defaultPrevented);
+ }
+ break;
+ }
+ },
+
+ handleFocus: function keyboardHandleFocus(msg) {
+ // Set the formMM to the new message manager received.
+ let mm = msg.target.QueryInterface(Ci.nsIFrameLoaderOwner)
+ .frameLoader.messageManager;
+ this.formMM = mm;
+
+ // Notify the nsIHardwareKeyHandler that the input-method-app is active now.
+ if (hardwareKeyHandler && !this._isConnectedToHardwareKeyHandler) {
+ this._isConnectedToHardwareKeyHandler = true;
+ hardwareKeyHandler.onInputMethodAppConnected();
+ }
+
+ // Notify the current active input app to gain focus.
+ this.forwardEvent('Keyboard:Focus', msg);
+
+ // Notify System app, used also to render value selectors for now;
+ // that's why we need the info about choices / min / max here as well...
+ this.sendToSystem('System:Focus', msg.data);
+
+ // XXX: To be removed when content migrate away from mozChromeEvents.
+ SystemAppProxy.dispatchEvent({
+ type: 'inputmethod-contextchange',
+ inputType: msg.data.inputType,
+ value: msg.data.value,
+ choices: JSON.stringify(msg.data.choices),
+ min: msg.data.min,
+ max: msg.data.max
+ });
+ },
+
+ handleBlur: function keyboardHandleBlur(msg) {
+ let mm = msg.target.QueryInterface(Ci.nsIFrameLoaderOwner)
+ .frameLoader.messageManager;
+ // A blur message can't be sent to the keyboard if the focus has
+ // already been taken away at first place.
+ // This check is here to prevent problem caused by out-of-order
+ // ipc messages from two processes.
+ if (mm !== this.formMM) {
+ return;
+ }
+
+ // unset formMM
+ this.formMM = null;
+
+ // Notify the nsIHardwareKeyHandler that
+ // the input-method-app is disabled now.
+ if (hardwareKeyHandler && this._isConnectedToHardwareKeyHandler) {
+ this._isConnectedToHardwareKeyHandler = false;
+ hardwareKeyHandler.onInputMethodAppDisconnected();
+ }
+
+ this.forwardEvent('Keyboard:Blur', msg);
+ this.sendToSystem('System:Blur', {});
+
+ // XXX: To be removed when content migrate away from mozChromeEvents.
+ SystemAppProxy.dispatchEvent({
+ type: 'inputmethod-contextchange',
+ inputType: 'blur'
+ });
+ },
+
+ forwardEvent: function keyboardForwardEvent(newEventName, msg) {
+ this.sendToKeyboard(newEventName, msg.data);
+ },
+
+ setSelectedOption: function keyboardSetSelectedOption(msg) {
+ this.sendToForm('Forms:Select:Choice', msg.data);
+ },
+
+ setSelectedOptions: function keyboardSetSelectedOptions(msg) {
+ this.sendToForm('Forms:Select:Choice', msg.data);
+ },
+
+ setSelectionRange: function keyboardSetSelectionRange(msg) {
+ this.sendToForm('Forms:SetSelectionRange', msg.data);
+ },
+
+ setValue: function keyboardSetValue(msg) {
+ this.sendToForm('Forms:Input:Value', msg.data);
+ },
+
+ removeFocus: function keyboardRemoveFocus() {
+ if (!this.formMM) {
+ return;
+ }
+
+ this.sendToForm('Forms:Select:Blur', {});
+ },
+
+ replaceSurroundingText: function keyboardReplaceSurroundingText(msg) {
+ this.sendToForm('Forms:ReplaceSurroundingText', msg.data);
+ },
+
+ showInputMethodPicker: function keyboardShowInputMethodPicker() {
+ this.sendToSystem('System:ShowAll', {});
+
+ // XXX: To be removed with mozContentEvent support from shell.js
+ SystemAppProxy.dispatchEvent({
+ type: "inputmethod-showall"
+ });
+ },
+
+ switchToNextInputMethod: function keyboardSwitchToNextInputMethod() {
+ this.sendToSystem('System:Next', {});
+
+ // XXX: To be removed with mozContentEvent support from shell.js
+ SystemAppProxy.dispatchEvent({
+ type: "inputmethod-next"
+ });
+ },
+
+ sendKey: function keyboardSendKey(msg) {
+ this.sendToForm('Forms:Input:SendKey', msg.data);
+ },
+
+ getContext: function keyboardGetContext(msg) {
+ if (!this.formMM) {
+ return;
+ }
+
+ this.sendToKeyboard('Keyboard:SupportsSwitchingTypesChange', {
+ types: this._supportsSwitchingTypes
+ });
+
+ this.sendToForm('Forms:GetContext', msg.data);
+ },
+
+ setComposition: function keyboardSetComposition(msg) {
+ this.sendToForm('Forms:SetComposition', msg.data);
+ },
+
+ endComposition: function keyboardEndComposition(msg) {
+ this.sendToForm('Forms:EndComposition', msg.data);
+ },
+
+ setSupportsSwitchingTypes: function setSupportsSwitchingTypes(msg) {
+ this._supportsSwitchingTypes = msg.data.types;
+ this.sendToKeyboard('Keyboard:SupportsSwitchingTypesChange', msg.data);
+ },
+ // XXX: To be removed with mozContentEvent support from shell.js
+ setLayouts: function keyboardSetLayouts(layouts) {
+ // The input method plugins may not have loaded yet,
+ // cache the layouts so on init we can respond immediately instead
+ // of going back and forth between keyboard_manager
+ var types = [];
+
+ Object.keys(layouts).forEach((type) => {
+ if (layouts[type] > 1) {
+ types.push(type);
+ }
+ });
+
+ this._supportsSwitchingTypes = types;
+
+ this.sendToKeyboard('Keyboard:SupportsSwitchingTypesChange', {
+ types: types
+ });
+ }
+};
+
+function InputRegistryGlue() {
+ this._messageId = 0;
+ this._msgMap = new Map();
+
+ ppmm.addMessageListener('InputRegistry:Add', this);
+ ppmm.addMessageListener('InputRegistry:Remove', this);
+ ppmm.addMessageListener('System:InputRegistry:Add:Done', this);
+ ppmm.addMessageListener('System:InputRegistry:Remove:Done', this);
+};
+
+InputRegistryGlue.prototype.receiveMessage = function(msg) {
+ let mm = Utils.getMMFromMessage(msg);
+
+ let permName = msg.name.startsWith("System:") ? "input-mgmt" : "input";
+ if (!Utils.checkPermissionForMM(mm, permName)) {
+ dump("InputRegistryGlue message " + msg.name +
+ " from a content process with no " + permName + " privileges.");
+ return;
+ }
+
+ switch (msg.name) {
+ case 'InputRegistry:Add':
+ this.addInput(msg, mm);
+
+ break;
+
+ case 'InputRegistry:Remove':
+ this.removeInput(msg, mm);
+
+ break;
+
+ case 'System:InputRegistry:Add:Done':
+ case 'System:InputRegistry:Remove:Done':
+ this.returnMessage(msg.data);
+
+ break;
+ }
+};
+
+InputRegistryGlue.prototype.addInput = function(msg, mm) {
+ let msgId = this._messageId++;
+ this._msgMap.set(msgId, {
+ mm: mm,
+ requestId: msg.data.requestId
+ });
+
+ let manifestURL = appsService.getManifestURLByLocalId(msg.data.appId);
+
+ Keyboard.sendToSystem('System:InputRegistry:Add', {
+ id: msgId,
+ manifestURL: manifestURL,
+ inputId: msg.data.inputId,
+ inputManifest: msg.data.inputManifest
+ });
+
+ // XXX: To be removed when content migrate away from mozChromeEvents.
+ SystemAppProxy.dispatchEvent({
+ type: 'inputregistry-add',
+ id: msgId,
+ manifestURL: manifestURL,
+ inputId: msg.data.inputId,
+ inputManifest: msg.data.inputManifest
+ });
+};
+
+InputRegistryGlue.prototype.removeInput = function(msg, mm) {
+ let msgId = this._messageId++;
+ this._msgMap.set(msgId, {
+ mm: mm,
+ requestId: msg.data.requestId
+ });
+
+ let manifestURL = appsService.getManifestURLByLocalId(msg.data.appId);
+
+ Keyboard.sendToSystem('System:InputRegistry:Remove', {
+ id: msgId,
+ manifestURL: manifestURL,
+ inputId: msg.data.inputId
+ });
+
+ // XXX: To be removed when content migrate away from mozChromeEvents.
+ SystemAppProxy.dispatchEvent({
+ type: 'inputregistry-remove',
+ id: msgId,
+ manifestURL: manifestURL,
+ inputId: msg.data.inputId
+ });
+};
+
+InputRegistryGlue.prototype.returnMessage = function(detail) {
+ if (!this._msgMap.has(detail.id)) {
+ dump('InputRegistryGlue: Ignoring already handled message response. ' +
+ 'id=' + detail.id + '\n');
+ return;
+ }
+
+ let { mm, requestId } = this._msgMap.get(detail.id);
+ this._msgMap.delete(detail.id);
+
+ if (Cu.isDeadWrapper(mm)) {
+ dump('InputRegistryGlue: Message manager has already died.\n');
+ return;
+ }
+
+ if (!('error' in detail)) {
+ mm.sendAsyncMessage('InputRegistry:Result:OK', {
+ requestId: requestId
+ });
+ } else {
+ mm.sendAsyncMessage('InputRegistry:Result:Error', {
+ error: detail.error,
+ requestId: requestId
+ });
+ }
+};
+
+this.Keyboard.init();
diff --git a/dom/inputmethod/MozKeyboard.js b/dom/inputmethod/MozKeyboard.js
new file mode 100644
index 000000000..3996f3e5d
--- /dev/null
+++ b/dom/inputmethod/MozKeyboard.js
@@ -0,0 +1,1255 @@
+/* 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 Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+const Cr = Components.results;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/DOMRequestHelper.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(this, "cpmm",
+ "@mozilla.org/childprocessmessagemanager;1", "nsISyncMessageSender");
+
+XPCOMUtils.defineLazyServiceGetter(this, "tm",
+ "@mozilla.org/thread-manager;1", "nsIThreadManager");
+
+/*
+ * A WeakMap to map input method iframe window to
+ * it's active status, kbID, and ipcHelper.
+ */
+var WindowMap = {
+ // WeakMap of <window, object> pairs.
+ _map: null,
+
+ /*
+ * Set the object associated to the window and return it.
+ */
+ _getObjForWin: function(win) {
+ if (!this._map) {
+ this._map = new WeakMap();
+ }
+ if (this._map.has(win)) {
+ return this._map.get(win);
+ } else {
+ let obj = {
+ active: false,
+ kbID: undefined,
+ ipcHelper: null
+ };
+ this._map.set(win, obj);
+
+ return obj;
+ }
+ },
+
+ /*
+ * Check if the given window is active.
+ */
+ isActive: function(win) {
+ if (!this._map || !win) {
+ return false;
+ }
+
+ return this._getObjForWin(win).active;
+ },
+
+ /*
+ * Set the active status of the given window.
+ */
+ setActive: function(win, isActive) {
+ if (!win) {
+ return;
+ }
+ let obj = this._getObjForWin(win);
+ obj.active = isActive;
+ },
+
+ /*
+ * Get the keyboard ID (assigned by Keyboard.jsm) of the given window.
+ */
+ getKbID: function(win) {
+ if (!this._map || !win) {
+ return undefined;
+ }
+
+ let obj = this._getObjForWin(win);
+ return obj.kbID;
+ },
+
+ /*
+ * Set the keyboard ID (assigned by Keyboard.jsm) of the given window.
+ */
+ setKbID: function(win, kbID) {
+ if (!win) {
+ return;
+ }
+ let obj = this._getObjForWin(win);
+ obj.kbID = kbID;
+ },
+
+ /*
+ * Get InputContextDOMRequestIpcHelper instance attached to this window.
+ */
+ getInputContextIpcHelper: function(win) {
+ if (!win) {
+ return;
+ }
+ let obj = this._getObjForWin(win);
+ if (!obj.ipcHelper) {
+ obj.ipcHelper = new InputContextDOMRequestIpcHelper(win);
+ }
+ return obj.ipcHelper;
+ },
+
+ /*
+ * Unset InputContextDOMRequestIpcHelper instance.
+ */
+ unsetInputContextIpcHelper: function(win) {
+ if (!win) {
+ return;
+ }
+ let obj = this._getObjForWin(win);
+ if (!obj.ipcHelper) {
+ return;
+ }
+ obj.ipcHelper = null;
+ }
+};
+
+var cpmmSendAsyncMessageWithKbID = function (self, msg, data) {
+ data.kbID = WindowMap.getKbID(self._window);
+ cpmm.sendAsyncMessage(msg, data);
+};
+
+/**
+ * ==============================================
+ * InputMethodManager
+ * ==============================================
+ */
+function MozInputMethodManager(win) {
+ this._window = win;
+}
+
+MozInputMethodManager.prototype = {
+ supportsSwitchingForCurrentInputContext: false,
+ _window: null,
+
+ classID: Components.ID("{7e9d7280-ef86-11e2-b778-0800200c9a66}"),
+
+ QueryInterface: XPCOMUtils.generateQI([]),
+
+ set oninputcontextfocus(handler) {
+ this.__DOM_IMPL__.setEventHandler("oninputcontextfocus", handler);
+ },
+
+ get oninputcontextfocus() {
+ return this.__DOM_IMPL__.getEventHandler("oninputcontextfocus");
+ },
+
+ set oninputcontextblur(handler) {
+ this.__DOM_IMPL__.setEventHandler("oninputcontextblur", handler);
+ },
+
+ get oninputcontextblur() {
+ return this.__DOM_IMPL__.getEventHandler("oninputcontextblur");
+ },
+
+ set onshowallrequest(handler) {
+ this.__DOM_IMPL__.setEventHandler("onshowallrequest", handler);
+ },
+
+ get onshowallrequest() {
+ return this.__DOM_IMPL__.getEventHandler("onshowallrequest");
+ },
+
+ set onnextrequest(handler) {
+ this.__DOM_IMPL__.setEventHandler("onnextrequest", handler);
+ },
+
+ get onnextrequest() {
+ return this.__DOM_IMPL__.getEventHandler("onnextrequest");
+ },
+
+ set onaddinputrequest(handler) {
+ this.__DOM_IMPL__.setEventHandler("onaddinputrequest", handler);
+ },
+
+ get onaddinputrequest() {
+ return this.__DOM_IMPL__.getEventHandler("onaddinputrequest");
+ },
+
+ set onremoveinputrequest(handler) {
+ this.__DOM_IMPL__.setEventHandler("onremoveinputrequest", handler);
+ },
+
+ get onremoveinputrequest() {
+ return this.__DOM_IMPL__.getEventHandler("onremoveinputrequest");
+ },
+
+ showAll: function() {
+ if (!WindowMap.isActive(this._window)) {
+ return;
+ }
+ cpmmSendAsyncMessageWithKbID(this, 'Keyboard:ShowInputMethodPicker', {});
+ },
+
+ next: function() {
+ if (!WindowMap.isActive(this._window)) {
+ return;
+ }
+ cpmmSendAsyncMessageWithKbID(this, 'Keyboard:SwitchToNextInputMethod', {});
+ },
+
+ supportsSwitching: function() {
+ if (!WindowMap.isActive(this._window)) {
+ return false;
+ }
+ return this.supportsSwitchingForCurrentInputContext;
+ },
+
+ hide: function() {
+ if (!WindowMap.isActive(this._window)) {
+ return;
+ }
+ cpmmSendAsyncMessageWithKbID(this, 'Keyboard:RemoveFocus', {});
+ },
+
+ setSupportsSwitchingTypes: function(types) {
+ cpmm.sendAsyncMessage('System:SetSupportsSwitchingTypes', {
+ types: types
+ });
+ },
+
+ handleFocus: function(data) {
+ let detail = new MozInputContextFocusEventDetail(this._window, data);
+ let wrappedDetail =
+ this._window.MozInputContextFocusEventDetail._create(this._window, detail);
+ let event = new this._window.CustomEvent('inputcontextfocus',
+ { cancelable: true, detail: wrappedDetail });
+
+ let handled = !this.__DOM_IMPL__.dispatchEvent(event);
+
+ // A gentle warning if the event is not preventDefault() by the content.
+ if (!handled) {
+ dump('MozKeyboard.js: A frame with input-manage permission did not' +
+ ' handle the inputcontextfocus event dispatched.\n');
+ }
+ },
+
+ handleBlur: function(data) {
+ let event =
+ new this._window.Event('inputcontextblur', { cancelable: true });
+
+ let handled = !this.__DOM_IMPL__.dispatchEvent(event);
+
+ // A gentle warning if the event is not preventDefault() by the content.
+ if (!handled) {
+ dump('MozKeyboard.js: A frame with input-manage permission did not' +
+ ' handle the inputcontextblur event dispatched.\n');
+ }
+ },
+
+ dispatchShowAllRequestEvent: function() {
+ this._fireSimpleEvent('showallrequest');
+ },
+
+ dispatchNextRequestEvent: function() {
+ this._fireSimpleEvent('nextrequest');
+ },
+
+ _fireSimpleEvent: function(eventType) {
+ let event = new this._window.Event(eventType);
+ let handled = !this.__DOM_IMPL__.dispatchEvent(event, { cancelable: true });
+
+ // A gentle warning if the event is not preventDefault() by the content.
+ if (!handled) {
+ dump('MozKeyboard.js: A frame with input-manage permission did not' +
+ ' handle the ' + eventType + ' event dispatched.\n');
+ }
+ },
+
+ handleAddInput: function(data) {
+ let p = this._fireInputRegistryEvent('addinputrequest', data);
+ if (!p) {
+ return;
+ }
+
+ p.then(() => {
+ cpmm.sendAsyncMessage('System:InputRegistry:Add:Done', {
+ id: data.id
+ });
+ }, (error) => {
+ cpmm.sendAsyncMessage('System:InputRegistry:Add:Done', {
+ id: data.id,
+ error: error || 'Unknown Error'
+ });
+ });
+ },
+
+ handleRemoveInput: function(data) {
+ let p = this._fireInputRegistryEvent('removeinputrequest', data);
+ if (!p) {
+ return;
+ }
+
+ p.then(() => {
+ cpmm.sendAsyncMessage('System:InputRegistry:Remove:Done', {
+ id: data.id
+ });
+ }, (error) => {
+ cpmm.sendAsyncMessage('System:InputRegistry:Remove:Done', {
+ id: data.id,
+ error: error || 'Unknown Error'
+ });
+ });
+ },
+
+ _fireInputRegistryEvent: function(eventType, data) {
+ let detail = new MozInputRegistryEventDetail(this._window, data);
+ let wrappedDetail =
+ this._window.MozInputRegistryEventDetail._create(this._window, detail);
+ let event = new this._window.CustomEvent(eventType,
+ { cancelable: true, detail: wrappedDetail });
+ let handled = !this.__DOM_IMPL__.dispatchEvent(event);
+
+ // A gentle warning if the event is not preventDefault() by the content.
+ if (!handled) {
+ dump('MozKeyboard.js: A frame with input-manage permission did not' +
+ ' handle the ' + eventType + ' event dispatched.\n');
+
+ return null;
+ }
+ return detail.takeChainedPromise();
+ }
+};
+
+function MozInputContextFocusEventDetail(win, data) {
+ this.type = data.type;
+ this.inputType = data.inputType;
+ this.value = data.value;
+ // Exposed as MozInputContextChoicesInfo dictionary defined in WebIDL
+ this.choices = data.choices;
+ this.min = data.min;
+ this.max = data.max;
+}
+MozInputContextFocusEventDetail.prototype = {
+ classID: Components.ID("{e0794208-ac50-40e8-b22e-6ee0b4c4e6e8}"),
+ QueryInterface: XPCOMUtils.generateQI([]),
+
+ type: undefined,
+ inputType: undefined,
+ value: '',
+ choices: null,
+ min: undefined,
+ max: undefined
+};
+
+function MozInputRegistryEventDetail(win, data) {
+ this._window = win;
+
+ this.manifestURL = data.manifestURL;
+ this.inputId = data.inputId;
+ // Exposed as MozInputMethodInputManifest dictionary defined in WebIDL
+ this.inputManifest = data.inputManifest;
+
+ this._chainedPromise = Promise.resolve();
+}
+MozInputRegistryEventDetail.prototype = {
+ classID: Components.ID("{02130070-9b3e-4f38-bbd9-f0013aa36717}"),
+ QueryInterface: XPCOMUtils.generateQI([]),
+
+ _window: null,
+
+ manifestURL: undefined,
+ inputId: undefined,
+ inputManifest: null,
+
+ waitUntil: function(p) {
+ // Need an extra protection here since waitUntil will be an no-op
+ // when chainedPromise is already returned.
+ if (!this._chainedPromise) {
+ throw new this._window.DOMException(
+ 'Must call waitUntil() within the event handling loop.',
+ 'InvalidStateError');
+ }
+
+ this._chainedPromise = this._chainedPromise
+ .then(function() { return p; });
+ },
+
+ takeChainedPromise: function() {
+ var p = this._chainedPromise;
+ this._chainedPromise = null;
+ return p;
+ }
+};
+
+/**
+ * ==============================================
+ * InputMethod
+ * ==============================================
+ */
+function MozInputMethod() { }
+
+MozInputMethod.prototype = {
+ __proto__: DOMRequestIpcHelper.prototype,
+
+ _window: null,
+ _inputcontext: null,
+ _wrappedInputContext: null,
+ _mgmt: null,
+ _wrappedMgmt: null,
+ _supportsSwitchingTypes: [],
+ _inputManageId: undefined,
+
+ classID: Components.ID("{4607330d-e7d2-40a4-9eb8-43967eae0142}"),
+
+ QueryInterface: XPCOMUtils.generateQI([
+ Ci.nsIDOMGlobalPropertyInitializer,
+ Ci.nsIObserver,
+ Ci.nsISupportsWeakReference
+ ]),
+
+ init: function mozInputMethodInit(win) {
+ this._window = win;
+ this._mgmt = new MozInputMethodManager(win);
+ this._wrappedMgmt = win.MozInputMethodManager._create(win, this._mgmt);
+ this.innerWindowID = win.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils)
+ .currentInnerWindowID;
+
+ Services.obs.addObserver(this, "inner-window-destroyed", false);
+
+ cpmm.addWeakMessageListener('Keyboard:Focus', this);
+ cpmm.addWeakMessageListener('Keyboard:Blur', this);
+ cpmm.addWeakMessageListener('Keyboard:SelectionChange', this);
+ cpmm.addWeakMessageListener('Keyboard:GetContext:Result:OK', this);
+ cpmm.addWeakMessageListener('Keyboard:SupportsSwitchingTypesChange', this);
+ cpmm.addWeakMessageListener('Keyboard:ReceiveHardwareKeyEvent', this);
+ cpmm.addWeakMessageListener('InputRegistry:Result:OK', this);
+ cpmm.addWeakMessageListener('InputRegistry:Result:Error', this);
+
+ if (this._hasInputManagePerm(win)) {
+ this._inputManageId = cpmm.sendSyncMessage('System:RegisterSync', {})[0];
+ cpmm.addWeakMessageListener('System:Focus', this);
+ cpmm.addWeakMessageListener('System:Blur', this);
+ cpmm.addWeakMessageListener('System:ShowAll', this);
+ cpmm.addWeakMessageListener('System:Next', this);
+ cpmm.addWeakMessageListener('System:InputRegistry:Add', this);
+ cpmm.addWeakMessageListener('System:InputRegistry:Remove', this);
+ }
+ },
+
+ uninit: function mozInputMethodUninit() {
+ this._window = null;
+ this._mgmt = null;
+ this._wrappedMgmt = null;
+
+ cpmm.removeWeakMessageListener('Keyboard:Focus', this);
+ cpmm.removeWeakMessageListener('Keyboard:Blur', this);
+ cpmm.removeWeakMessageListener('Keyboard:SelectionChange', this);
+ cpmm.removeWeakMessageListener('Keyboard:GetContext:Result:OK', this);
+ cpmm.removeWeakMessageListener('Keyboard:SupportsSwitchingTypesChange', this);
+ cpmm.removeWeakMessageListener('Keyboard:ReceiveHardwareKeyEvent', this);
+ cpmm.removeWeakMessageListener('InputRegistry:Result:OK', this);
+ cpmm.removeWeakMessageListener('InputRegistry:Result:Error', this);
+ this.setActive(false);
+
+ if (typeof this._inputManageId === 'number') {
+ cpmm.sendAsyncMessage('System:Unregister', {
+ 'id': this._inputManageId
+ });
+ cpmm.removeWeakMessageListener('System:Focus', this);
+ cpmm.removeWeakMessageListener('System:Blur', this);
+ cpmm.removeWeakMessageListener('System:ShowAll', this);
+ cpmm.removeWeakMessageListener('System:Next', this);
+ cpmm.removeWeakMessageListener('System:InputRegistry:Add', this);
+ cpmm.removeWeakMessageListener('System:InputRegistry:Remove', this);
+ }
+ },
+
+ receiveMessage: function mozInputMethodReceiveMsg(msg) {
+ if (msg.name.startsWith('Keyboard') &&
+ !WindowMap.isActive(this._window)) {
+ return;
+ }
+
+ let data = msg.data;
+
+ if (msg.name.startsWith('System') &&
+ this._inputManageId !== data.inputManageId) {
+ return;
+ }
+ delete data.inputManageId;
+
+ let resolver = ('requestId' in data) ?
+ this.takePromiseResolver(data.requestId) : null;
+
+ switch(msg.name) {
+ case 'Keyboard:Focus':
+ // XXX Bug 904339 could receive 'text' event twice
+ this.setInputContext(data);
+ break;
+ case 'Keyboard:Blur':
+ this.setInputContext(null);
+ break;
+ case 'Keyboard:SelectionChange':
+ if (this.inputcontext) {
+ this._inputcontext.updateSelectionContext(data, false);
+ }
+ break;
+ case 'Keyboard:GetContext:Result:OK':
+ this.setInputContext(data);
+ break;
+ case 'Keyboard:SupportsSwitchingTypesChange':
+ this._supportsSwitchingTypes = data.types;
+ break;
+ case 'Keyboard:ReceiveHardwareKeyEvent':
+ if (!Ci.nsIHardwareKeyHandler) {
+ break;
+ }
+
+ let defaultPrevented = Ci.nsIHardwareKeyHandler.NO_DEFAULT_PREVENTED;
+
+ // |event.preventDefault()| is allowed to be called only when
+ // |event.cancelable| is true
+ if (this._inputcontext && data.keyDict.cancelable) {
+ defaultPrevented |= this._inputcontext.forwardHardwareKeyEvent(data);
+ }
+
+ cpmmSendAsyncMessageWithKbID(this, 'Keyboard:ReplyHardwareKeyEvent', {
+ type: data.type,
+ defaultPrevented: defaultPrevented
+ });
+ break;
+ case 'InputRegistry:Result:OK':
+ resolver.resolve();
+
+ break;
+
+ case 'InputRegistry:Result:Error':
+ resolver.reject(data.error);
+
+ break;
+
+ case 'System:Focus':
+ this._mgmt.handleFocus(data);
+ break;
+
+ case 'System:Blur':
+ this._mgmt.handleBlur(data);
+ break;
+
+ case 'System:ShowAll':
+ this._mgmt.dispatchShowAllRequestEvent();
+ break;
+
+ case 'System:Next':
+ this._mgmt.dispatchNextRequestEvent();
+ break;
+
+ case 'System:InputRegistry:Add':
+ this._mgmt.handleAddInput(data);
+ break;
+
+ case 'System:InputRegistry:Remove':
+ this._mgmt.handleRemoveInput(data);
+ break;
+ }
+ },
+
+ observe: function mozInputMethodObserve(subject, topic, data) {
+ let wId = subject.QueryInterface(Ci.nsISupportsPRUint64).data;
+ if (wId == this.innerWindowID)
+ this.uninit();
+ },
+
+ get mgmt() {
+ return this._wrappedMgmt;
+ },
+
+ get inputcontext() {
+ if (!WindowMap.isActive(this._window)) {
+ return null;
+ }
+ return this._wrappedInputContext;
+ },
+
+ set oninputcontextchange(handler) {
+ this.__DOM_IMPL__.setEventHandler("oninputcontextchange", handler);
+ },
+
+ get oninputcontextchange() {
+ return this.__DOM_IMPL__.getEventHandler("oninputcontextchange");
+ },
+
+ setInputContext: function mozKeyboardContextChange(data) {
+ if (this._inputcontext) {
+ this._inputcontext.destroy();
+ this._inputcontext = null;
+ this._wrappedInputContext = null;
+ this._mgmt.supportsSwitchingForCurrentInputContext = false;
+ }
+
+ if (data) {
+ this._mgmt.supportsSwitchingForCurrentInputContext =
+ (this._supportsSwitchingTypes.indexOf(data.inputType) !== -1);
+
+ this._inputcontext = new MozInputContext(data);
+ this._inputcontext.init(this._window);
+ // inputcontext will be exposed as a WebIDL object. Create its
+ // content-side object explicitly to avoid Bug 1001325.
+ this._wrappedInputContext =
+ this._window.MozInputContext._create(this._window, this._inputcontext);
+ }
+
+ let event = new this._window.Event("inputcontextchange");
+ this.__DOM_IMPL__.dispatchEvent(event);
+ },
+
+ setActive: function mozInputMethodSetActive(isActive) {
+ if (WindowMap.isActive(this._window) === isActive) {
+ return;
+ }
+
+ WindowMap.setActive(this._window, isActive);
+
+ if (isActive) {
+ // Activate current input method.
+ // If there is already an active context, then this will trigger
+ // a GetContext:Result:OK event, and we can initialize ourselves.
+ // Otherwise silently ignored.
+
+ // get keyboard ID from Keyboard.jsm,
+ // or if we already have it, get it from our map
+ // Note: if we need to get it from Keyboard.jsm,
+ // we have to use a synchronous message
+ var kbID = WindowMap.getKbID(this._window);
+ if (kbID) {
+ cpmmSendAsyncMessageWithKbID(this, 'Keyboard:RegisterSync', {});
+ } else {
+ let res = cpmm.sendSyncMessage('Keyboard:RegisterSync', {});
+ WindowMap.setKbID(this._window, res[0]);
+ }
+
+ cpmmSendAsyncMessageWithKbID(this, 'Keyboard:GetContext', {});
+ } else {
+ // Deactive current input method.
+ cpmmSendAsyncMessageWithKbID(this, 'Keyboard:Unregister', {});
+ if (this._inputcontext) {
+ this.setInputContext(null);
+ }
+ }
+ },
+
+ addInput: function(inputId, inputManifest) {
+ return this.createPromiseWithId(function(resolverId) {
+ let appId = this._window.document.nodePrincipal.appId;
+
+ cpmm.sendAsyncMessage('InputRegistry:Add', {
+ requestId: resolverId,
+ inputId: inputId,
+ inputManifest: inputManifest,
+ appId: appId
+ });
+ }.bind(this));
+ },
+
+ removeInput: function(inputId) {
+ return this.createPromiseWithId(function(resolverId) {
+ let appId = this._window.document.nodePrincipal.appId;
+
+ cpmm.sendAsyncMessage('InputRegistry:Remove', {
+ requestId: resolverId,
+ inputId: inputId,
+ appId: appId
+ });
+ }.bind(this));
+ },
+
+ setValue: function(value) {
+ cpmm.sendAsyncMessage('System:SetValue', {
+ 'value': value
+ });
+ },
+
+ setSelectedOption: function(index) {
+ cpmm.sendAsyncMessage('System:SetSelectedOption', {
+ 'index': index
+ });
+ },
+
+ setSelectedOptions: function(indexes) {
+ cpmm.sendAsyncMessage('System:SetSelectedOptions', {
+ 'indexes': indexes
+ });
+ },
+
+ removeFocus: function() {
+ cpmm.sendAsyncMessage('System:RemoveFocus', {});
+ },
+
+ // Only the system app needs that, so instead of testing a permission which
+ // is allowed for all chrome:// url, we explicitly test that this is the
+ // system app's start URL.
+ _hasInputManagePerm: function(win) {
+ let url = win.location.href;
+ let systemAppIndex;
+ try {
+ systemAppIndex = Services.prefs.getCharPref('b2g.system_startup_url');
+ } catch(e) {
+ dump('MozKeyboard.jsm: no system app startup url set (pref is b2g.system_startup_url)');
+ }
+
+ dump(`MozKeyboard.jsm expecting ${systemAppIndex}\n`);
+ return url == systemAppIndex;
+ }
+};
+
+/**
+ * ==============================================
+ * InputContextDOMRequestIpcHelper
+ * ==============================================
+ */
+function InputContextDOMRequestIpcHelper(win) {
+ this.initDOMRequestHelper(win,
+ ["Keyboard:GetText:Result:OK",
+ "Keyboard:GetText:Result:Error",
+ "Keyboard:SetSelectionRange:Result:OK",
+ "Keyboard:ReplaceSurroundingText:Result:OK",
+ "Keyboard:SendKey:Result:OK",
+ "Keyboard:SendKey:Result:Error",
+ "Keyboard:SetComposition:Result:OK",
+ "Keyboard:EndComposition:Result:OK",
+ "Keyboard:SequenceError"]);
+}
+
+InputContextDOMRequestIpcHelper.prototype = {
+ __proto__: DOMRequestIpcHelper.prototype,
+ _inputContext: null,
+
+ attachInputContext: function(inputCtx) {
+ if (this._inputContext) {
+ throw new Error("InputContextDOMRequestIpcHelper: detach the context first.");
+ }
+
+ this._inputContext = inputCtx;
+ },
+
+ // Unset ourselves when the window is destroyed.
+ uninit: function() {
+ WindowMap.unsetInputContextIpcHelper(this._window);
+ },
+
+ detachInputContext: function() {
+ // All requests that are still pending need to be invalidated
+ // because the context is no longer valid.
+ this.forEachPromiseResolver(k => {
+ this.takePromiseResolver(k).reject("InputContext got destroyed");
+ });
+
+ this._inputContext = null;
+ },
+
+ receiveMessage: function(msg) {
+ if (!this._inputContext) {
+ dump('InputContextDOMRequestIpcHelper received message without context attached.\n');
+ return;
+ }
+
+ this._inputContext.receiveMessage(msg);
+ }
+};
+
+function MozInputContextSelectionChangeEventDetail(ctx, ownAction) {
+ this._ctx = ctx;
+ this.ownAction = ownAction;
+}
+
+MozInputContextSelectionChangeEventDetail.prototype = {
+ classID: Components.ID("ef35443e-a400-4ae3-9170-c2f4e05f7aed"),
+ QueryInterface: XPCOMUtils.generateQI([]),
+
+ ownAction: false,
+
+ get selectionStart() {
+ return this._ctx.selectionStart;
+ },
+
+ get selectionEnd() {
+ return this._ctx.selectionEnd;
+ }
+};
+
+function MozInputContextSurroundingTextChangeEventDetail(ctx, ownAction) {
+ this._ctx = ctx;
+ this.ownAction = ownAction;
+}
+
+MozInputContextSurroundingTextChangeEventDetail.prototype = {
+ classID: Components.ID("1c50fdaf-74af-4b2e-814f-792caf65a168"),
+ QueryInterface: XPCOMUtils.generateQI([]),
+
+ ownAction: false,
+
+ get text() {
+ return this._ctx.text;
+ },
+
+ get textBeforeCursor() {
+ return this._ctx.textBeforeCursor;
+ },
+
+ get textAfterCursor() {
+ return this._ctx.textAfterCursor;
+ }
+};
+
+/**
+ * ==============================================
+ * HardwareInput
+ * ==============================================
+ */
+function MozHardwareInput() {
+}
+
+MozHardwareInput.prototype = {
+ classID: Components.ID("{1e38633d-d08b-4867-9944-afa5c648adb6}"),
+ QueryInterface: XPCOMUtils.generateQI([]),
+};
+
+/**
+ * ==============================================
+ * InputContext
+ * ==============================================
+ */
+function MozInputContext(data) {
+ this._context = {
+ type: data.type,
+ inputType: data.inputType,
+ inputMode: data.inputMode,
+ lang: data.lang,
+ selectionStart: data.selectionStart,
+ selectionEnd: data.selectionEnd,
+ text: data.value
+ };
+
+ this._contextId = data.contextId;
+}
+
+MozInputContext.prototype = {
+ _window: null,
+ _context: null,
+ _contextId: -1,
+ _ipcHelper: null,
+ _hardwareinput: null,
+ _wrappedhardwareinput: null,
+
+ classID: Components.ID("{1e38633d-d08b-4867-9944-afa5c648adb6}"),
+
+ QueryInterface: XPCOMUtils.generateQI([
+ Ci.nsIObserver,
+ Ci.nsISupportsWeakReference
+ ]),
+
+ init: function ic_init(win) {
+ this._window = win;
+
+ this._ipcHelper = WindowMap.getInputContextIpcHelper(win);
+ this._ipcHelper.attachInputContext(this);
+ this._hardwareinput = new MozHardwareInput();
+ this._wrappedhardwareinput =
+ this._window.MozHardwareInput._create(this._window, this._hardwareinput);
+ },
+
+ destroy: function ic_destroy() {
+ // A consuming application might still hold a cached version of
+ // this object. After destroying all methods will throw because we
+ // cannot create new promises anymore, but we still hold
+ // (outdated) information in the context. So let's clear that out.
+ for (var k in this._context) {
+ if (this._context.hasOwnProperty(k)) {
+ this._context[k] = null;
+ }
+ }
+
+ this._ipcHelper.detachInputContext();
+ this._ipcHelper = null;
+
+ this._window = null;
+ this._hardwareinput = null;
+ this._wrappedhardwareinput = null;
+ },
+
+ receiveMessage: function ic_receiveMessage(msg) {
+ if (!msg || !msg.json) {
+ dump('InputContext received message without data\n');
+ return;
+ }
+
+ let json = msg.json;
+ let resolver = this._ipcHelper.takePromiseResolver(json.requestId);
+
+ if (!resolver) {
+ dump('InputContext received invalid requestId.\n');
+ return;
+ }
+
+ // Update context first before resolving promise to avoid race condition
+ if (json.selectioninfo) {
+ this.updateSelectionContext(json.selectioninfo, true);
+ }
+
+ switch (msg.name) {
+ case "Keyboard:SendKey:Result:OK":
+ resolver.resolve(true);
+ break;
+ case "Keyboard:SendKey:Result:Error":
+ resolver.reject(json.error);
+ break;
+ case "Keyboard:GetText:Result:OK":
+ resolver.resolve(json.text);
+ break;
+ case "Keyboard:GetText:Result:Error":
+ resolver.reject(json.error);
+ break;
+ case "Keyboard:SetSelectionRange:Result:OK":
+ case "Keyboard:ReplaceSurroundingText:Result:OK":
+ resolver.resolve(
+ Cu.cloneInto(json.selectioninfo, this._window));
+ break;
+ case "Keyboard:SequenceError":
+ // Occurs when a new element got focus, but the inputContext was
+ // not invalidated yet...
+ resolver.reject("InputContext has expired");
+ break;
+ case "Keyboard:SetComposition:Result:OK": // Fall through.
+ case "Keyboard:EndComposition:Result:OK":
+ resolver.resolve(true);
+ break;
+ default:
+ dump("Could not find a handler for " + msg.name);
+ resolver.reject();
+ break;
+ }
+ },
+
+ updateSelectionContext: function ic_updateSelectionContext(data, ownAction) {
+ if (!this._context) {
+ return;
+ }
+
+ let selectionDirty =
+ this._context.selectionStart !== data.selectionStart ||
+ this._context.selectionEnd !== data.selectionEnd;
+ let surroundDirty = selectionDirty || data.text !== this._contextId.text;
+
+ this._context.text = data.text;
+ this._context.selectionStart = data.selectionStart;
+ this._context.selectionEnd = data.selectionEnd;
+
+ if (selectionDirty) {
+ let selectionChangeDetail =
+ new MozInputContextSelectionChangeEventDetail(this, ownAction);
+ let wrappedSelectionChangeDetail =
+ this._window.MozInputContextSelectionChangeEventDetail
+ ._create(this._window, selectionChangeDetail);
+ let selectionChangeEvent = new this._window.CustomEvent("selectionchange",
+ { cancelable: false, detail: wrappedSelectionChangeDetail });
+
+ this.__DOM_IMPL__.dispatchEvent(selectionChangeEvent);
+ }
+
+ if (surroundDirty) {
+ let surroundingTextChangeDetail =
+ new MozInputContextSurroundingTextChangeEventDetail(this, ownAction);
+ let wrappedSurroundingTextChangeDetail =
+ this._window.MozInputContextSurroundingTextChangeEventDetail
+ ._create(this._window, surroundingTextChangeDetail);
+ let selectionChangeEvent = new this._window.CustomEvent("surroundingtextchange",
+ { cancelable: false, detail: wrappedSurroundingTextChangeDetail });
+
+ this.__DOM_IMPL__.dispatchEvent(selectionChangeEvent);
+ }
+ },
+
+ // tag name of the input field
+ get type() {
+ return this._context.type;
+ },
+
+ // type of the input field
+ get inputType() {
+ return this._context.inputType;
+ },
+
+ get inputMode() {
+ return this._context.inputMode;
+ },
+
+ get lang() {
+ return this._context.lang;
+ },
+
+ getText: function ic_getText(offset, length) {
+ let text;
+ if (offset && length) {
+ text = this._context.text.substr(offset, length);
+ } else if (offset) {
+ text = this._context.text.substr(offset);
+ } else {
+ text = this._context.text;
+ }
+
+ return this._window.Promise.resolve(text);
+ },
+
+ get selectionStart() {
+ return this._context.selectionStart;
+ },
+
+ get selectionEnd() {
+ return this._context.selectionEnd;
+ },
+
+ get text() {
+ return this._context.text;
+ },
+
+ get textBeforeCursor() {
+ let text = this._context.text;
+ let start = this._context.selectionStart;
+ return (start < 100) ?
+ text.substr(0, start) :
+ text.substr(start - 100, 100);
+ },
+
+ get textAfterCursor() {
+ let text = this._context.text;
+ let start = this._context.selectionStart;
+ let end = this._context.selectionEnd;
+ return text.substr(start, end - start + 100);
+ },
+
+ get hardwareinput() {
+ return this._wrappedhardwareinput;
+ },
+
+ setSelectionRange: function ic_setSelectionRange(start, length) {
+ let self = this;
+ return this._sendPromise(function(resolverId) {
+ cpmmSendAsyncMessageWithKbID(self, 'Keyboard:SetSelectionRange', {
+ contextId: self._contextId,
+ requestId: resolverId,
+ selectionStart: start,
+ selectionEnd: start + length
+ });
+ });
+ },
+
+ get onsurroundingtextchange() {
+ return this.__DOM_IMPL__.getEventHandler("onsurroundingtextchange");
+ },
+
+ set onsurroundingtextchange(handler) {
+ this.__DOM_IMPL__.setEventHandler("onsurroundingtextchange", handler);
+ },
+
+ get onselectionchange() {
+ return this.__DOM_IMPL__.getEventHandler("onselectionchange");
+ },
+
+ set onselectionchange(handler) {
+ this.__DOM_IMPL__.setEventHandler("onselectionchange", handler);
+ },
+
+ replaceSurroundingText: function ic_replaceSurrText(text, offset, length) {
+ let self = this;
+ return this._sendPromise(function(resolverId) {
+ cpmmSendAsyncMessageWithKbID(self, 'Keyboard:ReplaceSurroundingText', {
+ contextId: self._contextId,
+ requestId: resolverId,
+ text: text,
+ offset: offset || 0,
+ length: length || 0
+ });
+ });
+ },
+
+ deleteSurroundingText: function ic_deleteSurrText(offset, length) {
+ return this.replaceSurroundingText(null, offset, length);
+ },
+
+ sendKey: function ic_sendKey(dictOrKeyCode, charCode, modifiers, repeat) {
+ if (typeof dictOrKeyCode === 'number') {
+ // XXX: modifiers are ignored in this API method.
+
+ return this._sendPromise((resolverId) => {
+ cpmmSendAsyncMessageWithKbID(this, 'Keyboard:SendKey', {
+ contextId: this._contextId,
+ requestId: resolverId,
+ method: 'sendKey',
+ keyCode: dictOrKeyCode,
+ charCode: charCode,
+ repeat: repeat
+ });
+ });
+ } else if (typeof dictOrKeyCode === 'object') {
+ return this._sendPromise((resolverId) => {
+ cpmmSendAsyncMessageWithKbID(this, 'Keyboard:SendKey', {
+ contextId: this._contextId,
+ requestId: resolverId,
+ method: 'sendKey',
+ keyboardEventDict: this._getkeyboardEventDict(dictOrKeyCode)
+ });
+ });
+ } else {
+ // XXX: Should not reach here; implies WebIDL binding error.
+ throw new TypeError('Unknown argument passed.');
+ }
+ },
+
+ keydown: function ic_keydown(dict) {
+ return this._sendPromise((resolverId) => {
+ cpmmSendAsyncMessageWithKbID(this, 'Keyboard:SendKey', {
+ contextId: this._contextId,
+ requestId: resolverId,
+ method: 'keydown',
+ keyboardEventDict: this._getkeyboardEventDict(dict)
+ });
+ });
+ },
+
+ keyup: function ic_keyup(dict) {
+ return this._sendPromise((resolverId) => {
+ cpmmSendAsyncMessageWithKbID(this, 'Keyboard:SendKey', {
+ contextId: this._contextId,
+ requestId: resolverId,
+ method: 'keyup',
+ keyboardEventDict: this._getkeyboardEventDict(dict)
+ });
+ });
+ },
+
+ setComposition: function ic_setComposition(text, cursor, clauses, dict) {
+ let self = this;
+ return this._sendPromise((resolverId) => {
+ cpmmSendAsyncMessageWithKbID(self, 'Keyboard:SetComposition', {
+ contextId: self._contextId,
+ requestId: resolverId,
+ text: text,
+ cursor: (typeof cursor !== 'undefined') ? cursor : text.length,
+ clauses: clauses || null,
+ keyboardEventDict: this._getkeyboardEventDict(dict)
+ });
+ });
+ },
+
+ endComposition: function ic_endComposition(text, dict) {
+ let self = this;
+ return this._sendPromise((resolverId) => {
+ cpmmSendAsyncMessageWithKbID(self, 'Keyboard:EndComposition', {
+ contextId: self._contextId,
+ requestId: resolverId,
+ text: text || '',
+ keyboardEventDict: this._getkeyboardEventDict(dict)
+ });
+ });
+ },
+
+ // Generate a new keyboard event by the received keyboard dictionary
+ // and return defaultPrevented's result of the event after dispatching.
+ forwardHardwareKeyEvent: function ic_forwardHardwareKeyEvent(data) {
+ if (!Ci.nsIHardwareKeyHandler) {
+ return;
+ }
+
+ if (!this._context) {
+ return Ci.nsIHardwareKeyHandler.NO_DEFAULT_PREVENTED;
+ }
+ let evt = new this._window.KeyboardEvent(data.type,
+ Cu.cloneInto(data.keyDict,
+ this._window));
+ this._hardwareinput.__DOM_IMPL__.dispatchEvent(evt);
+ return this._getDefaultPreventedValue(evt);
+ },
+
+ _getDefaultPreventedValue: function(evt) {
+ if (!Ci.nsIHardwareKeyHandler) {
+ return;
+ }
+
+ let flags = Ci.nsIHardwareKeyHandler.NO_DEFAULT_PREVENTED;
+
+ if (evt.defaultPrevented) {
+ flags |= Ci.nsIHardwareKeyHandler.DEFAULT_PREVENTED;
+ }
+
+ if (evt.defaultPreventedByChrome) {
+ flags |= Ci.nsIHardwareKeyHandler.DEFAULT_PREVENTED_BY_CHROME;
+ }
+
+ if (evt.defaultPreventedByContent) {
+ flags |= Ci.nsIHardwareKeyHandler.DEFAULT_PREVENTED_BY_CONTENT;
+ }
+
+ return flags;
+ },
+
+ _sendPromise: function(callback) {
+ let self = this;
+ return this._ipcHelper.createPromiseWithId(function(aResolverId) {
+ if (!WindowMap.isActive(self._window)) {
+ self._ipcHelper.removePromiseResolver(aResolverId);
+ reject('Input method is not active.');
+ return;
+ }
+ callback(aResolverId);
+ });
+ },
+
+ // Take a MozInputMethodKeyboardEventDict dict, creates a keyboardEventDict
+ // object that can be sent to forms.js
+ _getkeyboardEventDict: function(dict) {
+ if (typeof dict !== 'object' || !dict.key) {
+ return;
+ }
+
+ var keyboardEventDict = {
+ key: dict.key,
+ code: dict.code,
+ repeat: dict.repeat,
+ flags: 0
+ };
+
+ if (dict.printable) {
+ keyboardEventDict.flags |=
+ Ci.nsITextInputProcessor.KEY_FORCE_PRINTABLE_KEY;
+ }
+
+ if (/^[a-zA-Z0-9]$/.test(dict.key)) {
+ // keyCode must follow the key value in this range;
+ // disregard the keyCode from content.
+ keyboardEventDict.keyCode = dict.key.toUpperCase().charCodeAt(0);
+ } else if (typeof dict.keyCode === 'number') {
+ // Allow keyCode to be specified for other key values.
+ keyboardEventDict.keyCode = dict.keyCode;
+
+ // Allow keyCode to be explicitly set to zero.
+ if (dict.keyCode === 0) {
+ keyboardEventDict.flags |=
+ Ci.nsITextInputProcessor.KEY_KEEP_KEYCODE_ZERO;
+ }
+ }
+
+ return keyboardEventDict;
+ }
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([MozInputMethod]);
diff --git a/dom/inputmethod/forms.js b/dom/inputmethod/forms.js
new file mode 100644
index 000000000..1884f2b4d
--- /dev/null
+++ b/dom/inputmethod/forms.js
@@ -0,0 +1,1561 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- /
+/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
+/* 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";
+
+dump("###################################### forms.js loaded\n");
+
+var Ci = Components.interfaces;
+var Cc = Components.classes;
+var Cu = Components.utils;
+var Cr = Components.results;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import('resource://gre/modules/XPCOMUtils.jsm');
+XPCOMUtils.defineLazyServiceGetter(Services, "fm",
+ "@mozilla.org/focus-manager;1",
+ "nsIFocusManager");
+
+/*
+ * A WeakMap to map window to objects keeping it's TextInputProcessor instance.
+ */
+var WindowMap = {
+ // WeakMap of <window, object> pairs.
+ _map: null,
+
+ /*
+ * Set the object associated to the window and return it.
+ */
+ _getObjForWin: function(win) {
+ if (!this._map) {
+ this._map = new WeakMap();
+ }
+ if (this._map.has(win)) {
+ return this._map.get(win);
+ } else {
+ let obj = {
+ tip: null
+ };
+ this._map.set(win, obj);
+
+ return obj;
+ }
+ },
+
+ getTextInputProcessor: function(win) {
+ if (!win) {
+ return;
+ }
+ let obj = this._getObjForWin(win);
+ let tip = obj.tip
+
+ if (!tip) {
+ tip = obj.tip = Cc["@mozilla.org/text-input-processor;1"]
+ .createInstance(Ci.nsITextInputProcessor);
+ }
+
+ if (!tip.beginInputTransaction(win, textInputProcessorCallback)) {
+ tip = obj.tip = null;
+ }
+ return tip;
+ }
+};
+
+const RESIZE_SCROLL_DELAY = 20;
+// In content editable node, when there are hidden elements such as <br>, it
+// may need more than one (usually less than 3 times) move/extend operations
+// to change the selection range. If we cannot change the selection range
+// with more than 20 opertations, we are likely being blocked and cannot change
+// the selection range any more.
+const MAX_BLOCKED_COUNT = 20;
+
+var HTMLDocument = Ci.nsIDOMHTMLDocument;
+var HTMLHtmlElement = Ci.nsIDOMHTMLHtmlElement;
+var HTMLBodyElement = Ci.nsIDOMHTMLBodyElement;
+var HTMLIFrameElement = Ci.nsIDOMHTMLIFrameElement;
+var HTMLInputElement = Ci.nsIDOMHTMLInputElement;
+var HTMLTextAreaElement = Ci.nsIDOMHTMLTextAreaElement;
+var HTMLSelectElement = Ci.nsIDOMHTMLSelectElement;
+var HTMLOptGroupElement = Ci.nsIDOMHTMLOptGroupElement;
+var HTMLOptionElement = Ci.nsIDOMHTMLOptionElement;
+
+function guessKeyNameFromKeyCode(KeyboardEvent, aKeyCode) {
+ switch (aKeyCode) {
+ case KeyboardEvent.DOM_VK_CANCEL:
+ return "Cancel";
+ case KeyboardEvent.DOM_VK_HELP:
+ return "Help";
+ case KeyboardEvent.DOM_VK_BACK_SPACE:
+ return "Backspace";
+ case KeyboardEvent.DOM_VK_TAB:
+ return "Tab";
+ case KeyboardEvent.DOM_VK_CLEAR:
+ return "Clear";
+ case KeyboardEvent.DOM_VK_RETURN:
+ return "Enter";
+ case KeyboardEvent.DOM_VK_SHIFT:
+ return "Shift";
+ case KeyboardEvent.DOM_VK_CONTROL:
+ return "Control";
+ case KeyboardEvent.DOM_VK_ALT:
+ return "Alt";
+ case KeyboardEvent.DOM_VK_PAUSE:
+ return "Pause";
+ case KeyboardEvent.DOM_VK_EISU:
+ return "Eisu";
+ case KeyboardEvent.DOM_VK_ESCAPE:
+ return "Escape";
+ case KeyboardEvent.DOM_VK_CONVERT:
+ return "Convert";
+ case KeyboardEvent.DOM_VK_NONCONVERT:
+ return "NonConvert";
+ case KeyboardEvent.DOM_VK_ACCEPT:
+ return "Accept";
+ case KeyboardEvent.DOM_VK_MODECHANGE:
+ return "ModeChange";
+ case KeyboardEvent.DOM_VK_PAGE_UP:
+ return "PageUp";
+ case KeyboardEvent.DOM_VK_PAGE_DOWN:
+ return "PageDown";
+ case KeyboardEvent.DOM_VK_END:
+ return "End";
+ case KeyboardEvent.DOM_VK_HOME:
+ return "Home";
+ case KeyboardEvent.DOM_VK_LEFT:
+ return "ArrowLeft";
+ case KeyboardEvent.DOM_VK_UP:
+ return "ArrowUp";
+ case KeyboardEvent.DOM_VK_RIGHT:
+ return "ArrowRight";
+ case KeyboardEvent.DOM_VK_DOWN:
+ return "ArrowDown";
+ case KeyboardEvent.DOM_VK_SELECT:
+ return "Select";
+ case KeyboardEvent.DOM_VK_PRINT:
+ return "Print";
+ case KeyboardEvent.DOM_VK_EXECUTE:
+ return "Execute";
+ case KeyboardEvent.DOM_VK_PRINTSCREEN:
+ return "PrintScreen";
+ case KeyboardEvent.DOM_VK_INSERT:
+ return "Insert";
+ case KeyboardEvent.DOM_VK_DELETE:
+ return "Delete";
+ case KeyboardEvent.DOM_VK_WIN:
+ return "OS";
+ case KeyboardEvent.DOM_VK_CONTEXT_MENU:
+ return "ContextMenu";
+ case KeyboardEvent.DOM_VK_SLEEP:
+ return "Standby";
+ case KeyboardEvent.DOM_VK_F1:
+ return "F1";
+ case KeyboardEvent.DOM_VK_F2:
+ return "F2";
+ case KeyboardEvent.DOM_VK_F3:
+ return "F3";
+ case KeyboardEvent.DOM_VK_F4:
+ return "F4";
+ case KeyboardEvent.DOM_VK_F5:
+ return "F5";
+ case KeyboardEvent.DOM_VK_F6:
+ return "F6";
+ case KeyboardEvent.DOM_VK_F7:
+ return "F7";
+ case KeyboardEvent.DOM_VK_F8:
+ return "F8";
+ case KeyboardEvent.DOM_VK_F9:
+ return "F9";
+ case KeyboardEvent.DOM_VK_F10:
+ return "F10";
+ case KeyboardEvent.DOM_VK_F11:
+ return "F11";
+ case KeyboardEvent.DOM_VK_F12:
+ return "F12";
+ case KeyboardEvent.DOM_VK_F13:
+ return "F13";
+ case KeyboardEvent.DOM_VK_F14:
+ return "F14";
+ case KeyboardEvent.DOM_VK_F15:
+ return "F15";
+ case KeyboardEvent.DOM_VK_F16:
+ return "F16";
+ case KeyboardEvent.DOM_VK_F17:
+ return "F17";
+ case KeyboardEvent.DOM_VK_F18:
+ return "F18";
+ case KeyboardEvent.DOM_VK_F19:
+ return "F19";
+ case KeyboardEvent.DOM_VK_F20:
+ return "F20";
+ case KeyboardEvent.DOM_VK_F21:
+ return "F21";
+ case KeyboardEvent.DOM_VK_F22:
+ return "F22";
+ case KeyboardEvent.DOM_VK_F23:
+ return "F23";
+ case KeyboardEvent.DOM_VK_F24:
+ return "F24";
+ case KeyboardEvent.DOM_VK_NUM_LOCK:
+ return "NumLock";
+ case KeyboardEvent.DOM_VK_SCROLL_LOCK:
+ return "ScrollLock";
+ case KeyboardEvent.DOM_VK_VOLUME_MUTE:
+ return "AudioVolumeMute";
+ case KeyboardEvent.DOM_VK_VOLUME_DOWN:
+ return "AudioVolumeDown";
+ case KeyboardEvent.DOM_VK_VOLUME_UP:
+ return "AudioVolumeUp";
+ case KeyboardEvent.DOM_VK_META:
+ return "Meta";
+ case KeyboardEvent.DOM_VK_ALTGR:
+ return "AltGraph";
+ case KeyboardEvent.DOM_VK_ATTN:
+ return "Attn";
+ case KeyboardEvent.DOM_VK_CRSEL:
+ return "CrSel";
+ case KeyboardEvent.DOM_VK_EXSEL:
+ return "ExSel";
+ case KeyboardEvent.DOM_VK_EREOF:
+ return "EraseEof";
+ case KeyboardEvent.DOM_VK_PLAY:
+ return "Play";
+ default:
+ return "Unidentified";
+ }
+}
+
+var FormVisibility = {
+ /**
+ * Searches upwards in the DOM for an element that has been scrolled.
+ *
+ * @param {HTMLElement} node element to start search at.
+ * @return {Window|HTMLElement|Null} null when none are found window/element otherwise.
+ */
+ findScrolled: function fv_findScrolled(node) {
+ let win = node.ownerDocument.defaultView;
+
+ while (!(node instanceof HTMLBodyElement)) {
+
+ // We can skip elements that have not been scrolled.
+ // We only care about top now remember to add the scrollLeft
+ // check if we decide to care about the X axis.
+ if (node.scrollTop !== 0) {
+ // the element has been scrolled so we may need to adjust
+ // where we think the root element is located.
+ //
+ // Otherwise it may seem visible but be scrolled out of the viewport
+ // inside this scrollable node.
+ return node;
+ } else {
+ // this node does not effect where we think
+ // the node is even if it is scrollable it has not hidden
+ // the element we are looking for.
+ node = node.parentNode;
+ continue;
+ }
+ }
+
+ // we also care about the window this is the more
+ // common case where the content is larger then
+ // the viewport/screen.
+ if (win.scrollMaxX != win.scrollMinX || win.scrollMaxY != win.scrollMinY) {
+ return win;
+ }
+
+ return null;
+ },
+
+ /**
+ * Checks if "top and "bottom" points of the position is visible.
+ *
+ * @param {Number} top position.
+ * @param {Number} height of the element.
+ * @param {Number} maxHeight of the window.
+ * @return {Boolean} true when visible.
+ */
+ yAxisVisible: function fv_yAxisVisible(top, height, maxHeight) {
+ return (top > 0 && (top + height) < maxHeight);
+ },
+
+ /**
+ * Searches up through the dom for scrollable elements
+ * which are not currently visible (relative to the viewport).
+ *
+ * @param {HTMLElement} element to start search at.
+ * @param {Object} pos .top, .height and .width of element.
+ */
+ scrollablesVisible: function fv_scrollablesVisible(element, pos) {
+ while ((element = this.findScrolled(element))) {
+ if (element.window && element.self === element)
+ break;
+
+ // remember getBoundingClientRect does not care
+ // about scrolling only where the element starts
+ // in the document.
+ let offset = element.getBoundingClientRect();
+
+ // the top of both the scrollable area and
+ // the form element itself are in the same document.
+ // We adjust the "top" so if the elements coordinates
+ // are relative to the viewport in the current document.
+ let adjustedTop = pos.top - offset.top;
+
+ let visible = this.yAxisVisible(
+ adjustedTop,
+ pos.height,
+ offset.height
+ );
+
+ if (!visible)
+ return false;
+
+ element = element.parentNode;
+ }
+
+ return true;
+ },
+
+ /**
+ * Verifies the element is visible in the viewport.
+ * Handles scrollable areas, frames and scrollable viewport(s) (windows).
+ *
+ * @param {HTMLElement} element to verify.
+ * @return {Boolean} true when visible.
+ */
+ isVisible: function fv_isVisible(element) {
+ // scrollable frames can be ignored we just care about iframes...
+ let rect = element.getBoundingClientRect();
+ let parent = element.ownerDocument.defaultView;
+
+ // used to calculate the inner position of frames / scrollables.
+ // The intent was to use this information to scroll either up or down.
+ // scrollIntoView(true) will _break_ some web content so we can't do
+ // this today. If we want that functionality we need to manually scroll
+ // the individual elements.
+ let pos = {
+ top: rect.top,
+ height: rect.height,
+ width: rect.width
+ };
+
+ let visible = true;
+
+ do {
+ let frame = parent.frameElement;
+ visible = visible &&
+ this.yAxisVisible(pos.top, pos.height, parent.innerHeight) &&
+ this.scrollablesVisible(element, pos);
+
+ // nothing we can do about this now...
+ // In the future we can use this information to scroll
+ // only the elements we need to at this point as we should
+ // have all the details we need to figure out how to scroll.
+ if (!visible)
+ return false;
+
+ if (frame) {
+ let frameRect = frame.getBoundingClientRect();
+
+ pos.top += frameRect.top + frame.clientTop;
+ }
+ } while (
+ (parent !== parent.parent) &&
+ (parent = parent.parent)
+ );
+
+ return visible;
+ }
+};
+
+// This object implements nsITextInputProcessorCallback
+var textInputProcessorCallback = {
+ onNotify: function(aTextInputProcessor, aNotification) {
+ try {
+ switch (aNotification.type) {
+ case "request-to-commit":
+ // TODO: Send a notification through asyncMessage to the keyboard here.
+ aTextInputProcessor.commitComposition();
+
+ break;
+ case "request-to-cancel":
+ // TODO: Send a notification through asyncMessage to the keyboard here.
+ aTextInputProcessor.cancelComposition();
+
+ break;
+
+ case "notify-detached":
+ // TODO: Send a notification through asyncMessage to the keyboard here.
+ break;
+
+ // TODO: Manage _focusedElement for text input from here instead.
+ // (except for <select> which will be need to handled elsewhere)
+ case "notify-focus":
+ break;
+
+ case "notify-blur":
+ break;
+ }
+ } catch (e) {
+ return false;
+ }
+ return true;
+ }
+};
+
+var FormAssistant = {
+ init: function fa_init() {
+ addEventListener("focus", this, true, false);
+ addEventListener("blur", this, true, false);
+ addEventListener("resize", this, true, false);
+ // We should not blur the fucus if the submit event is cancelled,
+ // therefore we are binding our event listener in the bubbling phase here.
+ addEventListener("submit", this, false, false);
+ addEventListener("pagehide", this, true, false);
+ addEventListener("beforeunload", this, true, false);
+ addEventListener("input", this, true, false);
+ addEventListener("keydown", this, true, false);
+ addEventListener("keyup", this, true, false);
+ addMessageListener("Forms:Select:Choice", this);
+ addMessageListener("Forms:Input:Value", this);
+ addMessageListener("Forms:Select:Blur", this);
+ addMessageListener("Forms:SetSelectionRange", this);
+ addMessageListener("Forms:ReplaceSurroundingText", this);
+ addMessageListener("Forms:Input:SendKey", this);
+ addMessageListener("Forms:GetContext", this);
+ addMessageListener("Forms:SetComposition", this);
+ addMessageListener("Forms:EndComposition", this);
+ },
+
+ ignoredInputTypes: new Set([
+ 'button', 'file', 'checkbox', 'radio', 'reset', 'submit', 'image',
+ 'range'
+ ]),
+
+ isHandlingFocus: false,
+ selectionStart: -1,
+ selectionEnd: -1,
+ text: "",
+
+ scrollIntoViewTimeout: null,
+ _focusedElement: null,
+ _focusCounter: 0, // up one for every time we focus a new element
+ _focusDeleteObserver: null,
+ _focusContentObserver: null,
+ _documentEncoder: null,
+ _editor: null,
+ _editing: false,
+ _selectionPrivate: null,
+
+ get focusedElement() {
+ if (this._focusedElement && Cu.isDeadWrapper(this._focusedElement))
+ this._focusedElement = null;
+
+ return this._focusedElement;
+ },
+
+ set focusedElement(val) {
+ this._focusCounter++;
+ this._focusedElement = val;
+ },
+
+ setFocusedElement: function fa_setFocusedElement(element) {
+ let self = this;
+
+ if (element === this.focusedElement)
+ return;
+
+ if (this.focusedElement) {
+ this.focusedElement.removeEventListener('compositionend', this);
+ if (this._focusDeleteObserver) {
+ this._focusDeleteObserver.disconnect();
+ this._focusDeleteObserver = null;
+ }
+ if (this._focusContentObserver) {
+ this._focusContentObserver.disconnect();
+ this._focusContentObserver = null;
+ }
+ if (this._selectionPrivate) {
+ this._selectionPrivate.removeSelectionListener(this);
+ this._selectionPrivate = null;
+ }
+ }
+
+ this._documentEncoder = null;
+ if (this._editor) {
+ // When the nsIFrame of the input element is reconstructed by
+ // CSS restyling, the editor observers are removed. Catch
+ // [nsIEditor.removeEditorObserver] failure exception if that
+ // happens.
+ try {
+ this._editor.removeEditorObserver(this);
+ } catch (e) {}
+ this._editor = null;
+ }
+
+ if (element) {
+ element.addEventListener('compositionend', this);
+ if (isContentEditable(element)) {
+ this._documentEncoder = getDocumentEncoder(element);
+ }
+ this._editor = getPlaintextEditor(element);
+ if (this._editor) {
+ // Add a nsIEditorObserver to monitor the text content of the focused
+ // element.
+ this._editor.addEditorObserver(this);
+
+ let selection = this._editor.selection;
+ if (selection) {
+ this._selectionPrivate = selection.QueryInterface(Ci.nsISelectionPrivate);
+ this._selectionPrivate.addSelectionListener(this);
+ }
+ }
+
+ // If our focusedElement is removed from DOM we want to handle it properly
+ let MutationObserver = element.ownerDocument.defaultView.MutationObserver;
+ this._focusDeleteObserver = new MutationObserver(function(mutations) {
+ var del = [].some.call(mutations, function(m) {
+ return [].some.call(m.removedNodes, function(n) {
+ return n.contains(element);
+ });
+ });
+ if (del && element === self.focusedElement) {
+ self.unhandleFocus();
+ }
+ });
+
+ this._focusDeleteObserver.observe(element.ownerDocument.body, {
+ childList: true,
+ subtree: true
+ });
+
+ // If contenteditable, also add a mutation observer on its content and
+ // call selectionChanged when a change occurs
+ if (isContentEditable(element)) {
+ this._focusContentObserver = new MutationObserver(function() {
+ this.updateSelection();
+ }.bind(this));
+
+ this._focusContentObserver.observe(element, {
+ childList: true,
+ subtree: true
+ });
+ }
+ }
+
+ this.focusedElement = element;
+ },
+
+ notifySelectionChanged: function(aDocument, aSelection, aReason) {
+ this.updateSelection();
+ },
+
+ get documentEncoder() {
+ return this._documentEncoder;
+ },
+
+ // Get the nsIPlaintextEditor object of current input field.
+ get editor() {
+ return this._editor;
+ },
+
+ // Implements nsIEditorObserver get notification when the text content of
+ // current input field has changed.
+ EditAction: function fa_editAction() {
+ if (this._editing || !this.isHandlingFocus) {
+ return;
+ }
+ this.sendInputState(this.focusedElement);
+ },
+
+ handleEvent: function fa_handleEvent(evt) {
+ let target = evt.composedTarget;
+
+ let range = null;
+ switch (evt.type) {
+ case "focus":
+ if (!target) {
+ break;
+ }
+
+ // Focusing on Window, Document or iFrame should focus body
+ if (target instanceof HTMLHtmlElement) {
+ target = target.document.body;
+ } else if (target instanceof HTMLDocument) {
+ target = target.body;
+ } else if (target instanceof HTMLIFrameElement) {
+ target = target.contentDocument ? target.contentDocument.body
+ : null;
+ }
+
+ if (!target) {
+ break;
+ }
+
+ if (isContentEditable(target)) {
+ this.handleFocus(this.getTopLevelEditable(target));
+ this.updateSelection();
+ break;
+ }
+
+ if (this.isFocusableElement(target)) {
+ this.handleFocus(target);
+ this.updateSelection();
+ }
+ break;
+
+ case "pagehide":
+ case "beforeunload":
+ // We are only interested to the pagehide and beforeunload events from
+ // the root document.
+ if (target && target != content.document) {
+ break;
+ }
+ // fall through
+ case "submit":
+ if (this.focusedElement && !evt.defaultPrevented) {
+ this.focusedElement.blur();
+ }
+ break;
+
+ case "blur":
+ if (this.focusedElement) {
+ this.unhandleFocus();
+ }
+ break;
+
+ case "resize":
+ if (!this.isHandlingFocus)
+ return;
+
+ if (this.scrollIntoViewTimeout) {
+ content.clearTimeout(this.scrollIntoViewTimeout);
+ this.scrollIntoViewTimeout = null;
+ }
+
+ // We may receive multiple resize events in quick succession, so wait
+ // a bit before scrolling the input element into view.
+ if (this.focusedElement) {
+ this.scrollIntoViewTimeout = content.setTimeout(function () {
+ this.scrollIntoViewTimeout = null;
+ if (this.focusedElement && !FormVisibility.isVisible(this.focusedElement)) {
+ scrollSelectionOrElementIntoView(this.focusedElement);
+ }
+ }.bind(this), RESIZE_SCROLL_DELAY);
+ }
+ break;
+
+ case "keydown":
+ if (!this.focusedElement || this._editing) {
+ break;
+ }
+
+ CompositionManager.endComposition('');
+ break;
+
+ case "keyup":
+ if (!this.focusedElement || this._editing) {
+ break;
+ }
+
+ CompositionManager.endComposition('');
+ break;
+
+ case "compositionend":
+ if (!this.focusedElement) {
+ break;
+ }
+
+ CompositionManager.onCompositionEnd();
+ break;
+ }
+ },
+
+ receiveMessage: function fa_receiveMessage(msg) {
+ let target = this.focusedElement;
+ let json = msg.json;
+
+ // To not break mozKeyboard contextId is optional
+ if ('contextId' in json &&
+ json.contextId !== this._focusCounter &&
+ json.requestId) {
+ // Ignore messages that are meant for a previously focused element
+ sendAsyncMessage("Forms:SequenceError", {
+ requestId: json.requestId,
+ error: "Expected contextId " + this._focusCounter +
+ " but was " + json.contextId
+ });
+ return;
+ }
+
+ if (!target) {
+ return;
+ }
+
+ this._editing = true;
+ switch (msg.name) {
+ case "Forms:Input:Value": {
+ CompositionManager.endComposition('');
+
+ target.value = json.value;
+
+ let event = target.ownerDocument.createEvent('HTMLEvents');
+ event.initEvent('input', true, false);
+ target.dispatchEvent(event);
+ break;
+ }
+
+ case "Forms:Input:SendKey":
+ CompositionManager.endComposition('');
+
+ let win = target.ownerDocument.defaultView;
+ let tip = WindowMap.getTextInputProcessor(win);
+ if (!tip) {
+ if (json.requestId) {
+ sendAsyncMessage("Forms:SendKey:Result:Error", {
+ requestId: json.requestId,
+ error: "Unable to start input transaction."
+ });
+ }
+
+ break;
+ }
+
+ // If we receive a keyboardEventDict from json, that means the user
+ // is calling the method with the new arguments.
+ // Otherwise, we would have to construct our own keyboardEventDict
+ // based on legacy values we have received.
+ let keyboardEventDict = json.keyboardEventDict;
+ let flags = 0;
+
+ if (keyboardEventDict) {
+ if ('flags' in keyboardEventDict) {
+ flags = keyboardEventDict.flags;
+ }
+ } else {
+ // The naive way to figure out if the key to dispatch is printable.
+ let printable = !!json.charCode;
+
+ // For printable keys, the value should be the actual character.
+ // For non-printable keys, it should be a value in the D3E spec.
+ // Here we make some educated guess for it.
+ let key = printable ?
+ String.fromCharCode(json.charCode) :
+ guessKeyNameFromKeyCode(win.KeyboardEvent, json.keyCode);
+
+ // keyCode from content is only respected when the key is not an
+ // an alphanumeric character. We also ask TextInputProcessor not to
+ // infer this value for non-printable keys to keep the original
+ // behavior.
+ let keyCode = (printable && /^[a-zA-Z0-9]$/.test(key)) ?
+ key.toUpperCase().charCodeAt(0) :
+ json.keyCode;
+
+ keyboardEventDict = {
+ key: key,
+ keyCode: keyCode,
+ // We don't have any information to tell the virtual key the
+ // user have interacted with.
+ code: "",
+ // We do not have the information to infer location of the virtual key
+ // either (and we would need TextInputProcessor not to compute it).
+ location: 0,
+ // This indicates the key is triggered for repeats.
+ repeat: json.repeat
+ };
+
+ flags = tip.KEY_KEEP_KEY_LOCATION_STANDARD;
+ if (!printable) {
+ flags |= tip.KEY_NON_PRINTABLE_KEY;
+ }
+ if (!keyboardEventDict.keyCode) {
+ flags |= tip.KEY_KEEP_KEYCODE_ZERO;
+ }
+ }
+
+ let keyboardEvent = new win.KeyboardEvent("", keyboardEventDict);
+
+ let keydownDefaultPrevented = false;
+ try {
+ switch (json.method) {
+ case 'sendKey': {
+ let consumedFlags = tip.keydown(keyboardEvent, flags);
+ keydownDefaultPrevented =
+ !!(tip.KEYDOWN_IS_CONSUMED & consumedFlags);
+ if (!keyboardEventDict.repeat) {
+ tip.keyup(keyboardEvent, flags);
+ }
+ break;
+ }
+ case 'keydown': {
+ let consumedFlags = tip.keydown(keyboardEvent, flags);
+ keydownDefaultPrevented =
+ !!(tip.KEYDOWN_IS_CONSUMED & consumedFlags);
+ break;
+ }
+ case 'keyup': {
+ tip.keyup(keyboardEvent, flags);
+
+ break;
+ }
+ }
+ } catch (err) {
+ dump("forms.js:" + err.toString() + "\n");
+
+ if (json.requestId) {
+ if (err instanceof Ci.nsIException &&
+ err.result == Cr.NS_ERROR_ILLEGAL_VALUE) {
+ sendAsyncMessage("Forms:SendKey:Result:Error", {
+ requestId: json.requestId,
+ error: "The values specified are illegal."
+ });
+ } else {
+ sendAsyncMessage("Forms:SendKey:Result:Error", {
+ requestId: json.requestId,
+ error: "Unable to type into destroyed input."
+ });
+ }
+ }
+
+ break;
+ }
+
+ if (json.requestId) {
+ if (keydownDefaultPrevented) {
+ sendAsyncMessage("Forms:SendKey:Result:Error", {
+ requestId: json.requestId,
+ error: "Key event(s) was cancelled."
+ });
+ } else {
+ sendAsyncMessage("Forms:SendKey:Result:OK", {
+ requestId: json.requestId,
+ selectioninfo: this.getSelectionInfo()
+ });
+ }
+ }
+
+ break;
+
+ case "Forms:Select:Choice":
+ let options = target.options;
+ let valueChanged = false;
+ if ("index" in json) {
+ if (options.selectedIndex != json.index) {
+ options.selectedIndex = json.index;
+ valueChanged = true;
+ }
+ } else if ("indexes" in json) {
+ for (let i = 0; i < options.length; i++) {
+ let newValue = (json.indexes.indexOf(i) != -1);
+ if (options.item(i).selected != newValue) {
+ options.item(i).selected = newValue;
+ valueChanged = true;
+ }
+ }
+ }
+
+ // only fire onchange event if any selected option is changed
+ if (valueChanged) {
+ let event = target.ownerDocument.createEvent('HTMLEvents');
+ event.initEvent('change', true, true);
+ target.dispatchEvent(event);
+ }
+ break;
+
+ case "Forms:Select:Blur": {
+ if (this.focusedElement) {
+ this.focusedElement.blur();
+ }
+
+ break;
+ }
+
+ case "Forms:SetSelectionRange": {
+ CompositionManager.endComposition('');
+
+ let start = json.selectionStart;
+ let end = json.selectionEnd;
+
+ if (!setSelectionRange(target, start, end)) {
+ if (json.requestId) {
+ sendAsyncMessage("Forms:SetSelectionRange:Result:Error", {
+ requestId: json.requestId,
+ error: "failed"
+ });
+ }
+ break;
+ }
+
+ if (json.requestId) {
+ sendAsyncMessage("Forms:SetSelectionRange:Result:OK", {
+ requestId: json.requestId,
+ selectioninfo: this.getSelectionInfo()
+ });
+ }
+ break;
+ }
+
+ case "Forms:ReplaceSurroundingText": {
+ CompositionManager.endComposition('');
+
+ if (!replaceSurroundingText(target,
+ json.text,
+ json.offset,
+ json.length)) {
+ if (json.requestId) {
+ sendAsyncMessage("Forms:ReplaceSurroundingText:Result:Error", {
+ requestId: json.requestId,
+ error: "failed"
+ });
+ }
+ break;
+ }
+
+ if (json.requestId) {
+ sendAsyncMessage("Forms:ReplaceSurroundingText:Result:OK", {
+ requestId: json.requestId,
+ selectioninfo: this.getSelectionInfo()
+ });
+ }
+ break;
+ }
+
+ case "Forms:GetContext": {
+ let obj = getJSON(target, this._focusCounter);
+ sendAsyncMessage("Forms:GetContext:Result:OK", obj);
+ break;
+ }
+
+ case "Forms:SetComposition": {
+ CompositionManager.setComposition(target, json.text, json.cursor,
+ json.clauses, json.keyboardEventDict);
+ sendAsyncMessage("Forms:SetComposition:Result:OK", {
+ requestId: json.requestId,
+ selectioninfo: this.getSelectionInfo()
+ });
+ break;
+ }
+
+ case "Forms:EndComposition": {
+ CompositionManager.endComposition(json.text, json.keyboardEventDict);
+ sendAsyncMessage("Forms:EndComposition:Result:OK", {
+ requestId: json.requestId,
+ selectioninfo: this.getSelectionInfo()
+ });
+ break;
+ }
+ }
+ this._editing = false;
+
+ },
+
+ handleFocus: function fa_handleFocus(target) {
+ if (this.focusedElement === target)
+ return;
+
+ if (target instanceof HTMLOptionElement)
+ target = target.parentNode;
+
+ this.setFocusedElement(target);
+ this.sendInputState(target);
+ this.isHandlingFocus = true;
+ },
+
+ unhandleFocus: function fa_unhandleFocus() {
+ this.setFocusedElement(null);
+ this.isHandlingFocus = false;
+ this.selectionStart = -1;
+ this.selectionEnd = -1;
+ this.text = "";
+ sendAsyncMessage("Forms:Blur", {});
+ },
+
+ isFocusableElement: function fa_isFocusableElement(element) {
+ if (element instanceof HTMLSelectElement ||
+ element instanceof HTMLTextAreaElement)
+ return true;
+
+ if (element instanceof HTMLOptionElement &&
+ element.parentNode instanceof HTMLSelectElement)
+ return true;
+
+ return (element instanceof HTMLInputElement &&
+ !this.ignoredInputTypes.has(element.type) &&
+ !element.readOnly);
+ },
+
+ getTopLevelEditable: function fa_getTopLevelEditable(element) {
+ function retrieveTopLevelEditable(element) {
+ while (element && !isContentEditable(element))
+ element = element.parentNode;
+
+ return element;
+ }
+
+ return retrieveTopLevelEditable(element) || element;
+ },
+
+ sendInputState: function(element) {
+ sendAsyncMessage("Forms:Focus", getJSON(element, this._focusCounter));
+ },
+
+ getSelectionInfo: function fa_getSelectionInfo() {
+ let element = this.focusedElement;
+ let range = getSelectionRange(element);
+
+ let text = isContentEditable(element) ? getContentEditableText(element)
+ : element.value;
+
+ let changed = this.selectionStart !== range[0] ||
+ this.selectionEnd !== range[1] ||
+ this.text !== text;
+
+ this.selectionStart = range[0];
+ this.selectionEnd = range[1];
+ this.text = text;
+
+ return {
+ selectionStart: range[0],
+ selectionEnd: range[1],
+ text: text,
+ changed: changed
+ };
+ },
+
+ _selectionTimeout: null,
+
+ // Notify when the selection range changes
+ updateSelection: function fa_updateSelection() {
+ // A call to setSelectionRange on input field causes 2 selection changes
+ // one to [0,0] and one to actual value. Both are sent in same tick.
+ // Prevent firing two events in that scenario, always only use the last 1.
+ //
+ // It is also a workaround for Bug 1053048, which prevents
+ // getSelectionInfo() accessing selectionStart or selectionEnd in the
+ // callback function of nsISelectionListener::NotifySelectionChanged().
+ if (this._selectionTimeout) {
+ content.clearTimeout(this._selectionTimeout);
+ }
+ this._selectionTimeout = content.setTimeout(function() {
+ if (!this.focusedElement) {
+ return;
+ }
+ let selectionInfo = this.getSelectionInfo();
+ if (selectionInfo.changed) {
+ sendAsyncMessage("Forms:SelectionChange", selectionInfo);
+ }
+ }.bind(this), 0);
+ }
+};
+
+FormAssistant.init();
+
+function isContentEditable(element) {
+ if (!element) {
+ return false;
+ }
+
+ if (element.isContentEditable || element.designMode == "on")
+ return true;
+
+ return element.ownerDocument && element.ownerDocument.designMode == "on";
+}
+
+function isPlainTextField(element) {
+ if (!element) {
+ return false;
+ }
+
+ return element instanceof HTMLTextAreaElement ||
+ (element instanceof HTMLInputElement &&
+ element.mozIsTextField(false));
+}
+
+function getJSON(element, focusCounter) {
+ // <input type=number> has a nested anonymous <input type=text> element that
+ // takes focus on behalf of the number control when someone tries to focus
+ // the number control. If |element| is such an anonymous text control then we
+ // need it's number control here in order to get the correct 'type' etc.:
+ element = element.ownerNumberControl || element;
+
+ let type = element.tagName.toLowerCase();
+ let inputType = (element.type || "").toLowerCase();
+ let value = element.value || "";
+ let max = element.max || "";
+ let min = element.min || "";
+
+ // Treat contenteditable element as a special text area field
+ if (isContentEditable(element)) {
+ type = "contenteditable";
+ inputType = "textarea";
+ value = getContentEditableText(element);
+ }
+
+ // Until the input type=date/datetime/range have been implemented
+ // let's return their real type even if the platform returns 'text'
+ let attributeInputType = element.getAttribute("type") || "";
+
+ if (attributeInputType) {
+ let inputTypeLowerCase = attributeInputType.toLowerCase();
+ switch (inputTypeLowerCase) {
+ case "datetime":
+ case "datetime-local":
+ case "month":
+ case "week":
+ case "range":
+ inputType = inputTypeLowerCase;
+ break;
+ }
+ }
+
+ // Gecko has some support for @inputmode but behind a preference and
+ // it is disabled by default.
+ // Gaia is then using @x-inputmode has its proprietary way to set
+ // inputmode for fields. This shouldn't be used outside of pre-installed
+ // apps because the attribute is going to disappear as soon as a definitive
+ // solution will be find.
+ let inputMode = element.getAttribute('x-inputmode');
+ if (inputMode) {
+ inputMode = inputMode.toLowerCase();
+ } else {
+ inputMode = '';
+ }
+
+ let range = getSelectionRange(element);
+
+ return {
+ "contextId": focusCounter,
+
+ "type": type,
+ "inputType": inputType,
+ "inputMode": inputMode,
+
+ "choices": getListForElement(element),
+ "value": value,
+ "selectionStart": range[0],
+ "selectionEnd": range[1],
+ "max": max,
+ "min": min,
+ "lang": element.lang || ""
+ };
+}
+
+function getListForElement(element) {
+ if (!(element instanceof HTMLSelectElement))
+ return null;
+
+ let optionIndex = 0;
+ let result = {
+ "multiple": element.multiple,
+ "choices": []
+ };
+
+ // Build up a flat JSON array of the choices.
+ // In HTML, it's possible for select element choices to be under a
+ // group header (but not recursively). We distinguish between headers
+ // and entries using the boolean "list.group".
+ let children = element.children;
+ for (let i = 0; i < children.length; i++) {
+ let child = children[i];
+
+ if (child instanceof HTMLOptGroupElement) {
+ result.choices.push({
+ "group": true,
+ "text": child.label || child.firstChild.data,
+ "disabled": child.disabled
+ });
+
+ let subchildren = child.children;
+ for (let j = 0; j < subchildren.length; j++) {
+ let subchild = subchildren[j];
+ result.choices.push({
+ "group": false,
+ "inGroup": true,
+ "text": subchild.text,
+ "disabled": child.disabled || subchild.disabled,
+ "selected": subchild.selected,
+ "optionIndex": optionIndex++
+ });
+ }
+ } else if (child instanceof HTMLOptionElement) {
+ result.choices.push({
+ "group": false,
+ "inGroup": false,
+ "text": child.text,
+ "disabled": child.disabled,
+ "selected": child.selected,
+ "optionIndex": optionIndex++
+ });
+ }
+ }
+
+ return result;
+};
+
+// Create a plain text document encode from the focused element.
+function getDocumentEncoder(element) {
+ let encoder = Cc["@mozilla.org/layout/documentEncoder;1?type=text/plain"]
+ .createInstance(Ci.nsIDocumentEncoder);
+ let flags = Ci.nsIDocumentEncoder.SkipInvisibleContent |
+ Ci.nsIDocumentEncoder.OutputRaw |
+ Ci.nsIDocumentEncoder.OutputDropInvisibleBreak |
+ // Bug 902847. Don't trim trailing spaces of a line.
+ Ci.nsIDocumentEncoder.OutputDontRemoveLineEndingSpaces |
+ Ci.nsIDocumentEncoder.OutputLFLineBreak |
+ Ci.nsIDocumentEncoder.OutputNonTextContentAsPlaceholder;
+ encoder.init(element.ownerDocument, "text/plain", flags);
+ return encoder;
+}
+
+// Get the visible content text of a content editable element
+function getContentEditableText(element) {
+ if (!element || !isContentEditable(element)) {
+ return null;
+ }
+
+ let doc = element.ownerDocument;
+ let range = doc.createRange();
+ range.selectNodeContents(element);
+ let encoder = FormAssistant.documentEncoder;
+ encoder.setRange(range);
+ return encoder.encodeToString();
+}
+
+function getSelectionRange(element) {
+ let start = 0;
+ let end = 0;
+ if (isPlainTextField(element)) {
+ // Get the selection range of <input> and <textarea> elements
+ start = element.selectionStart;
+ end = element.selectionEnd;
+ } else if (isContentEditable(element)){
+ // Get the selection range of contenteditable elements
+ let win = element.ownerDocument.defaultView;
+ let sel = win.getSelection();
+ if (sel && sel.rangeCount > 0) {
+ start = getContentEditableSelectionStart(element, sel);
+ end = start + getContentEditableSelectionLength(element, sel);
+ } else {
+ dump("Failed to get window.getSelection()\n");
+ }
+ }
+ return [start, end];
+ }
+
+function getContentEditableSelectionStart(element, selection) {
+ let doc = element.ownerDocument;
+ let range = doc.createRange();
+ range.setStart(element, 0);
+ range.setEnd(selection.anchorNode, selection.anchorOffset);
+ let encoder = FormAssistant.documentEncoder;
+ encoder.setRange(range);
+ return encoder.encodeToString().length;
+}
+
+function getContentEditableSelectionLength(element, selection) {
+ let encoder = FormAssistant.documentEncoder;
+ encoder.setRange(selection.getRangeAt(0));
+ return encoder.encodeToString().length;
+}
+
+function setSelectionRange(element, start, end) {
+ let isTextField = isPlainTextField(element);
+
+ // Check the parameters
+
+ if (!isTextField && !isContentEditable(element)) {
+ // Skip HTMLOptionElement and HTMLSelectElement elements, as they don't
+ // support the operation of setSelectionRange
+ return false;
+ }
+
+ let text = isTextField ? element.value : getContentEditableText(element);
+ let length = text.length;
+ if (start < 0) {
+ start = 0;
+ }
+ if (end > length) {
+ end = length;
+ }
+ if (start > end) {
+ start = end;
+ }
+
+ if (isTextField) {
+ // Set the selection range of <input> and <textarea> elements
+ element.setSelectionRange(start, end, "forward");
+ return true;
+ } else {
+ // set the selection range of contenteditable elements
+ let win = element.ownerDocument.defaultView;
+ let sel = win.getSelection();
+
+ // Move the caret to the start position
+ sel.collapse(element, 0);
+ for (let i = 0; i < start; i++) {
+ sel.modify("move", "forward", "character");
+ }
+
+ // Avoid entering infinite loop in case we cannot change the selection
+ // range. See bug https://bugzilla.mozilla.org/show_bug.cgi?id=978918
+ let oldStart = getContentEditableSelectionStart(element, sel);
+ let counter = 0;
+ while (oldStart < start) {
+ sel.modify("move", "forward", "character");
+ let newStart = getContentEditableSelectionStart(element, sel);
+ if (oldStart == newStart) {
+ counter++;
+ if (counter > MAX_BLOCKED_COUNT) {
+ return false;
+ }
+ } else {
+ counter = 0;
+ oldStart = newStart;
+ }
+ }
+
+ // Extend the selection to the end position
+ for (let i = start; i < end; i++) {
+ sel.modify("extend", "forward", "character");
+ }
+
+ // Avoid entering infinite loop in case we cannot change the selection
+ // range. See bug https://bugzilla.mozilla.org/show_bug.cgi?id=978918
+ counter = 0;
+ let selectionLength = end - start;
+ let oldSelectionLength = getContentEditableSelectionLength(element, sel);
+ while (oldSelectionLength < selectionLength) {
+ sel.modify("extend", "forward", "character");
+ let newSelectionLength = getContentEditableSelectionLength(element, sel);
+ if (oldSelectionLength == newSelectionLength ) {
+ counter++;
+ if (counter > MAX_BLOCKED_COUNT) {
+ return false;
+ }
+ } else {
+ counter = 0;
+ oldSelectionLength = newSelectionLength;
+ }
+ }
+ return true;
+ }
+}
+
+/**
+ * Scroll the given element into view.
+ *
+ * Calls scrollSelectionIntoView for contentEditable elements.
+ */
+function scrollSelectionOrElementIntoView(element) {
+ let editor = getPlaintextEditor(element);
+ if (editor) {
+ editor.selectionController.scrollSelectionIntoView(
+ Ci.nsISelectionController.SELECTION_NORMAL,
+ Ci.nsISelectionController.SELECTION_FOCUS_REGION,
+ Ci.nsISelectionController.SCROLL_SYNCHRONOUS);
+ } else {
+ element.scrollIntoView(false);
+ }
+}
+
+// Get nsIPlaintextEditor object from an input field
+function getPlaintextEditor(element) {
+ let editor = null;
+ // Get nsIEditor
+ if (isPlainTextField(element)) {
+ // Get from the <input> and <textarea> elements
+ editor = element.QueryInterface(Ci.nsIDOMNSEditableElement).editor;
+ } else if (isContentEditable(element)) {
+ // Get from content editable element
+ let win = element.ownerDocument.defaultView;
+ let editingSession = win.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIEditingSession);
+ if (editingSession) {
+ editor = editingSession.getEditorForWindow(win);
+ }
+ }
+ if (editor) {
+ editor.QueryInterface(Ci.nsIPlaintextEditor);
+ }
+ return editor;
+}
+
+function replaceSurroundingText(element, text, offset, length) {
+ let editor = FormAssistant.editor;
+ if (!editor) {
+ return false;
+ }
+
+ // Check the parameters.
+ if (length < 0) {
+ length = 0;
+ }
+
+ // Change selection range before replacing. For content editable element,
+ // searching the node for setting selection range is not needed when the
+ // selection is collapsed within a text node.
+ let fastPathHit = false;
+ if (!isPlainTextField(element)) {
+ let sel = element.ownerDocument.defaultView.getSelection();
+ let node = sel.anchorNode;
+ if (sel.isCollapsed && node && node.nodeType == 3 /* TEXT_NODE */) {
+ let start = sel.anchorOffset + offset;
+ let end = start + length;
+ // Fallback to setSelectionRange() if the replacement span multiple nodes.
+ if (start >= 0 && end <= node.textContent.length) {
+ fastPathHit = true;
+ sel.collapse(node, start);
+ sel.extend(node, end);
+ }
+ }
+ }
+ if (!fastPathHit) {
+ let range = getSelectionRange(element);
+ let start = range[0] + offset;
+ if (start < 0) {
+ start = 0;
+ }
+ let end = start + length;
+ if (start != range[0] || end != range[1]) {
+ if (!setSelectionRange(element, start, end)) {
+ return false;
+ }
+ }
+ }
+
+ if (length) {
+ // Delete the selected text.
+ editor.deleteSelection(Ci.nsIEditor.ePrevious, Ci.nsIEditor.eStrip);
+ }
+
+ if (text) {
+ // We don't use CR but LF
+ // see https://bugzilla.mozilla.org/show_bug.cgi?id=902847
+ text = text.replace(/\r/g, '\n');
+ // Insert the text to be replaced with.
+ editor.insertText(text);
+ }
+ return true;
+}
+
+var CompositionManager = {
+ _isStarted: false,
+ _tip: null,
+ _KeyboardEventForWin: null,
+ _clauseAttrMap: {
+ 'raw-input':
+ Ci.nsITextInputProcessor.ATTR_RAW_CLAUSE,
+ 'selected-raw-text':
+ Ci.nsITextInputProcessor.ATTR_SELECTED_RAW_CLAUSE,
+ 'converted-text':
+ Ci.nsITextInputProcessor.ATTR_CONVERTED_CLAUSE,
+ 'selected-converted-text':
+ Ci.nsITextInputProcessor.ATTR_SELECTED_CLAUSE
+ },
+
+ setComposition: function cm_setComposition(element, text, cursor, clauses, dict) {
+ // Check parameters.
+ if (!element) {
+ return;
+ }
+ let len = text.length;
+ if (cursor > len) {
+ cursor = len;
+ }
+ let clauseLens = [];
+ let clauseAttrs = [];
+ if (clauses) {
+ let remainingLength = len;
+ for (let i = 0; i < clauses.length; i++) {
+ if (clauses[i]) {
+ let clauseLength = clauses[i].length || 0;
+ // Make sure the total clauses length is not bigger than that of the
+ // composition string.
+ if (clauseLength > remainingLength) {
+ clauseLength = remainingLength;
+ }
+ remainingLength -= clauseLength;
+ clauseLens.push(clauseLength);
+ clauseAttrs.push(this._clauseAttrMap[clauses[i].selectionType] ||
+ Ci.nsITextInputProcessor.ATTR_RAW_CLAUSE);
+ }
+ }
+ // If the total clauses length is less than that of the composition
+ // string, extend the last clause to the end of the composition string.
+ if (remainingLength > 0) {
+ clauseLens[clauseLens.length - 1] += remainingLength;
+ }
+ } else {
+ clauseLens.push(len);
+ clauseAttrs.push(Ci.nsITextInputProcessor.ATTR_RAW_CLAUSE);
+ }
+
+ let win = element.ownerDocument.defaultView;
+ let tip = WindowMap.getTextInputProcessor(win);
+ if (!tip) {
+ return;
+ }
+ // Update the composing text.
+ tip.setPendingCompositionString(text);
+ for (var i = 0; i < clauseLens.length; i++) {
+ if (!clauseLens[i]) {
+ continue;
+ }
+ tip.appendClauseToPendingComposition(clauseLens[i], clauseAttrs[i]);
+ }
+ if (cursor >= 0) {
+ tip.setCaretInPendingComposition(cursor);
+ }
+
+ if (!dict) {
+ this._isStarted = tip.flushPendingComposition();
+ } else {
+ let keyboardEvent = new win.KeyboardEvent("", dict);
+ let flags = dict.flags;
+ this._isStarted = tip.flushPendingComposition(keyboardEvent, flags);
+ }
+
+ if (this._isStarted) {
+ this._tip = tip;
+ this._KeyboardEventForWin = win.KeyboardEvent;
+ }
+ },
+
+ endComposition: function cm_endComposition(text, dict) {
+ if (!this._isStarted) {
+ return;
+ }
+ let tip = this._tip;
+ if (!tip) {
+ return;
+ }
+
+ text = text || "";
+ if (!dict) {
+ tip.commitCompositionWith(text);
+ } else {
+ let keyboardEvent = new this._KeyboardEventForWin("", dict);
+ let flags = dict.flags;
+ tip.commitCompositionWith(text, keyboardEvent, flags);
+ }
+
+ this._isStarted = false;
+ this._tip = null;
+ this._KeyboardEventForWin = null;
+ },
+
+ // Composition ends due to external actions.
+ onCompositionEnd: function cm_onCompositionEnd() {
+ if (!this._isStarted) {
+ return;
+ }
+
+ this._isStarted = false;
+ this._tip = null;
+ this._KeyboardEventForWin = null;
+ }
+};
diff --git a/dom/inputmethod/jar.mn b/dom/inputmethod/jar.mn
new file mode 100644
index 000000000..c7bb9a0fe
--- /dev/null
+++ b/dom/inputmethod/jar.mn
@@ -0,0 +1,6 @@
+# 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/.
+
+toolkit.jar:
+ content/global/forms.js (forms.js)
diff --git a/dom/inputmethod/mochitest/bug1110030_helper.js b/dom/inputmethod/mochitest/bug1110030_helper.js
new file mode 100644
index 000000000..54f15825b
--- /dev/null
+++ b/dom/inputmethod/mochitest/bug1110030_helper.js
@@ -0,0 +1,267 @@
+// ***********************************
+// * Global variables
+// ***********************************
+const kIsWin = navigator.platform.indexOf("Win") == 0;
+
+// Bit value for the keyboard events
+const kKeyDown = 0x01;
+const kKeyPress = 0x02;
+const kKeyUp = 0x04;
+
+// Pair the event name to its bit value
+const kEventCode = {
+ 'keydown' : kKeyDown,
+ 'keypress' : kKeyPress,
+ 'keyup' : kKeyUp
+};
+
+// Holding the current test case's infomation:
+var gCurrentTest;
+
+// The current used input method of this test
+var gInputMethod;
+
+// ***********************************
+// * Utilities
+// ***********************************
+function addKeyEventListeners(eventTarget, handler)
+{
+ Object.keys(kEventCode).forEach(function(type) {
+ eventTarget.addEventListener(type, handler);
+ });
+}
+
+function eventToCode(type)
+{
+ return kEventCode[type];
+}
+
+// To test key events that will be generated by input method here,
+// we need to convert alphabets to native key code.
+// (Our input method for testing will handle alphabets)
+// On the other hand, to test key events that will not be generated by IME,
+// we use 0-9 for such case in our testing.
+function guessNativeKeyCode(key)
+{
+ let nativeCodeName = (kIsWin)? 'WIN_VK_' : 'MAC_VK_ANSI_';
+ if (/^[A-Z]$/.test(key)) {
+ nativeCodeName += key;
+ } else if (/^[a-z]$/.test(key)) {
+ nativeCodeName += key.toUpperCase();
+ } else if (/^[0-9]$/.test(key)) {
+ nativeCodeName += key.toString();
+ } else {
+ return 0;
+ }
+
+ return eval(nativeCodeName);
+}
+
+// ***********************************
+// * Frame loader and frame scripts
+// ***********************************
+function frameScript()
+{
+ function handler(e) {
+ sendAsyncMessage("forwardevent", { type: e.type, key: e.key });
+ }
+ function notifyFinish(e) {
+ if (e.type != 'keyup') return;
+ sendAsyncMessage("finish");
+ }
+ let input = content.document.getElementById('test-input');
+ input.addEventListener('keydown', handler);
+ input.addEventListener('keypress', handler);
+ input.addEventListener('keyup', handler);
+ input.addEventListener('keyup', notifyFinish);
+}
+
+function loadTestFrame(goNext) {
+ let iframe = document.createElement('iframe');
+ iframe.src = 'file_test_empty_app.html';
+ iframe.setAttribute('mozbrowser', true);
+
+ iframe.addEventListener("mozbrowserloadend", function onloadend() {
+ iframe.removeEventListener("mozbrowserloadend", onloadend);
+ iframe.focus();
+ var mm = SpecialPowers.getBrowserFrameMessageManager(iframe);
+ mm.addMessageListener("forwardevent", function(msg) {
+ inputtextEventReceiver(msg.json);
+ });
+ mm.addMessageListener("finish", function(msg) {
+ if(goNext) {
+ goNext();
+ }
+ });
+ mm.loadFrameScript("data:,(" + frameScript.toString() + ")();", false);
+ return;
+ });
+
+ document.body.appendChild(iframe);
+}
+
+// ***********************************
+// * Event firer and listeners
+// ***********************************
+function fireEvent(callback)
+{
+ let key = gCurrentTest.key;
+ synthesizeNativeKey(KEYBOARD_LAYOUT_EN_US, guessNativeKeyCode(key), {},
+ key, key, (callback) ? callback : null);
+}
+
+function hardwareEventReceiver(evt)
+{
+ if (!gCurrentTest) {
+ return;
+ }
+ gCurrentTest.hardwareinput.receivedEvents |= eventToCode(evt.type);
+ gCurrentTest.hardwareinput.receivedKeys += evt.key;
+}
+
+function inputtextEventReceiver(evt)
+{
+ if (!gCurrentTest) {
+ return;
+ }
+ gCurrentTest.inputtext.receivedEvents |= eventToCode(evt.type);
+ gCurrentTest.inputtext.receivedKeys += evt.key;
+}
+
+// ***********************************
+// * Event verifier
+// ***********************************
+function verifyResults(test)
+{
+ // Verify results received from inputcontent.hardwareinput
+ is(test.hardwareinput.receivedEvents,
+ test.hardwareinput.expectedEvents,
+ "received events from inputcontent.hardwareinput are wrong");
+
+ is(test.hardwareinput.receivedKeys,
+ test.hardwareinput.expectedKeys,
+ "received keys from inputcontent.hardwareinput are wrong");
+
+ // Verify results received from actual input text
+ is(test.inputtext.receivedEvents,
+ test.inputtext.expectedEvents,
+ "received events from input text are wrong");
+
+ is(test.inputtext.receivedKeys,
+ test.inputtext.expectedKeys,
+ "received keys from input text are wrong");
+}
+
+function areEventsSame(test)
+{
+ return (test.hardwareinput.receivedEvents ==
+ test.hardwareinput.expectedEvents) &&
+ (test.inputtext.receivedEvents ==
+ test.inputtext.expectedEvents);
+}
+
+// ***********************************
+// * Input Method
+// ***********************************
+// The method input used in this test
+// only handles alphabets
+function InputMethod(inputContext)
+{
+ this._inputContext = inputContext;
+ this.init();
+}
+
+InputMethod.prototype = {
+ init: function im_init() {
+ this._setKepMap();
+ },
+
+ handler: function im_handler(evt) {
+ // Ignore the key if the event is defaultPrevented
+ if (evt.defaultPrevented) {
+ return;
+ }
+
+ // Finish if there is no _inputContext
+ if (!this._inputContext) {
+ return;
+ }
+
+ // Generate the keyDict for inputcontext.keydown/keyup
+ let keyDict = this._generateKeyDict(evt);
+
+ // Ignore the key if IME doesn't want to handle it
+ if (!keyDict) {
+ return;
+ }
+
+ // Call preventDefault if the key will be handled.
+ evt.preventDefault();
+
+ // Call inputcontext.keydown/keyup
+ this._inputContext[evt.type](keyDict);
+ },
+
+ mapKey: function im_keymapping(key) {
+ if (!this._mappingTable) {
+ return;
+ }
+ return this._mappingTable[key];
+ },
+
+ _setKepMap: function im_setKeyMap() {
+ // A table to map characters:
+ // {
+ // 'A': 'B'
+ // 'a': 'b'
+ // 'B': 'C'
+ // 'b': 'c'
+ // ..
+ // ..
+ // 'Z': 'A',
+ // 'z': 'a',
+ // }
+ this._mappingTable = {};
+
+ let rotation = 1;
+
+ for (let i = 0 ; i < 26 ; i++) {
+ // Convert 'A' to 'B', 'B' to 'C', ..., 'Z' to 'A'
+ this._mappingTable[String.fromCharCode(i + 'A'.charCodeAt(0))] =
+ String.fromCharCode((i+rotation)%26 + 'A'.charCodeAt(0));
+
+ // Convert 'a' to 'b', 'b' to 'c', ..., 'z' to 'a'
+ this._mappingTable[String.fromCharCode(i + 'a'.charCodeAt(0))] =
+ String.fromCharCode((i+rotation)%26 + 'a'.charCodeAt(0));
+ }
+ },
+
+ _generateKeyDict: function im_generateKeyDict(evt) {
+
+ let mappedKey = this.mapKey(evt.key);
+
+ if (!mappedKey) {
+ return;
+ }
+
+ let keyDict = {
+ key: mappedKey,
+ code: this._guessCodeFromKey(mappedKey),
+ repeat: evt.repeat,
+ };
+
+ return keyDict;
+ },
+
+ _guessCodeFromKey: function im_guessCodeFromKey(key) {
+ if (/^[A-Z]$/.test(key)) {
+ return "Key" + key;
+ } else if (/^[a-z]$/.test(key)) {
+ return "Key" + key.toUpperCase();
+ } else if (/^[0-9]$/.test(key)) {
+ return "Digit" + key.toString();
+ } else {
+ return 0;
+ }
+ },
+};
diff --git a/dom/inputmethod/mochitest/chrome.ini b/dom/inputmethod/mochitest/chrome.ini
new file mode 100644
index 000000000..7575c2215
--- /dev/null
+++ b/dom/inputmethod/mochitest/chrome.ini
@@ -0,0 +1,52 @@
+[DEFAULT]
+# dom/inputmethod only made sense on B2G
+skip-if = true
+support-files =
+ bug1110030_helper.js
+ inputmethod_common.js
+ file_inputmethod.html
+ file_blank.html
+ file_test_app.html
+ file_test_bug1066515.html
+ file_test_bug1137557.html
+ file_test_bug1175399.html
+ file_test_empty_app.html
+ file_test_focus_blur_manage_events.html
+ file_test_sendkey_cancel.html
+ file_test_setSupportsSwitching.html
+ file_test_simple_manage_events.html
+ file_test_sms_app.html
+ file_test_sms_app_1066515.html
+ file_test_sync_edit.html
+ file_test_two_inputs.html
+ file_test_two_selects.html
+ file_test_unload.html
+ file_test_unload_action.html
+
+[test_basic.html]
+[test_bug944397.html]
+[test_bug949059.html]
+[test_bug953044.html]
+[test_bug960946.html]
+[test_bug978918.html]
+[test_bug1026997.html]
+[test_bug1043828.html]
+[test_bug1059163.html]
+disabled = fails because receiving bad values
+[test_bug1066515.html]
+[test_bug1137557.html]
+[test_bug1175399.html]
+[test_focus_blur_manage_events.html]
+disabled = fails because receiving bad events # also depends on bug 1254823
+[test_forward_hardware_key_to_ime.html]
+skip-if = true # Test only ran on Mulet
+[test_input_registry_events.html]
+disabled = timeout on pine
+[test_sendkey_cancel.html]
+[test_setSupportsSwitching.html]
+[test_simple_manage_events.html]
+disabled = fails because receiving bad events
+[test_sync_edit.html]
+[test_two_inputs.html]
+[test_two_selects.html]
+[test_unload.html]
diff --git a/dom/inputmethod/mochitest/file_blank.html b/dom/inputmethod/mochitest/file_blank.html
new file mode 100644
index 000000000..7879e1ce9
--- /dev/null
+++ b/dom/inputmethod/mochitest/file_blank.html
@@ -0,0 +1,4 @@
+<html>
+<body>
+</body>
+</html>
diff --git a/dom/inputmethod/mochitest/file_inputmethod.html b/dom/inputmethod/mochitest/file_inputmethod.html
new file mode 100644
index 000000000..193cb0505
--- /dev/null
+++ b/dom/inputmethod/mochitest/file_inputmethod.html
@@ -0,0 +1,25 @@
+<html>
+<body>
+<script>
+ var im = navigator.mozInputMethod;
+ if (im) {
+ im.oninputcontextchange = onIcc;
+
+ if (im.inputcontext) {
+ onIcc();
+ }
+ }
+
+ function onIcc() {
+ var ctx = im.inputcontext;
+ if (ctx) {
+ ctx.replaceSurroundingText(location.hash).then(function() {
+ /* Happy flow */
+ }, function(err) {
+ dump('ReplaceSurroundingText failed ' + err + '\n');
+ });
+ }
+ }
+</script>
+</body>
+</html>
diff --git a/dom/inputmethod/mochitest/file_test_app.html b/dom/inputmethod/mochitest/file_test_app.html
new file mode 100644
index 000000000..3063e9749
--- /dev/null
+++ b/dom/inputmethod/mochitest/file_test_app.html
@@ -0,0 +1,11 @@
+<!DOCTYPE HTML>
+<html>
+<body>
+<input id="test-input" type="text" value="Yuan" x-inputmode="verbatim" lang="zh"/>
+<script type="application/javascript;version=1.7">
+ let input = document.getElementById('test-input');
+ input.focus();
+ dump('file_test_app.html was loaded.');
+</script>
+</body>
+</html>
diff --git a/dom/inputmethod/mochitest/file_test_bug1066515.html b/dom/inputmethod/mochitest/file_test_bug1066515.html
new file mode 100644
index 000000000..6331ec40e
--- /dev/null
+++ b/dom/inputmethod/mochitest/file_test_bug1066515.html
@@ -0,0 +1,6 @@
+<!DOCTYPE HTML>
+<html>
+<body>
+<div id="text" contenteditable>Jan Jongboom</div>
+</body>
+</html>
diff --git a/dom/inputmethod/mochitest/file_test_bug1137557.html b/dom/inputmethod/mochitest/file_test_bug1137557.html
new file mode 100644
index 000000000..dc0c8d77e
--- /dev/null
+++ b/dom/inputmethod/mochitest/file_test_bug1137557.html
@@ -0,0 +1,6 @@
+<!DOCTYPE HTML>
+<html>
+<body>
+<textarea rows=30 cols=30></textarea>
+</body>
+</html>
diff --git a/dom/inputmethod/mochitest/file_test_bug1175399.html b/dom/inputmethod/mochitest/file_test_bug1175399.html
new file mode 100644
index 000000000..3fa7da46c
--- /dev/null
+++ b/dom/inputmethod/mochitest/file_test_bug1175399.html
@@ -0,0 +1 @@
+<html><body><input value="First" readonly></body></html>
diff --git a/dom/inputmethod/mochitest/file_test_empty_app.html b/dom/inputmethod/mochitest/file_test_empty_app.html
new file mode 100644
index 000000000..c071c1a31
--- /dev/null
+++ b/dom/inputmethod/mochitest/file_test_empty_app.html
@@ -0,0 +1,10 @@
+<!DOCTYPE HTML>
+<html>
+<body>
+<input id="test-input" type="text" value=""/>
+<script type="application/javascript;version=1.7">
+ let input = document.getElementById('test-input');
+ input.focus();
+</script>
+</body>
+</html>
diff --git a/dom/inputmethod/mochitest/file_test_focus_blur_manage_events.html b/dom/inputmethod/mochitest/file_test_focus_blur_manage_events.html
new file mode 100644
index 000000000..bb8c44573
--- /dev/null
+++ b/dom/inputmethod/mochitest/file_test_focus_blur_manage_events.html
@@ -0,0 +1,22 @@
+<html><body>
+<input type="text">
+<input type="search">
+<textarea></textarea>
+<p contenteditable></p>
+<input type="number">
+<input type="tel">
+<input type="url">
+<input type="email">
+<input type="password">
+<input type="datetime">
+<input type="date" value="2015-08-03" min="1990-01-01" max="2020-01-01">
+<input type="month">
+<input type="week">
+<input type="time">
+<input type="datetime-local">
+<input type="color">
+<select><option selected>foo</option><option disabled>bar</option>
+<optgroup label="group"><option>baz</option></optgroup></select>
+<select multiple><option selected>foo</option><option disabled>bar</option>
+<optgroup label="group"><option>baz</option></optgroup></select>
+</body></html>
diff --git a/dom/inputmethod/mochitest/file_test_sendkey_cancel.html b/dom/inputmethod/mochitest/file_test_sendkey_cancel.html
new file mode 100644
index 000000000..f40ee6959
--- /dev/null
+++ b/dom/inputmethod/mochitest/file_test_sendkey_cancel.html
@@ -0,0 +1,14 @@
+<!DOCTYPE HTML>
+<html>
+<body>
+<input id="test-input" type="text" value="Yolo"/>
+<script type="application/javascript;version=1.7">
+ let input = document.getElementById('test-input');
+ input.focus();
+
+ input.addEventListener('keydown', function(e) {
+ e.preventDefault();
+ });
+</script>
+</body>
+</html>
diff --git a/dom/inputmethod/mochitest/file_test_setSupportsSwitching.html b/dom/inputmethod/mochitest/file_test_setSupportsSwitching.html
new file mode 100644
index 000000000..5b5e1733a
--- /dev/null
+++ b/dom/inputmethod/mochitest/file_test_setSupportsSwitching.html
@@ -0,0 +1,5 @@
+<html><body>
+<input type="text">
+<input type="number">
+<input type="password">
+</body></html>
diff --git a/dom/inputmethod/mochitest/file_test_simple_manage_events.html b/dom/inputmethod/mochitest/file_test_simple_manage_events.html
new file mode 100644
index 000000000..5c0c25cf4
--- /dev/null
+++ b/dom/inputmethod/mochitest/file_test_simple_manage_events.html
@@ -0,0 +1 @@
+<html><body><input type="text"></body></html>
diff --git a/dom/inputmethod/mochitest/file_test_sms_app.html b/dom/inputmethod/mochitest/file_test_sms_app.html
new file mode 100644
index 000000000..7aa3e6081
--- /dev/null
+++ b/dom/inputmethod/mochitest/file_test_sms_app.html
@@ -0,0 +1,14 @@
+<!DOCTYPE HTML>
+<html>
+<body>
+ <div id="messages-input" x-inputmode="-moz-sms" contenteditable="true"
+ autofocus="autofocus">Httvb<br></div>
+ <script type="application/javascript;version=1.7">
+ let input = document.getElementById('messages-input');
+ input.focus();
+ </script>
+</body>
+</html>
+ </div>
+</body>
+</html>
diff --git a/dom/inputmethod/mochitest/file_test_sms_app_1066515.html b/dom/inputmethod/mochitest/file_test_sms_app_1066515.html
new file mode 100644
index 000000000..a515c90b5
--- /dev/null
+++ b/dom/inputmethod/mochitest/file_test_sms_app_1066515.html
@@ -0,0 +1,14 @@
+<!DOCTYPE HTML>
+<html>
+<body>
+ <div id="messages-input" x-inputmode="-moz-sms" contenteditable="true"
+ autofocus="autofocus">fxos<br>hello <b>world</b></div>
+ <script type="application/javascript;version=1.7">
+ let input = document.getElementById('messages-input');
+ input.focus();
+ </script>
+</body>
+</html>
+ </div>
+</body>
+</html>
diff --git a/dom/inputmethod/mochitest/file_test_sync_edit.html b/dom/inputmethod/mochitest/file_test_sync_edit.html
new file mode 100644
index 000000000..c450ad5cf
--- /dev/null
+++ b/dom/inputmethod/mochitest/file_test_sync_edit.html
@@ -0,0 +1 @@
+<html><body><input value="First"></body></html>
diff --git a/dom/inputmethod/mochitest/file_test_two_inputs.html b/dom/inputmethod/mochitest/file_test_two_inputs.html
new file mode 100644
index 000000000..af7a2866d
--- /dev/null
+++ b/dom/inputmethod/mochitest/file_test_two_inputs.html
@@ -0,0 +1 @@
+<html><body><input value="First"><input value="Second"></body></html>
diff --git a/dom/inputmethod/mochitest/file_test_two_selects.html b/dom/inputmethod/mochitest/file_test_two_selects.html
new file mode 100644
index 000000000..be2204f6e
--- /dev/null
+++ b/dom/inputmethod/mochitest/file_test_two_selects.html
@@ -0,0 +1 @@
+<html><body><select><option>First</option></select><select><option>Second</option></select></html>
diff --git a/dom/inputmethod/mochitest/file_test_unload.html b/dom/inputmethod/mochitest/file_test_unload.html
new file mode 100644
index 000000000..d1a939405
--- /dev/null
+++ b/dom/inputmethod/mochitest/file_test_unload.html
@@ -0,0 +1 @@
+<html><body><form id="form"><input value="First"><input type="submit"></form></body></html>
diff --git a/dom/inputmethod/mochitest/file_test_unload_action.html b/dom/inputmethod/mochitest/file_test_unload_action.html
new file mode 100644
index 000000000..20a9bb3af
--- /dev/null
+++ b/dom/inputmethod/mochitest/file_test_unload_action.html
@@ -0,0 +1 @@
+<html><body><input value="Second"></body></html>
diff --git a/dom/inputmethod/mochitest/inputmethod_common.js b/dom/inputmethod/mochitest/inputmethod_common.js
new file mode 100644
index 000000000..ad8103c9f
--- /dev/null
+++ b/dom/inputmethod/mochitest/inputmethod_common.js
@@ -0,0 +1,24 @@
+function inputmethod_setup(callback) {
+ SimpleTest.waitForExplicitFinish();
+ SimpleTest.requestCompleteLog();
+ let appInfo = SpecialPowers.Cc['@mozilla.org/xre/app-info;1']
+ .getService(SpecialPowers.Ci.nsIXULAppInfo);
+ if (appInfo.name != 'B2G') {
+ SpecialPowers.Cu.import("resource://gre/modules/Keyboard.jsm", this);
+ }
+
+ let prefs = [
+ ['dom.mozBrowserFramesEnabled', true],
+ ['network.disable.ipc.security', true],
+ // Enable navigator.mozInputMethod.
+ ['dom.mozInputMethod.enabled', true]
+ ];
+ SpecialPowers.pushPrefEnv({set: prefs}, function() {
+ SimpleTest.waitForFocus(callback);
+ });
+}
+
+function inputmethod_cleanup() {
+ SpecialPowers.wrap(navigator.mozInputMethod).setActive(false);
+ SimpleTest.finish();
+}
diff --git a/dom/inputmethod/mochitest/test_basic.html b/dom/inputmethod/mochitest/test_basic.html
new file mode 100644
index 000000000..bf22e99dd
--- /dev/null
+++ b/dom/inputmethod/mochitest/test_basic.html
@@ -0,0 +1,212 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=932145
+-->
+<head>
+ <title>Basic test for InputMethod API.</title>
+ <script type="application/javascript;version=1.7" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript;version=1.7" src="inputmethod_common.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=932145">Mozilla Bug 932145</a>
+<p id="display"></p>
+<pre id="test">
+<script class="testbody" type="application/javascript;version=1.7">
+
+SimpleTest.requestFlakyTimeout("untriaged");
+
+// The input context.
+var gContext = null;
+
+inputmethod_setup(function() {
+ runTest();
+});
+
+function runTest() {
+ let im = navigator.mozInputMethod;
+
+ im.oninputcontextchange = function() {
+ ok(true, 'inputcontextchange event was fired.');
+ im.oninputcontextchange = null;
+
+ gContext = im.inputcontext;
+ if (!gContext) {
+ ok(false, 'Should have a non-null inputcontext.');
+ inputmethod_cleanup();
+ return;
+ }
+
+ is(gContext.type, 'input', 'The input context type should match.');
+ is(gContext.inputType, 'text', 'The inputType should match.');
+ is(gContext.inputMode, 'verbatim', 'The inputMode should match.');
+ is(gContext.lang, 'zh', 'The language should match.');
+ is(gContext.text, 'Yuan', 'Should get the text.');
+ is(gContext.textBeforeCursor + gContext.textAfterCursor, 'Yuan',
+ 'Should get the text around the cursor.');
+
+ test_setSelectionRange();
+ };
+
+ // Set current page as an input method.
+ SpecialPowers.wrap(im).setActive(true);
+
+ let iframe = document.createElement('iframe');
+ iframe.src = 'file_test_app.html';
+ iframe.setAttribute('mozbrowser', true);
+ document.body.appendChild(iframe);
+}
+
+function test_setSelectionRange() {
+ // Move cursor position to 2.
+ gContext.setSelectionRange(2, 0).then(function() {
+ is(gContext.selectionStart, 2, 'selectionStart was set successfully.');
+ is(gContext.selectionEnd, 2, 'selectionEnd was set successfully.');
+ test_sendKey();
+ }, function(e) {
+ ok(false, 'setSelectionRange failed:' + e.name);
+ console.error(e);
+ inputmethod_cleanup();
+ });
+}
+
+function test_sendKey() {
+ // Add '-' to current cursor posistion and move the cursor position to 3.
+ gContext.sendKey(0, '-'.charCodeAt(0), 0).then(function() {
+ is(gContext.text, 'Yu-an',
+ 'sendKey should changed the input field correctly.');
+ is(gContext.textBeforeCursor + gContext.textAfterCursor, 'Yu-an',
+ 'sendKey should changed the input field correctly.');
+ test_deleteSurroundingText();
+ }, function(e) {
+ ok(false, 'sendKey failed:' + e.name);
+ inputmethod_cleanup();
+ });
+}
+
+function test_deleteSurroundingText() {
+ // Remove one character before current cursor position and move the cursor
+ // position back to 2.
+ gContext.deleteSurroundingText(-1, 1).then(function() {
+ ok(true, 'deleteSurroundingText finished');
+ is(gContext.text, 'Yuan',
+ 'deleteSurroundingText should changed the input field correctly.');
+ is(gContext.textBeforeCursor + gContext.textAfterCursor, 'Yuan',
+ 'deleteSurroundingText should changed the input field correctly.');
+ test_replaceSurroundingText();
+ }, function(e) {
+ ok(false, 'deleteSurroundingText failed:' + e.name);
+ inputmethod_cleanup();
+ });
+}
+
+function test_replaceSurroundingText() {
+ // Replace 'Yuan' with 'Xulei'.
+ gContext.replaceSurroundingText('Xulei', -2, 4).then(function() {
+ ok(true, 'replaceSurroundingText finished');
+ is(gContext.text, 'Xulei',
+ 'replaceSurroundingText changed the input field correctly.');
+ is(gContext.textBeforeCursor + gContext.textAfterCursor, 'Xulei',
+ 'replaceSurroundingText changed the input field correctly.');
+ test_setComposition();
+ }, function(e) {
+ ok(false, 'replaceSurroundingText failed: ' + e.name);
+ inputmethod_cleanup();
+ });
+}
+
+function test_setComposition() {
+ gContext.setComposition('XXX').then(function() {
+ ok(true, 'setComposition finished');
+ test_endComposition();
+ }, function(e) {
+ ok(false, 'setComposition failed: ' + e.name);
+ inputmethod_cleanup();
+ });
+}
+
+function test_endComposition() {
+ gContext.endComposition('2013').then(function() {
+ is(gContext.text, 'Xulei2013',
+ 'endComposition changed the input field correctly.');
+ is(gContext.textBeforeCursor + gContext.textAfterCursor, 'Xulei2013',
+ 'endComposition changed the input field correctly.');
+ test_onSelectionChange();
+ }, function (e) {
+ ok(false, 'endComposition failed: ' + e.name);
+ inputmethod_cleanup();
+ });
+}
+
+function test_onSelectionChange() {
+ var sccTimeout = setTimeout(function() {
+ ok(false, 'selectionchange event not fired');
+ cleanup(true);
+ }, 3000);
+
+ function cleanup(failed) {
+ gContext.onselectionchange = null;
+ clearTimeout(sccTimeout);
+ if (failed) {
+ inputmethod_cleanup();
+ }
+ else {
+ test_onSurroundingTextChange();
+ }
+ }
+
+ gContext.onselectionchange = function(evt) {
+ ok(true, 'onselectionchange fired');
+ is(evt.detail.selectionStart, 10);
+ is(evt.detail.selectionEnd, 10);
+ ok(evt.detail.ownAction);
+ };
+
+ gContext.sendKey(0, 'j'.charCodeAt(0), 0).then(function() {
+ cleanup();
+ }, function(e) {
+ ok(false, 'sendKey failed: ' + e.name);
+ cleanup(true);
+ });
+}
+
+function test_onSurroundingTextChange() {
+ var sccTimeout = setTimeout(function() {
+ ok(false, 'surroundingtextchange event not fired');
+ cleanup(true);
+ }, 3000);
+
+ function cleanup(failed) {
+ gContext.onsurroundingtextchange = null;
+ clearTimeout(sccTimeout);
+ if (failed) {
+ inputmethod_cleanup();
+ }
+ else {
+ // in case we want more tests leave this
+ inputmethod_cleanup();
+ }
+ }
+
+ gContext.onsurroundingtextchange = function(evt) {
+ ok(true, 'onsurroundingtextchange fired');
+ is(evt.detail.text, 'Xulei2013jj');
+ is(evt.detail.textBeforeCursor, 'Xulei2013jj');
+ is(evt.detail.textAfterCursor, '');
+ ok(evt.detail.ownAction);
+ };
+
+ gContext.sendKey(0, 'j'.charCodeAt(0), 0).then(function() {
+ cleanup();
+ }, function(e) {
+ ok(false, 'sendKey failed: ' + e.name);
+ cleanup(true);
+ });
+}
+
+</script>
+</pre>
+</body>
+</html>
+
diff --git a/dom/inputmethod/mochitest/test_bug1026997.html b/dom/inputmethod/mochitest/test_bug1026997.html
new file mode 100644
index 000000000..3d44e6cbd
--- /dev/null
+++ b/dom/inputmethod/mochitest/test_bug1026997.html
@@ -0,0 +1,101 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1026997
+-->
+<head>
+ <title>SelectionChange on InputMethod API.</title>
+ <script type="application/javascript;version=1.7" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript;version=1.7" src="inputmethod_common.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=1026997">Mozilla Bug 1026997</a>
+<p id="display"></p>
+<pre id="test">
+<script class="testbody" type="application/javascript;version=1.7">
+
+inputmethod_setup(function() {
+ runTest();
+});
+
+// The frame script running in file_test_app.html.
+function appFrameScript() {
+ let input = content.document.getElementById('test-input');
+
+ input.focus();
+
+ function next(start, end) {
+ input.setSelectionRange(start, end);
+ }
+
+ addMessageListener("test:KeyBoard:nextSelection", function(event) {
+ let json = event.json;
+ next(json[0], json[1]);
+ });
+}
+
+function runTest() {
+ let actions = [
+ [0, 4],
+ [1, 1],
+ [3, 3],
+ [2, 3]
+ ];
+
+ let counter = 0;
+ let mm = null;
+ let ic = null;
+
+ let im = navigator.mozInputMethod;
+ im.oninputcontextchange = function() {
+ ok(true, 'inputcontextchange event was fired.');
+ im.oninputcontextchange = null;
+
+ ic = im.inputcontext;
+ if (!ic) {
+ ok(false, 'Should have a non-null inputcontext.');
+ inputmethod_cleanup();
+ return;
+ }
+
+ ic.onselectionchange = function() {
+ is(ic.selectionStart, actions[counter][0], "start");
+ is(ic.selectionEnd, actions[counter][1], "end");
+
+ if (++counter === actions.length) {
+ inputmethod_cleanup();
+ return;
+ }
+
+ next();
+ };
+
+ next();
+ };
+
+ // Set current page as an input method.
+ SpecialPowers.wrap(im).setActive(true);
+
+ // Create an app frame to recieve keyboard inputs.
+ let app = document.createElement('iframe');
+ app.src = 'file_test_app.html';
+ app.setAttribute('mozbrowser', true);
+ document.body.appendChild(app);
+ app.addEventListener('mozbrowserloadend', function() {
+ mm = SpecialPowers.getBrowserFrameMessageManager(app);
+ mm.loadFrameScript('data:,(' + appFrameScript.toString() + ')();', false);
+ next();
+ });
+
+ function next() {
+ if (ic && mm) {
+ mm.sendAsyncMessage('test:KeyBoard:nextSelection', actions[counter]);
+ }
+ }
+}
+</script>
+</pre>
+</body>
+</html>
+
diff --git a/dom/inputmethod/mochitest/test_bug1043828.html b/dom/inputmethod/mochitest/test_bug1043828.html
new file mode 100644
index 000000000..84c1dc089
--- /dev/null
+++ b/dom/inputmethod/mochitest/test_bug1043828.html
@@ -0,0 +1,183 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1043828
+-->
+<head>
+ <title>Basic test for Switching Keyboards.</title>
+ <script type="application/javascript;version=1.7" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript;version=1.7" src="inputmethod_common.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=1043828">Mozilla Bug 1043828</a>
+<p id="display"></p>
+<pre id="test">
+<script class="testbody" type="application/javascript;version=1.7">
+
+SimpleTest.requestFlakyTimeout("untriaged");
+
+inputmethod_setup(function() {
+ runTest();
+});
+
+// The KB frame script running in Keyboard B.
+function kbFrameScript() {
+ function tryGetText() {
+ var ctx = content.navigator.mozInputMethod.inputcontext;
+ if (ctx) {
+ var p = ctx.getText();
+ p.then(function(){
+ sendAsyncMessage('test:InputMethod:getText:Resolve');
+ }, function(e){
+ sendAsyncMessage('test:InputMethod:getText:Reject');
+ });
+ } else {
+ dump("Could not get inputcontext") ;
+ }
+ }
+
+ addMessageListener('test:InputMethod:getText:Do', function(){
+ tryGetText();
+ });
+}
+
+function runTest() {
+ let app, keyboardA, keyboardB;
+ let getTextPromise;
+ let mmKeyboardA, mmKeyboardB;
+
+ /**
+ * Test flow:
+ * 1. Create two keyboard iframes & a mozbrowser iframe with a text field in it & focus the text
+ * field.
+ * 2. Set keyboard frame A as active input. Wait 200ms.
+ * 3. Set keyboard frame B as active input. Wait 200ms.
+ * 4. Set keyboard frame A as inactive. Wait 200ms.
+ * 5. Allow frame b to use getText() with inputcontext to get the content from the text field
+ * iframe. Wait 200ms.
+ * [Test would succeed if the Promise returned by getText() resolves correctly.
+ * Test would fail if otherwise]
+ */
+
+ let path = location.pathname;
+ let basePath = location.protocol + '//' + location.host +
+ path.substring(0, path.lastIndexOf('/'));
+
+ const WAIT_TIME = 200;
+
+ // STEP 1: Create the frames.
+ function step1() {
+ // app
+ app = document.createElement('iframe');
+ app.src = basePath + '/file_test_app.html';
+ app.setAttribute('mozbrowser', true);
+ document.body.appendChild(app);
+
+ // keyboards
+ keyboardA = document.createElement('iframe');
+ keyboardA.setAttribute('mozbrowser', true);
+ document.body.appendChild(keyboardA);
+
+ keyboardB = document.createElement('iframe');
+ keyboardB.setAttribute('mozbrowser', true);
+ document.body.appendChild(keyboardB);
+
+ // simulate two different keyboard apps
+ let imeUrl = basePath + '/file_blank.html';
+
+ keyboardA.src = imeUrl;
+ keyboardB.src = imeUrl;
+
+ var handler = {
+ handleEvent: function(){
+ keyboardB.removeEventListener('mozbrowserloadend', this);
+
+ mmKeyboardB = SpecialPowers.getBrowserFrameMessageManager(keyboardB);
+
+ mmKeyboardB.loadFrameScript('data:,(' + kbFrameScript.toString() + ')();', false);
+
+ mmKeyboardB.addMessageListener('test:InputMethod:getText:Resolve', function() {
+ info('getText() was resolved');
+ inputmethod_cleanup();
+ });
+
+ mmKeyboardB.addMessageListener('test:InputMethod:getText:Reject', function() {
+ ok(false, 'getText() was rejected');
+ inputmethod_cleanup();
+ });
+
+ setTimeout(function(){
+ step2();
+ }, WAIT_TIME);
+ }
+ };
+
+ keyboardB.addEventListener('mozbrowserloadend', handler);
+ }
+
+ // STEP 2: Set keyboard A active
+ function step2() {
+ info('step2');
+ let req = keyboardA.setInputMethodActive(true);
+
+ req.onsuccess = function(){
+ setTimeout(function(){
+ step3();
+ }, WAIT_TIME);
+ };
+
+ req.onerror = function(){
+ ok(false, 'setInputMethodActive failed: ' + this.error.name);
+ inputmethod_cleanup();
+ };
+ }
+
+ // STEP 3: Set keyboard B active
+ function step3() {
+ info('step3');
+ let req = keyboardB.setInputMethodActive(true);
+
+ req.onsuccess = function(){
+ setTimeout(function(){
+ step4();
+ }, WAIT_TIME);
+ };
+
+ req.onerror = function(){
+ ok(false, 'setInputMethodActive failed: ' + this.error.name);
+ inputmethod_cleanup();
+ };
+ }
+
+ // STEP 4: Set keyboard A inactive
+ function step4() {
+ info('step4');
+ let req = keyboardA.setInputMethodActive(false);
+
+ req.onsuccess = function(){
+ setTimeout(function(){
+ step5();
+ }, WAIT_TIME);
+ };
+
+ req.onerror = function(){
+ ok(false, 'setInputMethodActive failed: ' + this.error.name);
+ inputmethod_cleanup();
+ };
+ }
+
+ // STEP 5: getText
+ function step5() {
+ info('step5');
+ mmKeyboardB.sendAsyncMessage('test:InputMethod:getText:Do');
+ }
+
+ step1();
+}
+
+</script>
+</pre>
+</body>
+</html>
+
diff --git a/dom/inputmethod/mochitest/test_bug1059163.html b/dom/inputmethod/mochitest/test_bug1059163.html
new file mode 100644
index 000000000..c54a03b0e
--- /dev/null
+++ b/dom/inputmethod/mochitest/test_bug1059163.html
@@ -0,0 +1,87 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1059163
+-->
+<head>
+ <title>Basic test for repeat sendKey events</title>
+ <script type="application/javascript;version=1.7" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript;version=1.7" src="inputmethod_common.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=1059163">Mozilla Bug 1059163</a>
+<p id="display"></p>
+<pre id="test">
+<script class="testbody" type="application/javascript;version=1.7">
+inputmethod_setup(function() {
+ runTest();
+});
+
+// The frame script running in the file
+function appFrameScript() {
+ let document = content.document;
+ let window = content.document.defaultView;
+
+ let t = document.getElementById('text');
+ t.focus();
+
+ let range = document.createRange();
+ range.selectNodeContents(t);
+ range.collapse(false);
+ let selection = window.getSelection();
+ selection.removeAllRanges();
+ selection.addRange(range);
+
+ addMessageListener('test:InputMethod:clear', function() {
+ t.innerHTML = '';
+ });
+}
+
+function runTest() {
+ let im = navigator.mozInputMethod;
+
+ // Set current page as an input method.
+ SpecialPowers.wrap(im).setActive(true);
+
+ // Create an app frame to recieve keyboard inputs.
+ let app = document.createElement('iframe');
+ app.src = 'file_test_bug1066515.html';
+ app.setAttribute('mozbrowser', true);
+ document.body.appendChild(app);
+
+ app.addEventListener('mozbrowserloadend', function() {
+ let mm = SpecialPowers.getBrowserFrameMessageManager(app);
+ mm.loadFrameScript('data:,(' + encodeURIComponent(appFrameScript.toString()) + ')();', false);
+
+ im.oninputcontextchange = function() {
+ is(im.inputcontext.type, 'contenteditable', 'type');
+ is(im.inputcontext.inputType, 'textarea', 'inputType');
+
+ if (im.inputcontext) {
+ im.oninputcontextchange = null;
+ register();
+ }
+ };
+
+ function register() {
+ im.inputcontext.onselectionchange = function() {
+ im.inputcontext.onselectionchange = null;
+
+ is(im.inputcontext.textBeforeCursor, '', 'textBeforeCursor');
+ is(im.inputcontext.textAfterCursor, '', 'textAfterCursor');
+ is(im.inputcontext.selectionStart, 0, 'selectionStart');
+ is(im.inputcontext.selectionEnd, 0, 'selectionEnd');
+
+ inputmethod_cleanup();
+ };
+
+ mm.sendAsyncMessage('test:InputMethod:clear');
+ }
+ });
+}
+</script>
+</pre>
+</body>
+</html>
+
diff --git a/dom/inputmethod/mochitest/test_bug1066515.html b/dom/inputmethod/mochitest/test_bug1066515.html
new file mode 100644
index 000000000..56fe10772
--- /dev/null
+++ b/dom/inputmethod/mochitest/test_bug1066515.html
@@ -0,0 +1,93 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1066515
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1066515</title>
+ <script type="application/javascript;version=1.7" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript;version=1.7" src="inputmethod_common.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=1066515">Mozilla Bug 1066515</a>
+<p id="display"></p>
+<pre id="test">
+<script class="testbody" type="application/javascript;version=1.7">
+
+// The input context.
+var gContext = null;
+
+inputmethod_setup(function() {
+ runTest();
+});
+
+function runTest() {
+ let im = navigator.mozInputMethod;
+
+ im.oninputcontextchange = function() {
+ ok(true, 'inputcontextchange event was fired.');
+ im.oninputcontextchange = null;
+
+ gContext = im.inputcontext;
+ if (!gContext) {
+ ok(false, 'Should have a non-null inputcontext.');
+ inputmethod_cleanup();
+ return;
+ }
+
+ test_replaceSurroundingTextWithinTextNode();
+ };
+
+ // Set current page as an input method.
+ SpecialPowers.wrap(im).setActive(true);
+
+ let iframe = document.createElement('iframe');
+ iframe.src = 'file_test_sms_app_1066515.html';
+ iframe.setAttribute('mozbrowser', true);
+ document.body.appendChild(iframe);
+}
+
+function test_replaceSurroundingTextWithinTextNode() {
+ // Set cursor position after 'f'.
+ gContext.setSelectionRange(1, 0);
+
+ // Replace 'fxos' to 'Hitooo' which the range is within current text node.
+ gContext.replaceSurroundingText('Hitooo', -1, 4).then(function() {
+ gContext.getText().then(function(text) {
+ is(text, 'Hitooo\nhello world', 'replaceSurroundingText successfully.');
+ test_replaceSurroundingTextSpanMultipleNodes();
+ }, function(e) {
+ ok(false, 'getText failed: ' + e.name);
+ inputmethod_cleanup();
+ });
+ }, function(e) {
+ ok(false, 'replaceSurroundingText failed: ' + e.name);
+ inputmethod_cleanup();
+ });
+}
+
+function test_replaceSurroundingTextSpanMultipleNodes() {
+ // Set cursor position to the beginning.
+ gContext.setSelectionRange(0, 0);
+
+ // Replace whole content editable element to 'abc'.
+ gContext.replaceSurroundingText('abc', 0, 100).then(function() {
+ gContext.getText().then(function(text) {
+ is(text, 'abc', 'replaceSurroundingText successfully.');
+ inputmethod_cleanup();
+ }, function(e) {
+ ok(false, 'getText failed: ' + e.name);
+ inputmethod_cleanup();
+ });
+ }, function(e) {
+ ok(false, 'replaceSurroundingText failed: ' + e.name);
+ inputmethod_cleanup();
+ });
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/inputmethod/mochitest/test_bug1137557.html b/dom/inputmethod/mochitest/test_bug1137557.html
new file mode 100644
index 000000000..1f5053662
--- /dev/null
+++ b/dom/inputmethod/mochitest/test_bug1137557.html
@@ -0,0 +1,1799 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1137557
+-->
+<head>
+ <title>Test for new API arguments accepting D3E properties</title>
+ <script type="application/javascript;version=1.7" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript;version=1.7" src="inputmethod_common.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=1137557">Mozilla Bug 1137557</a>
+<p id="display"></p>
+<pre id="test">
+<script class="testbody" type="application/javascript;version=1.7">
+
+inputmethod_setup(function() {
+ runTest();
+});
+
+let gEventDetails = [];
+let gCurrentValue = '';
+let gTestDescription = '';
+
+let appFrameScript = function appFrameScript() {
+ let input = content.document.body.firstElementChild;
+
+ input.focus();
+
+ function sendEventDetail(evt) {
+ var eventDetail;
+
+ switch (evt.type) {
+ case 'compositionstart':
+ case 'compositionupdate':
+ case 'compositionend':
+ eventDetail = {
+ type: evt.type,
+ value: input.value,
+ data: evt.data
+ };
+ break;
+
+ case 'input':
+ eventDetail = {
+ type: evt.type,
+ value: input.value
+ };
+ break;
+
+ default: // keyboard events
+ eventDetail = {
+ type: evt.type,
+ charCode: evt.charCode,
+ keyCode: evt.keyCode,
+ key: evt.key,
+ code: evt.code,
+ location: evt.location,
+ repeat: evt.repeat,
+ value: input.value,
+ shift: evt.getModifierState('Shift'),
+ capsLock: evt.getModifierState('CapsLock'),
+ control: evt.getModifierState('Control'),
+ alt: evt.getModifierState('Alt')
+ };
+ break;
+ }
+
+ sendAsyncMessage('test:eventDetail', eventDetail);
+ }
+
+ input.addEventListener('compositionstart', sendEventDetail);
+ input.addEventListener('compositionupdate', sendEventDetail);
+ input.addEventListener('compositionend', sendEventDetail);
+ input.addEventListener('input', sendEventDetail);
+ input.addEventListener('keydown', sendEventDetail);
+ input.addEventListener('keypress', sendEventDetail);
+ input.addEventListener('keyup', sendEventDetail);
+};
+
+function waitForInputContextChange() {
+ return new Promise((resolve) => {
+ navigator.mozInputMethod.oninputcontextchange = resolve;
+ });
+}
+
+function assertEventDetail(expectedDetails, testName) {
+ is(gEventDetails.length, expectedDetails.length,
+ testName + ' expects ' + expectedDetails.map(d => d.type).join(', ') + ' events, got ' + gEventDetails.map(d => d.type).join(', '));
+
+ expectedDetails.forEach((expectedDetail, j) => {
+ for (let key in expectedDetail) {
+ is(gEventDetails[j][key], expectedDetail[key],
+ testName + ' expects ' + key + ' of ' + gEventDetails[j].type + ' to be equal to ' + expectedDetail[key]);
+ }
+ });
+}
+
+function sendKeyAndAssertResult(testdata) {
+ var dict = testdata.dict;
+ var testName = gTestDescription + 'sendKey(' + JSON.stringify(dict) + ')';
+ var promise = navigator.mozInputMethod.inputcontext.sendKey(dict);
+
+ if (testdata.expectedReject) {
+ promise = promise
+ .then(() => {
+ ok(false, testName + ' should not resolve.');
+ }, (e) => {
+ ok(true, testName + ' rejects.');
+ ok(e instanceof testdata.expectedReject, 'Reject with type.');
+ })
+
+ return promise;
+ }
+
+ promise = promise
+ .then((res) => {
+ is(res, true,
+ testName + ' should resolve to true.');
+
+ var expectedEventDetail = [];
+
+ var expectedValues = testdata.expectedValues;
+
+ expectedEventDetail.push({
+ type: 'keydown',
+ key: expectedValues.key,
+ charCode: 0,
+ code: expectedValues.code || '',
+ keyCode: expectedValues.keyCode || 0,
+ location: expectedValues.location ? expectedValues.location : 0,
+ repeat: expectedValues.repeat || false,
+ value: gCurrentValue,
+ shift: false,
+ capsLock: false,
+ control: false,
+ alt: false
+ });
+
+ if (testdata.expectedKeypress) {
+ expectedEventDetail.push({
+ type: 'keypress',
+ key: expectedValues.key,
+ charCode: expectedValues.charCode,
+ code: expectedValues.code || '',
+ keyCode: expectedValues.charCode ? 0 : expectedValues.keyCode,
+ location: expectedValues.location ? expectedValues.location : 0,
+ repeat: expectedValues.repeat || false,
+ value: gCurrentValue,
+ shift: false,
+ capsLock: false,
+ control: false,
+ alt: false
+ });
+ }
+
+ if (testdata.expectedInput) {
+ switch (testdata.expectedInput) {
+ case 'Enter':
+ gCurrentValue += '\n';
+ break;
+ case 'Backspace':
+ gCurrentValue =
+ gCurrentValue.substr(0, gCurrentValue.length - 1);
+ break;
+ default:
+ gCurrentValue += testdata.expectedInput;
+ break;
+ }
+
+ expectedEventDetail.push({
+ type: 'input',
+ value: gCurrentValue
+ });
+ }
+
+ if (!testdata.expectedRepeat) {
+ expectedEventDetail.push({
+ type: 'keyup',
+ key: expectedValues.key,
+ charCode: 0,
+ code: expectedValues.code || '',
+ keyCode: expectedValues.keyCode || 0,
+ location: expectedValues.location ? expectedValues.location : 0,
+ repeat: expectedValues.repeat || false,
+ value: gCurrentValue,
+ shift: false,
+ capsLock: false,
+ control: false,
+ alt: false
+ });
+ }
+
+ assertEventDetail(expectedEventDetail, testName);
+ gEventDetails = [];
+ }, (e) => {
+ ok(false, testName + ' should not reject. ' + e);
+ });
+
+ return promise;
+}
+
+function runSendKeyAlphabetTests() {
+ gTestDescription = 'runSendKeyAlphabetTests(): ';
+ var promiseQueue = Promise.resolve();
+
+ // Test the plain alphabets
+ var codeA = 'A'.charCodeAt(0);
+ for (var i = 0; i < 26; i++) {
+ // callbacks in then() are deferred; must only reference these block-scoped
+ // variable instead of i.
+ let keyCode = codeA + i;
+ let code = 'Key' + String.fromCharCode(keyCode);
+
+ [String.fromCharCode(keyCode),
+ String.fromCharCode(keyCode).toLowerCase()]
+ .forEach((chr) => {
+ // Test plain alphabet
+ promiseQueue = promiseQueue.then(() => {
+ return sendKeyAndAssertResult({
+ dict: {
+ key: chr
+ },
+ expectedKeypress: true,
+ expectedInput: chr,
+ expectedValues: {
+ key: chr, code: '',
+ keyCode: keyCode,
+ charCode: chr.charCodeAt(0)
+ }
+ });
+ });
+
+ // Test plain alphabet with keyCode set
+ promiseQueue = promiseQueue.then(() => {
+ return sendKeyAndAssertResult({
+ dict: {
+ key: chr,
+ keyCode: keyCode
+ },
+ expectedKeypress: true,
+ expectedInput: chr,
+ expectedValues: {
+ key: chr, code: '',
+ keyCode: keyCode,
+ charCode: chr.charCodeAt(0)
+ }
+ });
+ });
+
+ // Test plain alphabet with keyCode set to keyCode + 1,
+ // expects keyCode to follow key value and ignore the incorrect value.
+ promiseQueue = promiseQueue.then(() => {
+ return sendKeyAndAssertResult({
+ dict: {
+ key: chr,
+ keyCode: keyCode + 1
+ },
+ expectedKeypress: true,
+ expectedInput: chr,
+ expectedValues: {
+ key: chr, code: '',
+ keyCode: keyCode,
+ charCode: chr.charCodeAt(0)
+ }
+ });
+ });
+
+ // Test plain alphabet with code set
+ promiseQueue = promiseQueue.then(() => {
+ return sendKeyAndAssertResult({
+ dict: {
+ key: chr,
+ code: code
+ },
+ expectedKeypress: true,
+ expectedInput: chr,
+ expectedValues: {
+ key: chr, code: code,
+ keyCode: keyCode,
+ charCode: chr.charCodeAt(0)
+ }
+ });
+ });
+
+ // Test plain alphabet with code set to Digit1,
+ // expects keyCode to follow key value.
+ promiseQueue = promiseQueue.then(() => {
+ return sendKeyAndAssertResult({
+ dict: {
+ key: chr,
+ code: 'Digit1'
+ },
+ expectedKeypress: true,
+ expectedInput: chr,
+ expectedValues: {
+ key: chr, code: 'Digit1',
+ keyCode: keyCode,
+ charCode: chr.charCodeAt(0)
+ }
+ });
+ });
+
+ // Test plain alphabet with keyCode set to DOM_VK_1,
+ // expects keyCode to follow key value.
+ promiseQueue = promiseQueue.then(() => {
+ return sendKeyAndAssertResult({
+ dict: {
+ key: chr,
+ keyCode: KeyboardEvent.DOM_VK_1
+ },
+ expectedKeypress: true,
+ expectedInput: chr,
+ expectedValues: {
+ key: chr, code: '',
+ keyCode: keyCode,
+ charCode: chr.charCodeAt(0)
+ }
+ });
+ });
+
+ // Test plain alphabet with code set to Digit1
+ // and keyCode set to DOM_VK_1,
+ // expects keyCode to follow key value.
+ promiseQueue = promiseQueue.then(() => {
+ return sendKeyAndAssertResult({
+ dict: {
+ key: chr,
+ code: 'Digit1',
+ keyCode: KeyboardEvent.DOM_VK_1
+ },
+ expectedKeypress: true,
+ expectedInput: chr,
+ expectedValues: {
+ key: chr, code: 'Digit1',
+ keyCode: keyCode,
+ charCode: chr.charCodeAt(0)
+ }
+ });
+ });
+ });
+ }
+
+ return promiseQueue;
+}
+
+function runSendKeyNumberTests() {
+ gTestDescription = 'runSendKeyNumberTests(): ';
+ var promiseQueue = Promise.resolve();
+
+ // Test numbers
+ var code0 = '0'.charCodeAt(0);
+ for (var i = 0; i < 10; i++) {
+ // callbacks in then() are deferred; must only reference these block-scoped
+ // variable instead of i.
+ let keyCode = code0 + i;
+ let chr = String.fromCharCode(keyCode);
+
+ // Test plain number
+ promiseQueue = promiseQueue.then(() => {
+ return sendKeyAndAssertResult({
+ dict: {
+ key: chr
+ },
+ expectedKeypress: true,
+ expectedInput: chr,
+ expectedValues: {
+ key: chr, code: '',
+ keyCode: keyCode,
+ charCode: chr.charCodeAt(0)
+ }
+ });
+ });
+
+ // Test plain number with keyCode set
+ promiseQueue = promiseQueue.then(() => {
+ return sendKeyAndAssertResult({
+ dict: {
+ key: chr,
+ keyCode: keyCode
+ },
+ expectedKeypress: true,
+ expectedInput: chr,
+ expectedValues: {
+ key: chr, code: '',
+ keyCode: keyCode,
+ charCode: chr.charCodeAt(0)
+ }
+ });
+ });
+
+ // Test plain number with keyCode set to keyCode + 1,
+ // expects keyCode to follow key value and ignore the incorrect value.
+ promiseQueue = promiseQueue.then(() => {
+ return sendKeyAndAssertResult({
+ dict: {
+ key: chr,
+ keyCode: keyCode + 1
+ },
+ expectedKeypress: true,
+ expectedInput: chr,
+ expectedValues: {
+ key: chr, code: '',
+ keyCode: keyCode,
+ charCode: chr.charCodeAt(0)
+ }
+ });
+ });
+
+ // Test plain number with code set
+ promiseQueue = promiseQueue.then(() => {
+ return sendKeyAndAssertResult({
+ dict: {
+ key: chr,
+ code: 'Digit' + chr
+ },
+ expectedKeypress: true,
+ expectedInput: chr,
+ expectedValues: {
+ key: chr, code: 'Digit' + chr,
+ keyCode: keyCode,
+ charCode: chr.charCodeAt(0)
+ }
+ });
+ });
+
+ // Test plain upper caps alphabet with code set to KeyA,
+ // expects keyCode to follow key value.
+ promiseQueue = promiseQueue.then(() => {
+ return sendKeyAndAssertResult({
+ dict: {
+ key: chr,
+ code: 'KeyA'
+ },
+ expectedKeypress: true,
+ expectedInput: chr,
+ expectedValues: {
+ key: chr, code: 'KeyA',
+ keyCode: keyCode,
+ charCode: chr.charCodeAt(0)
+ }
+ });
+ });
+
+ // Test plain upper caps alphabet with code set to KeyA,
+ // and keyCode set to DOM_VK_A.
+ // expects keyCode to follow key value.
+ promiseQueue = promiseQueue.then(() => {
+ return sendKeyAndAssertResult({
+ dict: {
+ key: chr,
+ code: 'KeyA',
+ keyCode: KeyboardEvent.DOM_VK_A
+ },
+ expectedKeypress: true,
+ expectedInput: chr,
+ expectedValues: {
+ key: chr, code: 'KeyA',
+ keyCode: keyCode,
+ charCode: chr.charCodeAt(0)
+ }
+ });
+ });
+ }
+
+ return promiseQueue;
+}
+
+function runSendKeyDvorakTests() {
+ gTestDescription = 'runSendKeyDvorakTests(): ';
+ var promiseQueue = Promise.resolve();
+
+ // Test Dvorak layout emulation
+ var qwertyCodeForDvorakKeys = [
+ 'KeyR', 'KeyT', 'KeyY', 'KeyU', 'KeyI', 'KeyO', 'KeyP',
+ 'KeyA', 'KeyS', 'KeyD', 'KeyF', 'KeyG',
+ 'KeyH', 'KeyJ', 'KeyK', 'KeyL', 'Semicolon',
+ 'KeyX', 'KeyC', 'KeyV', 'KeyB', 'KeyN',
+ 'KeyM', 'Comma', 'Period', 'Slash'];
+ var dvorakKeys = 'PYFGCRL' +
+ 'AOEUIDHTNS' +
+ 'QJKXBMWVZ';
+ for (var i = 0; i < dvorakKeys.length; i++) {
+ // callbacks in then() are deferred; must only reference these block-scoped
+ // variable instead of i.
+ let keyCode = dvorakKeys.charCodeAt(i);
+ let code = qwertyCodeForDvorakKeys[i];
+
+ [dvorakKeys.charAt(i), dvorakKeys.charAt(i).toLowerCase()]
+ .forEach((chr) => {
+ // Test alphabet with code set to Qwerty code,
+ // expects keyCode to follow key value.
+ // (This is *NOT* the expected scenario for emulating a Dvorak keyboard,
+ // even though expected results are the same.)
+ promiseQueue = promiseQueue.then(() => {
+ return sendKeyAndAssertResult({
+ dict: {
+ key: chr,
+ code: code
+ },
+ expectedKeypress: true,
+ expectedInput: chr,
+ expectedValues: {
+ key: chr, code: code,
+ keyCode: keyCode,
+ charCode: chr.charCodeAt(0)
+ }
+ });
+ });
+
+ // Test alphabet with code set to Qwerty code and keyCode set,
+ // expects keyCode to follow key/keyCode value.
+ // (This is the expected scenario for emulating a Dvorak keyboard)
+ promiseQueue = promiseQueue.then(() => {
+ return sendKeyAndAssertResult({
+ dict: {
+ key: chr,
+ keyCode: keyCode,
+ code: code
+ },
+ expectedKeypress: true,
+ expectedInput: chr,
+ expectedValues: {
+ key: chr, code: code,
+ keyCode: keyCode,
+ charCode: chr.charCodeAt(0)
+ }
+ });
+ });
+ });
+ }
+
+ var qwertyCodeForDvorakSymbols = [
+ 'Minus', 'Equal',
+ 'KeyQ', 'KeyW', 'KeyE', 'BracketLeft', 'BracketRight', 'Backslash',
+ 'Quote', 'KeyZ'];
+
+ var shiftDvorakSymbols = '{}\"<>?+|_:';
+ var dvorakSymbols = '[]\',./=\\-;';
+ var dvorakSymbolsKeyCodes = [
+ KeyboardEvent.DOM_VK_OPEN_BRACKET,
+ KeyboardEvent.DOM_VK_CLOSE_BRACKET,
+ KeyboardEvent.DOM_VK_QUOTE,
+ KeyboardEvent.DOM_VK_COMMA,
+ KeyboardEvent.DOM_VK_PERIOD,
+ KeyboardEvent.DOM_VK_SLASH,
+ KeyboardEvent.DOM_VK_EQUALS,
+ KeyboardEvent.DOM_VK_BACK_SLASH,
+ KeyboardEvent.DOM_VK_HYPHEN_MINUS,
+ KeyboardEvent.DOM_VK_SEMICOLON
+ ];
+
+ for (var i = 0; i < dvorakSymbols.length; i++) {
+ // callbacks in then() are deferred; must only reference these block-scoped
+ // variable instead of i.
+ let keyCode = dvorakSymbolsKeyCodes[i];
+ let code = qwertyCodeForDvorakSymbols[i];
+
+ [dvorakSymbols.charAt(i), shiftDvorakSymbols.charAt(i)]
+ .forEach((chr) => {
+ // Test symbols with code set to Qwerty code,
+ // expects keyCode to be 0.
+ // (This is *NOT* the expected scenario for emulating a Dvorak keyboard)
+ promiseQueue = promiseQueue.then(() => {
+ return sendKeyAndAssertResult({
+ dict: {
+ key: chr,
+ code: code
+ },
+ expectedKeypress: true,
+ expectedInput: chr,
+ expectedValues: {
+ key: chr, code: code,
+ keyCode: 0,
+ charCode: chr.charCodeAt(0)
+ }
+ });
+ });
+
+ // Test alphabet with code set to Qwerty code and keyCode set,
+ // expects keyCode to follow keyCode value.
+ // (This is the expected scenario for emulating a Dvorak keyboard)
+ promiseQueue = promiseQueue.then(() => {
+ return sendKeyAndAssertResult({
+ dict: {
+ key: chr,
+ keyCode: keyCode,
+ code: code
+ },
+ expectedKeypress: true,
+ expectedInput: chr,
+ expectedValues: {
+ key: chr, code: code,
+ keyCode: keyCode,
+ charCode: chr.charCodeAt(0)
+ }
+ });
+ });
+ });
+ }
+
+ return promiseQueue;
+}
+
+function runSendKeyDigitKeySymbolsTests() {
+ gTestDescription = 'runSendKeyDigitKeySymbolsTests(): ';
+ var promiseQueue = Promise.resolve();
+
+ var digitKeySymbols = ')!@#$%^&*(';
+ for (var i = 0; i < digitKeySymbols.length; i++) {
+ // callbacks in then() are deferred; must only reference these block-scoped
+ // variable instead of i.
+ let keyCode = KeyboardEvent['DOM_VK_' + i];
+ let chr = digitKeySymbols.charAt(i);
+ let code = 'Digit' + i;
+
+ // Test plain symbol
+ promiseQueue = promiseQueue.then(() => {
+ return sendKeyAndAssertResult({
+ dict: {
+ key: chr
+ },
+ expectedKeypress: true,
+ expectedInput: chr,
+ expectedValues: {
+ key: chr, code: '', keyCode: 0,
+ charCode: chr.charCodeAt(0)
+ }
+ });
+ });
+
+ // Test plain symbol with keyCode set
+ promiseQueue = promiseQueue.then(() => {
+ return sendKeyAndAssertResult({
+ dict: {
+ key: chr,
+ keyCode: keyCode
+ },
+ expectedKeypress: true,
+ expectedInput: chr,
+ expectedValues: {
+ key: chr, code: '',
+ keyCode: keyCode,
+ charCode: chr.charCodeAt(0)
+ }
+ });
+ });
+
+ // Test plain symbol with code set
+ promiseQueue = promiseQueue.then(() => {
+ return sendKeyAndAssertResult({
+ dict: {
+ key: chr,
+ code: code
+ },
+ expectedKeypress: true,
+ expectedInput: chr,
+ expectedValues: {
+ key: chr, code: code,
+ keyCode: 0,
+ charCode: chr.charCodeAt(0)
+ }
+ });
+ });
+
+ // Test plain symbol with code set to KeyA,
+ // expects keyCode to be 0.
+ promiseQueue = promiseQueue.then(() => {
+ return sendKeyAndAssertResult({
+ dict: {
+ key: chr,
+ code: 'KeyA'
+ },
+ expectedKeypress: true,
+ expectedInput: chr,
+ expectedValues: {
+ key: chr, code: 'KeyA',
+ keyCode: 0,
+ charCode: chr.charCodeAt(0)
+ }
+ });
+ });
+
+ // Test plain symbol with keyCode set to DOM_VK_A,
+ // expects keyCode to follow the keyCode set.
+ promiseQueue = promiseQueue.then(() => {
+ return sendKeyAndAssertResult({
+ dict: {
+ key: chr,
+ keyCode: KeyboardEvent.DOM_VK_A
+ },
+ expectedKeypress: true,
+ expectedInput: chr,
+ expectedValues: {
+ key: chr, code: '',
+ keyCode: KeyboardEvent.DOM_VK_A,
+ charCode: chr.charCodeAt(0)
+ }
+ });
+ });
+
+ // Test plain symbol with code set to KeyA
+ // expects keyCode to follow the keyCode set.
+ promiseQueue = promiseQueue.then(() => {
+ return sendKeyAndAssertResult({
+ dict: {
+ key: chr,
+ code: 'KeyA',
+ keyCode: KeyboardEvent.DOM_VK_A
+ },
+ expectedKeypress: true,
+ expectedInput: chr,
+ expectedValues: {
+ key: chr, code: 'KeyA',
+ keyCode: KeyboardEvent.DOM_VK_A,
+ charCode: chr.charCodeAt(0)
+ }
+ });
+ });
+ }
+
+ return promiseQueue;
+}
+
+function runSendKeyUSKeyboardSymbolsTests() {
+ gTestDescription = 'runSendKeyUSKeyboardSymbolsTests(): ';
+ var promiseQueue = Promise.resolve();
+
+ // Test printable symbols on US Keyboard
+ var symbols = ' ;:=+,<-_.>/?`~[{\\|]}\'\"';
+ var symbolKeyCodes = [
+ KeyboardEvent.DOM_VK_SPACE,
+ KeyboardEvent.DOM_VK_SEMICOLON,
+ KeyboardEvent.DOM_VK_SEMICOLON,
+ KeyboardEvent.DOM_VK_EQUALS,
+ KeyboardEvent.DOM_VK_EQUALS,
+ KeyboardEvent.DOM_VK_COMMA,
+ KeyboardEvent.DOM_VK_COMMA,
+ KeyboardEvent.DOM_VK_HYPHEN_MINUS,
+ KeyboardEvent.DOM_VK_HYPHEN_MINUS,
+ KeyboardEvent.DOM_VK_PERIOD,
+ KeyboardEvent.DOM_VK_PERIOD,
+ KeyboardEvent.DOM_VK_SLASH,
+ KeyboardEvent.DOM_VK_SLASH,
+ KeyboardEvent.DOM_VK_BACK_QUOTE,
+ KeyboardEvent.DOM_VK_BACK_QUOTE,
+ KeyboardEvent.DOM_VK_OPEN_BRACKET,
+ KeyboardEvent.DOM_VK_OPEN_BRACKET,
+ KeyboardEvent.DOM_VK_BACK_SLASH,
+ KeyboardEvent.DOM_VK_BACK_SLASH,
+ KeyboardEvent.DOM_VK_CLOSE_BRACKET,
+ KeyboardEvent.DOM_VK_CLOSE_BRACKET,
+ KeyboardEvent.DOM_VK_QUOTE,
+ KeyboardEvent.DOM_VK_QUOTE
+ ];
+ var symbolCodes = [
+ 'Space',
+ 'Semicolon',
+ 'Semicolon',
+ 'Equal',
+ 'Equal',
+ 'Comma',
+ 'Comma',
+ 'Minus',
+ 'Minus',
+ 'Period',
+ 'Period',
+ 'Slash',
+ 'Slash',
+ 'Backquote',
+ 'Backquote',
+ 'BracketLeft',
+ 'BracketLeft',
+ 'Backslash',
+ 'Backslash',
+ 'BracketRight',
+ 'BracketRight',
+ 'Quote',
+ 'Quote'
+ ];
+ for (var i = 0; i < symbols.length; i++) {
+ // callbacks in then() are deferred; must only reference these block-scoped
+ // variable instead of i.
+ let keyCode = symbolKeyCodes[i];
+ let chr = symbols.charAt(i);
+ let code = symbolCodes[i];
+
+ // Test plain symbol
+ promiseQueue = promiseQueue.then(() => {
+ return sendKeyAndAssertResult({
+ dict: {
+ key: chr
+ },
+ expectedKeypress: true,
+ expectedInput: chr,
+ expectedValues: {
+ key: chr, code: '',
+ keyCode: 0,
+ charCode: chr.charCodeAt(0)
+ }
+ });
+ });
+
+ // Test plain symbol with keyCode set
+ promiseQueue = promiseQueue.then(() => {
+ return sendKeyAndAssertResult({
+ dict: {
+ key: chr,
+ keyCode: keyCode
+ },
+ expectedKeypress: true,
+ expectedInput: chr,
+ expectedValues: {
+ key: chr, code: '',
+ keyCode: keyCode,
+ charCode: chr.charCodeAt(0)
+ }
+ });
+ });
+
+ // Test plain symbol with code set
+ // expects keyCode to be 0.
+ promiseQueue = promiseQueue.then(() => {
+ return sendKeyAndAssertResult({
+ dict: {
+ key: chr,
+ code: code
+ },
+ expectedKeypress: true,
+ expectedInput: chr,
+ expectedValues: {
+ key: chr, code: code,
+ keyCode: 0,
+ charCode: chr.charCodeAt(0)
+ }
+ });
+ });
+
+ // Test plain symbol with code set to KeyA,
+ // expects keyCode to be 0.
+ promiseQueue = promiseQueue.then(() => {
+ return sendKeyAndAssertResult({
+ dict: {
+ key: chr,
+ code: 'KeyA'
+ },
+ expectedKeypress: true,
+ expectedInput: chr,
+ expectedValues: {
+ key: chr, code: 'KeyA',
+ keyCode: 0,
+ charCode: chr.charCodeAt(0)
+ }
+ });
+ });
+
+ // Test plain symbol with keyCode set to DOM_VK_A,
+ // expects keyCode to follow the keyCode set.
+ promiseQueue = promiseQueue.then(() => {
+ return sendKeyAndAssertResult({
+ dict: {
+ key: chr,
+ keyCode: KeyboardEvent.DOM_VK_A
+ },
+ expectedKeypress: true,
+ expectedInput: chr,
+ expectedValues: {
+ key: chr, code: '',
+ keyCode: KeyboardEvent.DOM_VK_A,
+ charCode: chr.charCodeAt(0)
+ }
+ });
+ });
+
+ // Test plain symbol with code set to KeyA
+ // expects keyCode to follow the keyCode set.
+ promiseQueue = promiseQueue.then(() => {
+ return sendKeyAndAssertResult({
+ dict: {
+ key: chr,
+ code: 'KeyA',
+ keyCode: KeyboardEvent.DOM_VK_A
+ },
+ expectedKeypress: true,
+ expectedInput: chr,
+ expectedValues: {
+ key: chr, code: 'KeyA',
+ keyCode: KeyboardEvent.DOM_VK_A,
+ charCode: chr.charCodeAt(0)
+ }
+ });
+ });
+ }
+
+ return promiseQueue;
+}
+
+function runSendKeyGreekLettersTests() {
+ gTestDescription = 'runSendKeyGreekLettersTests(): ';
+ var promiseQueue = Promise.resolve();
+
+ // Test Greek letters
+ var greekLetters =
+ '\u0391\u0392\u0393\u0394\u0395\u0396\u0397\u0398\u0399\u039a\u039b\u039c' +
+ '\u039d\u039e\u039f\u03a0\u03a1\u03a3\u03a4\u03a5\u03a6\u03a7\u03a8\u03a9' +
+ '\u03b1\u03b2\u03b3\u03b4\u03b5\u03b6\u03b7\u03b8\u03b9\u03ba\u03bb\u03bc' +
+ '\u03bd\u03be\u03bf\u03c0\u03c1\u03c3\u03c4\u03c5\u03c6\u03c7\u03c8\u03c9' +
+ '\u03c2';
+ var greekLettersLayoutMap =
+ 'ABGDEZHUIKLMNJOPRSTYFXCVABGDEZHUIKLMNJOPRSTYFXCVQ';
+ for (var i = 0; i < greekLetters.length; i++) {
+ // callbacks in then() are deferred; must only reference these block-scoped
+ // variable instead of i.
+ let keyCode = greekLettersLayoutMap.charCodeAt(i);
+ let chr = greekLetters.charAt(i);
+ let code = 'Key' + greekLettersLayoutMap.charAt(i);
+
+ // Test plain alphabet
+ promiseQueue = promiseQueue.then(() => {
+ return sendKeyAndAssertResult({
+ dict: {
+ key: chr
+ },
+ expectedKeypress: true,
+ expectedInput: chr,
+ expectedValues: {
+ key: chr, code: '',
+ keyCode: 0,
+ charCode: chr.charCodeAt(0)
+ }
+ });
+ });
+
+ // Test plain alphabet with keyCode set
+ promiseQueue = promiseQueue.then(() => {
+ return sendKeyAndAssertResult({
+ dict: {
+ key: chr,
+ keyCode: keyCode
+ },
+ expectedKeypress: true,
+ expectedInput: chr,
+ expectedValues: {
+ key: chr, code: '',
+ keyCode: keyCode,
+ charCode: chr.charCodeAt(0)
+ }
+ });
+ });
+
+ // Test plain alphabet with code set,
+ // expects keyCode to be 0.
+ promiseQueue = promiseQueue.then(() => {
+ return sendKeyAndAssertResult({
+ dict: {
+ key: chr,
+ code: code
+ },
+ expectedKeypress: true,
+ expectedInput: chr,
+ expectedValues: {
+ key: chr, code: code,
+ keyCode: 0,
+ charCode: chr.charCodeAt(0)
+ }
+ });
+ });
+
+ // Test plain alphabet with code set to Digit1,
+ // expects keyCode to be 0.
+ promiseQueue = promiseQueue.then(() => {
+ return sendKeyAndAssertResult({
+ dict: {
+ key: chr,
+ code: 'Digit1'
+ },
+ expectedKeypress: true,
+ expectedInput: chr,
+ expectedValues: {
+ key: chr, code: 'Digit1',
+ keyCode: 0,
+ charCode: chr.charCodeAt(0)
+ }
+ });
+ });
+
+ // Test plain alphabet with code set to Digit1,
+ // and keyCode set to DOM_VK_A.
+ // expects keyCode to follow the keyCode set.
+ promiseQueue = promiseQueue.then(() => {
+ return sendKeyAndAssertResult({
+ dict: {
+ key: chr,
+ code: 'Digit1',
+ keyCode: KeyboardEvent.DOM_VK_A
+ },
+ expectedKeypress: true,
+ expectedInput: chr,
+ expectedValues: {
+ key: chr, code: 'Digit1',
+ keyCode: KeyboardEvent.DOM_VK_A,
+ charCode: chr.charCodeAt(0)
+ }
+ });
+ });
+ }
+
+ return promiseQueue;
+}
+
+function runSendKeyEnterTests() {
+ gTestDescription = 'runSendKeyEnterTests(): ';
+ var promiseQueue = Promise.resolve();
+
+ // Test Enter with code unset
+ promiseQueue = promiseQueue.then(() => {
+ return sendKeyAndAssertResult({
+ dict: {
+ key: 'Enter'
+ },
+ expectedKeypress: true,
+ expectedInput: '\n',
+ expectedValues: {
+ key: 'Enter', code: '',
+ keyCode: KeyboardEvent.DOM_VK_RETURN,
+ charCode: 0
+ }
+ });
+ });
+
+ // Test Enter with code set
+ promiseQueue = promiseQueue.then(() => {
+ return sendKeyAndAssertResult({
+ dict: {
+ key: 'Enter',
+ code: 'Enter'
+ },
+ expectedKeypress: true,
+ expectedInput: '\n',
+ expectedValues: {
+ key: 'Enter', code: 'Enter',
+ keyCode: KeyboardEvent.DOM_VK_RETURN,
+ charCode: 0
+ }
+ });
+ });
+
+ // Test Enter with keyCode explict set to zero
+ promiseQueue = promiseQueue.then(() => {
+ return sendKeyAndAssertResult({
+ dict: {
+ key: 'Enter',
+ keyCode: 0
+ },
+ expectedKeypress: true,
+ expectedValues: {
+ key: 'Enter', code: '',
+ keyCode: 0,
+ charCode: 0
+ }
+ });
+ });
+
+ return promiseQueue;
+}
+
+function runSendKeyNumpadTests() {
+ gTestDescription = 'runSendKeyNumpadTests(): ';
+ var promiseQueue = Promise.resolve();
+
+ var tests = [];
+ ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']
+ .forEach(function(key) {
+ let charCode = key.charCodeAt(0);
+
+ tests.push({
+ dict: {
+ key: key,
+ code: 'Numpad' + key
+ },
+ expectedKeypress: true,
+ expectedInput: key,
+ expectedValues: {
+ key: key, code: 'Numpad' + key,
+ keyCode: charCode, charCode: charCode,
+ location: KeyboardEvent.DOM_KEY_LOCATION_NUMPAD
+ }
+ });
+ });
+
+ [['+', 'NumpadAdd'],
+ [',', 'NumpadComma'],
+ ['.', 'NumpadDecimal'],
+ ['.', 'NumpadComma'], // Locale-specific NumpadComma
+ [',', 'NumpadDecimal'], // Locale-specific NumpadDecimal
+ ['/', 'NumpadDivide'],
+ ['=', 'NumpadEqual'],
+ // ['#', 'NumpadHash'], // Not supported yet.
+ ['*', 'NumpadMultiply'],
+ ['(', 'NumpadParenLeft'],
+ [')', 'NumpadParenRight'],
+ // ['*', 'NumpadStar'], // Not supported yet.
+ ['-', 'NumpadSubtract']].forEach(function([key, code]) {
+ tests.push({
+ dict: {
+ key: key,
+ code: code
+ },
+ expectedKeypress: true,
+ expectedInput: key,
+ expectedValues: {
+ key: key, code: code, keyCode: 0, charCode: key.charCodeAt(0),
+ location: KeyboardEvent.DOM_KEY_LOCATION_NUMPAD
+ }
+ });
+ });
+
+ [
+ 'NumpadComma', // Locale-specific NumpadComma -- outputs nothing
+ 'NumpadClear',
+ 'NumpadClearEntry',
+ 'NumpadMemoryAdd',
+ 'NumpadMemoryClear',
+ 'NumpadMemoryRecall',
+ 'NumpadMemoryStore',
+ 'NumpadMemorySubtract'
+ ].forEach(function(code) {
+ tests.push({
+ dict: {
+ key: 'Unidentified',
+ code: code
+ },
+ expectedKeypress: true,
+ expectedValues: {
+ key: 'Unidentified', code: code, keyCode: 0, charCode: 0,
+ location: KeyboardEvent.DOM_KEY_LOCATION_NUMPAD
+ }
+ });
+ });
+
+ tests.push({
+ dict: {
+ key: 'Enter',
+ code: 'NumpadEnter'
+ },
+ expectedKeypress: true,
+ expectedInput: '\n',
+ expectedValues: {
+ key: 'Enter', code: 'NumpadEnter',
+ keyCode: KeyboardEvent.DOM_VK_RETURN, charCode: 0,
+ location: KeyboardEvent.DOM_KEY_LOCATION_NUMPAD
+ }
+ });
+
+ tests.push({
+ dict: {
+ key: 'Backspace',
+ code: 'NumpadBackspace'
+ },
+ expectedKeypress: true,
+ expectedInput: 'Backspace', // Special value
+ expectedValues: {
+ key: 'Backspace', code: 'NumpadBackspace',
+ keyCode: KeyboardEvent.DOM_VK_BACK_SPACE, charCode: 0,
+ location: KeyboardEvent.DOM_KEY_LOCATION_NUMPAD
+ }
+ });
+
+ tests.forEach((test) => {
+ promiseQueue = promiseQueue.then(() => {
+ return sendKeyAndAssertResult(test);
+ });
+ });
+
+ return promiseQueue;
+}
+
+function runSendKeyRejectionTests() {
+ gTestDescription = 'runSendKeyRejectionTests(): ';
+ var promiseQueue = Promise.resolve();
+
+ promiseQueue = promiseQueue.then(() => {
+ return sendKeyAndAssertResult({
+ dict: undefined,
+ expectedReject: TypeError
+ });
+ });
+
+ return promiseQueue;
+}
+
+function setCompositionAndAssertResult(testdata) {
+ var dict = testdata.dict;
+ var testName;
+ var promise;
+
+ if (dict) {
+ testName = gTestDescription +
+ 'setComposition(' + testdata.text +
+ ', undefined, undefined, '
+ + JSON.stringify(dict) + ')';
+ promise = navigator.mozInputMethod.inputcontext
+ .setComposition(testdata.text, undefined, undefined, dict);
+ } else {
+ testName = gTestDescription +
+ 'setComposition(' + testdata.text + ')';
+ promise = navigator.mozInputMethod.inputcontext
+ .setComposition(testdata.text);
+ }
+
+ if (testdata.expectedReject) {
+ promise = promise
+ .then(() => {
+ ok(false, testName + ' should not resolve.');
+ }, (e) => {
+ ok(true, testName + ' rejects.');
+ ok(e instanceof testdata.expectedReject, 'Reject with type.');
+ })
+
+ return promise;
+ }
+
+ promise = promise
+ .then((res) => {
+ is(res, true,
+ testName + ' should resolve to true.');
+
+ var expectedEventDetail = [];
+
+ var expectedValues = testdata.expectedValues;
+
+ if (testdata.expectsKeyEvents &&
+ (testdata.startsComposition ||
+ testdata.dispatchKeyboardEventDuringComposition)) {
+ expectedEventDetail.push({
+ type: 'keydown',
+ key: expectedValues.key,
+ charCode: 0,
+ code: expectedValues.code || '',
+ keyCode: expectedValues.keyCode || 0,
+ location: 0,
+ repeat: expectedValues.repeat || false,
+ value: gCurrentValue,
+ shift: false,
+ capsLock: false,
+ control: false,
+ alt: false
+ });
+ }
+
+ if (testdata.startsComposition) {
+ expectedEventDetail.push({
+ type: 'compositionstart',
+ data: '',
+ value: gCurrentValue
+ });
+ }
+
+ expectedEventDetail.push({
+ type: 'compositionupdate',
+ data: testdata.text,
+ value: gCurrentValue
+ });
+
+ expectedEventDetail.push({
+ type: 'input',
+ value: gCurrentValue += testdata.expectedInput
+ });
+
+ if (testdata.expectsKeyEvents &&
+ testdata.dispatchKeyboardEventDuringComposition) {
+ expectedEventDetail.push({
+ type: 'keyup',
+ key: expectedValues.key,
+ charCode: 0,
+ code: expectedValues.code || '',
+ keyCode: expectedValues.keyCode || 0,
+ location: 0,
+ repeat: expectedValues.repeat || false,
+ value: gCurrentValue,
+ shift: false,
+ capsLock: false,
+ control: false,
+ alt: false
+ });
+ }
+
+ assertEventDetail(expectedEventDetail, testName);
+ gEventDetails = [];
+ }, (e) => {
+ ok(false, testName + ' should not reject. ' + e);
+ });
+
+ return promise;
+}
+
+function endCompositionAndAssertResult(testdata) {
+ var dict = testdata.dict;
+ var testName;
+ var promise;
+ if (dict) {
+ testName = gTestDescription +
+ 'endComposition(' + testdata.text + ', ' + JSON.stringify(dict) + ')';
+ promise = navigator.mozInputMethod.inputcontext
+ .endComposition(testdata.text, dict);
+ } else {
+ testName = gTestDescription +
+ 'endComposition(' + testdata.text + ')';
+ promise = navigator.mozInputMethod.inputcontext
+ .endComposition(testdata.text);
+ }
+
+ if (testdata.expectedReject) {
+ promise = promise
+ .then(() => {
+ ok(false, testName + ' should not resolve.');
+ }, (e) => {
+ ok(true, testName + ' rejects.');
+ ok(e instanceof testdata.expectedReject, 'Reject with type.');
+ })
+
+ return promise;
+ }
+
+ promise = promise
+ .then((res) => {
+ is(res, true,
+ testName + ' should resolve to true.');
+
+ var expectedEventDetail = [];
+
+ var expectedValues = testdata.expectedValues;
+
+ if (testdata.expectsKeyEvents &&
+ testdata.dispatchKeyboardEventDuringComposition) {
+ expectedEventDetail.push({
+ type: 'keydown',
+ key: expectedValues.key,
+ charCode: 0,
+ code: expectedValues.code || '',
+ keyCode: expectedValues.keyCode || 0,
+ location: 0,
+ repeat: expectedValues.repeat || false,
+ value: gCurrentValue,
+ shift: false,
+ capsLock: false,
+ control: false,
+ alt: false
+ });
+ }
+
+ expectedEventDetail.push({
+ type: 'compositionend',
+ data: testdata.text,
+ value: gCurrentValue
+ });
+
+ expectedEventDetail.push({
+ type: 'input',
+ value: gCurrentValue
+ });
+
+ if (testdata.expectsKeyEvents) {
+ expectedEventDetail.push({
+ type: 'keyup',
+ key: expectedValues.key,
+ charCode: 0,
+ code: expectedValues.code || '',
+ keyCode: expectedValues.keyCode || 0,
+ location: 0,
+ repeat: expectedValues.repeat || false,
+ value: gCurrentValue,
+ shift: false,
+ capsLock: false,
+ control: false,
+ alt: false
+ });
+ }
+
+ assertEventDetail(expectedEventDetail, testName);
+ gEventDetails = [];
+ }, (e) => {
+ ok(false, testName + ' should not reject. ' + e);
+ });
+
+ return promise;
+}
+
+function runCompositionWithKeyEventTests() {
+ var promiseQueue = Promise.resolve();
+
+ [true, false].forEach((dispatchKeyboardEventDuringComposition) => {
+ gTestDescription = 'runCompositionWithKeyEventTests() (dispatchKeyboardEvent =' + dispatchKeyboardEventDuringComposition + '): ';
+
+ promiseQueue = promiseQueue
+ .then(() => {
+ SpecialPowers.setBoolPref(
+ 'dom.keyboardevent.dispatch_during_composition',
+ dispatchKeyboardEventDuringComposition);
+ })
+ .then(() => {
+ return setCompositionAndAssertResult({
+ text: 'foo',
+ expectsKeyEvents: true,
+ startsComposition: true,
+ dispatchKeyboardEventDuringComposition: dispatchKeyboardEventDuringComposition,
+ expectedInput: 'foo',
+ dict: {
+ key: 'a',
+ code: 'KeyA',
+ keyCode: KeyboardEvent.DOM_VK_A
+ },
+ expectedValues: {
+ key: 'a',
+ code: 'KeyA',
+ keyCode: KeyboardEvent.DOM_VK_A
+ }
+ });
+ })
+ .then(() => {
+ return setCompositionAndAssertResult({
+ text: 'foobar',
+ expectsKeyEvents: true,
+ startsComposition: false,
+ dispatchKeyboardEventDuringComposition: dispatchKeyboardEventDuringComposition,
+ expectedInput: 'bar',
+ dict: {
+ key: 'a',
+ code: 'KeyA',
+ keyCode: KeyboardEvent.DOM_VK_A
+ },
+ expectedValues: {
+ key: 'a',
+ code: 'KeyA',
+ keyCode: KeyboardEvent.DOM_VK_A
+ }
+ });
+ })
+ .then(() => {
+ return endCompositionAndAssertResult({
+ text: 'foobar',
+ expectsKeyEvents: true,
+ dispatchKeyboardEventDuringComposition: dispatchKeyboardEventDuringComposition,
+ expectedInput: '',
+ dict: {
+ key: 'a',
+ code: 'KeyA',
+ keyCode: KeyboardEvent.DOM_VK_A
+ },
+ expectedValues: {
+ key: 'a',
+ code: 'KeyA',
+ keyCode: KeyboardEvent.DOM_VK_A
+ }
+ });
+ })
+ .then(() => {
+ SpecialPowers.clearUserPref(
+ 'dom.keyboardevent.dispatch_during_composition');
+ });
+ });
+
+ return promiseQueue;
+}
+
+function runCompositionWithoutKeyEventTests() {
+ var promiseQueue = Promise.resolve();
+
+ gTestDescription = 'runCompositionWithoutKeyEventTests(): ';
+
+ promiseQueue = promiseQueue
+ .then(() => {
+ return setCompositionAndAssertResult({
+ text: 'foo',
+ expectsKeyEvents: false,
+ startsComposition: true,
+ expectedInput: 'foo'
+ });
+ })
+ .then(() => {
+ return setCompositionAndAssertResult({
+ text: 'foobar',
+ expectsKeyEvents: false,
+ startsComposition: false,
+ expectedInput: 'bar'
+ });
+ })
+ .then(() => {
+ return endCompositionAndAssertResult({
+ text: 'foobar',
+ expectsKeyEvents: false,
+ expectedInput: ''
+ });
+ });
+
+ return promiseQueue;
+}
+
+function keydownAndAssertResult(testdata) {
+ var dict = testdata.dict;
+ var testName = gTestDescription + 'keydown(' + JSON.stringify(dict) + ')';
+ var promise = navigator.mozInputMethod.inputcontext.keydown(dict);
+
+ if (testdata.expectedReject) {
+ promise = promise
+ .then(() => {
+ ok(false, testName + ' should not resolve.');
+ }, (e) => {
+ ok(true, testName + ' rejects.');
+ ok(e instanceof testdata.expectedReject, 'Reject with type.');
+ })
+
+ return promise;
+ }
+
+ promise = promise
+ .then((res) => {
+ is(res, true,
+ testName + ' should resolve to true.');
+
+ var expectedEventDetail = [];
+
+ var expectedValues = testdata.expectedValues;
+
+ expectedEventDetail.push({
+ type: 'keydown',
+ key: expectedValues.key,
+ charCode: 0,
+ code: expectedValues.code || '',
+ keyCode: expectedValues.keyCode || 0,
+ location: 0,
+ repeat: expectedValues.repeat || false,
+ value: gCurrentValue,
+ shift: false,
+ capsLock: false,
+ control: false,
+ alt: false
+ });
+
+ if (testdata.expectedKeypress) {
+ expectedEventDetail.push({
+ type: 'keypress',
+ key: expectedValues.key,
+ charCode: expectedValues.charCode,
+ code: expectedValues.code || '',
+ keyCode: expectedValues.charCode ? 0 : expectedValues.keyCode,
+ location: 0,
+ repeat: expectedValues.repeat || false,
+ value: gCurrentValue,
+ shift: false,
+ capsLock: false,
+ control: false,
+ alt: false
+ });
+ }
+
+ if (testdata.expectedInput) {
+ expectedEventDetail.push({
+ type: 'input',
+ value: gCurrentValue += testdata.expectedInput
+ });
+ }
+
+ assertEventDetail(expectedEventDetail, testName);
+ gEventDetails = [];
+ }, (e) => {
+ ok(false, testName + ' should not reject. ' + e);
+ });
+
+ return promise;
+}
+
+function keyupAndAssertResult(testdata) {
+ var dict = testdata.dict;
+ var testName = gTestDescription + 'keyup(' + JSON.stringify(dict) + ')';
+ var promise = navigator.mozInputMethod.inputcontext.keyup(dict);
+
+ if (testdata.expectedReject) {
+ promise = promise
+ .then(() => {
+ ok(false, testName + ' should not resolve.');
+ }, (e) => {
+ ok(true, testName + ' rejects.');
+ ok(e instanceof testdata.expectedReject, 'Reject with type.');
+ })
+
+ return promise;
+ }
+
+ promise = promise
+ .then((res) => {
+ is(res, true,
+ testName + ' should resolve to true.');
+
+ var expectedEventDetail = [];
+
+ var expectedValues = testdata.expectedValues;
+
+ expectedEventDetail.push({
+ type: 'keyup',
+ key: expectedValues.key,
+ charCode: 0,
+ code: expectedValues.code || '',
+ keyCode: expectedValues.keyCode || 0,
+ location: 0,
+ repeat: expectedValues.repeat || false,
+ value: gCurrentValue,
+ shift: false,
+ capsLock: false,
+ control: false,
+ alt: false
+ });
+
+ assertEventDetail(expectedEventDetail, testName);
+ gEventDetails = [];
+ }, (e) => {
+ ok(false, testName + ' should not reject. ' + e);
+ });
+
+ return promise;
+}
+
+function runKeyDownUpTests() {
+ gTestDescription = 'runKeyDownUpTests(): ';
+ var promiseQueue = Promise.resolve();
+
+ let chr = 'a';
+ let code = 'KeyA';
+ let keyCode = KeyboardEvent.DOM_VK_A;
+
+ promiseQueue = promiseQueue
+ .then(() => {
+ return keydownAndAssertResult({
+ dict: {
+ key: chr,
+ code: code,
+ keyCode: keyCode
+ },
+ expectedKeypress: true,
+ expectedInput: chr,
+ expectedValues: {
+ key: chr, code: code,
+ keyCode: keyCode,
+ charCode: chr.charCodeAt(0)
+ }
+ });
+ })
+ .then(() => {
+ return keyupAndAssertResult({
+ dict: {
+ key: chr,
+ code: code,
+ keyCode: keyCode
+ },
+ expectedValues: {
+ key: chr, code: code,
+ keyCode: keyCode,
+ charCode: chr.charCodeAt(0)
+ }
+ });
+ });
+
+ return promiseQueue;
+}
+
+function runKeyDownUpRejectionTests() {
+ gTestDescription = 'runKeyDownUpRejectionTests(): ';
+ var promiseQueue = Promise.resolve();
+
+ promiseQueue = promiseQueue.then(() => {
+ return keydownAndAssertResult({
+ dict: undefined,
+ expectedReject: TypeError
+ });
+ });
+
+ promiseQueue = promiseQueue.then(() => {
+ return keyupAndAssertResult({
+ dict: undefined,
+ expectedReject: TypeError
+ });
+ });
+
+ return promiseQueue;
+}
+
+function runRepeatTests() {
+ gTestDescription = 'runRepeatTests(): ';
+ var promiseQueue = Promise.resolve();
+
+ // Test repeat
+ promiseQueue = promiseQueue
+ .then(() => {
+ return sendKeyAndAssertResult({
+ dict: {
+ key: 'A',
+ repeat: true
+ },
+ expectedKeypress: true,
+ expectedRepeat: true,
+ expectedInput: 'A',
+ expectedValues: {
+ repeat: true,
+ key: 'A', code: '',
+ keyCode: KeyboardEvent.DOM_VK_A,
+ charCode: 'A'.charCodeAt(0)
+ }
+ });
+ })
+ .then(() => {
+ return keyupAndAssertResult({
+ dict: {
+ key: 'A'
+ },
+ expectedKeypress: true,
+ expectedRepeat: true,
+ expectedInput: 'A',
+ expectedValues: {
+ key: 'A', code: '',
+ keyCode: KeyboardEvent.DOM_VK_A,
+ charCode: 'A'.charCodeAt(0)
+ }
+ });
+ });
+
+ return promiseQueue;
+}
+
+function runTest() {
+ let im = navigator.mozInputMethod;
+
+ // Set current page as an input method.
+ SpecialPowers.wrap(im).setActive(true);
+
+ let iframe = document.createElement('iframe');
+ iframe.src = 'file_test_bug1137557.html';
+ iframe.setAttribute('mozbrowser', true);
+ document.body.appendChild(iframe);
+
+ let mm = SpecialPowers.getBrowserFrameMessageManager(iframe);
+
+ iframe.addEventListener('mozbrowserloadend', function() {
+ mm.addMessageListener('test:eventDetail', function(msg) {
+ gEventDetails.push(msg.data);
+ });
+ mm.loadFrameScript('data:,(' + encodeURIComponent(appFrameScript.toString()) + ')();', false);
+ });
+
+ waitForInputContextChange()
+ .then(() => {
+ var inputcontext = navigator.mozInputMethod.inputcontext;
+
+ ok(!!inputcontext, 'Receving the first input context');
+ })
+ .then(() => runSendKeyAlphabetTests())
+ .then(() => runSendKeyNumberTests())
+ .then(() => runSendKeyDvorakTests())
+ .then(() => runSendKeyDigitKeySymbolsTests())
+ .then(() => runSendKeyUSKeyboardSymbolsTests())
+ .then(() => runSendKeyGreekLettersTests())
+ .then(() => runSendKeyEnterTests())
+ .then(() => runSendKeyNumpadTests())
+ .then(() => runSendKeyRejectionTests())
+ .then(() => runCompositionWithKeyEventTests())
+ .then(() => runCompositionWithoutKeyEventTests())
+ .then(() => runKeyDownUpTests())
+ .then(() => runKeyDownUpRejectionTests())
+ .then(() => runRepeatTests())
+ .catch((err) => {
+ console.error(err);
+ is(false, err.message);
+ })
+ .then(() => {
+ var p = waitForInputContextChange();
+
+ // Revoke our right from using the IM API.
+ SpecialPowers.wrap(im).setActive(false);
+
+ return p;
+ })
+ .then(() => {
+ var inputcontext = navigator.mozInputMethod.inputcontext;
+
+ is(inputcontext, null, 'Receving null input context');
+
+ inputmethod_cleanup();
+ })
+ .catch((err) => {
+ console.error(err);
+ is(false, err.message);
+ });
+}
+
+</script>
+</pre>
+</body>
+</html>
+
diff --git a/dom/inputmethod/mochitest/test_bug1175399.html b/dom/inputmethod/mochitest/test_bug1175399.html
new file mode 100644
index 000000000..64fc85e88
--- /dev/null
+++ b/dom/inputmethod/mochitest/test_bug1175399.html
@@ -0,0 +1,62 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1175399
+-->
+<head>
+ <title>Test focus when page unloads</title>
+ <script type="application/javascript;version=1.7" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript;version=1.7" src="inputmethod_common.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=1175399">Mozilla Bug 1175399</a>
+<p id="display"></p>
+<pre id="test">
+<script class="testbody" type="application/javascript;version=1.7">
+
+inputmethod_setup(function() {
+ runTest();
+});
+
+let appFrameScript = function appFrameScript() {
+ let input = content.document.body.firstElementChild;
+ input.focus();
+
+ content.setTimeout(function() {
+ sendAsyncMessage('test:step');
+ });
+};
+
+function runTest() {
+ let im = navigator.mozInputMethod;
+
+ // Set current page as an input method.
+ SpecialPowers.wrap(im).setActive(true);
+
+ let iframe = document.createElement('iframe');
+ iframe.src = 'file_test_bug1175399.html';
+ iframe.setAttribute('mozbrowser', true);
+ document.body.appendChild(iframe);
+
+ let mm = SpecialPowers.getBrowserFrameMessageManager(iframe);
+ im.oninputcontextchange = function() {
+ is(false, 'should not receive inputcontextchange event');
+ };
+
+ iframe.addEventListener('mozbrowserloadend', function() {
+ mm.addMessageListener('test:step', function() {
+ let inputcontext = navigator.mozInputMethod.inputcontext;
+ is(inputcontext, null, 'inputcontext is null');
+
+ inputmethod_cleanup();
+ });
+ mm.loadFrameScript('data:,(' + encodeURIComponent(appFrameScript.toString()) + ')();', false);
+ });
+}
+
+</script>
+</pre>
+</body>
+</html>
+
diff --git a/dom/inputmethod/mochitest/test_bug944397.html b/dom/inputmethod/mochitest/test_bug944397.html
new file mode 100644
index 000000000..4be95f395
--- /dev/null
+++ b/dom/inputmethod/mochitest/test_bug944397.html
@@ -0,0 +1,107 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=944397
+-->
+<head>
+ <title>Basic test for InputMethod API.</title>
+ <script type="application/javascript;version=1.7" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript;version=1.7" src="inputmethod_common.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=944397">Mozilla Bug 944397</a>
+<p id="display"></p>
+<pre id="test">
+<script class="testbody" type="application/javascript;version=1.7">
+
+SimpleTest.requestFlakyTimeout("untriaged");
+
+inputmethod_setup(function() {
+ runTest();
+});
+
+// The frame script running in file_test_app.html.
+function appFrameScript() {
+ let input = content.document.getElementById('test-input');
+ input.oninput = function() {
+ sendAsyncMessage('test:InputMethod:oninput', {
+ value: input.value
+ });
+ };
+}
+
+function runTest() {
+ let app, keyboard;
+
+ /**
+ * So this test does the following:
+ * 1. Create a mozbrowser iframe with a text field in it, and focus the text field
+ * 2. 100ms. after loading we create new keyboard iframe, that will try to execute
+ * replaceSurroundingText on the current active inputcontext
+ * 3. That should trigger 'input' event on the said text field
+ * 4. And if that happens we know everything is OK
+ */
+
+ let path = location.pathname;
+ let basePath = location.protocol + '//' + location.host +
+ path.substring(0, path.lastIndexOf('/'));
+
+ // STEP 1: Create an app frame to recieve keyboard inputs.
+ function step1() {
+ app = document.createElement('iframe');
+ app.src = basePath + '/file_test_app.html';
+ app.setAttribute('mozbrowser', true);
+ document.body.appendChild(app);
+ app.addEventListener('mozbrowserloadend', function() {
+ let mm = SpecialPowers.getBrowserFrameMessageManager(app);
+ mm.loadFrameScript('data:,(' + appFrameScript.toString() + ')();', false);
+ mm.addMessageListener("test:InputMethod:oninput", function(ev) {
+ step4(SpecialPowers.wrap(ev).json.value);
+ });
+
+ step2();
+ });
+ }
+
+ function step2() {
+ // STEP 2a: Create a browser frame to load the input method app.
+ keyboard = document.createElement('iframe');
+ keyboard.setAttribute('mozbrowser', true);
+ document.body.appendChild(keyboard);
+
+ // STEP 2b: Grant input privileges to the keyboard iframe
+ let imeUrl = basePath + '/file_inputmethod.html#data';
+
+ // STEP 2c: Tell Gecko to use this iframe as its keyboard app
+ let req = keyboard.setInputMethodActive(true);
+
+ req.onsuccess = function() {
+ ok(true, 'setInputMethodActive succeeded.');
+ };
+
+ req.onerror = function() {
+ ok(false, 'setInputMethodActive failed: ' + this.error.name);
+ inputmethod_cleanup();
+ };
+
+ // STEP 3: Loads the input method app to the browser frame after a delay.
+ setTimeout(function() {
+ keyboard.src = imeUrl;
+ }, 100);
+ }
+
+ function step4(val) {
+ ok(true, 'Keyboard input was received.');
+ is(val, '#dataYuan', 'Input value');
+ inputmethod_cleanup();
+ }
+
+ step1();
+}
+
+</script>
+</pre>
+</body>
+</html>
+
diff --git a/dom/inputmethod/mochitest/test_bug949059.html b/dom/inputmethod/mochitest/test_bug949059.html
new file mode 100644
index 000000000..495f3d63c
--- /dev/null
+++ b/dom/inputmethod/mochitest/test_bug949059.html
@@ -0,0 +1,40 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=949059
+-->
+<head>
+ <title>Test "mgmt" property of MozInputMethod.</title>
+ <script type="application/javascript;version=1.7" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript;version=1.7" src="inputmethod_common.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=949059">Mozilla Bug 949059</a>
+<p id="display"></p>
+<pre id="test">
+<script class="testbody" type="application/javascript;version=1.7">
+
+inputmethod_setup(function() {
+ runTest();
+});
+
+function runTest() {
+ let im = navigator.mozInputMethod;
+
+ // Treat current page as an input method and activate it.
+ SpecialPowers.wrap(im).setActive(true);
+ ok(im.mgmt, 'The mgmt property should not be null.');
+
+ // Deactivate current page.
+ SpecialPowers.wrap(im).setActive(false);
+ ok(im.mgmt, 'The mgmt property should not be null.');
+
+ inputmethod_cleanup();
+}
+
+</script>
+</pre>
+</body>
+</html>
+
diff --git a/dom/inputmethod/mochitest/test_bug953044.html b/dom/inputmethod/mochitest/test_bug953044.html
new file mode 100644
index 000000000..2b1f022a6
--- /dev/null
+++ b/dom/inputmethod/mochitest/test_bug953044.html
@@ -0,0 +1,52 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=953044
+-->
+<head>
+ <title>Basic test for InputMethod API.</title>
+ <script type="application/javascript;version=1.7" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript;version=1.7" src="inputmethod_common.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=953044">Mozilla Bug 953044</a>
+<p id="display"></p>
+<pre id="test">
+<script class="testbody" type="application/javascript;version=1.7">
+
+inputmethod_setup(function() {
+ runTest();
+});
+
+function runTest() {
+ // Create an app frame to recieve keyboard inputs.
+ let app = document.createElement('iframe');
+ app.src = 'file_test_app.html';
+ app.setAttribute('mozbrowser', true);
+ document.body.appendChild(app);
+
+ // Create a browser frame to load the input method app.
+ let keyboard = document.createElement('iframe');
+ keyboard.setAttribute('mozbrowser', true);
+ document.body.appendChild(keyboard);
+
+ // Bug 953044 setInputMethodActive(false) before input method app loads should
+ // always succeed.
+ let req = keyboard.setInputMethodActive(false);
+ req.onsuccess = function() {
+ ok(true, 'setInputMethodActive before loading succeeded.');
+ inputmethod_cleanup();
+ };
+
+ req.onerror = function() {
+ ok(false, 'setInputMethodActive before loading failed: ' + this.error.name);
+ inputmethod_cleanup();
+ };
+}
+
+</script>
+</pre>
+</body>
+</html>
+
diff --git a/dom/inputmethod/mochitest/test_bug960946.html b/dom/inputmethod/mochitest/test_bug960946.html
new file mode 100644
index 000000000..f65ff42e7
--- /dev/null
+++ b/dom/inputmethod/mochitest/test_bug960946.html
@@ -0,0 +1,108 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=960946
+-->
+<head>
+ <title>Basic test for repeat sendKey events</title>
+ <script type="application/javascript;version=1.7" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript;version=1.7" src="inputmethod_common.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=960946">Mozilla Bug 960946</a>
+<p id="display"></p>
+<pre id="test">
+<script class="testbody" type="application/javascript;version=1.7">
+
+// The input context.
+var gContext = null;
+var gCounter = 0;
+var gBackSpaceCounter = 0;
+var result = ["keydown", "keypress", "keydown","keypress",
+ "keydown", "keypress", "keyup"
+ ];
+
+inputmethod_setup(function() {
+ runTest();
+});
+
+var input;
+// The frame script running in file_test_backspace_event.html.
+function appFrameScript() {
+ let input = content.document.getElementById('test-input');
+ input.onkeydown = input.onkeypress = input.onkeyup = function(event) {
+ dump('key event was fired in file_test_backspace_event.html: ' + event.type +'\n');
+ sendAsyncMessage('test:KeyBoard:keyEvent', {'type':event.type});
+ };
+}
+
+function runTest() {
+ let im = navigator.mozInputMethod;
+
+ im.oninputcontextchange = function() {
+ ok(true, 'inputcontextchange event was fired.');
+ im.oninputcontextchange = null;
+
+ gContext = im.inputcontext;
+ if (!gContext) {
+ ok(false, 'Should have a non-null inputcontext.');
+ inputmethod_cleanup();
+ return;
+ }
+
+ test_sendKey();
+ };
+
+ // Set current page as an input method.
+ SpecialPowers.wrap(im).setActive(true);
+
+ // Create an app frame to recieve keyboard inputs.
+ let app = document.createElement('iframe');
+ app.src = 'file_test_app.html';
+ app.setAttribute('mozbrowser', true);
+ document.body.appendChild(app);
+ app.addEventListener('mozbrowserloadend', function() {
+ let mm = SpecialPowers.getBrowserFrameMessageManager(app);
+ mm.loadFrameScript('data:,(' + appFrameScript.toString() + ')();', false);
+ mm.addMessageListener("test:KeyBoard:keyEvent", function(event) {
+ ok(true, 'Keyboard input was received.');
+ is(SpecialPowers.wrap(event).json.type, result[gCounter], "expected event");
+ gCounter++;
+ });
+ });
+}
+
+function test_sendKey() {
+ // Move cursor position to 4.
+ gContext.setSelectionRange(4, 0).then(function() {
+ is(gContext.selectionStart, 4, 'selectionStart was set successfully.');
+ is(gContext.selectionEnd, 4, 'selectionEnd was set successfully.');
+ for(let i = 0; i < 2; i++) {
+ test_sendBackspace(true);
+ }
+ test_sendBackspace(false);
+ }, function(e) {
+ ok(false, 'setSelectionRange failed:' + e.name);
+ inputmethod_cleanup();
+ });
+}
+
+function test_sendBackspace(repeat) {
+ // Send backspace
+ gContext.sendKey(KeyEvent.DOM_VK_BACK_SPACE, 0, 0, repeat).then(function() {
+ ok(true, 'sendKey success');
+ gBackSpaceCounter++;
+ if (gBackSpaceCounter == 3) {
+ inputmethod_cleanup();
+ }
+ }, function(e) {
+ ok(false, 'sendKey failed:' + e.name);
+ inputmethod_cleanup();
+ });
+}
+</script>
+</pre>
+</body>
+</html>
+
diff --git a/dom/inputmethod/mochitest/test_bug978918.html b/dom/inputmethod/mochitest/test_bug978918.html
new file mode 100644
index 000000000..56e02a57d
--- /dev/null
+++ b/dom/inputmethod/mochitest/test_bug978918.html
@@ -0,0 +1,77 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=978918
+-->
+<head>
+ <title>Basic test for InputMethod API.</title>
+ <script type="application/javascript;version=1.7" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript;version=1.7" src="inputmethod_common.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=978918">Mozilla Bug 978918</a>
+<p id="display"></p>
+<pre id="test">
+<script class="testbody" type="application/javascript;version=1.7">
+
+// The input context.
+var gContext = null;
+
+inputmethod_setup(function() {
+ runTest();
+});
+
+function runTest() {
+ let im = navigator.mozInputMethod;
+
+ im.oninputcontextchange = function() {
+ ok(true, 'inputcontextchange event was fired.');
+ im.oninputcontextchange = null;
+
+ gContext = im.inputcontext;
+ if (!gContext) {
+ ok(false, 'Should have a non-null inputcontext.');
+ inputmethod_cleanup();
+ return;
+ }
+
+ test_setSelectionRange();
+ };
+
+ // Set current page as an input method.
+ SpecialPowers.wrap(im).setActive(true);
+
+ let iframe = document.createElement('iframe');
+ iframe.src = 'file_test_sms_app.html';
+ iframe.setAttribute('mozbrowser', true);
+ document.body.appendChild(iframe);
+}
+
+function test_setSelectionRange() {
+ gContext.setSelectionRange(0, 100).then(function() {
+ is(gContext.selectionStart, 0, 'selectionStart was set successfully.');
+ is(gContext.selectionEnd, 5, 'selectionEnd was set successfully.');
+ test_replaceSurroundingText();
+ }, function(e) {
+ ok(false, 'setSelectionRange failed:' + e.name);
+ inputmethod_cleanup();
+ });
+}
+
+function test_replaceSurroundingText() {
+ // Replace 'Httvb' with 'Hito'.
+ gContext.replaceSurroundingText('Hito', 0, 100).then(function() {
+ ok(true, 'replaceSurroundingText finished');
+ inputmethod_cleanup();
+ }, function(e) {
+ ok(false, 'replaceSurroundingText failed: ' + e.name);
+ inputmethod_cleanup();
+ });
+}
+
+</script>
+</pre>
+</body>
+</html>
+
diff --git a/dom/inputmethod/mochitest/test_focus_blur_manage_events.html b/dom/inputmethod/mochitest/test_focus_blur_manage_events.html
new file mode 100644
index 000000000..939639050
--- /dev/null
+++ b/dom/inputmethod/mochitest/test_focus_blur_manage_events.html
@@ -0,0 +1,199 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1201407
+-->
+<head>
+ <title>Test inputcontextfocus and inputcontextblur event</title>
+ <script type="application/javascript;version=1.7" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript;version=1.7" src="inputmethod_common.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=1201407">Mozilla Bug 1201407</a>
+<p id="display"></p>
+<pre id="test">
+<script class="testbody" type="application/javascript;version=1.7">
+
+let contentFrameMM;
+
+function setupTestRunner() {
+ info('setupTestRunner');
+ let im = navigator.mozInputMethod;
+
+ let expectedEventDetails = [
+ { type: 'input', inputType: 'text' },
+ { type: 'input', inputType: 'search' },
+ { type: 'textarea', inputType: 'textarea' },
+ { type: 'contenteditable', inputType: 'textarea' },
+ { type: 'input', inputType: 'number' },
+ { type: 'input', inputType: 'tel' },
+ { type: 'input', inputType: 'url' },
+ { type: 'input', inputType: 'email' },
+ { type: 'input', inputType: 'password' },
+ { type: 'input', inputType: 'datetime' },
+ { type: 'input', inputType: 'date',
+ value: '2015-08-03', min: '1990-01-01', max: '2020-01-01' },
+ { type: 'input', inputType: 'month' },
+ { type: 'input', inputType: 'week' },
+ { type: 'input', inputType: 'time' },
+ { type: 'input', inputType: 'datetime-local' },
+ { type: 'input', inputType: 'color' },
+ { type: 'select', inputType: 'select-one',
+ choices: {
+ multiple: false,
+ choices: [
+ { group: false, inGroup: false, text: 'foo',
+ disabled: false, selected: true, optionIndex: 0 },
+ { group: false, inGroup: false, text: 'bar',
+ disabled: true, selected: false, optionIndex: 1 },
+ { group: true, text: 'group', disabled: false },
+ { group: false, inGroup: true, text: 'baz',
+ disabled: false, selected: false, optionIndex: 2 } ] }
+ },
+ { type: 'select', inputType: 'select-multiple',
+ choices: {
+ multiple: true,
+ choices: [
+ { group: false, inGroup: false, text: 'foo',
+ disabled: false, selected: true, optionIndex: 0 },
+ { group: false, inGroup: false, text: 'bar',
+ disabled: true, selected: false, optionIndex: 1 },
+ { group: true, text: 'group', disabled: false },
+ { group: false, inGroup: true, text: 'baz',
+ disabled: false, selected: false, optionIndex: 2 } ] }
+ }
+ ];
+
+ let expectBlur = false;
+
+ function deepAssertObject(obj, expectedObj, desc) {
+ for (let prop in expectedObj) {
+ if (typeof expectedObj[prop] === 'object') {
+ deepAssertObject(obj[prop], expectedObj[prop], desc + '.' + prop);
+ } else {
+ is(obj[prop], expectedObj[prop], desc + '.' + prop);
+ }
+ }
+ }
+
+ im.mgmt.oninputcontextfocus =
+ im.mgmt.oninputcontextblur = function(evt) {
+ if (expectBlur) {
+ is(evt.type, 'inputcontextblur', 'evt.type');
+ evt.preventDefault();
+ expectBlur = false;
+
+ return;
+ }
+
+ let expectedEventDetail = expectedEventDetails.shift();
+
+ if (!expectedEventDetail) {
+ ok(false, 'Receving extra events');
+ inputmethod_cleanup();
+
+ return;
+ }
+
+ is(evt.type, 'inputcontextfocus', 'evt.type');
+ evt.preventDefault();
+ expectBlur = true;
+
+ let detail = evt.detail;
+ deepAssertObject(detail, expectedEventDetail, 'detail');
+
+ if (expectedEventDetails.length) {
+ contentFrameMM.sendAsyncMessage('test:next');
+ } else {
+ im.mgmt.oninputcontextfocus = im.mgmt.oninputcontextblur = null;
+ inputmethod_cleanup();
+ }
+ };
+}
+
+function setupInputAppFrame() {
+ info('setupInputAppFrame');
+ return new Promise((resolve, reject) => {
+ let appFrameScript = function appFrameScript() {
+ let im = content.navigator.mozInputMethod;
+
+ im.mgmt.oninputcontextfocus =
+ im.mgmt.oninputcontextblur = function(evt) {
+ sendAsyncMessage('text:appEvent', { type: evt.type });
+ };
+
+ content.document.body.textContent = 'I am a input app';
+ };
+
+ let path = location.pathname;
+ let basePath = location.protocol + '//' + location.host +
+ path.substring(0, path.lastIndexOf('/'));
+ let imeUrl = basePath + '/file_blank.html';
+
+ let inputAppFrame = document.createElement('iframe');
+ inputAppFrame.setAttribute('mozbrowser', true);
+ inputAppFrame.src = imeUrl;
+ document.body.appendChild(inputAppFrame);
+
+ let mm = SpecialPowers.getBrowserFrameMessageManager(inputAppFrame);
+ inputAppFrame.addEventListener('mozbrowserloadend', function() {
+ mm.addMessageListener('text:appEvent', function(msg) {
+ ok(false, 'Input app should not receive ' + msg.data.type + ' event.');
+ });
+ mm.loadFrameScript('data:,(' + encodeURIComponent(appFrameScript.toString()) + ')();', false);
+
+ // Set the input app frame to be active
+ let req = inputAppFrame.setInputMethodActive(true);
+ resolve(req);
+ });
+ });
+}
+
+function setupContentFrame() {
+ info('setupContentFrame');
+ return new Promise((resolve, reject) => {
+ let contentFrameScript = function contentFrameScript() {
+ let input = content.document.body.firstElementChild;
+
+ let i = 0;
+
+ input.focus();
+
+ addMessageListener('test:next', function() {
+ content.document.body.children[++i].focus();
+ });
+ };
+
+ let iframe = document.createElement('iframe');
+ iframe.src = 'file_test_focus_blur_manage_events.html';
+ iframe.setAttribute('mozbrowser', true);
+ document.body.appendChild(iframe);
+
+ let mm = contentFrameMM =
+ SpecialPowers.getBrowserFrameMessageManager(iframe);
+
+ iframe.addEventListener('mozbrowserloadend', function() {
+ mm.loadFrameScript('data:,(' + encodeURIComponent(contentFrameScript.toString()) + ')();', false);
+
+ resolve();
+ });
+ });
+}
+
+inputmethod_setup(function() {
+ Promise.resolve()
+ .then(() => setupTestRunner())
+ .then(() => setupContentFrame())
+ .then(() => setupInputAppFrame())
+ .catch((e) => {
+ ok(false, 'Error' + e.toString());
+ console.error(e);
+ });
+});
+
+</script>
+</pre>
+</body>
+</html>
+
diff --git a/dom/inputmethod/mochitest/test_forward_hardware_key_to_ime.html b/dom/inputmethod/mochitest/test_forward_hardware_key_to_ime.html
new file mode 100644
index 000000000..13bd58ce9
--- /dev/null
+++ b/dom/inputmethod/mochitest/test_forward_hardware_key_to_ime.html
@@ -0,0 +1,149 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1110030
+-->
+<head>
+ <title>Forwarding Hardware Key to InputMethod</title>
+ <script type="application/javascript;version=1.7" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript;version=1.7" src="inputmethod_common.js"></script>
+ <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
+ <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/NativeKeyCodes.js"></script>
+ <script type="text/javascript" src="bug1110030_helper.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=1110030">Mozilla Bug 1110030</a>
+<p id="display"></p>
+<pre id="test">
+<script class="testbody" type="application/javascript;version=1.7">
+// The input context.
+var gContext = null;
+
+// The test cases.
+var gTests;
+
+inputmethod_setup(function() {
+ setInputContext();
+});
+
+function setInputContext() {
+ let im = navigator.mozInputMethod;
+
+ im.oninputcontextchange = function() {
+ ok(true, 'inputcontextchange event was fired.');
+ im.oninputcontextchange = null;
+
+ gContext = im.inputcontext;
+ if (!gContext || !gContext.hardwareinput) {
+ ok(false, 'Should have a non-null inputcontext.hardwareinput');
+ inputmethod_cleanup();
+ return;
+ }
+
+ prepareTest();
+ };
+
+ // Set current page as an input method.
+ SpecialPowers.wrap(im).setActive(true);
+
+ // verifyResultsAndMoveNext will be called after input#text-input
+ // receives all expected key events and it will verify results
+ // and start next test.
+ loadTestFrame(verifyResultsAndMoveNext);
+}
+
+function prepareTest()
+{
+ // Set the used input method of this test
+ gInputMethod = new InputMethod(gContext);
+
+ // Add listenr to hardwareinput
+ addKeyEventListeners(gContext.hardwareinput, function (evt) {
+ hardwareEventReceiver(evt);
+ gInputMethod.handler(evt);
+ });
+
+ // Set the test cases
+ gTests = [
+ // Case 1: IME handle the key input
+ {
+ key: 'z',
+ hardwareinput: {
+ expectedEvents: kKeyDown | kKeyUp,
+ receivedEvents: 0,
+ expectedKeys: 'zz', // One for keydown, the other for keyup
+ receivedKeys: '',
+ },
+ inputtext: {
+ expectedEvents: kKeyDown | kKeyPress | kKeyUp,
+ receivedEvents: 0,
+ expectedKeys: gInputMethod.mapKey('z') + // for keydown
+ gInputMethod.mapKey('z') + // for keypress
+ gInputMethod.mapKey('z'), // for keyup
+ receivedKeys: '',
+ }
+ },
+ // case 2: IME doesn't handle the key input
+ {
+ key: '7',
+ hardwareinput: {
+ expectedEvents: kKeyDown | kKeyUp,
+ receivedEvents: 0,
+ expectedKeys: '77', // One for keydown, the other for keyup
+ receivedKeys: '',
+ },
+ inputtext: {
+ expectedEvents: kKeyDown | kKeyPress | kKeyUp,
+ receivedEvents: 0,
+ expectedKeys: '777', // keydown, keypress, keyup all will receive key
+ receivedKeys: '',
+ }
+ },
+ // case 3: IME is disable
+ // This case is same as
+ // dom/events/test/test_dom_before_after_keyboard_event*.html
+ ];
+
+ startTesting();
+}
+
+function startTesting()
+{
+ if (gTests.length <= 0) {
+ finish();
+ return;
+ }
+
+ gCurrentTest = gTests.shift();
+
+ fireEvent();
+}
+
+function verifyResultsAndMoveNext()
+{
+ verifyResults(gCurrentTest);
+ startTesting();
+}
+
+function finish()
+{
+ inputmethod_cleanup();
+}
+
+function errorHandler(msg)
+{
+ // Clear the test cases
+ if (gTests) {
+ gTests = [];
+ }
+
+ ok(false, msg);
+
+ inputmethod_cleanup();
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/inputmethod/mochitest/test_input_registry_events.html b/dom/inputmethod/mochitest/test_input_registry_events.html
new file mode 100644
index 000000000..e2c3c1f9d
--- /dev/null
+++ b/dom/inputmethod/mochitest/test_input_registry_events.html
@@ -0,0 +1,251 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1201407
+-->
+<head>
+ <title>Test addinputrequest and removeinputrequest event</title>
+ <script type="application/javascript;version=1.7" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript;version=1.7" src="inputmethod_common.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=1201407">Mozilla Bug 1201407</a>
+<p id="display"></p>
+<pre id="test">
+<script class="testbody" type="application/javascript;version=1.7">
+
+let appFrameMM;
+let nextStep;
+
+function setupInputAppFrame() {
+ info('setupInputAppFrame');
+ return new Promise((resolve, reject) => {
+ let appFrameScript = function appFrameScript() {
+ let im = content.navigator.mozInputMethod;
+
+ addMessageListener('test:callAddInput', function() {
+ im.addInput('foo', {
+ launch_path: 'bar.html',
+ name: 'Foo',
+ description: 'foobar',
+ types: ['text', 'password']
+ })
+ .then((r) => {
+ sendAsyncMessage('test:resolved', { resolved: true, result: r });
+ }, (e) => {
+ sendAsyncMessage('test:rejected', { rejected: true, error: e });
+ });
+ });
+
+ addMessageListener('test:callRemoveInput', function() {
+ im.removeInput('foo')
+ .then((r) => {
+ sendAsyncMessage('test:resolved', { resolved: true, result: r });
+ }, (e) => {
+ sendAsyncMessage('test:rejected', { rejected: true, error: e });
+ });
+ });
+
+ im.mgmt.onaddinputrequest =
+ im.mgmt.onremoveinputrequest = function(evt) {
+ sendAsyncMessage('test:appEvent', { type: evt.type });
+ };
+
+ content.document.body.textContent = 'I am a input app';
+ };
+
+ let path = location.pathname;
+ let basePath = location.protocol + '//' + location.host +
+ path.substring(0, path.lastIndexOf('/'));
+ let imeUrl = basePath + '/file_blank.html';
+
+ let inputAppFrame = document.createElement('iframe');
+ inputAppFrame.setAttribute('mozbrowser', true);
+ // FIXME: Bug 1270790
+ inputAppFrame.setAttribute('remote', true);
+ inputAppFrame.src = imeUrl;
+ document.body.appendChild(inputAppFrame);
+
+ let mm = appFrameMM =
+ SpecialPowers.getBrowserFrameMessageManager(inputAppFrame);
+
+ inputAppFrame.addEventListener('mozbrowserloadend', function() {
+ mm.addMessageListener('test:appEvent', function(msg) {
+ ok(false, 'Input app should not receive ' + msg.data.type + ' event.');
+ });
+ mm.addMessageListener('test:resolved', function(msg) {
+ nextStep && nextStep(msg.data);
+ });
+ mm.addMessageListener('test:rejected', function(msg) {
+ nextStep && nextStep(msg.data);
+ });
+ mm.loadFrameScript('data:,(' + encodeURIComponent(appFrameScript.toString()) + ')();', false);
+
+ resolve();
+ });
+ });
+}
+
+function Deferred() {
+ this.promise = new Promise((res, rej) => {
+ this.resolve = res;
+ this.reject = rej;
+ });
+ return this;
+}
+
+function deepAssertObject(obj, expectedObj, desc) {
+ for (let prop in expectedObj) {
+ if (typeof expectedObj[prop] === 'object') {
+ deepAssertObject(obj[prop], expectedObj[prop], desc + '.' + prop);
+ } else {
+ is(obj[prop], expectedObj[prop], desc + '.' + prop);
+ }
+ }
+}
+
+function setupTestRunner() {
+ let im = navigator.mozInputMethod;
+ let d;
+
+ let i = -1;
+ nextStep = function next(evt) {
+ i++;
+ info('Step ' + i);
+
+ switch (i) {
+ case 0:
+ appFrameMM.sendAsyncMessage('test:callAddInput');
+
+ break;
+
+ case 1:
+ is(evt.type, 'addinputrequest', 'evt.type');
+ deepAssertObject(evt.detail, {
+ inputId: 'foo',
+ manifestURL: null, // todo
+ inputManifest: {
+ launch_path: 'bar.html',
+ name: 'Foo',
+ description: 'foobar',
+ types: ['text', 'password']
+ }
+ }, 'detail');
+
+ d = new Deferred();
+ evt.detail.waitUntil(d.promise);
+ evt.preventDefault();
+
+ Promise.resolve().then(next);
+ break;
+
+ case 2:
+ d.resolve();
+ d = null;
+ break;
+
+ case 3:
+ ok(evt.resolved, 'resolved');
+ appFrameMM.sendAsyncMessage('test:callAddInput');
+
+ break;
+
+ case 4:
+ is(evt.type, 'addinputrequest', 'evt.type');
+
+ d = new Deferred();
+ evt.detail.waitUntil(d.promise);
+ evt.preventDefault();
+
+ Promise.resolve().then(next);
+ break;
+
+ case 5:
+ d.reject('Foo Error');
+ d = null;
+ break;
+
+ case 6:
+ ok(evt.rejected, 'rejected');
+ is(evt.error, 'Foo Error', 'rejected');
+
+
+ appFrameMM.sendAsyncMessage('test:callRemoveInput');
+
+ break;
+
+ case 7:
+ is(evt.type, 'removeinputrequest', 'evt.type');
+ deepAssertObject(evt.detail, {
+ inputId: 'foo',
+ manifestURL: null // todo
+ }, 'detail');
+
+ d = new Deferred();
+ evt.detail.waitUntil(d.promise);
+ evt.preventDefault();
+
+ Promise.resolve().then(next);
+ break;
+
+ case 8:
+ d.resolve();
+ d = null;
+ break;
+
+ case 9:
+ ok(evt.resolved, 'resolved');
+ appFrameMM.sendAsyncMessage('test:callRemoveInput');
+
+ break;
+
+ case 10:
+ is(evt.type, 'removeinputrequest', 'evt.type');
+
+ d = new Deferred();
+ evt.detail.waitUntil(d.promise);
+ evt.preventDefault();
+
+ Promise.resolve().then(next);
+ break;
+
+ case 11:
+ d.reject('Foo Error');
+ d = null;
+ break;
+
+ case 12:
+ ok(evt.rejected, 'rejected');
+ is(evt.error, 'Foo Error', 'rejected');
+ inputmethod_cleanup();
+
+ break;
+
+ default:
+ ok(false, 'received extra call.');
+ inputmethod_cleanup();
+
+ break;
+ }
+ }
+
+ im.mgmt.onaddinputrequest =
+ im.mgmt.onremoveinputrequest = nextStep;
+}
+
+inputmethod_setup(function() {
+ Promise.resolve()
+ .then(() => setupTestRunner())
+ .then(() => setupInputAppFrame())
+ .then(() => nextStep())
+ .catch((e) => {
+ ok(false, 'Error' + e.toString());
+ console.error(e);
+ });
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/inputmethod/mochitest/test_sendkey_cancel.html b/dom/inputmethod/mochitest/test_sendkey_cancel.html
new file mode 100644
index 000000000..8affa8c09
--- /dev/null
+++ b/dom/inputmethod/mochitest/test_sendkey_cancel.html
@@ -0,0 +1,67 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=952080
+-->
+<head>
+ <title>SendKey with canceled keydown test for InputMethod API.</title>
+ <script type="application/javascript;version=1.7" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript;version=1.7" src="inputmethod_common.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=952080">Mozilla Bug 952080</a>
+<p id="display"></p>
+<pre id="test">
+<script class="testbody" type="application/javascript;version=1.7">
+
+// The input context.
+var gContext = null;
+
+inputmethod_setup(function() {
+ runTest();
+});
+
+function runTest() {
+ let im = navigator.mozInputMethod;
+
+ im.oninputcontextchange = function() {
+ ok(true, 'inputcontextchange event was fired.');
+ im.oninputcontextchange = null;
+
+ gContext = im.inputcontext;
+ if (!gContext) {
+ ok(false, 'Should have a non-null inputcontext.');
+ inputmethod_cleanup();
+ return;
+ }
+
+ test();
+ };
+
+ // Set current page as an input method.
+ SpecialPowers.wrap(im).setActive(true);
+
+ let iframe = document.createElement('iframe');
+ iframe.src = 'file_test_sendkey_cancel.html';
+ iframe.setAttribute('mozbrowser', true);
+ document.body.appendChild(iframe);
+}
+
+function test() {
+ gContext.sendKey(0, 'j', 0).then(function() {
+ ok(false, 'sendKey was incorrectly resolved');
+
+ inputmethod_cleanup();
+ }, function(e) {
+ ok(true, 'sendKey was rejected');
+
+ inputmethod_cleanup();
+ });
+}
+
+</script>
+</pre>
+</body>
+</html>
+
diff --git a/dom/inputmethod/mochitest/test_setSupportsSwitching.html b/dom/inputmethod/mochitest/test_setSupportsSwitching.html
new file mode 100644
index 000000000..2a7540cde
--- /dev/null
+++ b/dom/inputmethod/mochitest/test_setSupportsSwitching.html
@@ -0,0 +1,130 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1197682
+-->
+<head>
+ <title>Test inputcontext#inputType and MozInputMethodManager#supportsSwitching()</title>
+ <script type="application/javascript;version=1.7" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript;version=1.7" src="inputmethod_common.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=1197682">Mozilla Bug 1197682</a>
+<p id="display"></p>
+<pre id="test">
+<script class="testbody" type="application/javascript;version=1.7">
+
+inputmethod_setup(function() {
+ runTest();
+});
+
+let appFrameScript = function appFrameScript() {
+ let input = content.document.body.firstElementChild;
+
+ let i = 1;
+
+ input.focus();
+
+ addMessageListener('test:next', function() {
+ i++;
+ switch (i) {
+ case 2:
+ content.document.body.children[1].focus();
+ i++; // keep the same count with the parent frame.
+
+ break;
+
+ case 4:
+ content.document.body.lastElementChild.focus();
+ i++; // keep the same count with the parent frame.
+
+ break;
+
+ case 6:
+ content.document.body.lastElementChild.blur();
+
+ break;
+ }
+ });
+};
+
+function runTest() {
+ let im = navigator.mozInputMethod;
+
+ let i = 0;
+ im.oninputcontextchange = function(evt) {
+ var inputcontext = navigator.mozInputMethod.inputcontext;
+
+ i++;
+ switch (i) {
+ case 1:
+ ok(!!inputcontext, '1) Receving the input context');
+ is(inputcontext.inputType, 'text', '1) input type');
+ is(im.mgmt.supportsSwitching(), true, '1) supports switching');
+
+ mm.sendAsyncMessage('test:next');
+ break;
+
+ case 2:
+ is(inputcontext, null, '2) Receving null inputcontext');
+
+ break;
+
+ case 3:
+ ok(!!inputcontext, '3) Receving the input context');
+ is(inputcontext.inputType, 'number', '3) input type');
+ is(im.mgmt.supportsSwitching(), false, '3) supports switching');
+
+ mm.sendAsyncMessage('test:next');
+ break;
+
+ case 4:
+ is(inputcontext, null, '4) Receving null inputcontext');
+
+ break;
+
+ case 5:
+ ok(!!inputcontext, '5) Receving the input context');
+ is(inputcontext.inputType, 'password', '5) input type');
+ is(im.mgmt.supportsSwitching(), true, '5) supports switching');
+
+ mm.sendAsyncMessage('test:next');
+ break;
+
+ case 6:
+ is(inputcontext, null, '6) Receving null inputcontext');
+ is(im.mgmt.supportsSwitching(), false, '6) supports switching');
+
+ inputmethod_cleanup();
+ break;
+
+ default:
+ ok(false, 'Receving extra inputcontextchange calls');
+ inputmethod_cleanup();
+
+ break;
+ }
+ };
+
+ // Set current page as an input method.
+ SpecialPowers.wrap(im).setActive(true);
+ // Set text and password inputs as supports switching (and not supported for number type)
+ im.mgmt.setSupportsSwitchingTypes(['text', 'password']);
+
+ let iframe = document.createElement('iframe');
+ iframe.src = 'file_test_setSupportsSwitching.html';
+ iframe.setAttribute('mozbrowser', true);
+ document.body.appendChild(iframe);
+
+ let mm = SpecialPowers.getBrowserFrameMessageManager(iframe);
+
+ iframe.addEventListener('mozbrowserloadend', function() {
+ mm.loadFrameScript('data:,(' + encodeURIComponent(appFrameScript.toString()) + ')();', false);
+ });
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/inputmethod/mochitest/test_simple_manage_events.html b/dom/inputmethod/mochitest/test_simple_manage_events.html
new file mode 100644
index 000000000..bbcac498d
--- /dev/null
+++ b/dom/inputmethod/mochitest/test_simple_manage_events.html
@@ -0,0 +1,154 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1201407
+-->
+<head>
+ <title>Test simple manage notification events on MozInputMethodManager</title>
+ <script type="application/javascript;version=1.7" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript;version=1.7" src="inputmethod_common.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=1201407">Mozilla Bug 1201407</a>
+<p id="display"></p>
+<pre id="test">
+<script class="testbody" type="application/javascript;version=1.7">
+
+let appFrameMM;
+let nextStep;
+
+function setupTestRunner() {
+ info('setupTestRunner');
+ let im = navigator.mozInputMethod;
+
+ let i = 0;
+ im.mgmt.onshowallrequest =
+ im.mgmt.onnextrequest = nextStep = function(evt) {
+ i++;
+ switch (i) {
+ case 1:
+ is(evt.type, 'inputcontextchange', '1) inputcontextchange event');
+ appFrameMM.sendAsyncMessage('test:callShowAll');
+
+ break;
+
+ case 2:
+ is(evt.type, 'showallrequest', '2) showallrequest event');
+ ok(evt.target, im.mgmt, '2) evt.target');
+ evt.preventDefault();
+
+ appFrameMM.sendAsyncMessage('test:callNext');
+
+ break;
+
+ case 3:
+ is(evt.type, 'nextrequest', '3) nextrequest event');
+ ok(evt.target, im.mgmt, '3) evt.target');
+ evt.preventDefault();
+
+ im.mgmt.onshowallrequest =
+ im.mgmt.onnextrequest = nextStep = null;
+
+ inputmethod_cleanup();
+ break;
+
+ default:
+ ok(false, 'Receving extra events');
+ inputmethod_cleanup();
+
+ break;
+ }
+ };
+}
+
+function setupInputAppFrame() {
+ info('setupInputAppFrame');
+ return new Promise((resolve, reject) => {
+ let appFrameScript = function appFrameScript() {
+ let im = content.navigator.mozInputMethod;
+
+ addMessageListener('test:callShowAll', function() {
+ im.mgmt.showAll();
+ });
+
+ addMessageListener('test:callNext', function() {
+ im.mgmt.next();
+ });
+
+ im.mgmt.onshowallrequest =
+ im.mgmt.onnextrequest = function(evt) {
+ sendAsyncMessage('test:appEvent', { type: evt.type });
+ };
+
+ im.oninputcontextchange = function(evt) {
+ sendAsyncMessage('test:inputcontextchange', {});
+ };
+
+ content.document.body.textContent = 'I am a input app';
+ };
+
+ let path = location.pathname;
+ let basePath = location.protocol + '//' + location.host +
+ path.substring(0, path.lastIndexOf('/'));
+ let imeUrl = basePath + '/file_blank.html';
+
+ let inputAppFrame = document.createElement('iframe');
+ inputAppFrame.setAttribute('mozbrowser', true);
+ inputAppFrame.src = imeUrl;
+ document.body.appendChild(inputAppFrame);
+
+ let mm = appFrameMM =
+ SpecialPowers.getBrowserFrameMessageManager(inputAppFrame);
+
+ inputAppFrame.addEventListener('mozbrowserloadend', function() {
+ mm.addMessageListener('test:appEvent', function(msg) {
+ ok(false, 'Input app should not receive ' + msg.data.type + ' event.');
+ });
+ mm.addMessageListener('test:inputcontextchange', function(msg) {
+ nextStep && nextStep({ type: 'inputcontextchange' });
+ });
+ mm.loadFrameScript('data:,(' + encodeURIComponent(appFrameScript.toString()) + ')();', false);
+
+ // Set the input app frame to be active
+ let req = inputAppFrame.setInputMethodActive(true);
+ resolve(req);
+ });
+ });
+}
+
+function setupContentFrame() {
+ let contentFrameScript = function contentFrameScript() {
+ let input = content.document.body.firstElementChild;
+
+ input.focus();
+ };
+
+ let iframe = document.createElement('iframe');
+ iframe.src = 'file_test_simple_manage_events.html';
+ iframe.setAttribute('mozbrowser', true);
+ document.body.appendChild(iframe);
+
+ let mm = SpecialPowers.getBrowserFrameMessageManager(iframe);
+
+ iframe.addEventListener('mozbrowserloadend', function() {
+ mm.loadFrameScript('data:,(' + encodeURIComponent(contentFrameScript.toString()) + ')();', false);
+ });
+}
+
+inputmethod_setup(function() {
+ Promise.resolve()
+ .then(() => setupTestRunner())
+ .then(() => setupContentFrame())
+ .then(() => setupInputAppFrame())
+ .catch((e) => {
+ ok(false, 'Error' + e.toString());
+ console.error(e);
+ });
+});
+
+</script>
+</pre>
+</body>
+</html>
+
diff --git a/dom/inputmethod/mochitest/test_sync_edit.html b/dom/inputmethod/mochitest/test_sync_edit.html
new file mode 100644
index 000000000..a32ea23fa
--- /dev/null
+++ b/dom/inputmethod/mochitest/test_sync_edit.html
@@ -0,0 +1,81 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1079455
+-->
+<head>
+ <title>Sync edit of an input</title>
+ <script type="application/javascript;version=1.7" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript;version=1.7" src="inputmethod_common.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=1079455">Mozilla Bug 1079455</a>
+<p id="display"></p>
+<pre id="test">
+<script class="testbody" type="application/javascript;version=1.7">
+
+inputmethod_setup(function() {
+ runTest();
+});
+
+let appFrameScript = function appFrameScript() {
+ let input = content.document.body.firstElementChild;
+
+ input.focus();
+ input.value = 'First1';
+ input.blur();
+};
+
+function runTest() {
+ let im = navigator.mozInputMethod;
+
+ let i = 0;
+ im.oninputcontextchange = function() {
+ let inputcontext = im.inputcontext;
+ i++;
+ switch (i) {
+ case 1:
+ ok(!!inputcontext, 'Should receive inputcontext from focus().');
+ is(inputcontext.textAfterCursor, 'First');
+
+ break;
+
+ case 2:
+ ok(!!inputcontext, 'Should receive inputcontext from value change.');
+ is(inputcontext.textBeforeCursor, 'First1');
+
+ break;
+
+ case 3:
+ ok(!inputcontext, 'Should lost inputcontext from blur().');
+
+ inputmethod_cleanup();
+ break;
+
+ default:
+ ok(false, 'Unknown event count.');
+
+ inputmethod_cleanup();
+ }
+ };
+
+ // Set current page as an input method.
+ SpecialPowers.wrap(im).setActive(true);
+
+ let iframe = document.createElement('iframe');
+ iframe.src = 'file_test_sync_edit.html';
+ iframe.setAttribute('mozbrowser', true);
+ document.body.appendChild(iframe);
+
+ let mm = SpecialPowers.getBrowserFrameMessageManager(iframe);
+ iframe.addEventListener('mozbrowserloadend', function() {
+ mm.loadFrameScript('data:,(' + encodeURIComponent(appFrameScript.toString()) + ')();', false);
+ });
+}
+
+</script>
+</pre>
+</body>
+</html>
+
diff --git a/dom/inputmethod/mochitest/test_two_inputs.html b/dom/inputmethod/mochitest/test_two_inputs.html
new file mode 100644
index 000000000..e0f007cbc
--- /dev/null
+++ b/dom/inputmethod/mochitest/test_two_inputs.html
@@ -0,0 +1,184 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1057898
+https://bugzilla.mozilla.org/show_bug.cgi?id=952741
+-->
+<head>
+ <title>Test switching between two inputs</title>
+ <script type="application/javascript;version=1.7" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript;version=1.7" src="inputmethod_common.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=1057898">Mozilla Bug 1057898</a>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=952741">Mozilla Bug 952741</a>
+<p id="display"></p>
+<pre id="test">
+<script class="testbody" type="application/javascript;version=1.7">
+
+inputmethod_setup(function() {
+ runTest();
+});
+
+let appFrameScript = function appFrameScript() {
+ let input1 = content.document.body.firstElementChild;
+ let input2 = content.document.body.children[1];
+
+ let i = 1;
+
+ input1.focus();
+
+ addMessageListener('test:next', function() {
+ i++;
+ switch (i) {
+ case 2:
+ input2.focus();
+ i++; // keep the same count with the parent frame.
+
+ break;
+
+ case 4:
+ input2.blur();
+
+ break;
+
+ case 5:
+ input2.focus();
+
+ break;
+
+ case 6:
+ input1.focus();
+ i++; // keep the same count with the parent frame.
+
+ break;
+
+ case 8:
+ content.document.body.removeChild(input1);
+
+ break;
+
+ case 9:
+ input2.focus();
+
+ break;
+
+ case 10:
+ content.document.body.removeChild(input2);
+
+ break;
+ }
+ });
+};
+
+function runTest() {
+ let im = navigator.mozInputMethod;
+
+ let i = 0;
+ im.oninputcontextchange = function(evt) {
+ var inputcontext = navigator.mozInputMethod.inputcontext;
+
+ i++;
+ switch (i) {
+ // focus on the first input receives the first input context.
+ case 1:
+ ok(!!inputcontext, '1) Receving the first input context');
+ is(inputcontext.textAfterCursor, 'First');
+
+ mm.sendAsyncMessage('test:next');
+ break;
+
+ // focus on the second input should implicitly blur the first input
+ case 2:
+ is(inputcontext, null, '2) Receving null inputcontext');
+
+ break;
+
+ // ... and results the second input context.
+ case 3:
+ ok(!!inputcontext, '3) Receving the second input context');
+ is(inputcontext.textAfterCursor, 'Second');
+
+ mm.sendAsyncMessage('test:next');
+ break;
+
+ // blur on the second input results null input context
+ case 4:
+ is(inputcontext, null, '4) Receving null inputcontext');
+
+ mm.sendAsyncMessage('test:next');
+ break;
+
+ // focus on the second input receives the second input context.
+ case 5:
+ ok(!!inputcontext, '5) Receving the second input context');
+ is(inputcontext.textAfterCursor, 'Second');
+
+ mm.sendAsyncMessage('test:next');
+ break;
+
+ // focus on the second input should implicitly blur the first input
+ case 6:
+ is(inputcontext, null, '6) Receving null inputcontext');
+
+ break;
+
+ // ... and results the second input context.
+ case 7:
+ ok(!!inputcontext, '7) Receving the first input context');
+ is(inputcontext.textAfterCursor, 'First');
+
+ mm.sendAsyncMessage('test:next');
+ break;
+
+ // remove on the first focused input results null input context
+ case 8:
+ is(inputcontext, null, '8) Receving null inputcontext');
+
+ mm.sendAsyncMessage('test:next');
+ break;
+
+ // input context for the second input.
+ case 9:
+ ok(!!inputcontext, '9) Receving the second input context');
+ is(inputcontext.textAfterCursor, 'Second');
+
+ mm.sendAsyncMessage('test:next');
+ break;
+
+ // remove on the second focused input results null input context
+ case 10:
+ is(inputcontext, null, '10) Receving null inputcontext');
+
+ inputmethod_cleanup();
+ break;
+
+ default:
+ ok(false, 'Receving extra inputcontextchange calls');
+ inputmethod_cleanup();
+
+ break;
+ }
+ };
+
+ // Set current page as an input method.
+ SpecialPowers.wrap(im).setActive(true);
+
+ let iframe = document.createElement('iframe');
+ iframe.src = 'file_test_two_inputs.html';
+ iframe.setAttribute('mozbrowser', true);
+ document.body.appendChild(iframe);
+
+ let mm = SpecialPowers.getBrowserFrameMessageManager(iframe);
+
+ iframe.addEventListener('mozbrowserloadend', function() {
+ mm.loadFrameScript('data:,(' + encodeURIComponent(appFrameScript.toString()) + ')();', false);
+ });
+}
+
+</script>
+</pre>
+</body>
+</html>
+
diff --git a/dom/inputmethod/mochitest/test_two_selects.html b/dom/inputmethod/mochitest/test_two_selects.html
new file mode 100644
index 000000000..9869b8c14
--- /dev/null
+++ b/dom/inputmethod/mochitest/test_two_selects.html
@@ -0,0 +1,182 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1079728
+-->
+<head>
+ <title>Test switching between two inputs</title>
+ <script type="application/javascript;version=1.7" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript;version=1.7" src="inputmethod_common.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=1079728">Mozilla Bug 1079728</a>
+<p id="display"></p>
+<pre id="test">
+<script class="testbody" type="application/javascript;version=1.7">
+
+inputmethod_setup(function() {
+ runTest();
+});
+
+let appFrameScript = function appFrameScript() {
+ let select1 = content.document.body.firstElementChild;
+ let select2 = content.document.body.children[1];
+
+ let i = 1;
+
+ select1.focus();
+
+ addMessageListener('test:next', function() {
+ i++;
+ switch (i) {
+ case 2:
+ select2.focus();
+ i++; // keep the same count with the parent frame.
+
+ break;
+
+ case 4:
+ select2.blur();
+
+ break;
+
+ case 5:
+ select2.focus();
+
+ break;
+
+ case 6:
+ select1.focus();
+ i++; // keep the same count with the parent frame.
+
+ break;
+
+ case 8:
+ content.document.body.removeChild(select1);
+
+ break;
+
+ case 9:
+ select2.focus();
+
+ break;
+
+ case 10:
+ content.document.body.removeChild(select2);
+
+ break;
+ }
+ });
+};
+
+function runTest() {
+ let im = navigator.mozInputMethod;
+
+ let i = 0;
+ im.oninputcontextchange = function(evt) {
+ var inputcontext = navigator.mozInputMethod.inputcontext;
+
+ i++;
+ switch (i) {
+ // focus on the first input receives the first input context.
+ case 1:
+ ok(!!inputcontext, '1) Receving the first input context');
+ is(inputcontext.textAfterCursor, 'First');
+
+ mm.sendAsyncMessage('test:next');
+ break;
+
+ // focus on the second input should implicitly blur the first input
+ case 2:
+ is(inputcontext, null, '2) Receving null inputcontext');
+
+ break;
+
+ // ... and results the second input context.
+ case 3:
+ ok(!!inputcontext, '3) Receving the second input context');
+ is(inputcontext.textAfterCursor, 'Second');
+
+ mm.sendAsyncMessage('test:next');
+ break;
+
+ // blur on the second input results null input context
+ case 4:
+ is(inputcontext, null, '4) Receving null inputcontext');
+
+ mm.sendAsyncMessage('test:next');
+ break;
+
+ // focus on the second input receives the second input context.
+ case 5:
+ ok(!!inputcontext, '5) Receving the second input context');
+ is(inputcontext.textAfterCursor, 'Second');
+
+ mm.sendAsyncMessage('test:next');
+ break;
+
+ // focus on the second input should implicitly blur the first input
+ case 6:
+ is(inputcontext, null, '6) Receving null inputcontext');
+
+ break;
+
+ // ... and results the second input context.
+ case 7:
+ ok(!!inputcontext, '7) Receving the first input context');
+ is(inputcontext.textAfterCursor, 'First');
+
+ mm.sendAsyncMessage('test:next');
+ break;
+
+ // remove on the first focused input results null input context
+ case 8:
+ is(inputcontext, null, '8) Receving null inputcontext');
+
+ mm.sendAsyncMessage('test:next');
+ break;
+
+ // input context for the second input.
+ case 9:
+ ok(!!inputcontext, '9) Receving the second input context');
+ is(inputcontext.textAfterCursor, 'Second');
+
+ mm.sendAsyncMessage('test:next');
+ break;
+
+ // remove on the second focused input results null input context
+ case 10:
+ is(inputcontext, null, '10) Receving null inputcontext');
+
+ inputmethod_cleanup();
+ break;
+
+ default:
+ ok(false, 'Receving extra inputcontextchange calls');
+ inputmethod_cleanup();
+
+ break;
+ }
+ };
+
+ // Set current page as an input method.
+ SpecialPowers.wrap(im).setActive(true);
+
+ let iframe = document.createElement('iframe');
+ iframe.src = 'file_test_two_selects.html';
+ iframe.setAttribute('mozbrowser', true);
+ document.body.appendChild(iframe);
+
+ let mm = SpecialPowers.getBrowserFrameMessageManager(iframe);
+
+ iframe.addEventListener('mozbrowserloadend', function() {
+ mm.loadFrameScript('data:,(' + encodeURIComponent(appFrameScript.toString()) + ')();', false);
+ });
+}
+
+</script>
+</pre>
+</body>
+</html>
+
diff --git a/dom/inputmethod/mochitest/test_unload.html b/dom/inputmethod/mochitest/test_unload.html
new file mode 100644
index 000000000..bef9d1485
--- /dev/null
+++ b/dom/inputmethod/mochitest/test_unload.html
@@ -0,0 +1,167 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1122463
+https://bugzilla.mozilla.org/show_bug.cgi?id=820057
+-->
+<head>
+ <title>Test focus when page unloads</title>
+ <script type="application/javascript;version=1.7" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript;version=1.7" src="inputmethod_common.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=1122463">Mozilla Bug 1122463</a>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=820057">Mozilla Bug 820057</a>
+<p id="display"></p>
+<pre id="test">
+<script class="testbody" type="application/javascript;version=1.7">
+
+inputmethod_setup(function() {
+ runTest();
+});
+
+let appFrameScript = function appFrameScript() {
+ let form1 = content.document.body.firstElementChild;
+ let input1 = form1.firstElementChild;
+ let submit1 = form1.lastElementChild;
+ let input2;
+
+ let cancelSubmit = function(evt) {
+ evt.preventDefault();
+ };
+
+ // Content of the second page.
+ form1.action = 'file_test_unload_action.html';
+
+ let i = 1;
+
+ input1.focus();
+
+ addMessageListener('test:next', function() {
+ i++;
+ switch (i) {
+ case 2:
+ // Click the submit button, trigger the submit event and make our
+ // installed event listener preventing the submission.
+ form1.addEventListener('submit', cancelSubmit);
+ submit1.click();
+
+ sendAsyncMessage('test:step');
+
+ break;
+
+ case 3:
+ // Actually submit the form.
+ form1.removeEventListener('submit', cancelSubmit);
+ submit1.click();
+
+ break;
+
+ case 4:
+ if (!content.document.body) {
+ content.onload = function() {
+ content.onload = null;
+
+ let input2 = content.document.body.firstElementChild;
+ input2.focus();
+ };
+
+ return;
+ }
+
+ input2 = content.document.body.firstElementChild;
+ input2.focus();
+
+ break;
+
+ case 5:
+ content.location.href = 'data:text/html,Hello!';
+
+ break;
+ }
+ });
+};
+
+function runTest() {
+ let im = navigator.mozInputMethod;
+
+ let i = 0;
+ function nextStep() {
+ let inputcontext = navigator.mozInputMethod.inputcontext;
+
+ i++;
+ switch (i) {
+ // focus on the first input receives the first input context.
+ case 1:
+ ok(!!inputcontext, '1) Receving the first input context');
+ is(inputcontext.textAfterCursor, 'First');
+
+ mm.sendAsyncMessage('test:next');
+ break;
+
+ // Cancelled submission should not cause us lost focus.
+ case 2:
+ ok(!!inputcontext, '2) Receving the first input context');
+ is(inputcontext.textAfterCursor, 'First');
+
+ mm.sendAsyncMessage('test:next');
+ break;
+
+ // Real submit and page transition should cause us lost focus.
+ // XXX: Unless we could delay the page transition, we does not know if
+ // the inputcontext is lost because of the submit or the pagehide/beforeload
+ // event.
+ case 3:
+ is(inputcontext, null, '3) Receving null inputcontext');
+
+ mm.sendAsyncMessage('test:next');
+
+ break;
+
+ // Regaining focus of input in the second page.
+ case 4:
+ ok(!!inputcontext, '4) Receving the second input context');
+ is(inputcontext.textAfterCursor, 'Second');
+
+ mm.sendAsyncMessage('test:next');
+
+ break;
+
+ // Page transition should cause us lost focus
+ case 5:
+ is(inputcontext, null, '5) Receving null inputcontext');
+
+ inputmethod_cleanup();
+
+ break;
+ }
+ }
+
+ // Set current page as an input method.
+ SpecialPowers.wrap(im).setActive(true);
+
+ let iframe = document.createElement('iframe');
+ iframe.src = 'file_test_unload.html';
+ iframe.setAttribute('mozbrowser', true);
+ document.body.appendChild(iframe);
+
+ let mm = SpecialPowers.getBrowserFrameMessageManager(iframe);
+ im.oninputcontextchange = nextStep;
+
+ let frameScriptLoaded = false;
+ iframe.addEventListener('mozbrowserloadend', function() {
+ if (frameScriptLoaded)
+ return;
+
+ frameScriptLoaded = true;
+ mm.addMessageListener('test:step', nextStep);
+ mm.loadFrameScript('data:,(' + encodeURIComponent(appFrameScript.toString()) + ')();', false);
+ });
+}
+
+</script>
+</pre>
+</body>
+</html>
+
diff --git a/dom/inputmethod/moz.build b/dom/inputmethod/moz.build
new file mode 100644
index 000000000..504e2ebfc
--- /dev/null
+++ b/dom/inputmethod/moz.build
@@ -0,0 +1,41 @@
+# -*- 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/.
+
+if CONFIG['MOZ_B2G']:
+ XPIDL_SOURCES += [
+ 'nsIHardwareKeyHandler.idl',
+ ]
+
+ XPIDL_MODULE = 'inputmethod'
+
+ EXPORTS.mozilla += [
+ 'HardwareKeyHandler.h',
+ ]
+
+ SOURCES += [
+ 'HardwareKeyHandler.cpp'
+ ]
+
+ include('/ipc/chromium/chromium-config.mozbuild')
+
+ FINAL_LIBRARY = 'xul'
+ LOCAL_INCLUDES += [
+ '/dom/base',
+ '/layout/base',
+ ]
+
+EXTRA_COMPONENTS += [
+ 'InputMethod.manifest',
+ 'MozKeyboard.js',
+]
+
+EXTRA_PP_JS_MODULES += [
+ 'Keyboard.jsm',
+]
+
+JAR_MANIFESTS += ['jar.mn']
+
+MOCHITEST_CHROME_MANIFESTS += ['mochitest/chrome.ini']
diff --git a/dom/inputmethod/nsIHardwareKeyHandler.idl b/dom/inputmethod/nsIHardwareKeyHandler.idl
new file mode 100644
index 000000000..5bce4d980
--- /dev/null
+++ b/dom/inputmethod/nsIHardwareKeyHandler.idl
@@ -0,0 +1,142 @@
+/* -*- 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 "nsISupports.idl"
+
+interface nsIDOMKeyEvent;
+
+%{C++
+#define NS_HARDWARE_KEY_HANDLER_CID \
+ { 0xfb45921b, 0xe0a5, 0x45c6, \
+ { 0x90, 0xd0, 0xa6, 0x97, 0xa7, 0x72, 0xc4, 0x2a } }
+#define NS_HARDWARE_KEY_HANDLER_CONTRACTID \
+ "@mozilla.org/HardwareKeyHandler;1"
+
+#include "mozilla/EventForwards.h" /* For nsEventStatus */
+
+namespace mozilla {
+class WidgetKeyboardEvent;
+}
+
+using mozilla::WidgetKeyboardEvent;
+
+class nsINode;
+%}
+
+/**
+ * This interface is used to be registered to the nsIHardwareKeyHandler through
+ * |nsIHardwareKeyHandler.registerListener|.
+ */
+[scriptable, function, uuid(cd5aeee3-b4b9-459d-85e7-c0671c7a8a2e)]
+interface nsIHardwareKeyEventListener : nsISupports
+{
+ /**
+ * This method will be invoked by nsIHardwareKeyHandler to forward the native
+ * keyboard event to the active input method
+ */
+ bool onHardwareKey(in nsIDOMKeyEvent aEvent);
+};
+
+/**
+ * This interface has two main roles. One is to send a hardware keyboard event
+ * to the active input method app and the other is to receive its reply result.
+ * If a keyboard event is triggered from a hardware keyboard when an editor has
+ * focus, the event target should be the editor. However, the text input
+ * processor algorithm is implemented in an input method app and it should
+ * handle the event earlier than the real event target to do the mapping such
+ * as character conversion according to the language setting or the type of a
+ * hardware keyboard.
+ */
+[scriptable, builtinclass, uuid(25b34270-caad-4d18-a910-860351690639)]
+interface nsIHardwareKeyHandler : nsISupports
+{
+ /**
+ * Flags used to set the defaultPrevented's result. The default result
+ * from input-method-app should be set to NO_DEFAULT_PREVENTED.
+ * (It means the forwarded event isn't consumed by input-method-app.)
+ * If the input-method-app consumes the forwarded event,
+ * then the result should be set by DEFAULT_PREVENTED* before reply.
+ */
+ const unsigned short NO_DEFAULT_PREVENTED = 0x0000;
+ const unsigned short DEFAULT_PREVENTED = 0x0001;
+ const unsigned short DEFAULT_PREVENTED_BY_CHROME = 0x0002;
+ const unsigned short DEFAULT_PREVENTED_BY_CONTENT = 0x0004;
+
+ /**
+ * Registers a listener in input-method-app to receive
+ * the forwarded hardware keyboard events
+ *
+ * @param aListener Listener object to be notified for receiving
+ * the keyboard event fired from hardware
+ * @note A listener object must implement
+ * nsIHardwareKeyEventListener and
+ * nsSupportsWeakReference
+ * @see nsIHardwareKeyEventListener
+ * @see nsSupportsWeakReference
+ */
+ void registerListener(in nsIHardwareKeyEventListener aListener);
+
+ /**
+ * Unregisters the current listener from input-method-app
+ */
+ void unregisterListener();
+
+ /**
+ * Notifies nsIHardwareKeyHandler that input-method-app is active.
+ */
+ void onInputMethodAppConnected();
+
+ /**
+ * Notifies nsIHardwareKeyHandler that input-method-app is disabled.
+ */
+ void onInputMethodAppDisconnected();
+
+ /**
+ * Input-method-app will pass the processing result that the forwarded
+ * event is handled or not through this method, and the nsIHardwareKeyHandler
+ * can use this to receive the reply of |forwardKeyToInputMethodApp|
+ * from the active input method.
+ *
+ * The result should contain the original event type and the info whether
+ * the default is prevented, also, it is prevented by chrome or content.
+ *
+ * @param aEventType The type of an original event.
+ * @param aDefaultPrevented State that |evt.preventDefault|
+ * is called by content, chrome or not.
+ */
+ void onHandledByInputMethodApp(in DOMString aType,
+ in unsigned short aDefaultPrevented);
+
+ /**
+ * Sends the native keyboard events triggered from hardware to the
+ * active input method before dispatching to its event target.
+ * This method only forwards keydown and keyup events.
+ * If the event isn't allowed to be forwarded, we should continue the
+ * normal event processing. For those forwarded keydown and keyup events
+ * We will pause the further event processing to wait for the completion
+ * of the event handling in the active input method app.
+ * Once |onHandledByInputMethodApp| is called by the input method app,
+ * the pending event processing can be resumed according to its reply.
+ * On the other hand, the keypress will never be sent to the input-method-app.
+ * Depending on whether the keydown's reply arrives before the keypress event
+ * comes, the keypress event will be handled directly or pushed into
+ * the event queue to wait for its heading keydown's reply.
+ *
+ * This implementation will call |nsIHardwareKeyEventListener.onHardwareKey|,
+ * which is registered through |nsIHardwareKeyEventListener.registerListener|,
+ * to forward the events.
+ *
+ * Returns true, if the event is handled in this module.
+ * Returns false, otherwise.
+ *
+ * If it returns false, we should continue the normal event processing.
+ */
+ %{C++
+ virtual bool ForwardKeyToInputMethodApp(nsINode* aTarget,
+ WidgetKeyboardEvent* aEvent,
+ nsEventStatus* aEventStatus) = 0;
+ %}
+};