PluginProbe
Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) / 1.8.13
Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) v1.8.13
1.8.13.1 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 All 212 releases
charitable / assets / js / frontend / charitable-mini-widget.js

charitable-mini-widget.js in Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) 1.8.13, at assets/js/frontend/charitable-mini-widget.js

347 lines 11.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Charitable Mini Donation Widget — Frontend
3 *
4 * Handles tab switching, amount selection, impact text, validation, and CTA.
5 *
6 * @package Charitable
7 * @since 1.8.12
8 */
9 (function($) {
10 'use strict';
11
12 /**
13 * Initialize all mini widgets on the page.
14 */
15 function initAll() {
16 $('[data-charitable-mini-widget]').each(function() {
17 var $widget = $(this);
18 if ($widget.data('cmw-initialized')) {
19 return;
20 }
21 $widget.data('cmw-initialized', true);
22 initWidget($widget);
23 });
24 }
25
26 /**
27 * Initialize a single widget instance.
28 *
29 * @param {jQuery} $widget The widget wrapper element.
30 */
31 function initWidget($widget) {
32 var config;
33 try {
34 config = JSON.parse($widget.attr('data-charitable-mini-widget'));
35 } catch (e) {
36 return;
37 }
38
39 // Current state.
40 var state = {
41 tab: config.recurringActive ? 'monthly' : 'onetime',
42 amount: null,
43 customAmount: null,
44 isOther: false
45 };
46
47 // Set initial selected amount.
48 var tabCfg = config[state.tab];
49 state.amount = tabCfg['default'] || (tabCfg.amounts.length ? tabCfg.amounts[0] : 0);
50
51 // Tab clicks.
52 $widget.on('click', '.charitable-mini-widget__tab', function() {
53 var newTab = $(this).data('tab');
54 if (newTab === state.tab) {
55 return;
56 }
57 state.tab = newTab;
58 state.isOther = false;
59 state.customAmount = null;
60
61 // Update tab UI.
62 $widget.find('.charitable-mini-widget__tab').removeClass('charitable-mini-widget__tab--active').attr('aria-selected', 'false');
63 $(this).addClass('charitable-mini-widget__tab--active').attr('aria-selected', 'true');
64
65 // Reset to default for new tab.
66 var newTabCfg = config[newTab];
67 state.amount = newTabCfg['default'] || (newTabCfg.amounts.length ? newTabCfg.amounts[0] : 0);
68
69 // Re-render amounts.
70 renderAmounts($widget, config, state);
71 hideOtherInput($widget);
72 updateImpact($widget, config, state);
73 updateCta($widget, config, state);
74 });
75
76 // Amount button clicks.
77 $widget.on('click', '.charitable-mini-widget__amount', function() {
78 var $btn = $(this);
79 var amountVal = $btn.data('amount');
80
81 if (amountVal === 'other') {
82 state.isOther = true;
83 state.amount = null;
84 $widget.find('.charitable-mini-widget__amount').removeClass('charitable-mini-widget__amount--selected');
85 $btn.addClass('charitable-mini-widget__amount--selected');
86 showOtherInput($widget, config, state);
87 updateImpact($widget, config, state);
88 updateCta($widget, config, state);
89 return;
90 }
91
92 state.isOther = false;
93 state.amount = parseFloat(amountVal);
94 state.customAmount = null;
95
96 $widget.find('.charitable-mini-widget__amount').removeClass('charitable-mini-widget__amount--selected');
97 $btn.addClass('charitable-mini-widget__amount--selected');
98
99 hideOtherInput($widget);
100 clearError($widget);
101 updateImpact($widget, config, state);
102 updateCta($widget, config, state);
103 });
104
105 // Other amount input.
106 $widget.on('input', '.charitable-mini-widget__other-input', function() {
107 var val = parseFloat($(this).val());
108 state.customAmount = isNaN(val) ? null : val;
109 validateOther($widget, config, state);
110 updateCta($widget, config, state);
111 });
112
113 // CTA click — redirect mode.
114 $widget.on('click', '.charitable-mini-widget__cta:not(.charitable-modal-trigger)', function(e) {
115 e.preventDefault();
116 if (!canDonate(config, state)) {
117 return;
118 }
119 var amount = getEffectiveAmount(state);
120 var period = state.tab === 'monthly' ? 'month' : 'once';
121 var url = config.campaignUrl;
122 url += (url.indexOf('?') >= 0 ? '&' : '?') +
123 'amount=' + encodeURIComponent(amount) +
124 '&period=' + encodeURIComponent(period);
125 window.location.href = url;
126 });
127
128 // CTA click — modal mode: update data-* before modal JS handles the click.
129 $widget.on('click', '.charitable-mini-widget__cta.charitable-modal-trigger', function() {
130 if (!canDonate(config, state)) {
131 return false;
132 }
133 var amount = getEffectiveAmount(state);
134 var period = state.tab === 'monthly' ? 'month' : 'once';
135 $(this)
136 .attr('data-amount', amount)
137 .attr('data-period', period);
138 // Allow event to propagate to charitable-universal-modal.js.
139 });
140
141 // Initial render.
142 renderAmounts($widget, config, state);
143 updateImpact($widget, config, state);
144 updateCta($widget, config, state);
145 }
146
147 /**
148 * Re-render the amount buttons for the current tab.
149 */
150 function renderAmounts($widget, config, state) {
151 var tabCfg = config[state.tab];
152 var $container = $widget.find('.charitable-mini-widget__amounts');
153 $container.empty();
154
155 $.each(tabCfg.amounts, function(i, amount) {
156 var isSelected = (parseFloat(amount) === parseFloat(state.amount) && !state.isOther);
157 var $btn = $('<button>')
158 .attr('type', 'button')
159 .attr('data-amount', amount)
160 .addClass('charitable-mini-widget__amount')
161 .toggleClass('charitable-mini-widget__amount--selected', isSelected);
162
163 // Amount number display.
164 var displayAmount = config.currencyPos === 'right'
165 ? formatAmount(amount) + config.currencySymbol
166 : config.currencySymbol + formatAmount(amount);
167
168 var $num = $('<span>').addClass('charitable-mini-widget__amount-number').text(displayAmount);
169 $btn.append($num);
170
171 // Currency label.
172 if (config.showCurrency) {
173 var period = (state.tab === 'monthly' && config.recurringActive) ? '/mo' : '';
174 var $curr = $('<span>').addClass('charitable-mini-widget__amount-currency').text(config.currencyCode + period);
175 $btn.append($curr);
176 }
177
178 $container.append($btn);
179 });
180
181 // Other button.
182 if (tabCfg.showOther) {
183 var $other = $('<button>')
184 .attr('type', 'button')
185 .attr('data-amount', 'other')
186 .addClass('charitable-mini-widget__amount charitable-mini-widget__amount--other')
187 .toggleClass('charitable-mini-widget__amount--selected', state.isOther)
188 .text(wpCharitableMiniWidget.i18n.other || 'Other');
189 $container.append($other);
190 }
191 }
192
193 /**
194 * Format a number as a clean amount string (no trailing .00).
195 */
196 function formatAmount(amount) {
197 var n = parseFloat(amount);
198 return n === Math.floor(n) ? String(Math.floor(n)) : n.toFixed(2);
199 }
200
201 /**
202 * Show the "Other amount" input.
203 */
204 function showOtherInput($widget, config, state) {
205 var $wrap = $widget.find('.charitable-mini-widget__other-wrap');
206 var $input = $wrap.find('.charitable-mini-widget__other-input');
207
208 // Update placeholder for current period.
209 var periodHint = (state.tab === 'monthly' && config.recurringActive)
210 ? (wpCharitableMiniWidget.i18n.enterAmountMonthly || 'Enter amount/mo')
211 : (wpCharitableMiniWidget.i18n.enterAmount || 'Enter amount');
212 $input.attr('placeholder', periodHint).val('');
213
214 $wrap.show();
215 $input.focus();
216 }
217
218 /**
219 * Hide the "Other amount" input.
220 */
221 function hideOtherInput($widget) {
222 $widget.find('.charitable-mini-widget__other-wrap').hide();
223 $widget.find('.charitable-mini-widget__other-input').val('');
224 clearError($widget);
225 }
226
227 /**
228 * Validate the custom amount against min/max.
229 * Returns true if valid.
230 */
231 function validateOther($widget, config, state) {
232 var val = state.customAmount;
233 var $wrap = $widget.find('.charitable-mini-widget__other-wrap');
234 var $error = $widget.find('.charitable-mini-widget__error');
235
236 clearError($widget);
237
238 if (val === null || isNaN(val) || val <= 0) {
239 return false;
240 }
241
242 if (config.minAmount !== null && config.minAmount !== '' && val < parseFloat(config.minAmount)) {
243 $wrap.addClass('charitable-mini-widget__other-wrap--error');
244 $error.text(
245 (wpCharitableMiniWidget.i18n.minError || 'Minimum donation is ') +
246 config.currencySymbol + formatAmount(config.minAmount) + '.'
247 );
248 return false;
249 }
250
251 if (config.maxAmount !== null && config.maxAmount !== '' && val > parseFloat(config.maxAmount)) {
252 $wrap.addClass('charitable-mini-widget__other-wrap--error');
253 $error.text(
254 (wpCharitableMiniWidget.i18n.maxError || 'Maximum donation is ') +
255 config.currencySymbol + formatAmount(config.maxAmount) + '.'
256 );
257 return false;
258 }
259
260 return true;
261 }
262
263 /**
264 * Clear validation error state.
265 */
266 function clearError($widget) {
267 $widget.find('.charitable-mini-widget__other-wrap').removeClass('charitable-mini-widget__other-wrap--error');
268 $widget.find('.charitable-mini-widget__error').text('');
269 }
270
271 /**
272 * Update the impact text for the currently selected amount.
273 */
274 function updateImpact($widget, config, state) {
275 var $impact = $widget.find('.charitable-mini-widget__impact');
276
277 if (state.isOther) {
278 $impact.text('');
279 return;
280 }
281
282 var tabCfg = config[state.tab];
283 var key = String(state.amount);
284 // Try integer key too (e.g. "40" for amount 40.0).
285 var intKey = String(Math.floor(state.amount));
286 var text = tabCfg.impact[key] || tabCfg.impact[intKey] || '';
287
288 $impact.text(text); // text() is XSS-safe; HTML was stripped server-side.
289 }
290
291 /**
292 * Check if donation can proceed.
293 */
294 function canDonate(config, state) {
295 if (state.isOther) {
296 return validateOtherValue(config, state);
297 }
298 return state.amount > 0;
299 }
300
301 /**
302 * Validate without updating DOM (used in canDonate).
303 */
304 function validateOtherValue(config, state) {
305 var val = state.customAmount;
306 if (val === null || isNaN(val) || val <= 0) {
307 return false;
308 }
309 if (config.minAmount !== null && config.minAmount !== '' && val < parseFloat(config.minAmount)) {
310 return false;
311 }
312 if (config.maxAmount !== null && config.maxAmount !== '' && val > parseFloat(config.maxAmount)) {
313 return false;
314 }
315 return true;
316 }
317
318 /**
319 * Get the effective amount to donate.
320 */
321 function getEffectiveAmount(state) {
322 return state.isOther ? state.customAmount : state.amount;
323 }
324
325 /**
326 * Update CTA button enabled/disabled state.
327 */
328 function updateCta($widget, config, state) {
329 var $cta = $widget.find('.charitable-mini-widget__cta');
330 var able = canDonate(config, state);
331 $cta.prop('disabled', !able).toggleClass('charitable-mini-widget__cta--disabled', !able);
332 }
333
334 // Initialize on DOM ready and after AJAX content loads.
335 $(document).ready(function() {
336 initAll();
337 });
338
339 $(document).on('charitable:content:loaded', function() {
340 initAll();
341 });
342
343 // Expose for external init.
344 window.CharitableMiniWidget = { init: initAll };
345
346 })(jQuery);
347