diff --git a/src/__tests__/index.test.ts b/src/__tests__/index.test.ts new file mode 100644 index 0000000..6e3ac66 --- /dev/null +++ b/src/__tests__/index.test.ts @@ -0,0 +1,48 @@ +/** + * @jest-environment jsdom + */ + +import { CacheInterface, HTMLPagination } from "../index"; + +describe("HTMLPagination", () => { + let hp: HTMLPagination; + + const content = document.createElement("div"); + const container = document.createElement("div"); + class Cache extends CacheInterface { + g(key: string) { + return localStorage.getItem(key); + } + s(key: string, value: string) { + localStorage.setItem(key, value); + } + } + + beforeEach(() => { + content.innerHTML = ""; + content.innerHTML = "

aa

bbccdd

"; + + container.innerHTML = ""; + + hp = new HTMLPagination(content, container, new Cache()); + }); + + test("Computes positions for each text node", () => { + expect(hp.elementPositions.map((el) => el[0])).toEqual([0, 2, 4, 6]); + }); + + test("Gets element by position", () => { + expect(hp.getElementForPosition(0)[0]).toBe(0); + expect(hp.getElementForPosition(1)[0]).toBe(0); + expect(hp.getElementForPosition(2)[0]).toBe(2); + expect(hp.getElementForPosition(3)[0]).toBe(2); + }); + + test("Gets html content for range of text", () => { + expect(hp.getFromRange(0, 8)).toEqual( + "

aa

bbccdd

" + ); + expect(hp.getFromRange(0, 3)).toEqual("

aa

b

"); + expect(hp.getFromRange(5, 7)).toEqual("cd"); + }); +}); diff --git a/src/cache.ts b/src/cache.ts new file mode 100644 index 0000000..6283875 --- /dev/null +++ b/src/cache.ts @@ -0,0 +1,9 @@ +/** + * Interface to any storage which supports key-value storage + */ +export abstract class CacheInterface { + /** Get */ + abstract g(key: string): string | null; + /** Set */ + abstract s(key: string, value: string): void; +} diff --git a/src/index.ts b/src/index.ts index fe2a725..f4965d0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,22 +1,100 @@ -export declare abstract class CacheInterface { - abstract g(key: string): string; - abstract s(key: string, value: string): void; +import { CacheInterface } from "./cache"; +import { isElementNode, isTextNode, TextNode } from "./types"; + +class HTMLPagination { + content: HTMLElement; + container: HTMLElement; + cache: CacheInterface; + + elementPositions: [number, TextNode][]; + idPositions: Map; + + /** + * @param content HTML element with html content to display paginationly + * @param container HTML element which will store content to display + * @param cache Class implementing `g` and `s` methods for getting and setting elements of KV storage + */ + constructor( + content: HTMLElement, + container: HTMLElement, + cache: CacheInterface + ) { + this.content = content; + this.container = container; + this.cache = cache; + + this.elementPositions = new Array(); + this.idPositions = new Map(); + + this.computeElementsPositions(); + } + + getPage(n: number): string { + const from = 0; + const to = 1; + + return this.getFromRange(from, to); + } + + /** + * Computes html elements and text nodes positions. Must be run only on first setup + */ + computeElementsPositions(): void { + const recursive = (currentPosition: number, root: Node): number => { + if (isTextNode(root)) { + this.elementPositions.push([currentPosition, root]); + + return currentPosition + root.nodeValue.length; + } else if (isElementNode(root)) { + if (root.id !== null) this.idPositions.set(root.id, currentPosition); + + return Array.from(root.childNodes).reduce(recursive, currentPosition); + } else { + return currentPosition; + } + }; + + recursive(0, this.content); + } + + /** + * Finds node inside which `pos` is located. Returns node positions and itself + */ + getElementForPosition(pos: number): [number, Node] { + let s = 0, + e = this.elementPositions.length - 1; + + while (s <= e) { + const c = (s + e) >> 1; + if (pos > this.elementPositions[c][0]) s = c + 1; + else if (pos < this.elementPositions[c][0]) e = c - 1; + else return this.elementPositions[c]; + } + + return this.elementPositions[s - 1]; + } + + /** + * Sets `container` element content and return as string html content between `from` and `to` + */ + getFromRange(from: number, to: number): string { + this.container.innerHTML = ""; + const range = new Range(); + + const [startPosition, startElement] = this.getElementForPosition(from); + const startOffset = from - startPosition; + range.setStart(startElement, startOffset); + + const [endPosition, endElement] = this.getElementForPosition(to); + const endOffset = to - endPosition; + range.setEnd(endElement, endOffset); + + // TODO: copy range with all its parent elements + + this.container.appendChild(range.cloneContents()); + + return this.container.innerHTML; + } } -/** - * Function to get page with specific number - * @param n Page number - */ -declare function getPage(n: number): string; - -/** - * Function to prepare unific data and compose page content getting function - * @param content HTML element with html content to display paginationly - * @param container HTML element which will store content to display - * @param cache Class implementing `g` and `s` methods for getting and setting elements of KV storage - */ -export declare function setup( - content: HTMLElement, - container: HTMLElement, - cache: CacheInterface -): typeof getPage; +export { CacheInterface, HTMLPagination }; diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..b0f9685 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,7 @@ +export type TextNode = Node & { nodeValue: string }; + +export const isTextNode = (el: Node): el is TextNode => + el.nodeType === Node.TEXT_NODE; + +export const isElementNode = (el: Node): el is HTMLElement => + el.nodeType === Node.ELEMENT_NODE;