diff --git a/utils/transition-group/transition-group-item.directive.ts b/utils/transition-group/transition-group-item.directive.ts new file mode 100644 index 00000000..a9b08f97 --- /dev/null +++ b/utils/transition-group/transition-group-item.directive.ts @@ -0,0 +1,16 @@ +import {Directive, ElementRef} from "@angular/core"; + +@Directive({ + selector: '[transition-group-item]' +}) +export class TransitionGroupItemDirective { + prevPos: any; + newPos: any; + el: HTMLElement; + moved: boolean; + moveCallback: any; + + constructor(elRef: ElementRef) { + this.el = elRef.nativeElement; + } +} diff --git a/utils/transition-group/transition-group.component.less b/utils/transition-group/transition-group.component.less new file mode 100644 index 00000000..8eddb9c9 --- /dev/null +++ b/utils/transition-group/transition-group.component.less @@ -0,0 +1,3 @@ +.list-move { + transition: transform 1s; +} diff --git a/utils/transition-group/transition-group.component.ts b/utils/transition-group/transition-group.component.ts new file mode 100644 index 00000000..14fc2648 --- /dev/null +++ b/utils/transition-group/transition-group.component.ts @@ -0,0 +1,98 @@ +import {TransitionGroupItemDirective} from "./transition-group-item.directive"; +import {AfterViewInit, Component, ContentChildren, OnDestroy, QueryList, ViewEncapsulation} from "@angular/core"; +import {Subscription} from "rxjs"; + +@Component({ + selector: '[transition-group]', + template: '', + styleUrls: ['transition-group.component.less'], + encapsulation: ViewEncapsulation.None +}) +export class TransitionGroupComponent implements AfterViewInit, OnDestroy { + @ContentChildren(TransitionGroupItemDirective) items: QueryList; + private subscription: Subscription; + + ngAfterViewInit() { + this.init(); + this.subscription = this.items.changes.subscribe(items => { + items.forEach(item => item.prevPos = item.newPos || item.prevPos); + items.forEach(this.runCallback); + this.refreshPosition('newPos'); + items.forEach(item => item.prevPos = item.prevPos || item.newPos); // for new items + + const animate = () => { + items.forEach(this.applyTranslation); + this['_forceReflow'] = document.body.offsetHeight; // force reflow to put everything in position + this.items.forEach(this.runTransition.bind(this)); + } + + const willMoveSome = items.some((item) => { + const dx = item.prevPos.left - item.newPos.left; + const dy = item.prevPos.top - item.newPos.top; + return dx || dy; + }); + + if (willMoveSome) { + animate(); + } else { + setTimeout(() => { // for removed items + this.refreshPosition('newPos'); + animate(); + }, 0); + } + }); + } + + ngOnDestroy() { + if (this.subscription) { + this.subscription.unsubscribe(); + } + } + + init() { + this.refreshPosition('prevPos'); + this.refreshPosition('newPos'); + } + + runCallback(item: TransitionGroupItemDirective) { + if (item.moveCallback) { + item.moveCallback(); + } + } + + runTransition(item: TransitionGroupItemDirective) { + if (!item.moved) { + return; + } + const cssClass = 'list-move'; + let el = item.el; + let style: any = el.style; + el.classList.add(cssClass); + style.transform = style.WebkitTransform = style.transitionDuration = ''; + el.addEventListener('transitionend', item.moveCallback = (e: any) => { + if (!e || /transform$/.test(e.propertyName)) { + el.removeEventListener('transitionend', item.moveCallback); + item.moveCallback = null; + el.classList.remove(cssClass); + } + }); + } + + refreshPosition(prop: string) { + this.items.forEach(item => { + item[prop] = item.el.getBoundingClientRect(); + }); + } + + applyTranslation(item: TransitionGroupItemDirective) { + item.moved = false; + const dx = item.prevPos.left - item.newPos.left; + const dy = item.prevPos.top - item.newPos.top; + if (dx || dy) { + item.moved = true; + let style: any = item.el.style; + style.transform = style.WebkitTransform = 'translate(' + dx + 'px,' + dy + 'px)'; + style.transitionDuration = '0s'; + } + } +} diff --git a/utils/transition-group/transition-group.module.ts b/utils/transition-group/transition-group.module.ts new file mode 100644 index 00000000..a1d3d0cd --- /dev/null +++ b/utils/transition-group/transition-group.module.ts @@ -0,0 +1,13 @@ +import {NgModule} from "@angular/core"; +import {CommonModule} from "@angular/common"; +import {TransitionGroupItemDirective} from "./transition-group-item.directive"; +import {TransitionGroupComponent} from "./transition-group.component"; + +@NgModule({ + declarations: [TransitionGroupItemDirective, TransitionGroupComponent], + imports: [CommonModule], + exports: [TransitionGroupItemDirective, TransitionGroupComponent] +}) +export class TransitionGroupModule { + +}