From 57482a2b481accde9498703188574adcbfd119e2 Mon Sep 17 00:00:00 2001 From: Llewellyn van der Merwe Date: Tue, 28 Nov 2023 13:05:57 +0200 Subject: [PATCH] Implements a significantly more stable architecture for the library. --- README.md | 12 +- dist/js/getBible.js | 2127 +++++++++++------- dist/js/getBible.min.js | 4 +- package-lock.json | 561 +++-- package.json | 20 +- src/js/core/Action.js | 106 + src/js/core/Api.js | 2 +- src/js/core/Loader.js | 158 +- src/js/core/Memory.js | 138 +- src/js/core/Reference.js | 170 ++ src/js/core/Scripture.js | 46 + src/js/elements/Element.js | 61 +- src/js/elements/InlineElement.js | 54 +- src/js/elements/ModalElement.js | 25 +- src/js/elements/TooltipElement.js | 25 +- src/js/elements/modals/BaseModal.js | 49 +- src/js/elements/modals/BootstrapModal.js | 18 +- src/js/elements/modals/FoundationModal.js | 14 +- src/js/elements/modals/TailwindModal.js | 22 +- src/js/elements/modals/UikitModal.js | 16 +- src/js/elements/tooltip/BaseTooltip.js | 25 +- src/js/elements/tooltip/BootstrapTooltip.js | 6 +- src/js/elements/tooltip/FoundationTooltip.js | 14 +- src/js/elements/tooltip/TailwindTooltip.js | 16 +- src/js/elements/tooltip/UikitTooltip.js | 2 +- src/js/formats/BaseFormat.js | 31 +- src/js/formats/BlockFormat.js | 94 +- src/js/formats/Format.js | 59 +- src/js/formats/InlineFormat.js | 94 +- src/js/formats/PlainFormat.js | 91 +- src/js/getBible.js | 35 +- tests/basic.html | 2 +- tests/uikit.html | 2 +- 33 files changed, 2458 insertions(+), 1641 deletions(-) create mode 100644 src/js/core/Action.js create mode 100644 src/js/core/Reference.js create mode 100644 src/js/core/Scripture.js diff --git a/README.md b/README.md index c33e962..bb0da50 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ## Overview -GetBible loader is an intuitive and lightweight JavaScript solution for embedding Bible scripture into your website. By simply adding the `getBible` class to any element that has a scripture reference on your webpage, you can enable users to see the full scripture text as inline text, a tooltip or a modal. +GetBible loader is an intuitive and lightweight JavaScript solution for embedding Reference scripture into your website. By simply adding the `getBible` class to any element that has a scripture reference on your webpage, you can enable users to see the full scripture text as inline text, a tooltip or a modal. ## How to Add GetBible Tooltips to Your Website @@ -12,7 +12,7 @@ GetBible loader is an intuitive and lightweight JavaScript solution for embeddin ```html - + ``` 2. **Markup Your Scripture References:** @@ -34,7 +34,7 @@ GetBible loader is an intuitive and lightweight JavaScript solution for embeddin Data attributes allow you to customize the behavior and display of the scripture. - `data-format`: Specify the format you would like to load (e.g., `modal`). There are three options `modal`, `inline`, and `tooltip` you can just select one at a time. -- `data-translation`: Specify the Bible translations to use, separated by semicolons (e.g., `kjv;aov`). The tooltip will fetch the scripture from each translation listed. +- `data-translation`: Specify the Reference translations to use, separated by semicolons (e.g., `kjv;aov`). The tooltip will fetch the scripture from each translation listed. - `data-show-translation`: Set to `1` to display the translation name in the tooltip. - `data-show-abbreviation`: Set to `1` to display the abbreviation of the translation in the tooltip. - `data-show-language`: Set to `1` to display the language of the translation in the tooltip. @@ -57,12 +57,12 @@ Should you see room for improvement to these, please open an issue at our [suppo ### Some Guidelines -- **Chapter and Verse**: Follow the book name with the chapter number, a colon, and the verse number(s) (e.g., "Jn3:16"). +- **Reference and Verse**: Follow the book name with the chapter number, a colon, and the verse number(s) (e.g., "Jn3:16"). - **Multiple Verses**: Separate multiple verses with commas (e.g., "Jn 3:16,17"). - **Verse Ranges**: Indicate a range of verses using a hyphen (e.g., "John 3:16-19"). - **Multiple References**: Separate different scripture references with semicolons (e.g., "Joh 3:16-17; 1 Jo3:16-19"). -- **One Chapter Per/Reference**: Each reference should only target one chapter. -- **Missing Chapter**: All references that does not have a chapter will **default to chapter 1** (e.g., "Jhn :16-19"). +- **One Reference Per/Reference**: Each reference should only target one chapter. +- **Missing Reference**: All references that does not have a chapter will **default to chapter 1** (e.g., "Jhn :16-19"). - **Missing Verses**: All references that does not have verses will **default to verse 1** (e.g., "Jn3"). ## Copyright and License diff --git a/dist/js/getBible.js b/dist/js/getBible.js index e7beea6..f3925fe 100644 --- a/dist/js/getBible.js +++ b/dist/js/getBible.js @@ -1,976 +1,1377 @@ /** - * getBible Loader v3.0.0 + * getBible Loader v3.0.1 * https://getbible.net * (c) 2014 - 2023 Llewellyn van der Merwe * MIT License **/ (function (factory) { - typeof define === 'function' && define.amd ? define(factory) : - 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; + /** - * Class for managing local storage of scripture data. + * 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. */ - 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} 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}`; - } + 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; + } } /** - * Class for handling API calls to fetch scripture data. + * Retrieves scripture data from local storage. + * + * @param {string} reference - The scripture reference. + * @param {string} translation - The translation. + * @returns {Promise} The scripture data or null if not found. */ - 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} 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 BaseFormat { - /** - * Get formats the verses. - * - * @param {Object} data - The data containing verses and their details. - * @param {boolean} showBook - Whether to show book names. - * @param {boolean} showTrans - Whether to show translations. - * @param {boolean} showAbbr - Whether to show abbreviations. - * @param {boolean} showLang - Whether to show languages. - * @returns {string} The formatted verses. - * @abstract - */ - get(data, showBook, showTrans, showAbbr, showLang) { - throw new Error("The 'get' method must be implemented in BaseFormat subclass."); - } - } - - class BlockFormat extends BaseFormat { - /** - * Formats the verses for HTML block elements. - * - * @param {Object} data - The data containing verses and their details. - * @param {boolean} showBook - Whether to show book names. - * @param {boolean} showTrans - Whether to show translations. - * @param {boolean} showAbbr - Whether to show abbreviations. - * @param {boolean} showLang - Whether to show languages. - * @returns {string} The formatted HTML string. - */ - get(data, showBook, showTrans, showAbbr, showLang) { - let formattedHtml = ''; - let setBookName = new Set(); - let setTranslation = new Set(); - let setAbbreviation = new Set(); - let setLanguage = new Set(); - - for (const key in data) { - if (!data.hasOwnProperty(key)) continue; - - let headerParts = []; - if (showTrans && !setTranslation.has(key)) { - headerParts.push(`${data[key].translation}`); - setTranslation.add(key); - } - if (showAbbr && !setAbbreviation.has(key)) { - headerParts.push(`${data[key].abbreviation}`); - setAbbreviation.add(key); - } - if (showBook && !setBookName.has(key)) { - headerParts.push(`${data[key].name}`); - setBookName.add(key); - } - if (showLang && !setLanguage.has(key)) { - headerParts.push(`${data[key].language}`); - setLanguage.add(key); - } - - // Construct the header - if (headerParts.length > 0) { - formattedHtml += `
[${headerParts.join(' - ')}]
\n`; - } - - // Add verses - const verses = data[key].verses - .map(verse => `${verse.verse}. ${verse.text}`) - .join("
"); - formattedHtml += `
${verses}

`; - } - - return `
${formattedHtml}
`; - } - } - - class InlineFormat extends BaseFormat { - /** - * Formats the verses for HTML inline elements. - * - * @param {Object} data - The data containing verses and their details. - * @param {boolean} showBook - Whether to show book names. - * @param {boolean} showTrans - Whether to show translations. - * @param {boolean} showAbbr - Whether to show abbreviations. - * @param {boolean} showLang - Whether to show languages. - * @returns {string} The formatted HTML string. - */ - get(data, showBook, showTrans, showAbbr, showLang) { - let formattedHtml = ''; - let setBookName = new Set(); - let setTranslation = new Set(); - let setAbbreviation = new Set(); - let setLanguage = new Set(); - - for (const key in data) { - if (!data.hasOwnProperty(key)) continue; - - let footerParts = []; - if (showTrans && !setTranslation.has(key)) { - footerParts.push(`${data[key].translation}`); - setTranslation.add(key); - } - if (showAbbr && !setAbbreviation.has(key)) { - footerParts.push(`${data[key].abbreviation}`); - setAbbreviation.add(key); - } - if (showBook && !setBookName.has(key)) { - footerParts.push(`${data[key].name}`); - setBookName.add(key); - } - if (showLang && !setLanguage.has(key)) { - footerParts.push(`${data[key].language}`); - setLanguage.add(key); - } - - // Add verses - const verses = data[key].verses - .map(verse => `${verse.verse}. ${verse.text}`) - .join("\n"); - formattedHtml += `${verses}\n`; - - // Construct the footer - if (footerParts.length > 0) { - formattedHtml += `[${footerParts.join(' - ')}]\n`; - } - } - - return `${formattedHtml}`; - } - } - - class PlainFormat extends BaseFormat { - /** - * Formats the verses for plain text display. - * - * @param {Object} data - The data containing verses and their details. - * @param {boolean} showBook - Whether to show book names. - * @param {boolean} showTrans - Whether to show translations. - * @param {boolean} showAbbr - Whether to show abbreviations. - * @param {boolean} showLang - Whether to show languages. - * @returns {string} The formatted text. - */ - get(data, showBook, showTrans, showAbbr, showLang) { - let formattedText = ''; - let setBookName = new Set(); - let setTranslation = new Set(); - let setAbbreviation = new Set(); - let setLanguage = new Set(); - - for (const key in data) { - if (!data.hasOwnProperty(key)) continue; // Ensure processing only own properties - - let headerParts = []; - if (showTrans && !setTranslation.has(key)) { - headerParts.push(data[key].translation); - setTranslation.add(key); - } - if (showAbbr && !setAbbreviation.has(key)) { - headerParts.push(data[key].abbreviation); - setAbbreviation.add(key); - } - if (showBook && !setBookName.has(key)) { - headerParts.push(data[key].name); - setBookName.add(key); - } - if (showLang && !setLanguage.has(key)) { - headerParts.push(data[key].language); - setLanguage.add(key); - } - - // Construct the header - if (headerParts.length > 0) { - formattedText += '[' + headerParts.join(' - ') + "]\n"; - } - - // Add verses - const verses = data[key].verses.map(verse => `${verse.verse}. ${verse.text}`).join("\n"); - formattedText += verses + "\n\n"; // Add extra newline for separation - } - - return formattedText.trim(); - } + static async get(reference, translation) { + return this.#get(reference, translation); } /** - * Format class responsible for creating and managing different types of formats - * based on the specified type. + * 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 */ - class Format { - /** - * Constructs a Format instance based on the given type. - * - * @param {string} formatType - The format type. - */ - constructor(formatType = 'tooltip') { - const formatTypes = { - 'modal': BlockFormat, - 'inline': InlineFormat, - 'tooltip': PlainFormat - }; - - const FormatType = formatTypes[formatType] || PlainFormat; - this.format = new FormatType(); - } - - /** - * Get the formatted verses. - * - * @param {Object} data - The data containing verses and their details. - * @param {boolean} showBook - Whether to show book names. - * @param {boolean} showTrans - Whether to show translations. - * @param {boolean} showAbbr - Whether to show abbreviations. - * @param {boolean} showLang - Whether to show languages. - * @returns {string} The formatted verses. - */ - get(data, showBook, showTrans, showAbbr, showLang) { - return this.format.get(data, showBook, showTrans, showAbbr, showLang); + 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; + } } - class BaseModal { - /** - * Creates a new BaseModal instance. - * - * @param {HTMLElement} triggerElement - The elements that triggers the modal. - */ - constructor(triggerElement) { - this.modalId = `modal-${Math.random().toString(36).slice(2, 11)}`; - this.triggerElement = triggerElement; - this.triggerElement.style.cursor = 'pointer'; - this.initializeTrigger(); - } + /** + * 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}`; + } + } - /** - * Loads content into the modal. - * - * @param {string} content - The content to load into the modal. - */ - load(content) { - const existingModal = document.getElementById(this.modalId); - // Check if modal already exists - if (existingModal) { - // Update the content of the existing modal - const contentDiv = document.getElementById(`${this.modalId}-content`); - if (contentDiv) { - contentDiv.innerHTML += content; - } + /** + * 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} 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} 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} 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 { - // If modal doesn't exist, create it with the new content - this.create(content); + 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}`; } - /** - * Insert HTML into the dom. - * - * @param {string} html - The html to insert. - */ - insertIntoDOM(html) { - document.body.insertAdjacentHTML('beforeend', html); + // 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."); } - /** - * Creates the modal. - * - * @param {string} content - The initial content of the modal. - */ - create(content) { - const modalHtml = ` -