( function() {
	/** globals frmGlobal */

	let __;

	if ( 'undefined' === typeof wp || wp.i18n === undefined || 'function' !== typeof wp.i18n.__ ) {
		__ = text => text;
	} else {
		__ = wp.i18n.__;
	}

	const modal = {
		maybeCreateModal: ( id, { title, content, footer, width, dialogClass } = {} ) => {
			let modal = document.getElementById( id );

			if ( ! modal ) {
				modal = createEmptyModal( id );

				const titleElement = div( {
					className: 'frm-modal-title'
				} );

				if ( 'string' === typeof title ) {
					titleElement.textContent = title;
				}

				const a = tag(
					'a',
					{
						child: svg( { href: '#frm_close_icon' } ),
						className: 'dismiss'
					}
				);
				const postbox = modal.querySelector( '.postbox' );

				postbox.append(
					div( {
						className: 'frm_modal_top',
						children: [
							titleElement,
							div( { child: a } )
						]
					} )
				);
				postbox.append(
					div( { className: 'frm_modal_content' } )
				);

				if ( footer ) {
					postbox.append(
						div( { className: 'frm_modal_footer' } )
					);
				}
			} else if ( 'string' === typeof title ) {
				const titleElement = modal.querySelector( '.frm-modal-title' );
				titleElement.textContent = title;
			}

			if ( ! content && ! footer ) {
				makeModalIntoADialogAndOpen( modal, { width, dialogClass } );
				return modal;
			}

			const postbox = modal.querySelector( '.postbox' );
			const modalHelper = getModalHelper( modal, postbox );

			if ( content ) {
				modalHelper( content, 'frm_modal_content' );
			}

			if ( footer ) {
				modalHelper( footer, 'frm_modal_footer' );
			}

			makeModalIntoADialogAndOpen( modal, { width, dialogClass } );
			return modal;
		},
		footerButton: args => {
			const output = a( args );
			output.setAttribute( 'role', 'button' );
			output.setAttribute( 'tabindex', 0 );
			if ( args.buttonType ) {
				output.classList.add( 'button' );

				if ( ! args.noDismiss && [ 'red', 'primary' ].includes( args.buttonType ) ) {
					// Primary and red buttons close modals by default on click.
					// To disable this default behaviour you can use the noDismiss: 1 arg.
					output.classList.add( 'dismiss' );
				}

				switch ( args.buttonType ) {
					case 'red':
						output.classList.add( 'frm-button-red', 'frm-button-primary' );
						break;
					case 'primary':
						output.classList.add( 'button-primary', 'frm-button-primary' );
						break;
					case 'secondary':
						output.classList.add( 'button-secondary', 'frm-button-secondary' );
						output.style.marginRight = '10px';
						break;
					case 'cancel':
						output.classList.add( 'button-secondary', 'frm-modal-cancel' );
						break;
				}
			}
			return output;
		}
	};

	const ajax = {
		async doJsonFetch( action ) {
			let targetUrl = `${ ajaxurl }?action=frm_${ action }`;
			if ( ! targetUrl.includes( 'nonce=' ) ) {
				targetUrl += `&nonce=${ frmGlobal.nonce }`;
			}
			const response = await fetch( targetUrl );
			const json = await response.json();
			if ( ! json.success ) {
				return Promise.reject( json.data || 'JSON result is not successful' );
			}
			return Promise.resolve( json.data );
		},
		async doJsonPost( action, formData, { signal } = {} ) {
			formData.append( 'nonce', frmGlobal.nonce );
			const init = {
				method: 'POST',
				body: formData
			};
			if ( signal ) {
				init.signal = signal;
			}
			const response = await fetch( `${ ajaxurl }?action=frm_${ action }`, init );
			const json = await response.json();
			if ( ! json.success ) {
				return Promise.reject( json.data || 'JSON result is not successful' );
			}
			return Promise.resolve( json.data !== undefined ? json.data : json );
		}
	};

	const multiselect = {
		init() {
			const $select = jQuery( this );
			const id = $select.is( '[id]' ) ? $select.attr( 'id' ).replace( '[]', '' ) : false;

			let labelledBy = id ? jQuery( `#for_${ id }` ) : false;
			labelledBy = id && labelledBy.length ? `aria-labelledby="${ labelledBy.attr( 'id' ) }"` : '';

			// Set empty title attributes so that none of the dropdown options include title attributes.
			$select.find( 'option' ).attr( 'title', ' ' );
			$select.multiselect( {
				templates: {
					popupContainer: '<div class="multiselect-container frm-dropdown-menu dropdown-menu"></div>',
					option: '<button type="button" class="multiselect-option dropdown-item frm_no_style_button"></button>',
					button: `<button type="button" class="multiselect dropdown-toggle btn" data-bs-toggle="dropdown" ${ labelledBy }><span class="multiselect-selected-text"></span> <b class="caret"></b></button>`
				},
				buttonContainer: '<div class="btn-group frm-btn-group dropdown" />',
				nonSelectedText: __( '— Select —', 'formidable' ),
				// Prevent the dropdown from showing "All Selected" when every option is checked.
				allSelectedText: '',
				// This is 3 by default. We want to show more options before it starts showing a count.
				numberDisplayed: 8,
				onInitialized( _, $container ) {
					$container.find( '.multiselect.dropdown-toggle' ).removeAttr( 'title' );
				},
				onDropdownShown( event ) {
					const action = jQuery( event.currentTarget.closest( '.frm_form_action_settings, #frm-show-fields' ) );
					if ( action.length ) {
						jQuery( '#wpcontent' ).on( 'click', function() {
							if ( jQuery( '.multiselect-container.frm-dropdown-menu' ).is( ':visible' ) ) {
								jQuery( event.currentTarget ).removeClass( 'open' );
							}
						} );
					}

					const $dropdown = $select.next( '.frm-btn-group.dropdown' );
					$dropdown.find( '.dropdown-item' ).each(
						function() {
							const option = this;
							const dropdownInput = option.querySelector( 'input[type="checkbox"], input[type="radio"]' );
							if ( dropdownInput ) {
								option.setAttribute( 'role', 'checkbox' );
								option.setAttribute( 'aria-checked', dropdownInput.checked ? 'true' : 'false' );
							}
						}
					);
				},
				onChange( $option, checked ) {
					$select.trigger( 'frm-multiselect-changed', $option, checked );

					const $dropdown = $select.next( '.frm-btn-group.dropdown' );
					const optionValue = $option.val();
					const $dropdownItem = $dropdown.find( `input[value="${ optionValue }"]` ).closest( 'button.dropdown-item' );
					if ( $dropdownItem.length ) {
						$dropdownItem.attr( 'aria-checked', checked ? 'true' : 'false' );

						// Delay a focus event so the screen reader reads the option value again.
						// Without this, and without the setTimeout, it only reads "checked" or "unchecked".
						setTimeout( () => $dropdownItem.get( 0 ).focus(), 0 );
					}
				}
			} );
		}
	};

	const bootstrap = {
		setupBootstrapDropdowns() {
			// This function is no longer necessary.
			// It's call in Pro though, so keep it to avoid any errors for now.
		},
		multiselect
	};

	const autocomplete = {
		initSelectionAutocomplete( container ) {
			if ( jQuery.fn.autocomplete ) {
				autocomplete.initAutocomplete( 'page', container );
				autocomplete.initAutocomplete( 'user', container );
				autocomplete.initAutocomplete( 'custom', container );
			}
		},
		/**
		 * Init autocomplete.
		 *
		 * @since 4.10.01 Add container param to init autocomplete elements inside an element.
		 *
		 * @param {string}        type      Type of data. Accepts `page` or `user`.
		 * @param {string|Object} container Container class or element. Default is null.
		 */
		initAutocomplete( type, container ) {
			const basedUrlParams = `?action=frm_${ type }_search&nonce=${ frmGlobal.nonce }`;
			const elements = ! container ? jQuery( `.frm-${ type }-search` ) : jQuery( container ).find( `.frm-${ type }-search` );

			elements.each( initAutocompleteForElement );

			function initAutocompleteForElement() {
				let urlParams = basedUrlParams;
				const element = jQuery( this );

				// Check if a custom post type is specific.
				if ( element.attr( 'data-post-type' ) ) {
					urlParams += `&post_type=${ element.attr( 'data-post-type' ) }`;
				}

				let source = ajaxurl + urlParams;

				if ( this.dataset.source ) {
					const sourceData = JSON.parse( this.dataset.source );
					if ( sourceData ) {
						source = sourceData;
					}
				}

				element.autocomplete( {
					delay: 100,
					minLength: 0,
					source,
					change: autocomplete.selectBlank,
					select: autocomplete.completeSelectFromResults,
					focus: () => false,
					position: {
						my: 'left top',
						at: 'left bottom',
						collision: 'flip'
					},
					response( event, ui ) {
						if ( ! ui.content.length ) {
							const noResult = {
								value: '',
								label: frm_admin_js.no_items_found
							};
							ui.content.push( noResult );
						}
					},
					create() {
						let $container = jQuery( this ).parent();

						if ( $container.length === 0 ) {
							$container = 'body';
						}

						jQuery( this ).autocomplete( 'option', 'appendTo', $container );
					}
				} )
					.on( 'focus', function() {
					// Show options on click to make it work more like a dropdown.
						if ( this.value === '' || this.nextElementSibling.value < 1 ) {
							jQuery( this ).autocomplete( 'search', this.value );
						}
					} )
					.data( 'ui-autocomplete' )._renderItem = function( ul, item ) {
						return jQuery( '<li>' )
							.attr( 'aria-label', item.label )
							.append( jQuery( '<div>' ).text( item.label ) )
							.appendTo( ul );
					};
			}
		},

		selectBlank( e, ui ) {
			if ( ui.item === null ) {
				this.nextElementSibling.value = '';

				/**
				 * Fires when an autocomplete value is cleared.
				 *
				 * @since 6.21
				 */
				wp.hooks.doAction( 'frm_autocomplete_clear_value', e, ui, this );
			}
		},

		completeSelectFromResults( e, ui ) {
			e.preventDefault();
			this.value = ui.item.value === '' ? '' : ui.item.label;
			this.nextElementSibling.value = ui.item.value;

			/**
			 * Fires when an autocomplete item is selected.
			 *
			 * @since 6.21
			 */
			wp.hooks.doAction( 'frm_autocomplete_select', e, ui, this );
		}
	};

	const search = {
		wrapInput: ( searchInput, labelText ) => {
			const label = tag(
				'label',
				{
					className: 'screen-reader-text',
					text: labelText
				}
			);
			label.setAttribute( 'for', searchInput.id );
			return tag(
				'p',
				{
					className: 'frm-search',
					children: [
						label,
						span( { className: 'frmfont frm_search_icon' } ),
						searchInput
					]
				}
			);
		},
		newSearchInput: ( id, placeholder, targetClassName, args = {} ) => {
			const input = getAutoSearchInput( id, placeholder );
			const wrappedSearch = search.wrapInput( input, placeholder );
			search.init( input, targetClassName, args );

			function getAutoSearchInput( id, placeholder ) {
				const className = 'frm-search-input frm-auto-search frm-w-full';
				const inputArgs = { id, className };
				const input = tag( 'input', inputArgs );
				input.setAttribute( 'placeholder', placeholder );
				return input;
			}

			return wrappedSearch;
		},
		init: ( input, targetClassName, { handleSearchResult } = {} ) => {
			input.setAttribute( 'type', 'search' );
			input.setAttribute( 'autocomplete', 'off' );

			input.addEventListener( 'input', handleSearch );
			input.addEventListener( 'search', handleSearch );
			input.addEventListener( 'change', handleSearch );

			function handleSearch( event ) {
				const searchText = input.value.toLowerCase();
				const notEmptySearchText = searchText !== '';
				const items = Array.from( document.getElementsByClassName( targetClassName ) );

				let foundSomething = false;
				items.forEach( toggleSearchClassesForItem );
				if ( 'function' === typeof handleSearchResult ) {
					handleSearchResult( { foundSomething, notEmptySearchText }, event );
				}

				function toggleSearchClassesForItem( item ) {
					let itemText;

					if ( item.hasAttribute( 'frm-search-text' ) ) {
						itemText = item.getAttribute( 'frm-search-text' );
					} else {
						itemText = item.innerText.toLowerCase();
						item.setAttribute( 'frm-search-text', itemText );
					}

					const hide = notEmptySearchText && ! itemText.includes( searchText );
					item.classList.toggle( 'frm_hidden', hide );

					const isSearchResult = ! hide && notEmptySearchText;
					if ( isSearchResult ) {
						foundSomething = true;
					}
					item.classList.toggle( 'frm-search-result', isSearchResult );
				}
			}
		}
	};

	const util = {
		debounce: ( func, wait = 100 ) => {
			let timeout;
			return function( ...args ) {
				clearTimeout( timeout );
				timeout = setTimeout(
					() => func.apply( this, args ),
					wait
				);
			};
		},
		onClickPreventDefault: ( element, callback ) => {
			const listener = event => {
				event.preventDefault();
				callback( event );
			};
			element?.addEventListener( 'click', listener );
		},

		/**
		 * Does the same as jQuery( document ).on( 'event', 'selector', handler ).
		 *
		 * @since 6.0
		 *
		 * @param {string}         event    Event name.
		 * @param {string}         selector Selector.
		 * @param {Function}       handler  Handler.
		 * @param {boolean|Object} options  Options to be added to `addEventListener()` method. Default is `false`.
		 */
		documentOn: ( event, selector, handler, options ) => {
			if ( options === undefined ) {
				options = false;
			}

			document.addEventListener( event, function( e ) {
				let target;

				// loop parent nodes from the target to the delegation node.
				for ( target = e.target; target && target != this; target = target.parentNode ) {
					if ( target.matches && target.matches( selector ) ) {
						handler.call( target, e );
						break;
					}
				}
			}, options );
		},

		/**
		 * Retrieves the value of a cookie by its name.
		 *
		 * @param {string} name - The name of the cookie.
		 * @return {string|null} The value of the cookie, or undefined if the cookie does not exist.
		 */
		getCookie: name => {
			const cookie = document.cookie.split( '; ' ).find( cookie => cookie.startsWith( `${ name }=` ) );

			if ( cookie ) {
				return cookie.split( '=' )[ 1 ];
			}
			return null;
		},

		/**
		 * Sets a cookie with the specified name, value, and expiration time.
		 *
		 * @param {string} name    - The name of the cookie.
		 * @param {string} value   - The value of the cookie.
		 * @param {number} minutes - The number of minutes until the cookie expires.
		 */
		setCookie: ( name, value, minutes ) => {
			const expires = new Date();
			expires.setTime( expires.getTime() + ( minutes * 60 * 1000 ) );
			document.cookie = `${ name }=${ value };expires=${ expires.toUTCString() };path=/`;
		}
	};

	const wysiwyg = {
		init( editor, { setupCallback, height, addFocusEvents } = {} ) {
			if ( isTinyMceActive() ) {
				setTimeout( resetTinyMce, 0 );
			} else {
				initQuickTagsButtons();
			}

			setUpTinyMceVisualButtonListener();
			setUpTinyMceHtmlButtonListener();

			function initQuickTagsButtons() {
				if ( 'function' !== typeof window.quicktags || window.QTags.instances[ editor.id ] !== undefined ) {
					return;
				}

				const { id } = editor;
				window.quicktags( {
					name: `qt_${ id }`,
					id,
					canvas: editor,
					settings: { id },
					toolbar: document.getElementById( `qt_${ id }_toolbar` ),
					theButtons: {}
				} );
			}

			function initRichText() {
				const key = Object.keys( tinyMCEPreInit.mceInit )[ 0 ];
				const orgSettings = tinyMCEPreInit.mceInit[ key ];

				const settings = Object.assign(
					{},
					orgSettings,
					{
						selector: `#${ editor.id }`,
						body_class: orgSettings.body_class.replace( key, editor.id )
					}
				);

				settings.setup = editor => {
					if ( addFocusEvents ) {
						function focusInCallback() {
							jQuery( editor.targetElm ).trigger( 'focusin' );
							editor.off( 'focusin', '**' );
						}

						editor.on( 'focusin', focusInCallback );

						editor.on( 'focusout', function() {
							editor.on( 'focusin', focusInCallback );
						} );
					}
					if ( setupCallback ) {
						setupCallback( editor );
					}
				};

				if ( height ) {
					settings.height = height;
				}

				tinymce.init( settings );
			}

			function removeRichText() {
				tinymce.EditorManager.execCommand( 'mceRemoveEditor', true, editor.id );
			}

			function resetTinyMce() {
				removeRichText();
				initRichText();
			}

			function isTinyMceActive() {
				const { id } = editor;
				const wrapper = document.getElementById( `wp-${ id }-wrap` );
				return null !== wrapper && wrapper.classList.contains( 'tmce-active' );
			}

			function setUpTinyMceVisualButtonListener() {
				jQuery( document ).on(
					'click', `#${ editor.id }-html`,
					function() {
						editor.style.visibility = 'visible';
						initQuickTagsButtons();
					}
				);
			}

			function setUpTinyMceHtmlButtonListener() {
				jQuery( `#${ editor.id }-tmce` ).on( 'click', handleTinyMceHtmlButtonClick );
			}

			function handleTinyMceHtmlButtonClick() {
				if ( isTinyMceActive() ) {
					resetTinyMce();
				} else {
					initRichText();
				}

				const wrap = document.getElementById( `wp-${ editor.id }-wrap` );
				wrap.classList.add( 'tmce-active' );
				wrap.classList.remove( 'html-active' );
			}
		}
	};

	function getModalHelper( modal, appendTo ) {
		return function( child, uniqueClassName ) {
			let element = modal.querySelector( `.${ uniqueClassName }` );
			if ( null === element ) {
				element = div( {
					child,
					className: uniqueClassName
				} );
				appendTo.append( element );
			} else {
				redraw( element, child );
			}
		};
	}

	function createEmptyModal( id ) {
		const modal = div( { id, className: 'frm-modal' } );
		const postbox = div( { className: 'postbox' } );
		const metaboxHolder = div( { className: 'metabox-holder', child: postbox } );
		modal.append( metaboxHolder );
		document.body.append( modal );
		return modal;
	}

	function makeModalIntoADialogAndOpen( modal, { width, dialogClass = '' } = {} ) {
		const bodyWithModalClassName = 'frm-body-with-open-modal';

		const $modal = jQuery( modal );
		if ( ! $modal.hasClass( 'frm-dialog' ) ) {
			$modal.dialog( {
				dialogClass: `frm-dialog ${ dialogClass }`,
				modal: true,
				autoOpen: false,
				closeOnEscape: true,
				width: width || '550px',
				resizable: false,
				draggable: false,
				open() {
					jQuery( '.ui-dialog-titlebar' ).addClass( 'frm_hidden' ).removeClass( 'ui-helper-clearfix' );
					jQuery( '#wpwrap' ).addClass( 'frm_overlay' );
					jQuery( '.frm-dialog' ).removeClass( 'ui-widget ui-widget-content ui-corner-all' );

					modal.classList.remove( 'ui-dialog-content', 'ui-widget-content' );

					$modal.on( 'click', 'a.dismiss', function( event ) {
						event.preventDefault();
						$modal.dialog( 'close' );
					} );

					const overlay = document.querySelector( '.ui-widget-overlay' );
					if ( overlay ) {
						overlay.addEventListener(
							'click',
							function( event ) {
								event.preventDefault();
								$modal.dialog( 'close' );
							}
						);
					}
				},
				close() {
					document.body.classList.remove( bodyWithModalClassName );
					jQuery( '#wpwrap' ).removeClass( 'frm_overlay' );
					jQuery( '.spinner' ).css( 'visibility', 'hidden' );
				}
			} );
		}

		document.body.classList.add( bodyWithModalClassName );

		$modal.dialog( 'open' );
		return $modal;
	}

	function div( args ) {
		return tag( 'div', args );
	}

	function span( args ) {
		return tag( 'span', args );
	}

	function a( args = {} ) {
		const anchor = tag( 'a', args );
		anchor.setAttribute( 'href', 'string' === typeof args.href ? args.href : '#' );
		if ( 'string' === typeof args.target ) {
			anchor.target = args.target;
		}
		return anchor;
	}

	function img( args = {} ) {
		const output = tag( 'img', args );
		if ( 'string' === typeof args.src ) {
			output.setAttribute( 'src', args.src );
		}
		if ( 'string' === typeof args.alt ) {
			output.setAttribute( 'alt', args.alt );
		}
		return output;
	}

	/**
	 * Get a labelled text input and a matching label.
	 *
	 * @since 6.0
	 *
	 * @param {string} inputId
	 * @param {string} labelText
	 * @param {string} inputName
	 * @return {Element} The div element containing the label and input.
	 */
	function labelledTextInput( inputId, labelText, inputName ) {
		const label = tag( 'label', labelText );
		label.setAttribute( 'for', inputId );

		const input = tag(
			'input',
			{
				id: inputId,
				className: 'frm_long_input'
			}
		);
		input.type = 'text';
		input.setAttribute( 'name', inputName );

		return div( { children: [ label, input ] } );
	}

	/**
	 * Build an element.
	 *
	 * @since 6.4.1 Accept a string as one of `children` to append a text node inside the element.
	 *
	 * @param {string} type Element tag name.
	 * @param {Object} args The args.
	 * @return {Object} The created DOM element.
	 */
	function tag( type, args = {} ) {
		const output = document.createElement( type );

		if ( 'string' === typeof args ) {
			// Support passing just a string to a tag for simple text elements.
			output.textContent = args;
			return output;
		}

		const { id, className, children, child, text, data } = args;

		if ( id ) {
			output.id = id;
		}
		if ( className ) {
			output.className = className;
		}
		if ( children ) {
			children.forEach( child => {
				if ( 'string' === typeof child ) {
					output.append( document.createTextNode( child ) );
				} else {
					output.append( child );
				}
			} );
		} else if ( child ) {
			output.append( child );
		} else if ( text ) {
			output.textContent = text;
		}
		if ( data ) {
			Object.keys( data ).forEach( function( dataKey ) {
				output.setAttribute( `data-${ dataKey }`, data[ dataKey ] );
			} );
		}
		return output;
	}

	function svg( { href, classList } = {} ) {
		const namespace = 'http://www.w3.org/2000/svg';
		const output = document.createElementNS( namespace, 'svg' );
		if ( classList ) {
			output.classList.add( ...classList );
		}

		if ( href ) {
			const use = document.createElementNS( namespace, 'use' );
			use.setAttribute( 'href', href );
			output.append( use );
			output.classList.add( 'frmsvg' );
		}
		return output;
	}

	/**
	 * Pop up a success message in the lower right corner.
	 * It then fades out and gets deleted automatically.
	 *
	 * @param {HTMLElement|string} content
	 * @return {void}
	 */
	function success( content ) {
		const container = document.getElementById( 'wpbody' );
		const notice = div( {
			className: 'frm_updated_message frm-floating-success-message',
			child: div( {
				className: 'frm-satisfied',
				child: 'string' === typeof content ? document.createTextNode( content ) : content
			} )
		} );
		container.append( notice );

		setTimeout(
			() => jQuery( notice ).fadeOut( () => notice.remove() ),
			2000
		);
	}

	function setAttributes( element, attrs ) {
		Object.entries( attrs ).forEach(
			( [ key, value ] ) => element.setAttribute( key, value )
		);
	}

	function redraw( element, child ) {
		element.innerHTML = '';
		element.append( child );
	}

	const allowedHtml = {
		b: [],
		div: [ 'class' ],
		img: [ 'src', 'alt' ],
		p: [],
		span: [ 'class' ],
		strong: [],
		svg: [ 'class' ],
		use: [],
		a: [ 'href', 'class' ]
	};

	function cleanNode( node ) {
		if ( node.tagName === undefined ) {
			if ( '#text' === node.nodeName ) {
				return document.createTextNode( node.textContent );
			}
			return document.createTextNode( '' );
		}

		const tagType = node.tagName.toLowerCase();

		if ( 'svg' === tagType ) {
			const svgArgs = {
				classList: Array.from( node.classList )
			};
			const use = node.querySelector( 'use' );
			if ( use ) {
				svgArgs.href = use.getAttribute( 'href' ) || use.getAttribute( 'xlink:href' );
			}
			return svg( svgArgs );
		}

		if ( allowedHtml[ tagType ] === undefined ) {
			// Tag type is not allowed.
			return document.createTextNode( '' );
		}

		const newNode = document.createElement( tagType );

		allowedHtml[ tagType ].forEach(
			allowedTag => {
				if ( node.hasAttribute( allowedTag ) ) {
					newNode.setAttribute( allowedTag, node.getAttribute( allowedTag ) );
				}
			}
		);

		node.childNodes.forEach( child => newNode.append( cleanNode( child ) ) );
		return newNode;
	}

	window.frmDom = { tag, div, span, a, img, labelledTextInput, svg, setAttributes, success, modal, ajax, bootstrap, autocomplete, search, util, wysiwyg, cleanNode };
}() );
