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

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