import { createHTMLElement } from '../../utilities';
interface Options {
autoexit?: boolean;
centered?: boolean;
className?: string;
rounded?: boolean;
trigger?: HTMLElement;
unbounded?: boolean;
}
export class FlexyRipple {
options: Options;
surface: HTMLElement;
target: HTMLElement;
onTouchBinded = this.onTouch.bind(this);
constructor(target: HTMLElement, options: Options) {
if (!(target instanceof HTMLElement)) {
throw new TypeError('Argument 1 must be instanceof HTMLElement');
}
this.target = target;
this.surface = createHTMLElement('span', { classList: ['flexy-ripple'] });
this.target.prepend(this.surface);
this.options = {};
this.configure({
autoexit: true,
centered: false,
className: '',
rounded: false,
trigger: this.target,
unbounded: false,
...options,
});
}
configure(options: Options) {
const oldOptions = { ...this.options };
const newOptions = (this.options = { ...oldOptions, ...options });
if (oldOptions.trigger != newOptions.trigger) {
const onTouch = this.onTouchBinded;
if (oldOptions.trigger) {
oldOptions.trigger.removeEventListener('pointerdown', onTouch);
}
if (newOptions.trigger) {
newOptions.trigger.addEventListener('pointerdown', onTouch);
}
}
if (oldOptions.className != newOptions.className) {
if (oldOptions.className) {
this.surface.classList.remove(oldOptions.className);
}
if (newOptions.className) {
this.surface.classList.add(newOptions.className);
}
}
Object.assign(this.surface.style, {
borderRadius: this.options.rounded ? '50%' : 'inherit',
overflow: this.options.unbounded ? 'visible' : 'hidden',
});
}
onTouch(event: PointerEvent): void {
if (event.button !== 0) return;
let pressing = true;
let x, y;
const rect = this.surface.getBoundingClientRect();
if (this.options.centered) {
x = rect.width / 2;
y = rect.height / 2;
} else {
x = event.clientX - rect.x;
y = event.clientY - rect.y;
}
if (!this.options.autoexit) return void this.trigger(x, y);
let exitFunction: () => void;
this.trigger(x, y, (exit) => {
exitFunction = exit;
if (!pressing) exitFunction();
});
globalThis.addEventListener(
'pointerup',
() => {
pressing = false;
exitFunction?.();
},
{ once: true },
);
}
/**
* Trigger a new ripple effect
*/
trigger(x: number, y: number, exitFunction?: (exit: () => void) => void) {
const radius = Math.hypot(
Math.max(x, this.surface.clientWidth - x),
Math.max(y, this.surface.clientHeight - y),
);
const effect = createHTMLElement('span', {
classList: ['flexy-ripple__effect'],
style: {
left: x + 'px',
top: y + 'px',
width: radius * 2 + 'px',
height: radius * 2 + 'px',
},
});
this.surface.append(effect);
const exit = () => {
effect.classList.add('flexy-ripple__effect--exit');
};
const onAnimationEnd = (event: AnimationEvent) => {
switch (event.animationName) {
case 'flexy-ripple-enter': {
if (typeof exitFunction === 'function') {
exitFunction(exit);
} else {
exit();
}
break;
}
case 'flexy-ripple-exit': {
effect.removeEventListener('animationend', onAnimationEnd);
effect.remove();
}
}
};
effect.addEventListener('animationend', onAnimationEnd);
}
destroy() {
if (this.options.trigger) {
this.options.trigger.removeEventListener(
'pointerdown',
this.onTouchBinded,
);
}
this.surface.remove();
FlexyRipple.instances.delete(this.target);
}
static attachTo(target: HTMLElement, options: Options): FlexyRipple {
if (!this.instances.has(target)) {
this.instances.set(target, new FlexyRipple(target, options));
}
return this.instances.get(target)!;
}
static instances = new WeakMap();
}