common: add utils abbreviateNumber, anchorScroll, Evented, ScrollListener

Evented is now a class instead of an object - it can be extended from a class now. Object.assign can still be used with it, but instead of `evented` with `Evented.prototype`
This commit is contained in:
David Sevilla Martin 2020-02-07 09:14:03 -05:00
parent 4f79a05a4b
commit 8ba86f9c5e
No known key found for this signature in database
GPG Key ID: F764F1417E16B15F
4 changed files with 195 additions and 0 deletions

View File

@ -0,0 +1,81 @@
export type EventHandler = (...args: any) => any;
export default class Evented {
/**
* Arrays of registered event handlers, grouped by the event name.
*/
protected handlers: { [key: string]: EventHandler[] } = {};
/**
* Get all of the registered handlers for an event.
*
* @param event The name of the event.
*/
protected getHandlers(event: string): EventHandler[] {
this.handlers = this.handlers || {};
this.handlers[event] = this.handlers[event] || [];
return this.handlers[event];
}
/**
* Trigger an event.
*
* @param event The name of the event.
* @param args Arguments to pass to event handlers.
*/
public trigger(event: string, ...args: any): this {
this.getHandlers(event).forEach(handler => handler.apply(this, args));
return this;
}
/**
* Register an event handler.
*
* @param event The name of the event.
* @param handler The function to handle the event.
*/
on(event: string, handler: EventHandler): this {
this.getHandlers(event).push(handler);
return this;
}
/**
* Register an event handler so that it will run only once, and then
* unregister itself.
*
* @param event The name of the event.
* @param handler The function to handle the event.
*/
one(event: string, handler: EventHandler): this {
const wrapper = function() {
handler.apply(this, arguments);
this.off(event, wrapper);
};
this.getHandlers(event).push(wrapper);
return this;
}
/**
* Unregister an event handler.
*
* @param event The name of the event.
* @param handler The function that handles the event.
*/
off(event: string, handler: EventHandler): this {
const handlers = this.getHandlers(event);
const index = handlers.indexOf(handler);
if (index !== -1) {
handlers.splice(index, 1);
}
return this;
}
}

View File

@ -0,0 +1,70 @@
const later =
window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.msRequestAnimationFrame ||
window.oRequestAnimationFrame ||
(callback => window.setTimeout(callback, 1000 / 60));
/**
* The `ScrollListener` class sets up a listener that handles window scroll
* events.
*/
export default class ScrollListener {
ticking: boolean = false;
callback: Function;
active: EventListener;
/**
* @param callback The callback to run when the scroll position
* changes.
*/
public constructor(callback: Function) {
this.callback = callback;
}
/**
* On each animation frame, as long as the listener is active, run the
* `update` method.
*/
protected loop() {
// THROTTLE: If the callback is still running (or hasn't yet run), we ignore
// further scroll events.
if (this.ticking) return;
// Schedule the callback to be executed soon (TM), and stop throttling once
// the callback is done.
later(() => {
this.update();
this.ticking = false;
});
this.ticking = true;
}
/**
* Run the callback, whether there was a scroll event or not.
*/
public update() {
this.callback(window.pageYOffset);
}
/**
* Start listening to and handling the window's scroll position.
*/
public start() {
if (!this.active) {
window.addEventListener('scroll', (this.active = this.loop.bind(this)));
}
}
/**
* Stop listening to and handling the window's scroll position.
*/
public stop() {
window.removeEventListener('scroll', this.active);
this.active = null;
}
}

View File

@ -0,0 +1,17 @@
/**
* The `abbreviateNumber` utility converts a number to a shorter localized form.
*
* @example
* abbreviateNumber(1234);
* // "1.2K"
*/
export default (number: number): string => {
// TODO: translation
if (number >= 1000000) {
return Math.floor(number / 1000000) + app.translator.transText('core.lib.number_suffix.mega_text');
} else if (number >= 1000) {
return Math.floor(number / 1000) + app.translator.transText('core.lib.number_suffix.kilo_text');
} else {
return number.toString();
}
};

View File

@ -0,0 +1,27 @@
/**
* The `anchorScroll` utility saves the scroll position relative to an element,
* and then restores it after a callback has been run.
*
* This is useful if a redraw will change the page's content above the viewport.
* Normally doing this will result in the content in the viewport being pushed
* down or pulled up. By wrapping the redraw with this utility, the scroll
* position can be anchor to an element that is in or below the viewport, so
* the content in the viewport will stay the same.
*
* @param element The element to anchor the scroll position to.
* @param callback The callback to run that will change page content.
*/
export default function anchorScroll(element: Element, callback: Function) {
const $window = $(window);
const $el = $(element);
if (!element || !$el.length) {
return callback();
}
const relativeScroll = $el.offset().top - $window.scrollTop();
callback();
$window.scrollTop($el.offset().top - relativeScroll);
}