MemberScripts

An attribute-based solution to add features to your Webflow site.
Simply copy some code, add some attributes, and you're done.

Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.

Need help with MemberScripts?

All Memberstack customers can ask for assistance in the 2.0 Slack. Please note that these are not official features and support cannot be guaranteed.

UX

#104 - Online Indicator

Show your site visitors your online status based on time zones.

v0.1

<!-- 💙 MEMBERSCRIPT #104 v0.1 💙 ONLINE INDICATOR -->
<script>
document.addEventListener('DOMContentLoaded', function() {
    const businessHours = {
        start: 9, // Business hours start at 9 AM
        end: 17, // Business hours end at 5 PM
        days: [1, 2, 3, 4, 5] // Monday to Friday
    };
    const colors = {
        businessHours: '#34b426',
        outsideBusinessHours: '#F25022'
    };

    const wrappers = document.querySelectorAll('[ms-code-online-wrapper]');
    wrappers.forEach(wrapper => {
        const timeZone = wrapper.getAttribute('ms-code-online-wrapper');
        const dot = wrapper.querySelector('[ms-code-online="dot"]');
        const timeSpan = wrapper.querySelector('[ms-code-online="time"]');

        const now = new Date();
        const formatter = new Intl.DateTimeFormat('en-US', {
            hour: 'numeric',
            minute: '2-digit',
            timeZone: timeZone
        });
        const formattedTime = formatter.format(now);

        if (timeSpan) timeSpan.textContent = formattedTime;

        const currentDay = now.getDay();
        const currentHour = new Date().toLocaleTimeString('en-US', {
            hour: '2-digit',
            hour12: false,
            timeZone: timeZone
        });

        const isBusinessDay = businessHours.days.includes(currentDay);
        const isBusinessHour = currentHour >= businessHours.start && currentHour < businessHours.end;

        if (dot) {
            dot.style.backgroundColor = (isBusinessDay && isBusinessHour) ? colors.businessHours : colors.outsideBusinessHours;
        }
    });
});
</script>
Integration

#103 - Custom Booking Form

Add a custom booking form to your website which creates a Google calendar event.

v0.1

<!-- 💙 MEMBERSCRIPT #103 v0.1 💙 CUSTOM BOOKING FORM -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.1/moment.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment-timezone/0.5.33/moment-timezone-with-data.min.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
  function getNextBusinessDays() {
    let businessDays = [];
    let currentDate = moment();
    currentDate.add(1, 'days');
    
    while (businessDays.length < 14) {
      if (currentDate.day() !== 0 && currentDate.day() !== 6) {
        let formattedDay = currentDate.format('dddd, MMMM D');
        let rawDay = currentDate.format('YYYY-MM-DD');
        businessDays.push({formattedDay, rawDay});
      }
      currentDate.add(1, 'days');
    }
    return businessDays;
  }

  function generateTimeSlots() {
    let slots = [];
    let startHour = 9;
    let endHour = 16.5;
    let currentTime = moment().startOf('day').add(startHour, 'hours');
    
    while (currentTime.hour() + (currentTime.minute() / 60) <= endHour) {
      let formattedTime = currentTime.format('h:mm A');
      let timeValue = currentTime.format('HH:mm');
      slots.push({formattedTime, timeValue});
      currentTime.add(30, 'minutes');
    }
    return slots;
  }

  function updateTimestamp(day, time, timezone) {
    let timestampInput = document.getElementById('timestamp');
    if (!timestampInput) {
        timestampInput = document.createElement('input');
        timestampInput.type = 'hidden';
        timestampInput.id = 'timestamp';
        timestampInput.name = 'timestamp';
        document.querySelector('form').appendChild(timestampInput);
    }

    let datetime = moment.tz(`${day} ${time}`, "YYYY-MM-DD HH:mm", timezone);
    timestampInput.value = datetime.valueOf();
  }

  function populateFields() {
    const days = getNextBusinessDays();
    const times = generateTimeSlots();
    const daySelect = document.querySelector('[ms-code-booking="day"]');
    const timeSelect = document.querySelector('[ms-code-booking="time"]');
    const form = daySelect.closest('form');
    const timezone = form.getAttribute('ms-code-booking-timezone') || moment.tz.guess();
    
    days.forEach(({formattedDay, rawDay}) => {
      let option = new Option(formattedDay, rawDay);
      daySelect.appendChild(option);
    });
    
    times.forEach(({formattedTime, timeValue}) => {
      let option = new Option(formattedTime, timeValue);
      timeSelect.appendChild(option);
    });

    function handleSelectChange() {
      if (daySelect.value && timeSelect.value) {
        updateTimestamp(daySelect.value, timeSelect.value, timezone);
      }
    }

    daySelect.addEventListener('change', handleSelectChange);
    timeSelect.addEventListener('change', handleSelectChange);
  }

  populateFields();
});
</script>
Custom Fields
UX

#102 - Automatically Resize Textarea Height

Increase or decrease a textarea's height based on its content.

v0.1

<!-- 💙 MEMBERSCRIPT #102 v0.1 💙 RESIZE TEXTAREA VERTICALLY -->
<script>
document.addEventListener('DOMContentLoaded', function() {
    const elements = document.querySelectorAll('[data-ms-post="content"], [ms-code-resize-input="height"]');

    elements.forEach(element => {
        if (element.tagName.toLowerCase() === 'textarea') {
            element.addEventListener('input', function() {
                autoResize(this);
            }, false);
        }
    });

    function autoResize(element) {
        const maxHeight = parseInt(getComputedStyle(element).maxHeight, 10);
        element.style.height = 'auto';
        element.style.overflow = 'hidden'; // Prevents scrollbar appearance during height adjustment

        if (element.scrollHeight > maxHeight) {
            element.style.height = `${maxHeight}px`;
            element.style.overflow = 'auto'; // Adds scrollbar when content exceeds max height
        } else {
            element.style.height = `${element.scrollHeight}px`;
        }
    }
});
</script>
Custom Fields
UX

#101 - Automatically Resize Input Width

Increase or decrease an input's width based on content.

v0.1

<!-- 💙 MEMBERSCRIPT #101 v0.1 💙 RESIZE INPUT HORIZONTALLY -->
<script>
  document.addEventListener('DOMContentLoaded', function() {
    const elements = document.querySelectorAll('[ms-code-resize-input="width"]');

    // Store the initial widths
    const initialWidths = new Map();
    elements.forEach(element => {
        initialWidths.set(element, element.offsetWidth);
    });

    elements.forEach(element => {
        element.addEventListener('input', function() {
            autoResizeWidth(this);
        });
    });

    function autoResizeWidth(element) {
        // Find the nearest hidden measure element
        const measurer = element.nextElementSibling.getAttribute('ms-code-resize-input') === 'hidden-measure' 
                         ? element.nextElementSibling 
                         : null;
        if (!measurer) return; // Exit if no measurer is found

        measurer.textContent = element.value;

        const maxWidth = parseInt(getComputedStyle(element).maxWidth, 10);
        const minWidth = initialWidths.get(element);
        const contentWidth = measurer.offsetWidth;

        if (contentWidth > minWidth && contentWidth < maxWidth) {
            element.style.width = `${contentWidth}px`;
        } else if (contentWidth >= maxWidth) {
            element.style.width = `${maxWidth}px`;
        } else {
            element.style.width = `${minWidth}px`;
        }
    }
  });
</script>
Custom Fields
Integration

#100 - Auto-Compress Image Uploads

Compress image uploads, including profile images.

v0.1

<!-- 💙 MEMBERSCRIPT #100 v0.1 💙 AUTO-COMPRESSED IMAGE UPLOADS -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/compressorjs/1.2.1/compressor.min.js" integrity="sha512-MgYeYFj8R3S6rvZHiJ1xA9cM/VDGcT4eRRFQwGA7qDP7NHbnWKNmAm28z0LVjOuUqjD0T9JxpDMdVqsZOSHaSA==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script>
document.addEventListener('DOMContentLoaded', function () {
  const compressibleInputs = document.querySelectorAll('input[type="file"][ms-code-file_compress]');

  compressibleInputs.forEach(fileInput => {
    let isCompressing = false;

    fileInput.addEventListener('change', function (event) {
      if (isCompressing) {
        isCompressing = false;
        return;
      }

      if (fileInput.files.length === 0) {
        return;
      }

      const originalFile = fileInput.files[0];
      const compressionLevel = parseFloat(fileInput.getAttribute('ms-code-file_compress'));

      new Compressor(originalFile, {
        quality: compressionLevel,
        maxWidth: 2000,
        maxHeight: 2000,
        success(compressedResult) {
          const compressedFile = new File([compressedResult], originalFile.name, {
            type: compressedResult.type,
            lastModified: Date.now(),
          });

          const dataTransfer = new DataTransfer();
          dataTransfer.items.add(compressedFile);
          fileInput.files = dataTransfer.files;

          isCompressing = true;
          fileInput.dispatchEvent(new Event('change', { bubbles: true }));
        },
        error(err) {
          console.error('Compression Error: ', err.message);
        },
      });

      event.stopPropagation();
    }, true);
  });
});
</script>
Custom Fields

#99 - Custom File Inputs

Turn anything into a file input!

v0.1

<!-- 💙 MEMBERSCRIPT #99 v0.1 💙 CUSTOM FILE UPLOAD INPUT -->
<script>
document.addEventListener('DOMContentLoaded', function () {
  const uploadButtons = document.querySelectorAll('[ms-code-file_uploader]');

  uploadButtons.forEach(button => {
    const fileInput = document.createElement('input');
    fileInput.type = 'file';
    fileInput.style.display = 'none';
    fileInput.name = button.getAttribute('ms-code-file_uploader');
    fileInput.accept = button.getAttribute('ms-code-file_types');

    document.body.appendChild(fileInput);

    button.addEventListener('click', function (e) {
      e.preventDefault();
      fileInput.click();
    });

    fileInput.addEventListener('change', function () {
      const fileName = fileInput.files[0].name;
      button.querySelector('div').textContent = fileName;
    });
  });
});
</script>
Conditional Visibility

#98 - Age Gating

Make users confirm their age before proceeding.

v0.1

<!-- 💙 MEMBERSCRIPT #98 v0.1 💙 AGE GATE -->
<script>
document.addEventListener('DOMContentLoaded', (event) => {
  const form = document.querySelector('form[ms-code-age-gate]');
  const dayInput = document.querySelector('input[ms-code-age-gate="day"]');
  const monthInput = document.querySelector('input[ms-code-age-gate="month"]');
  const yearInput = document.querySelector('input[ms-code-age-gate="year"]');
  const backButton = document.querySelector('a[ms-code-age-gate="back"]');
  const wrapper = document.querySelector('[ms-code-age-gate="wrapper"]');
  const errorDiv = document.querySelector('div[ms-code-age-gate="error"]');

  if (localStorage.getItem('ageVerified') === 'true') {
    wrapper.remove();
    return;
  }

  backButton.addEventListener('click', (e) => {
    e.preventDefault();
    window.history.back();
  });

  const inputs = [dayInput, monthInput, yearInput];

  inputs.forEach((input, index) => {
    input.addEventListener('keyup', (e) => {
      const maxChars = input === yearInput ? 4 : 2;
      let value = e.target.value;

      if (input === dayInput && value.length === maxChars) {
        value = value > 31 ? '31' : value.padStart(2, '0');
      } else if (input === monthInput && value.length === maxChars) {
        value = value > 12 ? '12' : value.padStart(2, '0');
      }
      e.target.value = value;

      if (value.length === maxChars) {
        const nextInput = inputs[index + 1];
        if (nextInput) {
          nextInput.focus();
        }
      }
    });
  });

  form.addEventListener('submit', (e) => {
    e.preventDefault();
    const enteredDate = new Date(yearInput.value, monthInput.value - 1, dayInput.value);
    const currentDate = new Date();
    const ageDifference = new Date(currentDate - enteredDate);
    const age = Math.abs(ageDifference.getUTCFullYear() - 1970);

    const ageLimit = parseInt(form.getAttribute('ms-code-age-gate').split('-')[1], 10);

    if (age >= ageLimit) {
      console.log('Age verified.');
      errorDiv.style.display = 'none';
      localStorage.setItem('ageVerified', 'true');
      wrapper.remove();
    } else {
      console.log('Age verification failed, user is under the age limit.');
      errorDiv.style.display = 'block';
    }
  });
});
</script>
Integration

#97 - Upload Files To S3 Bucket

Allow uploads to an S3 bucket from a Webflow form.

v0.1

<!-- 💙 MEMBERSCRIPT #97 v0.1 💙 S3 FILE UPLOADS -->
<script>
    document.addEventListener('DOMContentLoaded', function() {
        function generateUUID() {
            return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
                var r = (Math.random() * 16) | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
                return v.toString(16);
            });
        }

        document.querySelectorAll('input[ms-code-s3-uploader]').forEach(input => {
            input.addEventListener('change', function() {
                if (this.files.length > 0) {
                    const file = this.files[0];
                    const uuid = generateUUID();
                    const extension = file.name.split('.').pop();
                    const newFileName = `${uuid}.${extension}`;
                    const wrapper = this.closest('div[ms-code-s3-wrapper]');
                    const s3FileInput = wrapper.querySelector('input[ms-code-s3-file]');

                    s3FileInput.value = s3FileInput.getAttribute('ms-code-s3-file') + encodeURIComponent(newFileName);

                    const apiGatewayUrl = wrapper.getAttribute('ms-code-s3-wrapper').replace('${encodeURIComponent(fileName)}', encodeURIComponent(newFileName));

                    fetch(apiGatewayUrl, {
                        method: 'PUT',
                        body: file,
                        headers: { 'Content-Type': file.type }
                    })
                    .then(response => {
                        if (response.status !== 200) {
                            throw new Error(`Upload failed with status: ${response.status}`);
                        }
                        console.log('File uploaded successfully:', newFileName);
                    })
                    .catch(error => {
                        console.error('Upload error:', error);
                        alert('Upload failed.');
                    });
                }
            });
        });

        document.querySelectorAll('form').forEach(form => {
            form.addEventListener('submit', function(event) {
                const s3Inputs = Array.from(form.querySelectorAll('input[ms-code-s3-file]'));
                const allUrlsSet = s3Inputs.every(input => input.value);

                if (!allUrlsSet) {
                    event.preventDefault();
                    alert('Please wait for all files to finish uploading before submitting.');
                }
            });
        });
    });
</script>
Custom Fields

#96 - Save a Tally

Create and update a count/tally which saves to a custom field!

v0.1

<!-- 💙 MEMBERSCRIPT #96 v0.1 💙 KEEPING A TALLY -->
<script>
document.addEventListener("DOMContentLoaded", function() {
    const memberstack = window.$memberstackDom;
    const addButtons = document.querySelectorAll("[ms-code-add-tally]");

    addButtons.forEach(button => {
        button.addEventListener("click", async () => {
            const tallyKey = button.getAttribute("ms-code-add-tally");
            const tallyText = document.querySelector(`[ms-code-tally="${tallyKey}"]`);
           
            if(tallyText){
                let currentCount = parseInt(tallyText.textContent, 10);
                currentCount += 1;
                tallyText.textContent = currentCount;

                // Store the new tally count to Memberstack
                let newFields = {};
                newFields[tallyKey] = currentCount;
                await memberstack.updateMember({customFields: newFields});
           }
        });
    });
});
</script>
UX

#95 - Confetti On Click

Make some fun confetti fly on click!

v0.1

<!-- 💙 MEMBERSCRIPT #95 v0.1 💙 CONFETTI -->
<script src="https://cdn.jsdelivr.net/npm/tsparticles-confetti@2.12.0/tsparticles.confetti.bundle.min.js"></script>
<script>
document.addEventListener("DOMContentLoaded", function() {
    const confettiElems = document.querySelectorAll("[ms-code-confetti]");

    confettiElems.forEach(item => {
        item.addEventListener("click", () => {
            const effect = item.getAttribute("ms-code-confetti");
            switch (effect) {
                case "falling":
                    const makeFall = () => {
                        confetti({
                            particleCount: 100,
                            startVelocity: 30,
                            spread: 360,
                            origin: { x: Math.random(), y: 0 },
                            colors: ['#ffffff','#ff0000','#00ff00','#0000ff']
                        });
                    }
                    setInterval(makeFall, 2000);
                    break;
                case "single":
                    confetti({
                        particleCount: 1,
                        startVelocity: 30,
                        spread: 360,
                        origin: { x: Math.random(), y: Math.random() }
                    });
                    break;
                case "sides":
                    confetti({
                        particleCount: 100,
                        startVelocity: 30,
                        spread: 360,
                        origin: { x: Math.random(), y: 0.5 }
                    });
                    break;
                case "explosions":
                    confetti({
                        particleCount: 100,
                        startVelocity: 50,
                        spread: 360
                    });
                    break;
                case "bottom":
                    confetti({
                        particleCount: 100,
                        startVelocity: 30,
                        spread: 360,
                        origin: { x: 0.5, y: 1 }
                    });
                    break;
                default:
                    console.log("Unknown confetti effect");
            }
        });
    });
});
</script>
UX

#94 - Set href Attribute

Dynamically set a link using the Webflow CMS (or anything else)

v0.1

<!-- 💙 MEMBERSCRIPT #94 v0.1 💙 SET HREF ATTRIBUTE -->
<script>
window.onload = function(){
  var elements = document.querySelectorAll('[ms-code-href]');
  elements.forEach(function(element) {
    var url = element.getAttribute('ms-code-href');
    element.setAttribute('href', url);
  });
};
</script>
Custom Fields
UX

#93 - Force Valid URLs in Form Input

Automatically convert anything in your input to a valid URL.

v0.1

<!-- 💙 MEMBERSCRIPT #93 v0.1 💙 FORCE INPUT TO BE A VALID URL -->
<script>
// Get all form fields with attribute ms-code-convert="link"
const formFields = document.querySelectorAll('input[ms-code-convert="link"], textarea[ms-code-convert="link"]');

// Add event listener to each form field
formFields.forEach((field) => {
  field.addEventListener('input', convertToLink);
});

// Function to convert input to a link
function convertToLink(event) {
  const input = event.target;

  // Get user input
  const userInput = input.value.trim();

  // Check if input starts with http:// or https://
  if (userInput.startsWith('http://') || userInput.startsWith('https://')) {
    input.value = userInput; // No conversion needed for valid links
  } else {
    input.value = `http://${userInput}`; // Prepend http:// for simplicity
  }
}
</script>
UX

#92 - Change Page on Click

Change the current page URL when clicking any element.

v0.1

<!-- 💙 MEMBERSCRIPT #92 v0.1 💙 TURN ANYTHING INTO A LINK -->
<script>
document.addEventListener('click', function(event) {
    let target = event.target;

    // Traverse up the DOM tree to find an element with the ms-code-navigate attribute
    while (target && !target.getAttribute('ms-code-navigate')) {
        target = target.parentElement;
    }

    // If we found an element with the ms-code-navigate attribute
    if (target) {
        const navigateUrl = target.getAttribute('ms-code-navigate');

        if (navigateUrl) {
            event.preventDefault();
            // Always open in a new tab
            window.open(navigateUrl, '_blank');
        }
    }
});
</script>
Modals
UX

#91 - Hide Popup For Set Duration

Hide a popup for X time when a button is clicked.

v0.1

<!-- 💙 MEMBERSCRIPT #91 v0.1 💙 HIDE POPUP FOR SET DURATION -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<script>
  $(document).ready(function() {
    // Look for elements with 'ms-code-hide-popup' attribute
    var items = $('[ms-code-hide-popup]');

    var button;
    var timeElement;

    // Determine which element is the button and which is the timer
    items.each(function(index, item) {
      var value = $(item).attr('ms-code-hide-popup');
      if (value === "button") {
        button = $(item);
      } else {
        timeElement = $(item);
      }
    });

    // Calculate the target date
    var calculateTargetDate = function(timeStr) {
      var splitTime = timeStr.split(':');
      var now = new Date();
      now.setDate(now.getDate() + parseInt(splitTime[0])); // add days
      now.setHours(now.getHours() + parseInt(splitTime[1])); // add hours
      now.setMinutes(now.getMinutes() + parseInt(splitTime[2])); // add minutes
      now.setSeconds(now.getSeconds() + parseInt(splitTime[3])); // add seconds
      return now;
    };

    // Check if element should be removed from DOM
    var checkTimeAndRemoveElement = function() {
      var targetDate = localStorage.getItem('targetDate');
      if (targetDate && new Date() < new Date(targetDate)) {
        timeElement.remove();
      } else {
        localStorage.removeItem('targetDate');
      }
    };

    // Action on button click
    button.on('click', function() {
      var time = timeElement.attr('ms-code-hide-popup');
      localStorage.setItem('targetDate', calculateTargetDate(time));
      checkTimeAndRemoveElement();
    });

    // Initial check
    checkTimeAndRemove AndRemoveElement();
  });
</script>
Custom Fields
UX

#90 - Show Elements On Input Change

Display 1 or more elements when a user changes the input value.

v0.1

<!-- 💙 MEMBERSCRIPT #90 v0.1 💙 SHOW ELEMENTS ON INPUT CHANGE -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<script>
$(document).ready(function() {
    // Initially hide all elements
    $('[ms-code-show-item]').css('display', 'none');

    setTimeout(function() {
        $('[ms-code-show-field]').change(function() {
            var field = $(this).attr('ms-code-show-field');
            $('[ms-code-show-item=' + field + ']').css('display', 'block');
        });
    }, 500); // Wait 500ms before starting, you can change this time based on your needs
});
</script>
UX

#89 - Custom Context Menus

Show a custom, built-in-Webflow context menu when your element is right-clicked.

v0.1

<!-- 💙 MEMBERSCRIPT #89 v0.1 💙 CUSTOM CONTEXT MENUS -->
<script>
// Cache elements
const items = document.querySelectorAll("[ms-code-context-item]");
const menus = document.querySelectorAll("[ms-code-context-menu]");

// Disable default context menu on item right click and show custom context menu
items.forEach(element => {
    element.addEventListener('contextmenu', event => {
        event.preventDefault(); // Prevents showing the default context menu
        hideAllMenus(); // Make sure other menus are hidden

        // fetch the related menu, make it visible
        const menuItemId = element.getAttribute("ms-code-context-item");
        const menu = document.querySelector(`[ms-code-context-menu="${menuItemId}"]`);

        if (menu) {
            menu.classList.remove('hidden');
            menu.classList.add('visible');
        }
    });
});

// Add click event on custom menus to stop event propagation
menus.forEach(menu => {
    menu.addEventListener('click', event => {
        event.stopPropagation();
    });
});

// Close custom context menu on outside click
document.body.addEventListener('click', hideAllMenus);

// Helper function to hide all custom context menus
function hideAllMenus() {
    menus.forEach(menu => {
        menu.classList.remove('visible');
        menu.classList.add('hidden');
    });
}
</script>
UX

#88 - Show Current State For CMS, Folder Links

Display the Webflow "current" state on your nested pages & CMS items.

v0.1

<!-- 💙 MEMBERSCRIPT #88 v0.1 💙 SHOW CURRENT STATE FOR NESTED URLS -->
<script>
window.onload = function() {
  var currentUrl = window.location.href;
  var elements = document.querySelectorAll('[ms-code-nested-link]'); // get all elements with ms-code-nested-link attribute

  elements.forEach(function (element) {
    var linkAttrValue = element.getAttribute('ms-code-nested-link'); // get the ms-code-nested-link value
    if (currentUrl.includes(linkAttrValue)) { // check if current url matches the attribute value
      element.classList.add('w--current'); // apply the class 
    }
  });
};
</script>
Custom Flows

#87 - Remove a Plan After Countdown

Create time-sensitive secure content!

v0.1

<!-- 💙 MEMBERSCRIPT #87 v0.1 💙 REMOVE PLAN AFTER COUNTDOWN -->
<script>
const memberstack = window.$memberstackDom;
const countdown = new Date(localStorage.getItem('countdownDateTime'));

// Check if date has passed
const checkDate = async () => {
  const now = new Date();
  if (now > countdown) {
    // Remove member's free plan
    await memberstack.removePlan({
      planId: "pln_10-minutes-of-gif-access-rw1fh0ktg"
    });
    console.log("Plan removed");

    // Reload the page
    location.reload();
  }
}

// Execute checkDate every 10s
const intervalId = setInterval(checkDate, 10000);
</script>
UX

#86 - Free & Simple Text-To-Speech

Add a button which allows visitors to listen to your article.

v0.1

<!-- 💙 MEMBERSCRIPT #86 v0.1 💙 VOICE TO TEXT BUTTON -->
<script>
document.addEventListener('DOMContentLoaded', (event) => {
    const textDiv = document.querySelector('[ms-code-text-to-speech="text"]');
    const speakButton = document.querySelector('[ms-code-text-to-speech="button"]');
    let utterance = new SpeechSynthesisUtterance();

    speakButton.addEventListener('click', () => {
        if(speechSynthesis.speaking || speechSynthesis.paused) {
            speechSynthesis.cancel(); // stops current speech
        } else {
            utterance.text = textDiv.innerText;

            speechSynthesis.speak(utterance); // starts speaking
        }
    });
});
</script>
Custom Fields

#85 - "Add a row" Form Inputs

Allow members to add and delete rows from a form input.

v0.1

<!-- 💙 MEMBERSCRIPT #85 v0.1 💙 ADD A ROW FORM INPUTS -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script>
  $(document).ready(function() {
    // Hide all rows except the original row
    $('[ms-code-row-input="new"]').hide();

    // Add row button click event
    $('[ms-code-row-input="add-row"]').click(function(e) {
      e.preventDefault();
      var clonedRow = $('[ms-code-row-input="new"]').first().clone();
      clonedRow.find('input').val('');
      clonedRow.show().appendTo('[ms-code-row-input="row-container"]');

      updateHolderValue();
    });

    // Delete row button click event
    $(document).on('click', '[ms-code-row-input="delete"]', function(e) {
      e.preventDefault();
      $(this).closest('[ms-code-row-input="new"]').remove();

      updateHolderValue();
    });

    // Event for all inputs
    $(document).on('input', '[ms-code-row-input="original"], [ms-code-row-input="new-input"], [ms-code-row-input="holder"]', function() {
      if ($(this).is('[ms-code-row-input="holder"]')) {
        updateRowsFromHolder();
      } else {
        updateHolderValue();
      }
    });

    // Function to update the holder input value
    function updateHolderValue() {
      var values = [];
      $('[ms-code-row-input="original"], [ms-code-row-input="new-input"]').each(function() {
        var value = $(this).val().trim();
        if (value) {
          values.push(value);
        }
      });
      $('[ms-code-row-input="holder"]').val(values.join(','));
    }

    // Function to update rows from the holder field
    function updateRowsFromHolder() {
      var holderValue = $('[ms-code-row-input="holder"]').val();
      var values = holderValue.split(',');

      $('[ms-code-row-input="new"]').not(':first').remove();

      // For each holder value, create a new row
      values.forEach(function(val, idx) {
        if (idx === 0) {
          $('[ms-code-row-input="original"]').val(val);
        } else {
          var newRow = $('[ms-code-row-input="new"]').first().clone().appendTo('[ms-code-row-input="row-container"]');
          newRow.find('input').val(val);
          newRow.show();
        }
      });
    }

    // Initial update of the holder input value
    updateHolderValue();

    // Adding MutationObserver to call updateRowsFromHolder on changes to the holder field
    var targetNode = $('[ms-code-row-input="holder"]')[0];
    var config = { attributes: true, childList: true, subtree: true };
    var callback = function(mutationsList, observer) {
      for(let mutation of mutationsList) {
        if (mutation.type === 'childList')
        {
          updateRowsFromHolder();
        }
      }
    };
    var observer = new MutationObserver(callback);
    observer.observe(targetNode, config);
  });
</script>
UX
Custom Fields

#84 - Clear Inputs OnLoad

Add this script to any page to clear the value of a custom field on page load.

v0.1

<!-- 💙 MEMBERSCRIPT #84 v0.1 💙 CLEAR INPUT VALUES ONLOAD -->
<script>
  document.addEventListener('DOMContentLoaded', async function() {
    const memberstack = window.$memberstackDom;
    const fieldsToClear = ["phone", "last-name"]; // Specify the fields to clear

    // Clear inputs and Memberstack fields on page load
    memberstack.getCurrentMember().then(async ({ data: member }) => {
      if (member) {
        const customFieldsToUpdate = {};
      
        fieldsToClear.forEach(fieldName => {
          customFieldsToUpdate[fieldName] = '';
        });

        try {
          await memberstack.updateMember({
            customFields: customFieldsToUpdate
          });
          console.log("Fields cleared on page load.");
        } catch (error) {
          console.error('Error clearing fields on page load:', error);
        }
      }
      
      // Clear input values on page load for specified fields
      fieldsToClear.forEach(fieldName => {
        const inputField = document.querySelector(`[data-ms-member="${fieldName}"]`);
        if (inputField) {
          inputField.value = '';
        }
      });
    });
  });
</script>
UX

#83 - Cross-Device Cookie Preferences

Allow members to save their cookie preferences to their account.

v0.1

<!-- 💙 MEMBERSCRIPT #83 v0.1 💙 CROSS-DEVICE COOKIE PREFERENCES -->
<script>
// Function to retrieve a cookie value by name
function getCookie(name) {
  const value = `; ${document.cookie}`;
  const parts = value.split(`; ${name}=`);
  if (parts.length === 2) return decodeURIComponent(parts.pop().split(';').shift());
}

async function updateMemberConsentPreferences(fsCcCookieValue) {
  try {
    const memberstack = window.$memberstackDom;
    const userData = await memberstack.getCurrentMember();

    if (userData && userData.data.customFields) {
      if (!userData.data.customFields['cookie-consent']) {
        const decodedFsCcCookieValue = decodeURIComponent(fsCcCookieValue);
        await memberstack.updateMember({
          customFields: {
            'cookie-consent': decodedFsCcCookieValue
          }
        });
      } else {
        document.cookie = `fs-cc=${encodeURIComponent(userData.data.customFields['cookie-consent'])}`;
      }
    }
  } catch (error) {}
}

async function initialize() {
  const fsCcCookieValue = getCookie('fs-cc');
  if (fsCcCookieValue) {
    await updateMemberConsentPreferences(fsCcCookieValue);

    const checkboxes = document.querySelectorAll('[fs-cc-checkbox]');
    checkboxes.forEach(checkbox => {
      checkbox.addEventListener('change', async () => {
        const memberstack = window.$memberstackDom;
        const userData = await memberstack.getCurrentMember();
        
        if (userData && userData.data.customFields) {
          const customFieldKey = 'cookie-consent';
          const checkboxName = checkbox.getAttribute('fs-cc-checkbox');

          if (userData.data.customFields[customFieldKey]) {
            const consentData = JSON.parse(userData.data.customFields[customFieldKey]);
            consentData.consents[checkboxName] = checkbox.checked;
            const updatedCustomField = JSON.stringify(consentData);

            await memberstack.updateMember({
              customFields: {
                [customFieldKey]: updatedCustomField
              }
            });

            document.cookie = `fs-cc=${encodeURIComponent(updatedCustomField)}`;
          }
        }
      });
    });
  }
}

// Initialize the script
initialize();
</script>
Custom Flows

#82 - License Keys

Secure your downloadable content with license keys.

v0.1

<!-- 💙 MEMBERSCRIPT #82 v0.1 💙 LICENSE KEYS -->
<script>
const memberstack = window.$memberstackDom;

// Initialize MutationObserver
const observer = new MutationObserver(async (mutations) => {
  const downloadBtn = document.getElementById("download");
  
  if (downloadBtn) {
    // Element exists, so add event listener
    downloadBtn.addEventListener("click", async () => {
      await memberstack.removePlan({
        planId: "pln_activate-license-key-952c0d8u"
      });
      console.log("Plan removed");
    });

    // Stop observing since we found the element
    observer.disconnect();
  }
});

// Observe the whole document
observer.observe(document.body, { childList: true, subtree: true });
</script>
Custom Fields

#81 - Custom Checkbox Values

Pass through a unique value based on whether or not the box is checked.

v0.1

<!-- 💙 MEMBERSCRIPT #81 v0.1 💙 CUSTOM CHECKBOX VALUES -->
<script>
document.addEventListener('submit', function(e) {
  var checkboxes = document.querySelectorAll('[ms-code-custom-checkbox]');
  
  checkboxes.forEach(function(checkbox) {
    var values = checkbox.getAttribute('ms-code-custom-checkbox').split(',');
    var valueToSubmit = checkbox.checked ? values[0] : values[1];

    var hiddenInput = document.createElement('input');
    
    // Copy all attributes except type and ms-code-custom-checkbox
    for (var i = 0; i < checkbox.attributes.length; i++) {
      var attr = checkbox.attributes[i];
      if (attr.name !== 'type' && attr.name !== 'ms-code-custom-checkbox') {
        hiddenInput.setAttribute(attr.name, attr.value);
      }
    }
    
    hiddenInput.type = 'hidden';
    hiddenInput.value = valueToSubmit;
    
    checkbox.form.appendChild(hiddenInput);
    checkbox.remove(); // Remove the original checkbox so it doesn't interfere with submission
  });
});
</script>
UX

#79 - Trigger Click onHover

Trigger a click event onHover.

v0.1

<!-- 💙 MEMBERSCRIPT #79 v0.1 💙 HOVER BASED TABS -->
<script>
  document.addEventListener('DOMContentLoaded', function() {
    const hoverTabElements = document.querySelectorAll('[ms-code-onhover="click"]');

    hoverTabElements.forEach(hoverTabElement => {
      hoverTabElement.addEventListener('mouseenter', function() {
        hoverTabElement.click(); // Click on the element when hovering
      });
    });
  });
</script>
UX

#78 - Clear Inputs OnClick

Create a button which can clear the values of one or more inputs.

v0.1

<!-- 💙 MEMBERSCRIPT #78 v0.1 💙 CLEAR INPUT VALUES ONCLICK -->
<script>
document.addEventListener('DOMContentLoaded', () => {
  const clearBtns = document.querySelectorAll('[ms-code-clear-value]');
  clearBtns.forEach(btn => {
    btn.addEventListener('click', () => {
      const fieldIds = btn.getAttribute('ms-code-clear-value').split(',');
      fieldIds.forEach(fieldId => {   
        const input = document.querySelector(`[data-ms-member="${fieldId}"]`);
        if (input) {
          input.value = '';
        }
      });
    });
  });
});
</script>
UX

#77 - Universal Emojis

Make your on-site emojis the same on all devices/OS.

v0.1

<!-- 💙 MEMBERSCRIPT #77 v0.1 💙 UNIVERSAL EMOJIS -->
<script>
document.querySelectorAll('[ms-code-emoji]').forEach(element => {
  var imageUrl = element.getAttribute('ms-code-emoji');
  var img = document.createElement('img');
  img.src = imageUrl;

  var textStyle = window.getComputedStyle(element);
  var adjustedHeight = parseFloat(textStyle.fontSize) * 1.0;

  img.style.height = adjustedHeight + 'px';
  img.style.width = 'auto';
  img.style.verticalAlign = 'text-top';

  element.innerHTML = ''; // Clears the text content inside the span
  element.appendChild(img);
});
</script>
Conditional Visibility
UX

#76 - Time-Based Visibility

Show different elements based on the current time of day.

v0.1

<!-- 💙 MEMBERSCRIPT #76 v0.1 💙 TIME-BASED VISIBILITY -->
<script>
function hideElements() {
  const elements = document.querySelectorAll('[ms-code-time]');
  elements.forEach(element => {
    element.style.display = 'none';
  });
}

function displayBasedOnTime() {
  const elements = document.querySelectorAll('[ms-code-time]');
  const currentTime = new Date();

  elements.forEach(element => {
    const timeRange = element.getAttribute('ms-code-time');
    const [start, end] = timeRange.split(' - ');
    const [startHour, startMinute] = start.split(':').map(Number);
    const [endHour, endMinute] = end.split(':').map(Number);

    let startTime = new Date(currentTime);
    startTime.setHours(startHour, startMinute, 0, 0);

    let endTime = new Date(currentTime);
    endTime.setHours(endHour, endMinute, 0, 0);

    // If the end time is earlier than the start time, add a day to the end time
    if (endTime < startTime) {
      endTime.setDate(endTime.getDate() + 1);
    }

    if (currentTime >= startTime && currentTime <= endTime) {
      element.style.display = 'flex';
    }
  });
}

// Call the functions
hideElements();
displayBasedOnTime();
</script>
UX
Custom Fields

#75 - Disallowed Character Inputs

Show a custom error message if a user enters something you set in an input.

v0.1

<!-- 💙 MEMBERSCRIPT #75 v0.1 💙 DISALOWED CHARACTER INPUTS -->
<script>
document.addEventListener('DOMContentLoaded', function() {
  const inputFields = document.querySelectorAll('[ms-code-disallow]');
  inputFields.forEach(inputField => {
    const errorBlock = inputField.nextElementSibling;
    errorBlock.innerHTML = ''; // Use innerHTML to interpret <br> tags

    inputField.addEventListener('input', function() {
      const rules = inputField.getAttribute('ms-code-disallow').split(')');
      let errorMessage = '';

      rules.forEach(rule => {
        const parts = rule.trim().split('=');
        const ruleType = parts[0].substring(1); // Remove the opening parenthesis
        const disallowedValue = parts[1];

        if (ruleType.startsWith('custom')) {
          const disallowedChar = ruleType.split('-')[1]; // Extract the character after the '-'
          if (inputField.value.includes(disallowedChar)) {
            errorMessage += disallowedValue + '<br>'; // Add line break
          }
        } else if (ruleType === 'space' && inputField.value.includes(' ')) {
          errorMessage += disallowedValue + '<br>'; // Add line break
        } else if (ruleType === 'number' && /\d/.test(inputField.value)) {
          errorMessage += disallowedValue + '<br>'; // Add line break
        } else if (ruleType === 'special' && /[^a-zA-Z0-9\s]/.test(inputField.value)) { // Notice the \s here
          errorMessage += disallowedValue + '<br>'; // Add line break
        }
      });

      errorBlock.innerHTML = errorMessage || ''; // Use innerHTML to interpret <br> tags
    });
  });
});
</script>
UX
Conditional Visibility

#74 - Styling with Link Parameters

Update page styling based on a link parameter. Ex. ?ms-code-target=CLASSNAME&ms-code-style=display:block

v0.1

<!-- 💙 MEMBERSCRIPT #74 v0.1 💙 UPDATE STYLING WITH LINK PARAMS -->
<script>
    // Function to parse URL parameters
    function getURLParameter(name) {
        return decodeURIComponent((new RegExp('[?|&]' + name + '=' + '([^&;]+?)(&|#|;|$)').exec(location.search) || [null, ''])[1].replace(/\+/g, '%20')) || null;
    }

    // Function to apply styles
    function applyStylesFromURL() {
        const targetClass = getURLParameter('ms-code-target');
        const rawStyles = getURLParameter('ms-code-style');

        if (targetClass && rawStyles) {
            const elements = document.querySelectorAll(`.${targetClass}`);

            const styles = rawStyles.split(';').filter(style => style.trim() !== ''); // filter out any empty strings
            styles.forEach(style => {
                const [property, value] = style.split(':');
                elements.forEach(element => {
                    element.style[property] = value;
                });
            });
        }
    }

    // Call the function once the DOM is loaded
    window.addEventListener('DOMContentLoaded', (event) => {
        applyStylesFromURL();
    });
</script>
Marketing
UX

#73 - Display Date & Time

Display the current time, time-of-day, day, month, or year to a user. Works if logged in or logged out.

v0.1

<!-- 💙 MEMBERSCRIPT #73 v0.1 💙 DATES AND TIMES -->

function getCurrentDateInfo(attribute) {
    const now = new Date();
    const options = { hour12: true };

    switch(attribute) {
        case "day":
            return now.toLocaleDateString('en-US', { weekday: 'long' });
        case "time":
            return now.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', hour12: true }).toLowerCase();
        case "month":
            return now.toLocaleDateString('en-US', { month: 'long' });
        case "year":
            return now.getFullYear().toString();
        case "time-of-day":
            const hour = now.getHours();
            if (5 <= hour && hour < 12) return "morning";
            if (12 <= hour && hour < 17) return "afternoon";
            if (17 <= hour && hour < 21) return "evening";
            return "night";
        default:
            return "Invalid attribute";
    }
}

function updateDateInfoOnPage() {
    const spanTags = document.querySelectorAll('span[ms-code-date]');

    spanTags.forEach(tag => {
        const attributeValue = tag.getAttribute('ms-code-date');
        const dateInfo = getCurrentDateInfo(attributeValue);
        tag.textContent = dateInfo;
    });
}

// Call the function to update the content on the page
updateDateInfoOnPage();
</script>
Custom Fields
Conditional Visibility

#72 - Validate Original Values

Only allow a form to be submitted if the input value is original (ie. Usernames)

v0.1

<!-- 💙 MEMBERSCRIPT #72 v0.1 💙 VALIDATE ORIGINAL VALUES -->

<style>
[ms-code-available="true"],
[ms-code-available="false"],
[ms-code-available="invalid"]{
    display: none;
}

.disabled {
    opacity: 0.5;
    pointer-events: none;
}
</style>

<script>
document.addEventListener('DOMContentLoaded', function() {
    let input = document.querySelector('[ms-code-available="input"]');
    let trueElement = document.querySelector('[ms-code-available="true"]');
    let falseElement = document.querySelector('[ms-code-available="false"]');
    let invalidElement = document.querySelector('[ms-code-available="invalid"]');
    let listElements = Array.from(document.querySelectorAll('[ms-code-available="list"]'));
    let submitButton = document.querySelector('[ms-code-available="submit"]');

    function checkUsername() {
        // Check if the input matches any of the list items
        let isTaken = listElements.some(elem => elem.textContent.trim() === input.value.trim());

        if (isTaken) {
            trueElement.style.display = 'none';
            falseElement.style.display = 'flex';
            submitButton.classList.add('disabled'); // disable the button if username is taken
        } else {
            trueElement.style.display = 'flex';
            falseElement.style.display = 'none';
            submitButton.classList.remove('disabled');
        }
    }

    input.addEventListener('input', function() {
        // Display the invalid element if input length is between 1 and 3
        if (input.value.length >= 1 && input.value.length <= 3) {
            invalidElement.style.display = 'flex';
        } else {
            invalidElement.style.display = 'none';
        }

        // Add the .disabled class to the submit button if input is empty or less than 3 characters
        if (input.value.length <= 3) {
            submitButton.classList.add('disabled');
            trueElement.style.display = 'none';
            falseElement.style.display = 'none';
        } else {
            checkUsername();
        }
    });
});
</script>
UX
Custom Fields

#71 - Redirect if Certain Fields are Empty

Redirect a member to an onboarding page if certain custom fields are empty.

v0.1

<!-- 💙 MEMBERSCRIPT #71 v0.1 💙 REDIRECT IF FIELDS ARE EMPTY -->
<script>
  document.addEventListener('DOMContentLoaded', async function() {
    const memberstack = window.$memberstackDom;

    const onboardingPageUrl = '/onboarding'; // replace
    const customFieldKeys = 'custom-field-1,custom-field-2'; // replace

    // No need to edit past this line
    const member = await memberstack.getCurrentMember();
    if (!member) {
      return;
    }

    // If current page slug matches the redirect slug, exit the script
    const currentPageSlug = window.location.pathname;
    if (currentPageSlug === onboardingPageUrl) {
      return;
    }

    async function checkOnboardingStatus() {
      try {
        const memberData = await memberstack.updateMember({});
        const customFields = customFieldKeys.split(',');

        for (let field of customFields) {
          if (!memberData.data.customFields[field.trim()]) {
            // Redirect to onboarding page if the custom field is empty
            window.location.href = onboardingPageUrl;
            return;
          }
        }
      } catch (error) {
        console.error(`Error in checkOnboardingStatus function: ${error}`);
      }
    }

    // Check onboarding status and potentially redirect
    checkOnboardingStatus().catch(error => {
      console.error(`Error in MemberScript #71 initial functions: ${error}`);
    });
  });
</script>
Marketing
JSON
Conditional Visibility
UX

#70 - Hide Old/Seen CMS Items

Only show CMS items which are new to a particular member. If they've seen it, hide it.

v0.1

<!-- 💙 MEMBERSCRIPT #70 v0.1 💙 HIDE OLD CMS ITEMS -->
<script>
  document.addEventListener('DOMContentLoaded', async function() {
    const memberstack = window.$memberstackDom;

    // Only proceed if a member is found
    const member = await memberstack.getCurrentMember();
    if (!member) {
      console.log('No member found in MemberScript #70, exiting script');
      return;
    }

    async function getCmsItemsFromJson() {
      try {
        const memberData = await memberstack.getMemberJSON();
        return memberData?.data?.cmsItems || [];
      } catch (error) {
        console.error(`Error in getCmsItemsFromJson function: ${error}`);
      }
    }

    async function updateCmsItemsInJson(newCmsItems) {
      try {
        const memberData = await memberstack.getMemberJSON();
        memberData.data = memberData.data || {};
        memberData.data.cmsItems = newCmsItems;
        console.log(`CMS items in JSON after update: ${JSON.stringify(newCmsItems)}`);
        await memberstack.updateMemberJSON({ json: memberData.data });
      } catch (error) {
        console.error(`Error in updateCmsItemsInJson function: ${error}`);
      }
    }

    async function hideSeenCmsItems() {
      try {
        const cmsItemsElements = document.querySelectorAll('[ms-code-cms-item]');
        const cmsItemsFromJson = await getCmsItemsFromJson();

        cmsItemsElements.forEach(element => {
          const cmsValue = element.getAttribute('ms-code-cms-item');
          
          if (cmsItemsFromJson.includes(cmsValue)) {
            element.style.display = 'none';
          } else {
            cmsItemsFromJson.push(cmsValue);
          }
        });

        // Update the CMS items in JSON after the checks
        await updateCmsItemsInJson(cmsItemsFromJson);

      } catch (error) {
        console.error(`Error in hideSeenCmsItems function: ${error}`);
      }
    }

    // Hide seen CMS items when the page loads
    hideSeenCmsItems().catch(error => {
      console.error(`Error in MemberScript #70 initial functions: ${error}`);
    });
  });
</script>
Marketing
Modals
JSON
Conditional Visibility
UX

#69 - Notify Members of New CMS Items

Display an element when there are new CMS items.

v0.1

<!-- 💙 MEMBERSCRIPT #69 v0.1 💙 DISPLAY ELEMENT IF NEW CMS ITEMS -->
<script>
  document.addEventListener('DOMContentLoaded', async function() {
    const memberstack = window.$memberstackDom;

    // Set this variable to 'YES' or 'NO' depending on whether you want the UI to be displayed for new users
    const displayForNewUsers = 'YES';

    // Only proceed if a member is found
    const member = await memberstack.getCurrentMember();
    if (!member) {
      console.log('No member found, exiting script');
      return;
    }

    async function getUpdatesIDFromJson() {
      try {
        const memberData = await memberstack.getMemberJSON();
        console.log(`Member data: ${JSON.stringify(memberData)}`);
        return memberData?.data?.updatesID || '';
      } catch (error) {
        console.error(`Error in getUpdatesIDFromJson function: ${error}`);
      }
    }

    async function updateUpdatesIDInJson(newUpdatesID) {
      try {
        const memberData = await memberstack.getMemberJSON();
        memberData.data = memberData.data || {};
        memberData.data.updatesID = newUpdatesID;
        console.log(`Updates ID in JSON after update: ${newUpdatesID}`);
        await memberstack.updateMemberJSON({ json: memberData.data });
      } catch (error) {
        console.error(`Error in updateUpdatesIDInJson function: ${error}`);
      }
    }

    async function checkAndUpdateUI() {
      try {
        const element = document.querySelector('[ms-code-update-item]');
        const cmsItem = element.textContent;
        console.log(`CMS item: ${cmsItem}`);

        // Get the current updates ID from JSON
        const updatesIDFromJson = await getUpdatesIDFromJson();
        console.log(`Updates ID from JSON: ${updatesIDFromJson}`);

        // Check displayForNewUsers variable to decide behavior
        if (displayForNewUsers === 'NO' && !updatesIDFromJson) {
          console.log('Updates ID from JSON is undefined, null, or empty, not changing UI');
          return;
        }

        if (cmsItem !== updatesIDFromJson) {
          const uiElements = document.querySelectorAll('[ms-code-update-ui]');
          uiElements.forEach(uiElement => {
            uiElement.style.display = 'block';
            uiElement.style.opacity = '1';
          });
        }

        // Update the updates ID in JSON after the UI has been updated
        await updateUpdatesIDInJson(cmsItem);

      } catch (error) {
        console.error(`Error in checkAndUpdateUI function: ${error}`);
      }
    }

    // Check and update UI when the page loads
    checkAndUpdateUI().catch(error => {
      console.error(`Error in initial functions: ${error}`);
    });
  });
</script>
Marketing
Custom Fields

#67 - Prefill Form Based On URL Parameters

Easily fill inputs using URL parameters.

v0.1

<!-- 💙 MEMBERSCRIPT #67 v0.1 💙 PREFILL INPUTS WITH URL PARAMETERS -->
<script>
  // Function to get URL parameters
  function getURLParams() {
    const urlParams = new URLSearchParams(window.location.search);
    return Object.fromEntries(urlParams.entries());
  }

  // Function to prefill inputs based on URL parameters
  function prefillInputs() {
    const urlParams = getURLParams();
    const inputElements = document.querySelectorAll('[ms-code-prefill-param]');
    inputElements.forEach((inputElement) => {
      const paramKey = inputElement.getAttribute('ms-code-prefill-param');
      if (paramKey && urlParams[paramKey]) {
        inputElement.value = urlParams[paramKey];
      }
    });
  }

  // Call the function to prefill inputs when the page loads
  prefillInputs();
</script>
Marketing

#66 - Member ID Invite Links

Create custom, unique invite/referral links.

v0.1

<!-- 💙 MEMBERSCRIPT #66 v0.1 💙 MEMBER ID INVITE LINKS -->
<script>
  // Function to get the member ID from local storage
  function getMemberIDFromLocalStorage() {
    // Assuming "_ms-mem" is the key that holds the member object in local storage
    const memberObject = JSON.parse(localStorage.getItem("_ms-mem"));
    if (memberObject && memberObject.id) {
      return memberObject.id;
    }
    return null;
  }

  // Function to update the invite link with the member ID as a URL parameter
  function updateInviteLink() {
    const inviteLinkElement = document.querySelector('[ms-code-invite-link]');
    if (inviteLinkElement) {
      const inviteLinkBase = inviteLinkElement.getAttribute('ms-code-invite-link');
      const memberID = getMemberIDFromLocalStorage();
      if (memberID) {
        const inviteLinkWithID = `${inviteLinkBase}?inviteCode=${memberID}`;
        inviteLinkElement.textContent = inviteLinkWithID;
        inviteLinkElement.href = inviteLinkWithID; // If it's an anchor link
      }
    }
  }

  // Call the function to update the invite link when the page loads
  updateInviteLink();
</script>
Marketing
Modals

#65 - Exit Intent Popup

Show visitors a popup when their mouse leaves to the top.

v0.1

<!-- 💙 MEMBERSCRIPT #65 v0.1 💙 EXIT INTENT POPUP -->
<script>
const CookieService = {
    setCookie(name, value, days) {
        const date = new Date();
        date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
        const expires = days ? '; expires=' + date.toUTCString() : '';
        document.cookie = name + '=' + (value || '')  + expires + ';';
    },

    getCookie(name) {
        const cookieValue = document.cookie
            .split('; ')
            .find(row => row.startsWith(name))
            ?.split('=')[1];
        return cookieValue || null;
    }
};

const exitPopup = document.querySelector('[ms-code-popup="exit-intent"]');

const mouseEvent = e => {
    const shouldShowExitIntent = 
        !e.toElement && 
        !e.relatedTarget &&
        e.clientY < 10;

    if (shouldShowExitIntent) {
        document.removeEventListener('mouseout', mouseEvent);
        exitPopup.style.display = 'flex';
        CookieService.setCookie('exitIntentShown', true, 30);
    }
};

if (!CookieService.getCookie('exitIntentShown')) {
    document.addEventListener('mouseout', mouseEvent);
    document.addEventListener('keydown', exit);
    exitPopup.addEventListener('click', exit);
}
</script>
Conditional Visibility
Custom Fields

#64 - Radio Form Logic

Show set elements depending on which radio is selected.

v0.1

<!-- 💙 MEMBERSCRIPT #64 v0.1 💙 RADIO FORM LOGIC -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<script>
$(document).ready(function() {
    // initially hide all divs with 'ms-code-more-info' attribute
    $("div[ms-code-more-info]").hide();

    // listen for change events on all radios with 'ms-code-radio-option' attribute
    $("input[ms-code-radio-option]").change(function() {
        // hide all divs again
        $("div[ms-code-more-info]").hide();

        // get the value of the selected radio button
        var selectedValue = $(this).attr("ms-code-radio-option");

        // find the div with the 'ms-code-more-info' attribute that matches the selected value and show it
        $("div[ms-code-more-info=" + selectedValue + "]").show();
    });
});
</script>
Custom Fields

#63 - Date Range Picker

Create a date range input in Webflow!

v0.1

<!-- 💙 MEMBERSCRIPT #62 v0.1 💙 DATE RANGE PICKER -->
<script type="text/javascript" src="https://cdn.jsdelivr.net/jquery/latest/jquery.min.js"></script>
<script type="text/javascript" src="https://cdn.jsdelivr.net/momentjs/latest/moment.min.js"></script>
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/daterangepicker/daterangepicker.min.js"></script>
<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/daterangepicker/daterangepicker.css" />
<style>
  .daterangepicker td.active {
    background-color: #006cfa !important ;
  }
</style>
<script type="text/javascript">
$(function() {

  $('input[ms-code-input="date-range"]').daterangepicker({
      "opens": "center",
      "locale": {
        "format": "MM/DD/YYYY",
        "separator": " - ",
        "applyLabel": "Apply",
        "cancelLabel": "Cancel",
        "fromLabel": "From",
        "toLabel": "To",
        "customRangeLabel": "Custom",
        "weekLabel": "W",
        "daysOfWeek": [
            "Su",
            "Mo",
            "Tu",
            "We",
            "Th",
            "Fr",
            "Sa"
        ],
        "monthNames": [
            "January",
            "February",
            "March",
            "April",
            "May",
            "June",
            "July",
            "August",
            "September",
            "October",
            "November",
            "December"
        ],
    },
  });

  $('input[name="datefilter"]').on('apply.daterangepicker', function(ev, picker) {
      $(this).val(picker.startDate.format('MM/DD/YYYY') + ' - ' + picker.endDate.format('MM/DD/YYYY'));
  });

  $('input[name="datefilter"]').on('cancel.daterangepicker', function(ev, picker) {
      $(this).val('');
  });

});
</script>
Marketing
UX
JSON

#62 - Upvote Button

Add upvote functionality to the Webflow CMS.

v0.2

<!-- 💙 MEMBERSCRIPT #62 v0.2 💙 UPVOTE FORM -->
<script> 
  document.addEventListener('DOMContentLoaded', function() {
    const memberstack = window.$memberstackDom;
    const upvoteButtons = document.querySelectorAll('[ms-code="upvote-button"]');
    const upvoteForms = document.querySelectorAll('[ms-code="upvote-form"]');
    const upvotedValues = document.querySelectorAll('[ms-code="upvoted-value"]');
    const upvoteCounts = document.querySelectorAll('[ms-code="upvote-count"]');
    let clickTimeout; // Variable to store the timer
    let lastClickedButton = null; // Variable to store the last clicked button

    // Function to handle upvote button click
    function handleUpvoteButtonClick(event) {
      event.preventDefault();
      const button = event.currentTarget;

      // Clear the timer if the same button is clicked
      if (button === lastClickedButton) {
        clearTimeout(clickTimeout);
      }
      
      lastClickedButton = button; // Store the reference to the currently clicked button

      // Set a new timer
      clickTimeout = setTimeout(function() {
        const form = button.closest('form');
        const cmsId = button.getAttribute('data-cms-id');
        const upvotedValue = form.querySelector('[ms-code="upvoted-value"]');
        const upvoteCount = form.querySelector('[ms-code="upvote-count"]');

        if (button.classList.contains('is-true')) {
          // Remove upvote
          button.classList.remove('is-true');
          upvotedValue.value = 'false';
          upvoteCount.textContent = parseInt(upvoteCount.textContent) - 1;

          memberstack.getMemberJSON()
            .then(function(memberData) {
              if (memberData.data && memberData.data.upvotes) {
                const upvotes = memberData.data.upvotes;
                const index = upvotes.indexOf(cmsId);
                if (index !== -1) {
                  upvotes.splice(index, 1);
                  memberstack.updateMemberJSON({ json: memberData.data });
                }
              }
            })
            .catch(function(error) {
              console.error('Error retrieving/updating member data:', error);
            });
        } else {
          // Add upvote
          button.classList.add('is-true');
          upvotedValue.value = 'true';
          upvoteCount.textContent = parseInt(upvoteCount.textContent) + 1;

          memberstack.getMemberJSON()
            .then(function(memberData) {
              memberData.data = memberData.data || {};
              memberData.data.upvotes = memberData.data.upvotes || [];
              memberData.data.upvotes.push(cmsId);
              memberstack.updateMemberJSON({ json: memberData.data });
            })
            .catch(function(error) {
              console.error('Error retrieving/updating member data:', error);
            });
        }

        // Make the API call
        fetch(form.action, {
          method: form.method,
          headers: {
            'Content-Type': 'application/x-www-form-urlencoded'
          },
          body: new URLSearchParams(new FormData(form))
        })
          .then(function(response) {
            if (response.ok) {
              // Handle successful API response
              return response.json();
            } else {
              // Handle API error
              throw new Error('API Error');
            }
          })
          .then(function(data) {
            // Handle API response to update vote count
            upvoteCount.textContent = data.upvoteCount; // Replace with the actual property holding the updated vote count
          })
          .catch(function(error) {
            console.error('API Error:', error);
          });
      }, 200); // 0.2 seconds
    }

    // Attach event listeners to upvote buttons
    upvoteButtons.forEach(function(button) {
      button.addEventListener('click', handleUpvoteButtonClick);
    });

    // Check if member has upvotes on page load
    memberstack.getMemberJSON()
      .then(function(memberData) {
        if (memberData.data && memberData.data.upvotes) {
          const upvotes = memberData.data.upvotes;
          upvoteButtons.forEach(function(button) {
            const cmsId = button.getAttribute('data-cms-id');
            if (upvotes.includes(cmsId)) {
              button.classList.add('is-true');
              const form = button.closest('form');
              const upvotedValue = form.querySelector('[ms-code="upvoted-value"]');
              upvotedValue.value = 'true';
            }
          });
        }
      })
      .catch(function(error) {
        console.error('Error retrieving member data:', error);
      });
  });
</script>
Conditional Visibility

#61 - Display Element If Checkbox Is Checked

Create conditional visibility based on a checkbox field.

v0.1

<!-- 💙 MEMBERSCRIPT #61 v0.1 💙 SHOW ELEMENT IF CHECKBOX IS CHECKED -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"> </script>
<script>
$(document).ready(function() {
    // Initially hide all elements with the 'ms-code-checkbox-display' attribute
    $("[ms-code-checkbox-display]").hide();

    // When a checkbox with 'ms-code-checkbox-input' attribute is clicked, perform the following
    $("[ms-code-checkbox-input]").click(function() {
        // Get the value of the 'ms-code-checkbox-input' attribute
        var checkboxVal = $(this).attr('ms-code-checkbox-input');
        
        // Find the corresponding element with the 'ms-code-checkbox-display' attribute and same value
        var displayElement = $("[ms-code-checkbox-display=" + checkboxVal + "]");

        // If this checkbox is checked, show the corresponding element
        if ($(this).is(":checked")) {
            displayElement.show();
        } else {
            // If this checkbox is unchecked, hide the corresponding element
            displayElement.hide();
        }
    });
});
</script>
Custom Fields
UX

#60 - Increase/Decrease Select Value

Create previous & next buttons for a select field.

v0.1

<!-- 💙 MEMBERSCRIPT #60 v0.1 💙 INCREASE/DECREASE SELECT VALUE -->
<script>
    var select = document.querySelector('[ms-code-select="input"]');
    var prev = document.querySelector('[ms-code-select="prev"]');
    var next = document.querySelector('[ms-code-select="next"]');

    function updateButtons() {
        prev.style.opacity = select.selectedIndex === 0 ? '0.5' : '1';
        next.style.opacity = select.selectedIndex === select.options.length - 1 ? '0.5' : '1';
    }

    prev.addEventListener('click', function() {
        if (select.selectedIndex > 0) {
            select.selectedIndex--;
        }
        updateButtons();
    });

    next.addEventListener('click', function() {
        if (select.selectedIndex < select.options.length - 1) {
            select.selectedIndex++;
        }
        updateButtons();
    });

    updateButtons();
</script>
UX
Marketing

#59 - Restart GIF On Hover

Start a GIF from the beginning on hover.

v0.1

<!-- 💙 MEMBERSCRIPT #59 v0.1 💙 RESTART GIF -->
<script>
    document.addEventListener('DOMContentLoaded', (event) => {
        const hoverElements = document.querySelectorAll('[data-gif-hover]');
        hoverElements.forEach((element) => {
            element.addEventListener('mouseover', function() {
                const gifNum = this.getAttribute('data-gif-hover');
                const gifElement = document.querySelector(`[data-gif="${gifNum}"]`);
                if (gifElement) {
                    const gifSrc = gifElement.getAttribute('src');
                    gifElement.setAttribute('src', '');
                    gifElement.setAttribute('src', gifSrc);
                }
            });
        });
    });
</script>
Custom Fields
UX

#58 - Price Range Slider Inputs

Create a price range input with individual inputs for min and max.

v0.1

<!-- 💙 MEMBERSCRIPT #58 v0.1 💙 RANGE SLIDER INPUT -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"> </script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/ion-rangeslider/2.3.1/css/ion.rangeSlider.min.css" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/ion-rangeslider/2.3.1/js/ion.rangeSlider.min.js"></script>
<style>
    .irs {
      font-family: inherit;
    }
</style>
<script>
    $(document).ready(function() {
        var rangeSlider = $('[ms-code-input="range-slider"]');
        var priceFromInput = $('[ms-code-input="price-from"]');
        var priceToInput = $('[ms-code-input="price-to"]');
        
        // Set the default range values
        var defaultFrom = 20000;
        var defaultTo = 50000;

        rangeSlider.ionRangeSlider({
            skin: "round", // You can also try "flat", "big", "modern", "sharp", or "square". Default styles can be overridden with CSS.
            type: "double",
            grid: true,
            min: 0,
            max: 100000,
            from: defaultFrom,
            to: defaultTo,
            prefix: "$",
            onStart: function(data) {
                priceFromInput.val(data.from);
                priceToInput.val(data.to);
            },
            onChange: function(data) {
                priceFromInput.val(data.from);
                priceToInput.val(data.to);
            }
        });

        // Get the initial range values and update the inputs
        var initialRange = rangeSlider.data("ionRangeSlider");
        var initialData = initialRange.result;
        priceFromInput.val(initialData.from);
        priceToInput.val(initialData.to);

        // Update the range slider and inputs when the inputs lose focus
        priceFromInput.on("blur", function() {
            var value = $(this).val();
            var toValue = priceToInput.val();
            
            // Perform validation
            if (
                value < initialRange.options.min ||
                value > initialRange.options.max ||
                value >= toValue ||
                value > initialData.to // Check if fromValue is higher than the current toValue
            ) {
                value = defaultFrom;
            }

            rangeSlider.data("ionRangeSlider").update({
                from: value
            });
            priceFromInput.val(value);
        });

        priceToInput.on("blur", function() {
            var value = $(this).val();
            var fromValue = priceFromInput.val();
            
            // Perform validation
            if (
                value < initialRange.options.min ||
                value > initialRange.options.max ||
                value <= fromValue ||
                value < initialData.from // Check if toValue is lower than the current fromValue
            ) {
                value = defaultTo;
            }

            rangeSlider.data("ionRangeSlider").update({
                to: value
            });
            priceToInput.val(value);
        });
    });
</script>
Custom Fields

#57 - Time Picker Input

Add a time picker to your form and prefill the time into a field.

v0.1

<!-- 💙 MEMBERSCRIPT #57 v0.1 💙 TIME PICKER -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"> </script>
<link rel="stylesheet" href="https://uicdn.toast.com/tui.time-picker/latest/tui-time-picker.css">
<script src="https://uicdn.toast.com/tui.time-picker/latest/tui-time-picker.js"> </script>
<script>
$(document).ready(function() {
    var tpSpinbox = new tui.TimePicker(document.querySelector('[ms-code-timepicker="box"]'), {
        inputType: 'spinbox',
        showMeridiem: true // If you don't use AM/PM remove this line
    });

    // Setup an event handler for when the time is selected
    tpSpinbox.on('change', function() {
        // Get the selected time
        var hour = tpSpinbox.getHour();
        var minute = tpSpinbox.getMinute();

        var selectedTime = hour + ':' + (minute < 10 ? '0' : '') + minute;

        // Update the value of the input element
        document.querySelector('[ms-code-timepicker="input"]').value = selectedTime;
    });
});
</script>
Custom Fields

#56 - Input Option Pairs

Combine the values of multiple inputs into one field.

v0.1

<!-- 💙 MEMBERSCRIPT #56 v0.1 💙 INPUT OPTION PAIRS -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"> </script>
<script>
$(document).ready(function() {
    var groups = {};

    // Get all inputs with the attribute ms-code-combine-inputs
    var inputs = $('input[ms-code-combine-inputs], select[ms-code-combine-inputs]');

    // For each input
    inputs.each(function() {
        // Split the attribute value at the dash
        var parts = $(this).attr('ms-code-combine-inputs').split('-');

        // If the group doesn't exist yet, create it
        if (!groups[parts[0]]) {
            groups[parts[0]] = {
                targets: [],
                values: [],
            };
        }

        // If it's a target, add it to the targets
        if (parts[1] == 'target') {
            groups[parts[0]].targets.push($(this));
        } else {
            // It's an input, add it to the values and attach a listener
            groups[parts[0]].values.push($(this));

            $(this).on('input change', function() {
                // On input or change, combine all values with a space in between
                // and set the targets' value
                var combinedValue = '';
                $.each(groups[parts[0]].values, function(index, value) {
                    combinedValue += $(this).val();
                    if (index < groups[parts[0]].values.length - 1) {
                        combinedValue += ' '; // Add a space between values
                    }
                });

                $.each(groups[parts[0]].targets, function() {
                    $(this).val(combinedValue);
                });
            });
        }
    });
});
</script>
Custom Fields

#55 - Change Checkbox Parent Styles

Change the styles of another element when a checkbox is checked.

v0.1

<!-- 💙 MEMBERSCRIPT #55 v0.1 💙 UPDATE CHECKBOX PARENT STYLES -->
<script>
// Wait for the DOM content to load
document.addEventListener('DOMContentLoaded', function() {
  // Get all the checkbox elements
  var checkboxes = document.querySelectorAll('[ms-code-checkbox="check"]');

  // Iterate over each checkbox element
  checkboxes.forEach(function(checkbox) {
    // Get the boolean wrap element associated with the current checkbox
    var booleanWrap = checkbox.closest('[ms-code-checkbox="wrap"]');

    // Function to update the boolean wrap class based on checkbox state
    function updateBooleanWrapClass() {
      if (checkbox.checked) {
        booleanWrap.classList.add('checked');
      } else {
        booleanWrap.classList.remove('checked');
      }
    }

    // Check the initial value of the checkbox
    updateBooleanWrapClass();

    // Add an event listener to the checkbox to handle changes
    checkbox.addEventListener('change', function() {
      updateBooleanWrapClass();
    });
  });
});
</script>
Custom Fields
UX

#54 - Checkbox Form Field Logic

Block other fields/items if a checkbox is not checked.

v0.1

<!-- 💙 MEMBERSCRIPT #54 v0.1 💙 CHECKBOX FIELD FORM LOGIC -->
<style>
  .disabled {
    pointer-events: none;
    opacity: 0.5;
  }
</style>
<script>
// Wait for the DOM content to load
document.addEventListener('DOMContentLoaded', function() {
  // Get all the trigger checkboxes
  var triggerCheckboxes = document.querySelectorAll('[ms-code-field-logic-trigger]');

  // Iterate over each trigger checkbox
  triggerCheckboxes.forEach(function(checkbox) {
    // Get the value of the trigger checkbox's attribute
    var triggerValue = checkbox.getAttribute('ms-code-field-logic-trigger');

    // Function to update the target elements' class based on checkbox state
    function updateTargetElementsClass() {
      // Find the associated target elements based on the attribute value
      var targetElements = document.querySelectorAll('[ms-code-field-logic-target="' + triggerValue + '"]');

      // Check the new value of the trigger checkbox
      if (!checkbox.checked) {
        // Add the "disabled" class to each target element
        targetElements.forEach(function(targetElement) {
          targetElement.classList.add('disabled');
        });
      } else {
        // Remove the "disabled" class from each target element
        targetElements.forEach(function(targetElement) {
          targetElement.classList.remove('disabled');
        });
      }
    }

    // Check the initial value of the trigger checkbox
    updateTargetElementsClass();

    // Add an event listener to the trigger checkbox to handle changes
    checkbox.addEventListener('change', function() {
      updateTargetElementsClass();
    });
  });
});
</script>
JSON

#53 - Update JSON Items With A Form

Allow your members to change details about their JSON items.

v0.1

<!-- 💙 MEMBERSCRIPT #53 v0.1 💙 UPDATE JSON ITEMS WITH A FORM -->
<script>
  document.addEventListener("DOMContentLoaded", function() {
    const memberstack = window.$memberstackDom;

    // Add click event listener to the document
    document.addEventListener("click", async function(event) {
      const target = event.target;

      // Check if the clicked element has ms-code-edit-item attribute
      const editItem = target.closest('[ms-code-edit-item="prompt"]');
      if (editItem) {
        // Get the item key from the closest ancestor element with ms-code-item-key attribute
        const key = editItem.closest('[ms-code-item-key]').getAttribute('ms-code-item-key');

        // Retrieve the current member JSON data
        const member = await memberstack.getMemberJSON();

        // SET THE TARGET - EDIT ME
        let targetObject = member.data.projects; // Update this line with the desired target location

        if (member.data && targetObject && targetObject[key]) {
          // Get the form element with the ms-code-edit-item="form" attribute
          const form = document.querySelector('form[ms-code-edit-item="form"]');

          if (form) {
            // Loop through the form fields
            for (const field of form.elements) {
              const jsonName = field.getAttribute('ms-code-json-name');
              if (jsonName && targetObject[key].hasOwnProperty(jsonName)) {
                // Pre-fill the form field with the corresponding value from the JSON item
                field.value = targetObject[key][jsonName];
              }
            }

            // Get the modal element with the ms-code-edit-item="modal" attribute
            const modal = document.querySelector('[ms-code-edit-item="modal"]');
            if (modal) {
              // Set the display property of the modal to flex
              modal.style.display = 'flex';
            }

            // Add submit event listener to the form
            form.addEventListener("submit", async function(event) {
              event.preventDefault(); // Prevent the form from submitting normally

              // Create an object to hold the updated values
              const updatedValues = {};

              // Loop through the form fields
              for (const field of form.elements) {
                const jsonName = field.getAttribute('ms-code-json-name');
                if (jsonName) {
                  // Update the corresponding value in the updatedValues object
                  updatedValues[jsonName] = field.value;
                }
              }

              // Update the target object with the new values
              targetObject[key] = { ...targetObject[key], ...updatedValues };

              // Update the member JSON using the Memberstack SDK
              await memberstack.updateMemberJSON({
                json: member.data
              });

              // Optional: Display a success message or perform any other desired action
              console.log('Member JSON updated successfully');
            });
          } else {
            console.error('Form element not found');
          }
        } else {
          console.error(`Could not find item with key: ${key}`);
        }
      }
    });
  });
</script>
UX

#52 - Delayed Page Redirect

Redirect members to a new page with an optional delay.

v0.1

<!-- 💙 MEMBERSCRIPT #52 v0.1 💙 DELAYED REDIRECT TO NEW PAGE -->
<script>
  const redirectToNewPage = function() {
    setTimeout(function() {
      window.location.href = "/your-page";
    }, 1000); // Delay in milliseconds
  };

  redirectToNewPage();
</script>
UX

#51 - Display Member Metadata

Display member metadata dynamically on your website.

v0.2

<!-- 💙 MEMBERSCRIPT #51 v0.2 💙 DISPLAY MEMBER METADATA -->
<script>
  function replaceTextWithMetadata(metadata) {
    var els = Array.from(document.querySelectorAll('[ms-code-member-meta]'));
    els.forEach((el) => {
      const key = el.getAttribute('ms-code-member-meta');
      const value = metadata[key];
      if (value !== undefined) {
        el.innerHTML = value;
        el.value = value;
        el.src = value;
      }
    });
  }

  const memberstack = window.$memberstackDom;
  memberstack.getCurrentMember()
    .then(({ data: member }) => {
      if (member && member.metaData) {
        replaceTextWithMetadata(member.metaData);
      }
    })
    .catch((error) => {
      console.error('Error retrieving member data:', error);
    });
</script>
JSON
UX

#50 - Cross-Device Dark Mode

Persistent dark mode option which stays working on your members' different devices.

v0.1

Head Code

Put this in the <head> section of your site.


<!-- 💙 MEMBERSCRIPT #50 HEAD CODE v0.1 💙 CROSS-DEVICE DARK MODE -->
<script> 
  document.addEventListener('DOMContentLoaded', function() {
    const themePreference = localStorage.getItem('themePreference');
    if (themePreference === 'dark') {
      document.body.classList.add('dark');
    }
  });
</script>

Body Code

Put this in the </body> section of your site.


<!-- 💙 MEMBERSCRIPT #50 BODY CODE v0.1 💙 CROSS-DEVICE DARK MODE -->
<script>
  document.addEventListener('DOMContentLoaded', function() {
    const darkModeToggle = document.querySelector('[ms-code-dark-mode="toggle"]');
    const bodyElement = document.querySelector('.body');

    // Function to check if theme preference is saved in member JSON
    function checkThemePreference() {
      const memberstack = window.$memberstackDom;
      memberstack.getMemberJSON()
        .then(function(memberData) {
          const themePreference = memberData.data?.themePreference;
          if (themePreference === 'dark') {
            enableDarkMode();
          } else {
            disableDarkMode();
          }
        })
        .catch(function(error) {
          console.error('Error retrieving member data:', error);
        });
    }

    // Function to enable dark mode
    function enableDarkMode() {
      darkModeToggle.classList.add('dark');
      bodyElement.classList.add('dark');
      updateThemePreference('dark');
    }

    // Function to disable dark mode
    function disableDarkMode() {
      darkModeToggle.classList.remove('dark');
      bodyElement.classList.remove('dark');
      updateThemePreference('light');
    }

    // Function to update theme preference in member JSON
    function updateThemePreference(themePreference) {
      const memberstack = window.$memberstackDom;
      memberstack.getMemberJSON()
        .then(function(memberData) {
          memberData.data = memberData.data || {};
          memberData.data.themePreference = themePreference;
          memberstack.updateMemberJSON({ json: memberData.data })
            .then(function() {
              localStorage.setItem('themePreference', themePreference);
            })
            .catch(function(error) {
              console.error('Error updating member data:', error);
            });
        })
        .catch(function(error) {
          console.error('Error retrieving member data:', error);
        });
    }

    // Event listener for dark mode toggle
    darkModeToggle.addEventListener('click', function() {
      if (darkModeToggle.classList.contains('dark')) {
        disableDarkMode();
      } else {
        enableDarkMode();
      }
    });

    // Apply transition duration and timing to all elements
    const transitionDuration = '0.0s';
    const transitionTiming = 'ease';
    const elementsToTransition = [darkModeToggle, bodyElement];
    elementsToTransition.forEach(function(element) {
      element.style.transitionDuration = transitionDuration;
      element.style.transitionTimingFunction = transitionTiming;
    });

    // Check theme preference on page load
    const savedThemePreference = localStorage.getItem('themePreference');
    if (savedThemePreference === 'dark') {
      enableDarkMode();
    } else {
      disableDarkMode();
    }
    checkThemePreference();
  });
</script>
Custom Fields
UX

#49 - Disable First Option in a Select Input

Prevent users from selecting the placeholder option in your select inputs.

v0.1

<!-- 💙 MEMBERSCRIPT #49 v0.1 💙 DISABLE FIRST OPTION IN SELECT INPUT -->
<script>
    let selects = document.querySelectorAll("select[ms-code=hide-first-option]");
    selects.forEach((select) => { 
        let options = select.getElementsByTagName("option");
        options[0].hidden = true;
    });
</script>
Custom Fields
UX

#48 - Autocomplete Address Inputs

Prefill all address inputs using the Google Places API!

v0.1

Head Code

Place this in your page <head>


<!-- 💙 MEMBERSCRIPT #48 HEAD CODE v0.1 💙 AUTOFILL ADDRESS INPUTS -->
<script src="https://maps.googleapis.com/maps/api/js?key=YOUR-API-KEY&libraries=places&callback=initAutocomplete" async defer> </script>
<style>
.pac-logo::after {
display: none;
}
.pac-container {
border-radius: 5px;
border: 1px solid #ccc;
}
.pac-item {
padding: 0 10px;
}
</style>

Body Code

Place this in your page </body>


<!-- 💙 MEMBERSCRIPT #48 BODY CODE v0.1 💙 AUTOFILL ADDRESS INPUTS -->
<script>
let autocomplete;

function initAutocomplete() {
  autocomplete = new google.maps.places.Autocomplete(
    document.querySelector('input[ms-code-input="address"]'),
    {
      componentRestrictions: { country: ['US'] },
      fields: ['address_components'],
      types: ['address']
    }
  );

  autocomplete.addListener('place_changed', function() {
    const place = autocomplete.getPlace();

    if (place) {
      const addressInput = document.querySelector('input[ms-code-input="address"]');
      const cityInput = document.querySelector('input[ms-code-input="city"]');
      const regionInput = document.querySelector('input[ms-code-input="region"]');
      const countryInput = document.querySelector('input[ms-code-input="country"]');
      const postalCodeInput = document.querySelector('input[ms-code-input="postal-code"]');

      addressInput.value = extractAddress(place);
      cityInput.value = extractCity(place);
      regionInput.value = extractRegion(place);
      countryInput.value = extractCountry(place);
      postalCodeInput.value = extractPostalCode(place);
    }
  });
}

function extractAddress(place) {
  let address = '';
  const streetNumber = extractComponent(place, 'street_number');
  const route = extractComponent(place, 'route');

  if (streetNumber) {
    address += streetNumber + ' ';
  }
  if (route) {
    address += route;
  }

  return address.trim();
}

function extractComponent(place, componentType) {
  for (const component of place.address_components) {
    if (component.types.includes(componentType)) {
      return component.long_name;
    }
  }
  return '';
}

function extractCity(place) {
  for (const component of place.address_components) {
    if (component.types.includes('locality')) {
      return component.long_name;
    }
  }
  return '';
}

function extractRegion(place) {
  for (const component of place.address_components) {
    if (component.types.includes('administrative_area_level_1')) {
      return component.long_name;
    }
  }
  return '';
}

function extractCountry(place) {
  for (const component of place.address_components) {
    if (component.types.includes('country')) {
      return component.long_name;
    }
  }
  return '';
}

function extractPostalCode(place) {
  for (const component of place.address_components) {
    if (component.types.includes('postal_code')) {
      return component.long_name;
    }
  }
  return '';
}
</script>
JSON
UX

#47 - Display Date From Member JSON

Show members a date - for example, when their plan will expire!

v0.1

<!-- 💙 MEMBERSCRIPT #47 v0.1 💙 DISPLAY ONE TIME DATE -->
<script>
  document.addEventListener("DOMContentLoaded", async function() {
    const memberstack = window.$memberstackDom;

    const formatDate = function(date) {
      const options = { month: 'long', day: 'numeric', year: 'numeric' };
      return new Date(date).toLocaleDateString('en-US', options);
      // Replace 'en-US' with one of these depending on your locale: en-US, en-GB, en-CA, en-AU, fr-FR, de-DE, es-ES, it-IT, ja-JP, ko-KR, pt-BR, ru-RU, zn-CH, ar-SA
    };

    const updateTextSpans = async function() {
      const member = await memberstack.getMemberJSON();

      if (!member.data || !member.data['one-time-date']) {
        // Member data or one-time date not available, do nothing
        return;
      }

      const oneTimeDate = formatDate(member.data['one-time-date']);
      const textSpans = document.querySelectorAll('[ms-code-display-text="one-time-date"]');

      textSpans.forEach(span => {
        span.textContent = oneTimeDate;
      });
    };

    updateTextSpans();
  });
</script>
Custom Fields
UX

#46 - Confirm Password

Add a confirm password input to your signup & password reset forms.

v0.1

<!-- 💙 MEMBERSCRIPT #46 v0.1 💙 CONFIRM PASSWORD INPUT -->
<script>

var password = document.querySelector('[data-ms-member=password]')
  , confirm_password = document.querySelector('[ms-code-password=confirm]')

function validatePassword(){
  if(password.value != confirm_password.value) {
 		confirm_password.setCustomValidity("Passwords Don't Match");
    confirm_password.classList.add("invalid")
    confirm_password.classList.remove("valid")
  } else {
 		confirm_password.setCustomValidity('');
    confirm_password.classList.remove("invalid")
    confirm_password.classList.add("valid")
  }
}

password.onchange = validatePassword;
confirm_password.onkeyup = validatePassword;
</script>
Custom Fields
UX

#45 - Show/Hide Password

Add a show/hide password button to any form with a password input.

v0.2

<!-- 💙 MEMBERSCRIPT #45 v0.2 💙 SHOW AND HIDE PASSWORD -->
<script>
  document.querySelectorAll("[ms-code-password='transform']").forEach(function(button) {
    button.addEventListener("click", transform);
  });

  var isPassword = true;

  function transform() {
    var passwordInputs = document.querySelectorAll("[data-ms-member='password'], [data-ms-member='new-password'], [data-ms-member='current-password']");

    passwordInputs.forEach(function(myInput) {
      var inputType = myInput.getAttribute("type");

      if (isPassword) {
        myInput.setAttribute("type", "text");
      } else {
        myInput.setAttribute("type", "password");
      }
    });

    isPassword = !isPassword;
  }
</script>
Conditional Visibility

#44 - Show Element If Attribute Matches Member ID

Conditionally show elements if they have an attribute matching the members' ID.

v0.1

<!-- 💙 MEMBERSCRIPT #44 v0.1 💙 SHOW ELEMENT IF ATTRIBUTE MATCHES MEMBER ID -->
<script>
document.addEventListener("DOMContentLoaded", function() {
  if (localStorage.getItem("_ms-mem")) {
    const memberData = JSON.parse(localStorage.getItem("_ms-mem"));
    const memberId = memberData.id;

    const elements = document.querySelectorAll("[ms-code-member-id='" + memberId + "']");
    elements.forEach(element => {
      element.style.display = "block";
    });
  }
});
</script>
Modals

#43 - Block Scrolling When Modal Is Open

Stop the page from scrolling when someone opens a modal.

v0.1

<!-- 💙 MEMBERSCRIPT #43 v0.1 💙 BLOCK SCROLLING WHEN MODAL IS OPEN -->
<style>
.no-scroll {
  overflow: hidden;
}
</style>
<script>
function isDesktopViewport() {
  return window.innerWidth >= 900; // Adjust the breakpoint width as needed
}

const codeBlocks = document.querySelectorAll('[ms-code-block-scroll]');

function handleScrollBlock(event) {
  if (isDesktopViewport()) {
    document.body.classList.add('no-scroll');
  }
}

function handleScrollUnblock(event) {
  if (isDesktopViewport()) {
    document.body.classList.remove('no-scroll');
  }
}

codeBlocks.forEach(codeBlock => {
  codeBlock.addEventListener('mouseenter', handleScrollBlock);
  codeBlock.addEventListener('mouseleave', handleScrollUnblock);
});
</script>
Custom Fields

#42 - Image Editor Form Field

Allow people to upload & edit photos, then send them to Google Drive!

v0.2

Head Code

Place this in your page <head>


<!-- 💙 MEMBERSCRIPT #42 HEAD CODE v0.2 💙 FILE EDITOR FEATURE -->
<link rel="stylesheet" href="https://unpkg.com/filepond@^4/dist/filepond.css" />
<link rel="stylesheet" href="https://unpkg.com/filepond-plugin-image-edit/dist/filepond-plugin-image-edit.css" />
<link rel="stylesheet" href="https://unpkg.com/filepond-plugin-image-preview/dist/filepond-plugin-image-preview.css" />

Body Code

Place this in your page </body>


<!-- 💙 MEMBERSCRIPT #42 BODY CODE v0.2 💙 FILE EDITOR FEATURE -->
<script> src="https://unpkg.com/filepond-plugin-file-encode/dist/filepond-plugin-file-encode.js"> </script>
<script> src="https://unpkg.com/filepond-plugin-image-preview/dist/filepond-plugin-image-preview.js"> </script>
<script> src="https://unpkg.com/filepond-plugin-image-edit/dist/filepond-plugin-image-edit.js"> </script>
<script> src="https://unpkg.com/filepond@^4/dist/filepond.js"> </script>
<script> src="https://scaleflex.cloudimg.io/v7/plugins/filerobot-image-editor/latest/filerobot-image-editor.min.js"> </script>
<style>
  .dXhZSB {
  background-color: #2962ff;
  }
  .FIE_root * {
  font-family: inherit !important;
  }
  .SfxModal-Wrapper * {
  font-family: inherit !important;
  }
  .jpHEiD {
  font-family: inherit !important;
  }
  #editor_container {
  position: fixed;
  top: 0;
  left: 0;
  width: 100vw;
  height: 100vh;
  z-index: 999;
  }
</style>
<script>
document.addEventListener('DOMContentLoaded', function() {
  // Register the plugins
  FilePond.registerPlugin(FilePondPluginImagePreview);
  FilePond.registerPlugin(FilePondPluginImageEdit);

  const inputElement = document.querySelector('input[type="file"]');
  const pond = FilePond.create(inputElement, {
    credits: false,
    name: 'fileToUpload',
    storeAsFile: true,
    imageEditEditor: {
      open: (file, instructions) => {
        console.log('Open editor', file, instructions);
        openFilerobotImageEditor(file, instructions);
      },
      onconfirm: (output) => {
        console.log('Confirm editor', output);
        handleImageEditConfirm(output);
      },
      oncancel: () => {
        console.log('Cancel editor');
        handleImageEditCancel();
      },
      onclose: () => {
        console.log('Close editor');
        handleImageEditClose();
      }
    }
  });

  function openFilerobotImageEditor(file, instructions) {
    const imageURL = URL.createObjectURL(file);
    const config = {
      source: imageURL,
      onSave: (updatedImage) => {
        confirmCallback(updatedImage);
      },
      annotationsCommon: {
        fill: '#ff0000'
      },
      Text: {
        text: 'Add your text here',
        font: 'inherit'
      }, // Set font to inherit from the page body
      Rotate: {
        angle: instructions.rotation,
        componentType: 'slider'
      },
      tabsIds: [
        'Adjust',
        'Annotate',
        'Watermark'
      ],
      defaultTabId: 'Annotate',
      defaultToolId: 'Text'
    };

    const editorContainer = document.createElement('div');
    editorContainer.id = 'editor_container';
    document.body.appendChild(editorContainer);

    const filerobotImageEditor = new window.FilerobotImageEditor(editorContainer, config);

    const confirmCallback = (output) => {
      console.log('Confirmed:', output);

      const dataURL = output.imageBase64;
      const file = dataURLToFile(dataURL, output.name);

      // Add the file to FilePond
      pond.addFiles([file]);

      document.body.removeChild(editorContainer); // Remove the editor container
    };

    function dataURLToFile(dataURL, fileName) {
      const arr = dataURL.split(',');
      const mime = arr[0].match(/:(.*?);/)[1];
      const fileExtension = mime.split('/')[1];
      const updatedFileName = fileName + '.' + fileExtension;
      const bstr = atob(arr[1]);
      const n = bstr.length;
      const u8arr = new Uint8Array(n);
      for (let i = 0; i < n; i++) {
        u8arr[i] = bstr.charCodeAt(i);
      }
      return new File([u8arr], updatedFileName, { type: mime });
    }

    const cancelCallback = () => {
      console.log('Canceled');
      document.body.removeChild(editorContainer); // Remove the editor container
    };

    const closeButton = document.createElement('button');
    closeButton.textContent = 'Close';
    closeButton.addEventListener('click', () => {
      filerobotImageEditor.onClose();
    });

    const buttonContainer = document.createElement('div');
    buttonContainer.appendChild(closeButton);

    editorContainer.appendChild(buttonContainer);

    filerobotImageEditor.render({
      onClose: (closingReason) => {
        console.log('Closing reason', closingReason);
        filerobotImageEditor.terminate();
      },
    });
  }

  function handleImageEditConfirm(output) {
    console.log('Image edit confirmed:', output);
    // Handle the confirmed output here
  }

  function handleImageEditCancel() {
    console.log('Image edit canceled');
    // Handle the canceled edit here
  }

  function handleImageEditClose() {
    console.log('Image editor closed');
    // Handle the editor close here
  }
});
</script>

Custom Fields

#41 - Perfect Phone Number Inputs

International phone number inputs, the way they should be.

v0.2

With IP Lookup

Use this if you want the users' IP country to automatically be prefilled. IMPORTANT: Do not use this with profile forms or it will behave erratically.


<!-- 💙 MEMBERSCRIPT #41 v0.2 💙 PERFECT PHONE NUMBER INPUTS (WITH IP LOOKUP) -->
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/intl-tel-input/17.0.8/css/intlTelInput.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"> </script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/intl-tel-input/17.0.8/js/intlTelInput.min.js"> </script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/intl-tel-input/17.0.8/js/utils.js"> </script>
<script>
  $(document).ready(function() {
    $('input[ms-code-phone-number]').each(function() {
      var input = this;
      var preferredCountries = $(input).attr('ms-code-phone-number').split(',');

      var iti = window.intlTelInput(input, {
        preferredCountries: preferredCountries,
        utilsScript: "https://cdnjs.cloudflare.com/ajax/libs/intl-tel-input/17.0.8/js/utils.js"
      });

      $.get("https://ipinfo.io", function(response) {
        var countryCode = response.country;
        iti.setCountry(countryCode);
      }, "jsonp");

      input.addEventListener('change', formatPhoneNumber);
      input.addEventListener('keyup', formatPhoneNumber);

      function formatPhoneNumber() {
        var formattedNumber = iti.getNumber(intlTelInputUtils.numberFormat.INTERNATIONAL);
        input.value = formattedNumber;
      }

      var form = $(input).closest('form');
      form.submit(function() {
        var formattedNumber = iti.getNumber(intlTelInputUtils.numberFormat.INTERNATIONAL);
        input.value = formattedNumber;
      });
    });
  });
</script>

Without IP Lookup

Use this on profile forms and/or if you do not want to automatically prefill based on user IP.


<!-- 💙 MEMBERSCRIPT #41 v0.2 💙 PERFECT PHONE NUMBER INPUTS (WITHOUT IP LOOKUP) -->
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/intl-tel-input/17.0.8/css/intlTelInput.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"> </script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/intl-tel-input/17.0.8/js/intlTelInput.min.js"> </script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/intl-tel-input/17.0.8/js/utils.js"> </script>
<script>
  $(document).ready(function() {
    $('input[ms-code-phone-number]').each(function() {
      var input = this;
      var preferredCountries = $(input).attr('ms-code-phone-number').split(',');

      var iti = window.intlTelInput(input, {
        preferredCountries: preferredCountries,
        utilsScript: "https://cdnjs.cloudflare.com/ajax/libs/intl-tel-input/17.0.8/js/utils.js"
      });

      input.addEventListener('change', formatPhoneNumber);
      input.addEventListener('keyup', formatPhoneNumber);

      function formatPhoneNumber() {
        var formattedNumber = iti.getNumber(intlTelInputUtils.numberFormat.INTERNATIONAL);
        input.value = formattedNumber;
      }

      var form = $(input).closest('form');
      form.submit(function() {
        var formattedNumber = iti.getNumber(intlTelInputUtils.numberFormat.INTERNATIONAL);
        input.value = formattedNumber;
      });
    });
  });
</script>
Custom Fields
UX

#40 - Drag And Drop File Uploader

Easily add a drag n' drop file upload feature to your Webflow site!

v0.1

Important

If you are using MemberScript #38, make sure you put this script AFTER!


<!-- 💙 MEMBERSCRIPT #40 v0.1 💙 DRAG AND DROP FILE UPLOADER -->
<script> src="https://unpkg.com/filepond@^4/dist/filepond.js"> </script>
<script>
  document.addEventListener('DOMContentLoaded', function() {
    const inputElement = document.querySelector('input[type="file"]');
    const pond = FilePond.create(inputElement, {
      credits: false,
      name: 'fileToUpload',
      storeAsFile: true
      // for more property options, go to https://pqina.nl/filepond/docs/api/instance/properties/
    });
  });
</script>
Custom Fields
UX

#39 - Better Select Fields

Add searches and a better UI to select & multi-select fields!

v0.1

Head Code

Put this in the <head> section of your page.


<!-- 💙 MEMBERSCRIPT #39 v0.1 HEAD CODE 💙 BETTER SELECT FIELDS -->
<script> src="https://code.jquery.com/jquery-3.7.0.min.js" integrity="sha256-2Pmvv0kuTBOenSvLm6bvfBSSHrUJ+3A7x6P5Ebd07/g=" crossorigin="anonymous"> </script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" />

Body Code

Put this in the </body> section of your page.


<!-- 💙 MEMBERSCRIPT #39 v0.1 BODY CODE 💙 BETTER SELECT FIELDS -->
<script> src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"> </script>
<script>
$(document).ready(function() {
    $('[ms-code-custom-select="select-with-search"]').select2();
});
</script>
Custom Fields

#38 - File Upload Field

Add a file uploader to any site and send the submission to Google Drive, email, or anywhere you want.

v0.1

<!-- 💙 MEMBERSCRIPT #38 v0.1 💙 FORM FILE UPLOADER -->
<script>
const forms = document.querySelectorAll('form[ms-code-file-upload="form"]');

forms.forEach((form) => {
  form.setAttribute('enctype', 'multipart/form-data');
  const uploadInputs = form.querySelectorAll('[ms-code-file-upload-input]');

  uploadInputs.forEach((uploadInput) => {
    const inputName = uploadInput.getAttribute('ms-code-file-upload-input');

    const fileInput = document.createElement('input');
    fileInput.setAttribute('type', 'file');
    fileInput.setAttribute('name', inputName);
    fileInput.setAttribute('id', inputName);
    fileInput.required = true; // delete this line to make the input optional

    uploadInput.appendChild(fileInput);
  });
});
</script>
JSON
Marketing

#37 - Automatically Remove Free Plan

Automatically remove a free plan after a set amount of time!

v0.1

<!-- 💙 MEMBERSCRIPT #37 v0.1 💙 MAKE FREE TRIAL EXPIRE AFTER SET TIME -->
<script>
let memberPlanId = "your_plan_ID"; // replace with your actual FREE plan ID

document.addEventListener("DOMContentLoaded", async function() {
  const memberstack = window.$memberstackDom;

  // Fetch the member's data
  const member = await memberstack.getMemberJSON();

  // Fetch the member's planConnections from local storage
  const memberDataFromLocalStorage = JSON.parse(localStorage.getItem('_ms-mem'));
  const planConnections = memberDataFromLocalStorage.planConnections;

  // Check if the member has x plan
  let hasPlan = false;
  if (planConnections) {
    hasPlan = planConnections.some(planConnection => planConnection.planId === memberPlanId);
  }

  if (hasPlan) {
    // Check the members one-time-date
    let currentDate = new Date();
    let oneTimeDate = new Date(member.data['one-time-date']);

    if (currentDate > oneTimeDate) {
      // If the members' one time date has passed, remove x plan
      memberstack.removePlan({
        planId: memberPlanId
      }).then(() => {
        // Redirect to /free-trial-expired
        window.location.href = "/free-trial-expired";
      }).catch(error => {
        // Handle error
      });
    }
  }
});
</script>
Conditional Visibility
UX

#36 - Password Validation

Use this simple method to confirm your members have entered a strong password.

v0.1

<!-- 💙 MEMBERSCRIPT #36 v0.1 💙 PASSWORD VALIDATION -->
<script>
window.addEventListener('load', function() {
    const passwordInput = document.querySelector('input[data-ms-member="password"]');
    const submitButton = document.querySelector('[ms-code-submit-button]');
    
    if (!passwordInput || !submitButton) return;  // Return if essential elements are not found

    function checkAllValid() {
        const validationPoints = document.querySelectorAll('[ms-code-pw-validation]');
        return Array.from(validationPoints).every(validationPoint => {
            const validIcon = validationPoint.querySelector('[ms-code-pw-validation-icon="true"]');
            return validIcon && validIcon.style.display === 'flex';  // Check for validIcon existence before accessing style
        });
    }

    passwordInput.addEventListener('keyup', function() {
        const password = passwordInput.value;
        const validationPoints = document.querySelectorAll('[ms-code-pw-validation]');
        
        validationPoints.forEach(function(validationPoint) {
            const rule = validationPoint.getAttribute('ms-code-pw-validation');
            let isValid = false;
            
            // MINIMUM LENGTH VALIDATION POINT
            if (rule.startsWith('minlength-')) {
                const minLength = parseInt(rule.split('-')[1]);
                isValid = password.length >= minLength;
            }

            // SPECIAL CHARACTER VALIDATION POINT
            else if (rule === 'special-character') {
                isValid = /[!@#$%^&*(),.?":{}|<>]/g.test(password);
            }

            // UPPER AND LOWER CASE VALIDATION POINT
            else if (rule === 'upper-lower-case') {
                isValid = /[a-z]/.test(password) && /[A-Z]/.test(password);
            }

            // NUMBER VALIDATION POINT
            else if (rule === 'number') {
                isValid = /\d/.test(password);
            }
            
            const validIcon = validationPoint.querySelector('[ms-code-pw-validation-icon="true"]');
            const invalidIcon = validationPoint.querySelector('[ms-code-pw-validation-icon="false"]');
            
            if (validIcon && invalidIcon) {  // Check for existence before accessing style
                if (isValid) {
                    validIcon.style.display = 'flex';
                    invalidIcon.style.display = 'none';
                } else {
                    validIcon.style.display = 'none';
                    invalidIcon.style.display = 'flex';
                }
            }
        });

        if (checkAllValid()) {
            submitButton.classList.remove('disabled');
        } else {
            submitButton.classList.add('disabled');
        }
    });

    // Trigger keyup event after adding event listener
    var event = new Event('keyup');
    passwordInput.dispatchEvent(event);
});
</script>
SEO

#35 - Easily Add FAQ Schema/Rich Snippets

Add one script and 2 attributes to enable constantly updating rich snippets on your page.

v0.1

<!-- 💙 MEMBERSCRIPT #35 v0.1 💙 FAQ RICH SNIPPETS -->
<script>
let faqArray = [];
let questionElements = document.querySelectorAll('[ms-code-snippet-q]');
let answerElements = document.querySelectorAll('[ms-code-snippet-a]');

for (let i = 0; i < questionElements.length; i++) {
  let question = questionElements[i].innerText;
  let answer = '';

  for (let j = 0; j < answerElements.length; j++) {
    if (questionElements[i].getAttribute('ms-code-snippet-q') === answerElements[j].getAttribute('ms-code-snippet-a')) {
      answer = answerElements[j].innerText;
      break;
    }
  }

  faqArray.push({
    "@type": "Question",
    "name": question,
    "acceptedAnswer": {
      "@type": "Answer",
      "text": answer
    }
  });
}

let faqSchema = {
  "@context": "https://schema.org",
  "@type": "FAQPage",
  "mainEntity": faqArray
}

let script = document.createElement('script');
script.type = "application/ld+json";
script.innerHTML = JSON.stringify(faqSchema);

document.getElementsByTagName('head')[0].appendChild(script);
</script>
UX

#34 - Require Business Email For Form Submission

Block people from submitting a form if their email uses a personal email such as gmail.

v0.1

<!-- 💙 MEMBERSCRIPT #34 v0.1 💙 REQUIRE BUSINESS EMAILS -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"> </script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/parsley.js/2.9.2/parsley.min.js"> </script>
<script>
function isPersonalEmail(email) {
  var personalDomains = [
    "gmail.com", 
    "yahoo.com", 
    "hotmail.com", 
    "aol.com", 
    "msn.com", 
    "comcast.net", 
    "live.com", 
    "outlook.com", 
    "ymail.com",
    "icloud.com"
  ];
  var emailDomain = email.split('@')[1];
  return personalDomains.includes(emailDomain);
}

window.Parsley.addValidator('businessEmail', {
  validateString: function(value) {
    return !isPersonalEmail(value);
  },
  messages: {
    en: 'Please enter a business email.'
  }
});

$(document).ready(function() {
  $('form[ms-code-validate-form]').attr('data-parsley-validate', '');
  $('input[ms-code-business-email]').attr('data-parsley-business-email', '');
  $('form').parsley();
});
  $('form').parsley().on('form:error', function() {
  $('.parsley-errors-list').addClass('ms-code-validation-error');
});
</script>
Conditional Visibility
UX

#33 - Automatically Format Form Inputs

Force form inputs to follow a set format, such as DD/MM/YYYY.

v0.2

<!-- 💙 MEMBERSCRIPT #33 v0.2 💙 AUTOMATICALLY FORMAT FORM INPUTS -->
<script src="https://cdn.jsdelivr.net/npm/cleave.js@1.6.0"> </script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/cleave.js/1.6.0/addons/cleave-phone.us.js"> </script>
<script>
document.addEventListener('DOMContentLoaded', function(){
    // SELECT ALL ELEMENTS WITH THE ATTRIBUTE "ms-code-autoformat" OR "ms-code-autoformat-prefix"
    const elements = document.querySelectorAll('[ms-code-autoformat], [ms-code-autoformat-prefix]');

    for (let element of elements) {
        const formatType = element.getAttribute('ms-code-autoformat');
        const prefix = element.getAttribute('ms-code-autoformat-prefix');
        
        // SET PREFIX
        let cleaveOptions = {
            prefix: prefix || '',
            blocks: [Infinity]
        };
        
        // BASED ON THE VALUE OF "ms-code-autoformat", FORMAT THE INPUT
        if (formatType) {
            switch (formatType) {
                // FORMAT PHONE NUMBERS
                case 'phone-number':
                    cleaveOptions.phone = true;
                    cleaveOptions.phoneRegionCode = 'US';
                    break;
                    
                // FORMAT DATES IN 'YYYY-MM-DD' FORMAT
                case 'date-yyyy-mm-dd':
                    cleaveOptions.date = true;
                    cleaveOptions.datePattern = ['Y', 'm', 'd'];
                    break;
                    
                // FORMAT DATES IN 'MM-DD-YYYY' FORMAT
                case 'date-mm-dd-yyyy':
                    cleaveOptions.date = true;
                    cleaveOptions.datePattern = ['m', 'd', 'Y'];
                    break;
                    
                // FORMAT DATES IN 'DD-MM-YYYY' FORMAT
                case 'date-dd-mm-yyyy':
                    cleaveOptions.date = true;
                    cleaveOptions.datePattern = ['d', 'm', 'Y'];
                    break;
                    
                // FORMAT TIMES IN 'HH-MM-SS' FORMAT
                case 'time-hh-mm-ss':
                    cleaveOptions.time = true;
                    cleaveOptions.timePattern = ['h', 'm', 's'];
                    break;
                    
                // FORMAT TIMES IN 'HH-MM' FORMAT
                case 'time-hh-mm':
                    cleaveOptions.time = true;
                    cleaveOptions.timePattern = ['h', 'm'];
                    break;
                    
                // FORMAT NUMBERS WITH THOUSANDS SEPARATORS
                case 'number-thousand':
                    cleaveOptions.numeral = true;
                    cleaveOptions.numeralThousandsGroupStyle = 'thousand';
                    break;
            }
        }
        
        new Cleave(element, cleaveOptions);
    }
});
</script>
Conditional Visibility
Custom Fields

#32 - Set Input to Required if Visible

Create conditional forms by showing and hiding required inputs.

v0.1

<!-- 💙 MEMBERSCRIPT #32 v0.1 💙 REQUIRE INPUT IF VISIBLE -->
<script>
document.addEventListener("DOMContentLoaded", function() {

  // Function to check if an element is visible
  function isElementVisible(element) {
    return element.offsetParent !== null;
  }

  // Every time the user clicks on the document
  document.addEventListener('click', function() {

    // Get all inputs with the ms-code attribute
    const inputs = document.querySelectorAll('[ms-code="required-if-visible"]');

    // Loop through each input
    inputs.forEach(function(input) {

      // Check if the input or its parent is visible
      if (isElementVisible(input)) {

        // If the input is visible, add the required attribute
        input.required = true;
      } else {

        // If the input is not visible, remove the required attribute
        input.required = false;
      }
    });
  });
});
</script>
UX

#31 - Open a Webflow Tab with a Link

This script automatically generates links to your Webflow tabs.

v0.2

<!-- 💙 MEMBERSCRIPT #31 v0.2 💙 OPEN WEBFLOW TAB w/ LINK -->
<!-- You can link to tabs like this 👉 www.yoursite.com#tab-name-lowercase -->
<!-- And sub tabs like this 👉 www.yoursite.com#tab-name/sub-tab-name -->
<script>
  var Webflow = Webflow || [];
  Webflow.push(() => {
    function changeTab(shouldScroll = false) {
      const hashSegments = window.location.hash.substring(1).split('/');
      const offset = 90; // change this to match your fixed header height if you have one
      let lastTabTarget;

      for (const segment of hashSegments) {
        const tabTarget = document.querySelector(`[data-w-tab="${segment}"]`);

        if (tabTarget) {
          tabTarget.click();
          lastTabTarget = tabTarget;
        }
      }

      if (shouldScroll && lastTabTarget) {
        window.scrollTo({
          top: $(lastTabTarget).offset().top - offset, behavior: 'smooth'
        });
      }
    }
    
    const tabs = document.querySelectorAll('[data-w-tab]');
    
    tabs.forEach(tab => {
      const dataWTabValue = tab.dataset.wTab;
      const parsedDataTab = dataWTabValue.replace(/\s+/g,"-").toLowerCase();
      tab.dataset.wTab = parsedDataTab;
      tab.addEventListener('click', () => {
        history.pushState({}, '', `#${parsedDataTab}`);
      });
    });
 
  	if (window.location.hash) {
      requestAnimationFrame(() => { changeTab(true); });
    }

    window.addEventListener('hashchange', () => { changeTab() });
  });
</script>
UX

#30 - Count Items On Page And Update Number

Check how many items with a set attribute are on the page and apply that number to some text.

v0.1

<!-- 💙 MEMBERSCRIPT #30 v0.1 💙 COUNT ITEMS AND DISPLAY COUNT -->
<script>
document.addEventListener("DOMContentLoaded", function() {
  setTimeout(function() {
    const rollupItems = document.querySelectorAll('[ms-code-rollup-item]');
    const rollupNumbers = document.querySelectorAll('[ms-code-rollup-number]');

    const updateRollupNumbers = function() {
      const rollupCountMap = new Map();

      rollupItems.forEach(item => {
        const rollupKey = item.getAttribute('ms-code-rollup-item');
        const count = rollupCountMap.get(rollupKey) || 0;
        rollupCountMap.set(rollupKey, count + 1);
      });

      rollupNumbers.forEach(number => {
        const rollupKey = number.getAttribute('ms-code-rollup-number');
        const count = rollupCountMap.get(rollupKey) || 0;
        number.textContent = count;
      });
    };

    updateRollupNumbers(); // Initial update

    // Polling function to periodically update rollup numbers
    const pollRollupNumbers = function() {
      updateRollupNumbers();
      setTimeout(pollRollupNumbers, 1000); // Adjust the polling interval as needed (in milliseconds)
    };

    pollRollupNumbers(); // Start polling
  }, 2000);
});
</script>
UX

#29 - Temporarily Fix Element Height On Load

Force an element to be a set height for a certain duration on page load.

v0.1

<!-- 💙 MEMBERSCRIPT #29 v0.1 💙 TEMPORARILY FIX ELEMENT HEIGHT -->
<script>
document.addEventListener("DOMContentLoaded", function() {
  const elements = document.querySelectorAll('[ms-code-temp-height]');
  
  elements.forEach(element => {
    const attributeValue = element.getAttribute('ms-code-temp-height');
    
    if (attributeValue) {
      const [time, height] = attributeValue.split(':');
      
      if (!isNaN(time) && !isNaN(height)) {
        const defaultHeight = element.style.height;
        
        setTimeout(() => {
          element.style.height = defaultHeight;
        }, parseInt(time));
        
        element.style.height = height + 'px';
      }
    }
  });
});
</script>
Conditional Visibility
JSON

#28 - Display Element Based On JSON Date Passing

Check the one time date from #27 and show/hide an element based on that.

v0.1

<!-- 💙 MEMBERSCRIPT #28 v0.1 💙 CHECK ONE-TIME DATE AND UPDATE ELEMENT DISPLAY -->
<script>
document.addEventListener("DOMContentLoaded", async function() {
  const memberstack = window.$memberstackDom;

  const updateElementVisibility = async function() {
    const member = await memberstack.getMemberJSON();

    if (!member.data || !member.data['one-time-date']) {
      // Member data or expiration date not available, do nothing
      return;
    }

    const expirationDate = new Date(member.data['one-time-date']);
    const currentDate = new Date();

    if (currentDate < expirationDate) {
      // Expiration date has not passed, update element visibility
      const elements = document.querySelectorAll('[ms-code-element-temporary]');

      elements.forEach(element => {
        const displayValue = element.getAttribute('ms-code-element-temporary');

        // Update element visibility based on the attribute value
        element.style.display = displayValue;
      });
    }
  };

  updateElementVisibility();
});
</script>
Conditional Visibility
JSON

#27 - Set One Time Date On Signup

Apply a date to your member JSON after signup which can be used for anything.

v0.1.1

JSON Only

If you don't need to add the date to a custom field too, use this.


<!-- 💙 MEMBERSCRIPT #27 v0.1 💙 SET ONE TIME DATE -->
<script>
document.addEventListener("DOMContentLoaded", async function() {
  const memberstack = window.$memberstackDom;

  const updateDate = async function() {
    const member = await memberstack.getMemberJSON();

    if (!member.data) {
      member.data = {};
    }

    if (!member.data['one-time-date']) {
      const currentTime = new Date();
      const expirationTime = new Date(currentTime.getTime() + (3 * 60 * 60 * 1000)); // Set the expiration time to 3 hours (adjust as needed)
      member.data['one-time-date'] = expirationTime.toISOString();
      
      // Update member JSON
      await memberstack.updateMemberJSON({
        json: member.data
      });
    }
  };

  updateDate();
});
</script>

JSON + Custom Field

Use this if you need to add the date to a custom field (usually for use with automations).


<!-- 💙💙 MEMBERSCRIPT #27 v0.1.1 (CUSTOM FIELD) 💙 SET ONE TIME DATE -->
<script>
  document.addEventListener('DOMContentLoaded', async function() {
    const memberstack = window.$memberstackDom;
    const msMem = JSON.parse(localStorage.getItem('_ms-mem'));
    const member = await memberstack.getMemberJSON();

    if (!member.data) {
      member.data = {};
    }

    // Check if the user has the 'one-time-date' custom field in Memberstack
    if (!msMem.customFields || !msMem.customFields['one-time-date']) {
      const currentTime = new Date();
      const expirationTime = new Date(currentTime.getTime() + (3 * 60 * 60 * 1000)); // Set the expiration time to 3 hours (adjust as needed)
      const updatedCustomFields = {
        ...msMem.customFields,
        'one-time-date': expirationTime.toISOString()
      };

      member.data['one-time-date'] = expirationTime.toISOString();
      await memberstack.updateMemberJSON({
        json: member.data
      });

      await memberstack.updateMember({
        customFields: updatedCustomFields
      });
    }
  });
</script>
Conditional Visibility
UX
Marketing

#26 - Gate Content With Custom Modals

Use custom modals to urge your visitors to get a paid account!

v0.1

<!-- 💙 MEMBERSCRIPT #26 v0.1 💙 GATE CONTENT WITH MODALS -->
<script>
$memberstackDom.getCurrentMember().then(({ data }) => {
  if (!data) {
    // Member is not logged in
    const triggers = document.querySelectorAll('[ms-code-gate-modal-trigger]');
    const boxes = document.querySelectorAll('[ms-code-gate-modal-box]');
    
    triggers.forEach(trigger => {
      trigger.addEventListener('click', () => {
        const targetId = trigger.getAttribute('ms-code-gate-modal-trigger');
        const box = document.querySelector(`[ms-code-gate-modal-box="${targetId}"]`);
        if (box) {
          box.style.display = 'flex';
        }
      });
      // Remove links and attributes from trigger
      // Uncomment the lines below to enable this functionality
      // trigger.removeAttribute('href');
      // trigger.removeAttribute('target');
      // trigger.removeAttribute('rel');
      // trigger.removeAttribute('onclick');
    });
  }
});
</script>
Conditional Visibility
UX

#25 - Hide Element Based On Other Element's Children

Remove an element from the page if another defined element has no child elements.

v0.1

<!-- 💙 MEMBERSCRIPT #25 v0.1 💙 HIDE ELEMENT BASED ON OTHER ELEMENT CHILDREN -->
<script>
window.addEventListener('DOMContentLoaded', function() {
  const subjectAttribute = 'ms-code-visibility-subject';
  const targetAttribute = 'ms-code-visibility-target';

  const subjectElement = document.querySelector(`[${subjectAttribute}]`);
  const targetElement = document.querySelector(`[${targetAttribute}]`);

  if (!subjectElement || !targetElement) {
    console.error('Subject or target element not found');
    return;
  }

  function checkVisibility() {
    const children = subjectElement.children;
    let allHidden = true;

    for (let i = 0; i < children.length; i++) {
      const child = children[i];
      const computedStyle = window.getComputedStyle(child);

      if (computedStyle.display !== 'none') {
        allHidden = false;
        break;
      }
    }

    if (children.length === 0 || allHidden) {
      targetElement.style.display = 'none';
    } else {
      targetElement.style.display = '';
    }
  }

  // Check visibility initially
  checkVisibility();

  // Check visibility whenever the subject element or its children change
  const observer = new MutationObserver(checkVisibility);
  observer.observe(subjectElement, { childList: true, subtree: true });
});
</script>
Conditional Visibility

#24 - Filter Lists Based On Element

Filter any kind of list based on the presence of an element within it's children.

v0.1.1

Standard option

This works in most use cases.


<!-- 💙 MEMBERSCRIPT #24 v0.1 💙 FILTER ITEMS WITHIN LIST BASED ON ELEMENT -->
<script>
document.addEventListener("DOMContentLoaded", function() {
  const filterListItems = function(list, filterAttribute) {
    const items = list.querySelectorAll(`[ms-code-filter-item="${filterAttribute}"]`);

    items.forEach(item => {
      const target = item.querySelector(`[ms-code-filter-target="${filterAttribute}"]`);

      if (!target || window.getComputedStyle(target).display === 'none') {
        item.style.display = 'none';
      } else {
        item.style.display = '';
      }
    });
  };

  const filterLists = document.querySelectorAll('[ms-code-filter-list]');

  const updateFiltering = function() {
    filterLists.forEach(list => {
      const filterAttribute = list.getAttribute('ms-code-filter-list');
      filterListItems(list, filterAttribute);
    });
  };

  const observeListChanges = function() {
    const observer = new MutationObserver(updateFiltering);
    filterLists.forEach(list => observer.observe(list, { childList: true, subtree: true }));
  };

  updateFiltering();
  observeListChanges();
});
</script>

Polling option

If standard does not work, try this.


<!-- 💙 MEMBERSCRIPT #24 v0.1.1 💙 FILTER ITEMS WITHIN LIST BASED ON ELEMENT (POLLING) -->
<script>
window.addEventListener("DOMContentLoaded", function() {
  const filterListItems = function(list, filterAttribute) {
    const items = list.querySelectorAll(`[ms-code-filter-item="${filterAttribute}"]`);

    items.forEach(item => {
      const target = item.querySelector(`[ms-code-filter-target="${filterAttribute}"]`);

      if (!target || window.getComputedStyle(target).display === 'none') {
        item.style.display = 'none';
      } else {
        item.style.display = '';
      }
    });
  };

  const filterLists = document.querySelectorAll('[ms-code-filter-list]');

  const updateFiltering = function() {
    filterLists.forEach(list => {
      const filterAttribute = list.getAttribute('ms-code-filter-list');
      filterListItems(list, filterAttribute);
    });
  };

  const pollPage = function() {
    updateFiltering();
    setTimeout(pollPage, 1000); // Poll every 1 second
  };

  pollPage();
});
</script>
UX

#23 - Skeleton Screens / Content Loaders

Easily add these industry-standard loading states to your site in just a few seconds.

v0.1

Light mode

Use this on a white background


<!-- 💙 MEMBERSCRIPT #23 v0.1 💙 SKELETON SCREENS/CONTENT LOADERS -->
<script>
window.addEventListener("DOMContentLoaded", (event) => {
  const skeletonElements = document.querySelectorAll('[ms-code-skeleton]');
  
  skeletonElements.forEach(element => {
    // Create a skeleton div
    const skeletonDiv = document.createElement('div');
    skeletonDiv.classList.add('skeleton-loader');

    // Add the skeleton div to the current element
    element.style.position = 'relative';
    element.appendChild(skeletonDiv);

    // Get delay from the attribute
    let delay = element.getAttribute('ms-code-skeleton');

    // If attribute value is not a number, set default delay as 2000ms
    if (isNaN(delay)) {
      delay = 2000;
    }

    setTimeout(() => {
      // Remove the skeleton loader div after delay
      const skeletonDiv = element.querySelector('.skeleton-loader');
      element.removeChild(skeletonDiv);
    }, delay);
  });
});
</script>
<style>
.skeleton-loader {
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  right: 0;
  border-radius: inherit; /* Inherit the border-radius of the parent element */
  background: linear-gradient(to right, #f6f7f8 25%, #e0e0e0 50%, #f6f7f8 75%);
  background-size: 200% 100%;  /* Increase the size of the background image */
  z-index: 1; /* Make sure the skeleton loader is on top of the content */
  animation: skeleton 1s infinite linear;
}

@keyframes skeleton {
  0% { background-position: -100% 0; }
  100% { background-position: 100% 0; }
}

[ms-code-skeleton] {
  background-clip: padding-box;
}
</style>

Dark mode

Use this on a black background


<!-- 💙 MEMBERSCRIPT #23 v0.1 💙 SKELETON SCREENS/CONTENT LOADERS -->
<script>
window.addEventListener("DOMContentLoaded", (event) => {
  const skeletonElements = document.querySelectorAll('[ms-code-skeleton]');
  
  skeletonElements.forEach(element => {
    // Create a skeleton div
    const skeletonDiv = document.createElement('div');
    skeletonDiv.classList.add('skeleton-loader');

    // Add the skeleton div to the current element
    element.style.position = 'relative';
    element.appendChild(skeletonDiv);

    // Get delay from the attribute
    let delay = element.getAttribute('ms-code-skeleton');

    // If attribute value is not a number, set default delay as 2000ms
    if (isNaN(delay)) {
      delay = 2000;
    }

    setTimeout(() => {
      // Remove the skeleton loader div after delay
      const skeletonDiv = element.querySelector('.skeleton-loader');
      element.removeChild(skeletonDiv);
    }, delay);
  });
});
</script>
<style>
.skeleton-loader {
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  right: 0;
  border-radius: inherit;
  background: linear-gradient(to right, #222222 25%, #333333 50%, #222222 75%); /* Updated background colors */
  background-size: 200% 100%;
  z-index: 1;
  animation: skeleton 1s infinite linear;
}

@keyframes skeleton {
  0% { background-position: -100% 0; }
  100% { background-position: 100% 0; }
}

[ms-code-skeleton] {
  background-clip: padding-box;
}
</style>
Conditional Visibility
UX

#22 - Disable Submit Button Until Required Fields Are Done

Gray out your submit button until all required values have something in them.

v0.1

<!-- 💙 MEMBERSCRIPT #22 v0.1 💙 DISABLE SUBMIT BUTTON UNTIL REQUIRED FIELDS ARE COMPLETE -->
<script>
window.onload = function() {
  const forms = document.querySelectorAll('form[ms-code-submit-form]');

  forms.forEach(form => {
    const submitButton = form.querySelector('input[type="submit"]');
    const requiredFields = form.querySelectorAll('input[required]');

    form.addEventListener('input', function() {
      const allFilled = Array.from(requiredFields).every(field => field.value.trim() !== '');

      if (allFilled) {
        submitButton.classList.add('submit-enabled');
      } else {
        submitButton.classList.remove('submit-enabled');
      }
    });
  });
};
</script>
Conditional Visibility
UX

#21 - Custom Toast Notifications

Display custom toast boxes on element click!

v0.1

<!-- 💙 MEMBERSCRIPT #21 v0.1 💙 CUSTOM TOAST BOXES -->
<script>
document.addEventListener("DOMContentLoaded", function() {
  const toastTriggers = document.querySelectorAll("[ms-code-toast-trigger]");

  toastTriggers.forEach(trigger => {
    trigger.addEventListener("click", function() {
      const triggerId = trigger.getAttribute("ms-code-toast-trigger");
      const toastBox = document.querySelector(`[ms-code-toast-box="${triggerId}"]`);

      if (toastBox) {
        const fadeInDuration = 200;
        const fadeOutDuration = 200;
        const staticDuration = 2000;
        const totalDuration = fadeInDuration + staticDuration + fadeOutDuration;

        toastBox.style.opacity = "0";
        toastBox.style.display = "block";

        let currentTime = 0;

        const fade = function() {
          currentTime += 10;
          const opacity = currentTime < fadeInDuration
            ? currentTime / fadeInDuration
            : currentTime < fadeInDuration + staticDuration
              ? 1
              : 1 - (currentTime - fadeInDuration - staticDuration) / fadeOutDuration;

          toastBox.style.opacity = opacity;

          if (currentTime < totalDuration) {
            setTimeout(fade, 10);
          } else {
            toastBox.style.display = "none";
          }
        };

        fade();
      }
    });
  });
});
</script>
JSON

#20 - Save & Unsave CMS Items

Allow your members to like/unlike CMS items and save to their JSON!

v0.3

Loads immediately

This is faster, so if ONLY logged in members will be on the page, use this.


<!-- 💙 MEMBERSCRIPT #20 v0.3 💙 SAVE & UNSAVE CMS ITEMS -->
<script>
document.addEventListener("DOMContentLoaded", async function() {
  const memberstack = window.$memberstackDom;
  const member = await memberstack.getMemberJSON();

  const updateButtonVisibility = function(button, jsonGroup, memberData) {
    const itemId = button.getAttribute('ms-code-save-child');
    const savedItems = memberData && memberData[jsonGroup] ? memberData[jsonGroup] : [];
    const isItemSaved = savedItems.includes(itemId);

    const saveButton = button;
    const parentElement = button.closest('[ms-code-save]');
    const unsaveButtons = parentElement.querySelectorAll(`[ms-code-unsave-child="${itemId}"]`);

    unsaveButtons.forEach(unsaveButton => {
      if (isItemSaved) {
        saveButton.style.display = 'none';
        unsaveButton.style.display = 'block';
      } else {
        saveButton.style.display = 'block';
        unsaveButton.style.display = 'none';
      }
    });
  };

  const toggleLikeButton = async function(button, jsonGroup) {
    const itemId = button.getAttribute('ms-code-save-child');

    if (!member.data) {
      member.data = {};
    }

    if (!member.data[jsonGroup]) {
      member.data[jsonGroup] = [];
    }

    const isItemSaved = member.data[jsonGroup].includes(itemId);

    const parentElement = button.closest('[ms-code-save]');
    const unsaveButtons = parentElement.querySelectorAll(`[ms-code-unsave-child="${itemId}"]`);

    if (isItemSaved) {
      member.data[jsonGroup] = member.data[jsonGroup].filter(item => item !== itemId);
      button.style.display = 'block';
      unsaveButtons.forEach(unsaveButton => {
        unsaveButton.style.display = 'none';
      });
    } else {
      member.data[jsonGroup].push(itemId);
      button.style.display = 'none';
      unsaveButtons.forEach(unsaveButton => {
        unsaveButton.style.display = 'block';
      });
    }

    await memberstack.updateMemberJSON({
      json: member.data
    });

    updateButtonVisibility(button, jsonGroup, member.data);
  };

  const saveButtons = document.querySelectorAll('[ms-code-save-child]');
  saveButtons.forEach(button => {
    const jsonGroup = button.getAttribute('ms-code-save') || button.closest('[ms-code-save]').getAttribute('ms-code-save');
    updateButtonVisibility(button, jsonGroup, member.data);
    button.addEventListener('click', async function(event) {
      event.preventDefault();
      await toggleLikeButton(button, jsonGroup);
    });
  });

  const unsaveButtons = document.querySelectorAll('[ms-code-unsave-child]');
  unsaveButtons.forEach(button => {
    const jsonGroup = button.getAttribute('ms-code-save') || button.closest('[ms-code-save]').getAttribute('ms-code-save');
    button.addEventListener('click', async function(event) {
      event.preventDefault();
      const parentElement = button.closest('[ms-code-save]');
      const saveButton = parentElement.querySelector(`[ms-code-save-child="${button.getAttribute('ms-code-unsave-child')}"]`);
      await toggleLikeButton(saveButton, jsonGroup);
    });
  });
});
</script>

Loads after member object

If both logged in & logged out visitors will be on the page with this script, using this method will prevent errors.


<!-- 💙 MEMBERSCRIPT #20.1 v0.2 💙 SAVE & UNSAVE CMS ITEMS AFTER MEMBERSTACK LOAD -->
<script>
const memberstack = window.$memberstackDom;

const updateButtonVisibility = async function(button, jsonGroup) {
  const itemId = button.getAttribute('ms-code-save-child');
  const member = await memberstack.getMemberJSON();

  const savedItems = member.data && member.data[jsonGroup] ? member.data[jsonGroup] : [];
  const isItemSaved = savedItems.includes(itemId);

  const saveButton = button;
  const parentElement = button.closest('[ms-code-save]');
  const unsaveButtons = parentElement.querySelectorAll(`[ms-code-unsave-child="${itemId}"]`);

  unsaveButtons.forEach(unsaveButton => {
    if (isItemSaved) {
      saveButton.style.display = 'none';
      unsaveButton.style.display = 'block';
    } else {
      saveButton.style.display = 'block';
      unsaveButton.style.display = 'none';
    }
  });
};

const toggleLikeButton = async function(button, jsonGroup) {
  const itemId = button.getAttribute('ms-code-save-child');
  const member = await memberstack.getMemberJSON();

  if (!member.data) {
    member.data = {};
  }

  if (!member.data[jsonGroup]) {
    member.data[jsonGroup] = [];
  }

  const isItemSaved = member.data[jsonGroup].includes(itemId);

  const parentElement = button.closest('[ms-code-save]');
  const unsaveButtons = parentElement.querySelectorAll(`[ms-code-unsave-child="${itemId}"]`);

  if (isItemSaved) {
    member.data[jsonGroup] = member.data[jsonGroup].filter(item => item !== itemId);
    button.style.display = 'block';
    unsaveButtons.forEach(unsaveButton => {
      unsaveButton.style.display = 'none';
    });
  } else {
    member.data[jsonGroup].push(itemId);
    button.style.display = 'none';
    unsaveButtons.forEach(unsaveButton => {
      unsaveButton.style.display = 'block';
    });
  }

  await memberstack.updateMemberJSON({
    json: member.data
  });

  updateButtonVisibility(button, jsonGroup);
};

memberstack.getCurrentMember().then(({ data }) => {
  if (data) {
    // Member is logged in
    const saveButtons = document.querySelectorAll('[ms-code-save-child]');

    saveButtons.forEach(button => {
      const jsonGroup = button.getAttribute('ms-code-save') || button.closest('[ms-code-save]').getAttribute('ms-code-save');
      updateButtonVisibility(button, jsonGroup);
      button.addEventListener('click', async function(event) {
        event.preventDefault();
        await toggleLikeButton(button, jsonGroup);
      });
    });

    const unsaveButtons = document.querySelectorAll('[ms-code-unsave-child]');

    unsaveButtons.forEach(button => {
      const jsonGroup = button.getAttribute('ms-code-save') || button.closest('[ms-code-save]').getAttribute('ms-code-save');
      button.addEventListener('click', async function(event) {
        event.preventDefault();
        const parentElement = button.closest('[ms-code-save]');
        const saveButton = parentElement.querySelector(`[ms-code-save-child="${button.getAttribute('ms-code-unsave-child')}"]`);
        await toggleLikeButton(saveButton, jsonGroup);
      });
    });
  } else {
    // If member is not logged in
  }
});
</script>
Custom Fields

#19 - Add URL From Custom Field To IFrame SRC

Create a member-specific embed functionality with this custom field iframe solution!

v0.1

<!-- 💙 MEMBERSCRIPT #19 v0.1 💙 ADD CUSTOM FIELD AS AN IFRAME SRC -->
<script>
document.addEventListener("DOMContentLoaded", function() {
  // Parse member data from local storage
  const memberData = JSON.parse(localStorage.getItem('_ms-mem') || '{}');

  // Check if the user is logged in
  if(memberData && memberData.id) {
    // Get custom fields
    const customFields = memberData.customFields;

    // Select all elements with 'ms-code-field-link' attribute
    const elements = document.querySelectorAll('[ms-code-field-link]');
    
    // Iterate over all selected elements
    elements.forEach(element => {
      // Get custom field key from 'ms-code-field-link' attribute
      const fieldKey = element.getAttribute('ms-code-field-link');
      
      // If key exists in custom fields, set element src to the corresponding value
      if(customFields.hasOwnProperty(fieldKey)) {
        element.setAttribute('src', customFields[fieldKey]);
      }
    });
  }
});
</script>
Marketing

#18 - Easily Truncate Text

Add one attribute and a simple script to programatically truncate text!

v0.2

<!-- 💙 MEMBERSCRIPT #18 v0.2 💙 - EASILY TRUNCATE TEXT -->
<script>
const elements = document.querySelectorAll('[ms-code-truncate]');

elements.forEach((element) => {
  const charLimit = parseInt(element.getAttribute('ms-code-truncate'));

  // Create a helper function that will recursively traverse the DOM tree
  const traverseNodes = (node, count) => {
    for (let child of node.childNodes) {
      // If the node is a text node, truncate if necessary
      if (child.nodeType === Node.TEXT_NODE) {
        if (count + child.textContent.length > charLimit) {
          child.textContent = child.textContent.slice(0, charLimit - count) + '...';
          return count + child.textContent.length;
        }
        count += child.textContent.length;
      }
      // If the node is an element, recurse through its children
      else if (child.nodeType === Node.ELEMENT_NODE) {
        count = traverseNodes(child, count);
      }
    }
    return count;
  }

  // Create a deep clone of the element to work on. This is so that we don't modify the original element
  // until we have completely finished processing.
  const clone = element.cloneNode(true);

  // Traverse and truncate the cloned node
  traverseNodes(clone, 0);

  // Replace the original element with our modified clone
  element.parentNode.replaceChild(clone, element);
});
</script>
Custom Fields

#17 - Populate Link From Custom Field

Use custom fields to populate link targets with one attribute!

v0.2

<!-- 💙 MEMBERSCRIPT #17 v0.2 💙 ADD CUSTOM FIELD AS A LINK -->
<script>
  document.addEventListener("DOMContentLoaded", function() {
    // Parse member data from local storage
    const memberData = JSON.parse(localStorage.getItem('_ms-mem') || '{}');

    // Check if the user is logged in
    if (memberData && memberData.id) {
      // Get custom fields
      const customFields = memberData.customFields;

      // Select all elements with 'ms-code-field-link' attribute
      const elements = document.querySelectorAll('[ms-code-field-link]');

      // Iterate over all selected elements
      elements.forEach(element => {
        // Get custom field key from 'ms-code-field-link' attribute
        const fieldKey = element.getAttribute('ms-code-field-link');

        // If key exists in custom fields
        if (customFields.hasOwnProperty(fieldKey)) {
          // Get the custom field value
          const fieldValue = customFields[fieldKey];

          // Check if the custom field value is empty
          if (fieldValue.trim() === '') {
            // Set the element to display none
            element.style.display = 'none';
          } else {
            // Check if the link has a protocol, if not, add http://
            let link = fieldValue;
            if (!/^https?:\/\//i.test(link)) {
              link = 'http://' + link;
            }

            // Set the element href to the corresponding value
            element.setAttribute('href', link);
          }
        } else {
          // Set the element to display none if the custom field key doesn't exist
          element.style.display = 'none';
        }
      });
    }
  });
</script>
JSON

#16 - End Session After X Minutes of Inactivity

This script redirects inactive users to a "Session Expired" page after a certain period of inactivity. A countdown time is displayed on the page and resets with page activity.

v0.2

Add this code before the closing </body> tag any page which needs a countdown timer.


<!-- 💙 MEMBERSCRIPT #16 v0.2 💙 LOGOUT AFTER X MINUTES OF INACTIVITY -->
<script>
let logoutTimer;
let countdownInterval;
let initialTime;

function startLogoutTimer() {
  // Get the logout time from the HTML element
  const timeElement = document.querySelector('[ms-code-logout-timer]');
  const timeParts = timeElement.textContent.split(':');
  const minutes = parseInt(timeParts[0], 10);
  const seconds = parseInt(timeParts[1], 10);
  const LOGOUT_TIME = (minutes * 60 + seconds) * 1000; // Convert to milliseconds

  // Store the initial time value
  if (!initialTime) {
    initialTime = LOGOUT_TIME;
  }

  // Clear the previous timer, if any
  clearTimeout(logoutTimer);
  clearInterval(countdownInterval);

  let startTime = Date.now();

  // Start a new timer to redirect the user after the specified time
  logoutTimer = setTimeout(() => {
    window.location.href = "/expired";
    startLogoutTimer(); // Start the logout timer again
  }, LOGOUT_TIME); 

  // Start a countdown timer
  countdownInterval = setInterval(() => {
    let elapsed = Date.now() - startTime;
    let remaining = LOGOUT_TIME - elapsed;
    updateCountdownDisplay(remaining);
  }, 1000); // update every second
}

function updateCountdownDisplay(remainingTimeInMs) {
  // convert ms to MM:SS format
  let minutes = Math.floor(remainingTimeInMs / 1000 / 60);
  let seconds = Math.floor((remainingTimeInMs / 1000) % 60);
  minutes = minutes < 10 ? "0" + minutes : minutes;
  seconds = seconds < 10 ? "0" + seconds : seconds;

  const timeElement = document.querySelector('[ms-code-logout-timer]');
  timeElement.textContent = `${minutes}:${seconds}`;
}

// Call this function whenever the user interacts with the page
function resetLogoutTimer() {
  const timeElement = document.querySelector('[ms-code-logout-timer]');
  timeElement.textContent = formatTime(initialTime); // Reset to the initial time
  startLogoutTimer();
}

function formatTime(timeInMs) {
  let minutes = Math.floor(timeInMs / 1000 / 60);
  let seconds = Math.floor((timeInMs / 1000) % 60);
  minutes = minutes < 10 ? "0" + minutes : minutes;
  seconds = seconds < 10 ? "0" + seconds : seconds;
  return `${minutes}:${seconds}`;
}

// Call this function when the user logs in
function cancelLogoutTimer() {
  clearTimeout(logoutTimer);
  clearInterval(countdownInterval);
}

// Start the timer when the page loads
startLogoutTimer();

// Add event listeners to reset timer on any page activity
document.addEventListener('mousemove', resetLogoutTimer);
document.addEventListener('keypress', resetLogoutTimer);
document.addEventListener('touchstart', resetLogoutTimer);

</script>

Add this code to your /expired page before the closing </body> tag.


<!-- 💙 MEMBERSCRIPT #16 v0.2 💙 LOGOUT AFTER X MINUTES OF INACTIVITY -->
<script>
window.addEventListener('load', () => {
  window.$memberstackDom.getCurrentMember().then(async ({ data: member }) => {   
    if (member) {
      try {
        await window.$memberstackDom.logout();
        console.log('Logged out successfully');
        
        setTimeout(() => {
          location.reload();
        }, 1000); // Refresh the page 1 second after logout

      } catch (error) {
        console.error(error);
      }
    }
  });
});
</script>
UX

#15 - Refresh Page After Set Duration On Click

Refresh the page after a set amount of time when an element is clicked.

v0.1

<!-- 💙 MEMBERSCRIPT #15 v0.1 💙 TRIGGER REFRESH ON CLICK -->
<script>
document.addEventListener("DOMContentLoaded", function() {
  document.addEventListener("click", function(event) {
    const clickedElement = event.target.closest('[ms-code-refresh]');

    if (clickedElement) {
      const refreshDelay = parseInt(clickedElement.getAttribute('ms-code-refresh'));

      if (!isNaN(refreshDelay)) {
        setTimeout(function() {
          location.reload();
        }, refreshDelay);
      }
    }
  });
});
</script>
UX

#14 - Create Loading State On Click

Simulate a custom loading state when an element is clicked.

v0.1

<!-- 💙 MEMBERSCRIPT #14 v0.1 💙 CREATE LOADING STATE ON CLICK -->
<script src="https://code.jquery.com/jquery-3.7.0.min.js" integrity="sha256-2Pmvv0kuTBOenSvLm6bvfBSSHrUJ+3A7x6P5Ebd07/g=" crossorigin="anonymous" ></script>
<script>
$(document).ready(function() {

    // Bind click event to elements with ms-code-loading-trigger attribute
    $(document).on('click', '[ms-code-loading-trigger]', function() {
        // Get the value of ms-code-loading-trigger attribute
        var triggerValue = $(this).attr("ms-code-loading-trigger");

        // Find the matching loading element
        var loadingElement = $("[ms-code-loading-element='" + triggerValue + "']");
        
        // Find the matching subject element
        var subjectElement = $("[ms-code-loading-subject='" + triggerValue + "']");

        // Show the loading element (with flex display), append it to the subject element
        loadingElement.css('display', 'flex').appendTo(subjectElement);

        // After 5 seconds (5000 milliseconds), hide the loading element
        setTimeout(function() {
            loadingElement.hide();
        }, 5000);
    });
});
</script>
JSON
Conditional Visibility

#13 - Filter JSON Item Groups

Show filtered lists to your members based on a JSON property!

v0.2

<!-- 💙 MEMBERSCRIPT #13 v0.2 💙 FILTER ITEM GROUPS FROM JSON BASED ON ATTRIBUTE VALUE -->
<script>
(function() {
  const memberstack = window.$memberstackDom;
  let member;

  const fetchMemberJSON = async function() {
    member = await memberstack.getMemberJSON();
    return member;
  }

  const filterItems = async function(printList) {
    const filterAttr = printList.getAttribute('ms-code-list-filter');
    if (!filterAttr) return;

    const filters = filterAttr.split(',').map(filter => filter.trim());
    const jsonGroup = printList.getAttribute('ms-code-print-list');
    const items = member.data && member.data[jsonGroup] ? Object.values(member.data[jsonGroup]) : [];

    const itemContainer = document.createElement('div');
    const placeholder = printList.querySelector(`[ms-code-print-item="${jsonGroup}"]`);
    const itemTemplate = placeholder.outerHTML;

    items.forEach(item => {
      const newItem = document.createElement('div');
      newItem.innerHTML = itemTemplate;
      const itemElements = newItem.querySelectorAll('[ms-code-item-text]');

      let skipItem = false;
      filters.forEach(filter => {
        const exclude = filter.startsWith('!');
        const filterKey = exclude ? filter.substring(1) : filter;
        if ((exclude && item.hasOwnProperty(filterKey)) || (!exclude && !item.hasOwnProperty(filterKey))) {
          skipItem = true;
        }
      });

      if (skipItem) return; // Skip this item

      itemElements.forEach(itemElement => {
        const jsonKey = itemElement.getAttribute('ms-code-item-text');
        const value = item && item[jsonKey] ? item[jsonKey] : '';
        itemElement.textContent = value;
      });

      const itemKey = Object.keys(member.data[jsonGroup]).find(k => member.data[jsonGroup][k] === item);
      newItem.firstChild.setAttribute('ms-code-item-key', itemKey);

      itemContainer.appendChild(newItem.firstChild);
    });

    printList.innerHTML = itemContainer.innerHTML;
  };

  // Fetch member JSON
  let intervalId = setInterval(async () => {
    member = await fetchMemberJSON();
    if (member && member.data) {
      clearInterval(intervalId);
      const printLists = document.querySelectorAll('[ms-code-print-list]');
      printLists.forEach(filterItems);
    }
  }, 500);

  // Add click event listener to elements with ms-code-update="json"
  const updateButtons = document.querySelectorAll('[ms-code-update="json"]');
  updateButtons.forEach(button => {
    button.addEventListener("click", async function() {
      // Fetch member JSON on each click
      let intervalIdClick = setInterval(async () => {
        member = await fetchMemberJSON();
        if (member && member.data) {
          clearInterval(intervalIdClick);
          const printLists = document.querySelectorAll('[ms-code-print-list]');
          printLists.forEach(filterItems);
        }
      }, 500);
    });
  });
})();
</script>
JSON

#12 - Add Items To JSON Groups On Click

Add an item/property to previously created JSON items with one click!

v0.1

<!-- 💙 MEMBERSCRIPT #12 v0.1 💙 ADD ITEM TO JSON GROUP ON CLICK -->
<script>
  document.addEventListener("DOMContentLoaded", function() {
    const memberstack = window.$memberstackDom;

    // Add click event listener to the document
    document.addEventListener("click", async function(event) {
      const target = event.target;

      // Check if the clicked element has ms-code-update-json-item attribute
      const updateJsonItem = target.closest('[ms-code-update-json-item]');
      if (updateJsonItem) {
        const key = updateJsonItem.closest('[ms-code-item-key]').getAttribute('ms-code-item-key');
        const jsonGroup = updateJsonItem.closest('[ms-code-print-list]').getAttribute('ms-code-print-list');

        // Retrieve the current member JSON data
        const member = await memberstack.getMemberJSON();

        if (member.data && member.data[jsonGroup] && member.data[jsonGroup][key]) {
          // Get the value to be added to the item in the member JSON
          const updateValue = updateJsonItem.getAttribute('ms-code-update-json-item');
          // Update the member JSON with the new value
          member.data[jsonGroup][key][updateValue] = true;

          // Update the member JSON using the Memberstack SDK
          await memberstack.updateMemberJSON({
            json: member.data
          });

          // Optional: Display a success message or perform any other desired action
          console.log('Member JSON updated successfully');
        } else {
          console.error(`Could not find item with key: ${key}`);
        }
      }

      // Check if the clicked element has ms-code-update attribute
      const updateButton = target.closest('[ms-code-update="json"]');
      if (updateButton) {
        // Add a delay
        await new Promise(resolve => setTimeout(resolve, 500));

        // Retrieve the current member JSON data
        const member = await memberstack.getMemberJSON();

        // Save the member JSON as a local storage item
        localStorage.setItem("memberJSON", JSON.stringify(member));

        // Optional: Display a success message or perform any other desired action
        console.log('Member JSON saved to local storage');
      }
    });
  });
</script>
Conditional Visibility

#11 - Automatically Open Login Modal

Show all logged-out visitors a login modal immediately on page visit. Both custom and default Memberstack work.

v0.2

<!-- 💙 MEMBERSCRIPT #11 v0.1 💙 SHOW LOGIN MODAL IF MEMBER IS NOT LOGGED IN -->

<!-- KEEP THIS FOR DEFAULT MEMBERSTACK MODAL -->
<script>
function handleRedirect(redirect) {
  if (redirect && (window.location.pathname !== redirect)) return window.location.href = redirect;
  window.location.reload()
}

$memberstackDom.getCurrentMember().then(async ({
  data
}) => {
  if (!data) {
    const {
      data
    } = await $memberstackDom.openModal("login");
    handleRedirect(data.redirect)
  }
})
</script>

<!-- KEEP THIS FOR YOUR OWN CUSTOM MODAL -->
<script>
$memberstackDom.getCurrentMember().then(({ data }) => {
  if (!data) {
    const loginModal = document.querySelector('[ms-code-modal="login"]');
    if (loginModal) {
      loginModal.style.display = "flex";
    }
  }
});
</script>
Conditional Visibility

#10 - Display/Remove Element based on Custom Field

Check if a member has filled out a custom field. If yes, set the target element to display.

v0.2

<!-- 💙 MEMBERSCRIPT #10 v0.2 💙 HIDE ELEMENTS IF CUSTOM FIELD IS BLANK -->
<script>
document.addEventListener('DOMContentLoaded', function() {
  // Get the `_ms-mem` object from the local storage
  const msMem = JSON.parse(localStorage.getItem('_ms-mem'));

  // Get all the elements that have the `ms-code-customfield` attribute
  const elements = document.querySelectorAll('[ms-code-customfield]');

  // Iterate over each element
  elements.forEach(element => {
    // Get the value of the `ms-code-customfield` attribute
    const customField = element.getAttribute('ms-code-customfield');

    // If customField starts with '!', we invert the logic
    if (customField.startsWith('!')) {
      const actualCustomField = customField.slice(1); // remove the '!' from the start

      // If the custom field is empty, remove the element from the DOM
      if (msMem.customFields && msMem.customFields[actualCustomField]) {
        element.parentNode.removeChild(element);
      }
    } else {
      // Check if the user has the corresponding custom field in Memberstack
      if (!msMem.customFields || !msMem.customFields[customField]) {
        // If the custom field is empty, remove the element from the DOM
        element.parentNode.removeChild(element);
      }
    }
  });
});
</script>
Marketing

#9 - Real, User Based Countdown

Dynamically set a per-user countdown time and then hide elements when time is up.

v0.1

<!-- 💙 MEMBERSCRIPT #9 v0.1 💙 COUNTDOWN BY USER -->
<script>
// Step 1: Get the current date and time
const currentDate = new Date();

// Step 2: Calculate the new date and time based on the attribute values
const addTime = (date, unit, value) => {
  const newDate = new Date(date);
  switch (unit) {
    case 'hour':
      newDate.setHours(newDate.getHours() + value);
      break;
    case 'minute':
      newDate.setMinutes(newDate.getMinutes() + value);
      break;
    case 'second':
      newDate.setSeconds(newDate.getSeconds() + value);
      break;
    case 'millisecond':
      newDate.setMilliseconds(newDate.getMilliseconds() + value);
      break;
  }
  return newDate;
};

// Retrieve attribute values and calculate the new date and time
const hourAttr = document.querySelector('[ms-code-time-hour]');
const minuteAttr = document.querySelector('[ms-code-time-minute]');
const secondAttr = document.querySelector('[ms-code-time-second]');
const millisecondAttr = document.querySelector('[ms-code-time-millisecond]');

const countdownDateTime = localStorage.getItem('countdownDateTime');
let newDate;

if (countdownDateTime) {
  newDate = new Date(countdownDateTime);
} else {
  newDate = currentDate;
  if (hourAttr.hasAttribute('ms-code-time-hour')) {
    const hours = parseInt(hourAttr.getAttribute('ms-code-time-hour'));
    newDate = addTime(newDate, 'hour', isNaN(hours) ? 0 : hours);
  }
  if (minuteAttr.hasAttribute('ms-code-time-minute')) {
    const minutes = parseInt(minuteAttr.getAttribute('ms-code-time-minute'));
    newDate = addTime(newDate, 'minute', isNaN(minutes) ? 0 : minutes);
  }
  if (secondAttr.hasAttribute('ms-code-time-second')) {
    const seconds = parseInt(secondAttr.getAttribute('ms-code-time-second'));
    newDate = addTime(newDate, 'second', isNaN(seconds) ? 0 : seconds);
  }
  if (millisecondAttr.hasAttribute('ms-code-time-millisecond')) {
    const milliseconds = parseInt(millisecondAttr.getAttribute('ms-code-time-millisecond'));
    newDate = addTime(newDate, 'millisecond', isNaN(milliseconds) ? 0 : milliseconds);
  }

  localStorage.setItem('countdownDateTime', newDate);
}

// Step 4: Update the text of the elements to show the continuous countdown
const countdownElements = [hourAttr, minuteAttr, secondAttr, millisecondAttr];

const updateCountdown = () => {
  const currentTime = new Date();
  const timeDifference = newDate - currentTime;

  if (timeDifference > 0) {
    const timeParts = [
      Math.floor(timeDifference / (1000 * 60 * 60)) % 24, // Hours
      Math.floor(timeDifference / (1000 * 60)) % 60, // Minutes
      Math.floor(timeDifference / 1000) % 60, // Seconds
      Math.floor(timeDifference) % 1000, // Milliseconds
    ];

    // Update the text of the elements with the countdown values
    countdownElements.forEach((element, index) => {
      const timeValue = timeParts[index];
      if (index === 3) {
        element.innerText = timeValue.toString().padStart(2, '0').slice(0, 2); // Display only two digits for milliseconds
      } else {
        element.innerText = timeValue < 10 ? `0${timeValue}` : timeValue;
      }
    });

    // Update the countdown every 10 milliseconds
    setTimeout(updateCountdown, 10);
  } else {
    // Countdown has reached zero or has passed
    countdownElements.forEach((element) => {
      element.innerText = '00';
    });

    // Remove elements with ms-code-countdown="hide-on-end" attribute
    const hideOnEndElements = document.querySelectorAll('[ms-code-countdown="hide-on-end"]');
    hideOnEndElements.forEach((element) => {
      element.remove();
    });

    // Optionally, you can perform additional actions or display a message when the countdown ends
  }
};

// Start the countdown
updateCountdown();
</script>
Marketing

#8 - Copy To Clipboard

Allow visitors to copy things such as coupon codes to their clipboard with just one click.

v0.1

<!-- 💙 MEMBERSCRIPT #8 v0.1 💙 SIMPLE COPY ELEMENT TO CLIPBOARD -->
<script>
// Step 1: Click the element with the attribute ms-code-copy="trigger"
const triggerElement = document.querySelector('[ms-code-copy="trigger"]');
triggerElement.addEventListener('click', () => {
  // Step 2: Copy text from the element with the attribute ms-code-copy="subject" to the clipboard
  const subjectElement = document.querySelector('[ms-code-copy="subject"]');
  const subjectText = subjectElement.innerText;

  // Create a temporary textarea element to facilitate the copying process
  const tempTextArea = document.createElement('textarea');
  tempTextArea.value = subjectText;
  document.body.appendChild(tempTextArea);

  // Select the text within the textarea and copy it to the clipboard
  tempTextArea.select();
  document.execCommand('copy');

  // Remove the temporary textarea
  document.body.removeChild(tempTextArea);
});
</script>
Conditional Visibility

#7 - Delay Loading Elements

Delay elements from appearing for a set duration to give the page time to update with correct member data.

v0.1

<!-- 💙 MEMBERSCRIPT #7 v0.1 💙 DELAY LOADING ELEMENTS -->
<script>
  document.addEventListener("DOMContentLoaded", function() {
    const elementsToDelay = document.querySelectorAll('[ms-code-delay]');

    elementsToDelay.forEach(element => {
      element.style.visibility = "hidden"; // Hide the element initially
    });

    setTimeout(function() {
      elementsToDelay.forEach(element => {
        element.style.visibility = "visible"; // Make the element visible after the delay
        element.style.opacity = "0"; // Set the initial opacity to 0
        element.style.animation = "fadeIn 0.5s"; // Apply the fadeIn animation
        element.addEventListener("animationend", function() {
          element.style.opacity = "1"; // Set opacity to 1 at the end of the animation
        });
      });
    }, 1000);
  });
</script>
<style>
  @keyframes fadeIn {
    0% {
      opacity: 0;
    }
    100% {
      opacity: 1;
    }
  }
</style>
JSON

#6 - Create Item Groups From Member JSON

Display JSON groups to logged in members using a placeholder element built in Webflow.

v0.2

<!-- 💙 MEMBERSCRIPT #6 v0.1 💙 CREATE ITEM GROUPS FROM JSON -->
<script>
document.addEventListener("DOMContentLoaded", async function() {
  const memberstack = window.$memberstackDom;

  // Function to display nested/sub items
  const displayNestedItems = async function(printList) {
    const jsonGroup = printList.getAttribute('ms-code-print-list');
    const placeholder = printList.querySelector(`[ms-code-print-item="${jsonGroup}"]`);
    if (!placeholder) return;

    const itemTemplate = placeholder.outerHTML;
    const itemContainer = document.createElement('div'); // Create a new container for the items

    const member = await memberstack.getMemberJSON();
    const items = member.data && member.data[jsonGroup] ? Object.values(member.data[jsonGroup]) : [];
    if (items.length === 0) return; // If no items, exit the function

    items.forEach(item => {
      if (Object.keys(item).length === 0) return;

      const newItem = document.createElement('div');
      newItem.innerHTML = itemTemplate;
      const itemElements = newItem.querySelectorAll('[ms-code-item-text]');
      itemElements.forEach(itemElement => {
        const jsonKey = itemElement.getAttribute('ms-code-item-text');
        const value = item && item[jsonKey] ? item[jsonKey] : '';
        itemElement.textContent = value;
      });

      // Add item key attribute
      const itemKey = Object.keys(member.data[jsonGroup]).find(k => member.data[jsonGroup][k] === item);
      newItem.firstChild.setAttribute('ms-code-item-key', itemKey);

      itemContainer.appendChild(newItem.firstChild);
    });

    // Replace the existing list with the new items
    printList.innerHTML = itemContainer.innerHTML;
  };

  // Call displayNestedItems function on initial page load for each instance
  const printLists = document.querySelectorAll('[ms-code-print-list]');
  printLists.forEach(printList => {
    displayNestedItems(printList);
  });

  // Add click event listener to elements with ms-code-update="json"
  const updateButtons = document.querySelectorAll('[ms-code-update="json"]');
  updateButtons.forEach(button => {
    button.addEventListener("click", async function() {
      // Add a delay of 500ms
      await new Promise(resolve => setTimeout(resolve, 500));
      printLists.forEach(printList => {
        displayNestedItems(printList);
      });
    });
  });
});
</script>
JSON

#5 - Fill Text Based On Simple JSON Item

Use Member JSON to update the text of any element on your page.

v0.1

<!-- 💙 MEMBERSCRIPT #5 v0.1 💙 FILL TEXT BASED ON SIMPLE ITEM IN JSON -->
<script>
document.addEventListener("DOMContentLoaded", function() {
  const memberstack = window.$memberstackDom;

  // Function to fill text elements with attribute ms-code-fill-text
  const fillTextElements = async function() {
    // Retrieve the current member JSON data
    const member = await memberstack.getMemberJSON();

    // Fill text elements with attribute ms-code-fill-text
    const textElements = document.querySelectorAll('[ms-code-fill-text]');
    textElements.forEach(element => {
      const jsonKey = element.getAttribute('ms-code-fill-text');
      const value = member.data && member.data[jsonKey] ? member.data[jsonKey] : '';
      element.textContent = value;
    });
  };

  // Function to handle script #4 event
  const handleScript4Event = async function() {
    // Add a delay of 500ms
    await new Promise(resolve => setTimeout(resolve, 500));
    await fillTextElements();
  };

  // Add click event listener to elements with ms-code-update="json"
  const updateButtons = document.querySelectorAll('[ms-code-update="json"]');
  updateButtons.forEach(button => {
    button.addEventListener("click", handleScript4Event);
  });

  // Call fillTextElements function on initial page load
  fillTextElements();
});
</script>

We couldn't find any scripts for that search... please try again.