UNPKG

@magflip/core

Version:

Contains core MagFlip objects such as Book, Page, BookShelf, and BookViewer.

1,304 lines (1,281 loc) 42.7 kB
class SizeExt { constructor(w, h) { this.width = w; this.height = h; this.diagonal = Math.sqrt(Math.pow(w, 2) + Math.pow(h, 2)); } } class BookSize { constructor(size) { this.closed = new SizeExt(size.closed.width, size.closed.height); this.opened = new SizeExt(size.opened.width, size.opened.height); } } class MZEvent { constructor() { this.listeners = {}; } // 이벤트 리스너 등록 addEventListener(event, listener) { if (!this.listeners[event]) { this.listeners[event] = []; } this.listeners[event].push(listener); } // 이벤트 리스너 제거 removeEventListener(event, listener) { if (!this.listeners[event]) return; this.listeners[event] = this.listeners[event].filter(l => l !== listener); } // 이벤트 발생 emitEvent(event, ...args) { if (!this.listeners[event]) return; this.listeners[event].forEach(listener => listener(...args)); } } var PageType; (function (PageType) { PageType["Page"] = "Page"; PageType["Cover"] = "Cover"; PageType["Empty"] = "Empty"; PageType["Blank"] = "Blank"; })(PageType || (PageType = {})); var PageLabelType; (function (PageLabelType) { PageLabelType["Default"] = "Default"; PageLabelType["Empty"] = "Empty"; })(PageLabelType || (PageLabelType = {})); var DefaultSize; (function (DefaultSize) { DefaultSize[DefaultSize["bookWidth"] = 600] = "bookWidth"; DefaultSize[DefaultSize["bookHeight"] = 900] = "bookHeight"; DefaultSize[DefaultSize["pageWidth"] = 600] = "pageWidth"; DefaultSize[DefaultSize["pageHeight"] = 900] = "pageHeight"; })(DefaultSize || (DefaultSize = {})); var BookType; (function (BookType) { BookType["Book"] = "Book"; BookType["Magazine"] = "Magazine"; BookType["Newspaper"] = "Newspaper"; })(BookType || (BookType = {})); var BookStatus; (function (BookStatus) { BookStatus["Open"] = "Open"; BookStatus["Close"] = "Close"; })(BookStatus || (BookStatus = {})); var EventStatus; (function (EventStatus) { EventStatus[EventStatus["None"] = 0] = "None"; EventStatus[EventStatus["AutoFlip"] = 8] = "AutoFlip"; EventStatus[EventStatus["AutoFlipFromCorner"] = 12] = "AutoFlipFromCorner"; EventStatus[EventStatus["AutoFlipToCorner"] = 10] = "AutoFlipToCorner"; EventStatus[EventStatus["Flipping"] = 128] = "Flipping"; EventStatus[EventStatus["SnappingBack"] = 144] = "SnappingBack"; EventStatus[EventStatus["FlippingForward"] = 160] = "FlippingForward"; EventStatus[EventStatus["FlippingBackward"] = 192] = "FlippingBackward"; EventStatus[EventStatus["Dragging"] = 2048] = "Dragging"; })(EventStatus || (EventStatus = {})); var Zone; (function (Zone) { Zone[Zone["LT"] = 66] = "LT"; Zone[Zone["LC"] = 34] = "LC"; Zone[Zone["LB"] = 18] = "LB"; Zone[Zone["RT"] = 65] = "RT"; Zone[Zone["RC"] = 33] = "RC"; Zone[Zone["RB"] = 17] = "RB"; Zone[Zone["Left"] = 2] = "Left"; Zone[Zone["Right"] = 1] = "Right"; Zone[Zone["Top"] = 64] = "Top"; Zone[Zone["Center"] = 32] = "Center"; Zone[Zone["Bottom"] = 16] = "Bottom"; })(Zone || (Zone = {})); var AutoFlipType; (function (AutoFlipType) { AutoFlipType[AutoFlipType["FixedWidth"] = 0] = "FixedWidth"; AutoFlipType[AutoFlipType["MouseCursor"] = 1] = "MouseCursor"; })(AutoFlipType || (AutoFlipType = {})); var ViewerType; (function (ViewerType) { ViewerType["Flipping"] = "flipping"; ViewerType["Scrolling"] = "scrolling"; })(ViewerType || (ViewerType = {})); class Point { constructor(point) { this.x = (point === null || point === void 0 ? void 0 : point.x) || 0; this.y = (point === null || point === void 0 ? void 0 : point.y) || 0; } toString() { return `${this.x},${this.y}`; } } class Line { constructor(p1, p2) { this.p1 = p1; this.p2 = p2; } } class Rect { constructor(rect) { this.left = (rect === null || rect === void 0 ? void 0 : rect.left) || 0; this.right = (rect === null || rect === void 0 ? void 0 : rect.right) || 0; this.top = (rect === null || rect === void 0 ? void 0 : rect.top) || 0; this.bottom = (rect === null || rect === void 0 ? void 0 : rect.bottom) || 0; this.width = (rect === null || rect === void 0 ? void 0 : rect.width) || 0; this.height = (rect === null || rect === void 0 ? void 0 : rect.height) || 0; this.center = new Point({ x: (this.left + this.right) / 2, y: (this.top + this.bottom) / 2 }); } get leftTop() { return { x: this.left, y: this.top }; } get leftCenter() { return { x: this.left, y: this.center.y }; } get leftBottom() { return { x: this.left, y: this.bottom }; } get rightTop() { return { x: this.right, y: this.top }; } get rightCenter() { return { x: this.right, y: this.center.y }; } get rightBottom() { return { x: this.right, y: this.bottom }; } get centerTop() { return { x: this.center.x, y: this.top }; } get centerCenter() { return this.center; } get centerBottom() { return { x: this.center.x, y: this.bottom }; } } /** * This is Magzog Math object that contains math util-methods. */ class MZMath { /** * Finds the symmetric point of a given target point with respect to a reference origin point. * * This function calculates the point that is symmetric to the target point, * using the origin point as the reference. The symmetric point is determined by * reflecting the target point across the origin. * * @param {Object} originPoint - The reference point for symmetry, containing x and y coordinates. * @param {Object} targetPoint - The point for which the symmetric point is calculated, containing x and y coordinates. * @returns {Object} - The symmetric point with x and y coordinates. */ static findSymmetricPoint(originPoint, targetPoint) { const symmetricX = 2 * originPoint.x - targetPoint.x; const symmetricY = 2 * originPoint.y - targetPoint.y; return { x: symmetricX, y: symmetricY }; } /** * Get degree. * 0 <= degree <= 180 or 0 < degree < -180 * @param startPoint * @param destPoint * @returns */ static getDegree(startPoint, destPoint) { const radian = MZMath.getRadian(startPoint, destPoint); const degree = radian * 180 / Math.PI; return degree; } /** * Get degree. * 0 <= degree * @param startPoint * @param destPoint * @returns */ static getDegreePositive(startPoint, destPoint) { const radian = MZMath.getRadian(startPoint, destPoint); let degree = (radian * 180 / Math.PI) % 360; if (degree < 0) { degree += 360; } return degree; } /** * Get radian angle. * 0 <= radian <= pi or 0 < radian < -pi * @param startPoint * @param destPoint * @returns */ static getRadian(startPoint, destPoint) { const dx = destPoint.x - startPoint.x; const dy = destPoint.y - startPoint.y; const radian = Math.atan2(dy, dx); return radian; } /** * Get radian angle. * 0 <= radian * @param startPoint * @param destPoint * @returns */ static getRadianPositive(startPoint, destPoint) { const dx = destPoint.x - startPoint.x; const dy = destPoint.y - startPoint.y; let radian = Math.atan2(dy, dx) % (2 * Math.PI); if (radian < 0) { radian += 2 * Math.PI; // 음수일 경우 2π를 더해 0 ~ 2π로 변환 } return radian; } /** * Returns the global location and size of the input element. * @param el * @returns */ static getOffset(el) { var top = 0, left = 0, width = 0, height = 0; let bound = el.getBoundingClientRect(); height = bound.height; width = bound.width; do { bound = el.getBoundingClientRect(); top += bound.top; left += bound.left; el = el.offsetParent; if (el !== null) { bound = el.getBoundingClientRect(); top -= bound.top - window.scrollY; left -= bound.left - window.scrollX; } } while (el); return { top: top, left: left, width: width, height: height, bottom: top + height, right: left + width, }; } /** * Ignore the scroll position. * @param el * @returns */ static getOffset4Fixed(el) { var top = 0, left = 0, width = 0, height = 0; let bound = el.getBoundingClientRect(); height = bound.height; width = bound.width; do { bound = el.getBoundingClientRect(); top += bound.top; left += bound.left; el = el.offsetParent; if (el !== null) { bound = el.getBoundingClientRect(); top -= bound.top; left -= bound.left; } } while (el); return new Rect({ top: top, left: left, width: width, height: height, bottom: top + height, right: left + width, }); } /** * Returns the length between two points. * @param point1 * @param point2 * @returns */ static getLength(point1, point2) { return Math.sqrt(Math.pow(point1.x - point2.x, 2) + Math.pow(point1.y - point2.y, 2)); } /** * Find a point on line AB that is a fixed distance from point A toward point B. * @param a * @param b * @param distance * @returns */ static findPointOnLine(a, b, distance) { // AB Vector calculation. const ab = { x: b.x - a.x, y: b.y - a.y }; // The length of vector AB const abLength = Math.sqrt(ab.x * ab.x + ab.y * ab.y); // Calculates the vector AB's unit. const unitVector = { x: ab.x / abLength, y: ab.y / abLength }; // Find the point. const point = { x: a.x + unitVector.x * distance, y: a.y + unitVector.y * distance }; return point; } /** * Calculates and returns the coordinates of point D, where the perpendicular from point C meets the line segment AB. * @param line line * @param c * @returns */ static findPerpendicularFoot(line, c) { const vectorX = line.p2.x - line.p1.x; const vectorY = line.p2.y - line.p1.y; const vector_squared = vectorX * vectorX + vectorY * vectorY; if (vector_squared === 0) { return line.p1; } // Vector p1,c const vector_p1_c_x = c.x - line.p1.x; const vector_p1_c_y = c.y - line.p1.y; // Vector production. const dotProduct = vector_p1_c_x * vectorX + vector_p1_c_y * vectorY; const t = dotProduct / vector_squared; // Point D const dx = line.p1.x + vectorX * t; const dy = line.p1.y + vectorY * t; return { x: dx, y: dy }; } } function deepMerge(target, source) { for (const key in source) { if (source[key] instanceof Object && key in target) { deepMerge(target[key], source[key]); // 재귀적으로 병합 } else { target[key] = source[key]; // 값 복사 } } return target; } class Base extends MZEvent { constructor() { super(); } } /****************************************************************************** Copyright (c) Microsoft Corporation. Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ***************************************************************************** */ /* global Reflect, Promise, SuppressedError, Symbol, Iterator */ function __awaiter(thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); } typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) { var e = new Error(message); return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e; }; /** * Page class */ class PageLabelEl extends Base { constructor(label) { super(); ({ element: this.element, // contentContainerEl:this.contentContainerEl, contentEl: this.contentEl } = this.createPageLabelElement(label)); } /** * Creates the elements of this page. * @param page * @returns */ createPageLabelElement(label) { var _a; const labelEl = document.createElement('div'); const initialTop = 30; const gap = 7; labelEl.className = "page-label"; labelEl.style.top = label.top ? label.top + "px" : label.index * (((_a = label.size) === null || _a === void 0 ? void 0 : _a.height) || 30 + gap) + initialTop + 'px'; if (label.type == PageLabelType.Empty) { labelEl.classList.add("blank"); } const contentEl = document.createElement('div'); contentEl.className = "page-label-content"; contentEl.innerHTML = label.content; contentEl.style.backgroundColor = label.backgroundColor || 'rgb(48, 171, 237)'; contentEl.style.opacity = label.opacity ? label.opacity.toString() : '1'; labelEl.appendChild(contentEl); if (label.onClick) { contentEl.addEventListener('click', (event) => { label.onClick && label.onClick(label.pageIndex); }); } return { element: labelEl, contentEl: contentEl }; } resetLabelEls() { const children = Array.from(this.element.children); children.forEach(child => { if (child === this.contentEl) { const grandChildren = Array.from(child.children); grandChildren.forEach(grandChild => { child.removeChild(grandChild); }); } else { this.element.removeChild(child); } }); } } /** * Book element management class */ class BookEl extends Base { /** * Returns the left sub container element. */ // readonly labelLeftSubContainerEl: HTMLElement; /** * Returns the right sub container element. */ // readonly labelRightSubContainerEl: HTMLElement; constructor(book) { super(); // TODO: id should be unique and exist. this.thumbnails = book.thumbnails || { spine: "resources/default_spine.webp", small: "resources/default_small.webp", medium: "resources/default_medium.webp", cover: { front: "resources/default_front_cover.webp", back: "resources/default_back_cover.webp", } }; // // Element creation // const elements = this.createBookElement(); this.elementOnShelf = elements.bookOnShelfEl; this.element = elements.bookEl; this.pageContainerEl = elements.containerEl; this.labelContainerEl = elements.labelContainerEl; this.createLabels(book.labels || {}); } /** * Appends a page element into the page container element. * @param pageEl the page element to append. */ appendPageEl(pageEl) { this.pageContainerEl.appendChild(pageEl); } /** * Prepends a page element into the page container element. * @param pageEl the page element to prepend. */ prependPageEl(pageEl) { this.pageContainerEl.prepend(pageEl); } /** * Remove a page element from the page container element. * @param pageEl the page element to remove. */ removePageEl(pageEl) { this.pageContainerEl.removeChild(pageEl); } /** * Creates the book element and child elements * @returns */ createBookElement() { const bookOnShelfEl = document.createElement('div'); bookOnShelfEl.className = "book-on-shelf"; const coverEl = document.createElement('img'); coverEl.src = this.thumbnails.medium; bookOnShelfEl.appendChild(coverEl); const bookEl = document.createElement('div'); const containerEl = document.createElement('div'); // const labelContainerEls = this.createLabelContainerEl(); const labelContainerEl = document.createElement('div'); labelContainerEl.className = "label-container"; bookEl.className = "book"; containerEl.className = "container"; bookEl.appendChild(labelContainerEl); bookEl.appendChild(containerEl); return { bookOnShelfEl: bookOnShelfEl, bookEl: bookEl, containerEl: containerEl, labelContainerEl: labelContainerEl, }; } /** * Creates and adds the page labels to the book. * @param labels */ createLabels(labels) { Object.keys(labels).forEach(label => { this.addLabelEl(labels[label]); }); } /** * Adds the page label to the book. * @param label */ addLabelEl(label) { const labelEl = new PageLabelEl(label); this.labelContainerEl.appendChild(labelEl.element); } /** * Clears the children elements of the page container's element. */ clearPageEls() { this.pageContainerEl.innerHTML = ""; } /** * */ resetBookEls() { const children = Array.from(this.element.children); children.forEach(child => { if (child !== this.pageContainerEl) { this.element.removeChild(child); } }); this.clearPageEls(); } } /** * Page class */ class PageEl extends Base { constructor(page) { super(); ({ element: this.element, contentContainerEl: this.contentContainerEl, contentEl: this.contentEl } = this.createPageElement(page)); } /** * Creates the elements of this page. * @param page * @returns */ createPageElement(page) { const pageEl = document.createElement('div'); pageEl.className = "page"; pageEl.setAttribute('pageIdx', `${page.index}`); if (page.type == PageType.Empty) { const contentContainerEl = document.createElement('div'); contentContainerEl.className = `content-container`; const contentEl = document.createElement('div'); contentEl.className = "content"; contentContainerEl.appendChild(contentEl); pageEl.classList.add("empty"); pageEl.appendChild(contentContainerEl); return { element: pageEl, contentContainerEl: contentContainerEl, contentEl: contentEl }; } else if (page.type == PageType.Blank) { const contentContainerEl = document.createElement('div'); contentContainerEl.className = `content-container`; const contentEl = document.createElement('div'); contentEl.className = "content"; contentContainerEl.appendChild(contentEl); pageEl.classList.add("blank"); pageEl.appendChild(contentContainerEl); return { element: pageEl, contentContainerEl: contentContainerEl, contentEl: contentEl }; } else { // Content const contentContainerEl = document.createElement('div'); contentContainerEl.className = `content-container`; const contentEl = document.createElement('div'); contentEl.className = `content`; contentEl.innerHTML = page.content || ''; contentEl.innerHTML += page.image ? `<img src="${page.image}"/>` : ''; contentContainerEl.appendChild(contentEl); pageEl.appendChild(contentContainerEl); return { element: pageEl, contentContainerEl: contentContainerEl, contentEl: contentEl }; } } resetPageEls() { const children = Array.from(this.element.children); children.forEach(child => { if (child === this.contentContainerEl) { const grandChildren = Array.from(child.children); grandChildren.forEach(grandChild => { if (grandChild !== this.contentEl) { child.removeChild(grandChild); } }); } else { this.element.removeChild(child); } }); } } /** * */ var PageEvent; (function (PageEvent) { })(PageEvent || (PageEvent = {})); /** * Page class */ class Page extends PageEl { constructor(page) { super(page); // TODO: id should be unique and exist. this.id = page.id; this.type = page.type || PageType.Page; this.number = page.number || undefined; this.size = page.size || { width: DefaultSize.pageWidth, height: DefaultSize.pageHeight }; // TODO: index should be unique and exist. this.index = page.index; this.ignore = page.ignore || false; this.content = page.content || ""; this.image = page.image || ''; this.setEvents(); } /** * Creates and return an empty page. * @param index * @param size * @returns */ static emptyPage(index, size) { return this.createEmptyOrBlankPage(PageType.Empty, index, size); } /** * Creates and return an blank page. * @param index * @param size * @returns */ static blankPage(index, size) { return this.createEmptyOrBlankPage(PageType.Blank, index, size); } /** * Creates and return an empty or blank page. * @param index * @param size * @returns */ static createEmptyOrBlankPage(type, index, size) { return new Page({ id: `emptyPage${index}`, type: type, size: size, number: undefined, index: index, ignore: true, content: "", }); } /** * Adds all events related to this page. * @param handlers */ setEvents() { } } /** * Page class */ class PageLabel extends PageLabelEl { constructor(label) { super(label); // TODO: id should be unique and exist. this.index = label.index; this.pageIndex = label.pageIndex; this.type = label.type || PageLabelType.Default; this.size = label.size || { width: DefaultSize.pageWidth, height: DefaultSize.pageHeight }; // TODO: index should be unique and exist. this.index = label.index; this.ignore = label.ignore || false; this.content = label.content || ""; this.setEvents(); } /** * Adds all events related to this page. * @param handlers */ setEvents() { } } var BookEvent; (function (BookEvent) { BookEvent["pageAdded"] = "pageAdded"; })(BookEvent || (BookEvent = {})); /** * Book class */ class Book extends BookEl { constructor(book) { super(book); this.bookData = { id: "book1", status: BookStatus.Close, type: BookType.Book, title: "", author: "", publication: { name: "", location: "", publishedDate: "" }, lastPageIndex: 0, labels: {}, size: { closed: new SizeExt(DefaultSize.bookWidth, DefaultSize.bookHeight), opened: new SizeExt(DefaultSize.bookWidth * 2, DefaultSize.bookHeight) }, thumbnails: { spine: "", small: "", medium: "", cover: { front: "", back: "", } } }; const bookdata = deepMerge(this.bookData, book); // TODO: id should be unique and exist. this.id = bookdata.id; this.status = BookStatus.Close; this.type = bookdata.type || BookType.Book; this.title = bookdata.title || "Title"; this.author = bookdata.author || "Author"; this.publication = bookdata.publication || { name: "Publisher", location: "Location", publishedDate: "Published Date" }; this.size = new BookSize(bookdata.size || { closed: new SizeExt(DefaultSize.bookWidth, DefaultSize.bookHeight), opened: new SizeExt(DefaultSize.bookWidth * 2, DefaultSize.bookHeight) }); this.lastPageIndex = bookdata.lastPageIndex % 2 == 0 ? bookdata.lastPageIndex + 1 : bookdata.lastPageIndex; this.pages = {}; this.pageLabels = {}; Object.keys(bookdata.labels || {}).forEach(pageIndex => { this.pageLabels[pageIndex] = new PageLabel(bookdata.labels[pageIndex]); }); this.thumbnails = bookdata.thumbnails || { spine: "resources/default_spine.webp", small: "resources/default_small.webp", medium: "resources/default_medium.webp", cover: { front: "resources/default_front_cover.webp", back: "resources/default_back_cover.webp", } }; } /** * Fetches and adds a page from server. * @param index * @returns */ fetchPage(index) { return __awaiter(this, void 0, void 0, function* () { // TODO: fecth page from the server const pageSample = { id: `page${index}`, type: PageType.Page, size: { width: 600, height: 900 }, index: index, number: undefined, ignore: false, content: "", image: `./resources/page${index}.jpg` }; const page = new Page(pageSample); this.addPage(page, index); return new Promise((resolve, reject) => { resolve(pageSample); }); }); } /** * Fetches and adds pages from server. * @param indexRange the indice of the pages to fetches * @returns */ fetchPages(indexRange) { return __awaiter(this, void 0, void 0, function* () { let startIndex = indexRange.start; // if start index is negative set it as zero. if (startIndex < 0) { startIndex = 0; } const pageSamples = []; let maxIndex = startIndex + indexRange.cnt; if (maxIndex > this.lastPageIndex) { maxIndex = this.lastPageIndex; } // TODO: fecth page from the server for (let i = startIndex; i < maxIndex; i++) { // If the page is already loaded, do not fetch the page. if (this.pages[i]) { continue; } pageSamples.push({ id: `page${i}`, type: PageType.Page, size: { width: 600, height: 900 }, index: i, number: undefined, ignore: false, content: "", image: `./resources/page${i}.jpg` }); } pageSamples.forEach((pageSample) => { const page = new Page(pageSample); this.addPage(page, page.index); }); return new Promise((resolve, reject) => { resolve(pageSamples); }); }); } importPages(pages, size) { this.size.closed = new SizeExt(size.width, size.height); this.size.opened = new SizeExt(size.width * 2, size.height); pages.forEach(pageData => { const page = new Page(pageData); this.addPage(page, page.index); }); } /** * Adds a page object to the book. * @param page * @param index */ addPage(page, index) { this.pages[index] = page; this.emitEvent(BookEvent.pageAdded, page); } /** * Remove a page object from the book. * @param index */ removePage(index) { delete this.pages[index]; } /** * Returns the page object with the page index. * @param index * @returns */ getPage(index, emptyPage = false) { return this.pages[index] || (emptyPage ? this.createEmptyPage(index) : undefined); } /** * Returns the pages object. * @returns */ getPages() { return this.pages; } /** * Return the pages array length. */ getPageCnt() { return Object.keys(this.pages).length; } /** * Returns the page element with the page index. * @param index * @returns */ getPageEl(index) { return this.pages[index].element; } /** * Creates and adds an empty page object. * @param index * @param size * @returns */ createEmptyPage(index, size) { const page = Page.emptyPage(index, size || this.size.closed); this.addPage(page, index); return page; } /** * Sets the events for the book. * @param event * @param handler */ setEvents(event, handler) { } resetBook() { return new Promise((resolve, reject) => { this.element.removeAttribute('style'); this.resetBookEls(); for (const idxStr in this.pages) { const page = this.pages[idxStr]; if (page.type == PageType.Empty) { this.removePage(Number(idxStr)); } page.resetPageEls(); } resolve(); }); } } /** * BookShelf class */ class BookShelf { constructor(bookManager) { this.booksOnShelf = {}; this.bookManager = bookManager; this.bookShelfDocId = "bookShelf"; this.element = this.createElement(); } /** * Creates all book shelf elements. * @returns */ createElement() { let bookShelfEl = document.getElementById(this.bookShelfDocId); if (!bookShelfEl) { bookShelfEl = document.createElement('div'); bookShelfEl.id = this.bookShelfDocId; document.body.appendChild(bookShelfEl); } bookShelfEl.classList.toggle('hidden', this.bookManager.config.hideBookShelf); return bookShelfEl; } /** * Gets the book holder's element with the book id. * @param id * @returns */ getBookHolder(id) { return this.booksOnShelf[id].bookHolderEl; } /** * Gets the book object with the book id. * @param id * @returns */ getBook(id) { return this.booksOnShelf[id].book; } /** * Adds a book to this book shelf. * @param book */ addBook(book, event) { const bookOnShelfEl = book.elementOnShelf; const bookHolderEl = document.createElement('div'); bookHolderEl.className = "book-holder"; bookHolderEl.appendChild(bookOnShelfEl); this.element.appendChild(bookHolderEl); this.booksOnShelf[book.id] = { book: book, bookHolderEl: bookHolderEl, position: Object.keys(this.booksOnShelf).length }; // // Add event listeners to the book element. // for (const key in event) { bookOnShelfEl.addEventListener(key, event[key]); } } /** * Put back the book from the viewer to the shelf. * @param book */ putbackBook(book) { if (book.elementOnShelf) { const bookHolderEl = this.getBookHolder(book.id); bookHolderEl.appendChild(book.elementOnShelf); } } } /** * BookViewer class * Gutter: * */ class BookViewer extends Base { /** * Getter of the bookContainerEl. */ get bookContainerEl() { return this._bookContainerEl; } /** * Setter of the bookContainerEl. * @param el */ set bookContainerEl(el) { this._bookContainerEl = el; } constructor(bookManager) { super(); /** * Zoom level of the viewer. */ this.zoomLevel = 1; /** * */ this.registeredViews = {}; this.registeredEventHandlers = {}; this.callbackEventHandler = (handlerName, event) => { if (this.registeredEventHandlers[handlerName]) { this.registeredEventHandlers[handlerName](event); } }; this.bookViewerDocId = "bookViewer"; this.bookShelfManager = bookManager; if (bookManager.config.onViewerClose) { this.registeredEventHandlers['close'] = bookManager.config.onViewerClose; } ({ bookViewerEl: this.element } = this.createViewerElements()); } /** * Creates the viewer related elements. * @returns ViewerElements */ createViewerElements() { let viewerEl = document.getElementById(this.bookViewerDocId); if (viewerEl) { viewerEl.innerHTML = ""; } else { viewerEl = document.createElement('div'); viewerEl.id = this.bookViewerDocId; document.body.appendChild(viewerEl); } // Viewer viewerEl.className = ""; viewerEl.classList.add("hidden"); // Close Button const btnClose = document.createElement('button'); btnClose.id = "btnClose"; btnClose.innerHTML = "X"; btnClose.addEventListener('click', (event) => { this.closeViewer(); }); viewerEl.appendChild(btnClose); return { bookViewerEl: viewerEl }; } ; /** * * @param id * @returns */ getView(id) { return this.registeredViews[id]; } /** * * @param id * @param view */ registerView(view) { this.registeredViews[view.id] = view; if (!this.curView) { this.curView = view; } } /** * Sets the zoom level of the viewer. * @param zoomLevel */ setZoomLevel(zoomLevel) { var _a; this.zoomLevel = zoomLevel; (_a = this.curView) === null || _a === void 0 ? void 0 : _a.zoom(zoomLevel); } /** * Sets the current view. * @param viewId */ setCurView(viewId) { this.curView = this.getView(viewId); } /** * Opens the book on the viewer. * @param book * @param openPageIndex */ view(book, openPageIndex = 0) { if (!this.curView) { throw new Error('Please select one view.'); } this.book = book; this.element.className = 'hidden'; const bookContainerEl = this.bookContainerEl = this.curView.getBookContainerEl(); this.element.appendChild(bookContainerEl); this.element.classList.add(this.curView.id); this.curView.view(book, openPageIndex); this.element.classList.remove("hidden"); } /** * Closes the book on the viewer. */ closeViewer() { var _a; this.element.className = 'hidden'; this.bookContainerEl && this.element.removeChild(this.bookContainerEl); (_a = this.curView) === null || _a === void 0 ? void 0 : _a.closeViewer(); this.bookShelfManager.returnBookToShelf(this.book); if (this.book) { this.book.resetBook(); if (this.bookContainerEl) { this.bookContainerEl.className = ""; this.bookContainerEl.removeChild(this.book.element); this.bookContainerEl = undefined; } this.book = undefined; } this.callbackEventHandler('close'); } /** * * @param id */ changeView(id) { const view = this.getView(id); if (view) { this.setCurView(id); } } } /** * BookManager class */ class BookShelfManager { /** * * @param bookShelfDocId * @param bookViewerId */ constructor(config) { this.config = { hideBookShelf: false }; this.config = deepMerge(this.config, config); this.bookShelf = new BookShelf(this); this.bookViewer = new BookViewer(this); } /** * Change viewing book style. * @param type */ getBookViewer() { return this.bookViewer; } /** * Gets a book holder's element with the book's id. * @param id * @returns */ getBookHolder(id) { return this.bookShelf.getBookHolder(id); } /** * Gets a book object with the book's id. * @param id * @returns */ getBook(id) { return this.bookShelf.getBook(id); } /** * Load books from the server and add them to the shelf. */ loadAndAddBooks() { return __awaiter(this, void 0, void 0, function* () { // TODO: Control the number of books to load. // TODO: fetch books from the server const bookSamplesLoaded = [ { id: "book1", status: BookStatus.Close, title: "Book 1", type: BookType.Magazine, author: "Shinkee", publication: { name: "Magzog", location: "Auckland in New Zealand", publishedDate: "2022-09-01" }, size: new BookSize({ closed: { width: 600, height: 900, diagonal: 0 }, opened: { width: 1200, height: 900, diagonal: 0 } }), lastPageIndex: 5, thumbnails: { spine: "resources/cover.jpg", small: "resources/cover.jpg", medium: "resources/cover.jpg", cover: { front: "resources/cover.jpg", back: "resources/cover.jpg", } }, } ]; bookSamplesLoaded.forEach((bookSample) => { this.addBookToShelf(bookSample); }); }); } /** * Load a book from the server and add it to the shelf. * @param id */ loadAndAddBook(id) { return __awaiter(this, void 0, void 0, function* () { // TODO: fetch book from the server const bookSample = { id: "book6", status: BookStatus.Close, title: "The Great Gatsby", type: BookType.Magazine, author: "Shinkee", publication: { name: "Magzog", location: "Auckland in New Zealand", publishedDate: "2021-09-01" }, size: new BookSize({ closed: { width: 600, height: 900, diagonal: 0 }, opened: { width: 1200, height: 900, diagonal: 0 } }), lastPageIndex: 5, thumbnails: { spine: "resources/cover.webp", small: "resources/cover.webp", medium: "resources/cover.webp", cover: { front: "resources/cover.webp", back: "resources/cover.webp", } }, }; this.addBookToShelf(bookSample); }); } importBookToShelf(book) { this.bookShelf.addBook(book, { click: (event) => { this.pickupAndView(this.getBook(book.id)); } }); } /** * Append the book to the shelf. * @param id */ addBookToShelf(book) { this.bookShelf.addBook(new Book(book), { click: (event) => { this.pickupAndView(this.getBook(book.id)); } }); } /** * Pickup a book from the shelf and view it on the viewer. * @param book * @returns */ pickupAndView(book) { var _a; if (book.status == BookStatus.Open) { return; } book.status = BookStatus.Open; (_a = this.bookViewer) === null || _a === void 0 ? void 0 : _a.view(book); } /** * Put back the book from the viewer to the shelf. * @param book */ returnBookToShelf(book) { if (!book) ; else { if (book.status == BookStatus.Close) { return; } book.status = BookStatus.Close; this.bookShelf.putbackBook(book); } } } export { AutoFlipType, Base, Book, BookEl, BookEvent, BookShelf, BookShelfManager, BookSize, BookStatus, BookType, BookViewer, DefaultSize, EventStatus, Line, MZEvent, MZMath, Page, PageEl, PageEvent, PageLabelType, PageType, Point, Rect, SizeExt, ViewerType, Zone, deepMerge };