Merge "Low-level plugin event helper API"
diff --git a/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper.html b/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper.html
new file mode 100644
index 0000000..d62ac99
--- /dev/null
+++ b/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper.html
@@ -0,0 +1,21 @@
+<!--
+Copyright (C) 2017 The Android Open Source Project
+
+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.
+-->
+
+<link rel="import" href="../../../bower_components/polymer/polymer.html">
+
+<dom-module id="gr-event-helper">
+  <script src="gr-event-helper.js"></script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper.js b/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper.js
new file mode 100644
index 0000000..e750c07
--- /dev/null
+++ b/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper.js
@@ -0,0 +1,69 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// 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.
+(function(window) {
+  'use strict';
+
+  function GrEventHelper(element) {
+    this.element = element;
+    this._unsubscribers = [];
+  }
+
+  /**
+   * Add a callback to element click or touch.
+   * The callback may return false to prevent event bubbling.
+   * @param {function(Event):boolean} callback
+   * @return {function()} Unsubscribe function.
+   */
+  GrEventHelper.prototype.onTap = function(callback) {
+    return this._listen(this.element, callback);
+  };
+
+  /**
+   * Add a callback to element click or touch ahead of normal flow.
+   * Callback is installed on parent during capture phase.
+   * https://www.w3.org/TR/DOM-Level-3-Events/#event-flow
+   * The callback may return false to cancel regular event listeners.
+   * @param {function(Event):boolean} callback
+   * @return {function()} Unsubscribe function.
+   */
+  GrEventHelper.prototype.captureTap = function(callback) {
+    return this._listen(this.element.parentElement, callback, {capture: true});
+  };
+
+  GrEventHelper.prototype._listen = function(container, callback, opt_options) {
+    const capture = opt_options && opt_options.capture;
+    const handler = e => {
+      if (e.path.indexOf(this.element) !== -1) {
+        let mayContinue = true;
+        try {
+          mayContinue = callback(e);
+        } catch (e) {
+          console.warn(`Plugin error handing event: ${e}`);
+        }
+        if (mayContinue === false) {
+          e.stopImmediatePropagation();
+          e.stopPropagation();
+          e.preventDefault();
+        }
+      }
+    };
+    container.addEventListener('tap', handler, capture);
+    const unsubscribe = () =>
+      container.removeEventListener('tap', handler, capture);
+    this._unsubscribers.push(unsubscribe);
+    return unsubscribe;
+  };
+
+  window.GrEventHelper = GrEventHelper;
+})(window);
diff --git a/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper_test.html b/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper_test.html
new file mode 100644
index 0000000..9d42851
--- /dev/null
+++ b/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper_test.html
@@ -0,0 +1,96 @@
+<!DOCTYPE html>
+<!--
+Copyright (C) 2017 The Android Open Source Project
+
+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.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<title>gr-event-helper</title>
+
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<link rel="import" href="../../../test/common-test-setup.html"/>
+<link rel="import" href="gr-event-helper.html"/>
+
+<script>void(0);</script>
+
+<dom-element id="some-element">
+  <script>
+    Polymer({
+      is: 'some-element',
+      properties: {
+        fooBar: {
+          type: Object,
+          notify: true,
+        },
+      },
+    });
+  </script>
+</dom-element>
+
+<test-fixture id="basic">
+  <template>
+    <some-element></some-element>
+  </template>
+</test-fixture>
+
+<script>
+  suite('gr-event-helper tests', () => {
+    let element;
+    let instance;
+    let sandbox;
+
+    setup(() => {
+      sandbox = sinon.sandbox.create();
+      element = fixture('basic');
+      instance = new GrEventHelper(element);
+    });
+
+    teardown(() => {
+      sandbox.restore();
+    });
+
+    test('onTap()', done => {
+      instance.onTap(() => {
+        done();
+      });
+      element.fire('tap');
+    });
+
+    test('onTap() cancel', () => {
+      const tapStub = sandbox.stub();
+      element.parentElement.addEventListener('tap', tapStub);
+      instance.onTap(() => false);
+      element.fire('tap');
+      flushAsynchronousOperations();
+      assert.isFalse(tapStub.called);
+    });
+
+    test('captureTap()', done => {
+      instance.captureTap(() => {
+        done();
+      });
+      element.fire('tap');
+    });
+
+    test('captureTap() cancels tap()', () => {
+      const tapStub = sandbox.stub();
+      element.addEventListener('tap', tapStub);
+      instance.captureTap(() => false);
+      element.fire('tap');
+      flushAsynchronousOperations();
+      assert.isFalse(tapStub.called);
+    });
+  });
+</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.html
index 53f889f..4133600 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.html
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.html
@@ -19,6 +19,7 @@
 <link rel="import" href="../../core/gr-reporting/gr-reporting.html">
 <link rel="import" href="../../plugins/gr-attribute-helper/gr-attribute-helper.html">
 <link rel="import" href="../../plugins/gr-dom-hooks/gr-dom-hooks.html">
+<link rel="import" href="../../plugins/gr-event-helper/gr-event-helper.html">
 <link rel="import" href="../../plugins/gr-popup-interface/gr-popup-interface.html">
 <link rel="import" href="../../plugins/gr-theme-api/gr-theme-api.html">
 <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js
index 0fe2da3..8f78dd3 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js
@@ -198,6 +198,10 @@
     return new GrAttributeHelper(element);
   };
 
+  Plugin.prototype.eventHelper = function(element) {
+    return new GrEventHelper(element);
+  };
+
   Plugin.prototype.popup = function(moduleName) {
     if (typeof moduleName !== 'string') {
       throw new Error('deprecated, use deprecated.popup');
diff --git a/polygerrit-ui/app/test/index.html b/polygerrit-ui/app/test/index.html
index 38755c4..2dba4fc 100644
--- a/polygerrit-ui/app/test/index.html
+++ b/polygerrit-ui/app/test/index.html
@@ -38,16 +38,16 @@
     'admin/gr-create-group-dialog/gr-create-group-dialog_test.html',
     'admin/gr-create-pointer-dialog/gr-create-pointer-dialog_test.html',
     'admin/gr-create-project-dialog/gr-create-project-dialog_test.html',
-    'admin/gr-group/gr-group_test.html',
     'admin/gr-group-audit-log/gr-group-audit-log_test.html',
     'admin/gr-group-members/gr-group-members_test.html',
+    'admin/gr-group/gr-group_test.html',
     'admin/gr-permission/gr-permission_test.html',
     'admin/gr-plugin-list/gr-plugin-list_test.html',
-    'admin/gr-project/gr-project_test.html',
     'admin/gr-project-access/gr-project-access_test.html',
     'admin/gr-project-commands/gr-project-commands_test.html',
     'admin/gr-project-detail-list/gr-project-detail-list_test.html',
     'admin/gr-project-list/gr-project-list_test.html',
+    'admin/gr-project/gr-project_test.html',
     'admin/gr-rule-editor/gr-rule-editor_test.html',
     'change-list/gr-change-list-item/gr-change-list-item_test.html',
     'change-list/gr-change-list-view/gr-change-list-view_test.html',
@@ -55,8 +55,8 @@
     'change/gr-account-entry/gr-account-entry_test.html',
     'change/gr-account-list/gr-account-list_test.html',
     'change/gr-change-actions/gr-change-actions_test.html',
-    'change/gr-change-metadata/gr-change-metadata_test.html',
     'change/gr-change-metadata/gr-change-metadata-it_test.html',
+    'change/gr-change-metadata/gr-change-metadata_test.html',
     'change/gr-change-view/gr-change-view_test.html',
     'change/gr-comment-list/gr-comment-list_test.html',
     'change/gr-commit-info/gr-commit-info_test.html',
@@ -65,22 +65,22 @@
     'change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.html',
     'change/gr-confirm-revert-dialog/gr-confirm-revert-dialog_test.html',
     'change/gr-download-dialog/gr-download-dialog_test.html',
-    'change/gr-label-scores/gr-label-scores_test.html',
-    'change/gr-label-score-row/gr-label-score-row_test.html',
-    'change/gr-file-list/gr-file-list_test.html',
     'change/gr-file-list-header/gr-file-list-header_test.html',
+    'change/gr-file-list/gr-file-list_test.html',
+    'change/gr-label-score-row/gr-label-score-row_test.html',
+    'change/gr-label-scores/gr-label-scores_test.html',
     'change/gr-message/gr-message_test.html',
     'change/gr-messages-list/gr-messages-list_test.html',
     'change/gr-related-changes-list/gr-related-changes-list_test.html',
-    'change/gr-reply-dialog/gr-reply-dialog_test.html',
     'change/gr-reply-dialog/gr-reply-dialog-it_test.html',
+    'change/gr-reply-dialog/gr-reply-dialog_test.html',
     'change/gr-reviewer-list/gr-reviewer-list_test.html',
     'core/gr-account-dropdown/gr-account-dropdown_test.html',
     'core/gr-error-manager/gr-error-manager_test.html',
     'core/gr-main-header/gr-main-header_test.html',
     'core/gr-navigation/gr-navigation_test.html',
-    'core/gr-router/gr-router_test.html',
     'core/gr-reporting/gr-reporting_test.html',
+    'core/gr-router/gr-router_test.html',
     'core/gr-search-bar/gr-search-bar_test.html',
     'diff/gr-comment-api/gr-comment-api_test.html',
     'diff/gr-diff-builder/gr-diff-builder_test.html',
@@ -102,6 +102,7 @@
     'diff/gr-syntax-layer/gr-syntax-layer_test.html',
     'diff/gr-syntax-lib-loader/gr-syntax-lib-loader_test.html',
     'plugins/gr-attribute-helper/gr-attribute-helper_test.html',
+    'plugins/gr-event-helper/gr-event-helper_test.html',
     'plugins/gr-external-style/gr-external-style_test.html',
     'plugins/gr-plugin-host/gr-plugin-host_test.html',
     'plugins/gr-popup-interface/gr-plugin-popup_test.html',
@@ -119,9 +120,8 @@
     'shared/gr-account-label/gr-account-label_test.html',
     'shared/gr-account-link/gr-account-link_test.html',
     'shared/gr-alert/gr-alert_test.html',
-    'shared/gr-autocomplete/gr-autocomplete_test.html',
-    'shared/gr-textarea/gr-textarea_test.html',
     'shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown_test.html',
+    'shared/gr-autocomplete/gr-autocomplete_test.html',
     'shared/gr-avatar/gr-avatar_test.html',
     'shared/gr-button/gr-button_test.html',
     'shared/gr-change-star/gr-change-star_test.html',
@@ -133,20 +133,21 @@
     'shared/gr-editable-content/gr-editable-content_test.html',
     'shared/gr-editable-label/gr-editable-label_test.html',
     'shared/gr-formatted-text/gr-formatted-text_test.html',
-    'shared/gr-page-nav/gr-page-nav_test.html',
     'shared/gr-js-api-interface/gr-change-actions-js-api_test.html',
     'shared/gr-js-api-interface/gr-change-reply-js-api_test.html',
     'shared/gr-js-api-interface/gr-js-api-interface_test.html',
     'shared/gr-linked-chip/gr-linked-chip_test.html',
     'shared/gr-linked-text/gr-linked-text_test.html',
     'shared/gr-list-view/gr-list-view_test.html',
+    'shared/gr-page-nav/gr-page-nav_test.html',
     'shared/gr-rest-api-interface/gr-auth_test.html',
     'shared/gr-rest-api-interface/gr-rest-api-interface_test.html',
     'shared/gr-rest-api-interface/gr-reviewer-updates-parser_test.html',
     'shared/gr-select/gr-select_test.html',
     'shared/gr-storage/gr-storage_test.html',
-    'shared/gr-tooltip/gr-tooltip_test.html',
+    'shared/gr-textarea/gr-textarea_test.html',
     'shared/gr-tooltip-content/gr-tooltip-content_test.html',
+    'shared/gr-tooltip/gr-tooltip_test.html',
   ];
   for (let file of elements) {
     file = elementsPath + file;