/**
* A set of generic utility functions used in this project.
*
* @module Utilities
*/
/**
* Create a script element to load a script from the given URL, append that
* script element to the page's head, and notify us (via a returned `Promise`)
* when the script completes loading successfully or fails with an error.
*
* Example use:
* ```
* loadScript( 'https://some.cdn.org/script.js' ).then( () => {
* // Run code that depends on the script having loaded.
* } )
* ```
*
* Note that if this function has already been called on this URL, so that there
* already is a script tag with this source, then the promise resolves
* immediately without doing anything first.
*
* @param {String} url - URL of the script to load
* @returns {Promise} a promise that is resolved if the script finishes loading
* or rejected if the script encounters an error
* @function
*/
export const loadScript = url =>
Array.from( document.head.querySelectorAll( 'script' ) ).some(
script => script.getAttribute( 'src' ) == url ) ?
Promise.resolve() :
new Promise( ( resolve, reject ) => {
const scriptTag = document.createElement( 'script' )
document.head.append( scriptTag )
scriptTag.setAttribute( 'defer', true )
scriptTag.setAttribute( 'referrerpolicy', 'origin' )
scriptTag.addEventListener( 'load', resolve )
scriptTag.addEventListener( 'error', reject )
scriptTag.setAttribute( 'src', url )
} )
/**
* From any JavaScript object, we can create another object by first
* constructing a new instance of the Object prototype, then copying over all of
* the "own" properties from the original object (by reference). This is like
* the original object, but without its prototype or any other inherited data or
* methods. This function does that.
*
* @param {Object} object - any JavaScript object
* @returns {Object} a shallow copy of the original object, but without copying
* its prototype information
* @function
*/
export const copyWithoutPrototype = object => {
const result = new Object()
for ( let key in object )
if ( object.hasOwnProperty( key ) )
result[key] = object[key]
return result
}
/**
* Return the URL for this app. It is taken directly from the browser's
* navigation bar, but it excludes any query string that may be present.
* It will be of the form `protocol://domain.name/path/to/file.html`.
*
* @function
* @see {@link module:ImportFromURL.autoOpenLink autoOpenLink()}
*/
export const appURL = () => {
const result = window.location.protocol + '//'
+ window.location.host + window.location.pathname
return result.endsWith( '/' ) ? result : result + '/'
}
/**
* Escape a string so that it can be safely inserted into an HTML document and
* still represent the plain text within the given string (not interpreting the
* string as HTML itself). For example, the string `"x < a and a > b"` should
* appear exactly that way in the rendered HTML, meaning that the `<` and `>`
* will need to be escaped so that `"<a and a>"` does not appear to be a tag.
*
* @param {string} text - text to escape for insertion into HTML
* @returns {string} the same text, but with the characters `"&"`, `"<"`, `">"`,
* `"'"`, and `'"'` replaced with character references instead
* @function
*/
export const escapeHTML = ( text = '' ) =>
text.replaceAll( '&', '&' )
.replaceAll( '<', '<' )
.replaceAll( '>', '>' )
.replaceAll( '"', '"' )
.replaceAll( "'", ''' )
/**
* The following function takes as input an string containing HTML code and
* removes from it all script tags, so that the code can be used safely within
* the app itself, knowing that no malicious code will be executed.
*
* @param {string} html - the HTML code from which to remove script tags
* @returns {string} the same HTML code, but with all script tags removed
* @function
*/
export const removeScriptTags = html => {
const regex = /<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi
return html.replace( regex, '' )
}
/**
* This function makes it easy to construct two-column tables of HTML content,
* which is something that several types of {@link module:Atoms.Atom Atoms} will
* want to do. The arguments to the function are the rows of the table, and
* they are treated as follows.
*
* * Any string is treated as the content for a row of the table spanning both
* columns (using `colspan`) and in bold font.
* * An array of strings containing just one entry is treated the same as a
* single string.
* * An array of two strings is treated as the contents of the two cells in
* the row.
* * An array of three strings is treated as two rows, first a two-cell row,
* and then an optional error row (only if the third entry is not falsy) that
* places the error message in red font in the second cell.
*
* Example use:
*
* ```js
* simpleHTMLTable(
* 'Here is the information you entered:',
* [ 'Your name:', 'Frederick the Great' ],
* [ 'Your age:', '42' ],
* [ 'Your favorite color:', color,
* !isColor(color) && 'That is not a valid color.' ]
* )
* ```
*
* @param {...any} rows - the data representing the rows of the table to construct
* @returns {string} the HTML code for the table
* @function
*/
export const simpleHTMLTable = ( ...rows ) => {
let result = '<table><colgroup><col><col></colgroup>'
const row = inside => `<tr>${removeScriptTags( inside )}</tr>`
const cell = inside => `<td>${removeScriptTags( inside )}</td>`
const bigCell = inside =>
`<td colspan='2'><b>${removeScriptTags( inside )}</b></td>`
const error = inside =>
`<td><font color=red>${removeScriptTags( inside )}</font></td>`
rows.forEach( rowData => {
if ( typeof( rowData ) == 'string' ) {
result += row( bigCell( rowData ) )
} else if ( rowData.length == 1 ) {
result += row( bigCell( rowData[0] ) )
} else if ( rowData.length == 2 ) {
result += row( cell( rowData[0] ) + cell( rowData[1] ) )
} else if ( rowData.length == 3 ) {
result += row( cell( rowData[0] ) + cell( rowData[1] ) )
if ( rowData[2] ) result += row( cell( '' ) + error( rowData[2] ) )
}
} )
result += '</table>'
return result
}
/**
* Given an ordered set of HTML Nodes in an array, and a node in the same
* document, return just the subset of `nodes` that appear before `point`.
* Because the given set of nodes are in order, this subset will always be an
* initial segment of the given array. It can be empty (if none precede `point`)
* and it can be the whole array (if all preceded `point`).
*
* While this could be done with a simple array filter, that could be slow on
* larger arrays; this uses a binary search. Furthermore, node comparisons are
* a tedious process that uses an enum, so this function is simpler.
*
* @param {Node[]} nodes - ordered array of Nodes to filter
* @param {Node} point - the node that will determine which subset of `nodes`
* gets rerturned
* @returns {Node[]} some initial segment of `nodes`, including precisely those
* that appear before `point`
* @function
*/
export const onlyBefore = ( nodes, point ) => {
const lt = ( a, b ) => {
const comparison = a.compareDocumentPosition( b )
if ( ( comparison & Node.DOCUMENT_POSITION_FOLLOWING ) // b follows a
|| ( comparison & Node.DOCUMENT_POSITION_CONTAINED_BY ) ) // a contains b
return true
if ( ( comparison & Node.DOCUMENT_POSITION_PRECEDING ) // b precedes a
|| ( comparison & Node.DOCUMENT_POSITION_CONTAINS ) // b contains a
|| ( comparison == 0 ) ) // a is the same node as b
return false
throw new Error( 'Cannot compare document positions' )
}
try {
if ( nodes.length == 0 || !lt( nodes[0], point ) ) return [ ]
if ( lt( nodes[nodes.length-1], point ) ) return nodes
let indexLT = 0
let indexGE = nodes.length - 1
while ( indexLT < indexGE - 1 ) {
const midIndex = Math.floor( ( indexLT + indexGE ) / 2 )
if ( lt( nodes[midIndex], point ) ) indexLT = midIndex
else indexGE = midIndex
}
return Array.from( nodes ).slice( 0, indexGE )
} catch ( e ) {
console.log( 'DEBUG:', e )
return nodes
}
}
/**
* Given a DOM Node, find the TinyMCE editor containing it.
*
* @param {Node} node - HTML node for which to find the editor
* @returns {tinymce.Editor} the editor whose document contains the given node
* @function
*/
export const editorForNode = node => {
const allEditors = tinymce.get()
for ( let i = 0 ; i < allEditors.length ; i++ )
if ( allEditors[i].getDoc() == node.ownerDocument )
return allEditors[i]
return null
}
/**
* Call a function on each element in an array, just like `array.forEach()`
* would do, except use a zero-second timeout between each call.
*
* @param {Function} func - the function to call for each element
* @param {any[]} array - the array to iterate over
* @param {number} [timeout=0] - the number of milliseconds to wait between calls
* @function
*/
Array.prototype.forEachWithTimeout = function( func, timeout = 0 ) {
if ( this.length == 0 ) return
func( this[0] )
setTimeout( () =>
this.slice(1).forEachWithTimeout( func, timeout ), timeout )
}
/**
* TinyMCE sometimes stores elements off screen, but still part of the document,
* so if we search for elements by selector, we will find them, even though they
* are invisible and should not be taken into account as part of the user's
* document content. This function checks to see if a given DOM nodes is really
* a visible, relevant part of the document or not.
*
* @param {Node} node - the node to test
* @returns {boolean} true if the node is on screen (and "real")
*/
export const isOnScreen = node => node.parentNode &&
!node.parentNode.classList.contains( 'mce-offscreen-selection' )
source