import { Component, ChangeDetectorRef, Input, Output, HostBinding, ElementRef, SimpleChanges,
ChangeDetectionStrategy, EventEmitter, Renderer, OnDestroy, OnChanges } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { Subject } from 'rxjs/Subject';
import 'rxjs/add/operator/debounceTime';
import { SplitAreaDirective } from './splitArea.directive';
export interface IAreaData {
component: SplitAreaDirective;
sizeUser: number | null;
size: number;
orderUser: number | null;
order: number;
minPixel: number;
}
interface Point {
x: number;
y: number;
}
@Component({
selector: 'split',
changeDetection: ChangeDetectionStrategy.OnPush,
styles: [`
:host {
display: flex;
flex-wrap: nowrap;
justify-content: flex-start;
align-items: stretch;
flex-direction: row;
}
:host.vertical {
flex-direction: column;
}
split-gutter {
flex-grow: 0;
flex-shrink: 0;
background-color: #eeeeee;
background-position: center center;
background-repeat: no-repeat;
}
:host.vertical split-gutter {
width: 100%;
}
:host /deep/ split-area {
transition: flex-basis 0.3s;
}
:host.notransition /deep/ split-area {
transition: none !important;
}
:host /deep/ split-area.hided {
flex-basis: 0 !important;
overflow: hidden !important;
}
:host.vertical /deep/ split-area.hided {
max-width: 0;
}
`],
template: `
`,
})
export class SplitComponent implements OnChanges, OnDestroy {
@Input() direction: string = 'horizontal';
@Input() width: number;
@Input() height: number;
@Input() gutterSize: number = 10;
@Input() disabled: boolean = false;
@Input() visibleTransition: boolean = false;
@Output() dragStart = new EventEmitter>(false);
@Output() dragProgress = new EventEmitter>(false);
@Output() dragEnd = new EventEmitter>(false);
visibleTransitionEndInternal = new Subject>();
@Output() visibleTransitionEnd = this.visibleTransitionEndInternal.asObservable().debounceTime(20);
@HostBinding('class.vertical') get styleFlexDirection() {
return this.direction === 'vertical';
}
@HostBinding('style.flex-direction') get styleFlexDirectionStyle() {
return this.direction === 'horizontal' ? 'row' : 'column';
}
@HostBinding('class.notransition') get dragging() {
// prevent animation of areas when visibleTransition is false, or resizing
return !this.visibleTransition || this.isDragging;
}
@HostBinding('style.width') get styleWidth() {
return (this.width && !isNaN(this.width) && this.width > 0) ? this.width + 'px' : '100%';
}
@HostBinding('style.height') get styleHeight() {
return (this.height && !isNaN(this.height) && this.height > 0) ? this.height + 'px' : '100%';
}
private get visibleAreas(): IAreaData[] {
return this.areas.filter(a => a.component.visible);
}
private get nbGutters(): number {
return this.visibleAreas.length - 1;
}
public areas: Array = [];
private minPercent: number = 5;
private isDragging: boolean = false;
private containerSize: number = 0;
private areaASize: number = 0;
private areaBSize: number = 0;
private eventsDragFct: Array = [];
constructor(private cdRef: ChangeDetectorRef,
private elementRef: ElementRef,
private renderer: Renderer) {}
public ngOnChanges(changes: SimpleChanges) {
if(changes['gutterSize'] || changes['disabled']) {
this.refresh();
}
}
public addArea(component: SplitAreaDirective, orderUser: number | null, sizeUser: number | null, minPixel: number) {
this.areas.push({
component,
orderUser,
order: -1,
sizeUser,
size: -1,
minPixel
});
this.refresh();
}
public updateArea(component: SplitAreaDirective, orderUser: number | null, sizeUser: number | null, minPixel: number) {
const item = this.areas.find(a => a.component === component);
if(item) {
item.orderUser = orderUser;
item.sizeUser = sizeUser;
item.minPixel = minPixel;
this.refresh();
}
}
public removeArea(area: SplitAreaDirective) {
const item = this.areas.find(a => a.component === area);
if(item) {
const index = this.areas.indexOf(item);
this.areas.splice(index, 1);
this.areas.forEach((a, i) => a.order = i * 2);
this.refresh();
}
}
public hideArea(area: SplitAreaDirective) {
const item = this.areas.find(a => a.component === area);
if(item) {
this.refresh();
}
}
public showArea(area: SplitAreaDirective) {
const item = this.areas.find(a => a.component === area);
if(item) {
this.refresh();
}
}
public isLastVisibleArea(area: IAreaData) {
const visibleAreas = this.visibleAreas;
return visibleAreas.length > 0 ? area === visibleAreas[visibleAreas.length - 1] : false;
}
private refresh() {
this.stopDragging();
const visibleAreas = this.visibleAreas;
// ORDERS: Set css 'order' property depending on user input or added order
const nbCorrectOrder = this.areas.filter(a => a.orderUser !== null && !isNaN(a.orderUser)).length;
if(nbCorrectOrder === this.areas.length) {
this.areas.sort((a, b) => +a.orderUser - +b.orderUser);
}
this.areas.forEach((a, i) => {
a.order = i * 2;
a.component.setStyle('order', a.order);
});
// SIZES: Set css 'flex-basis' property depending on user input or equal sizes
const totalSize = visibleAreas.map(a => a.sizeUser).reduce((acc, s) => acc + s, 0);
const nbCorrectSize = visibleAreas.filter(a => a.sizeUser !== null && !isNaN(a.sizeUser) && a.sizeUser >= this.minPercent).length;
if(totalSize < 99.99 || totalSize > 100.01 || nbCorrectSize !== visibleAreas.length) {
const size = Number((100 / visibleAreas.length).toFixed(3));
visibleAreas.forEach(a => a.size = size);
} else {
visibleAreas.forEach(a => a.size = Number(a.sizeUser));
}
this.refreshStyleSizes();
this.cdRef.markForCheck();
}
private refreshStyleSizes() {
const visibleAreas = this.visibleAreas;
const f = this.gutterSize * this.nbGutters / visibleAreas.length;
visibleAreas.forEach(a => a.component.setStyle('flex-basis', `calc( ${a.size}% - ${f}px )`));
}
public startDragging(startEvent: MouseEvent | TouchEvent, gutterOrder: number) {
startEvent.preventDefault();
if(this.disabled) {
return;
}
const areaA = this.areas.find(a => a.order === gutterOrder - 1);
const areaB = this.areas.find(a => a.order === gutterOrder + 1);
if(!areaA || !areaB) {
return;
}
const prop = (this.direction === 'horizontal') ? 'offsetWidth' : 'offsetHeight';
this.containerSize = this.elementRef.nativeElement[prop];
this.areaASize = this.containerSize * areaA.size / 100;
this.areaBSize = this.containerSize * areaB.size / 100;
let start: Point;
if(startEvent instanceof MouseEvent) {
start = {
x: startEvent.screenX,
y: startEvent.screenY
};
}
else if(startEvent instanceof TouchEvent) {
start = {
x: startEvent.touches[0].screenX,
y: startEvent.touches[0].screenY
};
}
else {
return;
}
this.eventsDragFct.push( this.renderer.listenGlobal('document', 'mousemove', e => this.dragEvent(e, start, areaA, areaB)) );
this.eventsDragFct.push( this.renderer.listenGlobal('document', 'touchmove', e => this.dragEvent(e, start, areaA, areaB)) );
this.eventsDragFct.push( this.renderer.listenGlobal('document', 'mouseup', e => this.stopDragging()) );
this.eventsDragFct.push( this.renderer.listenGlobal('document', 'touchend', e => this.stopDragging()) );
this.eventsDragFct.push( this.renderer.listenGlobal('document', 'touchcancel', e => this.stopDragging()) );
areaA.component.lockEvents();
areaB.component.lockEvents();
this.isDragging = true;
this.notify('start');
}
private dragEvent(event: MouseEvent | TouchEvent, start: Point, areaA: IAreaData, areaB: IAreaData) {
if(!this.isDragging) {
return;
}
let end: Point;
if(event instanceof MouseEvent) {
end = {
x: event.screenX,
y: event.screenY
};
}
else if(event instanceof TouchEvent) {
end = {
x: event.touches[0].screenX,
y: event.touches[0].screenY
};
}
else {
return;
}
this.drag(start, end, areaA, areaB);
}
private drag(start: Point, end: Point, areaA: IAreaData, areaB: IAreaData) {
const offsetPixel = (this.direction === 'horizontal') ? (start.x - end.x) : (start.y - end.y);
const newSizePixelA = this.areaASize - offsetPixel;
const newSizePixelB = this.areaBSize + offsetPixel;
if(newSizePixelA <= areaA.minPixel && newSizePixelB < areaB.minPixel) {
return;
}
let newSizePercentA = newSizePixelA / this.containerSize * 100;
let newSizePercentB = newSizePixelB / this.containerSize * 100;
if(newSizePercentA <= this.minPercent) {
newSizePercentA = this.minPercent;
newSizePercentB = areaA.size + areaB.size - this.minPercent;
} else if(newSizePercentB <= this.minPercent) {
newSizePercentB = this.minPercent;
newSizePercentA = areaA.size + areaB.size - this.minPercent;
} else {
newSizePercentA = Number(newSizePercentA.toFixed(3));
newSizePercentB = Number((areaA.size + areaB.size - newSizePercentA).toFixed(3));
}
areaA.size = newSizePercentA;
areaB.size = newSizePercentB;
this.refreshStyleSizes();
this.notify('progress');
}
private stopDragging() {
if(!this.isDragging) {
return;
}
this.areas.forEach(a => a.component.unlockEvents());
while(this.eventsDragFct.length > 0) {
const fct = this.eventsDragFct.pop();
if(fct) {
fct();
}
}
this.containerSize = 0;
this.areaASize = 0;
this.areaBSize = 0;
this.isDragging = false;
this.notify('end');
}
notify(type: string) {
const data: Array = this.visibleAreas.map(a => a.size);
switch(type) {
case 'start':
return this.dragStart.emit(data);
case 'progress':
return this.dragProgress.emit(data);
case 'end':
return this.dragEnd.emit(data);
case 'visibleTransitionEnd':
return this.visibleTransitionEndInternal.next(data);
}
}
public ngOnDestroy() {
this.stopDragging();
}
}