Adds event dispatch extension methods to DOMImplStandard

Fixes issue 8379

Change-Id: I7df2678ab26e703b3db437c81ccc8d768b44917e
diff --git a/user/src/com/google/gwt/user/client/impl/DOMImplStandard.java b/user/src/com/google/gwt/user/client/impl/DOMImplStandard.java
index 6bd383e..f4fb29a 100644
--- a/user/src/com/google/gwt/user/client/impl/DOMImplStandard.java
+++ b/user/src/com/google/gwt/user/client/impl/DOMImplStandard.java
@@ -15,6 +15,7 @@
  */
 package com.google.gwt.user.client.impl;
 
+import com.google.gwt.core.client.GWT;
 import com.google.gwt.core.client.JavaScriptObject;
 import com.google.gwt.dom.client.BrowserEvents;
 import com.google.gwt.dom.client.Element;
@@ -26,13 +27,79 @@
  * by those browsers that come a bit closer to supporting a common standard (ie,
  * not legacy IEs).
  */
-abstract class DOMImplStandard extends DOMImpl {
+public abstract class DOMImplStandard extends DOMImpl {
+
+  /**
+   * Adds custom bitless event dispatchers to GWT. If no specific event dispatcher supplied for an
+   * event, the default dispatcher is used.
+   * <p> Example usage:
+   * <pre>
+   * static {
+   *   DOMImplStandard.addBitlessEventDispatchers(getMyCustomDispatchers());
+   * }
+   *
+   * private static native JavaScriptObject getMyCustomDispatchers() /*-{
+   *   return {
+   *     click: @com.xxx.YYY::myCustomDispatcher(*),
+   *     ...
+   *   };
+   * }-* /;
+   * </pre>
+   *
+   * <p> Note that although this method is public for extensions, it is subject to change in
+   * different releases.
+   *
+   * @param eventMap an object that provides dispatching methods keyed with the name of the event
+   */
+  public static void addBitlessEventDispatchers(JavaScriptObject eventMap) {
+    ensureInit();
+    bitlessEventDispatchers.merge(eventMap);
+  }
+
+  /**
+   * Adds custom capture event dispatchers to GWT.
+   * <p> Example usage:
+   * <pre>
+   * static {
+   *   if (isIE10Plus())) {
+   *     DOMImplStandard.addCaptureEventDispatchers(getMsPointerCaptureDispatchers());
+   *   }
+   * }
+   *
+   * private static native JavaScriptObject getMsPointerCaptureDispatchers() /*-{
+   *   return {
+   *     MSPointerDown: @com.google.gwt.user.client.impl.DOMImplStandard::dispatchCapturedMouseEvent(*),
+   *     MSPointerUp:   @com.google.gwt.user.client.impl.DOMImplStandard::dispatchCapturedMouseEvent(*),
+   *     ...
+   *   };
+   * }-* /;
+   * </pre>
+   *
+   * <p> Note that although this method is public for extensions, it is subject to change in
+   * different releases.
+   *
+   * @param eventMap an object that provides dispatching methods keyed with the name of the event
+   */
+  public static void addCaptureEventDispatchers(JavaScriptObject eventMap) {
+    ensureInit();
+    captureEventDispatchers.merge(eventMap);
+  }
+
+  private static void ensureInit() {
+    if (eventSystemIsInitialized) {
+      throw new IllegalStateException("Event system already initialized");
+    }
+
+    // Ensure that any default extensions for the browser is registered via
+    // static initializers in deferred binding of DOMImpl:
+    GWT.create(DOMImpl.class);
+  }
 
   private static Element captureElem;
 
-  private static JavaScriptObject bitlessEventDispatchers = getBitlessEventDispatchers();
+  private static EventMap bitlessEventDispatchers = getBitlessEventDispatchers();
 
-  private static JavaScriptObject captureEventDispatchers = getCaptureEventDispatchers();
+  private static EventMap captureEventDispatchers = getCaptureEventDispatchers();
 
   @Deprecated // We no longer want any external JSNI dependencies
   private static JavaScriptObject dispatchEvent;
@@ -159,7 +226,7 @@
     @com.google.gwt.user.client.impl.DOMImplStandard::dispatchUnhandledEvent =
         $entry(@com.google.gwt.user.client.impl.DOMImplStandard::dispatchUnhandledEvent(*));
 
-    var foreach = @com.google.gwt.user.client.impl.DOMImplStandard::foreach(*);
+    var foreach = @com.google.gwt.user.client.impl.EventMap::foreach(*);
 
     // Ensure $entry for bitless event dispatchers
     var bitlessEvents = @com.google.gwt.user.client.impl.DOMImplStandard::bitlessEventDispatchers;
@@ -175,7 +242,7 @@
 
   @Override
   protected native void disposeEventSystem() /*-{
-    var foreach = @com.google.gwt.user.client.impl.DOMImplStandard::foreach(*);
+    var foreach = @com.google.gwt.user.client.impl.EventMap::foreach(*);
 
     // Remove capture event listeners
     foreach(captureEvents, function(e, fn) { $wnd.removeEventListener(e, fn, true); });
@@ -290,7 +357,7 @@
     }
   }
 
-  private static native JavaScriptObject getBitlessEventDispatchers() /*-{
+  private static native EventMap getBitlessEventDispatchers() /*-{
     return {
       _default_: @com.google.gwt.user.client.impl.DOMImplStandard::dispatchEvent(*),
       dragenter: @com.google.gwt.user.client.impl.DOMImplStandard::dispatchDragEvent(*),
@@ -298,7 +365,7 @@
     };
   }-*/;
 
-  private static native JavaScriptObject getCaptureEventDispatchers() /*-{
+  private static native EventMap getCaptureEventDispatchers() /*-{
     return {
       // Mouse events
       click:      @com.google.gwt.user.client.impl.DOMImplStandard::dispatchCapturedMouseEvent(*),
@@ -325,12 +392,4 @@
       gesturechange:@com.google.gwt.user.client.impl.DOMImplStandard::dispatchCapturedMouseEvent(*),
     };
   }-*/;
-
-  private static native void foreach(JavaScriptObject map, JavaScriptObject fn) /*-{
-    for (var e in map) {
-      if (map.hasOwnProperty(e)) {
-        fn(e, map[e]);
-      }
-    }
-  }-*/;
 }
diff --git a/user/src/com/google/gwt/user/client/impl/EventMap.java b/user/src/com/google/gwt/user/client/impl/EventMap.java
new file mode 100644
index 0000000..a32ae65
--- /dev/null
+++ b/user/src/com/google/gwt/user/client/impl/EventMap.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2013 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.user.client.impl;
+
+import com.google.gwt.core.client.JavaScriptObject;
+
+/**
+ * A simple helper class to abstract event maps.
+ */
+class EventMap extends JavaScriptObject {
+
+  protected EventMap() { /* Mandatory constructor for JSO */}
+
+  /**
+   * Merges the key-value pairs from the parameter into this object. If a key already exists, then
+   * it will be overridden by the new value.
+   */
+  public final void merge(JavaScriptObject eventMap) {
+    foreach(eventMap, copyTo(this));
+  }
+
+  /**
+   * Returns a function that copies the passed key-value to target object.
+   */
+  private static native JavaScriptObject copyTo(EventMap target) /*-{
+    return function(key, value) { target[key] = value; };
+  }-*/;
+
+  /**
+   * Executes a provided function over the key-value pairs of the map object.
+   */
+  static native void foreach(JavaScriptObject map, JavaScriptObject fn) /*-{
+    for (var e in map) {
+      if (map.hasOwnProperty(e)) {
+        fn(e, map[e]);
+      }
+    }
+  }-*/;
+}
diff --git a/user/test/com/google/gwt/user/CustomEventsTest.gwt.xml b/user/test/com/google/gwt/user/CustomEventsTest.gwt.xml
new file mode 100644
index 0000000..7ed3833
--- /dev/null
+++ b/user/test/com/google/gwt/user/CustomEventsTest.gwt.xml
@@ -0,0 +1,17 @@
+<!--                                                                        -->
+<!-- Copyright 2013 Google Inc.                                             -->
+<!-- Licensed under the Apache License, Version 2.0 (the "License"); you    -->
+<!-- may not use this file except in compliance with the License. You may   -->
+<!-- may obtain a copy of the License at                                    -->
+<!--                                                                        -->
+<!-- http://www.apache.org/licenses/LICENSE-2.0                             -->
+<!--                                                                        -->
+<!-- Unless required by applicable law or agreed to in writing, software    -->
+<!-- distributed under the License is distributed on an "AS IS" BASIS,      -->
+<!-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or        -->
+<!-- implied. License for the specific language governing permissions and   -->
+<!-- limitations under the License.                                         -->
+
+<module type="fileset">
+	<inherits name="com.google.gwt.user.User"/>
+</module>
diff --git a/user/test/com/google/gwt/user/UiPart1Suite.java b/user/test/com/google/gwt/user/UiPart1Suite.java
index dba4009..9052013 100644
--- a/user/test/com/google/gwt/user/UiPart1Suite.java
+++ b/user/test/com/google/gwt/user/UiPart1Suite.java
@@ -16,6 +16,7 @@
 package com.google.gwt.user;
 
 import com.google.gwt.junit.tools.GWTTestSuite;
+import com.google.gwt.user.client.CustomEventsTest;
 import com.google.gwt.user.client.ui.AbsolutePanelTest;
 import com.google.gwt.user.client.ui.AnchorTest;
 import com.google.gwt.user.client.ui.ButtonTest;
@@ -74,6 +75,7 @@
     suite.addTestSuite(CompositeTest.class);
     suite.addTestSuite(CreateEventTest.class);
     suite.addTestSuite(CustomButtonTest.class);
+    suite.addTestSuite(CustomEventsTest.class);
     suite.addTestSuite(CustomScrollPanelTest.class);
     suite.addTestSuite(DateBoxTest.class);
     suite.addTestSuite(DatePickerTest.class);
diff --git a/user/test/com/google/gwt/user/client/CustomEventsTest.java b/user/test/com/google/gwt/user/client/CustomEventsTest.java
new file mode 100644
index 0000000..9e6f26b
--- /dev/null
+++ b/user/test/com/google/gwt/user/client/CustomEventsTest.java
@@ -0,0 +1,147 @@
+/*
+ * Copyright 2013 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.user.client;
+
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.core.shared.GWT;
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.dom.client.NativeEvent;
+import com.google.gwt.event.dom.client.DomEvent;
+import com.google.gwt.event.shared.EventHandler;
+import com.google.gwt.junit.client.GWTTestCase;
+import com.google.gwt.user.client.impl.DOMImpl;
+import com.google.gwt.user.client.impl.DOMImplStandard;
+import com.google.gwt.user.client.ui.Button;
+import com.google.gwt.user.client.ui.RootPanel;
+
+/**
+ * Tests standard DOM operations in the {@link DOM} class.
+ * <p>
+ * The test has its own module so that it will always be the first test executed in the page. By
+ * this way, we can make sure that the custom event registration always takes place before the event
+ * system gets initialized.
+ */
+public class CustomEventsTest extends GWTTestCase {
+
+  private static class CustomHandler implements EventHandler {
+    void on(DomEvent<?> customEvent) {
+      Element el = customEvent.getRelativeElement();
+      el.setTitle(el.getTitle() + "-dispatched");
+    }
+  }
+
+  private static class CustomEvent1 extends DomEvent<CustomHandler> {
+    static final Type<CustomHandler> TYPE = new Type<CustomHandler>("c1", new CustomEvent1());
+
+    @Override
+    public Type<CustomHandler> getAssociatedType() {
+      return TYPE;
+    }
+
+    @Override
+    protected void dispatch(CustomHandler handler) {
+      handler.on(this);
+    }
+  }
+
+  private static class CustomEvent2 extends DomEvent<CustomHandler> {
+    static final Type<CustomHandler> TYPE = new Type<CustomHandler>("c2", new CustomEvent2());
+
+    @Override
+    public Type<CustomHandler> getAssociatedType() {
+      return TYPE;
+    }
+
+    @Override
+    protected void dispatch(CustomHandler handler) {
+      handler.on(this);
+    }
+  }
+
+  private Button button;
+
+  @Override
+  public String getModuleName() {
+    return "com.google.gwt.user.CustomEventsTest";
+  }
+
+  @Override
+  protected void gwtSetUp() throws Exception {
+    ensureCustomEventDispatch();
+
+    button = new Button();
+    button.setTitle("event");
+    button.addBitlessDomHandler(new CustomHandler(), CustomEvent1.TYPE);
+    button.addBitlessDomHandler(new CustomHandler(), CustomEvent2.TYPE);
+
+    RootPanel.get().add(button);
+  }
+
+  @Override
+  protected void gwtTearDown() throws Exception {
+    RootPanel.get().remove(button);
+  }
+
+  public void testCustomEvent() {
+    if (!isStandard()) {
+      return; // Custom event support is for standard browsers (i.e. no IE 6/7/8)
+    }
+
+    dispatchEvent("c1");
+    assertEquals("event-dispatched", button.getTitle());
+  }
+
+  public void testCustomDispatchEvent() {
+    if (!isStandard()) {
+      return; // Custom event support is for standard browsers (i.e. no IE 6/7/8)
+    }
+
+    dispatchEvent("c2");
+    assertEquals("event-dispatched-custom_method", button.getTitle());
+  }
+
+  private void dispatchEvent(String evt) {
+    button.getElement().dispatchEvent(createCustomEvent(evt));
+  }
+
+  private static native NativeEvent createCustomEvent(String evt) /*-{
+    var e = $doc.createEvent("Event");
+    e.initEvent(evt, true, false);
+    return e;
+  }-*/;
+
+  private static native JavaScriptObject getBitlessCustomDisptachers() /*-{
+    return {
+      c2: function(evt) {
+        @com.google.gwt.user.client.impl.DOMImplStandard::dispatchEvent(*)(evt);
+        evt.target.title += "-custom_method";
+      }
+    };
+  }-*/;
+
+  private static boolean initCustomEventDispatch = true;
+
+  private static void ensureCustomEventDispatch() {
+    if (initCustomEventDispatch) {
+      DOMImplStandard.addBitlessEventDispatchers(getBitlessCustomDisptachers());
+      initCustomEventDispatch = false;
+    }
+  }
+
+  private static boolean isStandard() {
+    return GWT.create(DOMImpl.class) instanceof DOMImplStandard;
+  }
+}