perf(app): tap-click is a ES

we can finally remove the slot!
This commit is contained in:
Manu Mtz.-Almeida
2018-08-01 16:24:18 +02:00
parent 49cac8beec
commit b0ac5badc2
11 changed files with 3890 additions and 5535 deletions

View File

@@ -39,7 +39,6 @@ exports.config.outputTargets = [
'ion-menu-controller',
'ion-animation-controller',
'ion-animation-controller',
'ion-gesture-controller',
// navigation
'ion-router',
@@ -51,8 +50,6 @@ exports.config.outputTargets = [
'ion-tab-button',
// auxiliar
'ion-gesture',
'ion-tap-click',
'ion-picker-column',
'ion-range-knob',
'ion-backdrop',

8884
core/src/components.d.ts vendored
View File

File diff suppressed because it is too large Load Diff

View File

@@ -44,7 +44,6 @@ ion-app.is-device {
// Misc
// --------------------------------------------------
ion-tap-click,
ion-route,
ion-route-redirect,
ion-router,

View File

@@ -13,6 +13,8 @@ import { createThemedClasses } from '../../utils/theme';
})
export class App {
private isDevice = false;
mode!: Mode;
@Element() el!: HTMLElement;
@@ -21,13 +23,17 @@ export class App {
@Prop({ context: 'config' }) config!: Config;
@Prop({ context: 'queue' }) queue!: QueueApi;
componentWillLoad() {
this.isDevice = this.config.getBoolean('isDevice', isDevice(this.win));
}
componentDidLoad() {
importTapClick(this.win, this.isDevice);
importInputShims(this.win, this.config);
importStatusTap(this.win, this.config, this.queue);
importStatusTap(this.win, this.isDevice, this.queue);
}
hostData() {
const device = this.config.getBoolean('isDevice', isDevice(this.win));
const hybrid = isHybrid(this.win);
const statusbarPadding = this.config.get('statusbarPadding', hybrid);
@@ -35,25 +41,23 @@ export class App {
class: {
...createThemedClasses(this.mode, 'app'),
'is-device': device,
'is-device': this.isDevice,
'is-hydrid': hybrid,
'statusbar-padding': statusbarPadding
}
};
}
}
render() {
return [
<ion-tap-click></ion-tap-click>,
<slot></slot>
];
async function importStatusTap(win: Window, device: boolean, queue: QueueApi) {
if (device) {
(await import('../../utils/status-tap')).startStatusTap(win, queue);
}
}
async function importStatusTap(win: Window, config: Config, queue: QueueApi) {
const device = config.getBoolean('isDevice', isDevice(win));
async function importTapClick(win: Window, device: boolean) {
if (device) {
(await import('../../utils/status-tap')).startStatusTap(win, queue);
(await import('../../utils/tap-click')).startTapClick(win.document);
}
}

View File

@@ -1,12 +0,0 @@
# ion-tap-click
TabClick is an internal component with no public API.
<!-- Auto Generated Below -->
----------------------------------------------
*Built with [StencilJS](https://stenciljs.com/)*

View File

@@ -1,164 +0,0 @@
import { Component, Element, EventListenerEnable, Listen, Prop } from '@stencil/core';
import { now, pointerCoord } from '../../utils/helpers';
/** @hidden */
@Component({
tag: 'ion-tap-click',
})
export class TapClick {
private lastTouch = -MOUSE_WAIT * 10;
private lastActivated = 0;
private cancelled = false;
private activatableEle?: HTMLElement;
private activeDefer: any;
private clearDefers = new WeakMap<HTMLElement, any>();
@Prop({ context: 'isServer' }) isServer!: boolean;
@Prop({ context: 'enableListener' }) enableListener!: EventListenerEnable;
@Element() el!: HTMLElement;
@Listen('body:click', { passive: false, capture: true })
onBodyClick(ev: Event) {
if (this.cancelled) {
ev.preventDefault();
ev.stopPropagation();
}
}
// Touch Events
@Listen('document:touchstart', { passive: true, capture: true })
onTouchStart(ev: TouchEvent) {
this.lastTouch = now(ev);
this.pointerDown(ev);
}
@Listen('document:touchcancel', { passive: false, capture: true })
@Listen('document:touchend', { passive: false, capture: true })
onTouchEnd(ev: TouchEvent) {
this.lastTouch = now(ev);
this.pointerUp(ev);
}
@Listen('document:mousedown', { passive: true, capture: true })
onMouseDown(ev: MouseEvent) {
const t = now(ev) - MOUSE_WAIT;
if (this.lastTouch < t) {
this.pointerDown(ev);
}
}
@Listen('document:mouseup', { passive: false, capture: true })
onMouseUp(ev: TouchEvent) {
const t = now(ev) - MOUSE_WAIT;
if (this.lastTouch < t) {
this.pointerUp(ev);
}
}
@Listen('body:ionScrollStart')
@Listen('body:ionGestureCaptured')
cancelActive() {
clearTimeout(this.activeDefer);
if (this.activatableEle) {
this.removeActivated(false);
this.activatableEle = undefined;
}
this.cancelled = true;
}
private pointerDown(ev: any) {
if (this.activatableEle) {
return;
}
this.cancelled = false;
this.setActivatedElement(getActivatableTarget(ev.target), ev);
}
private pointerUp(ev: UIEvent) {
this.setActivatedElement(undefined, ev);
if (this.cancelled && ev.cancelable) {
ev.preventDefault();
}
}
private setActivatedElement(el: HTMLElement | undefined, ev: UIEvent) {
// do nothing
const activatableEle = this.activatableEle;
if (el && el === activatableEle) {
return;
}
clearTimeout(this.activeDefer);
this.activeDefer = undefined;
const { x, y } = pointerCoord(ev);
// unactivate selected
if (activatableEle) {
if (this.clearDefers.has(activatableEle)) {
throw new Error('internal error');
}
if (!activatableEle.classList.contains(ACTIVATED)) {
this.addActivated(activatableEle, x, y);
}
this.removeActivated(true);
}
// activate
if (el) {
const deferId = this.clearDefers.get(el);
if (deferId) {
clearTimeout(deferId);
this.clearDefers.delete(el);
}
el.classList.remove(ACTIVATED);
this.activeDefer = setTimeout(() => {
this.addActivated(el, x, y);
this.activeDefer = undefined;
}, ADD_ACTIVATED_DEFERS);
}
this.activatableEle = el;
}
private addActivated(el: HTMLElement, x: number, y: number) {
this.lastActivated = Date.now();
el.classList.add(ACTIVATED);
const event = new CustomEvent('ionActivated', {
bubbles: false,
detail: { x, y }
});
el.dispatchEvent(event);
}
private removeActivated(smooth: boolean) {
const activatableEle = this.activatableEle;
if (!activatableEle) {
return;
}
const time = CLEAR_STATE_DEFERS - Date.now() + this.lastActivated;
if (smooth && time > 0) {
const deferId = setTimeout(() => {
activatableEle.classList.remove(ACTIVATED);
this.clearDefers.delete(activatableEle);
}, CLEAR_STATE_DEFERS);
this.clearDefers.set(activatableEle, deferId);
} else {
activatableEle.classList.remove(ACTIVATED);
}
}
}
function getActivatableTarget(el: HTMLElement): any {
return el.closest(':not([tappable]) > a, :not([tappable]) > button, [tappable]');
}
const ACTIVATED = 'activated';
const ADD_ACTIVATED_DEFERS = 200;
const CLEAR_STATE_DEFERS = 200;
const MOUSE_WAIT = 2500;

View File

@@ -1,75 +0,0 @@
<!DOCTYPE html>
<html dir="ltr">
<head>
<meta charset="UTF-8">
<title>Button Effect - Basic</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
<script src="/dist/ionic.js"></script>
<link rel="stylesheet" type="text/css" href="/css/ionic.min.css">
<style>
.my-block {
position: relative;
background: blue;
color: white;
width: 300px;
height: 100px;
margin: 1rem;
}
</style>
</head>
<body>
<ion-app>
<ion-header>
<ion-toolbar>
<ion-title>Button Effect - Basic</ion-title>
</ion-toolbar>
</ion-header>
<ion-content padding no-bounce>
<p>
<ion-button size="small">Small</ion-button>
</p>
<p>
<ion-button size="large">Large</ion-button>
</p>
<p>
<ion-button size="large" fill="outline">Large</ion-button>
</p>
<p>
<ion-button size="large" fill="clear">Large</ion-button>
</p>
<div class="my-block">
<ion-ripple-effect></ion-ripple-effect>
This is just a div + effect behind
<ion-button onclick="buttonClicked()">Nested button</ion-button>
</div>
<div class="my-block">
This is just a div + effect on top
<ion-button onclick="buttonClicked()">Nested button</ion-button>
<ion-ripple-effect></ion-ripple-effect>
</div>
<div class="my-block">
This is just a div + effect
<ion-ripple-effect></ion-ripple-effect>
</div>
</ion-content>
</ion-app>
<script>
function blockClicked() {
console.log('block clicked');
return true;
}
function buttonClicked() {
console.log('button clicked');
}
</script>
</body>
</html>

View File

@@ -1,82 +0,0 @@
<!DOCTYPE html>
<html dir="ltr">
<head>
<meta charset="UTF-8">
<title>Button Effect</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
<script src="/dist/ionic.js"></script>
<link rel="stylesheet" type="text/css" href="/css/ionic.min.css">
<style>
.my-block {
position: relative;
background: blue;
color: white;
width: 300px;
height: 100px;
margin: 1rem;
}
</style>
</head>
<body>
<ion-app>
<ion-header translucent>
<ion-toolbar>
<ion-title>Button Effect</ion-title>
</ion-toolbar>
</ion-header>
<ion-content padding no-bounce fullscreen>
<p>
<ion-button size="small">Small</ion-button>
</p>
<p>
<ion-button size="large">Large</ion-button>
</p>
<p>
<ion-button size="large" fill="outline">Large</ion-button>
</p>
<p>
<ion-button size="large" fill="clear">Large</ion-button>
</p>
<div class="my-block">
<ion-ripple-effect></ion-ripple-effect>
This is just a div + effect behind
<ion-button onclick="buttonClicked()">Nested button</ion-button>
</div>
<div class="my-block">
This is just a div + effect on top
<ion-button onclick="buttonClicked()">Nested button</ion-button>
<ion-ripple-effect></ion-ripple-effect>
</div>
<div class="my-block">
This is just a div + effect
<ion-ripple-effect></ion-ripple-effect>
</div>
</ion-content>
</ion-app>
<style>
ion-toolbar {
--background: white;
}
</style>
<script>
function blockClicked() {
console.log('block clicked');
return true;
}
function buttonClicked() {
console.log('button clicked');
}
</script>
</body>
</html>

155
core/src/utils/tap-click.ts Normal file
View File

@@ -0,0 +1,155 @@
import { now, pointerCoord } from './helpers';
export function startTapClick(doc: Document) {
let lastTouch = -MOUSE_WAIT * 10;
let lastActivated = 0;
let cancelled = false;
let activatableEle: HTMLElement | undefined;
let activeDefer: any;
const clearDefers = new WeakMap<HTMLElement, any>();
function onBodyClick(ev: Event) {
if (cancelled) {
ev.preventDefault();
ev.stopPropagation();
}
}
// Touch Events
function onTouchStart(ev: TouchEvent) {
lastTouch = now(ev);
pointerDown(ev);
}
function onTouchEnd(ev: TouchEvent) {
lastTouch = now(ev);
pointerUp(ev);
}
function onMouseDown(ev: MouseEvent) {
const t = now(ev) - MOUSE_WAIT;
if (lastTouch < t) {
pointerDown(ev);
}
}
function onMouseUp(ev: MouseEvent) {
const t = now(ev) - MOUSE_WAIT;
if (lastTouch < t) {
pointerUp(ev);
}
}
function cancelActive() {
clearTimeout(activeDefer);
if (activatableEle) {
removeActivated(false);
activatableEle = undefined;
}
cancelled = true;
}
function pointerDown(ev: any) {
if (activatableEle) {
return;
}
cancelled = false;
setActivatedElement(getActivatableTarget(ev.target), ev);
}
function pointerUp(ev: UIEvent) {
setActivatedElement(undefined, ev);
if (cancelled && ev.cancelable) {
ev.preventDefault();
}
}
function setActivatedElement(el: HTMLElement | undefined, ev: UIEvent) {
// do nothing
if (el && el === activatableEle) {
return;
}
clearTimeout(activeDefer);
activeDefer = undefined;
const { x, y } = pointerCoord(ev);
// unactivate selected
if (activatableEle) {
if (clearDefers.has(activatableEle)) {
throw new Error('internal error');
}
if (!activatableEle.classList.contains(ACTIVATED)) {
addActivated(activatableEle, x, y);
}
removeActivated(true);
}
// activate
if (el) {
const deferId = clearDefers.get(el);
if (deferId) {
clearTimeout(deferId);
clearDefers.delete(el);
}
el.classList.remove(ACTIVATED);
activeDefer = setTimeout(() => {
addActivated(el, x, y);
activeDefer = undefined;
}, ADD_ACTIVATED_DEFERS);
}
activatableEle = el;
}
function addActivated(el: HTMLElement, x: number, y: number) {
lastActivated = Date.now();
el.classList.add(ACTIVATED);
const event = new CustomEvent('ionActivated', {
bubbles: false,
detail: { x, y }
});
el.dispatchEvent(event);
}
function removeActivated(smooth: boolean) {
const active = activatableEle;
if (!active) {
return;
}
const time = CLEAR_STATE_DEFERS - Date.now() + lastActivated;
if (smooth && time > 0) {
const deferId = setTimeout(() => {
active.classList.remove(ACTIVATED);
clearDefers.delete(active);
}, CLEAR_STATE_DEFERS);
clearDefers.set(active, deferId);
} else {
active.classList.remove(ACTIVATED);
}
}
doc.body.addEventListener('click', onBodyClick, true);
doc.body.addEventListener('ionScrollStart', cancelActive);
doc.body.addEventListener('ionGestureCaptured', cancelActive);
doc.addEventListener('touchstart', onTouchStart, true);
doc.addEventListener('touchcancel', onTouchEnd, true);
doc.addEventListener('touchend', onTouchEnd, true);
doc.addEventListener('mousedown', onMouseDown, true);
doc.addEventListener('mouseup', onMouseUp, true);
}
function getActivatableTarget(el: HTMLElement): any {
return el.closest(':not([tappable]) > a, :not([tappable]) > button, [tappable]');
}
const ACTIVATED = 'activated';
const ADD_ACTIVATED_DEFERS = 200;
const CLEAR_STATE_DEFERS = 200;
const MOUSE_WAIT = 2500;

View File

@@ -51,13 +51,3 @@ export async function openURL(win: Window, url: string | undefined, ev: Event, d
}
return Promise.resolve();
}
export function getParentNode(node: Node) {
// this also checks for host elements of shadow root node
// if the parent node is a document fragment (shadow root)
// then use the "host" property on it
// otherwise use the parent node
// DOCUMENT_FRAGMENT_NODE nodeType === 11
const parentNode: any = node.parentNode;
return (parentNode && parentNode.NODE_TYPE === 11 ? parentNode.host : parentNode);
}

View File

@@ -6,9 +6,9 @@ export const config: Config = {
bundles: [
{ components: ['ion-action-sheet', 'ion-action-sheet-controller'] },
{ components: ['ion-alert', 'ion-alert-controller'] },
{ components: ['ion-anchor', 'ion-back-button', 'ion-nav', 'ion-nav-pop', 'ion-nav-push', 'ion-nav-set-root'] },
{ components: ['ion-anchor', 'ion-back-button'] },
{ components: ['ion-animation-controller'] },
{ components: ['ion-app', 'ion-buttons', 'ion-content', 'ion-footer', 'ion-header', 'ion-scroll', 'ion-tap-click', 'ion-title', 'ion-toolbar'] },
{ components: ['ion-app', 'ion-buttons', 'ion-content', 'ion-footer', 'ion-header', 'ion-scroll', 'ion-title', 'ion-toolbar'] },
{ components: ['ion-avatar', 'ion-badge', 'ion-thumbnail'] },
{ components: ['ion-backdrop'] },
{ components: ['ion-button', 'ion-icon'] },
@@ -20,13 +20,15 @@ export const config: Config = {
{ components: ['ion-grid', 'ion-row', 'ion-col'] },
{ components: ['ion-hide-when', 'ion-show-when'] },
{ components: ['ion-infinite-scroll', 'ion-infinite-scroll-content'] },
{ components: ['ion-input', 'ion-textarea'] },
{ components: ['ion-item', 'ion-item-divider', 'ion-item-group', 'ion-label', 'ion-list', 'ion-list-header', 'ion-skeleton-text'] },
{ components: ['ion-input'] },
{ components: ['ion-textarea'] },
{ components: ['ion-item', 'ion-item-divider', 'ion-item-group', 'ion-label', 'ion-list', 'ion-list-header', 'ion-skeleton-text', 'ion-note'] },
{ components: ['ion-item-sliding', 'ion-item-options', 'ion-item-option'] },
{ components: ['ion-loading', 'ion-loading-controller'] },
{ components: ['ion-menu', 'ion-menu-controller', 'ion-menu-toggle', 'ion-menu-button'] },
{ components: ['ion-modal', 'ion-modal-controller'] },
{ components: ['ion-note', 'ion-img', 'ion-text'] },
{ components: ['ion-nav', 'ion-nav-pop', 'ion-nav-push', 'ion-nav-set-root'] },
{ components: ['ion-img'] },
{ components: ['ion-popover', 'ion-popover-controller'] },
{ components: ['ion-radio', 'ion-radio-group'] },
{ components: ['ion-range', 'ion-range-knob'] },
@@ -41,6 +43,7 @@ export const config: Config = {
{ components: ['ion-spinner'] },
{ components: ['ion-split-pane'] },
{ components: ['ion-tabs', 'ion-tab', 'ion-tabbar', 'ion-tab-button'] },
{ components: ['ion-text'] },
{ components: ['ion-toast', 'ion-toast-controller'] },
{ components: ['ion-toggle'] },
{ components: ['ion-virtual-scroll'] },