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

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