Skip to content

Commit

Permalink
Merge pull request #1736 from exadel-inc/epic/esl-animate-4.0.0
Browse files Browse the repository at this point in the history
epic: esl-animate documentation, mixin and cleanup (#1281)
  • Loading branch information
ala-n authored Jun 26, 2023
2 parents e58a486 + f4fc7e1 commit 5ee712b
Show file tree
Hide file tree
Showing 10 changed files with 600 additions and 77 deletions.
2 changes: 2 additions & 0 deletions pages/src/localdev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {
ESLFootnotes,
ESLTooltip,
ESLAnimate,
ESLAnimateMixin,
ESLShare,
ESLRelatedTarget
} from '../../src/modules/all';
Expand Down Expand Up @@ -101,6 +102,7 @@ ESLNote.register();
ESLTooltip.register();

ESLAnimate.register();
ESLAnimateMixin.register();

ESLShare.config(() => fetch('/assets/share/config.json').then((response) => response.json()));
ESLShare.register();
Expand Down
27 changes: 18 additions & 9 deletions pages/views/examples/animate.njk
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,20 @@ aside:
<h2>Simple animation</h2>

<div class="simple-grid simple-grid-fw">
{% for i in range(0, 4) -%}
<esl-animate class="esl-animate-fade simple-grid-cell bg-green round">
{% for i in range(0, 2) -%}
<esl-animate repeat class="esl-animate-fade simple-grid-cell bg-green round text-white h3 text-center p-4">
esl-animate-fade
</esl-animate>
<esl-animate class="esl-animate-slide-up simple-grid-cell bg-green round">
<esl-animate repeat class="esl-animate-slide-up simple-grid-cell bg-green round text-white h3 text-center p-4">
esl-animate-slide-up
</esl-animate>
<esl-animate class="esl-animate-slide-left simple-grid-cell bg-green round">
<esl-animate repeat class="esl-animate-slide-left simple-grid-cell bg-green round text-white h3 text-center p-4">
esl-animate-slide-left
</esl-animate>
<esl-animate class="esl-animate-slide-down simple-grid-cell bg-green round">
<esl-animate repeat class="esl-animate-slide-down simple-grid-cell bg-green round text-white h3 text-center p-4">
esl-animate-slide-down
</esl-animate>
<esl-animate class="esl-animate-slide-right simple-grid-cell bg-green round">
<esl-animate repeat class="esl-animate-slide-right simple-grid-cell bg-green round text-white h3 text-center p-4">
esl-animate-slide-right
</esl-animate>
{%- endfor %}
Expand All @@ -37,9 +37,18 @@ aside:
<h2>Grouped and repetitive animation</h2>
<ul class="round simple-grid">
<esl-animate target="::parent::child(li)" group repeat></esl-animate>
{% for i in range(0, 72) -%}
<li class="bg-blue round esl-animate-slide-up simple-grid-cell {{ 'hide-xs' if i >= 12 }}"
is="my-comp my-com-2"></li>
{% for i in range(0, 36) -%}
<li class="bg-blue round esl-animate-slide-up simple-grid-cell {{ 'hide-xs' if i >= 12 }}"></li>
{%- endfor %}
</ul>
</section>

<section>
<h2>Grouped animation with custom delay by mixin</h2>
<ul class="round simple-grid">
{% for i in range(0, 36) -%}
<li class="bg-orange round esl-animate-slide-left simple-grid-cell {{ 'hide-xs' if i >= 12 }}"
esl-animate="{group: true, groupDelay: 50}"></li>
{%- endfor %}
</ul>
</section>
140 changes: 94 additions & 46 deletions src/modules/esl-animate/README.md
Original file line number Diff line number Diff line change
@@ -1,61 +1,80 @@
# [ESL](https://exadel-inc.github.io/esl/) Animate

Version: *1.0.0*.
Version: *2.0.0*.

Authors: *Anna-Mariia Petryk*, *Alexey Stsefanovich (ala'n)*, *Julia Murashko*.

**_Important Notice: the component is under beta version, it is tested and ready to use but be aware of its potential critical API changes._**
Authors: *Anna-Mariia Petryk*, *Feoktyst Shovchko*, *Alexey Stsefanovich (ala'n)*, *Julia Murashko*.

<a name="intro"></a>

**ESLAnimate** is a module to animate items on viewport intersecting.
`esl-animate` is a module that provides service and its DOM API to animate elements on their intersection with the viewport

The module consists of JS API `ESLAnimateService`, Custom element `ESLAnimate`, and Mixin element `ESLAnimateMixin`.
ESLAnimateService is a core of the `esl-animate` module. Element needs to be observed by ESLAnimateService in order to be
animated.

### Module Features:
- Add class(es) when observed elements enter the viewport area
- Animate in a group, that allows adding an animation delay for each next item in the intersection queue
- Automatic re-animation after the item exits the viewport area
- Pre-defined CSS animations
- `esl-animate-fade`
- `esl-animate-slide-left`
- `esl-animate-slide-right`
- `esl-animate-slide-up`
- `esl-animate-slide-down`

Features:
- Add class(es) when observed elements enter viewport area
- Support group animation that allows item delay its animation on the passed time after previously animated item
- Support forward and backward animations directions
- Support automatic re-animate after item exit viewport area
- JS API (Service) + Custom Element - Plugin for simple initialization
## `ESLAnimateService`

## ESLAnimateService
ESLAnimateService is a core of esl-animate module. Element needs to be observed by ESLAnimateService
in order to be animated.
**ESLAnimateService** provides a way to asynchronously add animation on the intersection of a target element with a viewport. It is based on [Intersection Observer Api](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API) and serves as a core functionality for `ESLAnimate` and `ESLAnimateMixin` elements.

### Service static and instance API:

### Service API:
- `ESLAnimateService.observe(els, config)` - method to start element observation
- `els` - Element or array of Elements to observe and animate
- `config` - an optional ESLAnimateConfig object to describe the behavior of the animation functionality
- `els` - element or array of elements to observe and animate
- `config` - optional ESLAnimateConfig object to describe the behavior of the animation functionality
- `ESLAnimateService.unobserve(els)` - method to unsubscribe ESLAnimateService from observing elements
- `ESLAnimateService.isObserved(el)` - check if element observed by ESLAnimateService

### ESLAnimateConfig (Configuration API)
- `cls` - CSS class or classes to control animation (`in` by default)
(supports ESL extended class definition syntax, [CSSClassUtil](../esl-utils/dom/class.ts))
- `group` (boolean) - enable group animation for items
(item will start animation with a delay after previous item animation start)
- `groupDelay` - number of milliseconds to delay animation in group
- `ESLAnimateService.isObserved(el)` - check if the element is observed by ESLAnimateService

You can also create a separate (from global) `ESLAnimateService` instance by calling its constructor.

### Configuration API: `ESLAnimateConfig`
- `cls` (`in` by default) - CSS class(es) to control animation.
The control class(es) will be added to the observed element(s) after they had intersected with the viewport area.
Service supports ESL extended class definition syntax, [CSSClassUtils](../esl-utils/dom/class.ts)
- `group` (boolean) - enable group animation for items, hence take `groupDelay` value into account while performing
animation (item will start the animation with a delay after the previous item's animation start)
- `groupDelay` (`100` by default) - number of milliseconds animation delay in group
- `repeat` (boolean) - refresh (re-animate) items when they became invisible (exit viewport)
- `force` (boolean) - if true then allows to re-animate items when ESLAnimateService subscribed
on already animated item
- `ratio` (0.2|0.4|0.6|0.8) - intersection ratio to consider element as visible.
Only 0.2 (20%), 0.4 (40%), 0.6 (60%), 0.8 (80%) values are allowed due to share of IntersectionObserver instance
with a fixed set of thresholds defined.

## ESLAnimate
ESLAnimate (`<esl-animate>`) - custom element to automatically initialize ESLAnimateService from html

### Element API
- `target` - target element or elements to animate, defined by [ESLTraversingQuery](../esl-traversing-query/README.md)
Default: empty (animates itself)
- `cls` - CSS class or classes to control animation (`in` by default)
(supports ESL extended class definition syntax, [CSSClassUtil](../esl-utils/dom/class.ts))
- `group` (boolean) - enable group animation for items
(item will start animation with a delay after previous item animation start)
- `group-delay` - number of milliseconds to delay animation in group
- `force` (boolean) - allows to re-animate items when ESLAnimateService subscribed
on already animated item if set to true
- `ratio` (0.2|0.4|0.6|0.8) - intersection ratio to consider an element as visible
Only 0.2 (20%), 0.4 (40%), 0.6 (60%), 0.8 (80%) values are allowed due to sharing of IntersectionObserver instance
with a fixed set of thresholds defined

## `ESLAnimate` custom element

**ESLAnimate** is a custom element that subscribes `ESLAnimateService` to elements from html.

To use ESLAnimate you need to include the following code:
```js
ESLAnimate.register();
```

### `ESLAnimate` Attributes | Properties:
- `target` - target element(s) to animate, defined by [ESLTraversingQuery](../esl-traversing-query/README.md). By default target value is empty, meaning component will animate itself
- `cls` (`in` by default) - CSS class(es) to control animation. The control class(es) will be added to observed element(s), after they had intersected with vieport area. Service supports ESL extended class definition syntax, [CSSClassUtils](../esl-utils/dom/class.ts)
- `group` (boolean) - enable group animation for items, hence take `groupDelay` value into account while performing
animation (item will start the animation with a delay after the previous item's animation start)
- `groupDelay` (`100` by default) - number of milliseconds animation delay in group
- `repeat` (boolean) - refresh (re-animate) items when they became invisible (exit viewport)
- `ratio` - number of intersection ratio to consider element as visible
Only 0.2 (20%), 0.4 (40%), 0.6 (60%), 0.8 (80%) values are allowed due to share of IntersectionObserver instance
with a fixed set of thresholds defined.
- `force` (boolean) - allows to re-animate items when ESLAnimateService subscribed
on already animated item if set to true
- `ratio` (0.2|0.4|0.6|0.8) - intersection ratio to consider an element as visible
Only 0.2 (20%), 0.4 (40%), 0.6 (60%), 0.8 (80%) values are allowed due to the share of the IntersectionObserver instance
with a fixed set of thresholds defined

By default, attributes `group`, `repeat`, and `target` are observed, meaning the animation sequence will restart once
theese attributes are changed. Additionally, you can do the re-animation manually by calling the instance method `reanimate()`.

### Use cases
- plugin (target attribute defined)
Expand All @@ -67,11 +86,40 @@ with a fixed set of thresholds defined.
...
</ul>
```
Note: `<esl-animate>` hidden (`display: none`) by default when there is no content inside
**Note: `<esl-animate>` hidden (`display: none`) by default when there is no content inside**

- wrapper (no target attribute defined)
```html
<esl-animate class="esl-animate-fade" cls="in">
...HTML Content...
</esl-animate>
```

## `ESLAnimateMixin`

**ESLAnimateMixin** is an ESL mixin attribute that automatically subscribes `ESLAnimateService` to the element from html.

To use ESLAnimateMixin you need to include the following code:
```js
ESLAnimateMixin.register();
```

### `ESLAnimateMixin` Attributes | Properties:
- `esl-animate-mixin` - json attribute containing following properties:
- `cls` (`in` by default) - CSS class(es) to control animation. The control class(es) will be added to observed element(s), after they had intersected with vieport area. Service supports ESL extended class definition syntax, [CSSClassUtils](../esl-utils/dom/class.ts)
- `repeat` (boolean) - refresh (re-animate) items when they became invisible (exit viewport)
- `ratio` (0.2|0.4|0.6|0.8) - intersection ratio to consider an element as visible
Only 0.2 (20%), 0.4 (40%), 0.6 (60%), 0.8 (80%) values are allowed due to sharing of the IntersectionObserver instance
with a fixed set of thresholds defined

Apart from `ESLAnimate` module mixin doesn't observe any of the element attributes. But you can do the re-animation manually by calling instance method `reanimate()`.

### Use cases
- default declaration
```html
<div esl-animate>...HTML Content...</div>
```
- custom config
```html
<div esl-animate="{repeat: true, ratio: 0.2, cls: 'in'}">...HTML Content...</div>
```
1 change: 1 addition & 0 deletions src/modules/esl-animate/core.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export type {ESLAnimateShape} from './core/esl-animate.shape';

export * from './core/esl-animate';
export * from './core/esl-animate-mixin';
export * from './core/esl-animate-service';
50 changes: 50 additions & 0 deletions src/modules/esl-animate/core/esl-animate-mixin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import {ESLMixinElement} from '../../esl-mixin-element/core';
import {ready, jsonAttr} from '../../esl-utils/decorators';
import {ExportNs} from '../../esl-utils/environment/export-ns';

import {ESLAnimateService} from './esl-animate-service';
import type {ESLAnimateConfig} from './esl-animate-service';

/**
* ESLAnimateMixin - custom mixin element for quick {@link ESLAnimateService} attaching
*
* Use example:
* `<div esl-animate>Content</div>`
*
* Supports additional parameters:
* `<div esl-animate={repeat: true, ratio: 0.8, cls: 'in'}>Content</div>`
*/
@ExportNs('AnimateMixin')
export class ESLAnimateMixin extends ESLMixinElement {
public static override is = 'esl-animate';

public static defaultConfig: ESLAnimateConfig = {
force: true
};

@jsonAttr({name: ESLAnimateMixin.is})
public options?: ESLAnimateConfig;

protected mergeDefaultParams(): ESLAnimateConfig {
const type = this.constructor as typeof ESLAnimateMixin;
return Object.assign({}, type.defaultConfig, this.options);
}

@ready
public override connectedCallback(): void {
super.connectedCallback();
this.reanimate();
}

@ready
public override disconnectedCallback(): void {
super.disconnectedCallback();
ESLAnimateService.unobserve(this.$host);
}

/** Reinitialize {@link ESLAnimateService} for target */
public reanimate(): void {
ESLAnimateService.unobserve(this.$host);
ESLAnimateService.observe(this.$host, this.mergeDefaultParams());
}
}
41 changes: 19 additions & 22 deletions src/modules/esl-animate/core/esl-animate-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,12 @@ export interface ESLAnimateConfig {
}

/** ESLAnimateService animation inner options. Contains system animation properties */
interface ESLAnimateConfigInner extends Required<ESLAnimateConfig> {
interface ESLAnimateConfigInner extends ESLAnimateConfig {
// Required parts
cls: string;
ratio: number;
groupDelay: number;

/** (private) animation requested */
_timeout?: number;
/** (private) marker to unobserve */
Expand All @@ -43,7 +48,7 @@ interface ESLAnimateConfigInner extends Required<ESLAnimateConfig> {
export class ESLAnimateService {

/** ESLAnimateService default animation configuration */
protected static DEFAULT_CONFIG: ESLAnimateConfig = {cls: 'in', groupDelay: 100, ratio: 0.4};
protected static DEFAULT_CONFIG: ESLAnimateConfigInner = {cls: 'in', groupDelay: 100, ratio: 0.4};
/** ESLAnimationService IntersectionObserver properties */
protected static OPTIONS_OBSERVER: IntersectionObserverInit = {threshold: [0.001, 0.2, 0.4, 0.6, 0.8]};

Expand All @@ -63,7 +68,7 @@ export class ESLAnimateService {

/** @returns if service observing target */
public static isObserved(target: Element): boolean {
return !!this.instance.getConfigFor(target);
return this.instance.isObserved(target);
}

@memoize()
Expand All @@ -83,7 +88,8 @@ export class ESLAnimateService {
* @param config - optional animation configuration
*/
public observe(el: Element, config: ESLAnimateConfig = {}): void {
const cfg = this.setConfigFor(el, config);
const cfg = Object.assign({}, ESLAnimateService.DEFAULT_CONFIG, config);
this._configMap.set(el, cfg);
cfg.force && CSSClassUtils.remove(el, cfg.cls);
this._io.observe(el);
}
Expand All @@ -94,11 +100,16 @@ export class ESLAnimateService {
this._configMap.delete(el);
}

/** @returns if service observing target */
public isObserved(target: Element): boolean {
return !!this._configMap.get(target);
}

/** Intersection observable callback */
@bind
protected onIntersect(entries: IntersectionObserverEntry[]): void {
entries.forEach(({target, intersectionRatio, isIntersecting}: IntersectionObserverEntry) => {
const config = this.getConfigFor(target);
const config = this._configMap.get(target);
if (!config) return;

// Item will be marked as visible in case it intersecting to the viewport with a ratio grater then passed visibleRatio
Expand All @@ -123,7 +134,7 @@ export class ESLAnimateService {
protected onAnimate(): void {
let time = -1;
this._entries.forEach((target) => {
const config = this.getConfigFor(target);
const config = this._configMap.get(target);
if (!config) return;

if (config._timeout) window.clearTimeout(config._timeout);
Expand All @@ -139,26 +150,12 @@ export class ESLAnimateService {

/** Animates passed item */
protected onAnimateItem(item: Element): void {
const config = this.getConfigFor(item);
const config = this._configMap.get(item);
if (!config) return;

CSSClassUtils.add(item, config.cls);
this._entries.delete(item);

if (config._unsubscribe) {
this._io.unobserve(item);
this._configMap.delete(item);
}
}

/** Returns config */
protected getConfigFor(el: Element): ESLAnimateConfigInner | undefined {
return this._configMap.get(el);
}
/** Returns config */
protected setConfigFor(el: Element, config: ESLAnimateConfig): ESLAnimateConfigInner {
const cfg = Object.assign({}, ESLAnimateService.DEFAULT_CONFIG, config) as ESLAnimateConfigInner;
this._configMap.set(el, cfg);
return cfg;
if (config._unsubscribe) this.unobserve(item);
}
}
Loading

0 comments on commit 5ee712b

Please sign in to comment.