@byloth/core
Version:
An unopinionated collection of useful functions and classes that I use widely in all my projects. 🔧
349 lines (318 loc) • 9.25 kB
text/typescript
import Publisher from "../callbacks/publisher.js";
import type { Callback } from "../types.js";
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import type MapView from "./map-view.js";
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import type SetView from "./set-view.js";
interface ArrayViewEventsMap<T>
{
"add": (value: T, index: number) => void;
"remove": (value: T, index: number) => void;
"clear": () => void;
}
/**
* A wrapper class around the native {@link Array} class that provides additional functionality
* for publishing events when entries are added, removed or the collection is cleared.
* There are also complementary classes that work with the native `Map` and `Set` classes.
* See also {@link MapView} and {@link SetView}.
*
* ---
*
* @example
* ```ts
* const array = new ArrayView<number>();
*
* array.onAdd((value: number, index: number) => console.log(`Added ${value} at index ${index}`));
* array.push(42); // "Added 42 at index 0"
* ```
*
* ---
*
* @template T The type of the values in the array.
*/
export default class ArrayView<T> extends Array<T>
{
/**
* The internal {@link Publisher} instance used to publish events.
*/
protected readonly _publisher: Publisher<ArrayViewEventsMap<T>>;
/**
* Initializes a new instance of the {@link ArrayView} class.
*
* ---
*
* @example
* ```ts
* const array = new ArrayView<number>();
* ```
*/
public constructor();
/**
* Initializes a new instance of the {@link ArrayView} class.
*
* ---
*
* @example
* ```ts
* const array = new ArrayView<number>(3);
* ```
*
* ---
*
* @param length The initial length of the {@link Array}.
*/
public constructor(length: number);
/**
* Initializes a new instance of the {@link ArrayView} class.
*
* ---
*
* @example
* ```ts
* const array = new ArrayView<number>(2, 4, 8);
* ```
*
* ---
*
* @param items The items to initialize the {@link Array} with.
*/
public constructor(...items: T[]);
public constructor(...items: T[])
{
super(...items);
this._publisher = new Publisher();
}
/**
* Appends new elements to the end of the {@link Array} and returns the new length of the array.
*
* ---
*
* @example
* ```ts
* const array = new ArrayView<number>();
* array.push(2, 4, 8);
*
* console.log(array); // ArrayView(3) [2, 4, 8]
* ```
*
* ---
*
* @param items New elements to add to the array.
*
* @returns The new length of the array.
*/
public override push(...items: T[]): number
{
const startIndex = this.length;
const result = super.push(...items);
for (let i = 0; i < items.length; i += 1)
{
this._publisher.publish("add", items[i], startIndex + i);
}
return result;
}
/**
* Removes the last element from the {@link Array} and returns it.
*
* ---
*
* @example
* ```ts
* const array = new ArrayView<number>(2, 4, 8);
* array.pop(); // 8
*
* console.log(array); // ArrayView(2) [2, 4]
* ```
*
* ---
*
* @returns The removed element, or `undefined` if the array is empty.
*/
public override pop(): T | undefined
{
const index = this.length - 1;
if (index < 0) { return undefined; }
const value = super.pop();
this._publisher.publish("remove", value!, index);
return value;
}
/**
* Removes the first element from the {@link Array} and returns it.
*
* ---
*
* @example
* ```ts
* const array = new ArrayView<number>(2, 4, 8);
* array.shift(); // 2
*
* console.log(array); // ArrayView(2) [4, 8]
* ```
*
* ---
*
* @returns The removed element, or `undefined` if the array is empty.
*/
public override shift(): T | undefined
{
if (this.length === 0) { return undefined; }
const value = super.shift();
this._publisher.publish("remove", value!, 0);
return value;
}
/**
* Inserts new elements at the start of the {@link Array} and returns the new length of the array.
*
* ---
*
* @example
* ```ts
* const array = new ArrayView<number>(4, 8);
* array.unshift(2);
*
* console.log(array); // ArrayView(3) [2, 4, 8]
* ```
*
* ---
*
* @param items Elements to insert at the start of the array.
*
* @returns The new length of the array.
*/
public override unshift(...items: T[]): number
{
const result = super.unshift(...items);
for (let i = 0; i < items.length; i += 1)
{
this._publisher.publish("add", items[i], i);
}
return result;
}
/**
* Removes elements from the {@link Array} and, if necessary, inserts new elements in their place,
* returning the deleted elements.
*
* ---
*
* @example
* ```ts
* const array = new ArrayView<number>(2, 4, 8, 16);
* array.splice(1, 2, 32, 64); // [4, 8]
*
* console.log(array); // ArrayView(4) [2, 32, 64, 16]
* ```
*
* ---
*
* @param start The zero-based location in the array from which to start removing elements.
* @param deleteCount The number of elements to remove.
* @param items Elements to insert into the array in place of the deleted elements.
*
* @returns An array containing the elements that were deleted.
*/
public override splice(start: number, deleteCount?: number, ...items: T[]): T[]
{
const normalizedStart = start < 0 ? Math.max(this.length + start, 0) : Math.min(start, this.length);
const actualDeleteCount = deleteCount === undefined ?
this.length - normalizedStart :
Math.min(Math.max(deleteCount, 0), this.length - normalizedStart);
const removed = super.splice(start, actualDeleteCount, ...items);
for (let i = 0; i < removed.length; i += 1)
{
this._publisher.publish("remove", removed[i], normalizedStart + i);
}
for (let i = 0; i < items.length; i += 1)
{
this._publisher.publish("add", items[i], normalizedStart + i);
}
return removed;
}
/**
* Removes all elements from the {@link Array}.
*
* ---
*
* @example
* ```ts
* const array = new ArrayView<number>(2, 4, 8);
* array.clear();
*
* console.log(array); // ArrayView(0) []
* ```
*/
public clear(): void
{
const length = this.length;
this.length = 0;
if (length > 0) { this._publisher.publish("clear"); }
}
/**
* Subscribes to the `add` event of the array with a callback that will be executed when an element is added.
*
* ---
*
* @example
* ```ts
* array.onAdd((value, index) => console.log(`Added ${value} at index ${index}`));
*
* array.push(2); // "Added 2 at index 0"
* array.push(42); // "Added 42 at index 1"
* ```
*
* ---
*
* @param callback The callback that will be executed when an element is added to the array.
*
* @returns A function that can be used to unsubscribe from the event.
*/
public onAdd(callback: (value: T, index: number) => void): Callback
{
return this._publisher.subscribe("add", callback);
}
/**
* Subscribes to the `remove` event of the array with a callback that will be executed when an element is removed.
*
* ---
*
* @example
* ```ts
* array.onRemove((value, index) => console.log(`Removed ${value} at index ${index}`));
*
* array.pop(); // "Removed 8 at index 2"
* array.shift(); // "Removed 2 at index 0"
* ```
*
* ---
*
* @param callback The callback that will be executed when an element is removed from the array.
*
* @returns A function that can be used to unsubscribe from the event.
*/
public onRemove(callback: (value: T, index: number) => void): Callback
{
return this._publisher.subscribe("remove", callback);
}
/**
* Subscribes to the `clear` event of the array with a callback that will be executed when the array is cleared.
*
* ---
*
* @example
* ```ts
* array.onClear(() => console.log("The array has been cleared."));
* array.clear(); // "The array has been cleared."
* ```
*
* ---
*
* @param callback The callback that will be executed when the array is cleared.
*
* @returns A function that can be used to unsubscribe from the event.
*/
public onClear(callback: () => void): Callback
{
return this._publisher.subscribe("clear", callback);
}
public readonly [Symbol.toStringTag]: string = "ArrayView";
public static override readonly [Symbol.species]: typeof Array = Array;
}