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

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