diff --git a/src/utils/DomElementObjectPool.test.ts b/src/utils/DomElementObjectPool.test.ts new file mode 100644 index 00000000..7298f192 --- /dev/null +++ b/src/utils/DomElementObjectPool.test.ts @@ -0,0 +1,47 @@ +import { assert } from 'chai'; +import { DomElementObjectPool } from './DomElementObjectPool'; + +class MockDocument { + private _attr: {[key: string]: string} = {}; + constructor() {} + public getAttribute(key: string): string { return this._attr[key]; }; + public setAttribute(key: string, value: string): void { this._attr[key] = value; } +} + +describe('DomElementObjectPool', () => { + let pool: DomElementObjectPool; + + beforeEach(() => { + pool = new DomElementObjectPool('span'); + (global).document = { + createElement: () => new MockDocument() + }; + }); + + it('should acquire distinct elements', () => { + const element1 = pool.acquire(); + const element2 = pool.acquire(); + assert.notEqual(element1, element2); + }); + + it('should acquire released elements', () => { + const element = pool.acquire(); + pool.release(element); + assert.equal(pool.acquire(), element); + }); + + it('should handle a series of acquisitions and releases', () => { + const element1 = pool.acquire(); + const element2 = pool.acquire(); + pool.release(element1); + assert.equal(pool.acquire(), element1); + pool.release(element1); + pool.release(element2); + assert.equal(pool.acquire(), element2); + assert.equal(pool.acquire(), element1); + }); + + it('should throw when releasing an element that was not acquired', () => { + assert.throws(() => pool.release(document.createElement('span'))); + }); +}); diff --git a/src/utils/DomElementObjectPool.ts b/src/utils/DomElementObjectPool.ts new file mode 100644 index 00000000..8b84fd6b --- /dev/null +++ b/src/utils/DomElementObjectPool.ts @@ -0,0 +1,53 @@ +/** + * @module xterm/utils/DomElementObjectPool + * @license MIT + */ + +/** + * An object pool that manages acquisition and releasing of DOM elements for + * when reuse is desirable. + */ +export class DomElementObjectPool { + private static readonly OBJECT_ID_ATTRIBUTE = 'data-obj-id'; + + private static _objectCount = 0; + + private _type: string; + private _pool: HTMLElement[]; + private _inUse: {[key: string]: HTMLElement}; + + /** + * @param type The DOM element type (div, span, etc.). + */ + constructor(private type: string) { + this._type = type; + this._pool = []; + this._inUse = {}; + } + + public acquire(): HTMLElement { + let element: HTMLElement; + if (this._pool.length === 0) { + element = this.createNew(); + } else { + element = this._pool.pop(); + } + this._inUse[element.getAttribute(DomElementObjectPool.OBJECT_ID_ATTRIBUTE)] = element; + return element; + } + + public release(element: HTMLElement) { + if (!this._inUse[element.getAttribute(DomElementObjectPool.OBJECT_ID_ATTRIBUTE)]) { + throw new Error('Could not release an element not yet acquired'); + } + delete this._inUse[element.getAttribute(DomElementObjectPool.OBJECT_ID_ATTRIBUTE)]; + this._pool.push(element); + } + + private createNew(): HTMLElement { + const element = document.createElement(this._type); + const id = DomElementObjectPool._objectCount++; + element.setAttribute(DomElementObjectPool.OBJECT_ID_ATTRIBUTE, id.toString(10)); + return element; + } +}