2023-12-02 19:04:51 +01:00
import { getContext } from './extensions.js' ;
2024-06-26 05:35:41 +02:00
import { getRequestHeaders } from '../script.js' ;
2023-12-02 19:04:51 +01:00
import { isMobile } from './RossAscends-mods.js' ;
import { collapseNewlines } from './power-user.js' ;
2024-04-28 18:47:53 +02:00
import { debounce _timeout } from './constants.js' ;
2024-06-26 05:35:41 +02:00
import { Popup } from './popup.js' ;
2023-07-20 19:32:15 +02:00
2023-08-22 21:45:12 +02:00
/ * *
* Pagination status string template .
* @ type { string }
* /
2023-08-21 20:10:11 +02:00
export const PAGINATION _TEMPLATE = '<%= rangeStart %>-<%= rangeEnd %> of <%= totalNumber %>' ;
2023-08-22 21:45:12 +02:00
/ * *
* Navigation options for pagination .
* @ enum { number }
* /
2023-12-02 21:06:57 +01:00
export const navigation _option = {
none : - 2000 ,
previous : - 1000 ,
} ;
2023-08-22 21:45:12 +02:00
2023-08-30 11:03:18 +02:00
export function escapeHtml ( str ) {
return String ( str ) . replace ( /&/g , '&' ) . replace ( /</g , '<' ) . replace ( />/g , '>' ) . replace ( /"/g , '"' ) ;
}
2023-10-15 23:50:29 +02:00
export function isValidUrl ( value ) {
2023-09-08 21:44:06 +02:00
try {
new URL ( value ) ;
return true ;
} catch ( _ ) {
return false ;
}
}
2023-11-08 22:04:32 +01:00
/ * *
* Parses ranges like 10 - 20 or 10.
* Range is inclusive . Start must be less than end .
* Returns null if invalid .
* @ param { string } input The input string .
* @ param { number } min The minimum value .
* @ param { number } max The maximum value .
* @ returns { { start : number , end : number } } The parsed range .
* /
export function stringToRange ( input , min , max ) {
let start , end ;
2024-03-15 15:31:43 +01:00
if ( typeof input !== 'string' ) {
input = String ( input ) ;
}
2023-11-08 22:04:32 +01:00
if ( input . includes ( '-' ) ) {
const parts = input . split ( '-' ) ;
start = parts [ 0 ] ? parseInt ( parts [ 0 ] , 10 ) : NaN ;
end = parts [ 1 ] ? parseInt ( parts [ 1 ] , 10 ) : NaN ;
} else {
start = end = parseInt ( input , 10 ) ;
}
if ( isNaN ( start ) || isNaN ( end ) || start > end || start < min || end > max ) {
return null ;
}
return { start , end } ;
}
2023-08-22 21:45:12 +02:00
/ * *
* Determines if a value is unique in an array .
* @ param { any } value Current value .
* @ param { number } index Current index .
* @ param { any } array The array being processed .
* @ returns { boolean } True if the value is unique , false otherwise .
* /
2023-07-20 19:32:15 +02:00
export function onlyUnique ( value , index , array ) {
return array . indexOf ( value ) === index ;
}
2024-05-27 03:35:03 +02:00
/ * *
* Removes the first occurrence of a specified item from an array
*
* @ param { * [ ] } array - The array from which to remove the item
* @ param { * } item - The item to remove from the array
* @ returns { boolean } - Returns true if the item was successfully removed , false otherwise .
* /
export function removeFromArray ( array , item ) {
const index = array . indexOf ( item ) ;
if ( index === - 1 ) return false ;
array . splice ( index , 1 ) ;
return true ;
}
2023-08-22 12:07:24 +02:00
/ * *
* Checks if a string only contains digits .
* @ param { string } str The string to check .
* @ returns { boolean } True if the string only contains digits , false otherwise .
* @ example
* isDigitsOnly ( '123' ) ; // true
* isDigitsOnly ( 'abc' ) ; // false
* /
2023-08-08 21:36:42 +02:00
export function isDigitsOnly ( str ) {
return /^\d+$/ . test ( str ) ;
}
2023-08-22 21:45:12 +02:00
/ * *
* Gets a drag delay for sortable elements . This is to prevent accidental drags when scrolling .
2023-08-24 22:52:03 +02:00
* @ returns { number } The delay in milliseconds . 50 ms for desktop , 750 ms for mobile .
2023-08-22 21:45:12 +02:00
* /
2023-08-18 12:41:46 +02:00
export function getSortableDelay ( ) {
2023-08-24 22:52:03 +02:00
return isMobile ( ) ? 750 : 50 ;
2023-08-18 12:41:46 +02:00
}
2023-08-27 17:27:34 +02:00
export async function bufferToBase64 ( buffer ) {
// use a FileReader to generate a base64 data URI:
const base64url = await new Promise ( resolve => {
2023-12-02 20:11:06 +01:00
const reader = new FileReader ( ) ;
reader . onload = ( ) => resolve ( reader . result ) ;
reader . readAsDataURL ( new Blob ( [ buffer ] ) ) ;
2023-08-27 17:27:34 +02:00
} ) ;
// remove the `data:...;base64,` part from the start
return base64url . slice ( base64url . indexOf ( ',' ) + 1 ) ;
}
2023-08-22 12:07:24 +02:00
/ * *
* Rearranges an array in a random order .
* @ param { any [ ] } array The array to shuffle .
* @ returns { any [ ] } The shuffled array .
* @ example
* shuffle ( [ 1 , 2 , 3 ] ) ; // [2, 3, 1]
* /
2023-07-20 19:32:15 +02:00
export function shuffle ( array ) {
let currentIndex = array . length ,
randomIndex ;
while ( currentIndex != 0 ) {
randomIndex = Math . floor ( Math . random ( ) * currentIndex ) ;
currentIndex -- ;
[ array [ currentIndex ] , array [ randomIndex ] ] = [
array [ randomIndex ] ,
array [ currentIndex ] ,
] ;
}
return array ;
}
2023-08-22 12:07:24 +02:00
/ * *
* Downloads a file to the user ' s devices .
* @ param { BlobPart } content File content to download .
* @ param { string } fileName File name .
* @ param { string } contentType File content type .
* /
2023-07-20 19:32:15 +02:00
export function download ( content , fileName , contentType ) {
2023-12-02 19:04:51 +01:00
const a = document . createElement ( 'a' ) ;
2023-07-20 19:32:15 +02:00
const file = new Blob ( [ content ] , { type : contentType } ) ;
a . href = URL . createObjectURL ( file ) ;
a . download = fileName ;
a . click ( ) ;
2024-05-22 00:36:38 +02:00
URL . revokeObjectURL ( a . href ) ;
2023-07-20 19:32:15 +02:00
}
2023-08-22 21:45:12 +02:00
/ * *
* Fetches a file by URL and parses its contents as data URI .
* @ param { string } url The URL to fetch .
* @ param { any } params Fetch parameters .
* @ returns { Promise < string > } A promise that resolves to the data URI .
* /
2023-07-20 19:32:15 +02:00
export async function urlContentToDataUri ( url , params ) {
const response = await fetch ( url , params ) ;
const blob = await response . blob ( ) ;
2023-08-22 21:45:12 +02:00
return await new Promise ( ( resolve , reject ) => {
const reader = new FileReader ( ) ;
reader . onload = function ( ) {
resolve ( String ( reader . result ) ) ;
} ;
reader . onerror = function ( error ) {
reject ( error ) ;
} ;
2023-07-20 19:32:15 +02:00
reader . readAsDataURL ( blob ) ;
} ) ;
}
2023-08-22 12:07:24 +02:00
/ * *
* Returns a promise that resolves to the file ' s text .
* @ param { Blob } file The file to read .
* @ returns { Promise < string > } A promise that resolves to the file ' s text .
* /
2023-07-20 19:32:15 +02:00
export function getFileText ( file ) {
return new Promise ( ( resolve , reject ) => {
const reader = new FileReader ( ) ;
reader . readAsText ( file ) ;
reader . onload = function ( ) {
2023-08-22 12:07:24 +02:00
resolve ( String ( reader . result ) ) ;
2023-07-20 19:32:15 +02:00
} ;
reader . onerror = function ( error ) {
reject ( error ) ;
} ;
} ) ;
}
2023-08-22 12:07:24 +02:00
/ * *
* Returns a promise that resolves to the file ' s array buffer .
* @ param { Blob } file The file to read .
* /
2023-07-20 19:32:15 +02:00
export function getFileBuffer ( file ) {
return new Promise ( ( resolve , reject ) => {
const reader = new FileReader ( ) ;
reader . readAsArrayBuffer ( file ) ;
reader . onload = function ( ) {
resolve ( reader . result ) ;
} ;
reader . onerror = function ( error ) {
reject ( error ) ;
} ;
} ) ;
}
2023-08-22 12:07:24 +02:00
/ * *
* Returns a promise that resolves to the base64 encoded string of a file .
* @ param { Blob } file The file to read .
* @ returns { Promise < string > } A promise that resolves to the base64 encoded string .
* /
2023-07-20 19:32:15 +02:00
export function getBase64Async ( file ) {
return new Promise ( ( resolve , reject ) => {
const reader = new FileReader ( ) ;
reader . readAsDataURL ( file ) ;
reader . onload = function ( ) {
2023-08-22 12:07:24 +02:00
resolve ( String ( reader . result ) ) ;
2023-07-20 19:32:15 +02:00
} ;
reader . onerror = function ( error ) {
reject ( error ) ;
} ;
} ) ;
}
2023-08-22 12:07:24 +02:00
/ * *
* Parses a file blob as a JSON object .
* @ param { Blob } file The file to read .
* @ returns { Promise < any > } A promise that resolves to the parsed JSON object .
* /
2023-07-20 19:32:15 +02:00
export async function parseJsonFile ( file ) {
return new Promise ( ( resolve , reject ) => {
const fileReader = new FileReader ( ) ;
fileReader . readAsText ( file ) ;
2023-08-22 12:07:24 +02:00
fileReader . onload = event => resolve ( JSON . parse ( String ( event . target . result ) ) ) ;
fileReader . onerror = error => reject ( error ) ;
2023-07-20 19:32:15 +02:00
} ) ;
}
2023-08-22 12:07:24 +02:00
/ * *
* Calculates a hash code for a string .
* @ param { string } str The string to hash .
* @ param { number } [ seed = 0 ] The seed to use for the hash .
* @ returns { number } The hash code .
* /
2023-07-20 19:32:15 +02:00
export function getStringHash ( str , seed = 0 ) {
if ( typeof str !== 'string' ) {
return 0 ;
}
let h1 = 0xdeadbeef ^ seed ,
h2 = 0x41c6ce57 ^ seed ;
for ( let i = 0 , ch ; i < str . length ; i ++ ) {
ch = str . charCodeAt ( i ) ;
h1 = Math . imul ( h1 ^ ch , 2654435761 ) ;
h2 = Math . imul ( h2 ^ ch , 1597334677 ) ;
}
h1 = Math . imul ( h1 ^ ( h1 >>> 16 ) , 2246822507 ) ^ Math . imul ( h2 ^ ( h2 >>> 13 ) , 3266489909 ) ;
h2 = Math . imul ( h2 ^ ( h2 >>> 16 ) , 2246822507 ) ^ Math . imul ( h1 ^ ( h1 >>> 13 ) , 3266489909 ) ;
return 4294967296 * ( 2097151 & h2 ) + ( h1 >>> 0 ) ;
2023-12-02 16:15:03 +01:00
}
2023-07-20 19:32:15 +02:00
2023-08-22 12:07:24 +02:00
/ * *
* Creates a debounced function that delays invoking func until after wait milliseconds have elapsed since the last time the debounced function was invoked .
* @ param { function } func The function to debounce .
2024-04-28 06:21:47 +02:00
* @ param { debounce _timeout | number } [ timeout = debounce _timeout . default ] The timeout based on the common enum values , or in milliseconds .
2023-08-22 12:07:24 +02:00
* @ returns { function } The debounced function .
* /
2024-04-28 06:21:47 +02:00
export function debounce ( func , timeout = debounce _timeout . standard ) {
2023-07-20 19:32:15 +02:00
let timer ;
return ( ... args ) => {
clearTimeout ( timer ) ;
timer = setTimeout ( ( ) => { func . apply ( this , args ) ; } , timeout ) ;
} ;
}
2023-08-22 12:07:24 +02:00
/ * *
* Creates a throttled function that only invokes func at most once per every limit milliseconds .
* @ param { function } func The function to throttle .
* @ param { number } [ limit = 300 ] The limit in milliseconds .
* @ returns { function } The throttled function .
* /
2023-07-20 19:32:15 +02:00
export function throttle ( func , limit = 300 ) {
let lastCall ;
return ( ... args ) => {
const now = Date . now ( ) ;
if ( ! lastCall || ( now - lastCall ) >= limit ) {
lastCall = now ;
func . apply ( this , args ) ;
}
} ;
}
2024-07-07 19:12:04 +02:00
/ * *
* Creates a debounced throttle function that only invokes func at most once per every limit milliseconds .
* @ param { function } func The function to throttle .
* @ param { number } [ limit = 300 ] The limit in milliseconds .
* @ returns { function } The throttled function .
* /
export function debouncedThrottle ( func , limit = 300 ) {
let last , deferTimer ;
let db = debounce ( func ) ;
return function ( ) {
let now = + new Date , args = arguments ;
if ( ! last || ( last && now < last + limit ) ) {
clearTimeout ( deferTimer ) ;
db . apply ( this , args ) ;
deferTimer = setTimeout ( function ( ) {
last = now ;
func . apply ( this , args ) ;
} , limit ) ;
} else {
last = now ;
func . apply ( this , args ) ;
}
} ;
}
2023-08-22 12:07:24 +02:00
/ * *
* Checks if an element is in the viewport .
2023-08-22 21:45:12 +02:00
* @ param { Element } el The element to check .
2023-08-22 12:07:24 +02:00
* @ returns { boolean } True if the element is in the viewport , false otherwise .
* /
2023-07-20 19:32:15 +02:00
export function isElementInViewport ( el ) {
2024-06-16 13:56:08 +02:00
if ( ! el ) {
return false ;
}
2023-12-02 19:04:51 +01:00
if ( typeof jQuery === 'function' && el instanceof jQuery ) {
2023-07-20 19:32:15 +02:00
el = el [ 0 ] ;
}
var rect = el . getBoundingClientRect ( ) ;
return (
rect . top >= 0 &&
rect . left >= 0 &&
rect . bottom <= ( window . innerHeight || document . documentElement . clientHeight ) && /* or $(window).height() */
rect . right <= ( window . innerWidth || document . documentElement . clientWidth ) /* or $(window).width() */
) ;
}
2023-08-22 12:07:24 +02:00
/ * *
* Returns a name that is unique among the names that exist .
* @ param { string } name The name to check .
* @ param { { ( y : any ) : boolean ; } } exists Function to check if name exists .
* @ returns { string } A unique name .
* /
2023-07-20 19:32:15 +02:00
export function getUniqueName ( name , exists ) {
let i = 1 ;
let baseName = name ;
while ( exists ( name ) ) {
name = ` ${ baseName } ( ${ i } ) ` ;
i ++ ;
}
return name ;
}
2023-08-22 12:07:24 +02:00
/ * *
* Returns a promise that resolves after the specified number of milliseconds .
* @ param { number } ms The number of milliseconds to wait .
* @ returns { Promise < void > } A promise that resolves after the specified number of milliseconds .
* /
export function delay ( ms ) {
return new Promise ( ( res ) => setTimeout ( res , ms ) ) ;
}
2023-07-20 19:32:15 +02:00
2023-08-22 12:07:24 +02:00
/ * *
* Checks if an array is a subset of another array .
* @ param { any [ ] } a Array A
* @ param { any [ ] } b Array B
* @ returns { boolean } True if B is a subset of A , false otherwise .
* /
export function isSubsetOf ( a , b ) {
return ( Array . isArray ( a ) && Array . isArray ( b ) ) ? b . every ( val => a . includes ( val ) ) : false ;
}
2023-07-20 19:32:15 +02:00
2023-08-22 12:07:24 +02:00
/ * *
* Increments the trailing number in a string .
* @ param { string } str The string to process .
* @ returns { string } The string with the trailing number incremented by 1.
* @ example
* incrementString ( 'Hello, world! 1' ) ; // 'Hello, world! 2'
* /
2023-07-20 19:32:15 +02:00
export function incrementString ( str ) {
// Find the trailing number or it will match the empty string
const count = str . match ( /\d*$/ ) ;
// Take the substring up until where the integer was matched
// Concatenate it to the matched count incremented by 1
2023-08-22 12:07:24 +02:00
return str . substring ( 0 , count . index ) + ( Number ( count [ 0 ] ) + 1 ) ;
2023-12-02 16:15:03 +01:00
}
2023-07-20 19:32:15 +02:00
2023-08-22 12:07:24 +02:00
/ * *
* Formats a string using the specified arguments .
* @ param { string } format The format string .
* @ returns { string } The formatted string .
* @ example
* stringFormat ( 'Hello, {0}!' , 'world' ) ; // 'Hello, world!'
* /
2023-07-20 19:32:15 +02:00
export function stringFormat ( format ) {
const args = Array . prototype . slice . call ( arguments , 1 ) ;
return format . replace ( /{(\d+)}/g , function ( match , number ) {
return typeof args [ number ] != 'undefined'
? args [ number ]
2023-12-02 20:56:16 +01:00
: match ;
2023-07-20 19:32:15 +02:00
} ) ;
2023-12-02 16:15:03 +01:00
}
2023-07-20 19:32:15 +02:00
2023-08-22 12:07:24 +02:00
/ * *
* Save the caret position in a contenteditable element .
* @ param { Element } element The element to save the caret position of .
* @ returns { { start : number , end : number } } An object with the start and end offsets of the caret .
* /
2023-07-20 19:32:15 +02:00
export function saveCaretPosition ( element ) {
// Get the current selection
const selection = window . getSelection ( ) ;
// If the selection is empty, return null
if ( selection . rangeCount === 0 ) {
return null ;
}
// Get the range of the current selection
const range = selection . getRangeAt ( 0 ) ;
// If the range is not within the specified element, return null
if ( ! element . contains ( range . commonAncestorContainer ) ) {
return null ;
}
// Return an object with the start and end offsets of the range
const position = {
start : range . startOffset ,
2023-12-02 21:06:57 +01:00
end : range . endOffset ,
2023-07-20 19:32:15 +02:00
} ;
console . debug ( 'Caret saved' , position ) ;
return position ;
}
2023-08-22 12:07:24 +02:00
/ * *
* Restore the caret position in a contenteditable element .
* @ param { Element } element The element to restore the caret position of .
* @ param { { start : any ; end : any ; } } position An object with the start and end offsets of the caret .
* /
2023-07-20 19:32:15 +02:00
export function restoreCaretPosition ( element , position ) {
// If the position is null, do nothing
if ( ! position ) {
return ;
}
console . debug ( 'Caret restored' , position ) ;
// Create a new range object
const range = new Range ( ) ;
// Set the start and end positions of the range within the element
range . setStart ( element . childNodes [ 0 ] , position . start ) ;
range . setEnd ( element . childNodes [ 0 ] , position . end ) ;
// Create a new selection object and set the range
const selection = window . getSelection ( ) ;
selection . removeAllRanges ( ) ;
selection . addRange ( range ) ;
}
export async function resetScrollHeight ( element ) {
$ ( element ) . css ( 'height' , '0px' ) ;
$ ( element ) . css ( 'height' , $ ( element ) . prop ( 'scrollHeight' ) + 3 + 'px' ) ;
}
2023-08-22 12:07:24 +02:00
/ * *
* Sets the height of an element to its scroll height .
* @ param { JQuery < HTMLElement > } element The element to initialize the scroll height of .
* @ returns { Promise < void > } A promise that resolves when the scroll height has been initialized .
* /
2023-07-20 19:32:15 +02:00
export async function initScrollHeight ( element ) {
await delay ( 1 ) ;
2023-12-02 19:04:51 +01:00
const curHeight = Number ( $ ( element ) . css ( 'height' ) . replace ( 'px' , '' ) ) ;
const curScrollHeight = Number ( $ ( element ) . prop ( 'scrollHeight' ) ) ;
2023-07-20 19:32:15 +02:00
const diff = curScrollHeight - curHeight ;
2023-12-02 20:11:06 +01:00
if ( diff < 3 ) { return ; } //happens when the div isn't loaded yet
2023-07-20 19:32:15 +02:00
const newHeight = curHeight + diff + 3 ; //the +3 here is to account for padding/line-height on text inputs
//console.log(`init height to ${newHeight}`);
2023-12-02 19:04:51 +01:00
$ ( element ) . css ( 'height' , '' ) ;
$ ( element ) . css ( 'height' , ` ${ newHeight } px ` ) ;
2023-07-20 19:32:15 +02:00
//resetScrollHeight(element);
}
2023-08-22 12:07:24 +02:00
/ * *
* Compares elements by their CSS order property . Used for sorting .
* @ param { any } a The first element .
* @ param { any } b The second element .
* @ returns { number } A negative number if a is before b , a positive number if a is after b , or 0 if they are equal .
* /
2023-07-20 19:32:15 +02:00
export function sortByCssOrder ( a , b ) {
const _a = Number ( $ ( a ) . css ( 'order' ) ) ;
const _b = Number ( $ ( b ) . css ( 'order' ) ) ;
return _a - _b ;
}
2023-08-22 12:07:24 +02:00
/ * *
* Trims a string to the end of a nearest sentence .
* @ param { string } input The string to trim .
* @ param { boolean } include _newline Whether to include a newline character in the trimmed string .
* @ returns { string } The trimmed string .
* @ example
2023-09-09 16:31:27 +02:00
* trimToEndSentence ( 'Hello, world! I am from' ) ; // 'Hello, world!'
2023-08-22 12:07:24 +02:00
* /
2023-09-09 16:31:27 +02:00
export function trimToEndSentence ( input , include _newline = false ) {
2024-06-06 20:45:47 +02:00
if ( ! input ) {
return '' ;
}
2024-06-11 22:37:00 +02:00
const isEmoji = x => / ( \ p { Emoji _Presentation } | \ p { Extended _Pictographic } ) / gu . test ( x ) ;
2024-05-30 01:29:28 +02:00
const punctuation = new Set ( [ '.' , '!' , '?' , '*' , '"' , ')' , '}' , '`' , ']' , '$' , '。' , '! ' , '? ' , '”' , ') ' , '】' , '’ ' , '」' , '_' ] ) ; // extend this as you see fit
2023-07-20 19:32:15 +02:00
let last = - 1 ;
2024-06-11 22:37:00 +02:00
const characters = Array . from ( input ) ;
for ( let i = characters . length - 1 ; i >= 0 ; i -- ) {
const char = characters [ i ] ;
2023-07-20 19:32:15 +02:00
2024-06-11 22:37:00 +02:00
if ( punctuation . has ( char ) || isEmoji ( char ) ) {
if ( i > 0 && /[\s\n]/ . test ( characters [ i - 1 ] ) ) {
2024-02-02 17:42:31 +01:00
last = i - 1 ;
} else {
last = i ;
}
2023-07-20 19:32:15 +02:00
break ;
}
if ( include _newline && char === '\n' ) {
last = i ;
break ;
}
}
if ( last === - 1 ) {
return input . trimEnd ( ) ;
}
2024-06-11 22:37:00 +02:00
return characters . slice ( 0 , last + 1 ) . join ( '' ) . trimEnd ( ) ;
2023-07-20 19:32:15 +02:00
}
2023-09-09 16:31:27 +02:00
export function trimToStartSentence ( input ) {
2024-06-06 20:45:47 +02:00
if ( ! input ) {
return '' ;
}
2023-12-02 19:04:51 +01:00
let p1 = input . indexOf ( '.' ) ;
let p2 = input . indexOf ( '!' ) ;
let p3 = input . indexOf ( '?' ) ;
let p4 = input . indexOf ( '\n' ) ;
2023-09-09 16:31:27 +02:00
let first = p1 ;
let skip1 = false ;
if ( p2 > 0 && p2 < first ) { first = p2 ; }
if ( p3 > 0 && p3 < first ) { first = p3 ; }
if ( p4 > 0 && p4 < first ) { first = p4 ; skip1 = true ; }
if ( first > 0 ) {
if ( skip1 ) {
return input . substring ( first + 1 ) ;
} else {
return input . substring ( first + 2 ) ;
}
}
return input ;
}
2023-11-18 18:23:58 +01:00
/ * *
* Format bytes as human - readable text .
*
* @ param bytes Number of bytes .
* @ param si True to use metric ( SI ) units , aka powers of 1000. False to use
* binary ( IEC ) , aka powers of 1024.
* @ param dp Number of decimal places to display .
*
* @ return Formatted string .
* /
export function humanFileSize ( bytes , si = false , dp = 1 ) {
const thresh = si ? 1000 : 1024 ;
if ( Math . abs ( bytes ) < thresh ) {
return bytes + ' B' ;
}
const units = si
? [ 'kB' , 'MB' , 'GB' , 'TB' , 'PB' , 'EB' , 'ZB' , 'YB' ]
: [ 'KiB' , 'MiB' , 'GiB' , 'TiB' , 'PiB' , 'EiB' , 'ZiB' , 'YiB' ] ;
let u = - 1 ;
const r = 10 * * dp ;
do {
bytes /= thresh ;
++ u ;
} while ( Math . round ( Math . abs ( bytes ) * r ) / r >= thresh && u < units . length - 1 ) ;
return bytes . toFixed ( dp ) + ' ' + units [ u ] ;
}
2023-08-22 12:07:24 +02:00
/ * *
* Counts the number of occurrences of a character in a string .
* @ param { string } string The string to count occurrences in .
* @ param { string } character The character to count occurrences of .
* @ returns { number } The number of occurrences of the character in the string .
* @ example
* countOccurrences ( 'Hello, world!' , 'l' ) ; // 3
* countOccurrences ( 'Hello, world!' , 'x' ) ; // 0
* /
2023-07-20 19:32:15 +02:00
export function countOccurrences ( string , character ) {
let count = 0 ;
for ( let i = 0 ; i < string . length ; i ++ ) {
2023-10-20 22:52:23 +02:00
if ( string . substring ( i , i + character . length ) === character ) {
2023-07-20 19:32:15 +02:00
count ++ ;
}
}
return count ;
}
2023-11-24 14:18:49 +01:00
/ * *
* Checks if a string is "true" value .
* @ param { string } arg String to check
* @ returns { boolean } True if the string is true , false otherwise .
* /
export function isTrueBoolean ( arg ) {
2023-12-01 23:04:38 +01:00
return [ 'on' , 'true' , '1' ] . includes ( arg ? . trim ( ) ? . toLowerCase ( ) ) ;
2023-11-24 14:18:49 +01:00
}
/ * *
* Checks if a string is "false" value .
* @ param { string } arg String to check
* @ returns { boolean } True if the string is false , false otherwise .
* /
export function isFalseBoolean ( arg ) {
2023-12-01 23:04:38 +01:00
return [ 'off' , 'false' , '0' ] . includes ( arg ? . trim ( ) ? . toLowerCase ( ) ) ;
2023-11-24 14:18:49 +01:00
}
2024-06-23 18:43:56 +02:00
/ * *
* Parses an array either as a comma - separated string or as a JSON array .
* @ param { string } value String to parse
* @ returns { string [ ] } The parsed array .
* /
export function parseStringArray ( value ) {
if ( ! value || typeof value !== 'string' ) return [ ] ;
try {
const parsedValue = JSON . parse ( value ) ;
if ( ! Array . isArray ( parsedValue ) ) {
throw new Error ( 'Not an array' ) ;
}
return parsedValue . map ( x => String ( x ) ) ;
} catch ( e ) {
return value . split ( ',' ) . map ( x => x . trim ( ) ) . filter ( x => x ) ;
}
}
2023-08-22 12:07:24 +02:00
/ * *
* Checks if a number is odd .
* @ param { number } number The number to check .
* @ returns { boolean } True if the number is odd , false otherwise .
* @ example
* isOdd ( 3 ) ; // true
* isOdd ( 4 ) ; // false
* /
2023-07-20 19:32:15 +02:00
export function isOdd ( number ) {
return number % 2 !== 0 ;
}
2024-03-12 19:29:07 +01:00
const dateCache = new Map ( ) ;
/ * *
* Cached version of moment ( ) to avoid re - parsing the same date strings .
2024-03-12 19:45:30 +01:00
* Important : Moment objects are mutable , so use clone ( ) before modifying them !
2024-05-11 13:49:11 +02:00
* @ param { string | number } timestamp String or number representing a date .
* @ returns { moment . Moment } Moment object
2024-03-12 19:29:07 +01:00
* /
2023-07-20 19:32:15 +02:00
export function timestampToMoment ( timestamp ) {
2024-03-12 19:29:07 +01:00
if ( dateCache . has ( timestamp ) ) {
return dateCache . get ( timestamp ) ;
}
const moment = parseTimestamp ( timestamp ) ;
dateCache . set ( timestamp , moment ) ;
return moment ;
}
function parseTimestamp ( timestamp ) {
2023-07-20 19:32:15 +02:00
if ( ! timestamp ) {
return moment . invalid ( ) ;
}
2023-11-10 20:56:25 +01:00
// Unix time (legacy TAI / tags)
2024-06-09 10:15:17 +02:00
if ( typeof timestamp === 'number' || /^\d+$/ . test ( timestamp ) ) {
2024-06-01 21:07:57 +02:00
if ( isNaN ( timestamp ) || ! isFinite ( timestamp ) || timestamp < 0 ) {
return moment . invalid ( ) ;
}
2024-06-09 13:41:49 +02:00
return moment ( Number ( timestamp ) ) ;
2023-07-20 19:32:15 +02:00
}
// ST "humanized" format pattern
const pattern1 = /(\d{4})-(\d{1,2})-(\d{1,2}) @(\d{1,2})h (\d{1,2})m (\d{1,2})s (\d{1,3})ms/ ;
const replacement1 = ( match , year , month , day , hour , minute , second , millisecond ) => {
2023-12-02 19:04:51 +01:00
return ` ${ year . padStart ( 4 , '0' ) } - ${ month . padStart ( 2 , '0' ) } - ${ day . padStart ( 2 , '0' ) } T ${ hour . padStart ( 2 , '0' ) } : ${ minute . padStart ( 2 , '0' ) } : ${ second . padStart ( 2 , '0' ) } . ${ millisecond . padStart ( 3 , '0' ) } Z ` ;
2023-07-20 19:32:15 +02:00
} ;
const isoTimestamp1 = timestamp . replace ( pattern1 , replacement1 ) ;
if ( moment ( isoTimestamp1 ) . isValid ( ) ) {
return moment ( isoTimestamp1 ) ;
}
// New format pattern: "June 19, 2023 4:13pm"
const pattern2 = /(\w+)\s(\d{1,2}),\s(\d{4})\s(\d{1,2}):(\d{1,2})(am|pm)/i ;
const replacement2 = ( match , month , day , year , hour , minute , meridiem ) => {
2023-12-02 19:04:51 +01:00
const monthNum = moment ( ) . month ( month ) . format ( 'MM' ) ;
2023-07-20 19:32:15 +02:00
const hour24 = meridiem . toLowerCase ( ) === 'pm' ? ( parseInt ( hour , 10 ) % 12 ) + 12 : parseInt ( hour , 10 ) % 12 ;
2023-12-02 19:04:51 +01:00
return ` ${ year } - ${ monthNum } - ${ day . padStart ( 2 , '0' ) } T ${ hour24 . toString ( ) . padStart ( 2 , '0' ) } : ${ minute . padStart ( 2 , '0' ) } :00 ` ;
2023-07-20 19:32:15 +02:00
} ;
const isoTimestamp2 = timestamp . replace ( pattern2 , replacement2 ) ;
if ( moment ( isoTimestamp2 ) . isValid ( ) ) {
return moment ( isoTimestamp2 ) ;
}
// If none of the patterns match, return an invalid moment object
return moment . invalid ( ) ;
}
2023-08-22 12:07:24 +02:00
/ * *
* Compare two moment objects for sorting .
2024-05-11 13:49:11 +02:00
* @ param { moment . Moment } a The first moment object .
* @ param { moment . Moment } b The second moment object .
2023-08-22 12:07:24 +02:00
* @ returns { number } A negative number if a is before b , a positive number if a is after b , or 0 if they are equal .
* /
2023-07-20 19:32:15 +02:00
export function sortMoments ( a , b ) {
if ( a . isBefore ( b ) ) {
return 1 ;
} else if ( a . isAfter ( b ) ) {
return - 1 ;
} else {
return 0 ;
}
}
2023-08-22 12:07:24 +02:00
/ * * S p l i t s t r i n g t o p a r t s n o m o r e t h a n l e n g t h i n s i z e .
* @ param { string } input The string to split .
* @ param { number } length The maximum length of each part .
* @ param { string [ ] } delimiters The delimiters to use when splitting the string .
* @ returns { string [ ] } The split string .
* @ example
* splitRecursive ( 'Hello, world!' , 3 ) ; // ['Hel', 'lo,', 'wor', 'ld!']
* /
export function splitRecursive ( input , length , delimiters = [ '\n\n' , '\n' , ' ' , '' ] ) {
2024-04-17 01:09:22 +02:00
// Invalid length
if ( length <= 0 ) {
return [ input ] ;
}
2023-08-22 12:07:24 +02:00
const delim = delimiters [ 0 ] ? ? '' ;
2023-07-20 19:32:15 +02:00
const parts = input . split ( delim ) ;
const flatParts = parts . flatMap ( p => {
if ( p . length < length ) return p ;
2024-04-20 00:24:46 +02:00
return splitRecursive ( p , length , delimiters . slice ( 1 ) ) ;
2023-07-20 19:32:15 +02:00
} ) ;
// Merge short chunks
const result = [ ] ;
let currentChunk = '' ;
for ( let i = 0 ; i < flatParts . length ; ) {
currentChunk = flatParts [ i ] ;
let j = i + 1 ;
while ( j < flatParts . length ) {
const nextChunk = flatParts [ j ] ;
if ( currentChunk . length + nextChunk . length + delim . length <= length ) {
currentChunk += delim + nextChunk ;
} else {
break ;
}
j ++ ;
}
i = j ;
result . push ( currentChunk ) ;
}
return result ;
}
2023-08-22 12:07:24 +02:00
/ * *
* Checks if a string is a valid data URL .
* @ param { string } str The string to check .
* @ returns { boolean } True if the string is a valid data URL , false otherwise .
* @ example
* isDataURL ( 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUA...' ) ; // true
* /
2023-07-20 19:32:15 +02:00
export function isDataURL ( str ) {
2023-12-02 16:17:31 +01:00
const regex = /^data:([a-z]+\/[a-z0-9-+.]+(;[a-z-]+=[a-z0-9-]+)*;?)?(base64)?,([a-z0-9!$&',()*+;=\-_%.~:@/?#]+)?$/i ;
2023-07-20 19:32:15 +02:00
return regex . test ( str ) ;
}
2024-05-14 00:08:31 +02:00
/ * *
* Gets the size of an image from a data URL .
* @ param { string } dataUrl Image data URL
* @ returns { Promise < { width : number , height : number } > } Image size
* /
export function getImageSizeFromDataURL ( dataUrl ) {
const image = new Image ( ) ;
image . src = dataUrl ;
return new Promise ( ( resolve , reject ) => {
image . onload = function ( ) {
resolve ( { width : image . width , height : image . height } ) ;
} ;
image . onerror = function ( ) {
reject ( new Error ( 'Failed to load image' ) ) ;
} ;
} ) ;
}
2023-07-20 19:32:15 +02:00
export function getCharaFilename ( chid ) {
const context = getContext ( ) ;
2024-07-01 23:16:46 +02:00
const fileName = context . characters [ chid ? ? context . characterId ] ? . avatar ;
2023-07-20 19:32:15 +02:00
if ( fileName ) {
2023-12-02 20:11:06 +01:00
return fileName . replace ( /\.[^/.]+$/ , '' ) ;
2023-07-20 19:32:15 +02:00
}
}
2023-08-22 12:07:24 +02:00
/ * *
* Extracts words from a string .
* @ param { string } value The string to extract words from .
* @ returns { string [ ] } The extracted words .
* @ example
* extractAllWords ( 'Hello, world!' ) ; // ['hello', 'world']
* /
2023-07-30 22:10:37 +02:00
export function extractAllWords ( value ) {
const words = [ ] ;
if ( ! value ) {
return words ;
}
const matches = value . matchAll ( /\b\w+\b/gim ) ;
for ( let match of matches ) {
words . push ( match [ 0 ] . toLowerCase ( ) ) ;
}
return words ;
}
2023-08-22 12:07:24 +02:00
/ * *
* Escapes a string for use in a regular expression .
* @ param { string } string The string to escape .
* @ returns { string } The escaped string .
* @ example
* escapeRegex ( '^Hello$' ) ; // '\\^Hello\\$'
* /
2023-07-20 19:32:15 +02:00
export function escapeRegex ( string ) {
return string . replace ( /[/\-\\^$*+?.()|[\]{}]/g , '\\$&' ) ;
}
2024-05-17 00:14:07 +02:00
/ * *
* Instantiates a regular expression from a string .
* @ param { string } input The input string .
* @ returns { RegExp } The regular expression instance .
* @ copyright Originally from : https : //github.com/IonicaBizau/regex-parser.js/blob/master/lib/index.js
* /
export function regexFromString ( input ) {
try {
// Parse input
var m = input . match ( /(\/?)(.+)\1([a-z]*)/i ) ;
// Invalid flags
if ( m [ 3 ] && ! /^(?!.*?(.).*?\1)[gmixXsuUAJ]+$/ . test ( m [ 3 ] ) ) {
return RegExp ( input ) ;
}
// Create the regular expression
return new RegExp ( m [ 2 ] , m [ 3 ] ) ;
} catch {
return ;
}
}
2023-12-12 21:11:23 +01:00
export class Stopwatch {
/ * *
* Initializes a Stopwatch class .
* @ param { number } interval Update interval in milliseconds . Must be a finite number above zero .
* /
constructor ( interval ) {
if ( isNaN ( interval ) || ! isFinite ( interval ) || interval <= 0 ) {
console . warn ( 'Invalid interval for Stopwatch, setting to 1' ) ;
interval = 1 ;
}
this . interval = interval ;
this . lastAction = Date . now ( ) ;
}
/ * *
* Executes a function if the interval passed .
* @ param { ( arg0 : any ) => any } action Action function
* @ returns Promise < void >
* /
async tick ( action ) {
const passed = ( Date . now ( ) - this . lastAction ) ;
if ( passed < this . interval ) {
return ;
}
await action ( ) ;
this . lastAction = Date . now ( ) ;
}
}
2023-08-22 12:07:24 +02:00
/ * *
* Provides an interface for rate limiting function calls .
* /
2023-07-20 19:32:15 +02:00
export class RateLimiter {
2023-08-22 12:07:24 +02:00
/ * *
* Creates a new RateLimiter .
* @ param { number } interval The interval in milliseconds .
* @ example
* const rateLimiter = new RateLimiter ( 1000 ) ;
* rateLimiter . waitForResolve ( ) . then ( ( ) => {
* console . log ( 'Waited 1000ms' ) ;
* } ) ;
* /
constructor ( interval ) {
this . interval = interval ;
this . lastResolveTime = 0 ;
this . pendingResolve = Promise . resolve ( ) ;
2023-07-20 19:32:15 +02:00
}
2023-08-22 12:07:24 +02:00
/ * *
* Waits for the remaining time in the interval .
* @ param { AbortSignal } abortSignal An optional AbortSignal to abort the wait .
* @ returns { Promise < void > } A promise that resolves when the remaining time has elapsed .
* /
2023-07-20 19:32:15 +02:00
_waitRemainingTime ( abortSignal ) {
const currentTime = Date . now ( ) ;
2023-08-22 12:07:24 +02:00
const elapsedTime = currentTime - this . lastResolveTime ;
const remainingTime = Math . max ( 0 , this . interval - elapsedTime ) ;
2023-07-20 19:32:15 +02:00
return new Promise ( ( resolve , reject ) => {
const timeoutId = setTimeout ( ( ) => {
resolve ( ) ;
} , remainingTime ) ;
if ( abortSignal ) {
abortSignal . addEventListener ( 'abort' , ( ) => {
clearTimeout ( timeoutId ) ;
reject ( new Error ( 'Aborted' ) ) ;
} ) ;
}
} ) ;
}
2023-08-22 12:07:24 +02:00
/ * *
* Waits for the next interval to elapse .
* @ param { AbortSignal } abortSignal An optional AbortSignal to abort the wait .
* @ returns { Promise < void > } A promise that resolves when the next interval has elapsed .
* /
2023-07-20 19:32:15 +02:00
async waitForResolve ( abortSignal ) {
2023-08-22 12:07:24 +02:00
await this . pendingResolve ;
this . pendingResolve = this . _waitRemainingTime ( abortSignal ) ;
2023-07-20 19:32:15 +02:00
// Update the last resolve time
2023-08-22 12:07:24 +02:00
this . lastResolveTime = Date . now ( ) + this . interval ;
console . debug ( ` RateLimiter.waitForResolve() ${ this . lastResolveTime } ` ) ;
2023-07-20 19:32:15 +02:00
}
}
2023-08-22 12:07:24 +02:00
/ * *
* Extracts a JSON object from a PNG file .
* Taken from https : //github.com/LostRuins/lite.koboldai.net/blob/main/index.html
* Adapted from png - chunks - extract under MIT license
* @ param { Uint8Array } data The PNG data to extract the JSON from .
* @ param { string } identifier The identifier to look for in the PNG tEXT data .
* @ returns { object } The extracted JSON object .
* /
2023-07-20 19:32:15 +02:00
export function extractDataFromPng ( data , identifier = 'chara' ) {
2023-12-02 19:04:51 +01:00
console . log ( 'Attempting PNG import...' ) ;
2023-07-20 19:32:15 +02:00
let uint8 = new Uint8Array ( 4 ) ;
let uint32 = new Uint32Array ( uint8 . buffer ) ;
//check if png header is valid
if ( ! data || data [ 0 ] !== 0x89 || data [ 1 ] !== 0x50 || data [ 2 ] !== 0x4E || data [ 3 ] !== 0x47 || data [ 4 ] !== 0x0D || data [ 5 ] !== 0x0A || data [ 6 ] !== 0x1A || data [ 7 ] !== 0x0A ) {
2023-12-02 20:11:06 +01:00
console . log ( 'PNG header invalid' ) ;
2023-07-20 19:32:15 +02:00
return null ;
}
let ended = false ;
let chunks = [ ] ;
let idx = 8 ;
while ( idx < data . length ) {
// Read the length of the current chunk,
// which is stored as a Uint32.
uint8 [ 3 ] = data [ idx ++ ] ;
uint8 [ 2 ] = data [ idx ++ ] ;
uint8 [ 1 ] = data [ idx ++ ] ;
uint8 [ 0 ] = data [ idx ++ ] ;
// Chunk includes name/type for CRC check (see below).
let length = uint32 [ 0 ] + 4 ;
let chunk = new Uint8Array ( length ) ;
chunk [ 0 ] = data [ idx ++ ] ;
chunk [ 1 ] = data [ idx ++ ] ;
chunk [ 2 ] = data [ idx ++ ] ;
chunk [ 3 ] = data [ idx ++ ] ;
// Get the name in ASCII for identification.
let name = (
String . fromCharCode ( chunk [ 0 ] ) +
String . fromCharCode ( chunk [ 1 ] ) +
String . fromCharCode ( chunk [ 2 ] ) +
String . fromCharCode ( chunk [ 3 ] )
) ;
// The IHDR header MUST come first.
if ( ! chunks . length && name !== 'IHDR' ) {
console . log ( 'Warning: IHDR header missing' ) ;
}
// The IEND header marks the end of the file,
// so on discovering it break out of the loop.
if ( name === 'IEND' ) {
ended = true ;
chunks . push ( {
name : name ,
2023-12-02 21:06:57 +01:00
data : new Uint8Array ( 0 ) ,
2023-07-20 19:32:15 +02:00
} ) ;
break ;
}
// Read the contents of the chunk out of the main buffer.
for ( let i = 4 ; i < length ; i ++ ) {
chunk [ i ] = data [ idx ++ ] ;
}
// Read out the CRC value for comparison.
// It's stored as an Int32.
uint8 [ 3 ] = data [ idx ++ ] ;
uint8 [ 2 ] = data [ idx ++ ] ;
uint8 [ 1 ] = data [ idx ++ ] ;
uint8 [ 0 ] = data [ idx ++ ] ;
// The chunk data is now copied to remove the 4 preceding
// bytes used for the chunk name/type.
let chunkData = new Uint8Array ( chunk . buffer . slice ( 4 ) ) ;
chunks . push ( {
name : name ,
2023-12-02 21:06:57 +01:00
data : chunkData ,
2023-07-20 19:32:15 +02:00
} ) ;
}
if ( ! ended ) {
console . log ( '.png file ended prematurely: no IEND header was found' ) ;
}
//find the chunk with the chara name, just check first and last letter
let found = chunks . filter ( x => (
2023-12-02 19:04:51 +01:00
x . name == 'tEXt'
2023-07-20 19:32:15 +02:00
&& x . data . length > identifier . length
&& x . data . slice ( 0 , identifier . length ) . every ( ( v , i ) => String . fromCharCode ( v ) == identifier [ i ] ) ) ) ;
if ( found . length == 0 ) {
console . log ( 'PNG Image contains no data' ) ;
return null ;
} else {
try {
2023-12-02 19:04:51 +01:00
let b64buf = '' ;
2023-07-20 19:32:15 +02:00
let bytes = found [ 0 ] . data ; //skip the chara
for ( let i = identifier . length + 1 ; i < bytes . length ; i ++ ) {
b64buf += String . fromCharCode ( bytes [ i ] ) ;
}
let decoded = JSON . parse ( atob ( b64buf ) ) ;
console . log ( decoded ) ;
return decoded ;
} catch ( e ) {
2023-12-02 19:04:51 +01:00
console . log ( 'Error decoding b64 in image: ' + e ) ;
2023-07-20 19:32:15 +02:00
return null ;
}
}
}
2024-05-22 21:11:39 +02:00
/ * *
* Sends a request to the server to sanitize a given filename
*
* @ param { string } fileName - The name of the file to sanitize
* @ returns { Promise < string > } A Promise that resolves to the sanitized filename if successful , or rejects with an error message if unsuccessful
* /
export async function getSanitizedFilename ( fileName ) {
try {
const result = await fetch ( '/api/files/sanitize-filename' , {
method : 'POST' ,
headers : getRequestHeaders ( ) ,
body : JSON . stringify ( {
fileName : fileName ,
} ) ,
} ) ;
if ( ! result . ok ) {
const error = await result . text ( ) ;
throw new Error ( error ) ;
}
const responseData = await result . json ( ) ;
return responseData . fileName ;
} catch ( error ) {
toastr . error ( String ( error ) , 'Could not sanitize fileName' ) ;
console . error ( 'Could not sanitize fileName' , error ) ;
throw error ;
}
}
2023-08-20 06:15:57 +02:00
/ * *
* Sends a base64 encoded image to the backend to be saved as a file .
2023-08-20 11:37:38 +02:00
*
2023-08-20 06:15:57 +02:00
* @ param { string } base64Data - The base64 encoded image data .
* @ param { string } characterName - The character name to determine the sub - directory for saving .
* @ param { string } ext - The file extension for the image ( e . g . , 'jpg' , 'png' , 'webp' ) .
2023-08-20 11:37:38 +02:00
*
* @ returns { Promise < string > } - Resolves to the saved image ' s path on the server .
2023-08-20 06:15:57 +02:00
* Rejects with an error if the upload fails .
* /
2023-12-02 19:04:51 +01:00
export async function saveBase64AsFile ( base64Data , characterName , filename = '' , ext ) {
2023-08-20 05:53:34 +02:00
// Construct the full data URL
const format = ext ; // Extract the file extension (jpg, png, webp)
const dataURL = ` data:image/ ${ format } ;base64, ${ base64Data } ` ;
// Prepare the request body
const requestBody = {
image : dataURL ,
2023-08-20 07:41:58 +02:00
ch _name : characterName ,
2024-03-03 19:39:20 +01:00
filename : String ( filename ) . replace ( /\./g , '_' ) ,
2023-08-20 05:53:34 +02:00
} ;
// Send the data URL to your backend using fetch
2024-03-19 23:59:06 +01:00
const response = await fetch ( '/api/images/upload' , {
2023-08-20 05:53:34 +02:00
method : 'POST' ,
body : JSON . stringify ( requestBody ) ,
headers : {
... getRequestHeaders ( ) ,
2023-12-02 21:06:57 +01:00
'Content-Type' : 'application/json' ,
2023-08-20 05:53:34 +02:00
} ,
} ) ;
// If the response is successful, get the saved image path from the server's response
if ( response . ok ) {
const responseData = await response . json ( ) ;
2023-12-05 23:55:52 +01:00
return responseData . path ;
2023-08-20 05:53:34 +02:00
} else {
const errorData = await response . json ( ) ;
throw new Error ( errorData . error || 'Failed to upload the image to the server' ) ;
}
}
2023-08-21 17:21:32 +02:00
/ * *
* Loads either a CSS or JS file and appends it to the appropriate document section .
2023-09-08 21:44:06 +02:00
*
2023-08-21 17:21:32 +02:00
* @ param { string } url - The URL of the file to be loaded .
* @ param { string } type - The type of file to load : "css" or "js" .
* @ returns { Promise } - Resolves when the file has loaded , rejects if there ' s an error or invalid type .
* /
export function loadFileToDocument ( url , type ) {
return new Promise ( ( resolve , reject ) => {
let element ;
2023-12-02 19:04:51 +01:00
if ( type === 'css' ) {
element = document . createElement ( 'link' ) ;
element . rel = 'stylesheet' ;
2023-08-21 17:21:32 +02:00
element . href = url ;
2023-12-02 19:04:51 +01:00
} else if ( type === 'js' ) {
element = document . createElement ( 'script' ) ;
2023-08-21 17:21:32 +02:00
element . src = url ;
} else {
2023-12-02 19:04:51 +01:00
reject ( 'Invalid type specified' ) ;
2023-08-21 17:21:32 +02:00
return ;
}
element . onload = resolve ;
element . onerror = reject ;
2023-12-02 19:04:51 +01:00
type === 'css'
2023-08-21 17:21:32 +02:00
? document . head . appendChild ( element )
: document . body . appendChild ( element ) ;
} ) ;
}
2024-03-21 13:47:22 +01:00
/ * *
* Ensure that we can import war crime image formats like WEBP and AVIF .
* @ param { File } file Input file
* @ returns { Promise < File > } A promise that resolves to the supported file .
* /
export async function ensureImageFormatSupported ( file ) {
const supportedTypes = [
'image/jpeg' ,
'image/png' ,
'image/bmp' ,
'image/tiff' ,
'image/gif' ,
'image/apng' ,
] ;
if ( supportedTypes . includes ( file . type ) || ! file . type . startsWith ( 'image/' ) ) {
return file ;
}
return await convertImageFile ( file , 'image/png' ) ;
}
/ * *
* Converts an image file to a given format .
* @ param { File } inputFile File to convert
* @ param { string } type Target file type
* @ returns { Promise < File > } A promise that resolves to the converted file .
* /
export async function convertImageFile ( inputFile , type = 'image/png' ) {
const base64 = await getBase64Async ( inputFile ) ;
const thumbnail = await createThumbnail ( base64 , null , null , type ) ;
const blob = await fetch ( thumbnail ) . then ( res => res . blob ( ) ) ;
const outputFile = new File ( [ blob ] , inputFile . name , { type } ) ;
return outputFile ;
}
2023-08-22 12:07:24 +02:00
/ * *
* Creates a thumbnail from a data URL .
* @ param { string } dataUrl The data URL encoded data of the image .
2024-03-21 13:47:22 +01:00
* @ param { number | null } maxWidth The maximum width of the thumbnail .
* @ param { number | null } maxHeight The maximum height of the thumbnail .
2023-11-23 19:50:08 +01:00
* @ param { string } [ type = 'image/jpeg' ] The type of the thumbnail .
2023-08-22 12:07:24 +02:00
* @ returns { Promise < string > } A promise that resolves to the thumbnail data URL .
* /
2024-03-21 13:47:22 +01:00
export function createThumbnail ( dataUrl , maxWidth = null , maxHeight = null , type = 'image/jpeg' ) {
2023-12-14 21:28:22 +01:00
// Someone might pass in a base64 encoded string without the data URL prefix
if ( ! dataUrl . includes ( 'data:' ) ) {
dataUrl = ` data:image/jpeg;base64, ${ dataUrl } ` ;
}
2023-07-20 19:32:15 +02:00
return new Promise ( ( resolve , reject ) => {
const img = new Image ( ) ;
img . src = dataUrl ;
img . onload = ( ) => {
const canvas = document . createElement ( 'canvas' ) ;
const ctx = canvas . getContext ( '2d' ) ;
// Calculate the thumbnail dimensions while maintaining the aspect ratio
const aspectRatio = img . width / img . height ;
let thumbnailWidth = maxWidth ;
let thumbnailHeight = maxHeight ;
2024-03-21 13:47:22 +01:00
if ( maxWidth === null ) {
thumbnailWidth = img . width ;
maxWidth = img . width ;
}
if ( maxHeight === null ) {
thumbnailHeight = img . height ;
maxHeight = img . height ;
}
2023-07-20 19:32:15 +02:00
if ( img . width > img . height ) {
thumbnailHeight = maxWidth / aspectRatio ;
} else {
thumbnailWidth = maxHeight * aspectRatio ;
}
// Set the canvas dimensions and draw the resized image
canvas . width = thumbnailWidth ;
canvas . height = thumbnailHeight ;
ctx . drawImage ( img , 0 , 0 , thumbnailWidth , thumbnailHeight ) ;
// Convert the canvas to a data URL and resolve the promise
2023-11-23 19:50:08 +01:00
const thumbnailDataUrl = canvas . toDataURL ( type ) ;
2023-07-20 19:32:15 +02:00
resolve ( thumbnailDataUrl ) ;
} ;
img . onerror = ( ) => {
reject ( new Error ( 'Failed to load the image.' ) ) ;
} ;
} ) ;
}
2023-08-22 12:07:24 +02:00
/ * *
* Waits for a condition to be true . Throws an error if the condition is not true within the timeout .
* @ param { { ( ) : boolean ; } } condition The condition to wait for .
* @ param { number } [ timeout = 1000 ] The timeout in milliseconds .
* @ param { number } [ interval = 100 ] The interval in milliseconds .
* @ returns { Promise < void > } A promise that resolves when the condition is true .
* /
2023-07-20 19:32:15 +02:00
export async function waitUntilCondition ( condition , timeout = 1000 , interval = 100 ) {
return new Promise ( ( resolve , reject ) => {
const timeoutId = setTimeout ( ( ) => {
clearInterval ( intervalId ) ;
reject ( new Error ( 'Timed out waiting for condition to be true' ) ) ;
} , timeout ) ;
const intervalId = setInterval ( ( ) => {
if ( condition ( ) ) {
clearTimeout ( timeoutId ) ;
clearInterval ( intervalId ) ;
resolve ( ) ;
}
} , interval ) ;
} ) ;
}
2023-08-22 12:07:24 +02:00
/ * *
* Returns a UUID v4 string .
* @ returns { string } A UUID v4 string .
* @ example
* uuidv4 ( ) ; // '3e2fd9e1-0a7a-4f6d-9aaf-8a7a4babe7eb'
* /
2023-07-20 19:32:15 +02:00
export function uuidv4 ( ) {
2024-06-10 13:20:52 +02:00
if ( 'randomUUID' in crypto ) {
return crypto . randomUUID ( ) ;
}
2023-07-20 19:32:15 +02:00
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx' . replace ( /[xy]/g , function ( c ) {
const r = Math . random ( ) * 16 | 0 ;
const v = c === 'x' ? r : ( r & 0x3 | 0x8 ) ;
return v . toString ( 16 ) ;
} ) ;
}
2023-11-29 16:51:30 +01:00
2024-02-29 15:37:52 +01:00
function postProcessText ( text , collapse = true ) {
2024-04-21 01:05:59 +02:00
// Remove carriage returns
text = text . replace ( /\r/g , '' ) ;
// Replace tabs with spaces
text = text . replace ( /\t/g , ' ' ) ;
// Normalize unicode spaces
text = text . replace ( /\u00A0/g , ' ' ) ;
2023-11-29 16:51:30 +01:00
// Collapse multiple newlines into one
2024-02-29 15:37:52 +01:00
if ( collapse ) {
text = collapseNewlines ( text ) ;
// Trim leading and trailing whitespace, and remove empty lines
text = text . split ( '\n' ) . map ( l => l . trim ( ) ) . filter ( Boolean ) . join ( '\n' ) ;
2024-04-21 01:05:59 +02:00
} else {
// Replace more than 4 newlines with 4 newlines
text = text . replace ( /\n{4,}/g , '\n\n\n\n' ) ;
// Trim lines that contain nothing but whitespace
text = text . split ( '\n' ) . map ( l => / ^ \ s + $ / . test ( l ) ? '' : l ) . join ( '\n' ) ;
2024-02-29 15:37:52 +01:00
}
2023-11-29 16:51:30 +01:00
// Collapse multiple spaces into one (except for newlines)
text = text . replace ( / {2,}/g , ' ' ) ;
// Remove leading and trailing spaces
text = text . trim ( ) ;
return text ;
}
2024-02-29 15:37:52 +01:00
/ * *
* Uses Readability . js to parse the text from a web page .
* @ param { Document } document HTML document
* @ param { string } [ textSelector = 'body' ] The fallback selector for the text to parse .
* @ returns { Promise < string > } A promise that resolves to the parsed text .
* /
export async function getReadableText ( document , textSelector = 'body' ) {
if ( isProbablyReaderable ( document ) ) {
const parser = new Readability ( document ) ;
const article = parser . parse ( ) ;
return postProcessText ( article . textContent , false ) ;
}
const elements = document . querySelectorAll ( textSelector ) ;
const rawText = Array . from ( elements ) . map ( e => e . textContent ) . join ( '\n' ) ;
const text = postProcessText ( rawText ) ;
return text ;
}
2023-11-29 16:51:30 +01:00
/ * *
* Use pdf . js to load and parse text from PDF pages
* @ param { Blob } blob PDF file blob
* @ returns { Promise < string > } A promise that resolves to the parsed text .
* /
export async function extractTextFromPDF ( blob ) {
async function initPdfJs ( ) {
const promises = [ ] ;
const workerPromise = new Promise ( ( resolve , reject ) => {
const workerScript = document . createElement ( 'script' ) ;
workerScript . type = 'module' ;
workerScript . async = true ;
workerScript . src = 'lib/pdf.worker.mjs' ;
workerScript . onload = resolve ;
workerScript . onerror = reject ;
document . head . appendChild ( workerScript ) ;
} ) ;
promises . push ( workerPromise ) ;
const pdfjsPromise = new Promise ( ( resolve , reject ) => {
const pdfjsScript = document . createElement ( 'script' ) ;
pdfjsScript . type = 'module' ;
pdfjsScript . async = true ;
pdfjsScript . src = 'lib/pdf.mjs' ;
pdfjsScript . onload = resolve ;
pdfjsScript . onerror = reject ;
document . head . appendChild ( pdfjsScript ) ;
} ) ;
promises . push ( pdfjsPromise ) ;
return Promise . all ( promises ) ;
}
if ( ! ( 'pdfjsLib' in window ) ) {
await initPdfJs ( ) ;
}
const buffer = await getFileBuffer ( blob ) ;
const pdf = await pdfjsLib . getDocument ( buffer ) . promise ;
const pages = [ ] ;
for ( let i = 1 ; i <= pdf . numPages ; i ++ ) {
const page = await pdf . getPage ( i ) ;
const textContent = await page . getTextContent ( ) ;
const text = textContent . items . map ( item => item . str ) . join ( ' ' ) ;
pages . push ( text ) ;
}
return postProcessText ( pages . join ( '\n' ) ) ;
}
/ * *
* Use DOMParser to load and parse text from HTML
* @ param { Blob } blob HTML content blob
* @ returns { Promise < string > } A promise that resolves to the parsed text .
* /
2023-12-12 00:08:47 +01:00
export async function extractTextFromHTML ( blob , textSelector = 'body' ) {
2023-11-29 16:51:30 +01:00
const html = await blob . text ( ) ;
const domParser = new DOMParser ( ) ;
const document = domParser . parseFromString ( DOMPurify . sanitize ( html ) , 'text/html' ) ;
2024-02-29 15:37:52 +01:00
return await getReadableText ( document , textSelector ) ;
2023-11-29 16:51:30 +01:00
}
/ * *
* Use showdown to load and parse text from Markdown
* @ param { Blob } blob Markdown content blob
* @ returns { Promise < string > } A promise that resolves to the parsed text .
* /
export async function extractTextFromMarkdown ( blob ) {
const markdown = await blob . text ( ) ;
2024-05-19 23:48:23 +02:00
const text = postProcessText ( markdown , false ) ;
2023-11-29 16:51:30 +01:00
return text ;
}
2024-03-12 00:49:05 +01:00
2024-04-20 00:24:46 +02:00
export async function extractTextFromEpub ( blob ) {
async function initEpubJs ( ) {
const epubScript = new Promise ( ( resolve , reject ) => {
const epubScript = document . createElement ( 'script' ) ;
epubScript . async = true ;
epubScript . src = 'lib/epub.min.js' ;
epubScript . onload = resolve ;
epubScript . onerror = reject ;
document . head . appendChild ( epubScript ) ;
} ) ;
const jszipScript = new Promise ( ( resolve , reject ) => {
const jszipScript = document . createElement ( 'script' ) ;
jszipScript . async = true ;
jszipScript . src = 'lib/jszip.min.js' ;
jszipScript . onload = resolve ;
jszipScript . onerror = reject ;
document . head . appendChild ( jszipScript ) ;
} ) ;
return Promise . all ( [ epubScript , jszipScript ] ) ;
}
if ( ! ( 'ePub' in window ) ) {
await initEpubJs ( ) ;
}
const book = ePub ( blob ) ;
await book . ready ;
const sectionPromises = [ ] ;
book . spine . each ( ( section ) => {
const sectionPromise = ( async ( ) => {
const chapter = await book . load ( section . href ) ;
if ( ! ( chapter instanceof Document ) || ! chapter . body ? . textContent ) {
return '' ;
}
return chapter . body . textContent . trim ( ) ;
} ) ( ) ;
sectionPromises . push ( sectionPromise ) ;
} ) ;
const content = await Promise . all ( sectionPromises ) ;
const text = content . filter ( text => text ) ;
return postProcessText ( text . join ( '\n' ) , false ) ;
}
2024-04-21 15:27:44 +02:00
/ * *
* Extracts text from an Office document using the server plugin .
* @ param { File } blob File to extract text from
* @ returns { Promise < string > } A promise that resolves to the extracted text .
* /
export async function extractTextFromOffice ( blob ) {
async function checkPluginAvailability ( ) {
try {
const result = await fetch ( '/api/plugins/office/probe' , {
method : 'POST' ,
headers : getRequestHeaders ( ) ,
} ) ;
return result . ok ;
} catch ( error ) {
return false ;
}
}
const isPluginAvailable = await checkPluginAvailability ( ) ;
if ( ! isPluginAvailable ) {
throw new Error ( 'Importing Office documents requires a server plugin. Please refer to the documentation for more information.' ) ;
}
const base64 = await getBase64Async ( blob ) ;
const response = await fetch ( '/api/plugins/office/parse' , {
method : 'POST' ,
headers : getRequestHeaders ( ) ,
body : JSON . stringify ( { data : base64 } ) ,
} ) ;
if ( ! response . ok ) {
throw new Error ( 'Failed to parse the Office document' ) ;
}
const data = await response . text ( ) ;
return postProcessText ( data , false ) ;
}
2024-03-12 00:49:05 +01:00
/ * *
* Sets a value in an object by a path .
* @ param { object } obj Object to set value in
* @ param { string } path Key path
* @ param { any } value Value to set
* @ returns { void }
* /
export function setValueByPath ( obj , path , value ) {
const keyParts = path . split ( '.' ) ;
let currentObject = obj ;
for ( let i = 0 ; i < keyParts . length - 1 ; i ++ ) {
const part = keyParts [ i ] ;
if ( ! Object . hasOwn ( currentObject , part ) ) {
currentObject [ part ] = { } ;
}
currentObject = currentObject [ part ] ;
}
currentObject [ keyParts [ keyParts . length - 1 ] ] = value ;
}
2024-04-27 10:26:01 +02:00
/ * *
* Flashes the given HTML element via CSS flash animation for a defined period
* @ param { JQuery < HTMLElement > } element - The element to flash
2024-07-10 19:43:58 +02:00
* @ param { number } timespan - A number in milliseconds how the flash should last ( default is 2000 ms . Multiples of 1000 ms work best , as they end with the flash animation being at 100 % opacity )
2024-04-27 10:26:01 +02:00
* /
export function flashHighlight ( element , timespan = 2000 ) {
2024-07-10 19:43:58 +02:00
const flashDuration = 2000 ; // Duration of a single flash cycle in milliseconds
2024-04-27 10:26:01 +02:00
element . addClass ( 'flash animated' ) ;
2024-07-10 19:43:58 +02:00
element . css ( '--animation-duration' , ` ${ flashDuration } ms ` ) ;
// Repeat the flash animation
const intervalId = setInterval ( ( ) => {
element . removeClass ( 'flash animated' ) ;
void element [ 0 ] . offsetWidth ; // Trigger reflow to restart animation
element . addClass ( 'flash animated' ) ;
} , flashDuration ) ;
setTimeout ( ( ) => {
clearInterval ( intervalId ) ;
element . removeClass ( 'flash animated' ) ;
element . css ( '--animation-duration' , '' ) ;
} , timespan ) ;
2024-04-27 10:26:01 +02:00
}
2024-04-30 06:03:41 +02:00
2024-07-10 19:43:58 +02:00
2024-05-27 03:35:03 +02:00
/ * *
* Checks if the given control has an animation applied to it
*
* @ param { HTMLElement } control - The control element to check for animation
* @ returns { boolean } Whether the control has an animation applied
* /
export function hasAnimation ( control ) {
2024-06-23 18:43:56 +02:00
const animatioName = getComputedStyle ( control , null ) [ 'animation-name' ] ;
return animatioName != 'none' ;
2024-05-27 03:35:03 +02:00
}
/ * *
* Run an action once an animation on a control ends . If the control has no animation , the action will be executed immediately .
*
* @ param { HTMLElement } control - The control element to listen for animation end event
* @ param { ( control : * ? ) => void } callback - The callback function to be executed when the animation ends
* /
export function runAfterAnimation ( control , callback ) {
if ( hasAnimation ( control ) ) {
const onAnimationEnd = ( ) => {
control . removeEventListener ( 'animationend' , onAnimationEnd ) ;
callback ( control ) ;
} ;
control . addEventListener ( 'animationend' , onAnimationEnd ) ;
} else {
callback ( control ) ;
}
}
2024-05-22 18:19:01 +02:00
/ * *
* A common base function for case - insensitive and accent - insensitive string comparisons .
*
* @ param { string } a - The first string to compare .
* @ param { string } b - The second string to compare .
* @ param { ( a : string , b : string ) => boolean } comparisonFunction - The function to use for the comparison .
* @ returns { * } - The result of the comparison .
* /
export function compareIgnoreCaseAndAccents ( a , b , comparisonFunction ) {
if ( ! a || ! b ) return comparisonFunction ( a , b ) ; // Return the comparison result if either string is empty
// Normalize and remove diacritics, then convert to lower case
const normalizedA = a . normalize ( 'NFD' ) . replace ( /[\u0300-\u036f]/g , '' ) . toLowerCase ( ) ;
const normalizedB = b . normalize ( 'NFD' ) . replace ( /[\u0300-\u036f]/g , '' ) . toLowerCase ( ) ;
// Check if the normalized strings are equal
return comparisonFunction ( normalizedA , normalizedB ) ;
}
2024-04-30 06:03:41 +02:00
/ * *
* Performs a case - insensitive and accent - insensitive substring search .
* This function normalizes the strings to remove diacritical marks and converts them to lowercase to ensure the search is insensitive to case and accents .
*
2024-05-22 18:19:01 +02:00
* @ param { string } text - The text in which to search for the substring
* @ param { string } searchTerm - The substring to search for in the text
* @ returns { boolean } true if the searchTerm is found within the text , otherwise returns false
2024-04-30 06:03:41 +02:00
* /
export function includesIgnoreCaseAndAccents ( text , searchTerm ) {
2024-05-22 18:19:01 +02:00
return compareIgnoreCaseAndAccents ( text , searchTerm , ( a , b ) => a ? . includes ( b ) === true ) ;
}
2024-04-30 06:03:41 +02:00
2024-05-22 18:19:01 +02:00
/ * *
* Performs a case - insensitive and accent - insensitive equality check .
* This function normalizes the strings to remove diacritical marks and converts them to lowercase to ensure the search is insensitive to case and accents .
*
* @ param { string } a - The first string to compare
* @ param { string } b - The second string to compare
* @ returns { boolean } true if the strings are equal , otherwise returns false
* /
export function equalsIgnoreCaseAndAccents ( a , b ) {
return compareIgnoreCaseAndAccents ( a , b , ( a , b ) => a === b ) ;
2024-04-30 06:03:41 +02:00
}
2024-05-07 02:01:54 +02:00
2024-05-08 20:34:53 +02:00
/ * *
* @ typedef { object } Select2Option The option object for select2 controls
* @ property { string } id - The unique ID inside this select
* @ property { string } text - The text for this option
* @ property { number ? } [ count ] - Optionally show the count how often that option was chosen already
* /
2024-05-07 05:44:18 +02:00
/ * *
* Returns a unique hash as ID for a select2 option text
2024-05-09 23:30:18 +02:00
*
2024-05-07 05:44:18 +02:00
* @ param { string } option - The option
* @ returns { string } A hashed version of that option
* /
export function getSelect2OptionId ( option ) {
return String ( getStringHash ( option ) ) ;
}
2024-05-07 02:01:54 +02:00
/ * *
* Modifies the select2 options by adding not existing one and optionally selecting them
*
* @ param { JQuery < HTMLElement > } element - The "select" element to add the options to
2024-05-08 20:34:53 +02:00
* @ param { string [ ] | Select2Option [ ] } items - The option items to build , add or select
2024-05-07 02:01:54 +02:00
* @ param { object } [ options ] - Optional arguments
* @ param { boolean } [ options . select = false ] - Whether the options should be selected right away
* @ param { object } [ options . changeEventArgs = null ] - Optional event args being passed into the "change" event when its triggered because a new options is selected
* /
export function select2ModifyOptions ( element , items , { select = false , changeEventArgs = null } = { } ) {
if ( ! items . length ) return ;
2024-05-08 20:34:53 +02:00
/** @type {Select2Option[]} */
2024-05-07 05:44:18 +02:00
const dataItems = items . map ( x => typeof x === 'string' ? { id : getSelect2OptionId ( x ) , text : x } : x ) ;
2024-05-07 02:01:54 +02:00
2024-05-08 20:34:53 +02:00
const existingValues = [ ] ;
2024-05-07 02:01:54 +02:00
dataItems . forEach ( item => {
// Set the value, creating a new option if necessary
2024-05-17 00:14:07 +02:00
if ( element . find ( 'option[value=\'' + item . id + '\']' ) . length ) {
2024-05-08 20:34:53 +02:00
if ( select ) existingValues . push ( item . id ) ;
2024-05-07 02:01:54 +02:00
} else {
// Create a DOM Option and optionally pre-select by default
var newOption = new Option ( item . text , item . id , select , select ) ;
// Append it to the select
element . append ( newOption ) ;
if ( select ) element . trigger ( 'change' , changeEventArgs ) ;
}
2024-05-08 20:34:53 +02:00
if ( existingValues . length ) element . val ( existingValues ) . trigger ( 'change' , changeEventArgs ) ;
2024-05-07 02:01:54 +02:00
} ) ;
}
2024-05-08 20:34:53 +02:00
/ * *
* Returns the ajax settings that can be used on the select2 ajax property to dynamically get the data .
* Can be used on a single global array , querying data from the server or anything similar .
2024-05-09 23:30:18 +02:00
*
2024-05-08 20:34:53 +02:00
* @ param { function ( ) : Select2Option [ ] } dataProvider - The provider / function to retrieve the data - can be as simple as "() => myData" for arrays
2024-05-14 04:51:22 +02:00
* @ return { { transport : ( params , success , failure ) => any } } The ajax object with the transport function to use on the select2 ajax property
2024-05-08 20:34:53 +02:00
* /
2024-05-09 03:31:41 +02:00
export function dynamicSelect2DataViaAjax ( dataProvider ) {
2024-05-08 20:34:53 +02:00
function dynamicSelect2DataTransport ( params , success , failure ) {
var items = dataProvider ( ) ;
// fitering if params.data.q available
if ( params . data && params . data . q ) {
items = items . filter ( function ( item ) {
2024-05-15 00:38:48 +02:00
return includesIgnoreCaseAndAccents ( item . text , params . data . q ) ;
2024-05-08 20:34:53 +02:00
} ) ;
}
var promise = new Promise ( function ( resolve , reject ) {
resolve ( { results : items } ) ;
} ) ;
promise . then ( success ) ;
promise . catch ( failure ) ;
2024-05-17 00:14:07 +02:00
}
2024-05-08 20:34:53 +02:00
const ajax = {
2024-05-17 00:14:07 +02:00
transport : dynamicSelect2DataTransport ,
2024-05-08 20:34:53 +02:00
} ;
return ajax ;
}
2024-05-09 03:31:41 +02:00
2024-05-14 04:51:22 +02:00
/ * *
* Checks whether a given control is a select2 choice element - meaning one of the results being displayed in the select multi select box
* @ param { JQuery < HTMLElement > | HTMLElement } element - The element to check
* @ returns { boolean } Whether this is a choice element
* /
export function isSelect2ChoiceElement ( element ) {
const $element = $ ( element ) ;
return ( $element . hasClass ( 'select2-selection__choice__display' ) || $element . parents ( '.select2-selection__choice__display' ) . length > 0 ) ;
}
2024-05-09 23:30:18 +02:00
/ * *
* Subscribes a 'click' event handler to the choice elements of a select2 multi - select control
*
* @ param { JQuery < HTMLElement > } control The original control the select2 was applied to
2024-05-14 04:51:22 +02:00
* @ param { function ( HTMLElement ) : void } action - The action to execute when a choice element is clicked
2024-05-09 23:30:18 +02:00
* @ param { object } options - Optional parameters
2024-05-10 00:42:35 +02:00
* @ param { boolean } [ options . buttonStyle = false ] - Whether the choices should be styles as a clickable button with color and hover transition , instead of just changed cursor
2024-05-09 23:30:18 +02:00
* @ param { boolean } [ options . closeDrawer = false ] - Whether the drawer should be closed and focus removed after the choice item was clicked
2024-05-10 00:42:35 +02:00
* @ param { boolean } [ options . openDrawer = false ] - Whether the drawer should be opened , even if this click would normally close it
2024-05-09 23:30:18 +02:00
* /
2024-05-10 00:42:35 +02:00
export function select2ChoiceClickSubscribe ( control , action , { buttonStyle = false , closeDrawer = false , openDrawer = false } = { } ) {
2024-05-09 23:30:18 +02:00
// Add class for styling (hover color, changed cursor, etc)
control . addClass ( 'select2_choice_clickable' ) ;
2024-05-10 00:42:35 +02:00
if ( buttonStyle ) control . addClass ( 'select2_choice_clickable_buttonstyle' ) ;
2024-05-09 23:30:18 +02:00
// Get the real container below and create a click handler on that one
const select2Container = control . next ( 'span.select2-container' ) ;
select2Container . on ( 'click' , function ( event ) {
2024-05-14 04:51:22 +02:00
const isChoice = isSelect2ChoiceElement ( event . target ) ;
if ( isChoice ) {
2024-05-09 23:30:18 +02:00
event . preventDefault ( ) ;
// select2 still bubbles the event to open the dropdown. So we close it here and remove focus if we want that
if ( closeDrawer ) {
control . select2 ( 'close' ) ;
setTimeout ( ( ) => select2Container . find ( 'textarea' ) . trigger ( 'blur' ) , debounce _timeout . quick ) ;
}
2024-05-10 00:42:35 +02:00
if ( openDrawer ) {
control . select2 ( 'open' ) ;
}
2024-05-09 23:30:18 +02:00
// Now execute the actual action that was subscribed
action ( event . target ) ;
}
} ) ;
}
2024-05-09 03:31:41 +02:00
/ * *
* Applies syntax highlighting to a given regex string by generating HTML with classes
2024-05-09 23:30:18 +02:00
*
2024-05-09 03:31:41 +02:00
* @ param { string } regexStr - The javascript compatible regex string
* @ returns { string } The html representation of the highlighted regex
* /
export function highlightRegex ( regexStr ) {
// Function to escape HTML special characters for safety
const escapeHtml = ( str ) => str . replace ( /[&<>"']/g , match => ( {
2024-05-17 00:14:07 +02:00
'&' : '&' , '<' : '<' , '>' : '>' , '"' : '"' , '\'' : ''' ,
2024-05-09 03:31:41 +02:00
} ) [ match ] ) ;
// Replace special characters with their HTML-escaped forms
regexStr = escapeHtml ( regexStr ) ;
// Patterns that we want to highlight only if they are not escaped
2024-05-19 21:16:14 +02:00
function getPatterns ( ) {
try {
return {
brackets : new RegExp ( '(?<!\\\\)\\[.*?\\]' , 'g' ) , // Non-escaped square brackets
quantifiers : new RegExp ( '(?<!\\\\)[*+?{}]' , 'g' ) , // Non-escaped quantifiers
operators : new RegExp ( '(?<!\\\\)[|.^$()]' , 'g' ) , // Non-escaped operators like | and ()
specialChars : new RegExp ( '\\\\.' , 'g' ) ,
flags : new RegExp ( '(?<=\\/)([gimsuy]*)$' , 'g' ) , // Match trailing flags
delimiters : new RegExp ( '^\\/|(?<![\\\\<])\\/' , 'g' ) , // Match leading or trailing delimiters
} ;
} catch ( error ) {
return {
brackets : new RegExp ( '(\\\\)?\\[.*?\\]' , 'g' ) , // Non-escaped square brackets
quantifiers : new RegExp ( '(\\\\)?[*+?{}]' , 'g' ) , // Non-escaped quantifiers
operators : new RegExp ( '(\\\\)?[|.^$()]' , 'g' ) , // Non-escaped operators like | and ()
specialChars : new RegExp ( '\\\\.' , 'g' ) ,
flags : new RegExp ( '/([gimsuy]*)$' , 'g' ) , // Match trailing flags
delimiters : new RegExp ( '^/|[^\\\\](/)' , 'g' ) , // Match leading or trailing delimiters
} ;
}
}
const patterns = getPatterns ( ) ;
2024-05-09 03:31:41 +02:00
// Function to replace each pattern with a highlighted HTML span
const wrapPattern = ( pattern , className ) => {
regexStr = regexStr . replace ( pattern , match => ` <span class=" ${ className } "> ${ match } </span> ` ) ;
} ;
// Apply highlighting patterns
wrapPattern ( patterns . brackets , 'regex-brackets' ) ;
wrapPattern ( patterns . quantifiers , 'regex-quantifier' ) ;
wrapPattern ( patterns . operators , 'regex-operator' ) ;
wrapPattern ( patterns . specialChars , 'regex-special' ) ;
wrapPattern ( patterns . flags , 'regex-flags' ) ;
wrapPattern ( patterns . delimiters , 'regex-delimiter' ) ;
return ` <span class="regex-highlight"> ${ regexStr } </span> ` ;
2024-05-09 23:30:18 +02:00
}
2024-05-22 23:52:35 +02:00
/ * *
* Confirms if the user wants to overwrite an existing data object ( like character , world info , etc ) if one exists .
* If no data with the name exists , this simply returns true .
*
* @ param { string } type - The type of the check ( "World Info" , "Character" , etc )
* @ param { string [ ] } existingNames - The list of existing names to check against
* @ param { string } name - The new name
* @ param { object } options - Optional parameters
* @ param { boolean } [ options . interactive = false ] - Whether to show a confirmation dialog when needing to overwrite an existing data object
* @ param { string } [ options . actionName = 'overwrite' ] - The action name to display in the confirmation dialog
* @ param { ( existingName : string ) => void } [ options . deleteAction = null ] - Optional action to execute wen deleting an existing data object on overwrite
* @ returns { Promise < boolean > } True if the user confirmed the overwrite or there is no overwrite needed , false otherwise
* /
export async function checkOverwriteExistingData ( type , existingNames , name , { interactive = false , actionName = 'Overwrite' , deleteAction = null } = { } ) {
const existing = existingNames . find ( x => equalsIgnoreCaseAndAccents ( x , name ) ) ;
if ( ! existing ) {
return true ;
}
2024-06-26 05:35:41 +02:00
const overwrite = interactive && await Popup . show . confirm ( ` ${ type } ${ actionName } ` , ` <p>A ${ type . toLowerCase ( ) } with the same name already exists:<br /> ${ existing } </p>Do you want to overwrite it? ` ) ;
2024-05-22 23:52:35 +02:00
if ( ! overwrite ) {
toastr . warning ( ` ${ type } ${ actionName . toLowerCase ( ) } cancelled. A ${ type . toLowerCase ( ) } with the same name already exists:<br /> ${ existing } ` , ` ${ type } ${ actionName } ` , { escapeHtml : false } ) ;
return false ;
}
toastr . info ( ` Overwriting Existing ${ type } :<br /> ${ existing } ` , ` ${ type } ${ actionName } ` , { escapeHtml : false } ) ;
// If there is an action to delete the existing data, do it, as the name might be slightly different so file name would not be the same
if ( deleteAction ) {
deleteAction ( existing ) ;
}
return true ;
}
2024-05-27 03:35:03 +02:00
/ * *
* Generates a free name by appending a counter to the given name if it already exists in the list
*
* @ param { string } name - The original name to check for existence in the list
* @ param { string [ ] } list - The list of names to check for existence
* @ param { ( n : number ) => string } [ numberFormatter = ( n ) => ` # ${ n } ` ] - The function used to format the counter
* @ returns { string } The generated free name
* /
export function getFreeName ( name , list , numberFormatter = ( n ) => ` # ${ n } ` ) {
if ( ! list . includes ( name ) ) {
return name ;
}
let counter = 1 ;
while ( list . includes ( ` ${ name } # ${ counter } ` ) ) {
counter ++ ;
}
return ` ${ name } ${ numberFormatter ( counter ) } ` ;
}