shoelace/src/components/animation/animation.ts

223 wiersze
6.7 KiB
TypeScript
Czysty Zwykły widok Historia

2021-07-10 00:45:44 +00:00
import { LitElement, html } from 'lit';
2021-05-27 21:00:43 +00:00
import { customElement, property, queryAsync } from 'lit/decorators.js';
2021-07-10 00:45:44 +00:00
import styles from './animation.styles';
import { animations } from './animations';
import { emit } from '~/internal/event';
import { watch } from '~/internal/watch';
2020-08-10 14:45:46 +00:00
2020-08-10 14:04:25 +00:00
/**
* @since 2.0
2020-09-02 21:45:59 +00:00
* @status stable
2020-08-10 14:04:25 +00:00
*
2021-06-25 20:25:46 +00:00
* @event sl-cancel - Emitted when the animation is canceled.
* @event sl-finish - Emitted when the animation finishes.
* @event sl-start - Emitted when the animation starts or restarts.
*
* @slot - The element to animate. If multiple elements are to be animated, wrap them in a single container or use
* multiple animation elements.
2020-08-10 14:04:25 +00:00
*/
2021-03-18 13:04:23 +00:00
@customElement('sl-animation')
2021-03-09 00:14:32 +00:00
export default class SlAnimation extends LitElement {
2021-07-10 00:45:44 +00:00
static styles = styles;
2021-02-26 14:09:13 +00:00
private animation?: Animation;
2021-02-26 14:09:13 +00:00
private hasStarted = false;
2020-08-10 14:04:25 +00:00
2021-03-06 17:01:39 +00:00
@queryAsync('slot') defaultSlot: Promise<HTMLSlotElement>;
2020-12-29 20:21:15 +00:00
/** The name of the built-in animation to use. For custom animations, use the `keyframes` prop. */
2021-07-01 00:04:46 +00:00
@property() name = 'none';
2020-08-10 14:04:25 +00:00
2021-07-01 13:34:00 +00:00
/**
* Plays the animation. When omitted, the animation will be paused. This prop will be automatically removed when the
* animation finishes or gets canceled.
*/
@property({ type: Boolean, reflect: true }) play = false;
2020-08-10 14:04:25 +00:00
/** The number of milliseconds to delay the start of the animation. */
2021-07-01 00:04:46 +00:00
@property({ type: Number }) delay = 0;
2020-08-10 14:04:25 +00:00
/** Determines the direction of playback as well as the behavior when reaching the end of an iteration. */
2021-03-06 17:01:39 +00:00
@property() direction: PlaybackDirection = 'normal';
2020-08-10 14:04:25 +00:00
/** The number of milliseconds each iteration of the animation takes to complete. */
2021-07-01 00:04:46 +00:00
@property({ type: Number }) duration = 1000;
2020-08-10 14:04:25 +00:00
2020-12-29 20:21:15 +00:00
/**
* The easing function to use for the animation. This can be a Shoelace easing function or a custom easing function
* such as `cubic-bezier(0, 1, .76, 1.14)`.
*/
2021-07-01 00:04:46 +00:00
@property() easing = 'linear';
2020-08-10 14:04:25 +00:00
/** The number of milliseconds to delay after the active period of an animation sequence. */
2021-07-01 00:04:46 +00:00
@property({ attribute: 'end-delay', type: Number }) endDelay = 0;
2020-08-10 14:04:25 +00:00
/** Sets how the animation applies styles to its target before and after its execution. */
2021-03-06 17:01:39 +00:00
@property() fill: FillMode = 'auto';
2020-08-10 14:04:25 +00:00
/** The number of iterations to run before the animation completes. Defaults to `Infinity`, which loops. */
@property({ type: Number }) iterations = Infinity;
2020-08-10 14:04:25 +00:00
/** The offset at which to start the animation, usually between 0 (start) and 1 (end). */
2021-07-01 00:04:46 +00:00
@property({ attribute: 'iteration-start', type: Number }) iterationStart = 0;
2020-08-10 14:04:25 +00:00
/** The keyframes to use for the animation. If this is set, `name` will be ignored. */
@property({ attribute: false }) keyframes?: Keyframe[];
2020-08-10 14:04:25 +00:00
/**
* Sets the animation's playback rate. The default is `1`, which plays the animation at a normal speed. Setting this
* to `2`, for example, will double the animation's speed. A negative value can be used to reverse the animation. This
* value can be changed without causing the animation to restart.
*/
2021-07-01 00:04:46 +00:00
@property({ attribute: 'playback-rate', type: Number }) playbackRate = 1;
2020-08-10 14:04:25 +00:00
2021-07-01 13:34:00 +00:00
/** Gets and sets the current animation time. */
get currentTime(): number {
return this.animation?.currentTime ?? 0;
2021-07-01 13:34:00 +00:00
}
set currentTime(time: number) {
if (typeof this.animation !== 'undefined') {
2021-07-01 13:34:00 +00:00
this.animation.currentTime = time;
}
}
2021-03-06 17:01:39 +00:00
connectedCallback() {
super.connectedCallback();
void this.createAnimation();
2021-03-22 15:23:58 +00:00
this.handleAnimationCancel = this.handleAnimationCancel.bind(this);
this.handleAnimationFinish = this.handleAnimationFinish.bind(this);
2020-08-10 14:04:25 +00:00
}
2021-03-06 17:01:39 +00:00
disconnectedCallback() {
super.disconnectedCallback();
this.destroyAnimation();
2020-08-10 14:04:25 +00:00
}
2021-03-06 19:39:48 +00:00
@watch('name')
@watch('delay')
@watch('direction')
@watch('duration')
@watch('easing')
@watch('endDelay')
@watch('fill')
@watch('iterations')
@watch('iterationsStart')
@watch('keyframes')
handleAnimationChange() {
2021-06-15 13:26:35 +00:00
if (!this.hasUpdated) {
return;
}
void this.createAnimation();
2021-03-06 17:01:39 +00:00
}
handleAnimationFinish() {
2021-07-01 13:34:00 +00:00
this.play = false;
this.hasStarted = false;
emit(this, 'sl-finish');
2020-08-10 14:04:25 +00:00
}
handleAnimationCancel() {
2021-07-01 13:34:00 +00:00
this.play = false;
this.hasStarted = false;
emit(this, 'sl-cancel');
2021-03-06 17:01:39 +00:00
}
2021-07-01 13:34:00 +00:00
@watch('play')
handlePlayChange() {
if (typeof this.animation !== 'undefined') {
2021-07-01 13:34:00 +00:00
if (this.play && !this.hasStarted) {
2021-03-06 17:01:39 +00:00
this.hasStarted = true;
emit(this, 'sl-start');
2021-03-06 17:01:39 +00:00
}
if (this.play) {
this.animation.play();
} else {
this.animation.pause();
}
2021-07-01 13:34:00 +00:00
2021-03-06 17:01:39 +00:00
return true;
}
return false;
2021-02-26 14:09:13 +00:00
}
2021-03-06 19:39:48 +00:00
@watch('playbackRate')
2021-02-26 14:09:13 +00:00
handlePlaybackRateChange() {
if (typeof this.animation !== 'undefined') {
2021-03-06 17:01:39 +00:00
this.animation.playbackRate = this.playbackRate;
}
2020-08-10 14:04:25 +00:00
}
2020-09-02 21:45:59 +00:00
handleSlotChange() {
this.destroyAnimation();
void this.createAnimation();
2020-09-02 21:45:59 +00:00
}
2021-03-06 17:01:39 +00:00
async createAnimation() {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- The specified easing may not exist
const easing = animations.easings[this.easing] ?? this.easing;
const keyframes = this.keyframes ?? (animations as unknown as Partial<Record<string, Keyframe[]>>)[this.name];
2021-03-06 17:01:39 +00:00
const slot = await this.defaultSlot;
const element = slot.assignedElements()[0] as HTMLElement | undefined;
2020-09-02 21:45:59 +00:00
if (typeof element === 'undefined' || typeof keyframes === 'undefined') {
2021-03-06 17:01:39 +00:00
return false;
2020-09-02 21:45:59 +00:00
}
2020-08-10 14:04:25 +00:00
this.destroyAnimation();
2020-09-02 21:45:59 +00:00
this.animation = element.animate(keyframes, {
delay: this.delay,
direction: this.direction,
duration: this.duration,
easing,
endDelay: this.endDelay,
fill: this.fill,
iterationStart: this.iterationStart,
iterations: this.iterations
});
2020-08-12 11:35:00 +00:00
this.animation.playbackRate = this.playbackRate;
this.animation.addEventListener('cancel', this.handleAnimationCancel);
this.animation.addEventListener('finish', this.handleAnimationFinish);
2020-08-10 14:04:25 +00:00
2021-07-01 13:34:00 +00:00
if (this.play) {
2020-08-11 22:47:02 +00:00
this.hasStarted = true;
emit(this, 'sl-start');
2021-07-01 13:34:00 +00:00
} else {
this.animation.pause();
}
2021-03-06 17:01:39 +00:00
return true;
2020-08-10 14:04:25 +00:00
}
destroyAnimation() {
if (typeof this.animation !== 'undefined') {
this.animation.cancel();
this.animation.removeEventListener('cancel', this.handleAnimationCancel);
this.animation.removeEventListener('finish', this.handleAnimationFinish);
2020-08-11 22:47:02 +00:00
this.hasStarted = false;
}
2020-08-10 14:04:25 +00:00
}
/** Clears all KeyframeEffects caused by this animation and aborts its playback. */
2021-02-26 14:09:13 +00:00
cancel() {
this.animation?.cancel();
2020-08-10 14:04:25 +00:00
}
/** Sets the playback time to the end of the animation corresponding to the current playback direction. */
2021-02-26 14:09:13 +00:00
finish() {
this.animation?.finish();
2020-08-10 14:04:25 +00:00
}
render() {
2021-03-06 17:01:39 +00:00
return html` <slot @slotchange=${this.handleSlotChange}></slot> `;
2020-08-10 14:04:25 +00:00
}
}
2021-03-12 14:09:08 +00:00
declare global {
interface HTMLElementTagNameMap {
'sl-animation': SlAnimation;
}
}