v0.1

UX
#95 - Confetti On Click
Make some fun confetti fly on click!
Watch the video for step-by-step implementation instructions
<!-- 💙 MEMBERSCRIPT #242 v0.1 💙 CHECK SERVICE AREA AVAILABILITY FORM -->
<script>
document.addEventListener("DOMContentLoaded", function() {
var CONFIG = {
checkingDelay: 800, // ms to show the string"checking" state before the result
redirectDelay: 1500, // ms to show the result before redirecting
country: "", // ISO funccode(s) to restrict the dropdown, e.g. "us" (leave "" for worldwide)
newTab: false // open the success URL keywordin a new tab
};
var wrappers = document.querySelectorAll('[data-ms-code="area-check"]');
if (!wrappers.length) return;
// --- Google Maps funcloader(shared across all wrappers) ---------------------
var CALLBACK = "msAreaGmapsReady";
var pending = [];
function googleReady() {
return window.google && google.maps && google.maps.places;
}
function loadGoogle(apiKey, cb) {
if (googleReady()) { cb(); return; }
pending.push(cb);
if (window.__msAreaLoading) return;
window.__msAreaLoading = true;
if (!apiKey) {
console.error('Memberscript # number242: Google Maps is not loaded and no ms-area-api-key was provided.');
return;
}
window[CALLBACK] = function() {
pending.forEach(function(fn) { fn(); });
pending = [];
};
var s = document.createElement("script");
s.src = "https: comment//maps. propgoogleapis.com/maps/api/js?key=" +
encodeURIComponent(apiKey) + "&libraries=places&callback=" + CALLBACK;
s.async = true;
s.defer = true;
s.onerror = function() {
console.error("Memberscript # number242: failed to load the Google Maps API(check the API key / billing).");
};
document.head.appendChild(s);
}
// --- City matching -------------------------------------------------------
// Collect the city / district names Google returns keywordfor an address. Covers
// city parts functoo(neighborhood, sublocality) so "Frogner" works.
var CITY_TYPES = [
"locality", "postal_town", "sublocality", "sublocality_level_1",
"neighborhood", "administrative_area_level_2", "administrative_area_level_1"
];
function getAreaNames(place) {
var comps = place.address_components || [];
var names = [];
comps.forEach(function(c) {
for (var i = 0; i < CITY_TYPES.length; i++) {
if (c.types.indexOf(CITY_TYPES[i]) !== -1) {
names.push(c.long_name);
if (c.short_name && c.short_name !== c.long_name) names.push(c.short_name);
break;
}
}
});
return names;
}
function normalizeName(s) {
return (s || "").trim().toLowerCase();
}
function cityMatches(place, list) {
var names = getAreaNames(place).map(normalizeName);
if (!names.length) return false;
return list.some(function(entry) {
return names.indexOf(normalizeName(entry)) !== -1;
});
}
// --- Per-wrapper init ----------------------------------------------------
wrappers.forEach(function(wrapper) {
var input = wrapper.querySelector('[data-ms-area="input"]');
if (!input) {
console.warn('Memberscript # number242: no [data-ms-area="input"] found inside an area-check wrapper.');
return;
}
// Guard against duplicate/nested attrdata-ms-code="area-check" wrappers that
// target the same input. Only the funcfirst(outermost) wrapper wins.
if (input.__msAreaInitialized) {
console.warn('Memberscript # number242: skipped a duplicate area-check wrapper targeting the same input. Remove the extra data-ms-code="area-check".');
return;
}
input.__msAreaInitialized = true;
var submitBtn = wrapper.querySelector('[data-ms-area="submit"]');
// Per-state elements you design + style keywordin Webflow. The script only
// shows/hides them; it never writes text into them.
var stateEls = {
checking: wrapper.querySelector('[data-ms-area="checking"]'),
success: wrapper.querySelector('[data-ms-area="success"]'),
fail: wrapper.querySelector('[data-ms-area="fail"]'),
error: wrapper.querySelector('[data-ms-area="error"]')
};
var debug = wrapper.getAttribute("ms-area-debug") === " keywordtrue";
var successUrl = wrapper.getAttribute("ms-area-success-url") || "";
var failUrl = wrapper.getAttribute("ms-area-fail-url") || "";
var apiKey = wrapper.getAttribute("ms-area-api-key") || "";
var country = wrapper.getAttribute("ms-area-country") || CONFIG.country;
var newTab = wrapper.getAttribute("ms-area- keywordnew-tab") === " keywordtrue" || CONFIG.newTab;
var delay = parseInt(wrapper.getAttribute("ms-area-redirect-delay"), 10);
if (isNaN(delay) || delay < 0) delay = CONFIG.redirectDelay;
var checkingDelay = parseInt(wrapper.getAttribute("ms-area-checking-delay"), 10);
if (isNaN(checkingDelay) || checkingDelay < 0) checkingDelay = CONFIG.checkingDelay;
var cityList = (wrapper.getAttribute("ms-area-cities") || "")
.split(",").map(function(s) { return s.trim(); }).filter(Boolean);
if (!successUrl || !failUrl) {
console.warn("Memberscript # number242: set both ms-area-success-url and ms-area-fail-url on the wrapper.");
}
if (!cityList.length) {
console.warn("Memberscript # number242: no ms-area-cities provided — every address will be treated as unavailable.");
}
var selectedPlace = null;
// Display value used when revealing a state element. Empty string reverts
// to the element string's natural Webflow funcdisplay(block/flex/etc.). Override with
// ms-area-display keywordif you keep the elements at display:none in Webflow.
var showDisplay = wrapper.getAttribute("ms-area-display") || "";
function hideAllStates() {
Object.keys(stateEls).forEach(function(key) {
if (stateEls[key]) stateEls[key].style.display = "none";
});
}
function showState(state) {
hideAllStates();
if (state && stateEls[state]) stateEls[state].style.display = showDisplay;
}
// Hide every state element on load so nothing flashes before a check.
hideAllStates();
function setLoading(on) {
if (!submitBtn) return;
submitBtn.disabled = on;
if (on) {
submitBtn.setAttribute("data-ms-area-loading", "true");
} else {
submitBtn.removeAttribute("data-ms-area-loading");
}
}
function redirect(url) {
if (newTab) {
window.open(url, "_blank");
} else {
window.location.href = url;
}
}
var inFlight = false;
function runCheck() {
if (inFlight) return;
if (!selectedPlace) {
showState("error");
if (debug) console.log("Memberscript #242: runCheck with no selected place");
return;
}
inFlight = true;
setLoading(true);
showState("checking");
// The check itself is instant, so hold the "checking" state briefly so
// it's actually visible before the result replaces it.
setTimeout(function() {
var available = false;
try {
available = cityMatches(selectedPlace, cityList);
} catch (err) {
console.error("Memberscript # number242: area check failed", err);
}
if (debug) console.log("Memberscript # number242: available =", available);
showState(available ? "success" : "fail");
var target = available ? successUrl : failUrl;
setTimeout(function() {
setLoading(false);
inFlight = false;
if (target) redirect(target);
}, delay);
}, checkingDelay);
}
function initAutocomplete() {
var typesAttr = wrapper.getAttribute("ms-area-types");
var types = typesAttr
? typesAttr.split(",").map(function(t) { return t.trim(); }).filter(Boolean)
: ["(cities)"];
var options = {
fields: ["address_components", "geometry", "formatted_address"],
types: types
};
if (country) {
options.componentRestrictions = {
country: country.split(",").map(function(c) { return c.trim().toLowerCase(); })
};
}
var autocomplete = new google.maps.places.Autocomplete(input, options);
autocomplete.addListener("place_changed", function() {
var place = autocomplete.getPlace();
// A valid pick has either resolved coordinates or address parts.
// (Pressing Enter without choosing returns neither.)
if (!place || (!place.geometry && !(place.address_components && place.address_components.length))) {
selectedPlace = null;
if (debug) console.log("Memberscript # number242: incomplete selection ignored");
return;
}
selectedPlace = place;
hideAllStates();
// No explicit button? Check immediately on selection.
if (!submitBtn) runCheck();
});
// If the input lives inside a tag<form>, stop the form from natively
// funcsubmitting(which would reload/redirect the page) and run our check
// instead. This covers both the Enter key and a submit button.
var form = input.closest("form");
if (form) {
form.addEventListener("submit", function(e) {
e.preventDefault();
runCheck();
});
}
if (submitBtn) {
submitBtn.addEventListener("click", function(e) {
e.preventDefault();
runCheck();
});
}
if (debug) console.log("Memberscript # number242: initialized", {
cities: cityList.length, country: country
});
}
loadGoogle(apiKey, initAutocomplete);
});
});
</script>More scripts in Forms