UNPKG

@sandlada/vue-mdc

Version:

![Vue MDC Logo](https://raw.githubusercontent.com/sandlada/vue-mdc/refs/heads/main/docs/vue-mdc-cover.png)

304 lines (303 loc) 8.87 kB
/** * @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