PluginProbe
Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) / 1.8.11.3
Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) v1.8.11.3
1.8.13 1.8.12.3 1.8.12.2 1.8.12.1 1.8.12 1.8.11.3 1.8.11.2 1.8.11.1 1.8.11 1.6.6 1.6.60 1.6.7 1.6.8 1.6.9 1.7.0 1.7.0.1 1.7.0.11 1.7.0.12 1.7.0.14 1.7.0.2 1.7.0.3 1.7.0.5 1.7.0.6 1.7.0.7 1.7.0.9 All 211 releases
charitable / assets / js / campaign-builder / admin-utils.js

admin-utils.js in Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) 1.8.11.3, at assets/js/campaign-builder/admin-utils.js

1,045 lines 26.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /* eslint-disable no-undef */
2 /* global charitable_builder, CharitableUtils */
3
4
5 var wpchar = {
6
7 cachedFields: {},
8 savedState: false,
9 initialSave: true,
10 orders: {
11 fields: [],
12 choices: {},
13 },
14
15 // This file contains a collection of utility functions.
16
17 /**
18 * Start the engine.
19 *
20 * @since 1.0.1
21 */
22 init: function() {
23
24 wpchar.bindUIActions();
25
26 // Init Radio Group for Checkboxes.
27 wpchar.initRadioGroupForCheckboxes();
28
29 jQuery( wpchar.ready );
30 },
31
32 /**
33 * Document ready.
34 *
35 * @since 1.0.1
36 */
37 ready: function() {
38
39 // Load initial form saved state.
40 wpchar.savedState = wpchar.getFormState( '#charitable-builder-form' );
41
42 // Save field and choice order for sorting later.
43 wpchar.setFieldOrders();
44 wpchar.setChoicesOrders();
45 },
46
47 /**
48 * Element bindings.
49 *
50 * @since 1.0.1
51 */
52 bindUIActions: function() {
53
54 // The following items should all trigger the fieldUpdate trigger.
55 jQuery( document ).on( 'charitableFieldAdd', wpchar.setFieldOrders );
56 jQuery( document ).on( 'charitableFieldDelete', wpchar.setFieldOrders );
57 jQuery( document ).on( 'charitableFieldMove', wpchar.setFieldOrders );
58 jQuery( document ).on( 'charitableFieldAdd', wpchar.setChoicesOrders );
59 jQuery( document ).on( 'charitableFieldChoiceAdd', wpchar.setChoicesOrders );
60 jQuery( document ).on( 'charitableFieldChoiceDelete', wpchar.setChoicesOrders );
61 jQuery( document ).on( 'charitableFieldChoiceMove', wpchar.setChoicesOrders );
62 jQuery( document ).on( 'charitableFieldAdd', wpchar.fieldUpdate );
63 jQuery( document ).on( 'charitableFieldDelete', wpchar.fieldUpdate );
64 jQuery( document ).on( 'charitableFieldMove', wpchar.fieldUpdate );
65 jQuery( document ).on( 'focusout', '.charitable-field-option-row-label input', wpchar.fieldUpdate );
66 jQuery( document ).on( 'charitableFieldChoiceAdd', wpchar.fieldUpdate );
67 jQuery( document ).on( 'charitableFieldChoiceDelete', wpchar.fieldUpdate );
68 jQuery( document ).on( 'charitableFieldChoiceMove', wpchar.fieldUpdate );
69 jQuery( document ).on( 'charitableFieldDynamicChoiceToggle', wpchar.fieldUpdate );
70 jQuery( document ).on( 'focusout', '.charitable-field-option-row-choices input.label', wpchar.fieldUpdate );
71 },
72
73 /**
74 * Store the order of the fields.
75 *
76 * @since 1.4.5
77 */
78 setFieldOrders: function() {
79
80 wpchar.orders.fields = [];
81
82 jQuery( '.charitable-field-option' ).each( function() {
83 wpchar.orders.fields.push( jQuery( this ).data( 'field-id' ) );
84 } );
85 },
86
87 /**
88 * Store the order of the choices for each field.
89 *
90 * @since 1.4.5
91 */
92 setChoicesOrders: function() {
93
94 wpchar.orders.choices = {};
95
96 jQuery( '.choices-list' ).each( function() {
97 var fieldID = jQuery( this ).data( 'field-id' );
98 wpchar.orders.choices[ 'field_' + fieldID ] = [];
99 jQuery( this ).find( 'li' ).each( function() {
100 wpchar.orders.choices[ 'field_' + fieldID ].push( jQuery( this ).data( 'key' ) );
101 } );
102 } );
103 },
104
105 /**
106 * Return the order of choices for a specific field.
107 *
108 * @since 1.4.5
109 *
110 * @param int id Field ID.
111 *
112 * @return array
113 */
114 getChoicesOrder: function( id ) {
115
116 var choices = [];
117
118 jQuery( '#charitable-field-option-' + id ).find( '.choices-list li' ).each( function() {
119 choices.push( jQuery( this ).data( 'key' ) );
120 } );
121
122 return choices;
123 },
124
125 /**
126 * Maintain multiselect dropdown with search.
127 * If a multiple select has selected choices - hide a placeholder text.
128 * In case if select is empty - we return placeholder text back.
129 *
130 * @since 1.7.6
131 *
132 * @param {object} self Current object.
133 */
134 initMultipleSelectWithSearch: function( self ) {
135
136 const $element = jQuery( self.passedElement.element ),
137 $input = jQuery( self.input.element );
138
139 if ( $element.prop( 'multiple' ) ) {
140
141 // On init event.
142 $input.data( 'placeholder', $input.attr( 'placeholder' ) );
143
144 if ( self.getValue( true ).length ) {
145 $input.removeAttr( 'placeholder' );
146 }
147
148 // On change event.
149 $element.on( 'change', function() {
150
151 self.getValue( true ).length ?
152 $input.removeAttr( 'placeholder' ) :
153 $input.attr( 'placeholder', $input.data( 'placeholder' ) );
154 } );
155 }
156 },
157
158 /**
159 * Trigger fired for all field update related actions.
160 *
161 * @since 1.0.1
162 */
163 fieldUpdate: function() {
164
165 var fields = wpchar.getFields();
166
167 jQuery( document ).trigger( 'charitableFieldUpdate', [ fields ] );
168
169 wpchar.debug( 'fieldUpdate triggered' );
170 },
171
172 /**
173 * Dynamically get the fields from the current form state.
174 *
175 * @since 1.0.1
176 * @param array allowedFields
177 * @param bool useCache
178 * @return object
179 */
180 getFields: function( allowedFields, useCache ) {
181
182 useCache = useCache || false;
183 fields = false;
184
185 if ( useCache && ! jQuery.isEmptyObject( wpchar.cachedFields ) ) {
186
187 // Use cache if told and cache is primed.
188 fields = jQuery.extend( {}, wpchar.cachedFields );
189
190 wpchar.debug( 'getFields triggered (cached)' );
191
192 } else {
193
194 // Normal processing, get fields from builder and prime cache.
195 var formData = wpchar.formObject( '#charitable-field-options' ),
196 fields = formData.fields,
197 fieldBlockList = [
198 'captcha',
199 'content',
200 'divider',
201 'entry-preview',
202 'html',
203 'internal-information',
204 'layout',
205 'pagebreak',
206 ];
207
208 if ( ! fields ) {
209 return false;
210 }
211
212 for ( var key in fields ) {
213 if ( ! fields[key].type || jQuery.inArray( fields[key].type, fieldBlockList ) > -1 ) {
214 delete fields[key];
215 }
216 }
217
218 // Cache the all the fields now that they have been ordered and initially processed.
219 wpchar.cachedFields = jQuery.extend( {}, fields );
220
221 wpchar.debug( 'getFields triggered' );
222 }
223
224 // If we should only return specific field types, remove the others.
225 if ( allowedFields && allowedFields.constructor === Array ) {
226 for ( var key in fields ) {
227 if ( jQuery.inArray( fields[key].type, allowedFields ) === -1 ) {
228 delete fields[key];
229 }
230 }
231 }
232
233 return fields;
234 },
235
236 /**
237 * Get field settings object.
238 *
239 * @since 1.4.5
240 *
241 * @param int id Field ID.
242 *
243 * @return object
244 */
245 getField: function( id ) {
246
247 var field = wpchar.formObject( '#charitable-field-option-' + id );
248
249 return field.fields[ Object.keys( field.fields )[0] ];
250 },
251
252 /**
253 * Toggle the loading state/indicator of a field option.
254 *
255 * @since 1.2.8
256 *
257 * @param {mixed} option jQuery object, or DOM element selector.
258 * @param {boolean} unload True if you need to unload spinner, and vice versa.
259 */
260 fieldOptionLoading: function( option, unload ) {
261
262 var $option = jQuery( option ),
263 $label = $option.find( 'label' ),
264 spinner = '<i class="charitable-loading-spinner charitable-loading-inline"></i>';
265
266 unload = typeof unload !== 'undefined';
267
268 if ( unload ) {
269 $label.find( '.charitable-loading-spinner' ).remove();
270 $label.find( '.charitable-help-tooltip' ).show();
271 $option.find( 'input,select,textarea' ).prop( 'disabled', false );
272 } else {
273 $label.append( spinner );
274 $label.find( '.charitable-help-tooltip' ).hide();
275 $option.find( 'input,select,textarea' ).prop( 'disabled', true );
276 }
277 },
278
279 /**
280 * Get form state.
281 *
282 * @since 1.3.8
283 * @param object el
284 */
285 getFormState: function( el ) {
286
287 // Serialize tested the most performant string we can use for
288 // comparisons.
289 return jQuery( el ).serialize();
290 },
291
292 /**
293 * Remove items from an array.
294 *
295 * @since 1.0.1
296 * @param array array
297 * @param mixed item index/key
298 * @return array
299 */
300 removeArrayItem: function( array, item ) {
301
302 var removeCounter = 0;
303
304 for ( var index = 0; index < array.length; index++ ) {
305 if ( array[index] === item ) {
306 array.splice( index, 1 );
307 removeCounter++;
308 index--;
309 }
310 }
311
312 return removeCounter;
313 },
314
315 /**
316 * Sanitize string.
317 *
318 * @since 1.0.1
319 * @deprecated 1.2.8
320 *
321 * @param {string} str String to sanitize.
322 *
323 * @returns {string} String after sanitization.
324 */
325 sanitizeString: function( str ) {
326
327 if ( typeof str === 'string' || str instanceof String ) {
328 return str.trim();
329 }
330 return str;
331 },
332
333 /**
334 * Update query string in URL.
335 *
336 * @since 1.0.0
337 */
338 updateQueryString: function( key, value, url ) {
339
340 if ( ! url ) {
341 url = window.location.href;
342 }
343
344 var re = new RegExp( '([?&])' + key + '=.*?(&|#|$)(.*)', 'gi' ),
345 hash;
346
347 if ( re.test( url ) ) {
348 if ( typeof value !== 'undefined' && value !== null )
349 return url.replace( re, '$1' + key + '=' + value + '$2$3' );
350 else {
351 hash = url.split( '#' );
352 url = hash[0].replace( re, '$1$3' ).replace( /(&|\?)$/, '' );
353 if ( typeof hash[1] !== 'undefined' && hash[1] !== null )
354 url += '#' + hash[1];
355 return url;
356 }
357 } else {
358 if ( typeof value !== 'undefined' && value !== null ) {
359 var separator = url.indexOf( '?' ) !== -1 ? '&' : '?';
360 hash = url.split( '#' );
361 url = hash[0] + separator + key + '=' + value;
362 if ( typeof hash[1] !== 'undefined' && hash[1] !== null )
363 url += '#' + hash[1];
364 return url;
365 }
366 else
367 return url;
368 }
369 },
370
371 /**
372 * Get query string in a URL.
373 *
374 * @since 1.0.0
375 */
376 getQueryString: function( name ) {
377
378 var match = new RegExp( '[?&]' + name + '=([^&]*)' ).exec( window.location.search );
379 return match && decodeURIComponent( match[1].replace( /\+/g, ' ' ) );
380 },
381
382 /**
383 * Remove defined query parameter in the current URL.
384 *
385 * @see https://gist.github.com/simonw/9445b8c24ddfcbb856ec#gistcomment-3117674
386 *
387 * @since 1.5.8
388 *
389 * @param {string} name The name of the parameter to be removed.
390 */
391 removeQueryParam: function( name ) {
392
393 if ( wpchar.getQueryString( name ) ) {
394 var replace = '[\\?&]' + name + '=[^&]+',
395 re = new RegExp( replace );
396
397 history.replaceState && history.replaceState(
398 null, '', location.pathname + location.search.replace( re, '' ).replace( /^&/, '?' ) + location.hash
399 );
400 }
401 },
402
403 /**
404 * Is number?
405 *
406 * @since 1.2.3
407 *
408 * @param {number|string} n Number to check.
409 *
410 * @returns {boolean} Whether this is a number.
411 */
412 isNumber: function( n ) {
413 return ! isNaN( parseFloat( n ) ) && isFinite( n );
414 },
415
416 /**
417 * Sanitize amount and convert to standard format for calculations.
418 *
419 * @since 1.2.6
420 *
421 * @param {string} amount Price amount to sanitize.
422 *
423 * @returns {string} Sanitized amount.
424 */
425 amountSanitize: function( amount ) {
426
427 // Convert to string and allow only numbers, dots and commas.
428 amount = String( amount ).replace( /[^0-9.,]/g, '' );
429
430 if ( charitable_builder.currency_decimal === ',' ) {
431 if ( charitable_builder.currency_thousands === '.' && amount.indexOf( charitable_builder.currency_thousands ) !== -1 ) {
432 amount = amount.replace( new RegExp( '\\' + charitable_builder.currency_thousands, 'g' ), '' );
433 } else if ( charitable_builder.currency_thousands === '' && amount.indexOf( '.' ) !== -1 ) {
434 amount = amount.replace( /\./g, '' );
435 }
436 amount = amount.replace( charitable_builder.currency_decimal, '.' );
437 } else if ( charitable_builder.currency_thousands === ',' && ( amount.indexOf( charitable_builder.currency_thousands ) !== -1 ) ) {
438 amount = amount.replace( new RegExp( '\\' + charitable_builder.currency_thousands, 'g' ), '' );
439 }
440
441 return wpchar.numberFormat( amount, charitable_builder.currency_decimals, '.', '' );
442 },
443
444 /**
445 * Format amount.
446 *
447 * @since 1.2.6
448 *
449 * @param {string} amount Price amount to format.
450 *
451 * @returns {string} Formatted amount.
452 */
453 amountFormat: function( amount ) {
454
455 amount = String( amount );
456
457 // Format the amount
458 if ( charitable_builder.currency_decimal === ',' && ( amount.indexOf( charitable_builder.currency_decimal ) !== -1 ) ) {
459 var sepFound = amount.indexOf( charitable_builder.currency_decimal );
460
461 amount = amount.substr( 0, sepFound ) + '.' + amount.substr( sepFound + 1, amount.length - 1 );
462 }
463
464 // Strip , from the amount (if set as the thousand separator)
465 if ( charitable_builder.currency_thousands === ',' && ( amount.indexOf( charitable_builder.currency_thousands ) !== -1 ) ) {
466 amount = amount.replace( /,/g, '' );
467 }
468
469 if ( wpchar.empty( amount ) ) {
470 amount = 0;
471 }
472
473 return wpchar.numberFormat( amount, charitable_builder.currency_decimals, charitable_builder.currency_decimal, charitable_builder.currency_thousands );
474 },
475
476 /**
477 * Format amount with currency symbol.
478 *
479 * @since 1.6.2
480 *
481 * @param {string} amount Amount to format.
482 *
483 * @returns {string} Formatted amount (for instance $ 128.00).
484 */
485 amountFormatCurrency: function( amount ) {
486
487 var sanitized = wpchar.amountSanitize( amount ),
488 formatted = wpchar.amountFormat( sanitized ),
489 result;
490
491 if ( charitable_builder.currency_symbol_pos === 'right' ) {
492 result = formatted + ' ' + charitable_builder.currency_symbol;
493 } else {
494 result = charitable_builder.currency_symbol + ' ' + formatted;
495 }
496
497 return result;
498 },
499
500 /**
501 * Format number.
502 *
503 * @see http://locutus.io/php/number_format/
504 *
505 * @since 1.2.6
506 *
507 * @param {string} number Number to format.
508 * @param {number} decimals How many decimals should be there.
509 * @param {string} decimalSep What is the decimal separator.
510 * @param {string} thousandsSep What is the thousand separator.
511 *
512 * @returns {string} Formatted number.
513 */
514 numberFormat: function( number, decimals, decimalSep, thousandsSep ) {
515
516 number = ( number + '' ).replace( /[^0-9+\-Ee.]/g, '' );
517 var n = ! isFinite( +number ) ? 0 : +number;
518 var prec = ! isFinite( +decimals ) ? 0 : Math.abs( decimals );
519 var sep = ( typeof thousandsSep === 'undefined' ) ? ',' : thousandsSep;
520 var dec = ( typeof decimalSep === 'undefined' ) ? '.' : decimalSep;
521 var s = '';
522
523 var toFixedFix = function( n, prec ) {
524 var k = Math.pow( 10, prec );
525 return '' + ( Math.round( n * k ) / k ).toFixed( prec );
526 };
527
528 // @todo: for IE parseFloat(0.55).toFixed(0) = 0;
529 s = ( prec ? toFixedFix( n, prec ) : '' + Math.round( n ) ).split( '.' );
530 if ( s[ 0 ].length > 3 ) {
531 s[ 0 ] = s[ 0 ].replace( /\B(?=(?:\d{3})+(?!\d))/g, sep );
532 }
533 if ( ( s[ 1 ] || '' ).length < prec ) {
534 s[ 1 ] = s[ 1 ] || '';
535 s[ 1 ] += new Array( prec - s[ 1 ].length + 1 ).join( '0' );
536 }
537
538 return s.join( dec );
539 },
540
541 /**
542 * Empty check similar to PHP.
543 *
544 * @link http://locutus.io/php/empty/
545 * @since 1.2.6
546 */
547 empty: function( mixedVar ) {
548
549 var undef;
550 var key;
551 var i;
552 var len;
553 var emptyValues = [ undef, null, false, 0, '', '0' ];
554
555 for ( i = 0, len = emptyValues.length; i < len; i++ ) {
556 if ( mixedVar === emptyValues[i] ) {
557 return true;
558 }
559 }
560
561 if ( typeof mixedVar === 'object' ) {
562 for ( key in mixedVar ) {
563 if ( mixedVar.hasOwnProperty( key ) ) {
564 return false;
565 }
566 }
567 return true;
568 }
569
570 return false;
571 },
572
573 /**
574 * Debug output helper.
575 *
576 * @since 1.3.8
577 *
578 * @param {string|integer|boolean|Array|object} msg Debug message (any data).
579 */
580 debug: function( msg, type = '' ) {
581
582 if ( ! wpchar.isDebug() ) {
583 return;
584 }
585
586 if ( type ) {
587 type = '(' + type + ')';
588 }
589
590 console.log( '%cCharitable Debug: ' + type, 'color: #cd6622;', msg );
591 },
592
593 /**
594 * Is debug mode.
595 *
596 * @since 1.3.8
597 */
598 isDebug: function() {
599 return ( ( window.location.hash && '#charitabledebug' === window.location.hash ) || charitable_builder.debug );
600 },
601
602 /**
603 * Focus the input/textarea and put the caret at the end of the text.
604 *
605 * @since 1.4.1
606 */
607 focusCaretToEnd: function( el ) {
608 el.trigger( 'focus' );
609 var $thisVal = el.val();
610 el.val( '' ).val( $thisVal );
611 },
612
613 /**
614 * Creates a object from form elements.
615 *
616 * @since 1.4.5
617 */
618 formObject: function( el ) {
619
620 var form = jQuery( el ),
621 fields = form.find( '[name]' ),
622 json = {},
623 arraynames = {};
624
625 for ( var v = 0; v < fields.length; v++ ) {
626
627 var field = jQuery( fields[v] ),
628 name = field.prop( 'name' ).replace( /\]/gi, '' ).split( '[' ),
629 value = field.val(),
630 lineconf = {};
631
632 if ( ( field.is( ':radio' ) || field.is( ':checkbox' ) ) && ! field.is( ':checked' ) ) {
633 continue;
634 }
635 for ( var i = name.length - 1; i >= 0; i-- ) {
636 var nestname = name[i];
637 if ( typeof nestname === 'undefined' ) {
638 nestname = '';
639 }
640 if ( nestname.length === 0 ){
641 lineconf = [];
642 if ( typeof arraynames[name[i - 1]] === 'undefined' ) {
643 arraynames[name[i - 1]] = 0;
644 } else {
645 arraynames[name[i - 1]] += 1;
646 }
647 nestname = arraynames[name[i - 1]];
648 }
649 if ( i === name.length - 1 ) {
650 if ( value ) {
651 if ( value === 'true' ) {
652 value = true;
653 } else if ( value === 'false' ) {
654 value = false;
655 } else if ( ! isNaN( parseFloat( value ) ) && parseFloat( value ).toString() === value ) {
656 value = parseFloat( value );
657 } else if ( typeof value === 'string' && ( value.substr( 0, 1 ) === '{' || value.substr( 0, 1 ) === '[' ) ) {
658 try {
659 value = JSON.parse( value );
660 } catch ( e ) {} // eslint-disable-line
661 } else if ( typeof value === 'object' && value.length && field.is( 'select' ) ) {
662 var newValue = {};
663 for ( var i = 0; i < value.length; i++ ) {
664 newValue[ 'n' + i ] = value[ i ];
665 }
666 value = newValue;
667 }
668 }
669 lineconf[nestname] = value;
670 } else {
671 var newobj = lineconf;
672 lineconf = {};
673 lineconf[nestname] = newobj;
674 }
675 }
676 jQuery.extend( true, json, lineconf );
677 }
678
679 return json;
680 },
681
682 /**
683 * Initialize Charitable admin area tooltips.
684 *
685 * @since 1.4.8
686 */
687 initTooltips: function() {
688
689 if ( typeof jQuery.fn.tooltipster === 'undefined' ) {
690 return;
691 }
692
693 jQuery( '.charitable-help-tooltip' ).tooltipster( {
694 contentAsHTML: true,
695 position: 'right',
696 maxWidth: 300,
697 multiple: true,
698 interactive: true,
699 debug: false,
700 IEmin: 11,
701 } );
702 },
703
704 /**
705 * Restore Charitable admin area tooltip's title.
706 *
707 * @since 1.6.5
708 *
709 * @param {mixed} $scope Searching scope.
710 */
711 restoreTooltips: function( $scope ) {
712
713 $scope = typeof $scope !== 'undefined' && $scope && $scope.length > 0 ? $scope.find( '.charitable-help-tooltip' ) : jQuery( '.charitable-help-tooltip' );
714 $scope.each( function() {
715 var $this = jQuery( this );
716 if ( jQuery.tooltipster.instances( this ).length !== 0 ) {
717
718 // Restoring title.
719 $this.attr( 'title', $this.tooltipster( 'content' ) );
720 }
721 } );
722 },
723
724 /**
725 * Validate a URL.
726 * source: `https://github.com/segmentio/is-url/blob/master/index.js`
727 *
728 * @since 1.5.8
729 *
730 * @param {string} url URL for checking.
731 *
732 * @returns {boolean} True if `url` is a valid URL.
733 */
734 isURL: function( url ) {
735
736 /**
737 * RegExps.
738 * A URL must match #1 and then at least one of #2/#3.
739 * Use two levels of REs to avoid REDOS.
740 */
741 var protocolAndDomainRE = /^(?:http(?:s?):)?\/\/(\S+)/;
742 var localhostDomainRE = /^localhost[\:?\d]*(?:[^\:?\d]\S*)?$/; // eslint-disable-line
743 var nonLocalhostDomainRE = /^[^\s\.]+\.\S{2,}$/; // eslint-disable-line
744
745 if ( typeof url !== 'string' ) {
746 return false;
747 }
748
749 var match = url.match( protocolAndDomainRE );
750 if ( ! match ) {
751 return false;
752 }
753
754 var everythingAfterProtocol = match[1];
755 if ( ! everythingAfterProtocol ) {
756 return false;
757 }
758
759 if ( localhostDomainRE.test( everythingAfterProtocol ) || nonLocalhostDomainRE.test( everythingAfterProtocol ) ) {
760 return true;
761 }
762
763 return false;
764 },
765
766 /**
767 * Sanitize HTML.
768 * Uses: `https://github.com/cure53/DOMPurify`
769 *
770 * @since 1.5.9
771 * @since 1.7.8 Introduced optional allowed parameter.
772 *
773 * @param {string} string HTML to sanitize.
774 * @param {undefined|Array} allowed Array of allowed HTML tags.
775 *
776 * @returns {string} Sanitized HTML.
777 */
778 sanitizeHTML: function( string, allowed ) {
779
780 var purify = window.DOMPurify;
781
782 if ( typeof purify === 'undefined' || typeof string === 'undefined' ) {
783 return string;
784 }
785
786 if ( typeof string !== 'string' ) {
787 string = string.toString();
788 }
789
790 const purifyOptions = {
791 ADD_ATTR: [ 'target' ],
792 };
793
794 if ( typeof allowed !== 'undefined' ) {
795 purifyOptions.ALLOWED_TAGS = allowed;
796 }
797
798 return purify.sanitize( string, purifyOptions ).trim();
799 },
800
801 /**
802 * Encode HTML entities.
803 * Uses: `https://stackoverflow.com/a/18750001/9745718`
804 *
805 * @since 1.6.3
806 *
807 * @param {string} string HTML to sanitize.
808 *
809 * @returns {string} String with encoded HTML entities.
810 */
811 encodeHTMLEntities: function( string ) {
812
813 if ( typeof string !== 'string' ) {
814 string = string.toString();
815 }
816
817 return string.replace( /[\u00A0-\u9999<>&]/gim, function( i ) {
818
819 return '&#' + i.charCodeAt( 0 ) + ';';
820 } );
821 },
822
823 /**
824 * Radio Group for Checkboxes.
825 *
826 * @since 1.6.6
827 */
828 initRadioGroupForCheckboxes: function() {
829
830 var $ = jQuery;
831
832 $( document ).on( 'change', 'input[type="checkbox"].charitable-radio-group', function() {
833
834 var $input = $( this ),
835 inputId = $input.attr( 'id' );
836
837 if ( ! $input.prop( 'checked' ) ) {
838 return;
839 }
840
841 var groupName = $input.data( 'radio-group' ),
842 $group = $( '.charitable-radio-group-' + groupName ),
843 $item;
844
845 $group.each( function() {
846
847 $item = $( this );
848 if ( $item.attr( 'id' ) !== inputId ) {
849 $item.prop( 'checked', false );
850 }
851 } );
852 } );
853 },
854
855 /**
856 * Pluck a certain field out of each object in a list.
857 *
858 * JS implementation of the `wp_list_pluck()`.
859 *
860 * @since 1.6.8
861 *
862 * @param {Array} arr Array of objects.
863 * @param {string} column Column.
864 *
865 * @returns {Array} Array with extracted column values.
866 */
867 listPluck: function( arr, column ) {
868
869 return arr.map( function( x ) {
870
871 if ( typeof x !== 'undefined' ) {
872 return x[ column ];
873 }
874
875 return x;
876 } );
877 },
878
879 /**
880 * Wrapper to trigger a native or custom event and return the event object.
881 *
882 * @since 1.7.5
883 * @since 1.7.6 Deprecated.
884 *
885 * @deprecated Use `CharitableUtils.triggerEvent` instead.
886 *
887 * @param {jQuery} $element Element to trigger event on.
888 * @param {string} eventName Event name to trigger (custom or native).
889 *
890 * @returns {Event} Event object.
891 */
892 triggerEvent: function( $element, eventName ) {
893
894 console.warn( 'WARNING! Function "wpchar.triggerEvent( $element, eventName )" has been deprecated, please use the new "CharitableUtils.triggerEvent( $element, eventName, args )" function instead!' );
895
896 return CharitableUtils.triggerEvent( $element, eventName );
897 },
898
899 /**
900 * Automatically add paragraphs to the text.
901 *
902 * JS implementation of the `wpautop()`.
903 *
904 * @see https://github.com/andymantell/node-wpautop/blob/master/lib/wpautop.js
905 *
906 * @since 1.7.7
907 *
908 * @param {string} pee Text to be replaced.
909 * @param {boolean} br Whether remaining \n characters should be replaced with <br />.
910 *
911 * @returns {string} Text with replaced paragraphs.
912 */
913 wpautop: function( pee, br = true ) { // eslint-disable-line max-lines-per-function, complexity
914
915 let preTags = new Map();
916 let _autopNewlinePreservationHelper = function( matches ) {
917
918 return matches[0].replace( '\n', '<WPPreserveNewline />' );
919 };
920
921 if ( ( typeof pee ) !== 'string' && ! ( pee instanceof String ) ) {
922 return pee;
923 }
924
925 if ( pee.trim() === '' ) {
926 return '';
927 }
928
929 pee = pee + '\n'; // Just to make things a little easier, pad the end.
930
931 if ( pee.indexOf( '<pre' ) > -1 ) {
932 let peeParts = pee.split( '</pre>' ),
933 lastPee = peeParts.pop();
934
935 pee = '';
936
937 peeParts.forEach(
938 function( peePart, index ) {
939
940 const start = peePart.indexOf( '<pre' );
941
942 // Malformed html?
943 if ( start === -1 ) {
944 pee += peePart;
945 return;
946 }
947
948 let name = '<pre wp-pre-tag-' + index + '></pre>';
949 preTags[name] = peePart.substring( start ) + '</pre>';
950 pee += peePart.substring( 0, start ) + name;
951
952 }
953 );
954
955 pee += lastPee;
956 }
957
958 pee = pee.replace( /<br \/>\s*<br \/>/, '\n\n' );
959
960 // Space things out a little.
961 let allblocks = '(?:table|thead|tfoot|caption|col|colgroup|tbody|tr|td|th|div|dl|dd|dt|ul|ol|li|pre|form|map|area|blockquote|address|math|style|p|h[1-6]|hr|fieldset|legend|section|article|aside|hgroup|header|footer|nav|figure|figcaption|details|menu|summary)';
962
963 pee = pee.replace( new RegExp( '(<' + allblocks + '[^>]*>)', 'gmi' ), '\n$1' );
964 pee = pee.replace( new RegExp( '(</' + allblocks + '>)', 'gmi' ), '$1\n\n' );
965 pee = pee.replace( /\r\n|\r/, '\n' ); // cross-platform newlines.
966
967 if ( pee.indexOf( '\n' ) === 0 ) {
968 pee = pee.substring( 1 );
969 }
970
971 if ( pee.indexOf( '<option' ) > -1 ) {
972
973 // no P/BR around option.
974 pee = pee.replace( /(?=(\s*))\2<option'/gmi, '<option' );
975 pee = pee.replace( /<\/option>\s*/gmi, '</option>' );
976 }
977
978 if ( pee.indexOf( '</object>' ) > -1 ) {
979
980 // no P/BR around param and embed.
981 pee = pee.replace( /(<object[^>]*>)\s*/gmi, '$1' );
982 pee = pee.replace( /(?=(\s*))\2<\/object>/gmi, '</object>' );
983 pee = pee.replace( /(?=(\s*))\2(<\/?(?:param|embed)[^>]*>)((?=(\s*))\2)/gmi, '$1' ); // eslint-disable-line
984 }
985
986 /* eslint-disable no-useless-escape */
987
988 if ( pee.indexOf( '<source' ) > -1 || pee.indexOf( '<track' ) > -1 ) {
989
990 // no P/BR around source and track.
991 pee = pee.replace( /([<\[](?:audio|video)[^>\]]*[>\]])\s*/gmi, '$1' ); // eslint-disable-line
992 pee = pee.replace( /(?=(\s*))\2([<\[]\/(?:audio|video)[>\]])/gmi, '$1' ); // eslint-disable-line
993 pee = pee.replace( /(?=(\s*))\2(<(?:source|track)[^>]*>)(?=(\s*))\2/gmi, '$1' ); // eslint-disable-line
994 }
995
996 pee = pee.replace( /\n\n+/gmi, '\n\n' ); // take care of duplicates.
997
998 // make paragraphs, including one at the end.
999 let pees = pee.split( /\n\s*\n/ );
1000
1001 pee = '';
1002
1003 pees.forEach(
1004 function( tinkle ) {
1005 pee += '<p>' + tinkle.replace( /^(?:\s+|\s+)$/g, '' ) + '</p>\n';
1006 }
1007 );
1008
1009 pee = pee.replace( /<p>\s*<\/p>/gmi, '' ); // under certain strange conditions it could create a P of entirely whitespace.
1010 pee = pee.replace( /<p>([^<]+)<\/(div|address|form)>/gmi, '<p>$1</p></$2>' );
1011 pee = pee.replace( new RegExp( '<p>\s*(</?' + allblocks + '[^>]*>)\s*</p>', 'gmi' ), '$1', pee ); // don't pee all over a tag.
1012 pee = pee.replace( /<p>(<li.+?)<\/p>/gmi, '$1' ); // problem with nested lists.
1013 pee = pee.replace( /<p><blockquote([^>]*)>/gmi, '<blockquote$1><p>' );
1014 pee = pee.replace( /<\/blockquote><\/p>/gmi, '</p></blockquote>' );
1015 pee = pee.replace( new RegExp( '<p>\s*(</?' + allblocks + '[^>]*>)', 'gmi' ), '$1' );
1016 pee = pee.replace( new RegExp( '(</?' + allblocks + '[^>]*>)\s*</p>', 'gmi' ), '$1' );
1017
1018 if ( br ) {
1019 pee = pee.replace( /<(script|style)(?:.|\n)*?<\/\\1>/gmi, _autopNewlinePreservationHelper ); // /s modifier from php PCRE regexp replaced with (?:.|\n).
1020 // eslint-disable-next-line
1021 pee = pee.replace( /(<br \/>)?((?=(\s*))\2)\n/gmi, '<br />\n' ); // optionally make line breaks.
1022 pee = pee.replace( '<WPPreserveNewline />', '\n' );
1023 }
1024
1025 pee = pee.replace( new RegExp( '(</?' + allblocks + '[^>]*>)\s*<br />', 'gmi' ), '$1' );
1026 pee = pee.replace( /<br \/>(\s*<\/?(?:p|li|div|dl|dd|dt|th|pre|td|ul|ol)[^>]*>)/gmi, '$1' );
1027 pee = pee.replace( /\n<\/p>$/gmi, '</p>' );
1028
1029 /* esline-enable */
1030
1031 if ( Object.keys( preTags ).length ) {
1032 pee = pee.replace(
1033 new RegExp( Object.keys( preTags ).join( '|' ), 'gi' ),
1034 function( matched ) {
1035 return preTags[matched];
1036 }
1037 );
1038 }
1039
1040 return pee;
1041 },
1042 };
1043
1044 wpchar.init();
1045