/* 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 VerseBox = require('../ui_models/verse_box.js');
const { getPlatform } = require('../helpers/ezra_helper.js');
const swordModuleHelper = require('../helpers/sword_module_helper.js');
const wheelnavController = require('../controllers/wheelnav_controller.js');
const eventController = require('../controllers/event_controller.js');
const PlatformHelper = require('../../lib/platform_helper.js');
const Hammer = require('../../../lib/hammerjs/hammer.js');
/**
* This controller provides an API for the verse list as well as event handlers for clicks within the verse list.
* @module verseListController
* @category Controller
*/
module.exports.init = function init() {
eventController.subscribe('on-all-translations-removed', async () => { this.onAllTranslationsRemoved(); });
eventController.subscribe('on-verse-list-init', async (tabIndex) => { this.updateVerseListClasses(tabIndex); });
eventController.subscribe('on-bible-text-loaded', async (tabIndex) => {
this.applyTagGroupFilter(tags_controller.currentTagGroupId, tabIndex);
this.bindEventsAfterBibleTextLoaded(tabIndex);
let platformHelper = new PlatformHelper();
if (platformHelper.isCordova()) {
this.initSwipeEvents(tabIndex);
}
});
eventController.subscribeMultiple(['on-tag-group-filter-enabled', 'on-tag-group-members-changed'], async () => {
this.applyTagGroupFilter(tags_controller.currentTagGroupId);
});
eventController.subscribe('on-tag-group-filter-disabled', async () => {
this.applyTagGroupFilter(null);
});
eventController.subscribe('on-tag-group-selected', async(tagGroup) => {
if (tagGroup != null && tagGroup != undefined) {
this.applyTagGroupFilter(tagGroup.id);
}
});
};
module.exports.getCurrentVerseListFrame = function(tabIndex=undefined) {
var currentVerseListTabs = app_controller.getCurrentVerseListTabs(tabIndex);
var currentVerseListFrame = currentVerseListTabs.find('.verse-list-frame');
return currentVerseListFrame;
};
module.exports.getCurrentVerseList = function(tabIndex=undefined) {
var currentVerseListFrame = this.getCurrentVerseListFrame(tabIndex);
var verseList = currentVerseListFrame.find('.verse-list');
return verseList;
};
module.exports.getCurrentVerseListHeader = function(tabIndex=undefined) {
var currentVerseListFrame = this.getCurrentVerseListFrame(tabIndex);
var verseListHeader = currentVerseListFrame.find('.verse-list-header');
return verseListHeader;
};
module.exports.getCurrentSearchProgressBar = function(tabIndex=undefined) {
const showSearchResultsInPopup = app_controller.optionsMenu._showSearchResultsInPopupOption.isChecked;
var parentElement = null;
if (showSearchResultsInPopup) {
parentElement = $('#search-results-box');
} else {
parentElement = this.getCurrentVerseListFrame(tabIndex);
}
var searchProgressBar = parentElement.find('.search-progress-bar');
return searchProgressBar;
};
module.exports.getCurrentSearchCancelButtonContainer = function(tabIndex=undefined) {
const showSearchResultsInPopup = app_controller.optionsMenu._showSearchResultsInPopupOption.isChecked;
var parentElement = null;
if (showSearchResultsInPopup) {
parentElement = $('#search-results-box');
} else {
parentElement = this.getCurrentVerseListFrame(tabIndex);
}
var searchCancelButton = parentElement.find('.cancel-module-search-button-container');
return searchCancelButton;
};
module.exports.hideSearchProgressBar = function(tabIndex=undefined) {
var searchProgressBar = this.getCurrentSearchProgressBar(tabIndex);
searchProgressBar.hide();
var cancelSearchButtonContainer = this.getCurrentSearchCancelButtonContainer(tabIndex);
cancelSearchButtonContainer.hide();
};
module.exports.getCurrentVerseListLoadingIndicator = function(tabIndex=undefined) {
var currentVerseListFrame = this.getCurrentVerseListFrame(tabIndex);
var loadingIndicator = currentVerseListFrame.find('.verse-list-loading-indicator');
return loadingIndicator;
};
module.exports.showVerseListLoadingIndicator = function(tabIndex=undefined, message=undefined, withLoader=true) {
var loadingIndicator = this.getCurrentVerseListLoadingIndicator(tabIndex);
var loadingText = loadingIndicator.find('.verse-list-loading-indicator-text');
if (message === undefined) {
message = i18n.t("bible-browser.loading-bible-text");
}
loadingText.html(message);
if (withLoader) {
loadingIndicator.find('.loader').show();
} else {
loadingIndicator.find('.loader').hide();
}
loadingIndicator.show();
};
module.exports.hideVerseListLoadingIndicator = function(tabIndex=undefined) {
var loadingIndicator = this.getCurrentVerseListLoadingIndicator(tabIndex);
loadingIndicator.hide();
};
module.exports.getBibleBookStatsFromVerseList = function(tabIndex) {
var bibleBookStats = {};
var currentVerseList = this.getCurrentVerseList(tabIndex)[0];
var verseBoxList = currentVerseList.querySelectorAll('.verse-box');
for (var i = 0; i < verseBoxList.length; i++) {
var currentVerseBox = verseBoxList[i];
var bibleBookShortTitle = new VerseBox(currentVerseBox).getBibleBookShortTitle();
if (bibleBookStats[bibleBookShortTitle] === undefined) {
bibleBookStats[bibleBookShortTitle] = 1;
} else {
bibleBookStats[bibleBookShortTitle] += 1;
}
}
return bibleBookStats;
};
module.exports.getFirstVisibleVerseAnchor = function() {
let verseListFrame = this.getCurrentVerseListFrame();
let firstVisibleVerseAnchor = null;
if (verseListFrame != null && verseListFrame.length > 0) {
let verseListFrameRect = verseListFrame[0].getBoundingClientRect();
let currentNavigationPane = app_controller.navigation_pane.getCurrentNavigationPane()[0];
let currentNavigationPaneWidth = currentNavigationPane.offsetWidth;
// We need to a add a few pixels to the coordinates of the verseListFrame so that we actually hit an element within the verseListFrame
const VERSE_LIST_CHILD_ELEMENT_OFFSET = 15;
let firstElementOffsetX = verseListFrameRect.x + currentNavigationPaneWidth + VERSE_LIST_CHILD_ELEMENT_OFFSET;
let firstElementOffsetY = verseListFrameRect.y + VERSE_LIST_CHILD_ELEMENT_OFFSET;
let currentElement = document.elementFromPoint(firstElementOffsetX, firstElementOffsetY);
if (currentElement != null && currentElement.classList != null && currentElement.classList.contains('verse-list')) {
// If the current element is the verse-list then we try once more 10 pixels lower.
currentElement = document.elementFromPoint(firstElementOffsetX, firstElementOffsetY + 10);
}
if (currentElement == null) {
return null;
}
if (currentElement.classList != null &&
(currentElement.classList.contains('sword-section-title') ||
currentElement.classList.contains('tag-browser-verselist-book-header'))) {
// We are dealing with a section header element (either sword-section-title or tag-browser-verselist-book-header)
if (currentElement.previousElementSibling != null &&
currentElement.previousElementSibling.nodeName == 'A') {
currentElement = currentElement.previousElementSibling;
}
} else {
// We are dealing with an element inside a verse-box
const MAX_ELEMENT_NESTING = 7;
// Traverse up the DOM to find the verse-box
for (let i = 0; i < MAX_ELEMENT_NESTING; i++) {
if (currentElement == null) {
break;
}
if (currentElement.classList != null && currentElement.classList.contains('verse-box')) {
// We have gotten a verse-box ... now get the a.nav element inside it!
currentElement = currentElement.querySelector('a.nav');
// Leave the loop since we found the anchor!
break;
} else {
// Proceed with the next parentNode
currentElement = currentElement.parentNode;
}
}
}
if (currentElement != null && currentElement.nodeName == 'A') {
firstVisibleVerseAnchor = currentElement.name;
}
}
return firstVisibleVerseAnchor;
};
module.exports.resetVerseListView = function() {
var textType = app_controller.tab_controller.getTab().getTextType();
if (textType != 'xrefs' && textType != 'tagged_verses') {
var currentReferenceVerse = this.getCurrentVerseListFrame().find('.reference-verse');
currentReferenceVerse[0].innerHTML = "";
}
var currentVerseList = this.getCurrentVerseList()[0];
if (currentVerseList != undefined) {
currentVerseList.innerHTML = "";
}
let verseListFrame = this.getCurrentVerseListFrame();
let verseListHeader = verseListFrame.find('.verse-list-header');
verseListHeader.hide();
let tagDistributionMatrix = verseListFrame.find('tag-distribution-matrix')[0];
tagDistributionMatrix.reset();
app_controller.docxExport.disableExportButton();
};
module.exports.getVerseListBookNumber = function(bibleBookLongTitle, bookHeaders=undefined) {
var bibleBookNumber = -1;
if (bookHeaders === undefined) {
var currentVerseListFrame = this.getCurrentVerseListFrame();
bookHeaders = currentVerseListFrame.find('.tag-browser-verselist-book-header');
}
for (let i = 0; i < bookHeaders.length; i++) {
var currentBookHeader = $(bookHeaders[i]);
var currentBookHeaderText = currentBookHeader.text();
if (currentBookHeaderText.includes(bibleBookLongTitle)) {
bibleBookNumber = i + 1;
break;
}
}
return bibleBookNumber;
};
module.exports.bindEventsAfterBibleTextLoaded = function(tabIndex=undefined, preventDoubleBinding=false, verseList=undefined) {
if (verseList == undefined) {
verseList = this.getCurrentVerseList(tabIndex);
}
var tagBoxes = verseList.find('.tag-box');
var tags = verseList.find('.tag');
var xref_markers = verseList.find('.sword-xref-marker');
if (preventDoubleBinding) {
tagBoxes = tagBoxes.filter(":not('.tag-events-configured')");
tags = tags.filter(":not('.tag-events-configured')");
xref_markers = xref_markers.filter(":not('.events-configured')");
}
tagBoxes.bind('mousedown', () => { app_controller.verse_selection.clearVerseSelection(); }).addClass('tag-events-configured');
tags.bind('mousedown', async (event) => {
event.stopPropagation();
await this.handleReferenceClick(event);
}).addClass('tag-events-configured');
xref_markers.bind('mousedown', async (event) => {
event.stopPropagation();
await this.handleReferenceClick(event);
}).addClass('events-configured');
verseList.find('.verse-box').bind('mouseover', (e) => { onVerseBoxMouseOver(e); });
verseList.find('a.chapter-nav').bind('mousedown', async (event) => {
event.preventDefault();
event.stopPropagation();
if (event.target.closest('a').classList.contains('disabled')) {
return;
}
const chapter = parseInt(event.target.closest('a').getAttribute('chapter'));
await app_controller.navigation_pane.goToChapter(chapter);
});
verseList.find('a.chapter-nav-dialog').bind('mousedown', async (event) => {
event.preventDefault();
event.stopPropagation();
const currentTab = app_controller.tab_controller.getTab();
const currentBook = currentTab.getBook();
const currentChapter = currentTab.getChapter();
app_controller.book_selection_menu.openBookChapterList(currentBook, currentChapter);
});
if (getPlatform().isFullScreen()) {
wheelnavController.bindEvents();
}
};
module.exports.initSwipeEvents = async function(tabIndex) {
let verseList = this.getCurrentVerseList(tabIndex);
let currentTab = app_controller.tab_controller.getTab(tabIndex);
const currentTranslationId = currentTab.getBibleTranslationId();
const currentBook = currentTab.getBook();
const isInstantLoadingBook = await app_controller.translation_controller.isInstantLoadingBook(currentTranslationId, currentBook);
if (!isInstantLoadingBook) {
let hammerTime = new Hammer(verseList[0], {
recognizers: [
[Hammer.Swipe,{ direction: Hammer.DIRECTION_HORIZONTAL }],
]
});
hammerTime.on('swiperight', () => {
this.goToPreviousChapter();
});
hammerTime.on('swipeleft', () => {
this.goToNextChapter();
});
}
};
module.exports.getCurrentChapter = function() {
let verseList = this.getCurrentVerseList();
let firstVerseBox = new VerseBox(verseList[0].querySelector('.verse-box'));
return firstVerseBox.getChapter();
};
module.exports.goToPreviousChapter = async function() {
let currentChapter = this.getCurrentChapter();
if (currentChapter > 1) {
let previousChapter = currentChapter - 1;
await app_controller.navigation_pane.goToChapter(previousChapter);
}
};
module.exports.goToNextChapter = async function() {
let currentTab = app_controller.tab_controller.getTab();
const currentTranslationId = currentTab.getBibleTranslationId();
const currentBook = currentTab.getBook();
const maxChapters = await ipcNsi.getBookChapterCount(currentTranslationId, currentBook);
let currentChapter = this.getCurrentChapter();
if (currentChapter < maxChapters) {
let nextChapter = currentChapter + 1;
await app_controller.navigation_pane.goToChapter(nextChapter);
}
};
module.exports.updateVerseListClasses = async function(tabIndex=undefined) {
let currentTab = app_controller.tab_controller.getTab(tabIndex);
const currentTranslationId = currentTab.getBibleTranslationId();
const isInstantLoadingBook = await app_controller.translation_controller.isInstantLoadingBook(currentTranslationId, currentTab.getBook());
const moduleIsRightToLeft = await swordModuleHelper.moduleIsRTL(currentTranslationId);
let currentVerseList = this.getCurrentVerseList(tabIndex);
if (!isInstantLoadingBook && currentVerseList[0] != null) {
currentVerseList.addClass('verse-list-without-chapter-titles');
} else {
currentVerseList.removeClass('verse-list-without-chapter-titles');
}
if (moduleIsRightToLeft) {
currentVerseList.addClass('verse-list-rtl');
} else {
currentVerseList.removeClass('verse-list-rtl');
}
};
module.exports.bindXrefEvents = function(tabIndex=undefined) {
var verseList = this.getCurrentVerseList(tabIndex);
var xref_markers = verseList.find('.sword-xref-marker');
xref_markers.unbind();
xref_markers.bind('mousedown', async (event) => {
await this.handleReferenceClick(event);
}).addClass('events-configured');
};
function onVerseBoxMouseOver(event) {
var focussedElement = event.target;
app_controller.navigation_pane.updateNavigationFromVerseBox(focussedElement);
}
module.exports.handleReferenceClick = async function(event) {
var currentTab = app_controller.tab_controller.getTab();
var currentTextType = currentTab.getTextType();
var verseBox = $(event.target).closest('.verse-box');
var isReferenceVerse = verseBox.parent().hasClass('reference-verse');
var isXrefMarker = event.target.classList.contains('sword-xref-marker');
var isTag = event.target.classList.contains('tag');
if (isReferenceVerse &&
((currentTextType == 'xrefs') || (currentTextType == 'tagged_verses'))
) {
if (isXrefMarker) {
await app_controller.verse_list_popup.initCurrentXrefs(event.target);
app_controller.openXrefVerses(app_controller.verse_list_popup.currentReferenceVerseBox,
app_controller.verse_list_popup.currentPopupTitle,
app_controller.verse_list_popup.currentXrefs);
} else if (isTag) {
await app_controller.verse_list_popup.initCurrentTag(event.target);
app_controller.openTaggedVerses(app_controller.verse_list_popup.currentTagId,
app_controller.verse_list_popup.currentTagTitle,
app_controller.verse_list_popup.currentReferenceVerseBox);
}
} else {
if (isXrefMarker) {
let referenceType = "XREFS";
if (app_controller.optionsMenu._verseListNewTabOption.isChecked &&
!getPlatform().isFullScreen()) { // No tabs available in fullscreen!
app_controller.verse_list_popup.currentReferenceType = referenceType;
await app_controller.verse_list_popup.initCurrentXrefs(event.target);
app_controller.verse_list_popup.openVerseListInNewTab();
} else {
await app_controller.verse_list_popup.openVerseListPopup(event, referenceType);
}
} else if (isTag) {
let referenceType = "TAGGED_VERSES";
if (app_controller.optionsMenu._verseListNewTabOption.isChecked &&
!getPlatform().isFullScreen()) { // No tabs available in fullscreen!
app_controller.verse_list_popup.currentReferenceType = referenceType;
await app_controller.verse_list_popup.initCurrentTag(event.target);
app_controller.verse_list_popup.openVerseListInNewTab();
} else {
await app_controller.verse_list_popup.openVerseListPopup(event, referenceType);
}
}
}
};
// Re-init application to state without Bible translations
module.exports.onAllTranslationsRemoved = function() {
this.resetVerseListView();
this.hideVerseListLoadingIndicator();
this.getCurrentVerseList().append("<div class='help-text'>" + i18n.t("help.help-text-no-translations") + "</div>");
$('.book-select-value').text(i18n.t("menu.book"));
};
module.exports.applyTagGroupFilter = async function(tagGroupId, tabIndex=undefined, rootElement=undefined) {
let tagGroupFilterOption = app_controller.optionsMenu._tagGroupFilterOption;
let verseList = this.getCurrentVerseList(tabIndex)[0];
if (rootElement === undefined) {
rootElement = verseList;
}
let allTagElements = rootElement.querySelectorAll('.tag');
if (tagGroupId == null || tagGroupId < 0 || !tagGroupFilterOption.isChecked) {
// Show all tags
allTagElements.forEach((tagElement) => {
tagElement.classList.remove('hidden');
});
} else {
// Show tags filtered by current tag group
let tagGroupMemberIds = await tags_controller.tag_store.getTagGroupMemberIds(tagGroupId);
allTagElements.forEach((tagElement) => {
let currentTagId = parseInt(tagElement.getAttribute('tag-id'));
if (tagGroupMemberIds.includes(currentTagId)) {
tagElement.classList.remove('hidden');
} else {
tagElement.classList.add('hidden');
}
});
}
let verseBoxes = verseList.querySelectorAll('.verse-box');
// Update visibility of verse tag indicators
verseBoxes.forEach((verseBox) => {
let visibleTagCount = verseBox.querySelectorAll('.tag:not(.hidden)').length;
let tagIndicator = verseBox.querySelector('.tag-info');
if (visibleTagCount > 0) {
tagIndicator.classList.add('visible');
} else {
tagIndicator.classList.remove('visible');
}
});
};
Source