You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
490 lines
13 KiB
490 lines
13 KiB
import { Directive, ElementRef, Input, OnDestroy, OnInit } from '@angular/core';
|
|
import { Router } from '@angular/router';
|
|
import { Platform } from '@angular/cdk/platform';
|
|
import { fromEvent, Subject } from 'rxjs';
|
|
import { debounceTime, takeUntil } from 'rxjs/operators';
|
|
import PerfectScrollbar from 'perfect-scrollbar';
|
|
import * as _ from 'lodash';
|
|
import { ScrollbarGeometry, ScrollbarPosition } from '@treo/directives/scrollbar/scrollbar.interfaces';
|
|
|
|
// -----------------------------------------------------------------------------------------------------
|
|
// Wrapper directive for the Perfect Scrollbar: https://github.com/mdbootstrap/perfect-scrollbar
|
|
// Based on https://github.com/zefoy/ngx-perfect-scrollbar
|
|
// -----------------------------------------------------------------------------------------------------
|
|
@Directive({
|
|
selector: '[treoScrollbar]',
|
|
exportAs: 'treoScrollbar'
|
|
})
|
|
export class TreoScrollbarDirective implements OnInit, OnDestroy
|
|
{
|
|
isMobile: boolean;
|
|
ps: PerfectScrollbar | any;
|
|
|
|
// Private
|
|
private _animation: number | null;
|
|
private _enabled: boolean;
|
|
private _options: any;
|
|
private _unsubscribeAll: Subject<void>;
|
|
|
|
/**
|
|
* Constructor
|
|
*
|
|
* @param {ElementRef} _elementRef
|
|
* @param {Platform} _platform
|
|
* @param {Router} _router
|
|
*/
|
|
constructor(
|
|
private _elementRef: ElementRef,
|
|
private _platform: Platform,
|
|
private _router: Router
|
|
)
|
|
{
|
|
// Set the private defaults
|
|
this._animation = null;
|
|
this._options = {};
|
|
this._unsubscribeAll = new Subject();
|
|
|
|
// Set the defaults
|
|
this.enabled = true;
|
|
this.isMobile = false;
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------------------------------
|
|
// @ Accessors
|
|
// -----------------------------------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Scrollbar options
|
|
*
|
|
* @param value
|
|
*/
|
|
@Input()
|
|
set treoScrollbarOptions(value: any)
|
|
{
|
|
// Merge the options
|
|
this._options = _.merge({}, this._options, value);
|
|
|
|
// Destroy and re-init the PerfectScrollbar to update its options
|
|
setTimeout(() => {
|
|
this._destroy();
|
|
});
|
|
|
|
setTimeout(() => {
|
|
this._init();
|
|
});
|
|
}
|
|
|
|
get treoScrollbarOptions(): any
|
|
{
|
|
// Return the options
|
|
return this._options;
|
|
}
|
|
|
|
/**
|
|
* Is enabled
|
|
*
|
|
* @param value
|
|
*/
|
|
@Input('treoScrollbar')
|
|
set enabled(value: boolean | '')
|
|
{
|
|
// If the value is an empty string, interpret it as 'true'
|
|
if ( value === '' )
|
|
{
|
|
value = true;
|
|
}
|
|
|
|
// If the value is the same, return...
|
|
if ( this._enabled === value )
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Store the value
|
|
this._enabled = value;
|
|
|
|
// If enabled...
|
|
if ( this.enabled )
|
|
{
|
|
// Init the directive
|
|
this._init();
|
|
}
|
|
else
|
|
{
|
|
// Otherwise destroy it
|
|
this._destroy();
|
|
}
|
|
}
|
|
|
|
get enabled(): boolean | ''
|
|
{
|
|
// Return the enabled status
|
|
return this._enabled;
|
|
}
|
|
|
|
/**
|
|
* Getter for _elementRef
|
|
*/
|
|
get elementRef(): ElementRef
|
|
{
|
|
return this._elementRef;
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------------------------------
|
|
// @ Lifecycle hooks
|
|
// -----------------------------------------------------------------------------------------------------
|
|
|
|
/**
|
|
* On init
|
|
*/
|
|
ngOnInit(): void
|
|
{
|
|
// Subscribe to window resize event
|
|
fromEvent(window, 'resize')
|
|
.pipe(
|
|
takeUntil(this._unsubscribeAll),
|
|
debounceTime(150)
|
|
)
|
|
.subscribe(() => {
|
|
|
|
// Update the PerfectScrollbar
|
|
this.update();
|
|
});
|
|
}
|
|
|
|
/**
|
|
* On destroy
|
|
*/
|
|
ngOnDestroy(): void
|
|
{
|
|
this._destroy();
|
|
|
|
// Unsubscribe from all subscriptions
|
|
this._unsubscribeAll.next();
|
|
this._unsubscribeAll.complete();
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------------------------------
|
|
// @ Private methods
|
|
// -----------------------------------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Initialize
|
|
*
|
|
* @private
|
|
*/
|
|
private _init(): void
|
|
{
|
|
// Return, if already initialized
|
|
if ( this.ps )
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Check if is mobile
|
|
if ( this._platform.ANDROID || this._platform.IOS )
|
|
{
|
|
this.isMobile = true;
|
|
}
|
|
|
|
// Return if it's mobile or the platform is not a browser
|
|
if ( this.isMobile || !this._platform.isBrowser )
|
|
{
|
|
// Silently set the enabled to false
|
|
this._enabled = false;
|
|
|
|
return;
|
|
}
|
|
|
|
// Initialize the PerfectScrollbar
|
|
this.ps = new PerfectScrollbar(this._elementRef.nativeElement, {...this.treoScrollbarOptions});
|
|
}
|
|
|
|
/**
|
|
* Destroy
|
|
*
|
|
* @private
|
|
*/
|
|
private _destroy(): void
|
|
{
|
|
if ( !this.ps )
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Destroy the PerfectScrollbar
|
|
this.ps.destroy();
|
|
|
|
// Clean up
|
|
this.ps = null;
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------------------------------
|
|
// @ Public methods
|
|
// -----------------------------------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Update the scrollbar
|
|
*/
|
|
update(): void
|
|
{
|
|
if ( !this.ps )
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Update the PerfectScrollbar
|
|
this.ps.update();
|
|
}
|
|
|
|
/**
|
|
* Destroy the scrollbar
|
|
*/
|
|
destroy(): void
|
|
{
|
|
this.ngOnDestroy();
|
|
}
|
|
|
|
/**
|
|
* Returns the geometry of the scrollable element
|
|
*
|
|
* @param prefix
|
|
*/
|
|
geometry(prefix: string = 'scroll'): ScrollbarGeometry
|
|
{
|
|
const scrollbarGeometry = new ScrollbarGeometry(
|
|
this._elementRef.nativeElement[prefix + 'Left'],
|
|
this._elementRef.nativeElement[prefix + 'Top'],
|
|
this._elementRef.nativeElement[prefix + 'Width'],
|
|
this._elementRef.nativeElement[prefix + 'Height']);
|
|
|
|
return scrollbarGeometry;
|
|
}
|
|
|
|
/**
|
|
* Returns the position of the scrollable element
|
|
*
|
|
* @param absolute
|
|
*/
|
|
position(absolute: boolean = false): ScrollbarPosition
|
|
{
|
|
let scrollbarPosition;
|
|
|
|
if ( !absolute && this.ps )
|
|
{
|
|
scrollbarPosition = new ScrollbarPosition(
|
|
this.ps.reach.x || 0,
|
|
this.ps.reach.y || 0
|
|
);
|
|
}
|
|
else
|
|
{
|
|
scrollbarPosition = new ScrollbarPosition(
|
|
this._elementRef.nativeElement.scrollLeft,
|
|
this._elementRef.nativeElement.scrollTop
|
|
);
|
|
}
|
|
|
|
return scrollbarPosition;
|
|
}
|
|
|
|
/**
|
|
* Scroll to
|
|
*
|
|
* @param x
|
|
* @param y
|
|
* @param speed
|
|
*/
|
|
scrollTo(x: number, y?: number, speed?: number): void
|
|
{
|
|
if ( y == null && speed == null )
|
|
{
|
|
this.animateScrolling('scrollTop', x, speed);
|
|
}
|
|
else
|
|
{
|
|
if ( x != null )
|
|
{
|
|
this.animateScrolling('scrollLeft', x, speed);
|
|
}
|
|
|
|
if ( y != null )
|
|
{
|
|
this.animateScrolling('scrollTop', y, speed);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Scroll to X
|
|
*
|
|
* @param {number} x
|
|
* @param {number} speed
|
|
*/
|
|
scrollToX(x: number, speed?: number): void
|
|
{
|
|
this.animateScrolling('scrollLeft', x, speed);
|
|
}
|
|
|
|
/**
|
|
* Scroll to Y
|
|
*
|
|
* @param {number} y
|
|
* @param {number} speed
|
|
*/
|
|
scrollToY(y: number, speed?: number): void
|
|
{
|
|
this.animateScrolling('scrollTop', y, speed);
|
|
}
|
|
|
|
/**
|
|
* Scroll to top
|
|
*
|
|
* @param {number} offset
|
|
* @param {number} speed
|
|
*/
|
|
scrollToTop(offset: number = 0, speed?: number): void
|
|
{
|
|
this.animateScrolling('scrollTop', offset, speed);
|
|
}
|
|
|
|
/**
|
|
* Scroll to bottom
|
|
*
|
|
* @param {number} offset
|
|
* @param {number} speed
|
|
*/
|
|
scrollToBottom(offset: number = 0, speed?: number): void
|
|
{
|
|
const top = this._elementRef.nativeElement.scrollHeight - this._elementRef.nativeElement.clientHeight;
|
|
this.animateScrolling('scrollTop', top - offset, speed);
|
|
}
|
|
|
|
/**
|
|
* Scroll to left
|
|
*
|
|
* @param {number} offset
|
|
* @param {number} speed
|
|
*/
|
|
scrollToLeft(offset: number = 0, speed?: number): void
|
|
{
|
|
this.animateScrolling('scrollLeft', offset, speed);
|
|
}
|
|
|
|
/**
|
|
* Scroll to right
|
|
*
|
|
* @param {number} offset
|
|
* @param {number} speed
|
|
*/
|
|
scrollToRight(offset: number = 0, speed?: number): void
|
|
{
|
|
const left = this._elementRef.nativeElement.scrollWidth - this._elementRef.nativeElement.clientWidth;
|
|
this.animateScrolling('scrollLeft', left - offset, speed);
|
|
}
|
|
|
|
/**
|
|
* Scroll to element
|
|
*
|
|
* @param {string} qs
|
|
* @param {number} offset
|
|
* @param {boolean} ignoreVisible If true, scrollToElement won't happen if element is already inside the current viewport
|
|
* @param {number} speed
|
|
*/
|
|
scrollToElement(qs: string, offset: number = 0, ignoreVisible: boolean = false, speed?: number): void
|
|
{
|
|
const element = this._elementRef.nativeElement.querySelector(qs);
|
|
|
|
if ( !element )
|
|
{
|
|
return;
|
|
}
|
|
|
|
const elementPos = element.getBoundingClientRect();
|
|
const scrollerPos = this._elementRef.nativeElement.getBoundingClientRect();
|
|
|
|
if ( this._elementRef.nativeElement.classList.contains('ps--active-x') )
|
|
{
|
|
if ( ignoreVisible && elementPos.right <= (scrollerPos.right - Math.abs(offset)) )
|
|
{
|
|
return;
|
|
}
|
|
|
|
const currentPos = this._elementRef.nativeElement['scrollLeft'];
|
|
const position = elementPos.left - scrollerPos.left + currentPos;
|
|
|
|
this.animateScrolling('scrollLeft', position + offset, speed);
|
|
}
|
|
|
|
if ( this._elementRef.nativeElement.classList.contains('ps--active-y') )
|
|
{
|
|
if ( ignoreVisible && elementPos.bottom <= (scrollerPos.bottom - Math.abs(offset)) )
|
|
{
|
|
return;
|
|
}
|
|
|
|
const currentPos = this._elementRef.nativeElement['scrollTop'];
|
|
const position = elementPos.top - scrollerPos.top + currentPos;
|
|
|
|
this.animateScrolling('scrollTop', position + offset, speed);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Animate scrolling
|
|
*
|
|
* @param target
|
|
* @param value
|
|
* @param speed
|
|
*/
|
|
animateScrolling(target: string, value: number, speed?: number): void
|
|
{
|
|
if ( this._animation )
|
|
{
|
|
window.cancelAnimationFrame(this._animation);
|
|
this._animation = null;
|
|
}
|
|
|
|
if ( !speed || typeof window === 'undefined' )
|
|
{
|
|
this._elementRef.nativeElement[target] = value;
|
|
}
|
|
else if ( value !== this._elementRef.nativeElement[target] )
|
|
{
|
|
let newValue = 0;
|
|
let scrollCount = 0;
|
|
|
|
let oldTimestamp = performance.now();
|
|
let oldValue = this._elementRef.nativeElement[target];
|
|
|
|
const cosParameter = (oldValue - value) / 2;
|
|
|
|
const step = (newTimestamp: number) => {
|
|
scrollCount += Math.PI / (speed / (newTimestamp - oldTimestamp));
|
|
newValue = Math.round(value + cosParameter + cosParameter * Math.cos(scrollCount));
|
|
|
|
// Only continue animation if scroll position has not changed
|
|
if ( this._elementRef.nativeElement[target] === oldValue )
|
|
{
|
|
if ( scrollCount >= Math.PI )
|
|
{
|
|
this.animateScrolling(target, value, 0);
|
|
}
|
|
else
|
|
{
|
|
this._elementRef.nativeElement[target] = newValue;
|
|
|
|
// On a zoomed out page the resulting offset may differ
|
|
oldValue = this._elementRef.nativeElement[target];
|
|
oldTimestamp = newTimestamp;
|
|
|
|
this._animation = window.requestAnimationFrame(step);
|
|
}
|
|
}
|
|
};
|
|
|
|
window.requestAnimationFrame(step);
|
|
}
|
|
}
|
|
}
|