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

777 lines 21.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 $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() {
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 });
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 return output;
663 }
664
665 /**
666 * Get a labelled text input and a matching label.
667 *
668 * @since 6.0
669 *
670 * @param {String} inputId
671 * @param {String} labelText
672 * @param {String} inputName
673 * @returns {Element}
674 */
675 function labelledTextInput( inputId, labelText, inputName ) {
676 const label = tag( 'label', labelText );
677 label.setAttribute( 'for', inputId );
678
679 const input = tag(
680 'input',
681 {
682 id: inputId,
683 className: 'frm_long_input'
684 }
685 );
686 input.type = 'text';
687 input.setAttribute( 'name', inputName );
688
689 return div({ children: [ label, input ] });
690 }
691
692 function tag( type, args = {}) {
693 const output = document.createElement( type );
694
695 if ( 'string' === typeof args ) {
696 // Support passing just a string to a tag for simple text elements.
697 output.textContent = args;
698 return output;
699 }
700
701 const { id, className, children, child, text, data } = args;
702
703 if ( id ) {
704 output.id = id;
705 }
706 if ( className ) {
707 output.className = className;
708 }
709 if ( children ) {
710 children.forEach( child => output.appendChild( child ) );
711 } else if ( child ) {
712 output.appendChild( child );
713 } else if ( text ) {
714 output.textContent = text;
715 }
716 if ( data ) {
717 Object.keys( data ).forEach( function( dataKey ) {
718 output.setAttribute( 'data-' + dataKey, data[dataKey] );
719 });
720 }
721 return output;
722 }
723
724 function svg({ href, classList } = {}) {
725 const namespace = 'http://www.w3.org/2000/svg';
726 const output = document.createElementNS( namespace, 'svg' );
727 if ( classList ) {
728 output.classList.add( ...classList );
729 }
730
731 if ( href ) {
732 const use = document.createElementNS( namespace, 'use' );
733 use.setAttribute( 'href', href );
734 output.appendChild( use );
735 output.classList.add( 'frmsvg' );
736 }
737 return output;
738 }
739
740 /**
741 * Pop up a success message in the lower right corner.
742 * It then fades out and gets deleted automatically.
743 *
744 * @param {HTMLElement|String} content
745 * @returns {void}
746 */
747 function success( content ) {
748 const container = document.getElementById( 'wpbody' );
749 const notice = div({
750 className: 'notice notice-info frm-review-notice frm_updated_message frm-floating-success-message',
751 child: div({
752 className: 'frm-satisfied',
753 child: 'string' === typeof content ? document.createTextNode( content ) : content
754 })
755 });
756 container.appendChild( notice );
757
758 setTimeout(
759 () => jQuery( notice ).fadeOut( () => notice.remove() ),
760 2000
761 );
762 }
763
764 function setAttributes( element, attrs ) {
765 Object.entries( attrs ).forEach(
766 ([ key, value ]) => element.setAttribute( key, value )
767 );
768 }
769
770 function redraw( element, child ) {
771 element.innerHTML = '';
772 element.appendChild( child );
773 }
774
775 window.frmDom = { tag, div, span, a, img, labelledTextInput, svg, setAttributes, success, modal, ajax, bootstrap, autocomplete, search, util, wysiwyg };
776 }() );
777