@elibrary-inno/bookreader
Version:
The Internet Archive BookReader.
777 lines (650 loc) • 26.7 kB
JavaScript
// @ts-check
import { customElement, property, query } from 'lit/decorators.js';
import {LitElement, html} from 'lit';
import { styleMap } from 'lit/directives/style-map.js';
import { ModeSmoothZoom } from './ModeSmoothZoom';
import { arrChanged, promisifyEvent } from './utils';
import { HTMLDimensionsCacher } from "./utils/HTMLDimensionsCacher";
import { PageModel } from './BookModel';
import { ModeCoordinateSpace } from './ModeCoordinateSpace';
/** @typedef {import('./BookModel').BookModel} BookModel */
/** @typedef {import('./BookModel').PageIndex} PageIndex */
/** @typedef {import('./ModeSmoothZoom').SmoothZoomable} SmoothZoomable */
/** @typedef {import('./PageContainer').PageContainer} PageContainer */
/** @typedef {import('../BookReader').default} BookReader */
// I _have_ to make this globally public, otherwise it won't let me call
// its constructor :/
/** @implements {SmoothZoomable} */
export class Mode2UpLit extends LitElement {
/****************************************/
/************** PROPERTIES **************/
/****************************************/
/** @type {BookReader} */
br;
/************** BOOK-RELATED PROPERTIES **************/
/** @type {BookModel} */
book;
/************** SCALE-RELATED PROPERTIES **************/
/** @type {ModeCoordinateSpace} Manage conversion between coordinates */
coordSpace = new ModeCoordinateSpace(this);
scale = 1;
initialScale = 1;
/** @type {import('./options').AutoFitValues} */
autoFit = 'auto';
/************** VIRTUAL-FLIPPING PROPERTIES **************/
/** ms for flip animation */
flipSpeed = 400;
$flippingEdges;
/** @type {PageModel[]} */
visiblePages = [];
/** @type {PageModel | null} */
get pageLeft() {
return this.visiblePages.find(p => p.pageSide == 'L');
}
/** @type {PageModel | null} */
get pageRight() {
return this.visiblePages.find(p => p.pageSide == 'R');
}
/** @type {PageModel[]} */
renderedPages = [];
/** @type {Record<PageIndex, PageContainer>} position in inches */
pageContainerCache = {};
/** @type {{ direction: 'left' | 'right', pagesFlipping: [PageIndex, PageIndex], pagesFlippingCount: number }} */
activeFlip = null;
/** @private cache this value */
_leftCoverWidth = 0;
/************** DOM-RELATED PROPERTIES **************/
/** @type {HTMLElement} */
get $container() { return this; }
/** @type {HTMLElement} */
get $visibleWorld() { return this.$book; }
/** @type {HTMLElement} */
$book;
get positions() {
return this.computePositions(this.pageLeft, this.pageRight);
}
/** @param {PageModel} page */
computePageHeight(page) {
return this.book.getMedianPageSizeInches().height;
}
/** @param {PageModel} page */
computePageWidth(page) {
return page.widthInches * this.computePageHeight(page) / page.heightInches;
}
/**
* @param {PageModel | null} pageLeft
* @param {PageModel | null} pageRight
*/
computePositions(pageLeft, pageRight) {
const computePageWidth = this.computePageWidth.bind(this);
const numLeafs = this.book.getNumLeafs();
const movingPagesWidth = this.activeFlip ? Math.ceil(this.activeFlip.pagesFlippingCount / 2) * this.PAGE_THICKNESS_IN : 0;
const leftPagesCount = this.book.pageProgression == 'lr' ? (pageLeft?.index ?? 0) : (!pageLeft ? 0 : numLeafs - pageLeft.index);
// Everything is relative to the gutter
const gutter = this._leftCoverWidth + leftPagesCount * this.PAGE_THICKNESS_IN;
const pageLeftEnd = gutter;
const pageLeftWidth = !pageLeft ? computePageWidth(pageRight.right) : computePageWidth(pageLeft);
const pageLeftStart = gutter - pageLeftWidth;
const leafEdgesLeftEnd = pageLeftStart; // leafEdgesLeftStart + leafEdgesLeftMainWidth + leafEdgesLeftMovingWidth;
const leafEdgesLeftMovingWidth = this.activeFlip?.direction != 'left' ? 0 : movingPagesWidth;
const leafEdgesLeftMainWidth = Math.ceil(leftPagesCount / 2) * this.PAGE_THICKNESS_IN - leafEdgesLeftMovingWidth;
const leafEdgesLeftFullWidth = leafEdgesLeftMovingWidth + leafEdgesLeftMainWidth;
const leafEdgesLeftMovingStart = leafEdgesLeftEnd - leafEdgesLeftMovingWidth;
const leafEdgesLeftStart = leafEdgesLeftMovingStart - leafEdgesLeftMainWidth;
const pageRightStart = gutter;
const pageRightWidth = !pageRight ? 0 : computePageWidth(pageRight);
const pageRightEnd = pageRightStart + pageRightWidth;
const rightPagesCount = this.book.pageProgression == 'lr' ? (!pageRight ? 0 : numLeafs - pageRight.index) : (pageRight?.index ?? 0);
const leafEdgesRightStart = pageRightEnd;
const leafEdgesRightMovingWidth = this.activeFlip?.direction != 'right' ? 0 : movingPagesWidth;
const leafEdgesRightMainStart = leafEdgesRightStart + leafEdgesRightMovingWidth;
const leafEdgesRightMainWidth = Math.ceil(rightPagesCount / 2) * this.PAGE_THICKNESS_IN - leafEdgesRightMovingWidth;
const leafEdgesRightEnd = leafEdgesRightStart + leafEdgesRightMainWidth + leafEdgesRightMovingWidth;
const leafEdgesRightFullWidth = leafEdgesRightMovingWidth + leafEdgesRightMainWidth;
const spreadWidth = pageRightEnd - pageLeftStart;
const bookWidth = leafEdgesRightEnd - leafEdgesLeftStart;
return {
leafEdgesLeftStart,
leafEdgesLeftMainWidth,
leafEdgesLeftMovingStart,
leafEdgesLeftMovingWidth,
leafEdgesLeftEnd,
leafEdgesLeftFullWidth,
pageLeftStart,
pageLeftWidth,
pageLeftEnd,
gutter,
pageRightStart,
pageRightWidth,
pageRightEnd,
leafEdgesRightStart,
leafEdgesRightMovingWidth,
leafEdgesRightMainStart,
leafEdgesRightMainWidth,
leafEdgesRightEnd,
leafEdgesRightFullWidth,
spreadWidth,
bookWidth,
};
}
/** @type {HTMLDimensionsCacher} Cache things like clientWidth to reduce repaints */
htmlDimensionsCacher = new HTMLDimensionsCacher(this);
smoothZoomer = new ModeSmoothZoom(this);
/************** CONSTANT PROPERTIES **************/
/** How much to zoom when zoom button pressed */
ZOOM_FACTOR = 1.1;
/** How thick a page is in the real world, as an estimate for the leafs */
PAGE_THICKNESS_IN = 0.002;
/****************************************/
/************** PUBLIC API **************/
/****************************************/
/************** MAIN PUBLIC METHODS **************/
/**
* @param {PageIndex} index
* TODO Remove smooth option from everywhere.
*/
async jumpToIndex(index, { smooth = true } = {}) {
await this.flipAnimation(index, { animate: smooth });
}
zoomIn() {
this.scale *= this.ZOOM_FACTOR;
}
zoomOut() {
this.scale *= 1 / this.ZOOM_FACTOR;
}
/********************************************/
/************** INTERNAL STUFF **************/
/********************************************/
/************** LIFE CYCLE **************/
/**
* @param {BookModel} book
* @param {BookReader} br
*/
constructor(book, br) {
super();
this.book = book;
/** @type {BookReader} */
this.br = br;
}
/** @override */
firstUpdated(changedProps) {
super.firstUpdated(changedProps);
this.htmlDimensionsCacher.updateClientSizes();
this.smoothZoomer.attach();
}
/** @override */
connectedCallback() {
super.connectedCallback();
this.htmlDimensionsCacher.attachResizeListener();
this.smoothZoomer.attach();
}
/** @override */
disconnectedCallback() {
this.htmlDimensionsCacher.detachResizeListener();
this.smoothZoomer.detach();
super.disconnectedCallback();
}
/** @override */
updated(changedProps) {
// this.X is the new value
// changedProps.get('X') is the old value
if (changedProps.has('book')) {
this._leftCoverWidth = this.computePageWidth(this.book.getPage(this.book.pageProgression == 'lr' ? 0 : -1));
}
if (changedProps.has('visiblePages')) {
this.renderedPages = this.computeRenderedPages();
this.br.displayedIndices = this.visiblePages.map(p => p.index);
this.br.updateFirstIndex(this.br.displayedIndices[0]);
this.br._components.navbar.updateNavIndexThrottled();
}
if (changedProps.has('autoFit')) {
if (this.autoFit != 'none') {
this.resizeViaAutofit();
}
}
if (changedProps.has('scale')) {
const oldVal = changedProps.get('scale');
this.recenter();
this.smoothZoomer.updateViewportOnZoom(this.scale, oldVal);
}
}
/************** LIT CONFIGS **************/
/** @override */
createRenderRoot() {
// Disable shadow DOM; that would require a huge rejiggering of CSS
return this;
}
/************** RENDERING **************/
/** @override */
render() {
return html`
<div class="br-mode-2up__book" =${this.handlePageClick}>
${this.renderLeafEdges('left')}
${this.renderedPages.map(p => this.renderPage(p))}
${this.renderLeafEdges('right')}
</div>`;
}
/** @param {PageModel} page */
createPageContainer = (page) => {
return this.pageContainerCache[page.index] || (
this.pageContainerCache[page.index] = (
// @ts-ignore I know it's protected, TS! But Mode2Up and BookReader are friends.
this.br._createPageContainer(page.index)
)
);
}
/**
* @param {PageIndex} startIndex
*/
initFirstRender(startIndex) {
const page = this.book.getPage(startIndex);
const spread = page.spread;
this.visiblePages = (
this.book.pageProgression == 'lr' ? [spread.left, spread.right] : [spread.right, spread.left]
).filter(p => p);
this.htmlDimensionsCacher.updateClientSizes();
this.resizeViaAutofit(page);
this.initialScale = this.scale;
}
/** @param {PageModel} page */
renderPage = (page) => {
const wToR = this.coordSpace.worldUnitsToRenderedPixels;
const wToV = this.coordSpace.worldUnitsToVisiblePixels;
const width = wToR(this.computePageWidth(page));
const height = wToR(this.computePageHeight(page));
const isVisible = this.visiblePages.map(p => p.index).includes(page.index);
const positions = this.computePositions(page.spread.left, page.spread.right);
const pageContainerEl = this.createPageContainer(page)
.update({
dimensions: {
width,
height,
top: 0,
left: wToR(page.pageSide == 'L' ? positions.pageLeftStart : positions.pageLeftEnd),
},
reduce: page.width / wToV(this.computePageWidth(page)),
}).$container[0];
pageContainerEl.classList.toggle('BRpage-visible', isVisible);
return pageContainerEl;
}
/**
* @param {'left' | 'right'} side
* Renders the current leaf edges, as well as any "moving" leaf edges,
* i.e. leaf edges that are currently being flipped. Uses a custom element
* to render br-leaf-edges.
**/
renderLeafEdges = (side) => {
if (!this.visiblePages.length) return html``;
const fullWidthIn = side == 'left' ? this.positions.leafEdgesLeftFullWidth : this.positions.leafEdgesRightFullWidth;
if (!fullWidthIn) return html``;
const wToR = this.coordSpace.worldUnitsToRenderedPixels;
const height = wToR(this.computePageHeight(this.visiblePages[0]));
const hasMovingPages = this.activeFlip?.direction == side;
const leftmostPage = this.book.getPage(this.book.pageProgression == 'lr' ? 0 : this.book.getNumLeafs() - 1);
const rightmostPage = this.book.getPage(this.book.pageProgression == 'lr' ? this.book.getNumLeafs() - 1 : 0);
const numPagesFlipping = hasMovingPages ? this.activeFlip.pagesFlippingCount : 0;
const range = side == 'left' ?
[leftmostPage.index, this.pageLeft.goLeft(numPagesFlipping).index] :
[this.pageRight.goRight(numPagesFlipping).index, rightmostPage.index];
const mainEdgesStyle = {
width: `${wToR(side == 'left' ? this.positions.leafEdgesLeftMainWidth : this.positions.leafEdgesRightMainWidth)}px`,
height: `${height}px`,
left: `${wToR(side == 'left' ? this.positions.leafEdgesLeftStart : this.positions.leafEdgesRightMainStart)}px`,
};
const mainEdges = html`
<br-leaf-edges
leftIndex=${range[0]}
rightIndex=${range[1]}
.book=${this.book}
.pageClickHandler=${(index) => this.br.jumpToIndex(index)}
side=${side}
class="br-mode-2up__leafs br-mode-2up__leafs--${side}"
style=${styleMap(mainEdgesStyle)}
></br-leaf-edges>
`;
if (hasMovingPages) {
const width = wToR(side == 'left' ? this.positions.leafEdgesLeftMovingWidth : this.positions.leafEdgesRightMovingWidth);
const style = {
width: `${width}px`,
height: `${height}px`,
left: `${wToR(side == 'left' ? this.positions.leafEdgesLeftMovingStart : this.positions.leafEdgesRightStart)}px`,
pointerEvents: 'none',
transformOrigin: `${wToR(side == 'left' ? this.positions.pageLeftWidth : -this.positions.pageRightWidth) + width / 2}px 0`,
};
const movingEdges = html`
<br-leaf-edges
leftIndex=${this.activeFlip.pagesFlipping[0]}
rightIndex=${this.activeFlip.pagesFlipping[1]}
.book=${this.book}
side=${side}
class="br-mode-2up__leafs br-mode-2up__leafs--${side} br-mode-2up__leafs--flipping"
style=${styleMap(style)}
></br-leaf-edges>
`;
return side == 'left' ? html`${mainEdges}${movingEdges}` : html`${movingEdges}${mainEdges}`;
} else {
return mainEdges;
}
}
resizeViaAutofit(page = this.visiblePages[0]) {
this.scale = this.computeScale(page, this.autoFit);
}
recenter(page = this.visiblePages[0]) {
const translate = this.computeTranslate(page, this.scale);
this.$book.style.transform = `translateX(${translate.x}px) translateY(${translate.y}px) scale(${this.scale})`;
}
/**
* @returns {PageModel[]}
*/
computeRenderedPages() {
// Also render 2 pages before/after
// @ts-ignore TS doesn't understand the filtering out of null values
return [
this.visiblePages[0]?.prev?.prev,
this.visiblePages[0]?.prev,
...this.visiblePages,
this.visiblePages[this.visiblePages.length - 1]?.next,
this.visiblePages[this.visiblePages.length - 1]?.next?.next,
]
.filter(p => p)
// Never render more than 10 pages! Usually means something is wrong
.slice(0, 10);
}
/**
* @param {PageModel} page
* @param {import('./options').AutoFitValues} autoFit
*/
computeScale(page, autoFit) {
if (!page) return 1;
const spread = page.spread;
const bookWidth = this.computePositions(spread.left, spread.right).bookWidth;
const bookHeight = this.computePageHeight(spread.left || spread.right);
const BOOK_PADDING_PX = 10;
const curScale = this.scale;
this.scale = 1; // Need this temporarily
const widthScale = this.coordSpace.renderedPixelsToWorldUnits(this.htmlDimensionsCacher.clientWidth - 2 * BOOK_PADDING_PX) / bookWidth;
const heightScale = this.coordSpace.renderedPixelsToWorldUnits(this.htmlDimensionsCacher.clientHeight - 2 * BOOK_PADDING_PX) / bookHeight;
this.scale = curScale;
const realScale = 1;
let scale = realScale;
if (autoFit == 'width') {
scale = widthScale;
} else if (autoFit == 'height') {
scale = heightScale;
} else if (autoFit == 'auto') {
scale = Math.min(widthScale, heightScale);
} else if (autoFit == 'none') {
scale = this.scale;
} else {
// Should be impossible
throw new Error(`Invalid autoFit value: ${autoFit}`);
}
return scale;
}
/**
* @param {PageModel} page
* @param {number} scale
* @returns {{x: number, y: number}}
*/
computeTranslate(page, scale = this.scale) {
if (!page) return { x: 0, y: 0 };
const spread = page.spread;
// Default to real size if it fits, otherwise default to full height
const positions = this.computePositions(spread.left, spread.right);
const bookWidth = positions.bookWidth;
const bookHeight = this.computePageHeight(spread.left || spread.right);
const visibleBookWidth = this.coordSpace.worldUnitsToRenderedPixels(bookWidth) * scale;
const visibleBookHeight = this.coordSpace.worldUnitsToRenderedPixels(bookHeight) * scale;
const leftOffset = this.coordSpace.worldUnitsToRenderedPixels(-positions.leafEdgesLeftStart) * scale;
const translateX = (this.htmlDimensionsCacher.clientWidth - visibleBookWidth) / 2 + leftOffset;
const translateY = (this.htmlDimensionsCacher.clientHeight - visibleBookHeight) / 2;
return { x: Math.max(leftOffset, translateX), y: Math.max(0, translateY) };
}
/************** VIRTUAL FLIPPING LOGIC **************/
/**
* @param {'left' | 'right' | 'next' | 'prev' | PageIndex | PageModel | {left: PageModel | null, right: PageModel | null}} nextSpread
*/
async flipAnimation(nextSpread, { animate = true } = {}) {
const curSpread = (this.pageLeft || this.pageRight)?.spread;
if (!curSpread) {
// Nothings been actually rendered yet! Will be corrected during initFirstRender
return;
}
nextSpread = this.parseNextSpread(nextSpread);
if (this.activeFlip || !nextSpread) return;
const progression = this.book.pageProgression;
const curLeftIndex = curSpread.left?.index ?? (progression == 'lr' ? -1 : this.book.getNumLeafs());
const nextLeftIndex = nextSpread.left?.index ?? (progression == 'lr' ? -1 : this.book.getNumLeafs());
if (curLeftIndex == nextLeftIndex) return;
const renderedIndices = this.renderedPages.map(p => p.index);
/** @type {PageContainer[]} */
const nextPageContainers = [];
for (const page of [nextSpread.left, nextSpread.right]) {
if (!page) continue;
nextPageContainers.push(this.createPageContainer(page));
if (!renderedIndices.includes(page.index)) {
this.renderedPages.push(page);
}
}
const curTranslate = this.computeTranslate(curSpread.left || curSpread.right, this.scale);
const idealNextTranslate = this.computeTranslate(nextSpread.left || nextSpread.right, this.scale);
const translateDiff = Math.sqrt((idealNextTranslate.x - curTranslate.x) ** 2 + (idealNextTranslate.y - curTranslate.y) ** 2);
let nextTranslate = `translate(${idealNextTranslate.x}px, ${idealNextTranslate.y}px)`;
if (translateDiff < 50) {
const activeTranslate = this.$book.style.transform.match(/translate\([^)]+\)/)?.[0];
if (activeTranslate) {
nextTranslate = activeTranslate;
}
}
const newTransform = `${nextTranslate} scale(${this.scale})`;
if (animate && 'animate' in Element.prototype) {
// This table is used to determine the direction of the flip animation:
// | < | >
// lr | L | R
// rl | R | L
const direction = progression == 'lr' ? (nextLeftIndex > curLeftIndex ? 'right' : 'left') : (nextLeftIndex > curLeftIndex ? 'left' : 'right');
this.activeFlip = {
direction,
pagesFlipping: [curLeftIndex, nextLeftIndex],
pagesFlippingCount: Math.abs(nextLeftIndex - curLeftIndex),
};
this.classList.add(`br-mode-2up--flipping-${direction}`);
this.classList.add(`BRpageFlipping`);
// Wait for lit update cycle to finish so that entering pages are rendered
this.requestUpdate();
await this.updateComplete;
this.visiblePages
.map(p => this.pageContainerCache[p.index].$container)
.forEach($c => $c.addClass('BRpage-exiting'));
nextPageContainers.forEach(c => c.$container.addClass('BRpage-entering'));
/** @type {KeyframeAnimationOptions} */
const animationStyle = {
duration: this.flipSpeed + this.activeFlip.pagesFlippingCount,
easing: 'ease-in',
fill: 'none',
};
const bookCenteringAnimation = this.$book.animate([
{ transform: newTransform },
], animationStyle);
const edgeTranslationAnimation = this.$flippingEdges.animate([
{ transform: `rotateY(0deg)` },
{
transform: direction == 'left' ? `rotateY(-180deg)` : `rotateY(180deg)`,
},
], animationStyle);
const exitingPageAnimation = direction == 'left' ?
this.querySelector('.BRpage-exiting[data-side=L]').animate([
{ transform: `rotateY(0deg)` },
{ transform: `rotateY(180deg)` },
], animationStyle) : this.querySelector('.BRpage-exiting[data-side=R]').animate([
{ transform: `rotateY(0deg)` },
{ transform: `rotateY(-180deg)` },
], animationStyle);
const enteringPageAnimation = direction == 'left' ?
this.querySelector('.BRpage-entering[data-side=R]').animate([
{ transform: `rotateY(-180deg)` },
{ transform: `rotateY(0deg)` },
], animationStyle) : this.querySelector('.BRpage-entering[data-side=L]').animate([
{ transform: `rotateY(180deg)` },
{ transform: `rotateY(0deg)` },
], animationStyle);
bookCenteringAnimation.play();
edgeTranslationAnimation.play();
exitingPageAnimation.play();
enteringPageAnimation.play();
nextPageContainers.forEach(c => c.$container.addClass('BRpage-visible'));
await Promise.race([
promisifyEvent(bookCenteringAnimation, 'finish'),
promisifyEvent(edgeTranslationAnimation, 'finish'),
promisifyEvent(exitingPageAnimation, 'finish'),
promisifyEvent(enteringPageAnimation, 'finish'),
]);
this.classList.remove(`br-mode-2up--flipping-${direction}`);
this.classList.remove(`BRpageFlipping`);
this.visiblePages
.map(p => this.pageContainerCache[p.index].$container)
.forEach($c => $c.removeClass('BRpage-exiting BRpage-visible'));
nextPageContainers.forEach(c => c.$container.removeClass('BRpage-entering'));
this.activeFlip = null;
}
this.$book.style.transform = newTransform;
this.visiblePages = (
progression == 'lr' ? [nextSpread.left, nextSpread.right] : [nextSpread.right, nextSpread.left]
).filter(x => x);
}
/**
* @param {'left' | 'right' | 'next' | 'prev' | PageIndex | PageModel | {left: PageModel | null, right: PageModel | null}} nextSpread
* @returns {{left: PageModel | null, right: PageModel | null}}
*/
parseNextSpread(nextSpread) {
if (nextSpread == 'next') {
nextSpread = this.book.pageProgression == 'lr' ? 'right' : 'left';
} else if (nextSpread == 'prev') {
nextSpread = this.book.pageProgression == 'lr' ? 'left' : 'right';
}
const curSpread = (this.pageLeft || this.pageRight).spread;
if (nextSpread == 'left') {
nextSpread = curSpread.left?.findLeft({ combineConsecutiveUnviewables: true })?.spread;
} else if (nextSpread == 'right') {
nextSpread = curSpread.right?.findRight({ combineConsecutiveUnviewables: true })?.spread;
}
if (typeof(nextSpread) == 'number') {
nextSpread = this.book.getPage(nextSpread).spread;
}
if (nextSpread instanceof PageModel) {
nextSpread = nextSpread.spread;
}
return nextSpread;
}
/************** INPUT HANDLERS **************/
/**
* @param {MouseEvent} ev
*/
handlePageClick = (ev) => {
// right click
if (ev.which == 3 && this.br.protected) {
return false;
}
if (ev.which != 1) return;
const $page = $(ev.target).closest('.BRpagecontainer');
if (!$page.length) return;
if ($page.data('side') == 'L') {
this.flipAnimation('left');
} else if ($page.data('side') == 'R') {
this.flipAnimation('right');
}
}
}
export class LeafEdges extends LitElement {
leftIndex = 0;
rightIndex = 0;
/** @type {'left' | 'right'} */
side = 'left';
/** @type {BookModel} */
book = null;
/** @type {(index: PageIndex) => void} */
pageClickHandler = null;
$hoverBar;
$hoverLabel;
get pageWidthPercent() {
return 100 * 1 / (this.rightIndex - this.leftIndex + 1);
}
render() {
return html`
<div
class="br-leaf-edges__bar"
style="${styleMap({width: `${this.pageWidthPercent}%`})}"
></div>
<div class="br-leaf-edges__label">Page</div>`;
}
connectedCallback() {
super.connectedCallback();
this.addEventListener('mouseenter', this.onMouseEnter);
this.addEventListener('mouseleave', this.onMouseLeave);
this.addEventListener('click', this.onClick);
}
disconnectedCallback() {
super.disconnectedCallback();
this.addEventListener('mouseenter', this.onMouseEnter);
this.removeEventListener('mousemove', this.onMouseMove);
this.removeEventListener('mouseleave', this.onMouseLeave);
}
/** @override */
createRenderRoot() {
// Disable shadow DOM; that would require a huge rejiggering of CSS
return this;
}
/**
* @param {MouseEvent} e
*/
onMouseEnter = (e) => {
this.addEventListener('mousemove', this.onMouseMove);
this.$hoverBar.style.display = 'block';
this.$hoverLabel.style.display = 'block';
}
/**
* @param {MouseEvent} e
*/
onMouseMove = (e) => {
this.$hoverBar.style.left = `${e.offsetX}px`;
if (this.side == 'right') {
this.$hoverLabel.style.left = `${e.offsetX}px`;
} else {
this.$hoverLabel.style.right = `${this.offsetWidth - e.offsetX}px`;
}
this.$hoverLabel.style.top = `${e.offsetY}px`;
const index = this.mouseEventToPageIndex(e);
this.$hoverLabel.textContent = this.book.getPageName(index);
}
/**
* @param {MouseEvent} e
*/
onMouseLeave = (e) => {
this.removeEventListener('mousemove', this.onMouseMove);
this.$hoverBar.style.display = 'none';
this.$hoverLabel.style.display = 'none';
}
/**
* @param {MouseEvent} e
*/
onClick = (e) => {
this.pageClickHandler(this.mouseEventToPageIndex(e));
}
/**
* @param {MouseEvent} e
* @returns {PageIndex}
*/
mouseEventToPageIndex(e) {
return Math.floor(this.leftIndex + (e.offsetX / this.offsetWidth) * (this.rightIndex - this.leftIndex + 1));
}
}