1380 lines
40 KiB
JavaScript
1380 lines
40 KiB
JavaScript
/**
|
|
* getBible Loader v3.0.2
|
|
* https://getbible.net
|
|
* (c) 2014 - 2023 Llewellyn van der Merwe
|
|
* MIT License
|
|
**/
|
|
|
|
(function (factory) {
|
|
typeof define === 'function' && define.amd ? define(factory) :
|
|
factory();
|
|
})((function () { 'use strict';
|
|
|
|
/**
|
|
* Class for managing local storage of scripture data.
|
|
*/
|
|
class Memory {
|
|
// Constant representing one month in milliseconds.
|
|
static ONE_MONTH_IN_MILLISECONDS = 30 * 24 * 60 * 60 * 1000;
|
|
|
|
/**
|
|
* Stores scripture data in local storage.
|
|
*
|
|
* @param {string} reference - The scripture reference.
|
|
* @param {string} translation - The translation.
|
|
* @param {Object} data - The scripture data to be stored.
|
|
* @throws {Error} If storing data fails.
|
|
*/
|
|
static set(reference, translation, data) {
|
|
const key = this.#key(reference, translation);
|
|
const item = {
|
|
data,
|
|
timestamp: Date.now(),
|
|
};
|
|
try {
|
|
localStorage.setItem(key, JSON.stringify(item));
|
|
} catch (error) {
|
|
console.error('Error storing data in local storage:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Retrieves scripture data from local storage.
|
|
*
|
|
* @param {string} reference - The scripture reference.
|
|
* @param {string} translation - The translation.
|
|
* @returns {Promise<Object|null>} The scripture data or null if not found.
|
|
*/
|
|
static async get(reference, translation) {
|
|
return this.#get(reference, translation);
|
|
}
|
|
|
|
/**
|
|
* Internal method to check local storage for scripture data.
|
|
*
|
|
* @param {string} reference - The scripture reference.
|
|
* @param {string} translation - The translation.
|
|
* @returns {Object|null} The stored data or null if not found.
|
|
* @throws {Error} If parsing or retrieval from local storage fails.
|
|
* @private
|
|
*/
|
|
static #get(reference, translation) {
|
|
const key = this.#key(reference, translation);
|
|
try {
|
|
const storedItem = localStorage.getItem(key);
|
|
if (storedItem) {
|
|
const {data, timestamp} = JSON.parse(storedItem);
|
|
if (timestamp > Date.now() - Memory.ONE_MONTH_IN_MILLISECONDS) {
|
|
return data;
|
|
}
|
|
}
|
|
return null;
|
|
} catch (error) {
|
|
console.error('Error parsing or retrieving data from local storage:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generates a key for scripture data storage.
|
|
*
|
|
* @param {string} reference - The scripture reference.
|
|
* @param {string} translation - The translation.
|
|
* @returns {string} A unique key for local storage.
|
|
* @private
|
|
*/
|
|
static #key(reference, translation) {
|
|
return `getBible-${translation}-${reference}`;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Class for handling API calls to fetch scripture data.
|
|
*/
|
|
class Api {
|
|
/**
|
|
* Constructs an Api instance with a default or specified API endpoint.
|
|
*
|
|
* @param {string} apiEndpoint - The endpoint URL for the API.
|
|
*/
|
|
constructor(apiEndpoint = 'https://query.getbible.net/v2/') {
|
|
this.apiEndpoint = apiEndpoint;
|
|
}
|
|
|
|
/**
|
|
* Fetches scripture from the API, using local storage to cache responses.
|
|
*
|
|
* @param {string} reference - The scripture reference.
|
|
* @param {string} translation - The translation.
|
|
* @returns {Promise<Object>} A promise that resolves with the scripture data.
|
|
* @throws {Error} If the API request fails.
|
|
*/
|
|
async get(reference, translation) {
|
|
try {
|
|
const localStorageData = await Memory.get(reference, translation);
|
|
if (localStorageData !== null) {
|
|
return localStorageData;
|
|
}
|
|
|
|
const response = await fetch(this.#url(reference, translation));
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`${response.status} - ${response.statusText || 'Failed to fetch scripture'}`);
|
|
}
|
|
|
|
const data = await response.json();
|
|
await Memory.set(reference, translation, data);
|
|
return data;
|
|
} catch (error) {
|
|
console.error('Error fetching data:', error);
|
|
throw new Error(error.message || 'Error fetching scripture');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Constructs the URL for the API call.
|
|
*
|
|
* @param {string} reference - The scripture reference.
|
|
* @param {string} translation - The translation.
|
|
* @returns {string} The constructed URL for the API request.
|
|
* @private
|
|
*/
|
|
#url(reference, translation) {
|
|
return `${this.apiEndpoint}${encodeURIComponent(translation)}/${encodeURIComponent(reference)}`;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Class for handling chapter data.
|
|
*/
|
|
class Reference {
|
|
#data; // Private data member
|
|
|
|
/**
|
|
* Initializes the BibleVerse object with verse data.
|
|
*
|
|
* @param {Object} data - The JSON data containing verse information.
|
|
*/
|
|
constructor(data) {
|
|
this.#data = data;
|
|
}
|
|
|
|
/**
|
|
* Retrieves the translation name.
|
|
*
|
|
* @returns {string} The name of the translation.
|
|
*/
|
|
getTranslation() {
|
|
return this.#data.translation;
|
|
}
|
|
|
|
/**
|
|
* Retrieves the abbreviation of the translation.
|
|
*
|
|
* @returns {string} The abbreviation of the translation.
|
|
*/
|
|
getAbbreviation() {
|
|
return this.#data.abbreviation;
|
|
}
|
|
|
|
/**
|
|
* Retrieves the language code.
|
|
*
|
|
* @returns {string} The language code.
|
|
*/
|
|
getLanguage() {
|
|
return this.#data.lang;
|
|
}
|
|
|
|
/**
|
|
* Retrieves the full language name.
|
|
*
|
|
* @returns {string} The full name of the language.
|
|
*/
|
|
getLanguageName() {
|
|
return this.#data.language;
|
|
}
|
|
|
|
/**
|
|
* Retrieves the text direction.
|
|
*
|
|
* @returns {string} The direction of the text (LTR or RTL).
|
|
*/
|
|
getTextDirection() {
|
|
return this.#data.direction;
|
|
}
|
|
|
|
/**
|
|
* Retrieves the encoding format.
|
|
*
|
|
* @returns {string} The encoding format (e.g., UTF-8).
|
|
*/
|
|
getEncoding() {
|
|
return this.#data.encoding;
|
|
}
|
|
|
|
/**
|
|
* Retrieves the book number.
|
|
*
|
|
* @returns {number} The book number.
|
|
*/
|
|
getBookNumber() {
|
|
return this.#data.book_nr;
|
|
}
|
|
|
|
/**
|
|
* Retrieves the name of the book.
|
|
*
|
|
* @returns {string} The name of the book.
|
|
*/
|
|
getBookName() {
|
|
return this.#data.book_name;
|
|
}
|
|
|
|
/**
|
|
* Retrieves the chapter number.
|
|
*
|
|
* @returns {number} The chapter number.
|
|
*/
|
|
getChapter() {
|
|
return this.#data.chapter;
|
|
}
|
|
|
|
/**
|
|
* Retrieves the name of the chapter.
|
|
*
|
|
* @returns {string} The name of the chapter.
|
|
*/
|
|
getChapterName() {
|
|
return this.#data.name;
|
|
}
|
|
|
|
/**
|
|
* Retrieves all verses of the chapter.
|
|
*
|
|
* @returns {Array<Object>} An array of objects representing each verse.
|
|
*/
|
|
getVerses() {
|
|
return this.#data.verses;
|
|
}
|
|
|
|
/**
|
|
* Retrieves a specific verse by its number.
|
|
*
|
|
* @param {number} verseNumber - The number of the verse to retrieve.
|
|
* @returns {Object|null} The verse object if found, or null if not.
|
|
*/
|
|
getVerse(verseNumber) {
|
|
return this.#data.verses.find(verse => verse.verse === verseNumber);
|
|
}
|
|
|
|
/**
|
|
* Retrieves a range of verses.
|
|
*
|
|
* @param {number} startVerse - The starting verse number.
|
|
* @param {number} endVerse - The ending verse number.
|
|
* @returns {Array<Object>} An array of verse objects within the range.
|
|
*/
|
|
getVersesInRange(startVerse, endVerse) {
|
|
return this.#data.verses.filter(verse => verse.verse >= startVerse && verse.verse <= endVerse);
|
|
}
|
|
|
|
/**
|
|
* Generates a reference string for the verses.
|
|
*
|
|
* @returns {string} The reference string.
|
|
*/
|
|
getReference() {
|
|
const verseNumbers = this.#data.verses.map(verse => verse.verse).sort((a, b) => a - b);
|
|
let refString = `${this.#data.name}:`;
|
|
let ranges = {};
|
|
let rangeStart = null;
|
|
let rangeEnd = null;
|
|
let previousVerse = null;
|
|
|
|
verseNumbers.forEach(verse => {
|
|
if (rangeStart === null) {
|
|
rangeStart = verse;
|
|
} else if (verse === previousVerse + 1) {
|
|
rangeEnd = verse;
|
|
} else {
|
|
ranges[rangeStart] = (rangeEnd !== null) ? `${rangeStart}-${rangeEnd}` : `${rangeStart}`;
|
|
rangeStart = verse;
|
|
rangeEnd = null;
|
|
}
|
|
previousVerse = verse;
|
|
});
|
|
|
|
// Handling the case for the last verse or a single-verse range
|
|
if (rangeStart !== null) {
|
|
ranges[rangeStart] = (rangeEnd !== null) ? `${rangeStart}-${rangeEnd}` : `${rangeStart}`;
|
|
}
|
|
|
|
// Join the range strings with commas
|
|
return refString + Object.values(ranges).join(',');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Class for handling Scripture.
|
|
*/
|
|
class Scripture {
|
|
#references; // Private array for storing references
|
|
|
|
/**
|
|
* Initializes the Bible translations, books, and chapters.
|
|
*
|
|
* @param {Object} data - An object with references data keyed by identifiers.
|
|
*/
|
|
constructor(data) {
|
|
this.#references = Object.values(data).map(reference => new Reference(reference));
|
|
}
|
|
|
|
/**
|
|
* Gets a reference by its numerical index.
|
|
*
|
|
* @param {number} index - The index of the reference.
|
|
* @returns {Reference|null} The Reference instance or null if out of bounds.
|
|
*/
|
|
getReference(index) {
|
|
return (index >= 0 && index < this.#references.length) ? this.#references[index] : null;
|
|
}
|
|
|
|
/**
|
|
* Gets the translation.
|
|
*
|
|
* @param {number} index - The index of the reference.
|
|
* @returns {Reference|null} The Reference instance or null if out of bounds.
|
|
*/
|
|
getReference(index) {
|
|
return (index >= 0 && index < this.#references.length) ? this.#references[index] : null;
|
|
}
|
|
|
|
/**
|
|
* Iterates over all references and performs a callback function.
|
|
*
|
|
* @param {Function} callback - The callback function to execute for each chapter.
|
|
*/
|
|
forEachReference(callback) {
|
|
this.#references.forEach(callback);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Class for managing actions based on element data attributes.
|
|
*/
|
|
class Action {
|
|
#element;
|
|
#format;
|
|
#translations;
|
|
#showBookName;
|
|
#showReference;
|
|
#showTranslation;
|
|
#showAbbreviation;
|
|
#showLanguage;
|
|
|
|
/**
|
|
* Initializes the Actions object with a DOM element and its data attributes.
|
|
*
|
|
* @param {HTMLElement} element - The DOM element containing data attributes.
|
|
*/
|
|
constructor(element) {
|
|
|
|
if (!(element instanceof HTMLElement)) {
|
|
throw new Error("triggerElement must be an instance of HTMLElement.");
|
|
}
|
|
|
|
this.#element = element;
|
|
this.#format = (element.dataset.format || 'inline').toLowerCase();
|
|
this.#translations = (element.dataset.translation || 'kjv').toLowerCase().split(';').map(translation => translation.trim());
|
|
this.#showBookName = element.dataset.showBookName ? parseInt(element.dataset.showBookName, 10) : 0;
|
|
this.#showReference = element.dataset.showReference ? parseInt(element.dataset.showReference, 10) : 1;
|
|
this.#showTranslation = element.dataset.showTranslation ? parseInt(element.dataset.showTranslation, 10) : 0;
|
|
this.#showAbbreviation = element.dataset.showAbbreviation ? parseInt(element.dataset.showAbbreviation, 10) : 0;
|
|
this.#showLanguage = element.dataset.showLanguage ? parseInt(element.dataset.showLanguage, 10) : 0;
|
|
}
|
|
|
|
/**
|
|
* Retrieves the translations.
|
|
*
|
|
* @returns {Array<string>} An array of translation strings.
|
|
*/
|
|
getTranslations() {
|
|
return this.#translations;
|
|
}
|
|
|
|
/**
|
|
* Retrieves the show book name flag.
|
|
*
|
|
* @returns {number} The show book name flag (0 or 1).
|
|
*/
|
|
showBookName() {
|
|
return this.#showBookName;
|
|
}
|
|
|
|
/**
|
|
* Retrieves the show reference flag.
|
|
*
|
|
* @returns {number} The show reference flag (0 or 1).
|
|
*/
|
|
showReference() {
|
|
return this.#showReference;
|
|
}
|
|
|
|
/**
|
|
* Retrieves the show translation flag.
|
|
*
|
|
* @returns {number} The show translation flag (0 or 1).
|
|
*/
|
|
showTranslation() {
|
|
return this.#showTranslation;
|
|
}
|
|
|
|
/**
|
|
* Retrieves the show abbreviation flag.
|
|
*
|
|
* @returns {number} The show abbreviation flag (0 or 1).
|
|
*/
|
|
showAbbreviation() {
|
|
return this.#showAbbreviation;
|
|
}
|
|
|
|
/**
|
|
* Retrieves the show language flag.
|
|
*
|
|
* @returns {number} The show language flag (0 or 1).
|
|
*/
|
|
showLanguage() {
|
|
return this.#showLanguage;
|
|
}
|
|
|
|
/**
|
|
* Retrieves the element format.
|
|
*
|
|
* @returns {string} The element format.
|
|
*/
|
|
getFormat() {
|
|
return this.#format;
|
|
}
|
|
|
|
/**
|
|
* Retrieves the DOM element.
|
|
*
|
|
* @returns {HTMLElement} The DOM element associated with this object.
|
|
*/
|
|
getElement() {
|
|
return this.#element;
|
|
}
|
|
}
|
|
|
|
class BaseFormat {
|
|
#action;
|
|
|
|
/**
|
|
* Creates a new BaseTooltip instance.
|
|
*
|
|
* @param {Action} action - The action elements that triggers the tooltip.
|
|
*/
|
|
constructor(action) {
|
|
this.#action = action;
|
|
}
|
|
|
|
/**
|
|
* Get action.
|
|
*
|
|
* @returns {Action} The current actions.
|
|
*/
|
|
action() {
|
|
return this.#action;
|
|
}
|
|
|
|
/**
|
|
* Get formats the verses.
|
|
*
|
|
* @param {Scripture} scripture - The data containing verses and their details.
|
|
* @returns {string} The formatted verses.
|
|
* @abstract
|
|
*/
|
|
get(scripture) {
|
|
throw new Error("The 'get' method must be implemented in BaseFormat subclass.");
|
|
}
|
|
}
|
|
|
|
class BlockFormat extends BaseFormat {
|
|
constructor(action) {
|
|
super(action);
|
|
}
|
|
|
|
/**
|
|
* Formats the verses for HTML block elements.
|
|
*
|
|
* @param {Scripture} scripture - The data containing verses and their details.
|
|
* @returns {string} The formatted verses.
|
|
*/
|
|
get(scripture) {
|
|
let display = [];
|
|
scripture.forEachReference((reference) => {
|
|
let header = [];
|
|
display.push(`<div dir="${reference.getTextDirection().toUpperCase()}" class="getbible-reference-block">`);
|
|
if (this.action().showBookName()) {
|
|
header.push(`<span class="getbible-book-name">${reference.getBookName()}</span>`);
|
|
}
|
|
if (this.action().showReference()) {
|
|
header.push(`<span class="getbible-reference">${reference.getReference()}</span>`);
|
|
}
|
|
if (this.action().showTranslation()) {
|
|
header.push(`<span class="getbible-translation">${reference.getTranslation()}</span>`);
|
|
}
|
|
if (this.action().showAbbreviation()) {
|
|
header.push(`<span class="getbible-abbreviation">${reference.getAbbreviation()}</span>`);
|
|
}
|
|
if (this.action().showLanguage()) {
|
|
header.push(`<span class="getbible-language">${reference.getLanguage()}</span>`);
|
|
}
|
|
// Construct the header
|
|
if (header.length > 0) {
|
|
display.push(`<b class="getbible-header">${header.join(' - ')}</b>`);
|
|
}
|
|
const verses = reference.getVerses()
|
|
.map(verse => `<div class="getbible-verse">${verse.verse}. ${verse.text}</div>`)
|
|
.join("\n");
|
|
display.push(`<div class="getbible-verses">${verses}</div>`);
|
|
display.push(`</div>`);
|
|
});
|
|
|
|
return `<div class="getbible-element getbible-block">${display.join("\n")}</div><br />`;
|
|
}
|
|
}
|
|
|
|
class InlineFormat extends BaseFormat {
|
|
constructor(action) {
|
|
super(action);
|
|
}
|
|
|
|
/**
|
|
* Formats the verses for HTML inline elements.
|
|
*
|
|
* @param {Scripture} scripture - The data containing verses and their details.
|
|
* @returns {string} The formatted verses.
|
|
*/
|
|
get(scripture) {
|
|
let display = [];
|
|
scripture.forEachReference((reference) => {
|
|
let footer = [];
|
|
display.push(`<div dir="${reference.getTextDirection().toUpperCase()}" class="getbible-reference-inline">`);
|
|
if (this.action().showBookName()) {
|
|
footer.push(`<span class="getbible-book-name">${reference.getBookName()}</span>`);
|
|
}
|
|
if (this.action().showReference()) {
|
|
footer.push(`<span class="getbible-reference">${reference.getReference()}</span>`);
|
|
}
|
|
if (this.action().showTranslation()) {
|
|
footer.push(`<span class="getbible-translation">${reference.getTranslation()}</span>`);
|
|
}
|
|
if (this.action().showAbbreviation()) {
|
|
footer.push(`<span class="getbible-abbreviation">${reference.getAbbreviation()}</span>`);
|
|
}
|
|
if (this.action().showLanguage()) {
|
|
footer.push(`<span class="getbible-language">${reference.getLanguage()}</span>`);
|
|
}
|
|
const verses = reference.getVerses()
|
|
.map(verse => `<span class="getbible-verse">${verse.verse}. ${verse.text}</span>`)
|
|
.join("\n");
|
|
display.push(`<span class="getbible-verses">${verses}</span>`);
|
|
// Construct the footer
|
|
if (footer.length > 0) {
|
|
display.push(`<b class="getbible-footer">${footer.join(' - ')}</b>`);
|
|
}
|
|
display.push(`</div>`);
|
|
});
|
|
|
|
return `<div class="getbible-element getbible-inline">${display.join("\n")}</div>`;
|
|
}
|
|
}
|
|
|
|
class PlainFormat extends BaseFormat {
|
|
constructor(action) {
|
|
super(action);
|
|
}
|
|
|
|
/**
|
|
* Formats the verses for plain text display.
|
|
*
|
|
* @param {Scripture} scripture - The data containing verses and their details.
|
|
* @returns {string} The formatted verses.
|
|
*/
|
|
get(scripture) {
|
|
let display = [];
|
|
scripture.forEachReference((reference) => {
|
|
let header = [];
|
|
if (this.action().showBookName()) {
|
|
header.push(`${reference.getBookName()}`);
|
|
}
|
|
if (this.action().showReference()) {
|
|
header.push(`${reference.getReference()}`);
|
|
}
|
|
if (this.action().showTranslation()) {
|
|
header.push(`${reference.getTranslation()}`);
|
|
}
|
|
if (this.action().showAbbreviation()) {
|
|
header.push(`${reference.getAbbreviation()}`);
|
|
}
|
|
if (this.action().showLanguage()) {
|
|
header.push(`${reference.getLanguage()}`);
|
|
}
|
|
// Construct the header
|
|
if (header.length > 0) {
|
|
display.push(`[${header.join(' - ')}]`);
|
|
}
|
|
display.push(
|
|
reference.getVerses()
|
|
.map(verse => `${verse.verse}. ${verse.text}`)
|
|
.join("\n")
|
|
);
|
|
});
|
|
|
|
return `${display.join("\n")}\n`;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Format class responsible for creating and managing different types of formats
|
|
* based on the specified type.
|
|
*/
|
|
class Format {
|
|
/**
|
|
* Constructs a Format instance based on the given type.
|
|
*
|
|
* @param {Action} action - The action details for this element
|
|
*/
|
|
constructor(action) {
|
|
const formatTypes = {
|
|
'modal': BlockFormat,
|
|
'inline': InlineFormat,
|
|
'tooltip': PlainFormat
|
|
};
|
|
|
|
const format = action.getFormat();
|
|
const FormatType = formatTypes[format] || InlineFormat;
|
|
this.format = new FormatType(action);
|
|
}
|
|
|
|
/**
|
|
* Get the formatted verses.
|
|
*
|
|
* @param {Scripture} scripture - The data containing verses and their details.
|
|
* @returns {string} The formatted verses.
|
|
*/
|
|
get(scripture) {
|
|
return this.format.get(scripture);
|
|
}
|
|
}
|
|
|
|
class BaseModal {
|
|
#modalId;
|
|
#action;
|
|
|
|
/**
|
|
* Creates a new BaseModal instance.
|
|
*
|
|
* @param {Action} action - The action element triggering the modal.
|
|
*/
|
|
constructor(action) {
|
|
this.#modalId = `modal-${Math.random().toString(36).slice(2, 11)}`;
|
|
this.#action = action;
|
|
this.getElement().style.cursor = 'pointer';
|
|
this.initializeTrigger();
|
|
}
|
|
|
|
/**
|
|
* Loads content into the modal.
|
|
*
|
|
* @param {string} content - The content to load into the modal.
|
|
*/
|
|
load(content) {
|
|
const existingModal = document.getElementById(this.getModalId());
|
|
// Check if modal already exists
|
|
if (existingModal) {
|
|
// Update the content of the existing modal
|
|
const contentDiv = document.getElementById(`${this.getModalId()}-content`);
|
|
if (contentDiv) {
|
|
contentDiv.innerHTML += content;
|
|
}
|
|
} else {
|
|
// If modal doesn't exist, create it with the new content
|
|
this.create(content);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Insert HTML into the dom.
|
|
*
|
|
* @param {string} html - The html to insert.
|
|
*/
|
|
insertIntoDOM(html) {
|
|
document.body.insertAdjacentHTML('beforeend', html);
|
|
}
|
|
|
|
/**
|
|
* Creates the modal.
|
|
*
|
|
* @param {string} content - The initial content of the modal.
|
|
*/
|
|
create(content) {
|
|
const modalHtml = `
|
|
<div id="${this.getModalId()}" style="display:none; position:fixed; top:0; left:0; width:100%; height:100%; background-color:rgba(0, 0, 0, 0.5); justify-content:center; align-items:center;">
|
|
<div style="position:relative; background-color:white; padding:20px; border-radius:5px; max-width:300px;">
|
|
<button class="getbible-modal-close" type="button" onclick="document.getElementById('${this.getModalId()}').style.display='none'" style="position:absolute; top:7px; right:7px; border:none; background:transparent; font-size:20px; cursor:pointer;">✖</button>
|
|
<div id="${this.getModalId()}-content">
|
|
${content}
|
|
</div>
|
|
</div>
|
|
</div>`;
|
|
this.insertIntoDOM(modalHtml);
|
|
|
|
const modalElement = document.getElementById(this.getModalId());
|
|
modalElement.addEventListener('click', (event) => {
|
|
if (event.target === modalElement) {
|
|
modalElement.style.display = 'none';
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Initializes the modal trigger.
|
|
*
|
|
*/
|
|
initializeTrigger() {
|
|
this.getElement().addEventListener('click', () => {
|
|
document.getElementById(this.getModalId()).style.display = 'flex';
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get the modal ID
|
|
*
|
|
* @returns {string} - The modal ID
|
|
*/
|
|
getModalId() {
|
|
return this.#modalId;
|
|
}
|
|
|
|
/**
|
|
* Get the action element
|
|
*
|
|
* @returns {HTMLElement} - The DOM element being worked with.
|
|
*/
|
|
getElement() {
|
|
return this.#action.getElement();
|
|
}
|
|
}
|
|
|
|
class UikitModal extends BaseModal {
|
|
constructor(action) {
|
|
super(action);
|
|
}
|
|
|
|
show() {
|
|
UIkit.modal(`#${this.getModalId()}`).show();
|
|
}
|
|
|
|
hide() {
|
|
UIkit.modal(`#${this.getModalId()}`).hide();
|
|
}
|
|
|
|
create(content) {
|
|
const modalHtml = `
|
|
<div id="${this.getModalId()}" uk-modal>
|
|
<div class="uk-modal-dialog uk-modal-body">
|
|
<button class="uk-modal-close-default" type="button" uk-close></button>
|
|
<div id="${this.getModalId()}-content">
|
|
${content}
|
|
</div>
|
|
</div>
|
|
</div>`;
|
|
this.insertIntoDOM(modalHtml);
|
|
}
|
|
|
|
initializeTrigger() {
|
|
this.getElement().setAttribute('uk-toggle', `target: #${this.getModalId()}`);
|
|
}
|
|
}
|
|
|
|
class BootstrapModal extends BaseModal {
|
|
constructor(action) {
|
|
super(action);
|
|
}
|
|
|
|
show() {
|
|
const modal = new bootstrap.Modal(document.getElementById(this.getModalId()));
|
|
modal.show();
|
|
}
|
|
|
|
hide() {
|
|
const modal = bootstrap.Modal.getInstance(document.getElementById(this.getModalId()));
|
|
if (modal) {
|
|
modal.hide();
|
|
}
|
|
}
|
|
|
|
create(content) {
|
|
const modalHtml = `
|
|
<div class="modal fade" id="${this.getModalId()}" tabindex="-1" role="dialog" aria-hidden="true">
|
|
<div class="modal-dialog" role="document">
|
|
<div class="modal-content p-3">
|
|
<div class="modal-header">
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
<div id="${this.getModalId()}-content" class="modal-body">
|
|
${content}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>`;
|
|
this.insertIntoDOM(modalHtml);
|
|
}
|
|
|
|
initializeTrigger() {
|
|
this.getElement().setAttribute('data-bs-toggle', 'modal');
|
|
this.getElement().setAttribute('data-bs-target', `#${this.getModalId()}`);
|
|
}
|
|
}
|
|
|
|
class FoundationModal extends BaseModal {
|
|
constructor(action) {
|
|
super(action);
|
|
this.modalElement = null;
|
|
}
|
|
|
|
show() {
|
|
if (this.modalElement) {
|
|
this.modalElement.open();
|
|
}
|
|
}
|
|
|
|
hide() {
|
|
if (this.modalElement) {
|
|
this.modalElement.close();
|
|
}
|
|
}
|
|
|
|
create(content) {
|
|
const modalHtml = `
|
|
<div class="reveal" id="${this.getModalId()}" data-reveal>
|
|
<div id="${this.getModalId()}-content">
|
|
${content}
|
|
</div>
|
|
<button class="close-button" data-close aria-label="Close modal" type="button">
|
|
<span aria-hidden="true">×</span>
|
|
</button>
|
|
</div>`;
|
|
this.insertIntoDOM(modalHtml);
|
|
this.modalElement = new Foundation.Reveal(document.getElementById(this.getModalId()));
|
|
}
|
|
|
|
initializeTrigger() {
|
|
this.getElement().setAttribute('data-open', this.getModalId());
|
|
}
|
|
}
|
|
|
|
class TailwindModal extends BaseModal {
|
|
constructor(action) {
|
|
super(action);
|
|
}
|
|
|
|
show() {
|
|
document.getElementById(this.getModalId()).classList.remove('hidden');
|
|
}
|
|
|
|
hide() {
|
|
document.getElementById(this.getModalId()).classList.add('hidden');
|
|
}
|
|
|
|
create(content) {
|
|
const modalHtml = `
|
|
<div class="modal hidden fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full" id="${this.getModalId()}">
|
|
<div class="modal-content container mx-auto p-5 bg-white">
|
|
<div id="${this.getModalId()}-content">
|
|
${content}
|
|
</div>
|
|
<button class="close-button" onclick="document.getElementById('${this.getModalId()}').classList.add('hidden')">Close</button>
|
|
</div>
|
|
</div>`;
|
|
this.insertIntoDOM(modalHtml);
|
|
}
|
|
|
|
initializeTrigger() {
|
|
this.getElement().addEventListener('click', () => {
|
|
document.getElementById(this.getModalId()).classList.remove('hidden');
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* ModalElement class responsible for creating and managing modal elements.
|
|
* It dynamically selects the appropriate modal style based on the available UI framework.
|
|
*/
|
|
class ModalElement {
|
|
/**
|
|
* Constructs an instance of ModalElement with the appropriate modal type
|
|
* based on the detected UI framework.
|
|
*
|
|
* @param {Action} action - The action element that triggers the modal.
|
|
*/
|
|
constructor(action) {
|
|
this.modal = ModalElement.framework(action);
|
|
}
|
|
|
|
/**
|
|
* Loads content into the modal.
|
|
*
|
|
* @param {string} content - The content to load into the modal.
|
|
*/
|
|
load(content) {
|
|
this.modal.load(content);
|
|
}
|
|
|
|
/**
|
|
* Determines the appropriate modal implementation based on the available UI framework.
|
|
*
|
|
* @param {Action} action - The action element triggering the modal.
|
|
* @returns {BaseModal|BootstrapModal|UikitModal|FoundationModal|TailwindModal} The modal instance.
|
|
*/
|
|
static framework(action) {
|
|
const frameworks = {
|
|
'UIkit': UikitModal,
|
|
'bootstrap': BootstrapModal,
|
|
'Foundation': FoundationModal,
|
|
'tailwind': TailwindModal
|
|
};
|
|
|
|
for (const [key, ModalType] of Object.entries(frameworks)) {
|
|
if (typeof window[key] !== 'undefined' || (key === 'tailwind' && document.querySelector('.tailwind-class') !== null)) {
|
|
{
|
|
console.log(`${key} modal selected`);
|
|
}
|
|
return new ModalType(action);
|
|
}
|
|
}
|
|
|
|
{
|
|
console.log(`base modal selected`);
|
|
}
|
|
return new BaseModal(action);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* InlineElement class responsible for adding inline elements.
|
|
*/
|
|
class InlineElement {
|
|
#action;
|
|
|
|
/**
|
|
* Creates an instance of InlineElement.
|
|
*
|
|
* @param {Action} action - The action element that triggers the inline display.
|
|
*/
|
|
constructor(action) {
|
|
this.#action = action;
|
|
// Clear initial content
|
|
this.getElement().innerHTML = '';
|
|
}
|
|
|
|
/**
|
|
* Loads content into the trigger element. Appends new content if existing content is present.
|
|
*
|
|
* @param {string} content - The content to load into the trigger element.
|
|
*/
|
|
load(content) {
|
|
const existingContent = this.getElement().innerHTML;
|
|
this.getElement().innerHTML = existingContent ? `${existingContent}\n ${content}` : content;
|
|
}
|
|
|
|
/**
|
|
* Get the action element
|
|
*
|
|
* @returns {HTMLElement} - The DOM element being worked with.
|
|
*/
|
|
getElement() {
|
|
return this.#action.getElement();
|
|
}
|
|
}
|
|
|
|
class BaseTooltip {
|
|
#action;
|
|
|
|
/**
|
|
* Creates a new BaseTooltip instance.
|
|
*
|
|
* @param {Action} action - The action elements that triggers the tooltip.
|
|
*/
|
|
constructor(action) {
|
|
this.#action = action;
|
|
this.getElement().style.cursor = 'help';
|
|
}
|
|
|
|
/**
|
|
* Loads content into the tooltip. If the trigger elements already has a title,
|
|
* the new content is appended to it.
|
|
*
|
|
* @param {string} content - The content to load into the tooltip.
|
|
* @throws {Error} Throws an error if the trigger elements is not valid.
|
|
*/
|
|
load(content) {
|
|
const existingTitle = this.getElement().getAttribute('title');
|
|
const newTitle = existingTitle ? existingTitle + "\n" + content : content;
|
|
this.getElement().setAttribute('title', newTitle);
|
|
}
|
|
|
|
/**
|
|
* Get the action element
|
|
*
|
|
* @returns {HTMLElement} - The DOM element being worked with.
|
|
*/
|
|
getElement() {
|
|
return this.#action.getElement();
|
|
}
|
|
}
|
|
|
|
class BootstrapTooltip extends BaseTooltip {
|
|
constructor(action) {
|
|
super(action);
|
|
}
|
|
|
|
load(content) {
|
|
try {
|
|
super.load(content);
|
|
const tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
|
|
const tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
|
|
return new bootstrap.Tooltip(tooltipTriggerEl);
|
|
});
|
|
} catch (error) {
|
|
console.error('Error loading BootstrapTooltip:', error);
|
|
}
|
|
}
|
|
}
|
|
|
|
class UikitTooltip extends BaseTooltip {
|
|
constructor(triggerElement) {
|
|
super(triggerElement);
|
|
}
|
|
|
|
load(content) {
|
|
try {
|
|
super.load(content);
|
|
UIkit.tooltip(this.triggerElement);
|
|
} catch (error) {
|
|
console.error('Error loading UikitTooltip:', error);
|
|
}
|
|
}
|
|
}
|
|
|
|
class FoundationTooltip extends BaseTooltip {
|
|
constructor(action) {
|
|
super(action);
|
|
}
|
|
|
|
load(content) {
|
|
try {
|
|
this.getElement().setAttribute('data-tooltip', '');
|
|
super.load(content);
|
|
this.getElement().classList.add('has-tip');
|
|
|
|
new Foundation.Tooltip(this.getElement(), {
|
|
// Default options
|
|
disableHover: false, // Allows tooltip to be hoverable
|
|
fadeOutDuration: 150, // Duration of fade out animation in milliseconds
|
|
fadeInDuration: 150, // Duration of fade in animation in milliseconds
|
|
showOn: 'all', // Can be 'all', 'large', 'medium', 'small'
|
|
templateClasses: '', // Custom class(es) to be added to the tooltip template
|
|
tipText: () => this.getElement().getAttribute('title'), // Function to define tooltip text
|
|
triggerClass: 'has-tip', // Class to be added on the trigger elements
|
|
touchCloseText: 'tap to close', // Text for close button on touch devices
|
|
positionClass: 'top', // Position of tooltip, can be 'top', 'bottom', 'left', 'right', etc.
|
|
vOffset: 10, // Vertical offset
|
|
hOffset: 12, // Horizontal offset
|
|
allowHtml: false // Allow HTML in tooltip content
|
|
});
|
|
} catch (error) {
|
|
console.error('Error loading FoundationTooltip:', error);
|
|
}
|
|
}
|
|
}
|
|
|
|
class TailwindTooltip extends BaseTooltip {
|
|
constructor(triggerElement) {
|
|
super(triggerElement);
|
|
}
|
|
|
|
load(content) {
|
|
try {
|
|
super.load(content);
|
|
this._createTooltipElement();
|
|
this._initializeEvents();
|
|
} catch (error) {
|
|
console.error('Error loading TailwindTooltip:', error);
|
|
}
|
|
}
|
|
|
|
_createTooltipElement() {
|
|
this.tooltipElement = document.createElement('div');
|
|
this.tooltipElement.id = this.tooltipId;
|
|
this.tooltipElement.className = 'absolute invisible bg-gray-800 text-white text-xs px-2 py-1 rounded-md';
|
|
this.tooltipElement.style.transition = 'visibility 0.3s linear, opacity 0.3s linear';
|
|
this.tooltipElement.textContent = this.getElement().getAttribute('title');
|
|
document.body.appendChild(this.tooltipElement);
|
|
}
|
|
|
|
_initializeEvents() {
|
|
this.getElement().addEventListener('mouseenter', () => {
|
|
const rect = this.getElement().getBoundingClientRect();
|
|
this._title = this.getElement().getAttribute('title');
|
|
this.tooltipElement.style.left = `${rect.left + window.scrollX}px`;
|
|
this.tooltipElement.style.top = `${rect.bottom + 5 + window.scrollY}px`;
|
|
this.tooltipElement.classList.remove('invisible');
|
|
this.tooltipElement.classList.add('opacity-100');
|
|
this.getElement().setAttribute('title', '');
|
|
});
|
|
|
|
this.getElement().addEventListener('mouseleave', () => {
|
|
this.tooltipElement.classList.add('invisible');
|
|
this.tooltipElement.classList.remove('opacity-100');
|
|
this.getElement().setAttribute('title', this._title);
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* TooltipElement class responsible for creating and managing tooltip elements.
|
|
* It dynamically selects the appropriate tooltip style based on the available UI framework.
|
|
*/
|
|
class TooltipElement {
|
|
/**
|
|
* Constructs an instance of TooltipElement with the appropriate tooltip type
|
|
* based on the detected UI framework.
|
|
*
|
|
* @param {Action} action - The action element that triggers the tooltip.
|
|
*/
|
|
constructor(action) {
|
|
this.tooltip = TooltipElement.framework(action);
|
|
}
|
|
|
|
/**
|
|
* Loads content into the tooltip.
|
|
*
|
|
* @param {string} content - The content to load into the tooltip.
|
|
*/
|
|
load(content) {
|
|
this.tooltip.load(content);
|
|
}
|
|
|
|
/**
|
|
* Determines the appropriate tooltip implementation based on the available UI framework.
|
|
*
|
|
* @param {Action} action - The action element triggering the tooltip.
|
|
* @returns {BaseTooltip|BootstrapTooltip|UikitTooltip|FoundationTooltip|TailwindTooltip} The tooltip instance.
|
|
* @param debug
|
|
*/
|
|
static framework(action) {
|
|
const frameworks = {
|
|
'UIkit': UikitTooltip,
|
|
'bootstrap': BootstrapTooltip,
|
|
'Foundation': FoundationTooltip,
|
|
'tailwind': TailwindTooltip
|
|
};
|
|
|
|
for (const [key, TooltipType] of Object.entries(frameworks)) {
|
|
if (typeof window[key] !== 'undefined' || (key === 'tailwind' && document.querySelector('.tailwind-class') !== null)) {
|
|
{
|
|
console.log(`${key} tooltip selected`);
|
|
}
|
|
return new TooltipType(action);
|
|
}
|
|
}
|
|
|
|
{
|
|
console.log(`base tooltip selected`);
|
|
}
|
|
return new BaseTooltip(action);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Element class responsible for creating and managing different types of elements
|
|
* based on the specified format.
|
|
*/
|
|
class Element {
|
|
/**
|
|
* Constructs an Element instance based on the given format.
|
|
*
|
|
* @param {Action} action - The action element that triggers the inline display.
|
|
*/
|
|
constructor(action) {
|
|
const elementTypes = {
|
|
'modal': ModalElement,
|
|
'inline': InlineElement,
|
|
'tooltip': TooltipElement
|
|
};
|
|
|
|
const format = action.getFormat();
|
|
const ElementType = elementTypes[format] || InlineElement;
|
|
this.element = new ElementType(action);
|
|
|
|
{
|
|
console.log(`${format} element selected`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Load the content into the element.
|
|
*
|
|
* @param {string} content - The content to load.
|
|
*/
|
|
load(content) {
|
|
this.element.load(content);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Loader class responsible for handling the loading of Reference references.
|
|
* It initializes necessary components and loads data into a specified HTML element.
|
|
*/
|
|
class Loader {
|
|
#api;
|
|
#action;
|
|
#element;
|
|
#format;
|
|
|
|
/**
|
|
* Constructs a Loader instance.
|
|
* Allows for dependency injection of the Api class for easier testing and flexibility.
|
|
* @param {Api} api - Instance of Api class for making API calls.
|
|
*/
|
|
constructor(api = new Api()) {
|
|
this.#api = api;
|
|
}
|
|
|
|
/**
|
|
* Load the Reference references into the specified HTML element.
|
|
* This method extracts references from the element, validates them, and loads each valid reference.
|
|
* @param {HTMLElement} element - The element to load Reference references into.
|
|
*/
|
|
async load(element) {
|
|
const references = element.innerHTML.split(';').map(ref => ref.trim());
|
|
|
|
if (references.length === 0) {
|
|
console.error("No references found in the getBible tagged class.");
|
|
return;
|
|
}
|
|
|
|
const validReferences = this.#validateReferences(references);
|
|
if (validReferences.length === 0) {
|
|
console.error("No valid references found in the getBible tagged class.");
|
|
return;
|
|
}
|
|
|
|
this.#init(element);
|
|
await this.#processReferences(validReferences);
|
|
}
|
|
|
|
/**
|
|
* Validates a list of references to ensure each is no longer than 30 characters and contains at least one number.
|
|
* Invalid references are logged and excluded from the return value.
|
|
* @param {string[]} references - The array of references to validate.
|
|
* @returns {string[]} A filtered array of valid references.
|
|
* @private
|
|
*/
|
|
#validateReferences(references) {
|
|
return references.filter(reference => {
|
|
// Check if the reference is not longer than 30 characters and contains at least one number
|
|
const isValid = reference.length <= 30 && /\d/.test(reference);
|
|
// Log invalid references
|
|
if (!isValid) {
|
|
console.error(`Invalid getBible reference: ${reference}`);
|
|
return false;
|
|
}
|
|
return true;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Processes each valid reference by fetching translations and loading the scripture.
|
|
* This method handles the asynchronous nature of API calls.
|
|
* @param {string[]} validReferences - Array of valid references to be processed.
|
|
* @private
|
|
*/
|
|
async #processReferences(validReferences) {
|
|
for (const reference of validReferences) {
|
|
for (const translation of this.#action.getTranslations()) {
|
|
try {
|
|
const scripture = await this.#api.get(reference, translation);
|
|
if (scripture) {
|
|
this.#load(scripture);
|
|
}
|
|
} catch (error) {
|
|
console.error(`Error loading reference ${reference}:`, error);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Initializes components necessary for loading references.
|
|
* This includes action, element, and format components.
|
|
* @param {HTMLElement} element - The element to be initialized for loading.
|
|
* @private
|
|
*/
|
|
#init(element) {
|
|
this.#action = new Action(element);
|
|
this.#element = new Element(this.#action);
|
|
this.#format = new Format(this.#action);
|
|
}
|
|
|
|
/**
|
|
* Loads the scripture data into the initialized element in the specified format.
|
|
* This method is responsible for the final rendering of scripture data.
|
|
* @param {Object} scripture - The data containing verses and their details.
|
|
* @private
|
|
*/
|
|
#load(scripture) {
|
|
this.#element.load(this.#format.get(new Scripture(scripture)));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Initializes loaders for elements with class 'getBible'.
|
|
* Each element gets its own Loader instance because each loader maintains state
|
|
* specific to the element it operates on. This function encapsulates the logic
|
|
* for finding relevant elements and initializing loaders for them.
|
|
* @param {Api} api - The Api instance to be used by each Loader.
|
|
*/
|
|
function initializeGetBibleLoaders(api) {
|
|
const elements = document.querySelectorAll('.getBible');
|
|
elements.forEach(element => {
|
|
// Create a new loader instance for each element
|
|
const loader = new Loader(api);
|
|
loader.load(element).catch(error => {
|
|
// Error handling for each loader instance
|
|
console.error(`Loading error for element ${element}:`, error);
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Entry point to load Reference references.
|
|
* Attaches event listener to DOMContentLoaded to ensure the DOM is fully loaded
|
|
* before attempting to initialize loaders.
|
|
*/
|
|
document.addEventListener('DOMContentLoaded', (event) => {
|
|
try {
|
|
const api = new Api();
|
|
initializeGetBibleLoaders(api);
|
|
} catch (error) {
|
|
console.error("Error initializing GetBible loaders:", error);
|
|
}
|
|
});
|
|
|
|
}));
|