refactor: separate classes into other files

This commit is contained in:
alistair3149 2024-05-25 16:38:57 -04:00
parent bbde0ae7bd
commit 476af1352b
No known key found for this signature in database
5 changed files with 307 additions and 297 deletions

View file

@ -46,7 +46,10 @@
"parseTabName": "TabberNeueParseTabName",
"updateLocationOnTabChange": "TabberNeueUpdateLocationOnTabChange"
}
}
},
"ext.tabberNeue/Hash.js",
"ext.tabberNeue/Transclude.js",
"ext.tabberNeue/Util.js"
],
"messages": [
"error"

View file

@ -0,0 +1,73 @@
let uniqueHashes;
/**
* Class representing a Hash utility for generating unique hash values.
*
* @class Hash
*/
class Hash {
/**
* Initializes the Hash class by creating a new Set to store unique hashes.
*/
static init() {
uniqueHashes = new Set();
}
/**
* Checks if a given hash is not unique by verifying if it exists in the Set of unique hashes.
*
* @param {string} hash - The hash to check for uniqueness.
* @return {boolean} - Returns true if the hash is not unique, false otherwise.
*/
static exists( hash ) {
return uniqueHashes.has( hash );
}
/**
* Generates a unique hash based on the input hash by appending a suffix if necessary.
*
* @param {string} hash - The base hash to make unique.
* @return {string} - A unique hash derived from the input hash.
*/
static makeUnique( hash ) {
const match = hash.match( /^(.+)_([0-9]+)$/ );
let suffix = match ? parseInt( match[ 2 ], 10 ) + 1 : 1;
const initialHash = hash;
let uniqueHash = `${ initialHash }_${ suffix }`;
// Increment suffix and generate a new unique hash until a unique one is found
while ( Hash.exists( uniqueHash ) ) {
suffix++;
uniqueHash = `${ initialHash }_${ suffix }`;
}
return uniqueHash;
}
/**
* Builds a unique hash based on the provided title text.
*
* @param {string} titleText - The title text to generate the hash from.
* @return {string} - A unique hash created from the title text.
*/
static build( titleText ) {
let hash = mw.util.escapeIdForAttribute( titleText );
if ( Hash.exists( hash ) ) {
hash = Hash.makeUnique( hash );
}
uniqueHashes.add( hash );
return hash;
}
/**
* Clears the Set of unique hashes, removing all stored hashes.
*/
static clear() {
uniqueHashes.clear();
}
}
module.exports = Hash;

View file

@ -0,0 +1,138 @@
/**
* Represents a class that handles transcluding content for a tab within a tabber component.
*
* @class Transclude
*/
class Transclude {
constructor( activeTabpanel, cacheExpiration = 3600 ) {
this.activeTabpanel = activeTabpanel;
this.pageTitle = this.activeTabpanel.dataset.mwTabberPageTitle;
this.url = this.activeTabpanel.dataset.mwTabberLoadUrl;
this.cacheKey = `tabber-transclude-${ encodeURIComponent( this.pageTitle ) }_v1`;
this.cacheExpiration = cacheExpiration;
}
/**
* Validates the URL format.
*
* @return {Promise} A Promise that resolves if the URL is valid, and rejects with an Error if the URL is empty, null, or in an invalid format.
*/
validateUrl() {
const urlPattern = /^(https?):\/\/[^\s/$.?#][^\s]*$/;
if ( !this.url || this.url.trim() === '' ) {
return Promise.reject( new Error( '[TabberNeue] URL is empty or null' ) );
}
if ( !urlPattern.test( this.url ) ) {
return Promise.reject( new Error( `[TabberNeue] Invalid URL format : ${ this.url }` ) );
}
return Promise.resolve();
}
/**
* Checks the session storage for cached data using the cache key.
*
* @return {Object|null} The cached data if found, or null if no cached data is found.
*/
checkCache() {
const cachedData = mw.storage.session.getObject( this.cacheKey );
if ( cachedData ) {
return cachedData;
}
return null;
}
/**
* Fetches data from the specified URL using a GET request.
*
* @return {Promise} A Promise that resolves with the response text if the network request is successful,
* and rejects with an Error if there is an issue with the network request.
*/
async fetchDataFromUrl() {
try {
const response = await fetch( this.url, { method: 'GET', timeout: 5000, credentials: 'same-origin' } );
if ( !response.ok ) {
throw new Error( `Network response was not ok: ${ response.status } - ${ response.statusText }` );
}
return Promise.resolve( response.text() );
} catch ( error ) {
return Promise.reject( `[TabberNeue] Error fetching data from URL: ${ this.url }`, error );
}
}
/**
* Parses the JSON data and extracts the 'parse.text' property.
*
* @param {string} data - The JSON data to be parsed.
* @return {string} The parsed 'parse.text' property from the JSON data.
* @throws {Error} If an error occurs while parsing the JSON data.
*/
parseData( data ) {
let parsedData;
try {
parsedData = JSON.parse( data );
parsedData = parsedData.parse.text;
} catch ( error ) {
mw.log.error( `[TabberNeue] Error occurred while parsing JSON data: ${ error }` );
return Promise.reject( new Error( `Error parsing JSON data: ${ error }` ) );
}
return parsedData;
}
/**
* Caches the parsed data in the session storage using the cache key.
*
* @param {string} parsedData - The parsed data to be cached.
* @return {string} The cached parsed data.
*/
cacheData( parsedData ) {
mw.storage.session.setObject( this.cacheKey, parsedData, this.cacheExpiration );
return parsedData;
}
/**
* Fetches data by validating the URL, checking the cache, fetching data from the URL,
* parsing the data, and caching the parsed data if not found in the cache.
*
* @return {Promise} A Promise that resolves with the fetched and cached data,
* or rejects with an error message if any step fails.
*/
async fetchData() {
try {
await this.validateUrl();
const cachedData = this.checkCache();
if ( cachedData ) {
return cachedData;
}
const data = await this.fetchDataFromUrl();
const parsedData = this.parseData( data );
return this.cacheData( parsedData );
} catch ( error ) {
return Promise.reject( `[TabberNeue] Error fetching data: ${ error }` );
}
}
/**
* Loads the page content by fetching data, updating the active tab panel's content,
* and handling errors if data fetching fails.
*
* @return {void}
*/
async loadPage() {
try {
this.activeTabpanel.classList.add( 'tabber__panel--loading' );
const data = await this.fetchData();
if ( data ) {
delete this.activeTabpanel.dataset.mwTabberLoadUrl;
this.activeTabpanel.classList.remove( 'tabber__panel--loading' );
this.activeTabpanel.innerHTML = data;
} else {
mw.log.error( `[TabberNeue] No valid API response or missing 'parse' field for ${ this.pageTitle } from: ${ this.url }` );
}
} catch ( error ) {
mw.log.error( `[TabberNeue] Failed to load data for ${ this.pageTitle }: ${ error }` );
}
}
}
module.exports = Transclude;

View file

@ -0,0 +1,87 @@
/**
* Utility class with methods for common utility functions.
*
* @class Util
*/
class Util {
/**
* Extracts text content from the given HTML string.
*
* @param {string} html - The HTML string to extract text content from.
* @return {string} The extracted text content.
*/
static extractTextFromHtml( html ) {
const tmp = document.createElement( 'div' );
tmp.innerHTML = html;
return tmp.textContent;
}
/**
* Returns the size (width or height) of the provided element.
* Required to calculate the size of hidden elements (e.g. nested tabs)
*
* @param {Element} element - The element for which to get the size.
* @param {string} type - The type of size to retrieve ('width' or 'height').
* @return {number} The actual size of the element based on the specified type.
*/
static getElementSize( element, type ) {
if ( !element || !( element instanceof Element ) || ( type !== 'width' && type !== 'height' ) ) {
mw.log.error( '[TabberNeue] Invalid element or type provided for getElementSize' );
return 0;
}
let value = element.getBoundingClientRect()[ type ];
if ( value === 0 ) {
value = this.getHiddenElementSize( element, type );
}
return value;
}
/**
* Retrieves the size of a hidden element by cloning it and calculating the size.
*
* @param {Element} element - The hidden element to retrieve the size from.
* @param {string} type - The type of size to retrieve ('width' or 'height').
* @return {number} The size of the hidden element based on the specified type.
*/
static getHiddenElementSize( element, type ) {
const shadowRoot = document.createElement( 'div' ).attachShadow( { mode: 'open' } );
const clone = element.cloneNode( true );
clone.style.position = 'absolute';
clone.style.visibility = 'hidden';
shadowRoot.appendChild( clone );
try {
const value = clone.getBoundingClientRect()[ type ];
return value;
} finally {
clone.parentNode.removeChild( clone );
}
}
/**
* Rounds the scrollLeft value to the nearest integer using Math.ceil.
* Used to avoid the fractional pixel issue caused by different browser implementations
*
* @param {number} val - The scrollLeft value to be rounded.
* @return {number} The rounded scrollLeft value.
*/
static roundScrollLeft( val ) {
return Math.ceil( val );
}
/**
* Sets the attributes of the given element based on the provided attributes object.
*
* @param {Element} element - The element to set attributes for.
* @param {Object} attributes - An object containing key-value pairs of attributes to set.
*/
static setAttributes( element, attributes ) {
for ( const key in attributes ) {
element.setAttribute( key, attributes[ key ] );
}
}
}
module.exports = Util;

View file

@ -6,303 +6,12 @@
* TODO: Split classes into different modules
*/
const config = require( './config.json' );
const Hash = require( './Hash.js' );
const Transclude = require( './Transclude.js' );
const Util = require( './Util.js' );
let uniqueHashes;
let resizeObserver;
/**
* Class representing a Hash utility for generating unique hash values.
*
* @class Hash
*/
class Hash {
/**
* Initializes the Hash class by creating a new Set to store unique hashes.
*/
static init() {
uniqueHashes = new Set();
}
/**
* Checks if a given hash is not unique by verifying if it exists in the Set of unique hashes.
*
* @param {string} hash - The hash to check for uniqueness.
* @return {boolean} - Returns true if the hash is not unique, false otherwise.
*/
static exists( hash ) {
return uniqueHashes.has( hash );
}
/**
* Generates a unique hash based on the input hash by appending a suffix if necessary.
*
* @param {string} hash - The base hash to make unique.
* @return {string} - A unique hash derived from the input hash.
*/
static makeUnique( hash ) {
const match = hash.match( /^(.+)_([0-9]+)$/ );
let suffix = match ? parseInt( match[ 2 ], 10 ) + 1 : 1;
const initialHash = hash;
let uniqueHash = `${ initialHash }_${ suffix }`;
// Increment suffix and generate a new unique hash until a unique one is found
while ( Hash.exists( uniqueHash ) ) {
suffix++;
uniqueHash = `${ initialHash }_${ suffix }`;
}
return uniqueHash;
}
/**
* Builds a unique hash based on the provided title text.
*
* @param {string} titleText - The title text to generate the hash from.
* @return {string} - A unique hash created from the title text.
*/
static build( titleText ) {
let hash = mw.util.escapeIdForAttribute( titleText );
if ( Hash.exists( hash ) ) {
hash = Hash.makeUnique( hash );
}
uniqueHashes.add( hash );
return hash;
}
/**
* Clears the Set of unique hashes, removing all stored hashes.
*/
static clear() {
uniqueHashes.clear();
}
}
/**
* Utility class with methods for common utility functions.
*
* @class Util
*/
class Util {
/**
* Extracts text content from the given HTML string.
*
* @param {string} html - The HTML string to extract text content from.
* @return {string} The extracted text content.
*/
static extractTextFromHtml( html ) {
const tmp = document.createElement( 'div' );
tmp.innerHTML = html;
return tmp.textContent;
}
/**
* Returns the size (width or height) of the provided element.
* Required to calculate the size of hidden elements (e.g. nested tabs)
*
* @param {Element} element - The element for which to get the size.
* @param {string} type - The type of size to retrieve ('width' or 'height').
* @return {number} The actual size of the element based on the specified type.
*/
static getElementSize( element, type ) {
if ( !element || !( element instanceof Element ) || ( type !== 'width' && type !== 'height' ) ) {
mw.log.error( '[TabberNeue] Invalid element or type provided for getElementSize' );
return 0;
}
let value = element.getBoundingClientRect()[ type ];
if ( value === 0 ) {
value = this.getHiddenElementSize( element, type );
}
return value;
}
/**
* Retrieves the size of a hidden element by cloning it and calculating the size.
*
* @param {Element} element - The hidden element to retrieve the size from.
* @param {string} type - The type of size to retrieve ('width' or 'height').
* @return {number} The size of the hidden element based on the specified type.
*/
static getHiddenElementSize( element, type ) {
const shadowRoot = document.createElement( 'div' ).attachShadow( { mode: 'open' } );
const clone = element.cloneNode( true );
clone.style.position = 'absolute';
clone.style.visibility = 'hidden';
shadowRoot.appendChild( clone );
try {
const value = clone.getBoundingClientRect()[ type ];
return value;
} finally {
clone.parentNode.removeChild( clone );
}
}
/**
* Rounds the scrollLeft value to the nearest integer using Math.ceil.
* Used to avoid the fractional pixel issue caused by different browser implementations
*
* @param {number} val - The scrollLeft value to be rounded.
* @return {number} The rounded scrollLeft value.
*/
static roundScrollLeft( val ) {
return Math.ceil( val );
}
/**
* Sets the attributes of the given element based on the provided attributes object.
*
* @param {Element} element - The element to set attributes for.
* @param {Object} attributes - An object containing key-value pairs of attributes to set.
*/
static setAttributes( element, attributes ) {
for ( const key in attributes ) {
element.setAttribute( key, attributes[ key ] );
}
}
}
/**
* Represents a class that handles transcluding content for a tab within a tabber component.
*
* @class TabberTransclude
*/
class TabberTransclude {
constructor( activeTabpanel, cacheExpiration = 3600 ) {
this.activeTabpanel = activeTabpanel;
this.pageTitle = this.activeTabpanel.dataset.mwTabberPageTitle;
this.url = this.activeTabpanel.dataset.mwTabberLoadUrl;
this.cacheKey = `tabber-transclude-${ encodeURIComponent( this.pageTitle ) }_v1`;
this.cacheExpiration = cacheExpiration;
}
/**
* Validates the URL format.
*
* @return {Promise} A Promise that resolves if the URL is valid, and rejects with an Error if the URL is empty, null, or in an invalid format.
*/
validateUrl() {
const urlPattern = /^(https?):\/\/[^\s/$.?#][^\s]*$/;
if ( !this.url || this.url.trim() === '' ) {
return Promise.reject( new Error( '[TabberNeue] URL is empty or null' ) );
}
if ( !urlPattern.test( this.url ) ) {
return Promise.reject( new Error( `[TabberNeue] Invalid URL format : ${ this.url }` ) );
}
return Promise.resolve();
}
/**
* Checks the session storage for cached data using the cache key.
*
* @return {Object|null} The cached data if found, or null if no cached data is found.
*/
checkCache() {
const cachedData = mw.storage.session.getObject( this.cacheKey );
if ( cachedData ) {
return cachedData;
}
return null;
}
/**
* Fetches data from the specified URL using a GET request.
*
* @return {Promise} A Promise that resolves with the response text if the network request is successful,
* and rejects with an Error if there is an issue with the network request.
*/
async fetchDataFromUrl() {
try {
const response = await fetch( this.url, { method: 'GET', timeout: 5000, credentials: 'same-origin' } );
if ( !response.ok ) {
throw new Error( `Network response was not ok: ${ response.status } - ${ response.statusText }` );
}
return Promise.resolve( response.text() );
} catch ( error ) {
return Promise.reject( `[TabberNeue] Error fetching data from URL: ${ this.url }`, error );
}
}
/**
* Parses the JSON data and extracts the 'parse.text' property.
*
* @param {string} data - The JSON data to be parsed.
* @return {string} The parsed 'parse.text' property from the JSON data.
* @throws {Error} If an error occurs while parsing the JSON data.
*/
parseData( data ) {
let parsedData;
try {
parsedData = JSON.parse( data );
parsedData = parsedData.parse.text;
} catch ( error ) {
mw.log.error( `[TabberNeue] Error occurred while parsing JSON data: ${ error }` );
return Promise.reject( new Error( `Error parsing JSON data: ${ error }` ) );
}
return parsedData;
}
/**
* Caches the parsed data in the session storage using the cache key.
*
* @param {string} parsedData - The parsed data to be cached.
* @return {string} The cached parsed data.
*/
cacheData( parsedData ) {
mw.storage.session.setObject( this.cacheKey, parsedData, this.cacheExpiration );
return parsedData;
}
/**
* Fetches data by validating the URL, checking the cache, fetching data from the URL,
* parsing the data, and caching the parsed data if not found in the cache.
*
* @return {Promise} A Promise that resolves with the fetched and cached data,
* or rejects with an error message if any step fails.
*/
async fetchData() {
try {
await this.validateUrl();
const cachedData = this.checkCache();
if ( cachedData ) {
return cachedData;
}
const data = await this.fetchDataFromUrl();
const parsedData = this.parseData( data );
return this.cacheData( parsedData );
} catch ( error ) {
return Promise.reject( `[TabberNeue] Error fetching data: ${ error }` );
}
}
/**
* Loads the page content by fetching data, updating the active tab panel's content,
* and handling errors if data fetching fails.
*
* @return {void}
*/
async loadPage() {
try {
this.activeTabpanel.classList.add( 'tabber__panel--loading' );
const data = await this.fetchData();
if ( data ) {
delete this.activeTabpanel.dataset.mwTabberLoadUrl;
this.activeTabpanel.classList.remove( 'tabber__panel--loading' );
this.activeTabpanel.innerHTML = data;
} else {
mw.log.error( `[TabberNeue] No valid API response or missing 'parse' field for ${ this.pageTitle } from: ${ this.url }` );
}
} catch ( error ) {
mw.log.error( `[TabberNeue] Failed to load data for ${ this.pageTitle }: ${ error }` );
}
}
}
/**
* Class representing TabberEvent functionality for handling tab events and animations.
*
@ -388,8 +97,8 @@ class TabberEvent {
const section = activeTabpanel.closest( '.tabber__section' );
if ( activeTabpanel.dataset.mwTabberLoadUrl ) {
const tabberTransclude = new TabberTransclude( activeTabpanel );
tabberTransclude.loadPage();
const transclude = new Transclude( activeTabpanel );
transclude.loadPage();
}
window.requestAnimationFrame( () => {