PluginProbe
Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More / 6.28
Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More v6.28
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.28, at js/admin/dom.js

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