PluginProbe
Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More / 6.32.1
Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More v6.32.1
6.35 6.34 6.33.1 6.33 6.32.1 6.32 6.31 6.25 6.25.1 6.26 6.26.1 6.27 6.28 6.29 6.3 6.3.1 6.3.2 6.30 6.4 6.4.1 6.4.2 6.5 6.5.1 6.5.2 6.5.3 All 141 releases
formidable / js / admin / dom.js

dom.js in Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More 6.32.1, at js/admin/dom.js

870 lines 23.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 ( function() {
2 /** globals frmGlobal */
3
4 let __;
5
6 if ( 'undefined' === typeof wp || wp.i18n === undefined || 'function' !== typeof wp.i18n.__ ) {
7 __ = text => text;
8 } else {
9 __ = wp.i18n.__;
10 }
11
12 const modal = {
13 maybeCreateModal: ( id, { title, content, footer, width, dialogClass } = {} ) => {
14 let modal = document.getElementById( id );
15
16 if ( ! modal ) {
17 modal = createEmptyModal( id );
18
19 const titleElement = div( {
20 className: 'frm-modal-title'
21 } );
22
23 if ( 'string' === typeof title ) {
24 titleElement.textContent = title;
25 }
26
27 const a = tag(
28 'a',
29 {
30 child: svg( { href: '#frm_close_icon' } ),
31 className: 'dismiss'
32 }
33 );
34 const postbox = modal.querySelector( '.postbox' );
35
36 postbox.append(
37 div( {
38 className: 'frm_modal_top',
39 children: [
40 titleElement,
41 div( { child: a } )
42 ]
43 } )
44 );
45 postbox.append(
46 div( { className: 'frm_modal_content' } )
47 );
48
49 if ( footer ) {
50 postbox.append(
51 div( { className: 'frm_modal_footer' } )
52 );
53 }
54 } else if ( 'string' === typeof title ) {
55 const titleElement = modal.querySelector( '.frm-modal-title' );
56 titleElement.textContent = title;
57 }
58
59 if ( ! content && ! footer ) {
60 makeModalIntoADialogAndOpen( modal, { width, dialogClass } );
61 return modal;
62 }
63
64 const postbox = modal.querySelector( '.postbox' );
65 const modalHelper = getModalHelper( modal, postbox );
66
67 if ( content ) {
68 modalHelper( content, 'frm_modal_content' );
69 }
70
71 if ( footer ) {
72 modalHelper( footer, 'frm_modal_footer' );
73 }
74
75 makeModalIntoADialogAndOpen( modal, { width, dialogClass } );
76 return modal;
77 },
78 footerButton: args => {
79 const output = a( args );
80 output.setAttribute( 'role', 'button' );
81 output.setAttribute( 'tabindex', 0 );
82 if ( args.buttonType ) {
83 output.classList.add( 'button' );
84
85 if ( ! args.noDismiss && [ 'red', 'primary' ].includes( args.buttonType ) ) {
86 // Primary and red buttons close modals by default on click.
87 // To disable this default behaviour you can use the noDismiss: 1 arg.
88 output.classList.add( 'dismiss' );
89 }
90
91 switch ( args.buttonType ) {
92 case 'red':
93 output.classList.add( 'frm-button-red', 'frm-button-primary' );
94 break;
95 case 'primary':
96 output.classList.add( 'button-primary', 'frm-button-primary' );
97 break;
98 case 'secondary':
99 output.classList.add( 'button-secondary', 'frm-button-secondary' );
100 output.style.marginRight = '10px';
101 break;
102 case 'cancel':
103 output.classList.add( 'button-secondary', 'frm-modal-cancel' );
104 break;
105 }
106 }
107 return output;
108 }
109 };
110
111 const ajax = {
112 async doJsonFetch( action ) {
113 let targetUrl = `${ ajaxurl }?action=frm_${ action }`;
114 if ( ! targetUrl.includes( 'nonce=' ) ) {
115 targetUrl += `&nonce=${ frmGlobal.nonce }`;
116 }
117 const response = await fetch( targetUrl );
118 const json = await response.json();
119 if ( ! json.success ) {
120 return Promise.reject( json.data || 'JSON result is not successful' );
121 }
122 return Promise.resolve( json.data );
123 },
124 async doJsonPost( action, formData, { signal } = {} ) {
125 formData.append( 'nonce', frmGlobal.nonce );
126 const init = {
127 method: 'POST',
128 body: formData
129 };
130 if ( signal ) {
131 init.signal = signal;
132 }
133 const response = await fetch( `${ ajaxurl }?action=frm_${ action }`, init );
134 const json = await response.json();
135 if ( ! json.success ) {
136 return Promise.reject( json.data || 'JSON result is not successful' );
137 }
138 return Promise.resolve( json.data !== undefined ? json.data : json );
139 }
140 };
141
142 const multiselect = {
143 init() {
144 const $select = jQuery( this );
145 const id = $select.is( '[id]' ) ? $select.attr( 'id' ).replace( '[]', '' ) : false;
146
147 let labelledBy = id ? jQuery( `#for_${ id }` ) : false;
148 labelledBy = id && labelledBy.length ? `aria-labelledby="${ labelledBy.attr( 'id' ) }"` : '';
149
150 $select.multiselect( {
151 templates: {
152 popupContainer: '<div class="multiselect-container frm-dropdown-menu dropdown-menu"></div>',
153 option: '<button type="button" class="multiselect-option dropdown-item frm_no_style_button"></button>',
154 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>`
155 },
156 buttonContainer: '<div class="btn-group frm-btn-group dropdown" />',
157 nonSelectedText: __( '— Select —', 'formidable' ),
158 // Prevent the dropdown from showing "All Selected" when every option is checked.
159 allSelectedText: '',
160 // This is 3 by default. We want to show more options before it starts showing a count.
161 numberDisplayed: 8,
162 onInitialized( _, $container ) {
163 $container.find( '.multiselect.dropdown-toggle,.multiselect-option.dropdown-item' ).removeAttr( 'title' );
164 },
165 onDropdownShown( event ) {
166 const action = jQuery( event.currentTarget.closest( '.frm_form_action_settings, #frm-show-fields' ) );
167 if ( action.length ) {
168 jQuery( '#wpcontent' ).on( 'click', function() {
169 if ( jQuery( '.multiselect-container.frm-dropdown-menu' ).is( ':visible' ) ) {
170 jQuery( event.currentTarget ).removeClass( 'open' );
171 }
172 } );
173 }
174
175 const $dropdown = $select.next( '.frm-btn-group.dropdown' );
176 $dropdown.find( '.dropdown-item' ).each(
177 function() {
178 const option = this;
179 const dropdownInput = option.querySelector( 'input[type="checkbox"], input[type="radio"]' );
180 if ( dropdownInput ) {
181 option.setAttribute( 'role', 'checkbox' );
182 option.setAttribute( 'aria-checked', dropdownInput.checked ? 'true' : 'false' );
183 }
184 }
185 );
186 },
187 onChange( $option, checked ) {
188 $select.trigger( 'frm-multiselect-changed', $option, checked );
189
190 const $dropdown = $select.next( '.frm-btn-group.dropdown' );
191 const optionValue = $option.val();
192 const $dropdownItem = $dropdown.find( `input[value="${ optionValue }"]` ).closest( 'button.dropdown-item' );
193 if ( $dropdownItem.length ) {
194 $dropdownItem.attr( 'aria-checked', checked ? 'true' : 'false' );
195
196 // Delay a focus event so the screen reader reads the option value again.
197 // Without this, and without the setTimeout, it only reads "checked" or "unchecked".
198 setTimeout( () => $dropdownItem.get( 0 ).focus(), 0 );
199 }
200 }
201 } );
202 }
203 };
204
205 const bootstrap = {
206 setupBootstrapDropdowns() {
207 // This function is no longer necessary.
208 // It's call in Pro though, so keep it to avoid any errors for now.
209 },
210 multiselect
211 };
212
213 const autocomplete = {
214 initSelectionAutocomplete( container ) {
215 if ( jQuery.fn.autocomplete ) {
216 autocomplete.initAutocomplete( 'page', container );
217 autocomplete.initAutocomplete( 'user', container );
218 autocomplete.initAutocomplete( 'custom', container );
219 }
220 },
221 /**
222 * Init autocomplete.
223 *
224 * @since 4.10.01 Add container param to init autocomplete elements inside an element.
225 *
226 * @param {string} type Type of data. Accepts `page` or `user`.
227 * @param {string|Object} container Container class or element. Default is null.
228 */
229 initAutocomplete( type, container ) {
230 const basedUrlParams = `?action=frm_${ type }_search&nonce=${ frmGlobal.nonce }`;
231 const elements = ! container ? jQuery( `.frm-${ type }-search` ) : jQuery( container ).find( `.frm-${ type }-search` );
232
233 elements.each( initAutocompleteForElement );
234
235 function initAutocompleteForElement() {
236 let urlParams = basedUrlParams;
237 const element = jQuery( this );
238
239 // Check if a custom post type is specific.
240 if ( element.attr( 'data-post-type' ) ) {
241 urlParams += `&post_type=${ element.attr( 'data-post-type' ) }`;
242 }
243
244 let source = ajaxurl + urlParams;
245
246 if ( this.dataset.source ) {
247 const sourceData = JSON.parse( this.dataset.source );
248 if ( sourceData ) {
249 source = sourceData;
250 }
251 }
252
253 element.autocomplete( {
254 delay: 100,
255 minLength: 0,
256 source,
257 change: autocomplete.selectBlank,
258 select: autocomplete.completeSelectFromResults,
259 focus: () => false,
260 position: {
261 my: 'left top',
262 at: 'left bottom',
263 collision: 'flip'
264 },
265 response( event, ui ) {
266 if ( ! ui.content.length ) {
267 const noResult = {
268 value: '',
269 label: frm_admin_js.no_items_found
270 };
271 ui.content.push( noResult );
272 }
273 },
274 create() {
275 let $container = jQuery( this ).parent();
276
277 if ( $container.length === 0 ) {
278 $container = 'body';
279 }
280
281 jQuery( this ).autocomplete( 'option', 'appendTo', $container );
282 }
283 } )
284 .on( 'focus', function() {
285 // Show options on click to make it work more like a dropdown.
286 if ( this.value === '' || this.nextElementSibling.value < 1 ) {
287 jQuery( this ).autocomplete( 'search', this.value );
288 }
289 } )
290 .data( 'ui-autocomplete' )._renderItem = function( ul, item ) {
291 return jQuery( '<li>' )
292 .attr( 'aria-label', item.label )
293 .append( jQuery( '<div>' ).text( item.label ) )
294 .appendTo( ul );
295 };
296 }
297 },
298
299 selectBlank( e, ui ) {
300 if ( ui.item === null ) {
301 this.nextElementSibling.value = '';
302
303 /**
304 * Fires when an autocomplete value is cleared.
305 *
306 * @since 6.21
307 */
308 wp.hooks.doAction( 'frm_autocomplete_clear_value', e, ui, this );
309 }
310 },
311
312 completeSelectFromResults( e, ui ) {
313 e.preventDefault();
314 this.value = ui.item.value === '' ? '' : ui.item.label;
315 this.nextElementSibling.value = ui.item.value;
316
317 /**
318 * Fires when an autocomplete item is selected.
319 *
320 * @since 6.21
321 */
322 wp.hooks.doAction( 'frm_autocomplete_select', e, ui, this );
323 }
324 };
325
326 const search = {
327 wrapInput: ( searchInput, labelText ) => {
328 const label = tag(
329 'label',
330 {
331 className: 'screen-reader-text',
332 text: labelText
333 }
334 );
335 label.setAttribute( 'for', searchInput.id );
336 return tag(
337 'p',
338 {
339 className: 'frm-search',
340 children: [
341 label,
342 span( { className: 'frmfont frm_search_icon' } ),
343 searchInput
344 ]
345 }
346 );
347 },
348 newSearchInput: ( id, placeholder, targetClassName, args = {} ) => {
349 const input = getAutoSearchInput( id, placeholder );
350 const wrappedSearch = search.wrapInput( input, placeholder );
351 search.init( input, targetClassName, args );
352
353 function getAutoSearchInput( id, placeholder ) {
354 const className = 'frm-search-input frm-auto-search frm-w-full';
355 const inputArgs = { id, className };
356 const input = tag( 'input', inputArgs );
357 input.setAttribute( 'placeholder', placeholder );
358 return input;
359 }
360
361 return wrappedSearch;
362 },
363 init: ( input, targetClassName, { handleSearchResult } = {} ) => {
364 input.setAttribute( 'type', 'search' );
365 input.setAttribute( 'autocomplete', 'off' );
366
367 input.addEventListener( 'input', handleSearch );
368 input.addEventListener( 'search', handleSearch );
369 input.addEventListener( 'change', handleSearch );
370
371 function handleSearch( event ) {
372 const searchText = input.value.toLowerCase();
373 const notEmptySearchText = searchText !== '';
374 const items = Array.from( document.getElementsByClassName( targetClassName ) );
375
376 let foundSomething = false;
377 items.forEach( toggleSearchClassesForItem );
378 if ( 'function' === typeof handleSearchResult ) {
379 handleSearchResult( { foundSomething, notEmptySearchText }, event );
380 }
381
382 function toggleSearchClassesForItem( item ) {
383 let itemText;
384
385 if ( item.hasAttribute( 'frm-search-text' ) ) {
386 itemText = item.getAttribute( 'frm-search-text' );
387 } else {
388 itemText = item.innerText.toLowerCase();
389 item.setAttribute( 'frm-search-text', itemText );
390 }
391
392 const hide = notEmptySearchText && ! itemText.includes( searchText );
393 item.classList.toggle( 'frm_hidden', hide );
394
395 const isSearchResult = ! hide && notEmptySearchText;
396 if ( isSearchResult ) {
397 foundSomething = true;
398 }
399 item.classList.toggle( 'frm-search-result', isSearchResult );
400 }
401 }
402 }
403 };
404
405 const util = {
406 debounce: ( func, wait = 100 ) => {
407 let timeout;
408 return function( ...args ) {
409 clearTimeout( timeout );
410 timeout = setTimeout(
411 () => func.apply( this, args ),
412 wait
413 );
414 };
415 },
416 onClickPreventDefault: ( element, callback ) => {
417 const listener = event => {
418 event.preventDefault();
419 callback( event );
420 };
421 element?.addEventListener( 'click', listener );
422 },
423
424 /**
425 * Does the same as jQuery( document ).on( 'event', 'selector', handler ).
426 *
427 * @since 6.0
428 *
429 * @param {string} event Event name.
430 * @param {string} selector Selector.
431 * @param {Function} handler Handler.
432 * @param {boolean|Object} options Options to be added to `addEventListener()` method. Default is `false`.
433 */
434 documentOn: ( event, selector, handler, options ) => {
435 if ( options === undefined ) {
436 options = false;
437 }
438
439 document.addEventListener( event, function( e ) {
440 let target;
441
442 // loop parent nodes from the target to the delegation node.
443 for ( target = e.target; target && target != this; target = target.parentNode ) {
444 if ( target.matches && target.matches( selector ) ) {
445 handler.call( target, e );
446 break;
447 }
448 }
449 }, options );
450 },
451
452 /**
453 * Retrieves the value of a cookie by its name.
454 *
455 * @param {string} name - The name of the cookie.
456 * @return {string|null} The value of the cookie, or undefined if the cookie does not exist.
457 */
458 getCookie: name => {
459 const cookie = document.cookie.split( '; ' ).find( cookie => cookie.startsWith( `${ name }=` ) );
460
461 if ( cookie ) {
462 return cookie.split( '=' )[ 1 ];
463 }
464 return null;
465 },
466
467 /**
468 * Sets a cookie with the specified name, value, and expiration time.
469 *
470 * @param {string} name - The name of the cookie.
471 * @param {string} value - The value of the cookie.
472 * @param {number} minutes - The number of minutes until the cookie expires.
473 */
474 setCookie: ( name, value, minutes ) => {
475 const expires = new Date();
476 expires.setTime( expires.getTime() + ( minutes * 60 * 1000 ) );
477 document.cookie = `${ name }=${ value };expires=${ expires.toUTCString() };path=/`;
478 }
479 };
480
481 const wysiwyg = {
482 init( editor, { setupCallback, height, addFocusEvents } = {} ) {
483 if ( isTinyMceActive() ) {
484 setTimeout( resetTinyMce, 0 );
485 } else {
486 initQuickTagsButtons();
487 }
488
489 setUpTinyMceVisualButtonListener();
490 setUpTinyMceHtmlButtonListener();
491
492 function initQuickTagsButtons() {
493 if ( 'function' !== typeof window.quicktags || window.QTags.instances[ editor.id ] !== undefined ) {
494 return;
495 }
496
497 const { id } = editor;
498 window.quicktags( {
499 name: `qt_${ id }`,
500 id,
501 canvas: editor,
502 settings: { id },
503 toolbar: document.getElementById( `qt_${ id }_toolbar` ),
504 theButtons: {}
505 } );
506 }
507
508 function initRichText() {
509 const key = Object.keys( tinyMCEPreInit.mceInit )[ 0 ];
510 const orgSettings = tinyMCEPreInit.mceInit[ key ];
511
512 const settings = Object.assign(
513 {},
514 orgSettings,
515 {
516 selector: `#${ editor.id }`,
517 body_class: orgSettings.body_class.replace( key, editor.id )
518 }
519 );
520
521 settings.setup = editor => {
522 if ( addFocusEvents ) {
523 function focusInCallback() {
524 jQuery( editor.targetElm ).trigger( 'focusin' );
525 editor.off( 'focusin', '**' );
526 }
527
528 editor.on( 'focusin', focusInCallback );
529
530 editor.on( 'focusout', function() {
531 editor.on( 'focusin', focusInCallback );
532 } );
533 }
534 if ( setupCallback ) {
535 setupCallback( editor );
536 }
537 };
538
539 if ( height ) {
540 settings.height = height;
541 }
542
543 tinymce.init( settings );
544 }
545
546 function removeRichText() {
547 tinymce.EditorManager.execCommand( 'mceRemoveEditor', true, editor.id );
548 }
549
550 function resetTinyMce() {
551 removeRichText();
552 initRichText();
553 }
554
555 function isTinyMceActive() {
556 const { id } = editor;
557 const wrapper = document.getElementById( `wp-${ id }-wrap` );
558 return wrapper && wrapper.classList.contains( 'tmce-active' );
559 }
560
561 function setUpTinyMceVisualButtonListener() {
562 jQuery( document ).on(
563 'click', `#${ editor.id }-html`,
564 function() {
565 editor.style.visibility = 'visible';
566 initQuickTagsButtons();
567 }
568 );
569 }
570
571 function setUpTinyMceHtmlButtonListener() {
572 jQuery( `#${ editor.id }-tmce` ).on( 'click', handleTinyMceHtmlButtonClick );
573 }
574
575 function handleTinyMceHtmlButtonClick() {
576 if ( isTinyMceActive() ) {
577 resetTinyMce();
578 } else {
579 initRichText();
580 }
581
582 const wrap = document.getElementById( `wp-${ editor.id }-wrap` );
583 wrap.classList.add( 'tmce-active' );
584 wrap.classList.remove( 'html-active' );
585 }
586 }
587 };
588
589 function getModalHelper( modal, appendTo ) {
590 return function( child, uniqueClassName ) {
591 let element = modal.querySelector( `.${ uniqueClassName }` );
592 if ( null === element ) {
593 element = div( {
594 child,
595 className: uniqueClassName
596 } );
597 appendTo.append( element );
598 } else {
599 redraw( element, child );
600 }
601 };
602 }
603
604 function createEmptyModal( id ) {
605 const modal = div( { id, className: 'frm-modal' } );
606 const postbox = div( { className: 'postbox' } );
607 const metaboxHolder = div( { className: 'metabox-holder', child: postbox } );
608 modal.append( metaboxHolder );
609 document.body.append( modal );
610 return modal;
611 }
612
613 function makeModalIntoADialogAndOpen( modal, { width, dialogClass = '' } = {} ) {
614 const bodyWithModalClassName = 'frm-body-with-open-modal';
615
616 const $modal = jQuery( modal );
617 if ( ! $modal.hasClass( 'frm-dialog' ) ) {
618 $modal.dialog( {
619 dialogClass: `frm-dialog ${ dialogClass }`,
620 modal: true,
621 autoOpen: false,
622 closeOnEscape: true,
623 width: width || '550px',
624 resizable: false,
625 draggable: false,
626 open() {
627 jQuery( '.ui-dialog-titlebar' ).addClass( 'frm_hidden' ).removeClass( 'ui-helper-clearfix' );
628 jQuery( '#wpwrap' ).addClass( 'frm_overlay' );
629 jQuery( '.frm-dialog' ).removeClass( 'ui-widget ui-widget-content ui-corner-all' );
630
631 modal.classList.remove( 'ui-dialog-content', 'ui-widget-content' );
632
633 $modal.on( 'click', 'a.dismiss', function( event ) {
634 event.preventDefault();
635 $modal.dialog( 'close' );
636 } );
637
638 const overlay = document.querySelector( '.ui-widget-overlay' );
639 if ( overlay ) {
640 overlay.addEventListener(
641 'click',
642 function( event ) {
643 event.preventDefault();
644 $modal.dialog( 'close' );
645 }
646 );
647 }
648 },
649 close() {
650 document.body.classList.remove( bodyWithModalClassName );
651 jQuery( '#wpwrap' ).removeClass( 'frm_overlay' );
652 jQuery( '.spinner' ).css( 'visibility', 'hidden' );
653 }
654 } );
655 }
656
657 document.body.classList.add( bodyWithModalClassName );
658
659 $modal.dialog( 'open' );
660 return $modal;
661 }
662
663 function div( args ) {
664 return tag( 'div', args );
665 }
666
667 function span( args ) {
668 return tag( 'span', args );
669 }
670
671 function a( args = {} ) {
672 const anchor = tag( 'a', args );
673 anchor.setAttribute( 'href', 'string' === typeof args.href ? args.href : '#' );
674 if ( 'string' === typeof args.target ) {
675 anchor.target = args.target;
676 }
677 return anchor;
678 }
679
680 function img( args = {} ) {
681 const output = tag( 'img', args );
682 if ( 'string' === typeof args.src ) {
683 output.setAttribute( 'src', args.src );
684 }
685 if ( 'string' === typeof args.alt ) {
686 output.setAttribute( 'alt', args.alt );
687 }
688 return output;
689 }
690
691 /**
692 * Get a labelled text input and a matching label.
693 *
694 * @since 6.0
695 *
696 * @param {string} inputId
697 * @param {string} labelText
698 * @param {string} inputName
699 * @return {Element} The div element containing the label and input.
700 */
701 function labelledTextInput( inputId, labelText, inputName ) {
702 const label = tag( 'label', labelText );
703 label.setAttribute( 'for', inputId );
704
705 const input = tag(
706 'input',
707 {
708 id: inputId,
709 className: 'frm_long_input'
710 }
711 );
712 input.type = 'text';
713 input.setAttribute( 'name', inputName );
714
715 return div( { children: [ label, input ] } );
716 }
717
718 /**
719 * Build an element.
720 *
721 * @since 6.4.1 Accept a string as one of `children` to append a text node inside the element.
722 *
723 * @param {string} type Element tag name.
724 * @param {Object} args The args.
725 * @return {Object} The created DOM element.
726 */
727 function tag( type, args = {} ) {
728 const output = document.createElement( type );
729
730 if ( 'string' === typeof args ) {
731 // Support passing just a string to a tag for simple text elements.
732 output.textContent = args;
733 return output;
734 }
735
736 const { id, className, children, child, text, data } = args;
737
738 if ( id ) {
739 output.id = id;
740 }
741 if ( className ) {
742 output.className = className;
743 }
744 if ( children ) {
745 children.forEach( child => {
746 if ( 'string' === typeof child ) {
747 output.append( document.createTextNode( child ) );
748 } else {
749 output.append( child );
750 }
751 } );
752 } else if ( child ) {
753 output.append( child );
754 } else if ( text ) {
755 output.textContent = text;
756 }
757 if ( data ) {
758 Object.keys( data ).forEach( function( dataKey ) {
759 output.setAttribute( `data-${ dataKey }`, data[ dataKey ] );
760 } );
761 }
762 return output;
763 }
764
765 function svg( { href, classList } = {} ) {
766 const namespace = 'http://www.w3.org/2000/svg';
767 const output = document.createElementNS( namespace, 'svg' );
768 if ( classList ) {
769 output.classList.add( ...classList );
770 }
771
772 if ( href ) {
773 const use = document.createElementNS( namespace, 'use' );
774 use.setAttribute( 'href', href );
775 output.append( use );
776 output.classList.add( 'frmsvg' );
777 }
778 return output;
779 }
780
781 /**
782 * Pop up a success message in the lower right corner.
783 * It then fades out and gets deleted automatically.
784 *
785 * @param {HTMLElement|string} content
786 * @return {void}
787 */
788 function success( content ) {
789 const container = document.getElementById( 'wpbody' );
790 const notice = div( {
791 className: 'frm_updated_message frm-floating-success-message',
792 child: div( {
793 className: 'frm-satisfied',
794 child: 'string' === typeof content ? document.createTextNode( content ) : content
795 } )
796 } );
797 container.append( notice );
798
799 setTimeout(
800 () => jQuery( notice ).fadeOut( () => notice.remove() ),
801 2000
802 );
803 }
804
805 function setAttributes( element, attrs ) {
806 Object.entries( attrs ).forEach(
807 ( [ key, value ] ) => element.setAttribute( key, value )
808 );
809 }
810
811 function redraw( element, child ) {
812 element.innerHTML = '';
813 element.append( child );
814 }
815
816 const allowedHtml = {
817 b: [],
818 div: [ 'class' ],
819 img: [ 'src', 'alt' ],
820 p: [],
821 span: [ 'class' ],
822 strong: [],
823 svg: [ 'class' ],
824 use: [],
825 a: [ 'href', 'class' ]
826 };
827
828 function cleanNode( node ) {
829 if ( node.tagName === undefined ) {
830 if ( '#text' === node.nodeName ) {
831 return document.createTextNode( node.textContent );
832 }
833 return document.createTextNode( '' );
834 }
835
836 const tagType = node.tagName.toLowerCase();
837
838 if ( 'svg' === tagType ) {
839 const svgArgs = {
840 classList: Array.from( node.classList )
841 };
842 const use = node.querySelector( 'use' );
843 if ( use ) {
844 svgArgs.href = use.getAttribute( 'href' ) || use.getAttribute( 'xlink:href' );
845 }
846 return svg( svgArgs );
847 }
848
849 if ( allowedHtml[ tagType ] === undefined ) {
850 // Tag type is not allowed.
851 return document.createTextNode( '' );
852 }
853
854 const newNode = document.createElement( tagType );
855
856 allowedHtml[ tagType ].forEach(
857 allowedTag => {
858 if ( node.hasAttribute( allowedTag ) ) {
859 newNode.setAttribute( allowedTag, node.getAttribute( allowedTag ) );
860 }
861 }
862 );
863
864 node.childNodes.forEach( child => newNode.append( cleanNode( child ) ) );
865 return newNode;
866 }
867
868 window.frmDom = { tag, div, span, a, img, labelledTextInput, svg, setAttributes, success, modal, ajax, bootstrap, autocomplete, search, util, wysiwyg, cleanNode };
869 }() );
870