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

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