/** * External dependencies */ import { isString } from '@woocommerce/types'; interface LazyLoadScriptParams { handle: string; src: string; version?: string; after?: string; before?: string; translations?: string; } interface AppendScriptAttributesParam { id: string; innerHTML?: string; onerror?: OnErrorEventHandlerNonNull; onload?: () => void; src?: string; } /** * In WP, registered scripts are loaded into the page with an element like this: * `<script src='...' id='[SCRIPT_ID]'></script>` * This function checks whether an element matching that selector exists. * Useful to know if a script has already been appended to the page. */ const isScriptTagInDOM = ( scriptId: string ): boolean => { const scriptElements = document.querySelectorAll( `script#${ scriptId }` ); return scriptElements.length > 0; }; /** * Appends a script element to the document body if a script with the same id * doesn't exist. */ const appendScript = ( attributes: AppendScriptAttributesParam ): void => { // Abort if id is not valid or a script with the same id exists. if ( ! isString( attributes.id ) || isScriptTagInDOM( attributes.id ) ) { return; } const scriptElement = document.createElement( 'script' ); for ( const attr in attributes ) { // We could technically be iterating over inherited members here, so // if this is the case we should skip it. if ( ! attributes.hasOwnProperty( attr ) ) { continue; } const key = attr as keyof AppendScriptAttributesParam; // Skip the keys that aren't strings, because TS can't be sure which // key in the scriptElement object we're assigning to. if ( key === 'onload' || key === 'onerror' ) { continue; } // This assignment stops TS complaining about the value maybe being // undefined following the isString check below. const value = attributes[ key ]; if ( isString( value ) ) { scriptElement[ key ] = value; } } // Now that we've assigned all the strings, we can explicitly assign to the // function keys. if ( typeof attributes.onload === 'function' ) { scriptElement.onload = attributes.onload; } if ( typeof attributes.onerror === 'function' ) { scriptElement.onerror = attributes.onerror; } document.body.appendChild( scriptElement ); }; /** * Appends a `<script>` tag to the document body based on the src and handle * parameters. In addition, it appends additional script tags to load the code * needed for translations and any before and after inline scripts. See these * documentation pages for more information: * * https://developer.wordpress.org/reference/functions/wp_set_script_translations/ * https://developer.wordpress.org/reference/functions/wp_add_inline_script/ */ const lazyLoadScript = ( { handle, src, version, after, before, translations, }: LazyLoadScriptParams ): Promise< void > => { return new Promise( ( resolve, reject ) => { if ( isScriptTagInDOM( `${ handle }-js` ) ) { resolve(); } if ( translations ) { appendScript( { id: `${ handle }-js-translations`, innerHTML: translations, } ); } if ( before ) { appendScript( { id: `${ handle }-js-before`, innerHTML: before, } ); } const onload = () => { if ( after ) { appendScript( { id: `${ handle }-js-after`, innerHTML: after, } ); } resolve(); }; appendScript( { id: `${ handle }-js`, onerror: reject, onload, src: version ? `${ src }?ver=${ version }` : src, } ); } ); }; export default lazyLoadScript;