@ryusei/code
Version:
<div align="center"> <a href="https://code.ryuseijs.com"> <img alt="RyuseiCode" src="https://code.ryuseijs.com/images/svg/logo.svg" width="70"> </a>
166 lines (141 loc) • 4.79 kB
text/typescript
import { AbstractDraggableBar } from '../../classes/AbstractDraggableBar/AbstractDraggableBar';
import { CLASS_ACTIVE, CLASS_SCROLLBAR } from '../../constants/classes';
import { EVENT_MOUNTED, EVENT_RESIZE } from '../../constants/events';
import { Editor } from '../../core/Editor/Editor';
import { attr, hasClass, isArray, off, on, rafThrottle, round, toggleClass, unit } from '../../utils';
/**
* The class for creating a scrollbar.
*
* @since 0.1.0
*/
export class Scrollbar extends AbstractDraggableBar {
/**
* Holds the Editor element.
*/
protected readonly Editor: Editor;
/**
* The target element to scroll.
*/
protected readonly scroller: HTMLElement;
/**
* Holds the margin settings.
*/
private readonly margin: () => [ number, number ];
/**
* Keeps the scrollbar height.
*/
private lastHeight: number;
/**
* The conversion ratio from the scroll offset to the bar offset.
* - top = scrollTop * ratio
* - scrollTop = top / ratio
*/
private ratio = 1;
/**
* The Scrollbar constructor.
*
* @param Editor - An EventBus instance.
* @param parent - A parent element.
* @param scroller - A target element to scroll.
* @param vertical - Determines whether to create a vertical or horizontal scroll bar.
* @param margin - Optional. Margins in pixel as `[ top, bottom ]` ( or `[ left, right ]` ).
*/
constructor(
Editor: Editor,
parent: HTMLElement,
scroller: HTMLElement,
vertical: boolean,
margin: [ number, number ] | ( () => [ number, number ] ) = [ 0, 0 ]
) {
super( [ CLASS_SCROLLBAR, `${ CLASS_SCROLLBAR }--${ vertical ? 'vertical' : 'horizontal' }` ], parent, vertical );
this.Editor = Editor;
this.scroller = scroller;
this.margin = isArray( margin ) ? () => margin : margin;
this.init();
this.listen();
}
/**
* Initializes the instance.
* Note that `aria-valuemin` and `aria-valuemax` is not necessary because their default values are `0` and `100`.
*
* @link https://www.w3.org/TR/wai-aria-1.2/#scrollbar
*/
private init(): void {
const { Editor, scroller } = this;
attr( this.elm, {
role : 'scrollbar',
'aria-controls' : scroller.id,
'aria-orientation': this.names.vertical,
'aria-valuenow' : 0,
'aria-label' : Editor.options.i18n.scrollbar,
} );
this.update = this.update.bind( this );
}
/**
* Listens to some events.
*/
protected listen(): void {
on( this.scroller, 'scroll', rafThrottle( this.update ), this );
this.Editor.event.on( [ EVENT_MOUNTED, EVENT_RESIZE ], rafThrottle( () => {
this.toggle();
this.update();
} ) );
}
/**
* Called while the bar is dragged.
*
* @param e - A PointerEvent object.
*/
protected onDragging( e: PointerEvent ): void {
super.onDragging( e );
const coord = this.getCoord( e );
const diff = coord - this.lastCoord;
this.scroller[ this.names.scrollTop ] += diff / this.ratio;
this.lastCoord = coord;
}
/**
* Updates the scrollbar height and offset according to the current scroll offset.
*/
protected update(): void {
const { scroller, names, elm } = this;
const { style } = elm;
const sh = scroller[ names.scrollHeight ];
const ch = scroller[ names.clientHeight ];
const st = scroller[ names.scrollTop ];
const margin = this.margin();
const heightRatio = 1 - ( ( margin[ 0 ] + margin[ 1 ] ) / ch );
const height = ( ch * ch / sh ) * heightRatio;
if ( this.lastHeight !== height ) {
style[ names.height ] = unit( height );
this.lastHeight = height;
}
if ( this.isActive() ) {
const offsetRatio = ( ch * heightRatio - elm[ names.clientHeight ] ) / ( sh - ch );
style.transform = `${ names.translateY }(${ unit( st * offsetRatio + margin[ 0 ] ) })`;
attr( elm, { 'aria-valuenow': round( 100 * 100 * st / ( sh - ch ) ) / 100 } );
this.ratio = offsetRatio;
}
}
/**
* Checks if the scrollbar is active or not.
*
* @return `true` if the scrollbar is active, or otherwise `false`.
*/
private isActive(): boolean {
return hasClass( this.elm, CLASS_ACTIVE );
}
/**
* Toggles the scrollbar.
*/
protected toggle(): void {
const { scroller, names, elm } = this;
toggleClass( elm, CLASS_ACTIVE, scroller[ names.scrollHeight ] > scroller[ names.clientHeight ] );
}
/**
* Destroys the instance.
*/
destroy(): void {
off( null, '', this );
super.destroy();
}
}