@sandlada/vue-mdc
Version:

304 lines (303 loc) • 8.87 kB
JavaScript
/**
* @license
* Copyright 2022 Google LLC
* SPDX-License-Identifier: Apache-2.0
* @link
* https://github.com/material-components/material-web/blob/main/ripple/internal/ripple.ts
*
* [Modified by Sandlada & Kai Orion]
*
* @license
* Copyright 2025 Sandlada & Kai Orion
* SPDX-License-Identifier: MIT
*/
import { useAttachable } from "../../internals/controller/use-attachable.js";
import { isServer } from "../../utils/is-server.js";
import { MaterialDesignSystem } from "../../utils/material-design-system.js";
import { RippleConfiguration } from "./ripple-configuration.js";
import { RippleState } from "./ripple-state.js";
const Events = [
"click",
"contextmenu",
"pointercancel",
"pointerdown",
"pointerenter",
"pointerleave",
"pointerup"
];
class RippleAttachableController {
host;
_hover = false;
_pressed = false;
get hover() {
return this._hover;
}
set hover(hover) {
if (this._hover === hover || !this.host.value) {
return;
}
this._hover = hover;
if (hover) {
this.host.value.setAttribute("hover", ``);
} else if (this.host.value.hasAttribute("hover")) {
this.host.value.removeAttribute("hover");
}
}
get pressed() {
return this._pressed;
}
set pressed(pressed) {
if (this._pressed === pressed || !this.host.value) {
return;
}
this._pressed = pressed;
if (pressed) {
this.host.value.setAttribute("pressed", ``);
} else if (this.host.value.hasAttribute("pressed")) {
this.host.value.removeAttribute("pressed");
}
}
get disabled() {
if (!this.host.value) {
return false;
}
return this.host.value.hasAttribute("disabled");
}
state = RippleState.INACTIVE;
startEvent = null;
checkBoundsAfterContextMenu = false;
initialSize = 0;
rippleScale = "";
rippleSize = "";
growAnimation = null;
constructor(host) {
this.host = host;
useAttachable(host, (prev, next) => {
if (isServer()) {
return;
}
for (const event of Events) {
prev?.removeEventListener(event, this.handleEvent);
next?.addEventListener(event, this.handleEvent);
}
});
}
/**
* Event handles
*/
handlePointerenter() {
if (this.disabled)
return;
this.hover = true;
}
handlePointerleave() {
if (this.disabled)
return;
this.hover = false;
if (this.state !== RippleState.INACTIVE) {
this.endPressAnimation();
}
}
handlePointerup() {
if (this.disabled)
return;
if (this.state === RippleState.HOLDING) {
this.state = RippleState.WAITING_FOR_CLICK;
return;
}
if (this.state === RippleState.TOUCH_DELAY) {
this.state = RippleState.WAITING_FOR_CLICK;
this.startPressAnimation(this.startEvent || void 0);
return;
}
}
async handlePointerdown(event) {
if (this.disabled)
return;
this.startEvent = event;
if (!this.isTouch(event)) {
this.state = RippleState.WAITING_FOR_CLICK;
this.startPressAnimation(event);
return;
}
if (this.checkBoundsAfterContextMenu && !this.inBounds(event)) {
return;
}
this.checkBoundsAfterContextMenu = false;
this.state = RippleState.TOUCH_DELAY;
await new Promise((resolve) => {
setTimeout(resolve, RippleConfiguration.touchDelayMs);
});
if (this.state !== RippleState.TOUCH_DELAY)
return;
this.state = RippleState.HOLDING;
this.startPressAnimation(event);
}
handlePointercancel(event) {
if (!this.shouldReactToEvent(event))
return;
this.endPressAnimation();
}
handleClick() {
if (this.disabled)
return;
if (this.state === RippleState.WAITING_FOR_CLICK) {
this.endPressAnimation();
return;
}
if (this.state === RippleState.INACTIVE) {
this.startPressAnimation();
this.endPressAnimation();
}
}
handleContextmenu() {
if (this.disabled)
return;
this.checkBoundsAfterContextMenu = true;
this.endPressAnimation();
}
/**
* Animations about
*/
startPressAnimation(positionEvent) {
this.pressed = true;
this.growAnimation?.cancel();
this.determineRippleSize();
const { startPoint, endPoint } = this.getTranslationCoordinates(positionEvent);
const translateStart = `${startPoint.x}px, ${startPoint.y}px`;
const translateEnd = `${endPoint.x}px, ${endPoint.y}px`;
if (this.host.value === null)
return;
this.growAnimation = this.host.value.animate({
top: [0, 0],
left: [0, 0],
height: [this.rippleSize, this.rippleSize],
width: [this.rippleSize, this.rippleSize],
transform: [
`translate(${translateStart}) scale(1)`,
`translate(${translateEnd}) scale(${this.rippleScale})`
]
}, {
pseudoElement: RippleConfiguration.pressPseudo,
duration: RippleConfiguration.pressGrowMs,
easing: MaterialDesignSystem.Motion.Easing.Standard,
fill: RippleConfiguration.animationFill
});
}
getTranslationCoordinates(positionEvent) {
if (this.host.value === null)
return;
const { height, width } = this.host.value.getBoundingClientRect();
const endPoint = {
x: (width - this.initialSize) / 2,
y: (height - this.initialSize) / 2
};
let startPoint = {
x: width / 2,
y: height / 2
};
if (positionEvent instanceof PointerEvent) {
startPoint = this.getNormalizedPointerEventCoords(positionEvent);
}
startPoint = {
x: startPoint.x - this.initialSize / 2,
y: startPoint.y - this.initialSize / 2
};
return { startPoint, endPoint };
}
getNormalizedPointerEventCoords(pointerEvent) {
if (this.host.value === null)
return;
const { scrollX, scrollY } = window;
const { left, top } = this.host.value.getBoundingClientRect();
const documentX = scrollX + left;
const documentY = scrollY + top;
const { pageX, pageY } = pointerEvent;
return { x: pageX - documentX, y: pageY - documentY };
}
async endPressAnimation() {
this.state = RippleState.INACTIVE;
const animation = this.growAnimation;
let pressAnimationPlayState = Infinity;
if (typeof animation?.currentTime === "number") {
pressAnimationPlayState = animation.currentTime;
} else if (animation?.currentTime) {
pressAnimationPlayState = animation.currentTime.to("ms").value;
}
if (pressAnimationPlayState >= RippleConfiguration.minimumPressMs) {
this.pressed = false;
return;
}
await new Promise((resolve) => {
setTimeout(resolve, RippleConfiguration.minimumPressMs - pressAnimationPlayState);
});
if (this.growAnimation !== animation) {
return;
}
this.pressed = false;
}
determineRippleSize() {
if (this.host.value === null)
return;
const { height, width } = this.host.value.getBoundingClientRect();
const maxDim = Math.max(height, width);
const softEdgeSize = Math.max(RippleConfiguration.softEdgeContainerRadio * maxDim, RippleConfiguration.softEdgeMinimumSize);
const initialSizeNew = Math.floor(maxDim * RippleConfiguration.initialOriginScale);
const hypotenuse = Math.sqrt(width ** 2 + height ** 2);
const maxRadius = hypotenuse + RippleConfiguration.padding;
this.initialSize = initialSizeNew;
this.rippleScale = `${(maxRadius + softEdgeSize) / this.initialSize}`;
this.rippleSize = `${this.initialSize}px`;
}
// @ts-ignore
inBounds({ x, y }) {
if (this.host.value === null)
return;
const { top, left, bottom, right } = this.host.value.getBoundingClientRect();
return x >= left && x <= right && y >= top && y <= bottom;
}
isTouch({ pointerType }) {
return pointerType === "touch";
}
shouldReactToEvent(event) {
if (this.disabled || !event.isPrimary)
return false;
if (this.startEvent && this.startEvent.pointerId !== event.pointerId) {
return false;
}
if (event.type === "pointerenter" || event.type === "pointerleave") {
return !this.isTouch(event);
}
const isPrimaryButton = event.buttons === 1;
return this.isTouch(event) || isPrimaryButton;
}
handleEvent = async (e) => {
switch (e.type) {
case "click":
this.handleClick();
break;
case "contextmenu":
this.handleContextmenu();
break;
case "pointercancel":
this.handlePointercancel(e);
break;
case "pointerdown":
await this.handlePointerdown(e);
break;
case "pointerenter":
this.handlePointerenter();
break;
case "pointerleave":
this.handlePointerleave();
break;
case "pointerup":
this.handlePointerup();
break;
}
};
}
export {
RippleAttachableController
};
//# sourceMappingURL=ripple-attachable-controller.js.map