Source

frontend/components/book_selection_menu.js

/* This file is part of Ezra Bible App.

   Copyright (C) 2019 - 2023 Ezra Bible App Development Team <contact@ezrabibleapp.net>

   Ezra Bible App is free software: you can redistribute it and/or modify
   it under the terms of the GNU General Public License as published by
   the Free Software Foundation, either version 2 of the License, or
   (at your option) any later version.

   Ezra Bible App is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU General Public License for more details.

   You should have received a copy of the GNU General Public License
   along with Ezra Bible App. See the file LICENSE.
   If not, see <http://www.gnu.org/licenses/>. */

const eventController = require('../controllers/event_controller.js');
const i18nHelper = require('../helpers/i18n_helper.js');
const cacheController = require('../controllers/cache_controller.js');
const swordModuleHelper = require('../helpers/sword_module_helper.js');

/**
 * The BookSelectionMenu component implements all event handling for the book selection menu.
 * 
 * @category Component
 */
class BookSelectionMenu {
  constructor() {
    this.book_menu_is_opened = false;
    this.init_completed = false;
  }

  async init() {
    if (this.init_completed) return;

    const menuBookList = document.querySelector('#app-container #book-selection-menu-book-list');
    menuBookList.addEventListener('click', app_controller.handleBodyClick);

    var cachedHtml = await cacheController.getCachedItem('bookSelectionMenuCache');

    if (cachedHtml) {
      menuBookList.innerHTML = cachedHtml;
    } else {
      console.log("Localizing book selection menu ...");
      await this.localizeBookSelectionMenu();
    }

    document.getElementById('bookMenuBackButton').addEventListener('click', () => {
      setTimeout(() => { this.hideBookMenu(); }, 100);
    });

    this.initLinks();
    this.subscribeForEvents();
    this.init_completed = true;
  }

  initLinks() {
    var menu = $('#app-container').find('#book-selection-menu-book-list');
    var links = menu.find('a');

    for (var i = 0; i < links.length; i++) {
      var current_link = $(links[i]);

      current_link.click((event) => {
        event.preventDefault();
        event.stopPropagation();

        var current_link_href = $(event.target).attr('href');
        var current_book_title = $(event.target).html();
        var current_reference_book_title = $(event.target).attr('book-name');

        this.selectBibleBook(current_link_href, current_book_title, current_reference_book_title);
      });
    }
  }

  subscribeForEvents() {
    eventController.subscribe('on-translation-changed', async () => {
      await this.updateAvailableBooks();
    });

    eventController.subscribe('on-translation-added', async (moduleCode) => {
      await this.updateAvailableBooks(undefined, moduleCode);
    });

    eventController.subscribe('on-tab-selected', async (tabIndex) => {
      var metaTab = app_controller.tab_controller.getTab(tabIndex);

      if (metaTab != null && metaTab.selectCount >= 2) {
        // Only perform the following action from the 2nd select (The first is done when the tab is created)
        this.clearSelectedBookInMenu();
      }

      await this.updateAvailableBooks(tabIndex);

      // Highlight currently selected book (only in book mode)
      if (metaTab != null) {
        const textType = metaTab.getTextType();
        if (textType == 'book') this.highlightCurrentlySelectedBookInMenu(tabIndex);
      }

    });

    eventController.subscribe('on-tab-added', async () => {
      this.clearSelectedBookInMenu();
    });

    eventController.subscribe('on-locale-changed', async () => {
      this.localizeBookSelectionMenu();
    });
  }

  // This function is rather slow and it delays app startup! (~175ms)
  async localizeBookSelectionMenu() {
    var aElements = document.querySelectorAll("#book-selection-menu-book-list a");

    for (var i = 0; i < aElements.length; i++) {
      var currentBook = aElements[i];
      var currentBookName = currentBook.getAttribute('book-name');

      if (currentBookName != null) {
        var currentBookTranslation = await i18nHelper.getSwordTranslation(currentBookName);
        currentBook.textContent = currentBookTranslation;
      }
    }

    var html = document.getElementById("book-selection-menu-book-list").innerHTML;
    cacheController.setCachedItem('bookSelectionMenuCache', html);
  }

  async updateAvailableBooks(tabIndex=undefined, moduleCode=undefined) {
    var currentTab = app_controller.tab_controller.getTab(tabIndex);

    if (currentTab != null) {
      var currentBibleTranslationId = currentTab.getBibleTranslationId();

      if (moduleCode !== undefined) {
        currentBibleTranslationId = moduleCode;
      }

      var moduleHasApocryphalBooks = await swordModuleHelper.moduleHasApocryphalBooks(currentBibleTranslationId);

      if (currentBibleTranslationId != null) {
        const books = await ipcNsi.getBookList(currentBibleTranslationId);
        let bookLinks = document.getElementById('book-selection-menu-book-list').querySelectorAll('li');

        for (let i = 0; i < bookLinks.length; i++) {
          let currentBookLink = bookLinks[i];

          if (currentBookLink.getAttribute('class') != null) {
            let currentBook = currentBookLink.getAttribute('class').split(' ')[0];
            let currentBookId = currentBook.split('-')[1];

            if (books.includes(currentBookId)) {
              currentBookLink.classList.remove('book-unavailable');
              currentBookLink.classList.add('book-available');
            } else {
              currentBookLink.classList.add('book-unavailable');
              currentBookLink.classList.remove('book-available');
            }
          }
        }
      }

      let apocryphalBookList = document.getElementsByClassName('apocryphal-books')[0];

      if (apocryphalBookList != null) {
        if (moduleHasApocryphalBooks) {
          apocryphalBookList.style.display = '';
        } else {
          apocryphalBookList.style.display = 'none';
        }
      }
    }
  }

  async selectBibleBook(bookCode, bookTitle, referenceBookTitle, currentChapter=null) {
    this.currentBibleTranslationId = app_controller.tab_controller.getTab().getBibleTranslationId();
    if (this.currentBibleTranslationId == null || this.currentBibleTranslationId == undefined) {
      return;
    }

    Sentry.addBreadcrumb({category: "app",
                          message: `Selected book ${bookCode} using translation ${this.currentBibleTranslationId}`,
                          level: "info"});
    
    var books = await ipcNsi.getBookList(this.currentBibleTranslationId);
    if (!books.includes(bookCode)) {
      return;
    }

    const selectChapterBeforeLoading = app_controller.optionsMenu._selectChapterBeforeLoadingOption;
    const bookChapterCount = await ipcNsi.getBookChapterCount(this.currentBibleTranslationId, bookCode);

    if ((selectChapterBeforeLoading.isChecked || currentChapter != null) && bookChapterCount > 1) {
      //console.log(`Showing chapter list for ${bookTitle} ` +
      //            `since its chapter count (${bookChapterCount}) is above the limit for instant loading!`);
      
      const bookMenu = document.getElementById('book-selection-menu');
      bookMenu.classList.add('select-chapter');

      this.currentBookCode = bookCode;
      this.currentBookTitle = bookTitle;
      this.currentReferenceBookTitle = referenceBookTitle;

      await this.loadChapterList(bookChapterCount, currentChapter);

    } else { // Load directly without first showing chapter list

      const instantLoad = await app_controller.translation_controller.isInstantLoadingBook(this.currentBibleTranslationId, bookCode);

      app_controller.text_controller.loadBook(bookCode,
                                              bookTitle,
                                              referenceBookTitle,
                                              instantLoad,
                                              1);
    }
  }

  async loadChapterList(bookChapterCount, currentChapter=null) {
    var menuChapterList = document.getElementById('book-selection-menu-chapter-list');
    menuChapterList.innerHTML = `
      <h2>${this.currentBookTitle}</h2>
      <div id='chapter-list-chapters'></div>
    `;

    var chapters = menuChapterList.querySelector('#chapter-list-chapters');

    for (let c = 1; c <= bookChapterCount; c++) {
      let newLink = document.createElement('a');
      newLink.href = c;
      newLink.innerText = c;

      if (currentChapter != null && c == currentChapter) {
        newLink.setAttribute('class', 'current-chapter');
      }
      
      chapters.appendChild(newLink);

      newLink.addEventListener('click', async (event) => {
        event.preventDefault();
        event.stopPropagation();

        const selectedChapter = parseInt(event.target.getAttribute('href'));
        const instantLoad = await app_controller.translation_controller.isInstantLoadingBook(this.currentBibleTranslationId, this.currentBookCode);

        app_controller.text_controller.loadBook(this.currentBookCode,
                                                this.currentBookTitle,
                                                this.currentReferenceBookTitle,
                                                instantLoad,
                                                selectedChapter);
      });
    }
  }

  hideBookMenu() {
    if (this.book_menu_is_opened) {
      document.getElementById('app-container').classList.remove('fullscreen-menu');

      var bookMenu = document.querySelector('#app-container #book-selection-menu');
      bookMenu.style.display = 'none';
      bookMenu.classList.remove('select-chapter');
      this.book_menu_is_opened = false;

      if (app_controller.getCurrentVerseListMenu() != null) {
        var currentVerseListMenu = app_controller.getCurrentVerseListMenu()[0];
        var bookButton = currentVerseListMenu.querySelector('.book-select-button');
        bookButton.classList.remove('ui-state-active');
      }
    }
  }

  handleBookMenuClick(event) {
    if ($('.book-select-button').hasClass('ui-state-disabled')) {
      return;
    }

    if (this.book_menu_is_opened) {
      app_controller.handleBodyClick();
    } else {
      app_controller.hideAllMenus();
      
      var currentVerseListMenu = app_controller.getCurrentVerseListMenu();
      var book_button = currentVerseListMenu.find('.book-select-button');
      var menu = $('#app-container').find('#book-selection-menu');

      document.getElementById('app-container').classList.add('fullscreen-menu');

      uiHelper.showButtonMenu(book_button, menu);

      this.book_menu_is_opened = true;

      if (event != null) {
        event.stopPropagation();
      }
    }
  }

  async openBookChapterList(bookCode, currentChapter) {
    let bookLongTitle = await ipcDb.getBookLongTitle(bookCode);
    let bookTitleTranslation = await ipcDb.getBookTitleTranslation(bookCode);

    this.handleBookMenuClick();
    await this.selectBibleBook(bookCode, bookTitleTranslation, bookLongTitle, currentChapter);
  }

  highlightCurrentlySelectedBookInMenu(tabIndex=undefined) {
    var currentTab = app_controller.tab_controller.getTab(tabIndex);
    
    if (currentTab != null) {
      var bookCode = currentTab.getBook();
      if (bookCode != null) {
        this.highlightSelectedBookInMenu(bookCode);
      }
    }
  }

  clearSelectedBookInMenu() {
    // Remove highlighting for previously selected book
    var selectedBook = document.getElementsByClassName('book-selected');
    if (selectedBook.length > 0) {
      selectedBook[0].classList.remove('book-selected');
    }
  }

  highlightSelectedBookInMenu(bookCode) {
    this.clearSelectedBookInMenu();
    
    // Highlight the newly selected book
    var bookId = '.book-' + bookCode;
    document.getElementById('book-selection-menu-book-list').querySelector(bookId).classList.add('book-selected');
  }
}

module.exports = BookSelectionMenu;