From 460d58d4d6528db619db49a4a76f3cfcb6a19d73 Mon Sep 17 00:00:00 2001 From: dm1sh Date: Wed, 4 Aug 2021 14:59:41 +0300 Subject: [PATCH] Added page break methods --- src/__tests__/index.test.ts | 54 ++++++++++++------ src/index.ts | 106 +++++++++++++++++++++++++++++++----- src/utils.ts | 21 +++++++ 3 files changed, 151 insertions(+), 30 deletions(-) create mode 100644 src/utils.ts diff --git a/src/__tests__/index.test.ts b/src/__tests__/index.test.ts index 6e3ac66..aafab4a 100644 --- a/src/__tests__/index.test.ts +++ b/src/__tests__/index.test.ts @@ -4,20 +4,20 @@ import { CacheInterface, HTMLPagination } from "../index"; -describe("HTMLPagination", () => { - let hp: 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); - } +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); + } +} +describe("Text position stuff", () => { beforeEach(() => { content.innerHTML = ""; content.innerHTML = "

aa

bbccdd

"; @@ -27,22 +27,42 @@ describe("HTMLPagination", () => { hp = new HTMLPagination(content, container, new Cache()); }); - test("Computes positions for each text node", () => { + it("computes positions for each text node", () => { expect(hp.elementPositions.map((el) => el[0])).toEqual([0, 2, 4, 6]); }); - test("Gets element by position", () => { + it("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( + it("gets html content for range of text", () => { + expect(hp.getContentFromRange(0, 8)).toEqual( "

aa

bbccdd

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

aa

b

"); - expect(hp.getFromRange(5, 7)).toEqual("cd"); + expect(hp.getContentFromRange(0, 3)).toEqual("

aa

b

"); + expect(hp.getContentFromRange(5, 7)).toEqual("cd"); }); }); + +// TODO: Add pagination tests using puppeteer + +// let browser: puppeteer.Browser, page: puppeteer.Page; + +// describe("Page splitting stuff", () => { +// beforeAll(async () => { +// browser = await puppeteer.launch({ +// defaultViewport: { +// height: 150, +// width: 150, +// }, +// }); +// page = await browser.newPage(); +// }); + +// it("Inserts page break to prevent scroll", async () => { +// await page.goto("localhost:5000"); +// }); +// }); diff --git a/src/index.ts b/src/index.ts index f4965d0..5a26ffe 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,12 +1,13 @@ import { CacheInterface } from "./cache"; import { isElementNode, isTextNode, TextNode } from "./types"; +import { binSearch } from "./utils"; class HTMLPagination { content: HTMLElement; container: HTMLElement; cache: CacheInterface; - elementPositions: [number, TextNode][]; + elementPositions: [number, Node][]; idPositions: Map; /** @@ -33,7 +34,7 @@ class HTMLPagination { const from = 0; const to = 1; - return this.getFromRange(from, to); + return this.getContentFromRange(from, to); } /** @@ -57,27 +58,106 @@ class HTMLPagination { recursive(0, this.content); } + /** + * Finds position for next page break + * initialJump may be computed in a clever way + */ + getPageBreak(start: number, initialJump: number) { + let previousEnd = this.getMaxPosition(); + let end = this.getNextSpaceForPosition(start + initialJump); + + this.getContentFromRange(start, end); + while (!this.scrollNecessary() && end < this.getMaxPosition()) { + previousEnd = end; + end = this.getNextSpaceForPosition(end + 1); + this.getContentFromRange(start, end); + } + + while (this.scrollNecessary() && end > start) { + previousEnd = end; + end = this.getPreviousSpaceForPosition(end - 1); + this.getContentFromRange(start, end); + } + + if (start === end) return previousEnd; + else return end; + } + + /** + * Gets next space or gap between elements for position + */ + getNextSpaceForPosition(startPos: number): number { + const nodeIndex = this.getElementIndexForPosition(startPos); + const [nodePosition, node] = this.elementPositions[nodeIndex]; + + let nodeOffset = startPos - nodePosition; + const str = node.nodeValue || ""; + while (nodeOffset < str.length && str.charAt(nodeOffset) !== " ") + nodeOffset++; + + if (nodeOffset === str.length) { + if (nodeIndex === this.elementPositions.length - 1) + return this.getMaxPosition(); + else return this.elementPositions[nodeIndex + 1][0]; + } else { + return this.elementPositions[nodeIndex][0] + nodeOffset; + } + } + + /** + * Gets previous space or gap between elements for position + */ + getPreviousSpaceForPosition(startPos: number): number { + const nodeIndex = this.getElementIndexForPosition(startPos); + const [nodePosition, node] = this.elementPositions[nodeIndex]; + + let nodeOffset = startPos - nodePosition; + const str = node.nodeValue || ""; + while (nodeOffset > 0 && str.charAt(nodeOffset) !== " ") nodeOffset--; + + return this.elementPositions[nodeIndex][0] + nodeOffset; + } + + /** + * Checks if container is overflowing with content + */ + scrollNecessary(): boolean { + return this.container.clientHeight < this.container.scrollHeight; + } + + /** + * Returns end position of content + */ + getMaxPosition(): number { + const [offset, element] = + this.elementPositions[this.elementPositions.length - 1]; + return offset + (element.nodeValue?.length || 0); + } + + /** + * Wrapper for `binSearch` util to find index of element for position + */ + getElementIndexForPosition(pos: number): number { + return binSearch( + this.elementPositions, + pos, + (i) => this.elementPositions[i][0] + ); + } + /** * 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; + const elementIndex = this.getElementIndexForPosition(pos); - 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]; + return this.elementPositions[elementIndex]; } /** * Sets `container` element content and return as string html content between `from` and `to` */ - getFromRange(from: number, to: number): string { + getContentFromRange(from: number, to: number): string { this.container.innerHTML = ""; const range = new Range(); diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..4138fa1 --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,21 @@ +/** + * Binarycally searches element in array or last element lower than searched + * @param getter Function to convert array element to comparable with searched element + */ +export const binSearch = ( + arr: T[], + el: number, + getter: (i: number) => number +): number => { + let s = 0, + e = arr.length - 1; + + while (s <= e) { + const c = (s + e) >> 1; + if (el > getter(c)) s = c + 1; + else if (el < getter(c)) e = c - 1; + else return c; + } + + return s - 1; +};