Low-level plugin event helper API

Can be used to listen to tap evens (i.e. click/touch), preventing their
bubbling, or preventing normal execution. To stop either bubbling or
normal execution, callback should explicitly return false. By default,
bubbling and normal execution is not prevented.

onTap() adds a listener to a click or touch event to element wrapped
with event helper.

captureTap() installs a capture phase listener and callback returning
false at that moment intercepts tap before any action is taken by other
listeners (i.e. PolyGerrit buttons).

Sample code

``` js
Gerrit.install(plugin => {
  plugin.hook('reply-text').onAttached(element => {
    if (!element.content) { return; }
    plugin.eventHelper(element.content).onTap(() => {
      console.log('reply test tapped!');
    });
    plugin.eventHelper(element.content).captureTap(() => {
      // Prevent onTap() handler from being called.
      return false;
    });
  });
});
```

Change-Id: Ie10169e2c801ce85590e4f700e6041e9c8a02bff
This commit is contained in:
Viktar Donich
2017-09-08 14:00:15 -07:00
parent 1bd466a24b
commit ae7c9618d3
6 changed files with 204 additions and 12 deletions

View File

@@ -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>

View File

@@ -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);

View File

@@ -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>

View File

@@ -19,6 +19,7 @@ limitations under the License.
<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">

View File

@@ -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');

View File

@@ -38,16 +38,16 @@ limitations under the License.
'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 @@ limitations under the License.
'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 @@ limitations under the License.
'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 @@ limitations under the License.
'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 @@ limitations under the License.
'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 @@ limitations under the License.
'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;