/* * https://github.com/morethanwords/tweb * Copyright (C) 2019-2021 Eduard Kuzmenko * https://github.com/morethanwords/tweb/blob/master/LICENSE */ import findUpClassName from '../helpers/dom/findUpClassName'; import sequentialDom from '../helpers/sequentialDom'; import IS_TOUCH_SUPPORTED from '../environment/touchSupport'; import rootScope from '../lib/rootScope'; import findUpAsChild from '../helpers/dom/findUpAsChild'; import {fastRaf} from '../helpers/schedulers'; import liteMode from '../helpers/liteMode'; let rippleClickId = 0; export default function ripple( elem: HTMLElement, callback: (id: number) => Promise = () => Promise.resolve(), onEnd: (id: number) => void = null, prepend = false, attachListenerTo = elem ) { // return; if(elem.querySelector('.c-ripple')) return; elem.classList.add('rp'); const r = document.createElement('div'); r.classList.add('c-ripple'); const isSquare = elem.classList.contains('rp-square'); if(isSquare) { r.classList.add('is-square'); } elem[prepend ? 'prepend' : 'append'](r); let handler: () => void; // let animationEndPromise: Promise; const drawRipple = (clientX: number, clientY: number) => { const startTime = Date.now(); const circle = document.createElement('div'); const clickId = rippleClickId++; // console.log('ripple drawRipple'); // const auto = elem.classList.contains('row-sortable') && !elem.classList.contains('cant-sort'); const auto = false; const duration = (auto ? .3 : +window.getComputedStyle(r).getPropertyValue('--ripple-duration').replace('s', '')) * 1000; // console.log('ripple duration', duration); const _handler = handler = () => { // handler = () => animationEndPromise.then((duration) => { // console.log('ripple animation was:', duration); // const duration = isSquare || mediaSizes.isMobile ? 200 : 700; // return; const elapsedTime = Date.now() - startTime; const cb = () => { // console.log('ripple elapsedTime total pre-remove:', Date.now() - startTime); sequentialDom.mutate(() => { circle.remove(); }); onEnd?.(clickId); }; if(elapsedTime < duration) { const delay = Math.max(duration - elapsedTime, duration / 2); setTimeout(() => circle.classList.add('hiding'), Math.max(delay - duration / 2, 0)); setTimeout(cb, delay); } else { circle.classList.add('hiding'); setTimeout(cb, duration / 2); } if(!IS_TOUCH_SUPPORTED) { window.removeEventListener('contextmenu', handler); window.removeEventListener('mousemove', handler); } handler = null; touchStartFired = false; }; // }); callback?.(clickId); /* callback().then((bad) => { if(bad) { span.remove(); return; } */ // console.log('ripple after promise', Date.now() - startTime); // console.log('ripple tooSlow:', tooSlow); /* if(tooSlow) { span.remove(); return; } */ fastRaf(() => { if(_handler !== handler) { return; } const rect = r.getBoundingClientRect(); circle.classList.add('c-ripple__circle'); const clickX = clientX - rect.left; const clickY = clientY - rect.top; const radius = Math.sqrt((Math.abs(clickY - rect.height / 2) + rect.height / 2) ** 2 + (Math.abs(clickX - rect.width / 2) + rect.width / 2) ** 2); const size = radius; // center of circle const x = clickX - size / 2; const y = clickY - size / 2; // console.log('ripple click', offsetFromCenter, size, clickX, clickY); circle.style.width = circle.style.height = size + 'px'; circle.style.left = x + 'px'; circle.style.top = y + 'px'; // нижний код выполняется с задержкой /* animationEndPromise = new Promise((resolve) => { span.addEventListener('animationend', () => { // 713 -> 700 resolve(((Date.now() - startTime) / 100 | 0) * 100); }, {once: true}); }); */ // нижний код не всегда включает анимацию ПРИ КЛИКЕ НА ТАЧПАД БЕЗ ТАПТИК ЭНЖИНА /* span.style.display = 'none'; r.append(span); duration = +window.getComputedStyle(span).getPropertyValue('animation-duration').replace('s', '') * 1000; span.style.display = ''; */ r.append(circle); if(auto) { // window.addEventListener('mousemove', handler, {once: true, passive: true}); handler(); } // r.classList.add('active'); // handler(); }); // }); }; const isRippleUnneeded = (e: Event) => { return e.target !== elem && ( ['BUTTON', 'A'].includes((e.target as HTMLElement).tagName) || findUpClassName(e.target as HTMLElement, 'c-ripple') !== r ) && ( attachListenerTo === elem || !findUpAsChild(e.target as HTMLElement, attachListenerTo) ) && !findUpClassName(e.target, 'checkbox-field'); }; // TODO: rename this variable let touchStartFired = false; if(IS_TOUCH_SUPPORTED) { const touchEnd = () => { handler?.(); }; attachListenerTo.addEventListener('touchstart', (e) => { if(!liteMode.isAvailable('animations')) { return; } // console.log('ripple touchstart', e); if(e.touches.length > 1 || touchStartFired || isRippleUnneeded(e)) { return; } // console.log('touchstart', e); touchStartFired = true; const {clientX, clientY} = e.touches[0]; drawRipple(clientX, clientY); attachListenerTo.addEventListener('touchend', touchEnd, {once: true}); window.addEventListener('touchmove', (e) => { e.cancelBubble = true; e.stopPropagation(); touchEnd(); attachListenerTo.removeEventListener('touchend', touchEnd); }, {once: true}); }, {passive: true}); } else { attachListenerTo.addEventListener('mousedown', (e) => { if(![0, 2].includes(e.button)) { // only left and right buttons return; } if(!liteMode.isAvailable('animations')) { return; } // console.log('ripple mousedown', e, e.target, findUpClassName(e.target as HTMLElement, 'c-ripple') === r); if(attachListenerTo.dataset.ripple === '0' || isRippleUnneeded(e)) { return; } else if(touchStartFired) { touchStartFired = false; return; } const {clientX, clientY} = e; drawRipple(clientX, clientY); window.addEventListener('mouseup', handler, {once: true, passive: true}); window.addEventListener('contextmenu', handler, {once: true, passive: true}); }, {passive: true}); } }