From 88c547c5358b108347bb864905fc664c8380f35d Mon Sep 17 00:00:00 2001 From: Adam Bradley Date: Mon, 21 Sep 2015 13:48:58 -0500 Subject: [PATCH] refactor(activator): ripple activator --- ionic/components/app/app.ts | 6 +- ionic/components/button/modes/md.scss | 11 +- ionic/components/material/ripple.ts | 67 ---------- ionic/components/tap-click/activator.ts | 80 ++++++++++++ .../{material => tap-click}/ripple.scss | 8 +- ionic/components/tap-click/ripple.ts | 104 ++++++++++++++++ .../activator.ts => tap-click/tap-click.ts} | 115 ++++-------------- ionic/ionic.md.scss | 4 +- 8 files changed, 222 insertions(+), 173 deletions(-) delete mode 100644 ionic/components/material/ripple.ts create mode 100644 ionic/components/tap-click/activator.ts rename ionic/components/{material => tap-click}/ripple.scss (71%) create mode 100644 ionic/components/tap-click/ripple.ts rename ionic/components/{app/activator.ts => tap-click/tap-click.ts} (64%) diff --git a/ionic/components/app/app.ts b/ionic/components/app/app.ts index 03ddb5afc3..b68717fb58 100644 --- a/ionic/components/app/app.ts +++ b/ionic/components/app/app.ts @@ -7,7 +7,7 @@ import {ClickBlock} from '../../util/click-block'; import * as util from '../../util/util'; // injectables -import {Activator} from './activator'; +import {TapClick} from '../tap-click/tap-click'; import {ActionSheet} from '../action-sheet/action-sheet'; import {Modal} from '../modal/modal'; import {Popup} from '../popup/popup'; @@ -279,7 +279,7 @@ export function ionicBootstrap(rootComponentType, config) { let app = initApp(window, document, config, platform); // TODO: probs need a better way to inject global injectables - let activator = new Activator(app, config, window, document); + let tapClick = new TapClick(app, config, window, document); let actionSheet = new ActionSheet(app, config); let modal = new Modal(app, config); let popup = new Popup(app, config); @@ -289,7 +289,7 @@ export function ionicBootstrap(rootComponentType, config) { bind(IonicApp).toValue(app), bind(IonicConfig).toValue(config), bind(IonicPlatform).toValue(platform), - bind(Activator).toValue(activator), + bind(TapClick).toValue(tapClick), bind(ActionSheet).toValue(actionSheet), bind(Modal).toValue(modal), bind(Popup).toValue(popup), diff --git a/ionic/components/button/modes/md.scss b/ionic/components/button/modes/md.scss index 3443eedec9..060fa838c7 100644 --- a/ionic/components/button/modes/md.scss +++ b/ionic/components/button/modes/md.scss @@ -9,6 +9,7 @@ $button-md-box-shadow: 0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px $button-md-box-shadow-active: 0 4px 5px 0 rgba(0, 0, 0, 0.14),0 1px 10px 0 rgba(0, 0, 0, 0.12),0 2px 4px -1px rgba(0, 0, 0, 0.2); //0 1px 3px 0 rgba(0, 0, 0, 0.3); //0 2px 5px 0 rgba(0, 0, 0, 0.26) !default; $button-md-border-radius: 2px !default; $button-md-animation-curve: cubic-bezier(0.4, 0, 0.2, 1) !default; +$button-md-transition-duration: 300ms !default; $button-md-clear-hover-bg-color: rgba(158, 158, 158, 0.1); $button-md-clear-active-bg-color: rgba(158, 158, 158, 0.2); @@ -27,9 +28,9 @@ button, font-size: $button-md-font-size; box-shadow: $button-md-box-shadow; - transition: box-shadow 0.2s $button-md-animation-curve, - background-color 0.2s $button-md-animation-curve, - color 0.2s $button-md-animation-curve; + transition: box-shadow $button-md-transition-duration $button-md-animation-curve, + background-color $button-md-transition-duration $button-md-animation-curve, + color $button-md-transition-duration $button-md-animation-curve; &.activated { box-shadow: $button-md-box-shadow-active; @@ -122,8 +123,8 @@ button, // Generate Material Design Button Auxiliary Colors // -------------------------------------------------- -@each $color-name, $value in auxiliary-colors() { +@each $color-name, $color-value in auxiliary-colors() { - @include button-theme-md($color-name, $value); + @include button-theme-md($color-name, $color-value); } diff --git a/ionic/components/material/ripple.ts b/ionic/components/material/ripple.ts deleted file mode 100644 index 3fce46c267..0000000000 --- a/ionic/components/material/ripple.ts +++ /dev/null @@ -1,67 +0,0 @@ -import * as dom from '../../util/dom'; - - -export function start(key, ele, pointerX, pointerY) { - // ensure this key is cleaned up - removeRipple(key); - - // throttle how many ripples can happen quickly - if (lastRipple + 150 > Date.now()) return; - lastRipple = Date.now(); - - let r = ele.getBoundingClientRect(); - let rippleSize = Math.sqrt(r.width * r.width + r.height * r.height) * 2 + 2; - - let rippleEle = activeElements[key] = document.createElement('md-ripple'); - rippleEle.style.width = rippleSize + 'px'; - rippleEle.style.height = rippleSize + 'px'; - rippleEle.style.marginTop = -(rippleSize / 2) + 'px'; - rippleEle.style.marginLeft = -(rippleSize / 2) + 'px'; - rippleEle.style.left = (pointerX - r.left) + 'px'; - rippleEle.style.top = (pointerY - r.top) + 'px'; - - let centerX = (r.width / 2); - let hitX = (pointerX - r.left); - let distanceFromCenter = Math.abs(centerX - hitX); - let percent = 1 - (distanceFromCenter / centerX); - let duration = 300 + (300 * percent); - rippleEle.style.transitionDuration = rippleEle.style.webkitTransitionDuration = (duration + 'ms'); - - ele.classList.add('ripple-wave'); - ele.appendChild(rippleEle); - - // kick off animaiton - setTimeout(function() { - rippleEle.classList.add('ripple-animate'); - }, 32); -} - -export function end(key) { - let rippleEle = activeElements[key]; - - if (rippleEle) { - rippleEle.classList.add('ripple-animate-out'); - rippleEle.classList.remove('ripple-animate'); - - removeElements[key] = rippleEle; - setTimeout(function() { - removeRipple(key); - }, 500); - } - - delete activeElements[key]; -} - -function removeRipple(key) { - let rippleEle = removeElements[key]; - - if (rippleEle) { - rippleEle.parentNode.removeChild(rippleEle); - } - - delete removeElements[key]; -} - -let lastRipple = 0; -let activeElements = {}; -let removeElements = {}; diff --git a/ionic/components/tap-click/activator.ts b/ionic/components/tap-click/activator.ts new file mode 100644 index 0000000000..f7358c1e05 --- /dev/null +++ b/ionic/components/tap-click/activator.ts @@ -0,0 +1,80 @@ +import {raf} from '../../util/dom'; + + +export class Activator { + + constructor(app, config) { + this.app = app; + this.id = 0; + this.queue = {}; + this.active = {}; + this.clearStateTimeout = 180; + this.clearAttempt = 0; + this.activatedClass = config.setting('activatedClass') || 'activated'; + this.x = 0; + this.y = 0; + this.lastActivate = 0; + } + + downAction(targetEle, pointerX, pointerY, callback) { + // the user just pressed down + + // throttle how many activates fire off in XXms + let now = Date.now(); + if (this.lastActivate + 50 > now) return; + this.lastActivate = now; + + // remember where they pressed + this.x = pointerX; + this.y = pointerY; + + // remember this is the active element + let id = ++this.id; + this.queue[id] = targetEle; + if (this.id > 9) this.id = 0; + + // activate targetEle + raf(() => { + for (let eleId in this.queue) { + this.queue[eleId].classList.add(this.activatedClass); + this.active[eleId] = this.queue[eleId]; + } + callback && callback(targetEle); + this.queue = {}; + }); + } + + upAction(ev) { + // the user was pressing down, then just let up + setTimeout(() => { + this.clearState(); + }, this.clearStateTimeout); + } + + clearState(ev) { + // all states should return to normal + + if (!this.app.isEnabled() && this.clearAttempt < 30) { + // the app is actively disabled, so don't bother deactivating anything. + // this makes it easier on the GPU so it doesn't have to redraw any + // buttons during a transition. This will retry in XX milliseconds. + ++this.clearAttempt; + this.upAction(); + + } else { + // not actively transitioning, good to deactivate any elements + this.queue = {}; + + // remove the active class from all active elements + for (var key in this.active) { + if (this.active[key]) { + this.active[key].classList.remove(this.activatedClass); + delete this.active[key]; + } + } + + this.clearAttempt = 0; + } + } + +} diff --git a/ionic/components/material/ripple.scss b/ionic/components/tap-click/ripple.scss similarity index 71% rename from ionic/components/material/ripple.scss rename to ionic/components/tap-click/ripple.scss index 041a353fd6..e64e26b26a 100644 --- a/ionic/components/material/ripple.scss +++ b/ionic/components/tap-click/ripple.scss @@ -18,15 +18,15 @@ md-ripple { overflow: hidden; pointer-events: none; - transform: scale(0.01) translateZ(0); - transition: transform 300ms $ripple-animation-curve, opacity 400ms $ripple-animation-curve; + transform: scale(0.001) translateZ(0); + transition: transform 280ms $ripple-animation-curve, opacity 400ms ease; } -md-ripple.ripple-animate { +md-ripple.ripple-expand { transform: scale(1) translateZ(0); } -md-ripple.ripple-animate-out { +md-ripple.ripple-fade-out { transform: scale(1) translateZ(0); opacity: 0; } diff --git a/ionic/components/tap-click/ripple.ts b/ionic/components/tap-click/ripple.ts new file mode 100644 index 0000000000..ab05c31692 --- /dev/null +++ b/ionic/components/tap-click/ripple.ts @@ -0,0 +1,104 @@ +import {Activator} from './activator'; +import {raf, transitionEnd} from '../../util/dom'; + + +export class RippleActivator extends Activator { + + constructor(app, config) { + super(app, config); + } + + downAction(targetEle, pointerX, pointerY) { + super.downAction(targetEle, pointerX, pointerY, (targetEle) => { + + if (!targetEle || !targetEle.parentNode) return; + + // clean out any existing ripple elements + removeAll(targetEle); + + // create a new ripple element + let r = targetEle.getBoundingClientRect(); + let rippleSize = Math.sqrt(r.width * r.width + r.height * r.height) * 2 + 2; + + let rippleEle = document.createElement('md-ripple'); + rippleEle.style.width = rippleSize + 'px'; + rippleEle.style.height = rippleSize + 'px'; + rippleEle.style.marginTop = -(rippleSize / 2) + 'px'; + rippleEle.style.marginLeft = -(rippleSize / 2) + 'px'; + rippleEle.style.left = (pointerX - r.left) + 'px'; + rippleEle.style.top = (pointerY - r.top) + 'px'; + + targetEle.appendChild(rippleEle); + targetEle.clientWidth; // force render + + transitionEnd(rippleEle).then(ev => { + // the ripple animation has ended + ev.target.dataset.expanded = true; + fadeOutRipple(ev.target); + }); + + // kick off animaiton + rippleEle.classList.add('ripple-expand'); + }); + } + + upAction(ev) { + // the user was pressing down, then just let up + super.upAction(ev); + + // immediately remove the activated css class and clear the queue + // this stops the background from changing colors, not stop the ripple + this.queue = {}; + + let targetEle = ev.target; + raf(() => { + if (targetEle && targetEle.parentNode) { + targetEle.classList.remove(this.activatedClass); + + let rippleElements = getRippleElements(targetEle); + for (let i = 0, l = rippleElements.length; i < l; i++) { + rippleElements[i].dataset.pointerUp = true; + fadeOutRipple(rippleElements[i]); + } + } + }); + + } + + clearState(ev) { + super.clearState(ev); + if (ev && ev.target) { + removeAll(ev.target); + } + } + +} + +function fadeOutRipple(rippleEle) { + if (rippleEle && rippleEle.dataset.pointerUp && rippleEle.dataset.expanded && !rippleEle.dataset.removing) { + + transitionEnd(rippleEle).then(ev => { + // the ripple has faded out completely + let rippleEle = ev.target; + if (rippleEle.parentNode) { + rippleEle.parentNode.removeChild(rippleEle); + } + }); + + // start fading out the ripple + rippleEle.classList.add('ripple-fade-out'); + rippleEle.dataset.removing = true; + } +} + +function removeAll(targetEle) { + let rippleElements = getRippleElements(targetEle); + for (let i = 0; i < rippleElements.length; i++) { + rippleElements[i].dataset.pointerUp = rippleElements[i].dataset.expanded = true; + fadeOutRipple(rippleElements[i]); + } +} + +function getRippleElements(containerEle) { + return containerEle && containerEle.querySelectorAll('md-ripple'); +} diff --git a/ionic/components/app/activator.ts b/ionic/components/tap-click/tap-click.ts similarity index 64% rename from ionic/components/app/activator.ts rename to ionic/components/tap-click/tap-click.ts index 9233f8d319..1e164a1584 100644 --- a/ionic/components/app/activator.ts +++ b/ionic/components/tap-click/tap-click.ts @@ -1,8 +1,10 @@ -import {raf, pointerCoord, hasPointerMoved} from '../../util/dom'; -import * as ripple from '../material/ripple'; +import {pointerCoord, hasPointerMoved, transitionEnd} from '../../util/dom'; +import * as ripple from './ripple'; +import {Activator} from './activator'; +import {RippleActivator} from './ripple'; -export class Activator { +export class TapClick { constructor(app: IonicApp, config: IonicConfig, window, document) { const self = this; @@ -11,19 +13,18 @@ export class Activator { self.win = window; self.doc = document; - self.id = 0; - self.queue = {}; - self.active = {}; - self.activatedClass = 'activated'; - self.deactivateTimeout = 180; - self.deactivateAttempt = 0; self.pointerTolerance = 4; - self.isTouch = false; + self.lastTouch = 0; self.disableClick = 0; self.disableClickLimit = 2500; self.tapPolyfill = config.setting('tapPolyfill'); - self.mdRipple = config.setting('mdRipple'); + + if (config.setting('mdRipple')) { + self.activator = new RippleActivator(app, config); + } else { + self.activator = new Activator(app, config); + } function bindDom(type, listener, useCapture) { @@ -35,17 +36,17 @@ export class Activator { }, true); bindDom('touchstart', function(ev) { - self.isTouch = true; + self.lastTouch = Date.now(); self.pointerStart(ev); }); bindDom('touchend', function(ev) { - self.isTouch = true; + self.lastTouch = Date.now(); self.touchEnd(ev); }); bindDom('touchcancel', function(ev) { - self.isTouch = true; + self.lastTouch = Date.now(); self.pointerCancel(ev); }); @@ -62,7 +63,7 @@ export class Activator { let moveCoord = pointerCoord(ev); if ( hasPointerMoved(10, self.start, moveCoord) ) { - self.pointerCancel(); + self.pointerCancel(ev); } }; @@ -115,7 +116,7 @@ export class Activator { ev.preventDefault(); ev.stopPropagation(); - } else if (!self.isTouch) { + } else if (self.lastTouch + 999 < Date.now()) { this.pointerStart(ev); } } @@ -131,7 +132,7 @@ export class Activator { ev.stopPropagation(); } - if (!self.isTouch) { + if (self.lastTouch + 999 < Date.now()) { this.pointerEnd(ev); } } @@ -143,10 +144,9 @@ export class Activator { pointerStart(ev) { let targetEle = this.getActivatableTarget(ev.target); - if (targetEle && this.app.isEnabled()) { + if (targetEle) { this.start = pointerCoord(ev); - - this.queueActivate(targetEle); + this.activator.downAction(targetEle, this.start.x, this.start.y); this.moveListeners(true); } else { @@ -158,16 +158,16 @@ export class Activator { * TODO */ pointerEnd(ev) { - this.queueDeactivate(); + this.activator.upAction(ev); this.moveListeners(false); } /** * TODO */ - pointerCancel() { + pointerCancel(ev) { console.debug('pointerCancel') - this.deactivate(); + this.activator.clearState(ev); this.moveListeners(false); this.disableClick = Date.now(); } @@ -203,7 +203,6 @@ export class Activator { ev.preventDefault(); ev.stopPropagation(); } - this.isTouch = false; } getActivatableTarget(ele) { @@ -231,72 +230,4 @@ export class Activator { return false; } - queueActivate(ele) { - const self = this; - - self.queue[++self.id] = ele; - if (self.id > 19) self.id = 0; - - raf(function(){ - // activate all elements in the queue - for (var key in self.queue) { - self.activate(key, self.queue[key]); - } - self.queue = {}; - }); - } - - activate(key, ele) { - if (ele) { - ele.classList.add(this.activatedClass); - - if (this.mdRipple) { - ripple.start(key, ele, this.start.x, this.start.y); - } - - this.active[key] = ele; - } - } - - queueDeactivate() { - const self = this; - - setTimeout(function() { - self.deactivate(); - }, self.deactivateTimeout); - } - - deactivate() { - const self = this; - - if (!self.app.isEnabled() && self.deactivateAttempt < 30) { - // the app is actively disabled, so don't bother deactivating anything. - // this makes it easier on the GPU so it doesn't have to redraw any - // buttons during a transition. This will retry in XX milliseconds. - ++self.deactivateAttempt; - self.queueDeactivate(); - - } else { - // not actively transitioning, good to deactivate any elements - // clear out any elements that are queued to be set to active - self.queue = {}; - - // in the next frame, remove the active class from all active elements - raf(function() { - for (var key in self.active) { - if (self.active[key]) { - self.active[key].classList.remove(self.activatedClass); - } - if (self.mdRipple) { - ripple.end(key); - } - delete self.active[key]; - } - }); - - self.deactivateAttempt = 0; - } - - } - } diff --git a/ionic/ionic.md.scss b/ionic/ionic.md.scss index 1a94720cd2..c73f2de031 100644 --- a/ionic/ionic.md.scss +++ b/ionic/ionic.md.scss @@ -3,7 +3,6 @@ // Material Design Components @import - "components/material/ripple", "components/toolbar/modes/md", "components/action-sheet/modes/md", "components/button/modes/md", @@ -16,4 +15,5 @@ "components/nav-bar/modes/md", "components/radio/modes/md", "components/switch/modes/md", - "components/tabs/modes/md"; + "components/tabs/modes/md", + "components/tap-click/ripple";