@elibrary-inno/bookreader
Version:
The Internet Archive BookReader.
550 lines (485 loc) • 15.1 kB
JavaScript
import { LitElement, html, css, render } from 'lit';
// eslint-disable-next-line no-unused-vars
import { ModalConfig, ModalManager } from '@internetarchive/modal-manager';
import buttonStyles from '../assets/button-base.js';
import './bookmarks-loginCTA.js';
const api = {
endpoint: '/services/bookmarks.php',
headers: {
'Content-Type': 'application/json',
},
delete(page) {
return fetch(`${this.endpoint}?identifier=${this.identifier}&page_num=${page}`, {
credentials: 'same-origin',
method: 'DELETE',
headers: this.headers,
});
},
get(page) {
return fetch(`${this.endpoint}?identifier=${this.identifier}&page_num=${page}`, {
credentials: 'same-origin',
method: 'GET',
headers: this.headers,
});
},
getAll() {
return fetch(`${this.endpoint}?identifier=${this.identifier}`, {
credentials: 'same-origin',
method: 'GET',
headers: this.headers,
});
},
post(bookmark) {
return this.sendBookmarkData(bookmark, 'POST');
},
put(bookmark) {
return this.sendBookmarkData(bookmark, 'POST');
},
sendBookmarkData(bookmark, method) {
const notes = {
note: bookmark.note,
color: bookmark.color,
};
return fetch(`${this.endpoint}?identifier=${this.identifier}&page_num=${bookmark.id}`, {
credentials: 'same-origin',
method,
headers: this.headers,
body: JSON.stringify({
notes,
}),
});
},
};
class IABookmarks extends LitElement {
static get properties() {
return {
activeBookmarkID: { type: String },
bookmarks: { type: Array },
bookreader: { type: Object },
displayMode: { type: String },
editedBookmark: { type: Object },
deleteModalConfig: { type: Object},
modal: { attribute: false },
loginOptions: { type: Object, attribute: false }
};
}
static get styles() {
const mainCss = css`
.bookmarks {
height: 100%;
overflow: hidden;
padding-bottom: 20px;
}
.list ia-bookmark-edit {
display: none;
}
.edit ia-bookmarks-list {
display: none;
}
`;
return [buttonStyles, mainCss];
}
static formatPage(page) {
return isNaN(+page) ? `(${page.replace(/\D/g, '')})` : page;
}
constructor() {
super();
this.bookmarks = [];
this.bookreader = {};
this.editedBookmark = {};
/** @type {ModalManager} */
this.modal = undefined;
this.loginOptions = {
loginClicked: () => {},
loginUrl: '',
};
/**
* Toggles display to either bookmarks or login cta
* @param {('bookmarks'|'login')} displayMode
*/
this.displayMode = 'bookmarks';
this.bookmarkColors = [{
id: 0,
className: 'red',
}, {
id: 1,
className: 'blue',
}, {
id: 2,
className: 'green',
}];
// eslint-disable-next-line
this.defaultColor = this.bookmarkColors[0];
this.api = api;
this.deleteModalConfig = new ModalConfig({
title: 'Delete Bookmark',
headline: 'This bookmark contains a note. Deleting it will permanently delete the note. Are you sure?',
headerColor: '#194880',
});
}
updated(changed) {
if (changed.has('displayMode')) {
this.updateDisplay();
}
this.emitBookmarksChanged();
}
setup() {
this.api.identifier = this.getIdentifier();
if (this.displayMode === 'login') {
return;
}
this.fetchUserBookmarks();
this.setBREventListeners();
}
/**
* get identifier for current book including sub-files
*
* @returns Identifer
*/
getIdentifier() {
if (this.bookreader.bookId !== this.bookreader.subPrefix) {
return `${this.bookreader.bookId}/${this.bookreader.subPrefix}`;
}
return this.bookreader.bookId;
}
updateDisplay() {
if (this.displayMode === 'bookmarks') {
this.fetchUserBookmarks();
}
}
async fetchUserBookmarks() {
if (!this.api.identifier) {
return;
}
await this.fetchBookmarks();
this.initializeBookmarks();
}
setBREventListeners() {
['3PageViewSelected'].forEach((event) => {
window.addEventListener(`BookReader:${event}`, (e) => {
setTimeout(() => {
// wait a lil bit so bookreader can draw its DOM to attach onto
this.renderBookmarkButtons();
}, 100);
});
});
['pageChanged', '1PageViewSelected', '2PageViewSelected'].forEach((event) => {
window.addEventListener(`BookReader:${event}`, (e) => {
setTimeout(() => {
// wait a lil bit so bookreader can draw its DOM to attach onto
this.renderBookmarkButtons();
this.markActiveBookmark();
}, 100);
});
});
['zoomOut', 'zoomIn', 'resize'].forEach((event) => {
window.addEventListener(`BookReader:${event}`, () => {
this.renderBookmarkButtons();
});
});
}
initializeBookmarks() {
this.renderBookmarkButtons();
this.markActiveBookmark(true);
this.emitBookmarksChanged();
}
/**
* @typedef {object} Bookmark
* @property {number} id - bookreader page index, becomes key store
* @property {number} color - color number
* @property {string} page - bookmark's page label to display
* @property {string} note - optional, note that one can add
* @property {string} thumbnail - optional, image url
*/
/**
* Formats bookmark view model
* @param {Object} bookmarkAttrs
* @param {number} bookmarkAttrs.leafNum
* @param {string} bookmarkAttrs.notes
*
* @returns Bookmark
*/
formatBookmark({ leafNum = '', notes = {} }) {
const { note = '', color } = notes;
const nomalizedParams = {
note,
color: this.getBookmarkColor(color) ? color : this.defaultColor.id,
};
const page = IABookmarks.formatPage(this.bookreader.book.getPageNum(leafNum));
const thumbnail = this.bookreader.book.getPageURI(`${leafNum}`.replace(/\D/g, ''), 32); // Request thumbnail 1/32 the size of original image
const bookmark = {
...nomalizedParams,
id: leafNum,
leafNum,
page,
thumbnail,
};
return bookmark;
}
async fetchBookmarks() {
const resText = await this.api.getAll().then(r=> r.text());
let parsedResponse;
try {
parsedResponse = JSON.parse(resText);
} catch (e) {
parsedResponse = {error : e.message};
}
const {
success,
error = 'Something happened while fetching bookmarks.',
value: bkmrks = [],
} = parsedResponse;
if (!success) {
console?.warn('Error fetching bookmarks', error);
}
const bookmarks = {};
Object.keys(bkmrks).forEach((leafNum) => {
const bookmark = bkmrks[leafNum];
const formattedLeafNum = parseInt(leafNum, 10);
const formattedBookmark = this.formatBookmark({ ...bookmark, leafNum: formattedLeafNum });
bookmarks[leafNum] = formattedBookmark;
});
this.bookmarks = bookmarks;
return bookmarks;
}
emitBookmarksChanged() {
this.dispatchEvent(new CustomEvent('bookmarksChanged', {
bubbles: true,
composed: true,
detail: {
bookmarks: this.bookmarks,
},
}));
}
emitBookmarkButtonClicked() {
this.dispatchEvent(new CustomEvent('bookmarkButtonClicked', {
bubbles: true,
composed: true,
detail: {
editedBookmark: this.editedBookmark,
},
}));
}
bookmarkButtonClicked(pageID) {
if (this.getBookmark(pageID)) {
this.confirmDeletion(pageID);
} else {
this.createBookmark(pageID);
}
}
renderBookmarkButtons() {
const pages = this.bookreader.$('.BRpagecontainer').not('.BRemptypage').get();
pages.forEach((pageEl) => {
const existingButton = pageEl.querySelector('.bookmark-button');
if (existingButton) {
existingButton.remove();
}
const pageID = +pageEl.classList.value.match(/pagediv\d+/)[0].replace(/\D/g, '');
const pageBookmark = this.getBookmark(pageID);
const bookmarkState = pageBookmark ? 'filled' : 'hollow';
// eslint-disable-next-line
const pageData = this.bookreader.book.getPage(pageID);
const { isViewable } = pageData;
if (!isViewable) { return; }
const bookmarkButton = document.createElement('div');
['mousedown', 'mouseup'].forEach((event) => {
bookmarkButton.addEventListener(event, (e) => e.stopPropagation());
});
bookmarkButton.classList.add('bookmark-button', bookmarkState);
if (pageBookmark) {
bookmarkButton.classList.add(this.getBookmarkColor(pageBookmark.color));
}
const pageSide = (pageEl.getAttribute('data-side') === 'L' && this.bookreader.mode === this.bookreader.constMode2up)
? 'left' : 'right';
render(html`
<bookmark-button
=${() => this.bookmarkButtonClicked(pageID)}
state=${bookmarkState}
side=${pageSide}
></bookmark-button>`, bookmarkButton);
pageEl.appendChild(bookmarkButton);
});
}
/**
* Notes which bookmark is active
*
* @param {boolean} atSetup - denotes the first time this is fired
*/
markActiveBookmark(atSetup = false) {
const { mode, constMode2up, constModeThumb } = this.bookreader;
const currentIndex = this.bookreader.currentIndex();
if (mode === constModeThumb) {
// keep active bookmark the same
// no syncing until we can verify when a bookmark is "in view"
const requestedPageHasBookmark = this.bookmarks[currentIndex];
if (atSetup && requestedPageHasBookmark) {
this.activeBookmarkID = currentIndex;
}
return;
}
// In 2up, we prefer the right side of the page to bookmark
// so let's make sure we light that one up.
if (mode === constMode2up) {
const pagesInView = this.bookreader.displayedIndices;
const pagesHaveActiveBookmark = pagesInView.includes(+this.activeBookmarkID);
if (pagesHaveActiveBookmark) {
return;
}
}
// If a bookmark exists with the current index, set it as active
if (this.bookmarks[currentIndex]) {
this.activeBookmarkID = currentIndex;
return;
}
// No bookmark for this page
this.activeBookmarkID = '';
}
bookmarkEdited({ detail }) {
const closeEdit = detail.bookmark.id === this.editedBookmark.id;
this.editedBookmark = closeEdit ? {} : detail.bookmark;
}
/**
* Gets bookmark by pageindex
* @param {number} id
*/
getBookmark(id) {
return this.bookmarks[id];
}
getBookmarkColor(id) {
return this.bookmarkColors.find((m) => m.id === id)?.className;
}
/**
* Adds bookmark for current page
*/
addBookmark() {
let pageID = this.bookreader.currentIndex();
if (this.bookreader.mode === this.bookreader.constMode2up) {
const pagesInView = this.bookreader.displayedIndices;
// add bookmark to right hand page
pageID = pagesInView[pagesInView.length - 1];
}
this.createBookmark(pageID);
}
/**
* Creates bookmark for a given page
* @param {number} pageID
*/
createBookmark(pageID) {
const existingBookmark = this.getBookmark(pageID);
if (existingBookmark) {
this.bookmarkEdited({ detail: { bookmark: existingBookmark } });
this.emitBookmarkButtonClicked();
return;
}
this.editedBookmark = this.formatBookmark({ leafNum: pageID });
this.api.post(this.editedBookmark);
this.bookmarks[pageID] = this.editedBookmark;
this.activeBookmarkID = pageID;
this.disableAddBookmarkButton = true;
this.renderBookmarkButtons();
this.emitBookmarkButtonClicked();
}
bookmarkSelected({ detail }) {
const { leafNum } = detail.bookmark;
this.bookreader.jumpToPage(`${this.bookreader.book.getPageNum(`${leafNum}`.replace(/\D/g, ''))}`);
this.activeBookmarkID = leafNum;
}
saveBookmark({ detail }) {
const existingBookmark = this.bookmarks[detail.bookmark.id];
Object.assign(existingBookmark, detail.bookmark);
this.api.put(existingBookmark);
this.editedBookmark = {};
this.renderBookmarkButtons();
}
confirmDeletion(pageID) {
const existingBookmark = this.getBookmark(pageID);
if (existingBookmark.note) {
this.displayDeletionModal(pageID);
return;
}
this.deleteBookmark({ detail: { id: `${pageID}` } });
}
displayDeletionModal(pageID) {
const customModalContent = html`
<delete-modal-actions
.deleteAction=${() => this.deleteBookmark({ detail: { id: `${pageID}` } })}
.cancelAction=${() => this.modal.closeModal()}
.pageID=${pageID}
></delete-modal-actions>
`;
this.modal.showModal({
config: this.deleteModalConfig,
customModalContent,
});
}
deleteBookmark({ detail }) {
const { id } = detail;
const currBookmarks = this.bookmarks;
delete currBookmarks[id];
this.bookmarks = { ...currBookmarks };
this.api.delete(detail.id);
this.editedBookmark = {};
this.modal.closeModal();
this.renderBookmarkButtons();
}
/**
* Tells us if we should allow user to add bookmark via menu panel
* returns { Boolean }
*/
get shouldEnableAddBookmarkButton() {
const pageToCheck = this.bookreader.mode === this.bookreader.constMode2up
? this.bookreader.displayedIndices[this.bookreader.displayedIndices.length - 1]
: this.bookreader.currentIndex();
const pageHasBookmark = this.getBookmark(pageToCheck);
return !!pageHasBookmark;
}
get allowAddingBookmark() {
return this.bookreader.mode !== this.bookreader.constModeThumb;
}
get addBookmarkButton() {
return html`
<button
class="ia-button primary"
tabindex="-1"
?disabled=${this.shouldEnableAddBookmarkButton}
=${this.addBookmark}>
Add bookmark
</button>
`;
}
get bookmarksList() {
return html`
<ia-bookmarks-list
=${this.bookmarkEdited}
=${this.bookmarkSelected}
=${this.saveBookmark}
=${this.deleteBookmark}
.editedBookmark=${this.editedBookmark}
.bookmarks=${{ ...this.bookmarks }}
.activeBookmarkID=${this.activeBookmarkID}
.bookmarkColors=${this.bookmarkColors}
.defaultBookmarkColor=${this.defaultColor}>
</ia-bookmarks-list>
`;
}
get bookmarkHelperMessage() {
return html`<p>Please use 1up or 2up view modes to add bookmark.</p>`;
}
render() {
const bookmarks = html`
${this.bookmarksList}
${this.allowAddingBookmark ? this.addBookmarkButton : this.bookmarkHelperMessage}
`;
return html`
<section class="bookmarks">
${ this.displayMode === 'login'
? html`<bookmarks-login
=${() => this.loginOptions.loginClicked()}
.url=${this.loginOptions.loginUrl}></bookmarks-login>`
: bookmarks
}
</section>
`;
}
}
customElements.define('ia-bookmarks', IABookmarks);