We’re blessed to know that our resources encouraged your family.
\n
We want you to have this inspirational new devotional from bestselling author Jackie Hill Perry, Upon Waking. It’s an invitation to step away from your to-do lists and anxiety and into a daily reminder of what truly matters.
\n
It is our gift to you when you become one of the X new monthly donors we\'re looking for today. As a FamilyLife partner, your gift will strengthen your family and other families all year. \n
We’re blessed to know that our resources encouraged your family.
\n
We want you to have this inspirational new devotional from bestselling author Jackie Hill Perry, Upon Waking. It’s an invitation to step away from your to-do lists and anxiety and into a daily reminder of what truly matters.
\n
It is our gift to you when you become one of the X new monthly donors we\'re looking for today. As a FamilyLife partner, your gift will strengthen your family and other families all year. \n
',document.body.appendChild(o),a.querySelector("svg").onclick=()=>n(a),o.querySelector("svg").onclick=()=>n(o),a.querySelector("#popup-no").onclick=()=>{const e=Date.now()+6048e5;localStorage.setItem("popupDismissedUntil",String(e)),n(a)},a.querySelector("#popup-yes").onclick=()=>{n(a),setTimeout(l,300)},setTimeout(()=>a.showModal(),3e3)}();}catch(e) {VWO._.vAEH(e);}
return vwo_$('head')[0] && vwo_$('head')[0].lastChild;})("head")}}, R_725969_176_1_3_0:{ fn:function(log,nonce=''){return (function(x) {
try{
var ctx=vwo_$(x),el;
/*vwo_debug log("Revert","content",""); vwo_debug*/;
el=vwo_$('[vwo-element-id="1746468826149"]');
el.revertContentOp().remove();
} catch(e) {VWO._.vAEH(e);}
try{
var el,ctx=vwo_$(x);
/*vwo_debug log("Revert","addElement","body"); vwo_debug*/(el=vwo_$('[vwo-element-id="1746468826150"]')).remove();
} catch(e) {VWO._.vAEH(e);}
return vwo_$('head')[0] && vwo_$('head')[0].lastChild;})("head")}}, C_725969_176_1_3_1:{ fn:function(log,nonce=''){return (function(x) {var el,ctx=vwo_$(x);
/*vwo_debug log("_clickElement",".btn"); vwo_debug*/(el=vwo_$(".btn")).addClass("_vwo_coal_1697827292277");})(".btn")}}, R_725969_176_1_3_1:{ fn:function(log,nonce=''){return (function(x) {
if(!vwo_$.fn.vwoRevertHtml){
return;
};
var el,ctx=vwo_$(x);
/*vwo_debug log("Revert","_clickElement",".btn"); vwo_debug*/(el=vwo_$(".btn")).removeClass("_vwo_coal_1697827292277");})(".btn")}}, R_725969_183_1_2_1:{ fn:function(log,nonce=''){return (function(x) {
try{
var ctx=vwo_$(x),el;
/*vwo_debug log("Revert","content",""); vwo_debug*/;
el=vwo_$('[vwo-element-id="1746479885398"]');
el.revertContentOp().remove();
} catch(e) {VWO._.vAEH(e);}
try{
var el,ctx=vwo_$(x);
/*vwo_debug log("Revert","addElement","body"); vwo_debug*/(el=vwo_$('[vwo-element-id="1746479885399"]')).remove();
} catch(e) {VWO._.vAEH(e);}
return vwo_$('head')[0] && vwo_$('head')[0].lastChild;})("head")}}, C_725969_183_1_2_1:{ fn:function(log,nonce=''){return (function(x) {
try{
var _vwo_sel = vwo_$("`);
!vwo_$("head").find('#1746479885398').length && vwo_$('head').append(_vwo_sel);}catch(e) {VWO._.vAEH(e);}
try{;
/*vwo_debug log("addElement","body"); vwo_debug*/;
el=vwo_$("body")
;
!el.find('[vwo-op-1746479885400=""]').length && el.vwoElement({"position":"append","html":""});}catch(e) {VWO._.vAEH(e);}
try{const ARM_EXIT_INTENT_AFTER_SECONDS = 5;
const IDLE_TIMER_COUNTDOWN_SECONDS = 90;
//
//
//
//
//
const TOKEN_NAME = "NA__FL_ExitIntentPopup_Apr2025";
const getToken = (t) => JSON.parse(localStorage.getItem(t)) || null;
const setToken = (t, v) => localStorage.setItem(t, JSON.stringify(v));
function getCurrentDate (d = new Date()) { return d.toISOString().split('T')[0] }
function vwoCustomEvent (labelValue) {
window.VWO = window.VWO || [];
VWO.event = VWO.event || function () {VWO.push(["event"].concat([].slice.call(arguments)))};
VWO.event("customEvent", { "label": labelValue.toString() });
}
const onExitIntent=(e,t=5,...q)=>{let n=t=>{if(!t.toElement&&!t.relatedTarget){document.removeEventListener("mouseout",n),window.onblur=()=>{},e.call();let o=e=>{"dialog"==e.target.tagName.toLowerCase()&&(e.target.close(),window.onclick=()=>{})};window.onclick=o}};/*console.log(`exit intent trigger will be armed in ${t} seconds`),*/setTimeout(()=>{console.log("exit intent trigger armed"),document.addEventListener("mouseout",n),window.onblur=n},1e3*t,...q)};
const isAsyncFunction = (func) => typeof func === 'function' && func.constructor.name === "AsyncFunction";
function onIdleTimer (callbackFn, timeoutMs = 60000, ...args) {
const RESET_TIMER_ON_EVENTS = [ 'click', 'scroll' ]; // these events set the timer back to the original value
const CANCEL_TIMER_ON_EVENTS = [ /*'input'*/ ]; // these events will clear the idle timer
let timer;
const removeTimer = function () {
const interval = this;
clearTimeout(interval);
}.bind(timer);
const resetTimer = () => {
clearTimeout(timer);
timer = setTimeout(() => {
try {
callbackFn.call(this, ...args);
} catch (error) {
console.error(error);
} finally {
RESET_TIMER_ON_EVENTS.forEach((eventName) => document.removeEventListener(eventName, eventHandler));
CANCEL_TIMER_ON_EVENTS.forEach((eventName) => document.removeEventListener(eventName, eventHandler));
}
}, timeoutMs);
console.log("Idle timer reset.", (new Date).toTimeString());
};
const eventHandler = () => resetTimer();
RESET_TIMER_ON_EVENTS.forEach((eventName) => document.addEventListener(eventName, eventHandler));
CANCEL_TIMER_ON_EVENTS.forEach((eventName) => document.addEventListener(eventName, removeTimer));
resetTimer();
}
function showExitIntent () {
let status = getToken(TOKEN_NAME) || {};
if (status.hasOwnProperty("lastShown") && status.lastShown === getCurrentDate()) // if last shown today
return console.info("showExitIntent:", "Last shown today already (maybe from idle timer trigger). Exit intent will NOT be shown."), false;
//
const dialog = this;
dialog.dispatchEvent(new CustomEvent('update')); // trigger an update
vwoCustomEvent(TOKEN_NAME+":show");
//
Array.from(dialog.querySelectorAll('[data-target="window.NA.DonationForm"]')).forEach((button) => {
const eventType = (this.dataset.trigger || button.getAttribute('data-trigger')) ?? 'click';
button.addEventListener(eventType, function updateDonationForm (event) {
const instructionQueue = (this.dataset.action || this.getAttribute('data-action')).split(';').map((instruction) => instruction.split('=')).map((instruction) => {
const [ functionName, argumentsString ] = instruction;
let argumentsArray = argumentsString.split(',').map((argument) => {
argument = argument.replace(/[\'\"]/gm,'');
argument = !Number.isNaN(parseInt(argument)) ? parseInt(argument) : argument;
return argument;
}); // split, map on argumentsString
return [ functionName.trim(), argumentsArray ];
}); // split, map, map on instructionQueue
instructionQueue.forEach(async ([ donationFormApiFunction, args ]) => {
if (window.NA.DonationForm.hasOwnProperty(donationFormApiFunction) && typeof window.NA.DonationForm[donationFormApiFunction] === 'function') {
if (isAsyncFunction(typeof window.NA.DonationForm[donationFormApiFunction])) {
await window.NA.DonationForm[donationFormApiFunction](...args);
} else {
window.NA.DonationForm[donationFormApiFunction](...args);
}
} else {
console.warn("window.NA.DonationForm does not have property", donationFormApiFunction);
}
}) // forEach of instructionQueue
}); // addEventListener to button
button.addEventListener('click', function pushState () {
const params = new URLSearchParams(window.location.search);
params.set('src', 'exitIntentPopup'); // adds or updates
const newUrl = `${window.location.pathname}?${params.toString()}`;
history.pushState(null, '', newUrl);
});
button.addEventListener('click', function scroll () {
const scrollTarget = document.querySelector('.donation-form .row-option-other');
if (scrollTarget) {
let marginTop = '16px';
scrollTarget.style.setProperty('scroll-margin-block-start', marginTop);
scrollTarget.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
});
}); // forEach [data-target="window.NA.DonationForm"]
//
const dialogButton1 = dialog.querySelector('.btn');
dialogButton1?.addEventListener('click', (e) => {
vwoCustomEvent(TOKEN_NAME+":click");
let status = getToken(TOKEN_NAME) || {};
status.converted = true; // mark as converted
setToken(TOKEN_NAME, status);
});
//
dialog.showModal();
//
status.lastShown = getCurrentDate();
status.lastDismissed = status.lastDismissed || false;
status.totalDismissed = status.totalDismissed || 0;
status.totalShown = status.totalShown && status.totalShown >= 1 ? status.totalShown + 1 : 1;
status.converted = status.converted || status.converted === false ? status.converted : false;
setToken(TOKEN_NAME, status);
//
return dialog;
}
function main () {
const dialog = this;
//
const onExitIntentCallback = (function () {
console.info("onExitIntent");
const dialog = this;
showExitIntent.call(dialog);
}).bind(dialog);
if (shouldShowExitIntent() === true) {
console.info("main:", "should show, arm in " + ARM_EXIT_INTENT_AFTER_SECONDS + " seconds");
if (window.innerWidth < 768) {
console.log("Idle timer started."),
onIdleTimer(onExitIntentCallback, IDLE_TIMER_COUNTDOWN_SECONDS * 1000);
}
onExitIntent(onExitIntentCallback, ARM_EXIT_INTENT_AFTER_SECONDS); // arm exit intent popup after N seconds
} else {
dialog.style.setProperty("display", "none", "important");
}
}
function shouldShowExitIntent () {
dismissHandler();
const status = getToken(TOKEN_NAME) || {};
//console.log(status);
if ( !status.hasOwnProperty("lastShown") ) { // first time; token is not defined
return console.info("shouldShowExitIntent:", "Exit intent popup has not been seen. Exit intent will be shown."), true; // show
}
else if ( status.hasOwnProperty("converted") && status.converted === true ) { // if the user has already converted (clicked the link)
return console.info("shouldShowExitIntent:", "Exit intent popup has already been seen and the user converted from it. Exit intent will be NOT shown."), false; // do not show
}
else if ( status.hasOwnProperty("lastDismissed") && status.lastDismissed !== getCurrentDate() ) { // returning; token has data, last seen NOT today, and user has NOT signed up yet
return console.info("shouldShowExitIntent:", "Exit intent popup has been seen, but not today AND they have not converted from it yet. Exit intent will be shown."), true; // show
}
else {
return console.info("shouldShowExitIntent: Exit intent popup has been seen, the user has not converted from it yet, and the popup was last dismissed TODAY. Exit intent will be NOT shown."), false; // do not show
}
}
window.NA.DonationForm.init({ makeTabbed: false }).then((donationFormReference) => {
console.log("Donation Form API: %c READY %c", 'background-color: MediumSeaGreen; color: white; font-weight: bold;', 'background-color: unset; color: unset; font-weight: unset;');
const asyncWaitForElement=(e,t=10)=>{return new Promise((n,r)=>{if(document.querySelector(e)&&document.querySelector(e).isConnected)return n(document.querySelector(e));let o,c=new MutationObserver(t=>{for(let r of t)for(let i of r.addedNodes)if(i instanceof HTMLElement&&i.matches(e)&&i.isConnected)return clearInterval(o),c.disconnect(),n(i)});c.observe(document.documentElement,{childList:!0,subtree:!0}),o=setInterval(e=>{if(document.querySelector(e)&&document.querySelector(e).isConnected)return clearInterval(o),c.disconnect(),n(document.querySelector(e))},50,e),setTimeout(t=>{t.disconnect(),r(Error(`Element with selector "${e}" not found within the time limit.`))},1e3*t,c)})},asyncWaitForElements=(...S)=>{return Promise.all(S.map((s)=>asyncWaitForElement(s)));};
asyncWaitForElement('#'+TOKEN_NAME).then((dialogElement) => main.call(dialogElement)).catch((error) => { throw (error instanceof Error ? error : new Error(error)) });
});
function dismissHandler (dismiss = (target) => target.tagName.toLowerCase() === 'dialog' ? target.close() : target.style.setProperty("display", "none")) {
document.querySelectorAll('[data-action="dismiss"]').forEach((element) => {
let targets = Array.from(document.querySelectorAll(element.getAttribute('data-target'))) || [];
targets.length > 0 && element.addEventListener('click', (e) => Array.from(document.querySelectorAll(e.srcElement.getAttribute('data-target'))).forEach((target) => {
dismiss(target);
let status = getToken(TOKEN_NAME);
status.lastDismissed = getCurrentDate();
status.totalDismissed += 1;
setToken(TOKEN_NAME, status);
}));
});
}
}catch(e) {VWO._.vAEH(e);}
return vwo_$('head')[0] && vwo_$('head')[0].lastChild;})("head")}}, C_725969_183_1_2_0:{ fn:function(log,nonce=''){return (function(x) {
try{
var _vwo_sel = vwo_$("`);
!vwo_$("head").find('#1746479885395').length && vwo_$('head').append(_vwo_sel);}catch(e) {VWO._.vAEH(e);}
try{}catch(e) {VWO._.vAEH(e);}
try{function getCurrentDate (d = new Date()) { return d.toISOString().split('T')[0] }
function vwoCustomEvent (labelValue) {
window.VWO = window.VWO || [];
VWO.event = VWO.event || function () {VWO.push(["event"].concat([].slice.call(arguments)))};
VWO.event("customEvent", { "label": labelValue.toString() });
}
function vwoDonationInterrupter2 (id, type) {
window.VWO = window.VWO || [];
VWO.event = VWO.event || function () {VWO.push(["event"].concat([].slice.call(arguments)))};
if (type === "shown" || type === "error") {
VWO.event("donationInterrupter2", {
"id": id?.toString(),
"type": type?.toString(),
});
} else {
VWO.event("donationInterrupter2action", {
"id": id?.toString(),
"type": type?.toString(),
});
}
}
class SortOfSimpleCountdownTimer{constructor(t,i=document.createElement("div"),e={onEnd:function(t){t.hide()},onChange:function(t){}}){return console.log(t),this._time={end:t instanceof Date?t:new Date(t).getTime(),now:new Date().getTime()},this._time.remaining=this._time.end-this._time.now,this._data=null,i.classList.contains("countdown")||i.classList.add("countdown"),this.element=i,this.callbacks=e,setTimeout(()=>this.init(),50),this}init(){console.log("Countdown Timer initialized."),this._interval=setInterval(t=>t.update(),1e3,this),this.update()}update(){try{this.now=new Date().getTime(),this.remaining=this.end-this.now,this.now;let t=this.remaining;if(!this.remaining||"NaN"==this.remaining){console.warn("Remaining time is not a number or is falsy.");return}if(this.remaining<=0)this.finish(),this.hide();else{let i={days:Math.floor(t/864e5),hours:Math.floor(t%864e5/36e5),minutes:Math.floor(t%36e5/6e4),seconds:Math.floor(t%6e4/1e3)};this.data=i;let e=[];Object.entries(i).filter(([t,i])=>"seconds"==t).forEach(([t,i])=>e.push(`
${i||0}
`)),this.element.innerHTML=e.join("\n")}this.callbacks&&"function"==typeof this.callbacks.onChange&&this.callbacks.onChange.call(this,this.element)}catch(n){this.element.style.setProperty("display","none"),console.error(n)}}finish(){this._interval?(clearInterval(this._interval),console.info("Countdown finished.")):console.error("Failed to clear interval."),this.callbacks&&"function"==typeof this.callbacks.onEnd&&this.callbacks.onEnd.call(this,this.element)}hide(){this.element.style.setProperty("display","none")}get now(){return this._time.now}set now(t){this._time.now=t||new Date().getTime()}get end(){return this._time.end}set end(t){this._time.end=t}get remaining(){return this._time.remaining}set remaining(t){this._time.remaining=t||this._time.end-this._time.now}get data(){return this._data}set data(t){this._data=t}}
class NextAfterDonationMicrosite_GiftArrayButton {
static activeClass = 'active';
constructor (element) {
if (!(element instanceof HTMLElement) || !(element.classList.contains('amt-button') || element.classList.contains('option')))
throw new TypeError("Argument 0 must be an instance of HTMLElement with a class of '.amt-button' or '.option'.");
this.button = element;
}
get value () {
return parseInt(this.button.dataset.amt);
}
set value (newValue) {
this.button.dataset.amt = parseInt(newValue);
}
get text () {
return this.button.textContent;
}
set text (newText) {
this.button.textContent = newText;
}
get active () {
return this.button.classList.contains(NextAfterDonationMicrosite_GiftArrayButton.activeClass) ? true : false;
}
set active (boolean) {
if (boolean) {
this.select();
if (!this.active) // if for some reason the button was selected but still does not have the active class
this.button.classList.add(NextAfterDonationMicrosite_GiftArrayButton.activeClass);
} else {
this.button.classList.remove(NextAfterDonationMicrosite_GiftArrayButton.activeClass);
}
}
click () { this.button.click() }
select () { this.click() }
addEventListener (eventType, eventHandlerCallback) {
this.button.addEventListener(eventType, eventHandlerCallback);
}
get amount () {
return this.value;
}
set amount (newAmount) {
if (Number.isNaN(parseInt(newAmount)) || parseInt(newAmount) < 0)
throw new Error("New amount must be a valid number greater than 0.");
newAmount = parseInt(newAmount);
this.text = parseFloat(newAmount).toLocaleString('en-US', { format: 'currency', currency: 'USD', minimumFractionDigits: 0, maximumFractionDigits: 0 });
this.value = parseInt(newAmount);
}
}
class NextAfterDonationMicrosite_GiftArrayButtonRadio extends NextAfterDonationMicrosite_GiftArrayButton {
constructor (element) {
super(element);
this.radio = element.matches('input[type="radio"]') ? element : element.querySelector('input[type="radio"]');
}
get value () {
return parseInt(this.radio.value);
}
set value (newValue) {
this.radio.value = parseInt(newValue);
}
set text (newText) {
const textNode = this.radio.nextSibling.nodeType === Node.TEXT_NODE ? this.radio.nextSibling : [...this.element.childNodes].find(({ nodeType, nodeValue }) => nodeType === Node.TEXT_NODE && nodeValue.length > 0);
textNode.nodeValue = newText.toString();
}
addEventListener (eventType, eventHandlerCallback) {
if (eventType == 'click') {
this.button.addEventListener(eventType, eventHandlerCallback);
} else {
this.radio.addEventListener(eventType, eventHandlerCallback);
}
}
}
class NextAfterDonationMicrosite_GiftArrayOtherAmount {
constructor (element) {
if (!(element instanceof HTMLInputElement) || !(element.type == 'text'))
throw new TypeError("Argument 0 must be an instance of HTMLInputElement with a 'type' of 'text'.");
this.input = element;
}
get value () {
return this.input.value;
}
set value (newValue) {
this.input.dispatchEvent(new Event('click'));
this.input.dispatchEvent(new Event('focus'));
this.input.value = newValue;
// this.input.dispatchEvent(new Event('keyup'));
this.input.dispatchEvent(new Event('input'));
this.input.dispatchEvent(new Event('change'));
this.input.dispatchEvent(new Event('blur'));
}
get text () {
return this.input.placeholder;
}
set text (newPlaceholderText) {
this.input.placeholder = newPlaceholderText;
}
get amount () {
return parseFloat(this.value);
}
set amount (newAmount) {
if (Number.isNaN(parseInt(newAmount)) || parseInt(newAmount) <= 0)
throw new Error("New amount must be a valid number greater than 0.");
newAmount = parseFloat(newAmount);
this.value = newAmount;
}
addEventListener (eventType, eventHandlerCallback) {
this.input.addEventListener(eventType, eventHandlerCallback);
}
}
class NextAfterDonationMicrosite_GiftArray extends Array {
constructor (items) {
if (!Array.isArray(items) && items.length === 0) {
throw !Array.isArray(items) ? new TypeError("GiftArray: Arugment 1 is not an instance of Array:" + items.join(', ')) : new RangeError("GiftArray: Arugment 1 is not an instance of Array with a length greater than 0:" + items.join(', '));
}
if (items.every((item) => item instanceof NextAfterDonationMicrosite_GiftArrayButton || item instanceof NextAfterDonationMicrosite_GiftArrayOtherAmount)) {
if (items.find((item) => item instanceof NextAfterDonationMicrosite_GiftArrayOtherAmount)) { // if the array is mixed (both GiftArrayButton and GiftArrayButtonOtherAmount)
let temp = items.find((item) => item instanceof NextAfterDonationMicrosite_GiftArrayOtherAmount); // temporarily hold the other amount
items = items.filter((item) => item instanceof NextAfterDonationMicrosite_GiftArrayButton); // assign items to be only the gift array button temporarily (removed the other amount)
items.push(temp); // add back the other amount to items so that it's always the last entry in the array
}
} else if (items.every((item) => item instanceof HTMLElement)) {
items = items.map((item) => {
if (item.matches('.stripe-donation-other-amt, .option-other-text'))
return new NextAfterDonationMicrosite_GiftArrayOtherAmount(item);
return item.querySelector('input[type="radio"]') ? new NextAfterDonationMicrosite_GiftArrayButtonRadio(item) : new NextAfterDonationMicrosite_GiftArrayButton(item);
});
} else {
throw new Error("GiftArray: Arugment 1 is not of type HTMLElement[], or GiftArrayButton[]:" + items.join(', '));
}
super(...items);
this.Buttons = items.filter((item) => item instanceof NextAfterDonationMicrosite_GiftArrayButton) ?? [];
this.OtherAmountInput = items.find((item) => item instanceof NextAfterDonationMicrosite_GiftArrayOtherAmount) ?? null;
}
get amount () {
const activeButton = this.Buttons.find((item) => item.active);
if (activeButton) { // active button was found in the gift array
return activeButton.amount;
} else {
return this.OtherAmountInput.amount;
}
}
set amount (newAmount) {
if (Number.isNaN(parseInt(newAmount)) || parseInt(newAmount) <= 0)
throw new Error("New amount must be a valid number greater than 0.");
newAmount = parseFloat(newAmount);
const matchingButton = this.find((item) => parseFloat(item.value) === newAmount);
if (matchingButton) {
matchingButton.click();
} else {
this.OtherAmountInput.amount = newAmount;
const activeButton = this.Buttons.find((item) => item.active);
if (activeButton) activeButton.active = false;
}
}
addEventListeners (eventType, eventHandlerCallback) {
this.forEach((item) => item.addEventListener(eventType, eventHandlerCallback));
}
}
class NextAfterDonationMicrosite_FrequencyToggle {
constructor (element) {
if (!(element instanceof HTMLInputElement) || !(element.type == 'checkbox'))
throw new TypeError("Argument 0 must be an instance of HTMLInputElement with a 'type' of 'checkbox'.");
this.checkbox = element;
}
get frequency () {
return this.checkbox.checked ? true : false;
}
set frequency (boolean) {
if (boolean !== this.frequency) { // only if the arugment is different from what is already set
if (boolean === true)
this.checkbox.checked = true;
if (boolean === false)
this.checkbox.checked = true;
}
}
get recurring () {
return this.frequency ? true : false;
}
set recurring (bool) {
this.frequency = bool;
}
addEventListener (eventType, eventHandlerCallback) {
this.checkbox.addEventListener(eventType, eventHandlerCallback);
}
}
class NextAfterDonationMicrosite_FrequencyRadio {
constructor (element) {
if (!(element instanceof HTMLInputElement) || !(element.type == 'radio'))
throw new TypeError("Argument 0 must be an instance of HTMLInputElement with a 'type' of 'radio'.");
this.radio = element;
}
get value () {
return this.radio.value;
}
set value (newValue) {
this.radio.value = newValue;
}
get frequency () {
return this.radio.checked ? true : false;
}
set frequency (boolean) {
if (boolean !== this.frequency) { // only if the arugment is different from what is already set
if (boolean === true)
this.radio.checked = true;
if (boolean === false)
this.radio.checked = true;
}
}
get recurring () {
return this.frequency ? true : false;
}
set recurring (bool) {
this.frequency = bool;
}
get checked () {
return this.radio.checked;
}
set checked (bool) {
this.radio.checked = bool ? true : false;
}
click () {
this.radio.click();
}
addEventListener (eventType, eventHandlerCallback) {
this.radio.addEventListener(eventType, eventHandlerCallback);
}
}
class NextAfterDonationMicrosite_FrequencyArray extends Array {
constructor (items) {
if (!Array.isArray(items) && items.length === 0) {
throw !Array.isArray(items) ? new TypeError("FrequencyArray: Arugment 1 is not an instance of Array:" + items.join(', ')) : new RangeError("FrequencyArray: Arugment 1 is not an instance of Array with a length greater than 0:" + items.join(', '));
}
if (items.every((item) => item instanceof NextAfterDonationMicrosite_FrequencyRadio)) {
items = items;
} else if (items.every((item) => item instanceof HTMLElement)) {
items = items.map((item) => (item instanceof HTMLInputElement && item.type === 'radio') ? new NextAfterDonationMicrosite_FrequencyRadio(item) : undefined);
} else {
throw new Error("FrequencyArray: Arugment 1 is not of type HTMLInputElement[]|HTMLElement[], or FrequencyRadio[]:" + items.join(', '));
}
super(...items);
this.Options = items.filter((item) => item instanceof NextAfterDonationMicrosite_FrequencyRadio) ?? [];
}
get frequency () {
const checkedRadio = this.Options.find(({ radio }) => radio.checked);
if (checkedRadio) { // active button was found in the gift array
return checkedRadio.value;
} else {
return undefined;
}
}
set frequency (newFrequency) {
const matchingRadio = this.Options.find(({ value }) => value === newFrequency);
if (matchingRadio) {
matchingRadio.click();
} else {
console.warn("Invalid frequency: ", newFrequency);
}
}
addEventListener (eventType, eventHandlerCallback) {
this.Options.forEach((option) => option.addEventListener(eventType, eventHandlerCallback));
}
}
class NextAfterDonationMicrosite_FrequencyTab {
constructor (element) {
if (!(element instanceof HTMLElement))
throw new TypeError("Argument 0 must be an instance of HTMLElement.");
this.element = element;
}
get text () {
return this.element.textContent;
}
set text (newText) {
this.element.textContent = newText;
}
click () {
this.element.click();
}
addEventListener (eventType, eventHandlerCallback) {
this.radio.addEventListener(eventType, eventHandlerCallback);
}
}
class NextAfterDonationMicrosite_FrequencyTabs extends Array {
constructor (items) {
if (!Array.isArray(items) && items.length === 0) {
throw !Array.isArray(items) ? new TypeError("FrequencyTabs: Arugment 1 is not an instance of Array:" + items.join(', ')) : new RangeError("FrequencyTabs: Arugment 1 is not an instance of Array with a length greater than 0:" + items.join(', '));
}
if (items.every((item) => item instanceof NextAfterDonationMicrosite_FrequencyTab)) {
items = items;
} else if (items.every((item) => item instanceof HTMLElement)) {
items = items.map((item) => new NextAfterDonationMicrosite_FrequencyTab(item) ?? undefined);
} else {
throw new Error("FrequencyTabs: Arugment 1 is not of type HTMLElement[], or FrequencyTab[]:" + items.join(', '));
}
super(...items);
this.Buttons = items.filter((item) => item instanceof NextAfterDonationMicrosite_FrequencyTab) ?? [];
}
select (value) {
const matchingTab = this.Buttons.find(({ text }) => text.match(value) || text.includes(value));
if (matchingTab) {
matchingTab.click();
} else {
console.warn("Invalid value: ", value);
}
}
addEventListener (eventType, eventHandlerCallback) {
this.Buttons.forEach((option) => option.addEventListener(eventType, eventHandlerCallback));
}
}
//
const lockedProperty = { writable: false, configurable: false, enumerable: true };
function DonationFormAPI (elements, options = {}) {
const defaultOptions = {
min: 1.00,
max: 999999.99,
makeTabbed: false,
fakeSubmit: true,
overrideGiftArrayValues: false,
};
options = { ...defaultOptions, ...options };
//
const { recurringOptions, amountButtons, submitButton, root } = elements;
const debug = {
log: (...args) => window.NA.DonationForm.DEBUG_MODE && console.log(...args),
info: (...args) => window.NA.DonationForm.DEBUG_MODE && console.log(...args),
warn: (...args) => window.NA.DonationForm.DEBUG_MODE && console.log(...args),
error: (...args) => window.NA.DonationForm.DEBUG_MODE && console.log(...args),
};
//
const api = new Object();
Object.defineProperty(api, 'root', {
value: root,
writable: false,
configurable: true,
enumerable: true,
});
Object.defineProperties(api, {
FORM_MINIMUM: { // constant
value: options.min || 0,
...lockedProperty
},
FORM_MAXIMUM: { // constant
value: options.max || Infinity,
...lockedProperty
},
});
Object.defineProperties(api, {
GiftArray: { // variable
value: new NextAfterDonationMicrosite_GiftArray(amountButtons),
writable: false,
configurable: true,
enumerable: true,
},
Frequency: { // variable
value: Array.isArray(recurringOptions) ? new NextAfterDonationMicrosite_FrequencyArray(recurringOptions) : new NextAfterDonationMicrosite_FrequencyToggle(recurringOptions),
writable: false,
configurable: true,
enumerable: true,
},
SubmitButton: { // variable
value: submitButton,
writable: false,
configurable: false,
enumerable: true,
}
});
if (elements.tabs && Array.isArray(elements.tabs) && elements.tabs.length >= 2 && elements.tabs.every((item) => item instanceof HTMLElement)) {
Object.defineProperty(api, 'Tabs', {
value: new NextAfterDonationMicrosite_FrequencyTabs(elements.tabs),
writable: false,
configurable: true,
enumerable: true,
});
}
Object.defineProperties(api, {
getFrequency: { // function
value: async function () {
if (!this || this === null)
throw new Error("validate: Unable to read API context.");
/*if (this.Tabs && this.Tabs instanceof NextAfterDonationMicrosite_FrequencyTabs) { // tabs
}
else*/ if (this.Frequency instanceof NextAfterDonationMicrosite_FrequencyArray) { // radios
return new Promise((resolve, reject) => {
try {
resolve(this.Frequency.frequency);
} catch (error) {
reject(error);
}
});
}
else if (this.Frequency instanceof NextAfterDonationMicrosite_FrequencyToggle) { // checkbox
return new Promise((resolve, reject) => {
try {
resolve(this.Frequency.frequency);
} catch (error) {
reject(error);
}
});
}
}, ...lockedProperty
},
setFrequency: { // function
value: async function (frequency) {
if (!this || this === null)
throw new Error("validate: Unable to read API context.");
if (this.Frequency instanceof NextAfterDonationMicrosite_FrequencyArray) { // radios
if (this.Tabs && this.Tabs instanceof NextAfterDonationMicrosite_FrequencyTabs) { // tabs
let index = this.Frequency.Options.findIndex(({ value }) => value.trim().match(new RegExp(frequency, 'i')) || value.trim().includes(frequency)) ?? -1;
if (index > -1 && index < this.Tabs.Buttons.length) {
this.Tabs.Buttons[index]?.click();
} else {
this.Tabs.select(frequency);
}
}
return new Promise(async (resolve, reject) => {
try {
this.Frequency.frequency = frequency;
if (await this.getFrequency() === frequency)
resolve(frequency);
} catch (error) {
reject(error);
}
});
}
if (this.Frequency instanceof NextAfterDonationMicrosite_FrequencyToggle) { // checkbox
if (this.Tabs && this.Tabs instanceof NextAfterDonationMicrosite_FrequencyTabs) { // tabs
let index = this.Frequency.Options.findIndex(({ value }) => value.trim().match(new RegExp(frequency, 'i')) || value.trim().includes(frequency)) ?? -1;
if (index > -1 && index < this.Tabs.Buttons.length) {
this.Tabs.Buttons[index]?.click();
} else {
this.Tabs.select(frequency);
}
}
return new Promise(async (resolve, reject) => {
try {
this.Frequency.frequency = frequency;
if (await this.getFrequency() === frequency)
resolve(frequency);
} catch (error) {
reject(error);
}
});
}
}, ...lockedProperty
},
getAmount: { // function
value: async function () {
if (!this || this === null)
throw new Error("validate: Unable to read API context.");
return new Promise((resolve, reject) => {
try {
resolve(this.GiftArray.amount);
} catch (error) {
reject(error);
}
});
}, ...lockedProperty
},
setAmount: { // function
value: async function (amount, frequency = undefined) {
if (!this || this === null)
throw new Error("validate: Unable to read API context.");
return new Promise(async (resolve, reject) => {
try {
this.GiftArray.amount = amount;
if (await this.getAmount() === amount)
resolve(amount);
} catch (error) {
reject(error);
}
});
}, ...lockedProperty
},
getRecurring: { // function
value: async function () {
if (!this || this === null)
throw new Error("validate: Unable to read API context.");
return new Promise((resolve, reject) => {
try {
resolve(this.Frequency.recurring);
} catch (error) {
reject(error);
}
});
}, ...lockedProperty
},
setRecurring: { // function
value: async function (bool) {
if (!this || this === null)
throw new Error("validate: Unable to read API context.");
return new Promise(async (resolve, reject) => {
try {
this.Frequency.frequency = bool ? true : false;
if (await this.getRecurring() === bool)
resolve(bool);
} catch (error) {
reject(error);
}
});
}, ...lockedProperty
},
freqency: {
get () { return this.getFrequency() },
set (value) { this.setFrequency(value) },
enumerable: true, configurable: true,
},
amount: {
get () { return this.getAmount() },
set (value) { this.setAmount(value) },
enumerable: true, configurable: true,
},
recurring: {
get () { return this.getRecurring() },
set (value) { this.setRecurring(value) },
enumerable: true, configurable: true,
},
});
Object.defineProperties(api, {
submit: { // function
value: async function (condition = this.validate||function(){return true}) {
//this.hooks['onBeforeSubmit'].forEach((callback) => callback.call(this));
let result;
const isAsyncFunction = (func) => func.constructor.name === "AsyncFunction";
if (Array.isArray(condition)) {
if (condition.every((c) => typeof c === 'function' && isAsyncFunction(c))) {
result = await Promise.all(condition.map(async (c) => await c.call(this)));
} else if (condition.every((c) => typeof c === 'function')) {
result = condition.every((c) => c.call(this));
} else if (condition.every((c) => c === true || c === false)) {
result = condition.every((c) => c);
}
} else if (typeof condition === 'function' && isAsyncFunction(condition)) {
result = await condition.call(this);
} else if (typeof condition === 'function') {
result = condition.call(this);
} else if (condition === true || condition === false) {
result = condition;
} else {
console.error("Unknown error.");
debugger;
}
//
if (result === true) {
if (window.NA.DonationForm.hasOwnProperty("DEBUG_MODE") && window.NA.DonationForm["DEBUG_MODE"] == true)
return console.log("Submit aborted (debug mode is enabled).");
this.SubmitButton.click(),
this.hooks['onSubmit'].forEach((callback) => callback.call(this));
//this.hooks['onAfterSubmit'].forEach((callback) => callback.call(this));
} else {
return console.log("Submit failed (conditions did not evaluate to true).");
}
}, ...lockedProperty
},
interceptSubmit: { // function
value: function (handleInterceptedSubmit = () => { return new Promise((resolve) => resolve(undefined)) }) {
try {
window.NA.DonationForm.SubmitButtonCopy = window.NA.DonationForm.SubmitButtonCopy || createNewSubmitButton(window.NA.DonationForm.SubmitButton, { cloneOriginal: false, hideOriginal: true, observeOriginal: false });
window.NA.DonationForm.SubmitButtonCopy.addEventListener('click', async (event) => {
event.preventDefault(), event.stopPropagation();
const shouldRetryInterrupterAfterFailedSubmit = false;
const shouldFormSubmit = await handleInterceptedSubmit.call(this, event);
if (shouldFormSubmit) {
console.info("Submit allowed by initial interceptSubmit callback function resulting in a truthy evaluation.");
const formIsValid = await window.NA.DonationForm.validate();
if (!formIsValid) { // if the the form SHOULD submit (based on handleInterceptedSubmit return value) and if the form IS NOT valid (there are known errors in the form found by calling the form API's validate function)
console.warn("Form has known errors. Attempting to submit to show errors then retrying.");
console.log("Submitting...");
window.NA.DonationForm.submit(true); // submit anyway to trigger the error to be shown
if (shouldRetryInterrupterAfterFailedSubmit) { // if true, the fake button that will intercept submit will be shown again after a failed submit, otherwise it will show the original to ensure it submits the next time
window.NA.DonationForm.SubmitButton.style.setProperty("display", "none"), debug.info("SubmitButton hidden."); // hide the original submit button again
// window.NA.DonationForm.SubmitButtonCopy.style.setProperty("display", "none"), debug.info("SubmitButtonCopy hidden."); // hide the copy of the submit button again
window.NA.DonationForm.SubmitButtonCopy.style.removeProperty("display"), debug.info("SubmitButtonCopy unhidden."); // show the copy of the submit button
} else {
window.NA.DonationForm.SubmitButtonCopy.style.setProperty("display", "none"), debug.info("SubmitButtonCopy hidden."); // hide the copy of the submit button again
// window.NA.DonationForm.SubmitButton.style.setProperty("display", "none"), debug.info("SubmitButton hidden."); // hide the copy of the submit button again
window.NA.DonationForm.SubmitButton.style.removeProperty("display"), debug.info("SubmitButton unhidden."); // hide the original submit button again
}
} else { // if the the form SHOULD submit (based on handleInterceptedSubmit return value) and if the form IS valid
console.log("Submitting...");
window.NA.DonationForm.submit(true);
}
} else { // if the form SHOULD NOT submit (based on handleInterceptedSubmit return value) -- usually means show donation interrupter after preventing submit action
console.log("Submit prevented."), console.info("Next submit will be allowed.");
window.NA.DonationForm.SubmitButton.style.removeProperty("display"), debug.info("SubmitButton unhidden."); // show the original submit button so that if something goes wrong the user can still click the submit button
window.NA.DonationForm.SubmitButtonCopy.style.setProperty("display", "none"), debug.info("SubmitButtonCopy hidden."); // hide the copy of the submit button that intercepts submit attempts so that there aren't two buttons
}
});
console.log("Submit intercept added.\nButton:", window.NA.DonationForm.SubmitButtonCopy);
} catch (error) {
console.error("Failed to add submit intercept:", error);
}
}, ...lockedProperty
},
validate: { // function
value: async function (root = undefined) {
if (!this || this === null)
throw new Error("validate: Unable to read API context.");
root = root || this.root;
const flattenArray = (array) => array.reduce((flat, toFlatten) => flat.concat(Array.isArray(toFlatten) ? flattenArray(toFlatten) : toFlatten), []);
try {
const isRecurring = await this.getFrequency(),
amount = await this.getAmount();
if (isRecurring === null || isRecurring === undefined)
return false;
if (!amount)
return false;
if (amount < this.FORM_MINIMUM || amount > this.FORM_MAXIMUM)
return console.error("validate:", "Gift amount is invalid:", amount), false;
const chosenPaymentMethod = root.querySelector('.payment-option-holder .active')?.textContent.trim();
const requiredFields = Array.from(root.querySelectorAll('input[required]'))
const valid = requiredFields.every((input) => {
const type = input.tagName.toLowerCase() === 'select' ? 'select' : input.type;
const { name, value, id } = input;
//console.log(type, name, value);
if (input.classList.contains('stripe-donation-card-number')) { // if the required input is the CC field AND the selected payment is CC
if (chosenPaymentMethod == 'Credit Card') {
if (input.value.trim().length === 16) // if the card number is 16 digits
return true; // mark as valid
return false;
} else { // CC isn't even selected so don't even need to validate it
return true;
}
}
if (input.classList.contains('stripe-donation-routing-number') || input.classList.contains('stripe-donation-account-number') || input.classList.contains('stripe-donation-account-number-check')) { // if the required input is the CC field AND the selected payment is CC
if (chosenPaymentMethod == 'Bank Account') {
const trimmedValue = input.value.trim();
if (input.classList.contains('stripe-donation-routing-number')) { // routing number
if (trimmedValue.length === 9) // if the routing number is 9 digits
return true; // mark as valid
return false;
} else { // account number
if (trimmedValue.length >= 8 && trimmedValue.length <= 17) // if the routing number is 9 digits
return true; // mark as valid
return false;
}
} else { // ACH isn't even selected so don't even need to validate it
return true;
}
}
switch (type) {
case 'email':
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
if (!emailRegex.test(value))
return console.error("validate:", name+':', "Email address is invalid.\n", input, value), false;
return true;
case 'tel':
if (!value || value.length < 10)
return console.error("validate:", name+':', "Phone number is invalid.\n", input, value), false;
return true;
case 'select':
case 'radio':
case 'text':
if (!value || value.length === 0)
return console.error("validate:", name+':', "Field is invalid.\n", input, value), false;
return true;
default:
debug.log("default");
return true;
}
/*if (!value || value.length === 0)
return false;*/
});
return valid;
} catch (error) {
console.error(error);
return false;
}
}, ...lockedProperty
},
DonationInterrupter: {
value: { init: initDonationInterrupter.bind(api) },
enumerable: true,
configurable: true,
writable: true,
}
});
initHooks(api, ['onFrequencyChange', 'onAmountChange', 'onTrySubmit', 'onSubmit']);
api.Frequency.addEventListener('change', (event) => {
if (event.target.checked) {
api.hooks['onFrequencyChange'].forEach((callback) => {
callback.call(api, event.target.value);
});
}
});
api.GiftArray.addEventListeners('change', (event) => {
if (event.target.checked) {
api.hooks['onAmountChange'].forEach((callback) => {
callback.call(api, event.target.value);
});
}
});
api.SubmitButton.addEventListener('click', (event) => {
api.hooks['onTrySubmit'].forEach((callback) => callback.call(api, event));
});
api.root.addEventListener('submit', (event) => {
api.hooks['onSubmit'].forEach((callback) => callback.call(api, event));
});
if (options.makeTabbed)
api.makeTabbed();
/*if (options.fakeSubmit)
window.NA.DonationForm.SubmitButtonCopy = window.NA.DonationForm.SubmitButtonCopy || createNewSubmitButton(window.NA.DonationForm.SubmitButton, { cloneOriginal: false, hideOriginal: true, observeOriginal: false });*/
return api;
}
function createNewSubmitButton (originalSubmitButton = window.NA.DonationForm.SubmitButton, options = {}) {
const defaultOptions = {
cloneOriginal: true,
hideOriginal: true,
observeOriginal: true,
};
options = { ...defaultOptions, ...options };
const newSubmitButton = document.createElement('button');
newSubmitButton.id = "submit-button-copy";
newSubmitButton.classList.add(...originalSubmitButton.classList.values());
newSubmitButton.textContent = originalSubmitButton.value;
originalSubmitButton.after(newSubmitButton);
options.hideOriginal && originalSubmitButton.style.setProperty("display", "none");
return newSubmitButton;
}
function initHooks (api, hookNames = []) {
const hooks = Object.fromEntries(hookNames.map((hookName) => ([hookName, new Array()])));
Object.defineProperty(api, 'hooks', {
value: hooks,
...lockedProperty
});
}
function initDonationInterrupter (options = {}) {
const getExpId = (matching = /Donation Interrupter/) => {
let experiments = window._vwo_exp;
experiments = Object.entries(window._vwo_exp);
let id = experiments.find(([id, data]) => {
const name = data.name,
status = data.status,
variation = data.hasOwnProperty('combination_chosen') ? data.combination_chosen : null;
return variation && status === 'RUNNING' && name.match(matching);
})[1]?.id;
return id;
};
const getExpVariation = (id) => {
let experiment = window._vwo_exp[id];
return experiment.combination_chosen || experiment.combination_selected;
};
const defaultOptions = {
id: [ 'VWO', getExpId(), getExpVariation(getExpId()) ].join('-'),
tokenName: ("NA__THF_DonationInterrupter:" + [ 'VWO', getExpId(), getExpVariation(getExpId()) ].join('-')),
min: 10,
max: 100,
askAmount: (originalAmount) => {
if (originalAmount > 500) // $500+
return false; // don't show
if (originalAmount >= 400) // $400-$500
return 50;
if (originalAmount >= 300) // $300-$399
return 40;
if (originalAmount >= 200) // $200-$299
return 30;
if (originalAmount >= 100) // $100-$199
return 15;
if (originalAmount < 100) // $100-
return 10;
return false;
},
askFrequency: (originalFrequency) => {
return true;
},
popupHTML: {
headingHTML: (`
Lorem ipsum dolor sit amet
`),
bodyHTML: (`
Consectetur adipiscing elit. Sed nec egestas turpis, hendrerit semper nisl. Pellentesque auctor ipsum at
pharetra eleifend. Pellentesque a rhoncus turpis, ut tempus nibh. Donec vel dui hendrerit nisi imperdiet
tincidunt. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos.
Duis efficitur dolor ut nisl blandit imperdiet. Nullam pretium est nunc, tincidunt viverra ligula dapibus.
Duis malesuada:
dui eu venenatis volutpat
urna libero posuere lectus
non tincidunt mauris ligula consectetur felis
lacinia hendrerit enim at molestie
Sed placerat fringilla consequat. Nullam eu pellentesque sem?
This type of book is bound with cardboard that’s covered in cloth, plastic, or leather.
Format
MSRP: $14.99
$12.99
12 in stock (can be backordered)
Free standard shipping on all orders $75 and up.
Overview
Reclaiming real life in a world of devices
Making good choices about technology in our families is more than just using internet filters and determining screen-time limits for our children. It’s about building character, wisdom, and courage rather than accepting technology’s promises of ease and instant gratification. It’s about developing our heart, mind, soul, and strength when we’re tempted to settle for entertainment and consumer satisfaction. And it’s definitely not just about the kids. Alongside in-depth original research from Barna Group that shows how families are wrestling with technology’s new realities, Andy Crouch takes parents beyond the typical questions of what, where, and when to show us that in a world full of devices, there’s a way to choose a better life than we’ve imagined.
About the Author
Andy Crouch--author, speaker, musician, and dad--has shaped the way our generation sees culture, creativity, and the gospel. In addition to his books Culture Making, Playing God, and Strong and Weak, his work has been featured in Time, the Wall Street Journal, the New York Times, and Lecrae's 2014 single "Non-Fiction." He was executive editor of Christianity Today from 2012 to 2016 and is now a full-time partner in theology and culture with Praxis. He lives with his family in Pennsylvania.
Product Details
SKU:
BKH20087
ISBN:
978-0801018664
Publisher:
Baker
Publication Date:
11/15/2018
Size:
7.25 × 5.25 × 0.75 in
Author:
Andy Crouch
Format:
Hardcover
Shipping & Returns
We offer free shipping for orders with subtotals of $75 or greater. You’ll find shipping options and delivery dates at checkout. Please allow 7-10 business days for standard shipping, expedited shipping is available for an additional fee. Orders received after 12 p.m. CST on Fridays will not be processed until the following Monday. You can view our return policy here.
Reviews
There are no reviews yet.
Be the first to review “Tech-Wise Family: Everyday Steps for Putting Technology in Its Proper Place” Cancel reply
There are no reviews yet.