Componentes
Plantillas
Atributos
Integraciones
Comprobador del sitio
Código personalizado

MemberScripts

Una solución basada en atributos para añadir funciones a su sitio Webflow.
Simplemente copie algo de código, añada algunos atributos y listo.

Muchas gracias. Hemos recibido su envío.
¡Uy! Algo ha ido mal al enviar el formulario.
¿Necesita ayuda con MemberScripts?

Todos los clientes de Memberstack pueden solicitar asistencia en el Slack 2.0. Tenga en cuenta que no se trata de funciones oficiales y que no se puede garantizar la asistencia.

Campos personalizados

#131 - Sumar entradas de números

Toma el valor de las entradas numéricas y lo muestra en un valor de entrada, o en un span de texto.


<!-- 💙 MEMBERSCRIPT #131 v0.1 💙 - CALCULATE NUMBER INPUTS -->
<script>
  // Function to initialize the counter functionality
  function initializeCounter() {
    const counters = {};

    // Find all elements with ms-code-show-number attribute
    document.querySelectorAll('[ms-code-show-number]').forEach(el => {
      const counterName = el.getAttribute('ms-code-show-number');
      if (!counters[counterName]) {
        counters[counterName] = { total: 0, displays: [] };
      }
      counters[counterName].displays.push(el);
    });

    // Find all input elements with ms-code-add-number attribute
    document.querySelectorAll('input[ms-code-add-number]').forEach(input => {
      const counterName = input.getAttribute('ms-code-add-number');
      if (counters[counterName]) {
        input.addEventListener('input', updateCounter);
      }
    });

    // Function to update counter when input changes
    function updateCounter(event) {
      const input = event.target;
      const counterName = input.getAttribute('ms-code-add-number');
      const counter = counters[counterName];

      if (counter) {
        counter.total = 0;
        document.querySelectorAll(`input[ms-code-add-number="${counterName}"]`).forEach(input => {
          counter.total += parseInt(input.value) || 0;
        });

        counter.displays.forEach(display => {
          if (display.tagName === 'INPUT') {
            display.value = counter.total;
          } else {
            display.textContent = counter.total;
          }
        });
      }
    }

    // Initial update for all counters
    Object.keys(counters).forEach(counterName => {
      const input = document.querySelector(`input[ms-code-add-number="${counterName}"]`);
      if (input) {
        input.dispatchEvent(new Event('input'));
      }
    });
  }

  // Run the initialization when the DOM is fully loaded
  document.addEventListener('DOMContentLoaded', initializeCounter);
</script>
v0.1

<!-- 💙 MEMBERSCRIPT #131 v0.1 💙 - CALCULATE NUMBER INPUTS -->
<script>
  // Function to initialize the counter functionality
  function initializeCounter() {
    const counters = {};

    // Find all elements with ms-code-show-number attribute
    document.querySelectorAll('[ms-code-show-number]').forEach(el => {
      const counterName = el.getAttribute('ms-code-show-number');
      if (!counters[counterName]) {
        counters[counterName] = { total: 0, displays: [] };
      }
      counters[counterName].displays.push(el);
    });

    // Find all input elements with ms-code-add-number attribute
    document.querySelectorAll('input[ms-code-add-number]').forEach(input => {
      const counterName = input.getAttribute('ms-code-add-number');
      if (counters[counterName]) {
        input.addEventListener('input', updateCounter);
      }
    });

    // Function to update counter when input changes
    function updateCounter(event) {
      const input = event.target;
      const counterName = input.getAttribute('ms-code-add-number');
      const counter = counters[counterName];

      if (counter) {
        counter.total = 0;
        document.querySelectorAll(`input[ms-code-add-number="${counterName}"]`).forEach(input => {
          counter.total += parseInt(input.value) || 0;
        });

        counter.displays.forEach(display => {
          if (display.tagName === 'INPUT') {
            display.value = counter.total;
          } else {
            display.textContent = counter.total;
          }
        });
      }
    }

    // Initial update for all counters
    Object.keys(counters).forEach(counterName => {
      const input = document.querySelector(`input[ms-code-add-number="${counterName}"]`);
      if (input) {
        input.dispatchEvent(new Event('input'));
      }
    });
  }

  // Run the initialization when the DOM is fully loaded
  document.addEventListener('DOMContentLoaded', initializeCounter);
</script>
Ver Memberscript
UX

#130 - Enviar formulario automáticamente cuando cambian todas las entradas

Omita la necesidad de un botón de envío y envíe el formulario cuando cambien todas las entradas.


<!-- 💙 MEMBERSCRIPT #130 v0.1 💙 - AUTO SUBMIT FORMS FROM INPUT CHANGE -->
<script>
  document.addEventListener('DOMContentLoaded', () => {
    const forms = document.querySelectorAll('form[ms-code-auto-submit]');

    forms.forEach(form => {
      const fields = form.querySelectorAll('input:not([type="submit"]):not([type="button"]):not([type="reset"]), select, textarea');
      const fieldStates = new Map(Array.from(fields).map(field => [field, false]));

      function updateFieldState(field, checkImmediately = false) {
        switch (field.type) {
          case 'checkbox':
            fieldStates.set(field, true); // Considered interacted with once changed
            break;
          case 'radio':
            const radioGroup = form.querySelectorAll(`input[type="radio"][name="${field.name}"]`);
            radioGroup.forEach(radio => fieldStates.set(radio, true));
            break;
          case 'select-one':
          case 'select-multiple':
            fieldStates.set(field, field.value !== '');
            break;
          case 'file':
            fieldStates.set(field, field.files.length > 0);
            break;
          case 'hidden':
            fieldStates.set(field, true); // Always consider hidden fields as filled
            break;
          default:
            // For text inputs, only update on blur or if checkImmediately is true
            if (field.type === 'text' || field.type === 'password' || field.type === 'email' || field.type === 'tel' || field.type === 'url' || field.tagName === 'TEXTAREA') {
              if (checkImmediately || !field.dataset.blurred) {
                fieldStates.set(field, field.value.trim() !== '');
              }
            } else {
              fieldStates.set(field, field.value.trim() !== '');
            }
        }
        if (checkImmediately) {
          checkAndSubmit();
        }
      }

      function checkAndSubmit() {
        if (Array.from(fieldStates.values()).every(state => state)) {
          // Create and dispatch a submit event
          const submitEvent = new Event('submit', {
            bubbles: true,
            cancelable: true
          });

          const submitted = form.dispatchEvent(submitEvent);

          // If the event wasn't prevented, manually submit the form
          if (submitted) {
            form.submit();
          }
        }
      }

      fields.forEach(field => {
        // Use 'change' event for checkboxes, radios, file inputs, and selects
        if (['checkbox', 'radio', 'file', 'select-one', 'select-multiple'].includes(field.type) || field.tagName === 'SELECT') {
          field.addEventListener('change', () => updateFieldState(field, true));
        }

        // For text-like inputs, use 'blur' event
        if (field.type === 'text' || field.type === 'password' || field.type === 'email' || field.type === 'tel' || field.type === 'url' || field.tagName === 'TEXTAREA') {
          field.addEventListener('blur', () => {
            field.dataset.blurred = 'true';
            updateFieldState(field, true);
          });
          // Also check on input, but don't submit immediately
          field.addEventListener('input', () => updateFieldState(field, false));
        }
      });

      // Initial check for pre-filled fields (e.g., browser autofill)
      fields.forEach(field => updateFieldState(field, false));
      checkAndSubmit();
    });
  });
</script>
v0.1

<!-- 💙 MEMBERSCRIPT #130 v0.1 💙 - AUTO SUBMIT FORMS FROM INPUT CHANGE -->
<script>
  document.addEventListener('DOMContentLoaded', () => {
    const forms = document.querySelectorAll('form[ms-code-auto-submit]');

    forms.forEach(form => {
      const fields = form.querySelectorAll('input:not([type="submit"]):not([type="button"]):not([type="reset"]), select, textarea');
      const fieldStates = new Map(Array.from(fields).map(field => [field, false]));

      function updateFieldState(field, checkImmediately = false) {
        switch (field.type) {
          case 'checkbox':
            fieldStates.set(field, true); // Considered interacted with once changed
            break;
          case 'radio':
            const radioGroup = form.querySelectorAll(`input[type="radio"][name="${field.name}"]`);
            radioGroup.forEach(radio => fieldStates.set(radio, true));
            break;
          case 'select-one':
          case 'select-multiple':
            fieldStates.set(field, field.value !== '');
            break;
          case 'file':
            fieldStates.set(field, field.files.length > 0);
            break;
          case 'hidden':
            fieldStates.set(field, true); // Always consider hidden fields as filled
            break;
          default:
            // For text inputs, only update on blur or if checkImmediately is true
            if (field.type === 'text' || field.type === 'password' || field.type === 'email' || field.type === 'tel' || field.type === 'url' || field.tagName === 'TEXTAREA') {
              if (checkImmediately || !field.dataset.blurred) {
                fieldStates.set(field, field.value.trim() !== '');
              }
            } else {
              fieldStates.set(field, field.value.trim() !== '');
            }
        }
        if (checkImmediately) {
          checkAndSubmit();
        }
      }

      function checkAndSubmit() {
        if (Array.from(fieldStates.values()).every(state => state)) {
          // Create and dispatch a submit event
          const submitEvent = new Event('submit', {
            bubbles: true,
            cancelable: true
          });

          const submitted = form.dispatchEvent(submitEvent);

          // If the event wasn't prevented, manually submit the form
          if (submitted) {
            form.submit();
          }
        }
      }

      fields.forEach(field => {
        // Use 'change' event for checkboxes, radios, file inputs, and selects
        if (['checkbox', 'radio', 'file', 'select-one', 'select-multiple'].includes(field.type) || field.tagName === 'SELECT') {
          field.addEventListener('change', () => updateFieldState(field, true));
        }

        // For text-like inputs, use 'blur' event
        if (field.type === 'text' || field.type === 'password' || field.type === 'email' || field.type === 'tel' || field.type === 'url' || field.tagName === 'TEXTAREA') {
          field.addEventListener('blur', () => {
            field.dataset.blurred = 'true';
            updateFieldState(field, true);
          });
          // Also check on input, but don't submit immediately
          field.addEventListener('input', () => updateFieldState(field, false));
        }
      });

      // Initial check for pre-filled fields (e.g., browser autofill)
      fields.forEach(field => updateFieldState(field, false));
      checkAndSubmit();
    });
  });
</script>
Ver Memberscript
Visibilidad condicional

#129 - El paso del país

Impida que los visitantes vean su sitio si se encuentran en uno de los países no permitidos.


<!-- 💙 MEMBERSCRIPT #129 v0.1 💙 - COUNTRY GATING -->
<script>
  // Configuration
  const ACCESS_DENIED_PAGE = '/access-denied';

  // List of disallowed countries using ISO 3166-1 alpha-2 country codes
  const DISALLOWED_COUNTRIES = [
    // "US",  // United States
    // "CN",  // China
    // "RU",  // Russia
    // "IN",  // India
    // "JP",  // Japan
    // "DE",  // Germany
    // "GB",  // United Kingdom
    // "FR",  // France
    // "BR",  // Brazil
    // "IT",  // Italy
    // Add more countries as needed
  ];

  // Function to get visitor's country and check access
  function checkCountryAccess() {
    // Check if we're already on the access denied page
    if (window.location.pathname === ACCESS_DENIED_PAGE) {
      return; // Don't redirect if already on the access denied page
    }

    fetch('https://ipapi.co/json/')
      .then(response => response.json())
      .then(data => {
        const visitorCountry = data.country_code; // This returns the ISO 3166-1 alpha-2 country code
        if (DISALLOWED_COUNTRIES.includes(visitorCountry)) {
          window.location.href = ACCESS_DENIED_PAGE;
        }
      })
      .catch(error => {
        console.error('Error fetching IP data:', error);
      });
  }

  // Run the check when the page loads
  document.addEventListener('DOMContentLoaded', checkCountryAccess);
</script>
v0.1

<!-- 💙 MEMBERSCRIPT #129 v0.1 💙 - COUNTRY GATING -->
<script>
  // Configuration
  const ACCESS_DENIED_PAGE = '/access-denied';

  // List of disallowed countries using ISO 3166-1 alpha-2 country codes
  const DISALLOWED_COUNTRIES = [
    // "US",  // United States
    // "CN",  // China
    // "RU",  // Russia
    // "IN",  // India
    // "JP",  // Japan
    // "DE",  // Germany
    // "GB",  // United Kingdom
    // "FR",  // France
    // "BR",  // Brazil
    // "IT",  // Italy
    // Add more countries as needed
  ];

  // Function to get visitor's country and check access
  function checkCountryAccess() {
    // Check if we're already on the access denied page
    if (window.location.pathname === ACCESS_DENIED_PAGE) {
      return; // Don't redirect if already on the access denied page
    }

    fetch('https://ipapi.co/json/')
      .then(response => response.json())
      .then(data => {
        const visitorCountry = data.country_code; // This returns the ISO 3166-1 alpha-2 country code
        if (DISALLOWED_COUNTRIES.includes(visitorCountry)) {
          window.location.href = ACCESS_DENIED_PAGE;
        }
      })
      .catch(error => {
        console.error('Error fetching IP data:', error);
      });
  }

  // Run the check when the page loads
  document.addEventListener('DOMContentLoaded', checkCountryAccess);
</script>
Ver Memberscript
Visibilidad condicional
UX

#128 - Ocultar elementos una vez vistos

Añada un atributo, y sus visitantes sólo verán ese elemento una vez. Después de actualizar, desaparecerá.


<!-- 💙 MEMBERSCRIPT #128 💙 - ONLY SHOW ELEMENT ONCE -->
<script>
  document.addEventListener('DOMContentLoaded', function() {
    // Find all elements with the ms-code-show-once attribute
    const elements = document.querySelectorAll('[ms-code-show-once]');

    elements.forEach(element => {
      const identifier = element.getAttribute('ms-code-show-once');
      const storageKey = `ms-code-shown-${identifier}`;

      // Check if the element has been seen before
      if (localStorage.getItem(storageKey) !== 'true') {
        // If not seen, show the element
        element.style.display = 'block';

        // Mark it as seen in localStorage
        localStorage.setItem(storageKey, 'true');
      } else {
        // If already seen, hide the element
        element.style.display = 'none';
      }
    });
  });
</script>
v0.1

<!-- 💙 MEMBERSCRIPT #128 💙 - ONLY SHOW ELEMENT ONCE -->
<script>
  document.addEventListener('DOMContentLoaded', function() {
    // Find all elements with the ms-code-show-once attribute
    const elements = document.querySelectorAll('[ms-code-show-once]');

    elements.forEach(element => {
      const identifier = element.getAttribute('ms-code-show-once');
      const storageKey = `ms-code-shown-${identifier}`;

      // Check if the element has been seen before
      if (localStorage.getItem(storageKey) !== 'true') {
        // If not seen, show the element
        element.style.display = 'block';

        // Mark it as seen in localStorage
        localStorage.setItem(storageKey, 'true');
      } else {
        // If already seen, hide the element
        element.style.display = 'none';
      }
    });
  });
</script>
Ver Memberscript
UX

#127 - Validar entradas de texto

Valide las entradas de texto con cualquier lista de cadenas, incluidos los comodines.


<!-- 💙 MEMBERSCRIPT #127 v0.1 💙 - TEXT INPUT VALIDATION -->
<script>
document.addEventListener('DOMContentLoaded', function() {
    // Debounce function
    function debounce(func, wait) {
        let timeout;
        return function executedFunction(...args) {
            const later = () => {
                clearTimeout(timeout);
                func(...args);
            };
            clearTimeout(timeout);
            timeout = setTimeout(later, wait);
        };
    }

    // Find all fields with ms-code-require attribute
    const fields = document.querySelectorAll('[ms-code-require]');
    fields.forEach(field => {
        // Get the error element for this field
        const errorElement = document.querySelector(`[ms-code-require-error="${field.getAttribute('ms-code-require')}"]`);
        // Hide error message initially
        if (errorElement) {
            errorElement.style.display = 'none';
        }
        // Get the form containing the field
        const form = field.closest('form');
        // Get the submit button
        const submitButton = form ? form.querySelector(`[ms-code-submit-button="${field.getAttribute('ms-code-require')}"]`) : null;
        // Get the require-list attribute value
        const requireList = field.getAttribute('ms-code-require-list');
        if (requireList) {
            // Convert the require-list to an array of regex patterns
            const patterns = requireList.split(',').map(pattern => {
                return pattern.replace(/\{([^}]+)\}/g, (match, p1) => {
                    return p1.split('').map(char => {
                        switch(char) {
                            case '0': return '\\d';
                            case 'A': return '[A-Z]';
                            case 'a': return '[a-z]';
                            default: return char;
                        }
                    }).join('');
                });
            });
            // Validate function
            function validateField() {
                const value = field.value;
                const isValid = patterns.some(pattern => new RegExp(`^${pattern}$`).test(value));
                if (errorElement) {
                    errorElement.style.display = isValid ? 'none' : 'block';
                }
                if (submitButton) {
                    submitButton.style.opacity = isValid ? '1' : '0.5';
                    submitButton.style.pointerEvents = isValid ? 'auto' : 'none';
                }
                return isValid;
            }
            // Debounced validate function
            const debouncedValidate = debounce(validateField, 500);
            // Add blur event listener
            field.addEventListener('blur', validateField);
            // Add input event listener for debounced validation
            field.addEventListener('input', debouncedValidate);
            // Handle form submission
            if (form) {
                form.addEventListener('submit', function(event) {
                    if (!validateField() && submitButton) {
                        event.preventDefault();
                        field.focus();
                    }
                });
            }
        }
    });
});
</script>
v0.1

<!-- 💙 MEMBERSCRIPT #127 v0.1 💙 - TEXT INPUT VALIDATION -->
<script>
document.addEventListener('DOMContentLoaded', function() {
    // Debounce function
    function debounce(func, wait) {
        let timeout;
        return function executedFunction(...args) {
            const later = () => {
                clearTimeout(timeout);
                func(...args);
            };
            clearTimeout(timeout);
            timeout = setTimeout(later, wait);
        };
    }

    // Find all fields with ms-code-require attribute
    const fields = document.querySelectorAll('[ms-code-require]');
    fields.forEach(field => {
        // Get the error element for this field
        const errorElement = document.querySelector(`[ms-code-require-error="${field.getAttribute('ms-code-require')}"]`);
        // Hide error message initially
        if (errorElement) {
            errorElement.style.display = 'none';
        }
        // Get the form containing the field
        const form = field.closest('form');
        // Get the submit button
        const submitButton = form ? form.querySelector(`[ms-code-submit-button="${field.getAttribute('ms-code-require')}"]`) : null;
        // Get the require-list attribute value
        const requireList = field.getAttribute('ms-code-require-list');
        if (requireList) {
            // Convert the require-list to an array of regex patterns
            const patterns = requireList.split(',').map(pattern => {
                return pattern.replace(/\{([^}]+)\}/g, (match, p1) => {
                    return p1.split('').map(char => {
                        switch(char) {
                            case '0': return '\\d';
                            case 'A': return '[A-Z]';
                            case 'a': return '[a-z]';
                            default: return char;
                        }
                    }).join('');
                });
            });
            // Validate function
            function validateField() {
                const value = field.value;
                const isValid = patterns.some(pattern => new RegExp(`^${pattern}$`).test(value));
                if (errorElement) {
                    errorElement.style.display = isValid ? 'none' : 'block';
                }
                if (submitButton) {
                    submitButton.style.opacity = isValid ? '1' : '0.5';
                    submitButton.style.pointerEvents = isValid ? 'auto' : 'none';
                }
                return isValid;
            }
            // Debounced validate function
            const debouncedValidate = debounce(validateField, 500);
            // Add blur event listener
            field.addEventListener('blur', validateField);
            // Add input event listener for debounced validation
            field.addEventListener('input', debouncedValidate);
            // Handle form submission
            if (form) {
                form.addEventListener('submit', function(event) {
                    if (!validateField() && submitButton) {
                        event.preventDefault();
                        field.focus();
                    }
                });
            }
        }
    });
});
</script>
Ver Memberscript
Flujos personalizados

#126 - Enviar formulario a Webhook sin redireccionar

Enviar datos a un webhook y mantener el comportamiento predeterminado del formulario Webflow.


<!-- 💙 MEMBERSCRIPT #126 v0.1 💙 - POST FORM DATA TO WEBHOOK WITHOUT REDIRECTING -->
<script>
  // Wait for the DOM to be fully loaded
  document.addEventListener('DOMContentLoaded', function() {
    // Select all forms with the ms-code-form-no-redirect attribute
    const forms = document.querySelectorAll('form[ms-code-form-no-redirect]');

    forms.forEach(form => {
      // Select the success and error message elements for this form
      const formWrapper = form.closest('.w-form');
      const successMessage = formWrapper.querySelector('.w-form-done');
      const errorMessage = formWrapper.querySelector('.w-form-fail');

      // Add submit event listener to the form
      form.addEventListener('submit', function(event) {
        // Prevent the default form submission
        event.preventDefault();

        // Get the form data
        const formData = new FormData(form);

        // Get the submit button and set its text to the waiting message
        const submitButton = form.querySelector('input[type="submit"], button[type="submit"]');
        const originalButtonText = submitButton.value || submitButton.textContent;
        const waitingText = submitButton.getAttribute('data-wait') || 'Please wait...';

        if (submitButton.tagName === 'INPUT') {
          submitButton.value = waitingText;
        } else {
          submitButton.textContent = waitingText;
        }

        // Disable the submit button
        submitButton.disabled = true;

        // Send the form data to the form's action URL using fetch
        fetch(form.action, {
          method: 'POST',
          body: formData
        })
          .then(response => {
            if (response.ok) {
              // If the submission was successful, show the success message
              form.style.display = 'none';
              successMessage.style.display = 'block';
              errorMessage.style.display = 'none';
            } else {
              // If there was an error, show the error message
              throw new Error('Form submission failed');
            }
          })
          .catch(error => {
            // If there was a network error or the submission failed, show the error message
            console.error('Error:', error);
            errorMessage.style.display = 'block';
            successMessage.style.display = 'none';
          })
          .finally(() => {
            // Reset the submit button text and re-enable it
            if (submitButton.tagName === 'INPUT') {
              submitButton.value = originalButtonText;
            } else {
              submitButton.textContent = originalButtonText;
            }
            submitButton.disabled = false;
          });
      });
    });
  });
</script>
v0.1

<!-- 💙 MEMBERSCRIPT #126 v0.1 💙 - POST FORM DATA TO WEBHOOK WITHOUT REDIRECTING -->
<script>
  // Wait for the DOM to be fully loaded
  document.addEventListener('DOMContentLoaded', function() {
    // Select all forms with the ms-code-form-no-redirect attribute
    const forms = document.querySelectorAll('form[ms-code-form-no-redirect]');

    forms.forEach(form => {
      // Select the success and error message elements for this form
      const formWrapper = form.closest('.w-form');
      const successMessage = formWrapper.querySelector('.w-form-done');
      const errorMessage = formWrapper.querySelector('.w-form-fail');

      // Add submit event listener to the form
      form.addEventListener('submit', function(event) {
        // Prevent the default form submission
        event.preventDefault();

        // Get the form data
        const formData = new FormData(form);

        // Get the submit button and set its text to the waiting message
        const submitButton = form.querySelector('input[type="submit"], button[type="submit"]');
        const originalButtonText = submitButton.value || submitButton.textContent;
        const waitingText = submitButton.getAttribute('data-wait') || 'Please wait...';

        if (submitButton.tagName === 'INPUT') {
          submitButton.value = waitingText;
        } else {
          submitButton.textContent = waitingText;
        }

        // Disable the submit button
        submitButton.disabled = true;

        // Send the form data to the form's action URL using fetch
        fetch(form.action, {
          method: 'POST',
          body: formData
        })
          .then(response => {
            if (response.ok) {
              // If the submission was successful, show the success message
              form.style.display = 'none';
              successMessage.style.display = 'block';
              errorMessage.style.display = 'none';
            } else {
              // If there was an error, show the error message
              throw new Error('Form submission failed');
            }
          })
          .catch(error => {
            // If there was a network error or the submission failed, show the error message
            console.error('Error:', error);
            errorMessage.style.display = 'block';
            successMessage.style.display = 'none';
          })
          .finally(() => {
            // Reset the submit button text and re-enable it
            if (submitButton.tagName === 'INPUT') {
              submitButton.value = originalButtonText;
            } else {
              submitButton.textContent = originalButtonText;
            }
            submitButton.disabled = false;
          });
      });
    });
  });
</script>
Ver Memberscript
Visibilidad condicional
UX

#125 - Controlar Estado Requerido Con Checkbox

Establecer campos de formulario como obligatorios/opcionales en función del estado de una casilla de verificación.


<!-- 💙 MEMBERSCRIPT #125 v0.1 💙 - CHECKBOX TOGGLE REQUIRED ON FORM FIELDS -->
<script>
  // Function to initialize the form field requirements
  function initFormFieldRequirements() {
    // Handle checkbox changes
    document.querySelectorAll('[ms-code-req-if-checked], [ms-code-req-if-unchecked]').forEach(checkbox => {
      checkbox.addEventListener('change', updateFieldRequirements);
      // Initial update
      updateFieldRequirements.call(checkbox);
    });
  }

  // Function to update field requirements based on checkbox state
  function updateFieldRequirements() {
    const isChecked = this.checked;
    const group = this.getAttribute('ms-code-req-if-checked') || this.getAttribute('ms-code-req-if-unchecked');
    const ifChecked = this.hasAttribute('ms-code-req-if-checked');
    const shouldBeRequired = ifChecked ? isChecked : !isChecked;
    const shouldDisableIfNotReq = this.hasAttribute('ms-code-disable-if-not-req');

    // Update associated input fields
    document.querySelectorAll(`[ms-code-req-input="${group}"]`).forEach(input => {
      input.required = shouldBeRequired;
      updateInputStyle(input, shouldBeRequired, shouldDisableIfNotReq);
    });

    // Update associated labels
    document.querySelectorAll(`[ms-code-req-label="${group}"]`).forEach(label => {
      label.style.display = shouldBeRequired ? '' : 'none';
    });
  }

  // Function to update input style based on required state and disable setting
  function updateInputStyle(input, isRequired, shouldDisableIfNotReq) {
    if (shouldDisableIfNotReq) {
      input.style.opacity = isRequired ? '1' : '0.4';
      input.style.pointerEvents = isRequired ? '' : 'none';
    } else {
      input.style.opacity = '';
      input.style.pointerEvents = '';
    }
  }

  // Initialize when the DOM is fully loaded
  document.addEventListener('DOMContentLoaded', initFormFieldRequirements);
</script>
v0.1

<!-- 💙 MEMBERSCRIPT #125 v0.1 💙 - CHECKBOX TOGGLE REQUIRED ON FORM FIELDS -->
<script>
  // Function to initialize the form field requirements
  function initFormFieldRequirements() {
    // Handle checkbox changes
    document.querySelectorAll('[ms-code-req-if-checked], [ms-code-req-if-unchecked]').forEach(checkbox => {
      checkbox.addEventListener('change', updateFieldRequirements);
      // Initial update
      updateFieldRequirements.call(checkbox);
    });
  }

  // Function to update field requirements based on checkbox state
  function updateFieldRequirements() {
    const isChecked = this.checked;
    const group = this.getAttribute('ms-code-req-if-checked') || this.getAttribute('ms-code-req-if-unchecked');
    const ifChecked = this.hasAttribute('ms-code-req-if-checked');
    const shouldBeRequired = ifChecked ? isChecked : !isChecked;
    const shouldDisableIfNotReq = this.hasAttribute('ms-code-disable-if-not-req');

    // Update associated input fields
    document.querySelectorAll(`[ms-code-req-input="${group}"]`).forEach(input => {
      input.required = shouldBeRequired;
      updateInputStyle(input, shouldBeRequired, shouldDisableIfNotReq);
    });

    // Update associated labels
    document.querySelectorAll(`[ms-code-req-label="${group}"]`).forEach(label => {
      label.style.display = shouldBeRequired ? '' : 'none';
    });
  }

  // Function to update input style based on required state and disable setting
  function updateInputStyle(input, isRequired, shouldDisableIfNotReq) {
    if (shouldDisableIfNotReq) {
      input.style.opacity = isRequired ? '1' : '0.4';
      input.style.pointerEvents = isRequired ? '' : 'none';
    } else {
      input.style.opacity = '';
      input.style.pointerEvents = '';
    }
  }

  // Initialize when the DOM is fully loaded
  document.addEventListener('DOMContentLoaded', initFormFieldRequirements);
</script>
Ver Memberscript
UX

#124 - Alternar la visibilidad de los elementos

Utilice botones personalizados para ocultar y mostrar elementos, con los estados guardados en el almacenamiento local.


<!-- 💙 MEMBERSCRIPT #124 v0.1 💙 - TOGGLE ITEM VISIBILITY -->
<script>
  document.addEventListener('DOMContentLoaded', function() {
    const STORAGE_KEY = 'ms-code-vis-states';

    // Load saved states from local storage
    let savedStates = JSON.parse(localStorage.getItem(STORAGE_KEY)) || {};

    // Find all unique group identifiers
    const groups = new Set([...document.querySelectorAll('[ms-code-vis-item]')].map(el => el.getAttribute('ms-code-vis-item')));

    groups.forEach(group => {
      const item = document.querySelector(`[ms-code-vis-item="${group}"]`);
      const showButton = document.querySelector(`[ms-code-vis-show="${group}"]`);
      const hideButton = document.querySelector(`[ms-code-vis-hide="${group}"]`);

      if (!item || !showButton || !hideButton) return;

      // Function to update visibility
      function updateVisibility(isVisible) {
        if (isVisible) {
          item.style.removeProperty('display');
          showButton.style.display = 'none';
          hideButton.style.removeProperty('display');
        } else {
          item.style.display = 'none';
          showButton.style.removeProperty('display');
          hideButton.style.display = 'none';
        }
        // Save state to local storage
        savedStates[group] = isVisible;
        localStorage.setItem(STORAGE_KEY, JSON.stringify(savedStates));
      }

      // Set initial visibility
      const defaultState = item.getAttribute('ms-code-vis-default');
      const savedState = savedStates[group];

      if (savedState !== undefined) {
        updateVisibility(savedState);
      } else if (defaultState === 'hide') {
        updateVisibility(false);
      } else {
        updateVisibility(true);
      }

      // Add click event listeners
      showButton.addEventListener('click', function(e) {
        e.preventDefault();
        updateVisibility(true);
      });

      hideButton.addEventListener('click', function(e) {
        e.preventDefault();
        updateVisibility(false);
      });
    });
  });
</script>
v0.1

<!-- 💙 MEMBERSCRIPT #124 v0.1 💙 - TOGGLE ITEM VISIBILITY -->
<script>
  document.addEventListener('DOMContentLoaded', function() {
    const STORAGE_KEY = 'ms-code-vis-states';

    // Load saved states from local storage
    let savedStates = JSON.parse(localStorage.getItem(STORAGE_KEY)) || {};

    // Find all unique group identifiers
    const groups = new Set([...document.querySelectorAll('[ms-code-vis-item]')].map(el => el.getAttribute('ms-code-vis-item')));

    groups.forEach(group => {
      const item = document.querySelector(`[ms-code-vis-item="${group}"]`);
      const showButton = document.querySelector(`[ms-code-vis-show="${group}"]`);
      const hideButton = document.querySelector(`[ms-code-vis-hide="${group}"]`);

      if (!item || !showButton || !hideButton) return;

      // Function to update visibility
      function updateVisibility(isVisible) {
        if (isVisible) {
          item.style.removeProperty('display');
          showButton.style.display = 'none';
          hideButton.style.removeProperty('display');
        } else {
          item.style.display = 'none';
          showButton.style.removeProperty('display');
          hideButton.style.display = 'none';
        }
        // Save state to local storage
        savedStates[group] = isVisible;
        localStorage.setItem(STORAGE_KEY, JSON.stringify(savedStates));
      }

      // Set initial visibility
      const defaultState = item.getAttribute('ms-code-vis-default');
      const savedState = savedStates[group];

      if (savedState !== undefined) {
        updateVisibility(savedState);
      } else if (defaultState === 'hide') {
        updateVisibility(false);
      } else {
        updateVisibility(true);
      }

      // Add click event listeners
      showButton.addEventListener('click', function(e) {
        e.preventDefault();
        updateVisibility(true);
      });

      hideButton.addEventListener('click', function(e) {
        e.preventDefault();
        updateVisibility(false);
      });
    });
  });
</script>
Ver Memberscript
UX
Accesibilidad

#123 - Una reproducción de audio cada vez

Ponga en pausa automáticamente todos los demás reproductores de audio cuando un usuario haga clic para reproducir uno.


<!-- 💙 MEMBERSCRIPT #123 💙 - ONE AUDIO AT A TIME -->
<script>
  document.addEventListener('DOMContentLoaded', () => {
    const pauseOthers = current => 
      document.querySelectorAll('audio, video').forEach(el => el !== current && !el.paused && el.pause());
  
    const addPlayListener = el => el.addEventListener('play', e => pauseOthers(e.target));
  
    new MutationObserver(mutations => 
      mutations.forEach(m => m.addedNodes.forEach(n => 
        (n.nodeName === 'AUDIO' || n.nodeName === 'VIDEO') && addPlayListener(n)
      ))
    ).observe(document.body, { childList: true, subtree: true });
  
    document.querySelectorAll('audio, video').forEach(addPlayListener);
  });
</script>
v0.1

<!-- 💙 MEMBERSCRIPT #123 💙 - ONE AUDIO AT A TIME -->
<script>
  document.addEventListener('DOMContentLoaded', () => {
    const pauseOthers = current => 
      document.querySelectorAll('audio, video').forEach(el => el !== current && !el.paused && el.pause());
  
    const addPlayListener = el => el.addEventListener('play', e => pauseOthers(e.target));
  
    new MutationObserver(mutations => 
      mutations.forEach(m => m.addedNodes.forEach(n => 
        (n.nodeName === 'AUDIO' || n.nodeName === 'VIDEO') && addPlayListener(n)
      ))
    ).observe(document.body, { childList: true, subtree: true });
  
    document.querySelectorAll('audio, video').forEach(addPlayListener);
  });
</script>
Ver Memberscript
SEO
UX

#122 - Abrir enlaces externos en una nueva pestaña

Haga que los enlaces externos se abran automáticamente en una nueva pestaña y añada atributos nofollow y noreferrer.


<!-- 💙 MEMBERSCRIPT #122 💙 - OPEN EXTERNAL LINKS IN NEW TAB -->
<script>
  document.addEventListener('DOMContentLoaded', () => {
    const thisDomain = location.hostname;
    const externalSelector = `a:not([href*="${thisDomain}"]):not([href^="/"]):not([href^="#"]):not([ms-code-ext-link="ignore"])`;

    document.querySelectorAll(externalSelector).forEach(link => {
      link.target = '_blank';  // Open in new tab (comment out to disable)
      link.rel = (link.rel ? link.rel + ' ' : '') + 'noreferrer';  // Add noreferrer (comment out to disable)
      link.rel = (link.rel ? link.rel + ' ' : '') + 'nofollow';  // Add nofollow (comment out to disable)
    });
  });
</script>
v0.1

<!-- 💙 MEMBERSCRIPT #122 💙 - OPEN EXTERNAL LINKS IN NEW TAB -->
<script>
  document.addEventListener('DOMContentLoaded', () => {
    const thisDomain = location.hostname;
    const externalSelector = `a:not([href*="${thisDomain}"]):not([href^="/"]):not([href^="#"]):not([ms-code-ext-link="ignore"])`;

    document.querySelectorAll(externalSelector).forEach(link => {
      link.target = '_blank';  // Open in new tab (comment out to disable)
      link.rel = (link.rel ? link.rel + ' ' : '') + 'noreferrer';  // Add noreferrer (comment out to disable)
      link.rel = (link.rel ? link.rel + ' ' : '') + 'nofollow';  // Add nofollow (comment out to disable)
    });
  });
</script>
Ver Memberscript
Campos personalizados
UX

#121 - Renderizar Matriz desde Campo Personalizado

Muestre cualquier lista separada por comas a sus miembros en Webflow.


<!-- 💙 MEMBERSCRIPT #121 v0.1 💙 - RENDER ARRAY FROM CUSTOM FIELD -->
<script>
  // Function to render arrays from Memberstack custom fields
  function renderMemberstackArrays() {
    // Get all elements with the ms-code-render-array attribute
    const arrayElements = document.querySelectorAll('[ms-code-render-array]');

    arrayElements.forEach(element => {
      const customFieldKey = element.getAttribute('ms-code-render-array');

      // Get Memberstack data from localStorage
      const memberData = JSON.parse(localStorage.getItem('_ms-mem'));

      if (!memberData || !memberData.customFields || !memberData.customFields[customFieldKey]) {
        // If no data found, remove the element
        element.remove();
        return;
      }

      const arrayString = memberData.customFields[customFieldKey];

      // Convert string to array, trim whitespace
      const arrayItems = arrayString.split(',').map(item => item.trim()).filter(item => item !== '');

      if (arrayItems.length === 0) {
        // If array is empty, remove the element
        element.remove();
        return;
      }

      // Store the parent element
      const parentElement = element.parentNode;

      // Clone the template
      const templateItem = element.cloneNode(true);

      // Remove the ms-code-render-array attribute from the template
      templateItem.removeAttribute('ms-code-render-array');

      // Create a document fragment to hold the new items
      const fragment = document.createDocumentFragment();

      // Render array items
      arrayItems.forEach(item => {
        const newItem = templateItem.cloneNode(true);

        // Replace the content of the new item
        replaceContent(newItem, item);

        fragment.appendChild(newItem);
      });

      // Replace the original element with the new items
      parentElement.replaceChild(fragment, element);
    });
  }

  // Helper function to replace content in the element
  function replaceContent(element, newContent) {
    if (element.childNodes.length > 0) {
      element.childNodes.forEach(child => {
        if (child.nodeType === Node.ELEMENT_NODE) {
          replaceContent(child, newContent);
        } else if (child.nodeType === Node.TEXT_NODE) {
          child.textContent = newContent;
        }
      });
    } else {
      element.textContent = newContent;
    }
  }

  // Run the function when the DOM is fully loaded
  document.addEventListener('DOMContentLoaded', renderMemberstackArrays);
</script>
v0.1

<!-- 💙 MEMBERSCRIPT #121 v0.1 💙 - RENDER ARRAY FROM CUSTOM FIELD -->
<script>
  // Function to render arrays from Memberstack custom fields
  function renderMemberstackArrays() {
    // Get all elements with the ms-code-render-array attribute
    const arrayElements = document.querySelectorAll('[ms-code-render-array]');

    arrayElements.forEach(element => {
      const customFieldKey = element.getAttribute('ms-code-render-array');

      // Get Memberstack data from localStorage
      const memberData = JSON.parse(localStorage.getItem('_ms-mem'));

      if (!memberData || !memberData.customFields || !memberData.customFields[customFieldKey]) {
        // If no data found, remove the element
        element.remove();
        return;
      }

      const arrayString = memberData.customFields[customFieldKey];

      // Convert string to array, trim whitespace
      const arrayItems = arrayString.split(',').map(item => item.trim()).filter(item => item !== '');

      if (arrayItems.length === 0) {
        // If array is empty, remove the element
        element.remove();
        return;
      }

      // Store the parent element
      const parentElement = element.parentNode;

      // Clone the template
      const templateItem = element.cloneNode(true);

      // Remove the ms-code-render-array attribute from the template
      templateItem.removeAttribute('ms-code-render-array');

      // Create a document fragment to hold the new items
      const fragment = document.createDocumentFragment();

      // Render array items
      arrayItems.forEach(item => {
        const newItem = templateItem.cloneNode(true);

        // Replace the content of the new item
        replaceContent(newItem, item);

        fragment.appendChild(newItem);
      });

      // Replace the original element with the new items
      parentElement.replaceChild(fragment, element);
    });
  }

  // Helper function to replace content in the element
  function replaceContent(element, newContent) {
    if (element.childNodes.length > 0) {
      element.childNodes.forEach(child => {
        if (child.nodeType === Node.ELEMENT_NODE) {
          replaceContent(child, newContent);
        } else if (child.nodeType === Node.TEXT_NODE) {
          child.textContent = newContent;
        }
      });
    } else {
      element.textContent = newContent;
    }
  }

  // Run the function when the DOM is fully loaded
  document.addEventListener('DOMContentLoaded', renderMemberstackArrays);
</script>
Ver Memberscript
UX

#120 - Mostrar/Ocultar Elemento Basado en Dispositivo, OS o Navegador

Visibilidad condicional en función del dispositivo, sistema operativo o navegador que utilice el visitante.


<!-- 💙 MEMBERSCRIPT #120 v0.1 💙 - DEVICE/OS/BROWSER CONDITIONAL VISIBILITY -->
<script>
  // Define device types, operating systems, and browsers with their detection methods
  const detectionTypes = {
    // Device types
    mobile: () => /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent),
    tablet: () => /iPad|Android|Silk/i.test(navigator.userAgent) && !/Mobile/i.test(navigator.userAgent),
    desktop: () => !(/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)),
    touchdevice: () => 'ontouchstart' in window || navigator.maxTouchPoints > 0,
    landscape: () => window.matchMedia("(orientation: landscape)").matches,
    portrait: () => window.matchMedia("(orientation: portrait)").matches,

    // Operating Systems
    ios: () => /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream,
    android: () => /Android/.test(navigator.userAgent),
    macos: () => /Mac OS X/.test(navigator.userAgent) && !(/iPad|iPhone|iPod/.test(navigator.userAgent)),
    windows: () => /Win/.test(navigator.platform),
    linux: () => /Linux/.test(navigator.platform),

    // Browsers
    chrome: () => /Chrome/.test(navigator.userAgent) && !/Chromium/.test(navigator.userAgent),
    firefox: () => /Firefox/.test(navigator.userAgent),
    safari: () => /Safari/.test(navigator.userAgent) && !/Chrome/.test(navigator.userAgent),
    edge: () => /Edg/.test(navigator.userAgent),
    opera: () => /OPR/.test(navigator.userAgent) || /Opera/.test(navigator.userAgent),
    ie: () => /Trident/.test(navigator.userAgent)
  };

  function detectTypes() {
    const detected = [];
    for (const [type, detector] of Object.entries(detectionTypes)) {
      if (detector()) {
        detected.push(type);
      }
    }
    return detected;
  }

  function evaluateCondition(condition, currentTypes) {
    const parts = condition.split('&').map(part => part.trim());
    return parts.every(part => {
      const orParts = part.split('|').map(p => p.trim());
      return orParts.some(p => {
        if (p.startsWith('!')) {
          return !currentTypes.includes(p.slice(1));
        }
        return currentTypes.includes(p);
      });
    });
  }

  function updateElementVisibility() {
    const currentTypes = detectTypes();
    const elements = document.querySelectorAll('[ms-code-device-show]');

    elements.forEach(element => {
      const showAttribute = element.getAttribute('ms-code-device-show').toLowerCase();
      const shouldShow = evaluateCondition(showAttribute, currentTypes);
      element.style.display = shouldShow ? '' : 'none';
    });
  }

  // Run on page load and window resize
  window.addEventListener('load', updateElementVisibility);
  window.addEventListener('resize', updateElementVisibility);
</script>
v0.1

<!-- 💙 MEMBERSCRIPT #120 v0.1 💙 - DEVICE/OS/BROWSER CONDITIONAL VISIBILITY -->
<script>
  // Define device types, operating systems, and browsers with their detection methods
  const detectionTypes = {
    // Device types
    mobile: () => /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent),
    tablet: () => /iPad|Android|Silk/i.test(navigator.userAgent) && !/Mobile/i.test(navigator.userAgent),
    desktop: () => !(/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)),
    touchdevice: () => 'ontouchstart' in window || navigator.maxTouchPoints > 0,
    landscape: () => window.matchMedia("(orientation: landscape)").matches,
    portrait: () => window.matchMedia("(orientation: portrait)").matches,

    // Operating Systems
    ios: () => /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream,
    android: () => /Android/.test(navigator.userAgent),
    macos: () => /Mac OS X/.test(navigator.userAgent) && !(/iPad|iPhone|iPod/.test(navigator.userAgent)),
    windows: () => /Win/.test(navigator.platform),
    linux: () => /Linux/.test(navigator.platform),

    // Browsers
    chrome: () => /Chrome/.test(navigator.userAgent) && !/Chromium/.test(navigator.userAgent),
    firefox: () => /Firefox/.test(navigator.userAgent),
    safari: () => /Safari/.test(navigator.userAgent) && !/Chrome/.test(navigator.userAgent),
    edge: () => /Edg/.test(navigator.userAgent),
    opera: () => /OPR/.test(navigator.userAgent) || /Opera/.test(navigator.userAgent),
    ie: () => /Trident/.test(navigator.userAgent)
  };

  function detectTypes() {
    const detected = [];
    for (const [type, detector] of Object.entries(detectionTypes)) {
      if (detector()) {
        detected.push(type);
      }
    }
    return detected;
  }

  function evaluateCondition(condition, currentTypes) {
    const parts = condition.split('&').map(part => part.trim());
    return parts.every(part => {
      const orParts = part.split('|').map(p => p.trim());
      return orParts.some(p => {
        if (p.startsWith('!')) {
          return !currentTypes.includes(p.slice(1));
        }
        return currentTypes.includes(p);
      });
    });
  }

  function updateElementVisibility() {
    const currentTypes = detectTypes();
    const elements = document.querySelectorAll('[ms-code-device-show]');

    elements.forEach(element => {
      const showAttribute = element.getAttribute('ms-code-device-show').toLowerCase();
      const shouldShow = evaluateCondition(showAttribute, currentTypes);
      element.style.display = shouldShow ? '' : 'none';
    });
  }

  // Run on page load and window resize
  window.addEventListener('load', updateElementVisibility);
  window.addEventListener('resize', updateElementVisibility);
</script>
Ver Memberscript
Campos personalizados

#119 - Calculadora de edad a partir de una fecha

Calcula el total de años transcurridos y rellena previamente una entrada con el número.


<!-- 💙 MEMBERSCRIPT #119 v0.1 💙 - AGE CALCULATOR FROM DATE INPUT -->
<script>
  // Function to calculate age
  function calculateAge(birthDate) {
    const today = new Date();
    const birth = new Date(birthDate);
    let age = today.getFullYear() - birth.getFullYear();
    const monthDiff = today.getMonth() - birth.getMonth();

    if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birth.getDate())) {
      age--;
    }

    return age;
  }

  // Function to update age input
  function updateAgeInput(birthdayInput) {
    const attrValue = birthdayInput.getAttribute('ms-code-bday-input');
    const ageInput = document.querySelector(`[ms-code-age-input="${attrValue}"]`);

    if (birthdayInput.value) {
      const age = calculateAge(birthdayInput.value);
      ageInput.value = age;
    } else {
      ageInput.value = '';
    }
  }

  // Function to set up event listeners for all birthday inputs
  function setupAgeCalculators() {
    const birthdayInputs = document.querySelectorAll('[ms-code-bday-input]');

    birthdayInputs.forEach(input => {
      input.addEventListener('input', () => updateAgeInput(input));
      // Initial call to set age if birthday is pre-filled
      updateAgeInput(input);
    });
  }

  // Run setup when the DOM is fully loaded
  document.addEventListener('DOMContentLoaded', setupAgeCalculators);
</script>
v0.1

<!-- 💙 MEMBERSCRIPT #119 v0.1 💙 - AGE CALCULATOR FROM DATE INPUT -->
<script>
  // Function to calculate age
  function calculateAge(birthDate) {
    const today = new Date();
    const birth = new Date(birthDate);
    let age = today.getFullYear() - birth.getFullYear();
    const monthDiff = today.getMonth() - birth.getMonth();

    if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birth.getDate())) {
      age--;
    }

    return age;
  }

  // Function to update age input
  function updateAgeInput(birthdayInput) {
    const attrValue = birthdayInput.getAttribute('ms-code-bday-input');
    const ageInput = document.querySelector(`[ms-code-age-input="${attrValue}"]`);

    if (birthdayInput.value) {
      const age = calculateAge(birthdayInput.value);
      ageInput.value = age;
    } else {
      ageInput.value = '';
    }
  }

  // Function to set up event listeners for all birthday inputs
  function setupAgeCalculators() {
    const birthdayInputs = document.querySelectorAll('[ms-code-bday-input]');

    birthdayInputs.forEach(input => {
      input.addEventListener('input', () => updateAgeInput(input));
      // Initial call to set age if birthday is pre-filled
      updateAgeInput(input);
    });
  }

  // Run setup when the DOM is fully loaded
  document.addEventListener('DOMContentLoaded', setupAgeCalculators);
</script>
Ver Memberscript
Campos personalizados

#118 - Guardar la última dirección IP del miembro

Actualice un campo personalizado con la dirección IP más reciente desde la que se conectan sus usuarios.


<!-- 💙 MEMBERSCRIPT #118 v0.1 💙 - SAVE LAST IP ADDRESS -->
<script>
  const memberstack = window.$memberstackDom;

  memberstack.getCurrentMember().then(async (response) => {
    if (response && response.data) {
      const member = response.data;
      try {
        // Fetch the current IP address from the ipify API
        const ipResponse = await fetch('https://api.ipify.org?format=json');
        const ipData = await ipResponse.json();
        const currentIpAddress = ipData.ip;

        // Retrieve the stored IP address from Memberstack's custom fields
        const storedIpAddress = member.customFields["last-ip"];

        // Check if the IP address has changed and update if necessary
        if (currentIpAddress !== storedIpAddress) {
          await memberstack.updateMember({
            customFields: {
              "last-ip": currentIpAddress
            }
          });
          // Optional: Uncomment the line below to log a message when the IP is updated
          // console.log('IP address updated');
        } else {
          // Optional: Uncomment the line below to log when the IP remains unchanged
          // console.log('IP address unchanged, no update needed');
        }
      } catch (error) {
        // Log any errors encountered during the fetch or update process
        console.error('Error checking or updating member IP:', error);
      }
    } else {
      // Optional: Uncomment the line below to log when no member is logged in
      // console.log('No member is currently logged in');
    }
  });
</script>
v0.1

<!-- 💙 MEMBERSCRIPT #118 v0.1 💙 - SAVE LAST IP ADDRESS -->
<script>
  const memberstack = window.$memberstackDom;

  memberstack.getCurrentMember().then(async (response) => {
    if (response && response.data) {
      const member = response.data;
      try {
        // Fetch the current IP address from the ipify API
        const ipResponse = await fetch('https://api.ipify.org?format=json');
        const ipData = await ipResponse.json();
        const currentIpAddress = ipData.ip;

        // Retrieve the stored IP address from Memberstack's custom fields
        const storedIpAddress = member.customFields["last-ip"];

        // Check if the IP address has changed and update if necessary
        if (currentIpAddress !== storedIpAddress) {
          await memberstack.updateMember({
            customFields: {
              "last-ip": currentIpAddress
            }
          });
          // Optional: Uncomment the line below to log a message when the IP is updated
          // console.log('IP address updated');
        } else {
          // Optional: Uncomment the line below to log when the IP remains unchanged
          // console.log('IP address unchanged, no update needed');
        }
      } catch (error) {
        // Log any errors encountered during the fetch or update process
        console.error('Error checking or updating member IP:', error);
      }
    } else {
      // Optional: Uncomment the line below to log when no member is logged in
      // console.log('No member is currently logged in');
    }
  });
</script>
Ver Memberscript
UX

#117 - Barra de progreso de desplazamiento de página

Un indicador de desplazamiento de página flexible y personalizado para mostrar el progreso del desplazamiento de página.


<!-- 💙 MEMBERSCRIPT #117 v0.1 💙 - PAGE SCROLL PROGRESS BAR -->
<script>
  // Function to update the progress bar
  function updateProgressBar() {
    const container = document.querySelector('[ms-code-ps="container"]');
    const bar = document.querySelector('[ms-code-ps="bar"]');
    const startElement = document.querySelector('[ms-code-ps="start"]');
    const endElement = document.querySelector('[ms-code-ps="end"]');

    if (!container || !bar) return;

    const windowHeight = window.innerHeight;
    const documentHeight = document.documentElement.scrollHeight;
    const scrollTop = window.pageYOffset || document.documentElement.scrollTop;

    let startPosition = 0;
    let endPosition = documentHeight - windowHeight;

    if (startElement) {
      const startRect = startElement.getBoundingClientRect();
      startPosition = scrollTop + startRect.top - windowHeight;
    }

    if (endElement) {
      const endRect = endElement.getBoundingClientRect();
      endPosition = scrollTop + endRect.top - windowHeight;
    }

    const scrollRange = endPosition - startPosition;
    const scrollProgress = scrollTop - startPosition;
    const scrollPercentage = Math.max(0, Math.min(100, (scrollProgress / scrollRange) * 100));

    // Use requestAnimationFrame for smooth animation
    requestAnimationFrame(() => {
      bar.style.width = `${scrollPercentage}%`;
      bar.style.transition = 'width 0.1s linear';
    });
  }

  // Throttle function to limit how often updateProgressBar is called
  function throttle(func, limit) {
    let inThrottle;
    return function() {
      const args = arguments;
      const context = this;
      if (!inThrottle) {
        func.apply(context, args);
        inThrottle = true;
        setTimeout(() => inThrottle = false, limit);
      }
    };
  }

  // Add scroll event listener with throttling
  window.addEventListener('scroll', throttle(updateProgressBar, 10));

  // Initial call to set the correct width on page load
  updateProgressBar();
</script>
v0.1

<!-- 💙 MEMBERSCRIPT #117 v0.1 💙 - PAGE SCROLL PROGRESS BAR -->
<script>
  // Function to update the progress bar
  function updateProgressBar() {
    const container = document.querySelector('[ms-code-ps="container"]');
    const bar = document.querySelector('[ms-code-ps="bar"]');
    const startElement = document.querySelector('[ms-code-ps="start"]');
    const endElement = document.querySelector('[ms-code-ps="end"]');

    if (!container || !bar) return;

    const windowHeight = window.innerHeight;
    const documentHeight = document.documentElement.scrollHeight;
    const scrollTop = window.pageYOffset || document.documentElement.scrollTop;

    let startPosition = 0;
    let endPosition = documentHeight - windowHeight;

    if (startElement) {
      const startRect = startElement.getBoundingClientRect();
      startPosition = scrollTop + startRect.top - windowHeight;
    }

    if (endElement) {
      const endRect = endElement.getBoundingClientRect();
      endPosition = scrollTop + endRect.top - windowHeight;
    }

    const scrollRange = endPosition - startPosition;
    const scrollProgress = scrollTop - startPosition;
    const scrollPercentage = Math.max(0, Math.min(100, (scrollProgress / scrollRange) * 100));

    // Use requestAnimationFrame for smooth animation
    requestAnimationFrame(() => {
      bar.style.width = `${scrollPercentage}%`;
      bar.style.transition = 'width 0.1s linear';
    });
  }

  // Throttle function to limit how often updateProgressBar is called
  function throttle(func, limit) {
    let inThrottle;
    return function() {
      const args = arguments;
      const context = this;
      if (!inThrottle) {
        func.apply(context, args);
        inThrottle = true;
        setTimeout(() => inThrottle = false, limit);
      }
    };
  }

  // Add scroll event listener with throttling
  window.addEventListener('scroll', throttle(updateProgressBar, 10));

  // Initial call to set the correct width on page load
  updateProgressBar();
</script>
Ver Memberscript
UX

#116 - Compartir enlaces de texto resaltados

Permita a los usuarios resaltar texto y compartir el enlace con los demás.


<!-- 💙 MEMBERSCRIPT #116 v0.1 💙 - SHARE HIGHLIGHTED TEXT LINKS -->
<script>
  // Function to encode text and position for URL
  function encodeSelection(text, nodeIndex, textOffset) {
    return btoa(encodeURIComponent(JSON.stringify({ text, nodeIndex, textOffset })));
  }

  // Function to decode selection from URL
  function decodeSelection(encoded) {
    try {
      return JSON.parse(decodeURIComponent(atob(encoded)));
    } catch (e) {
      // If parsing fails, assume it's just the text in the old format
      return { text: decodeURIComponent(atob(encoded)) };
    }
  }

  // Function to remove existing highlight
  function removeExistingHighlight() {
    const existingHighlight = document.querySelector('.ms-highlight');
    if (existingHighlight) {
      const parent = existingHighlight.parentNode;
      parent.replaceChild(document.createTextNode(existingHighlight.textContent), existingHighlight);
      parent.normalize(); // Merge adjacent text nodes
    }
  }

  // Function to handle text selection
  function handleSelection() {
    const selection = window.getSelection();
    if (selection.toString().length > 0) {
      removeExistingHighlight();

      const range = selection.getRangeAt(0);
      const selectedText = selection.toString();
      const textNodes = getAllTextNodes(document.body);
      const nodeIndex = textNodes.indexOf(range.startContainer);
      const textOffset = range.startOffset;

      // Create a unique identifier for the selection
      const selectionId = encodeSelection(selectedText, nodeIndex, textOffset);

      // Update URL with the selection parameter
      const url = new URL(window.location);
      url.searchParams.set('highlight', selectionId);
      window.history.pushState({}, '', url);

      // Highlight the selected text
      highlightText(selectionId, range);
    }
  }

  // Function to highlight text
  function highlightText(selectionId, range) {
    const span = document.createElement('span');
    span.className = 'ms-highlight';
    span.id = selectionId;
    range.surroundContents(span);
  }

  // Function to highlight and scroll to text based on URL parameter
  function highlightFromURL() {
    removeExistingHighlight();

    const url = new URL(window.location);
    const highlightId = url.searchParams.get('highlight');

    if (highlightId) {
      const { text, nodeIndex, textOffset } = decodeSelection(highlightId);
      const textNodes = getAllTextNodes(document.body);

      if (nodeIndex !== undefined && textOffset !== undefined) {
        // Use precise location if available
        if (nodeIndex < textNodes.length) {
          const node = textNodes[nodeIndex];
          if (node.textContent.substr(textOffset, text.length) === text) {
            const range = document.createRange();
            range.setStart(node, textOffset);
            range.setEnd(node, textOffset + text.length);
            highlightText(highlightId, range);
          }
        }
      } else {
        // Fall back to searching for the first occurrence of the text
        for (let node of textNodes) {
          const index = node.textContent.indexOf(text);
          if (index !== -1) {
            const range = document.createRange();
            range.setStart(node, index);
            range.setEnd(node, index + text.length);
            highlightText(highlightId, range);
            break;
          }
        }
      }

      const highlightedSpan = document.getElementById(highlightId);
      if (highlightedSpan) {
        highlightedSpan.scrollIntoView({ behavior: 'smooth', block: 'center' });
      }
    }
  }

  // Helper function to get all text nodes
  function getAllTextNodes(element) {
    const textNodes = [];
    const walk = document.createTreeWalker(element, NodeFilter.SHOW_TEXT, null, false);
    let node;
    while (node = walk.nextNode()) {
      textNodes.push(node);
    }
    return textNodes;
  }

  // Add event listener for text selection
  document.addEventListener('mouseup', handleSelection);

  // Call highlightFromURL when the page loads
  window.addEventListener('load', highlightFromURL);
</script>
v0.1

<!-- 💙 MEMBERSCRIPT #116 v0.1 💙 - SHARE HIGHLIGHTED TEXT LINKS -->
<script>
  // Function to encode text and position for URL
  function encodeSelection(text, nodeIndex, textOffset) {
    return btoa(encodeURIComponent(JSON.stringify({ text, nodeIndex, textOffset })));
  }

  // Function to decode selection from URL
  function decodeSelection(encoded) {
    try {
      return JSON.parse(decodeURIComponent(atob(encoded)));
    } catch (e) {
      // If parsing fails, assume it's just the text in the old format
      return { text: decodeURIComponent(atob(encoded)) };
    }
  }

  // Function to remove existing highlight
  function removeExistingHighlight() {
    const existingHighlight = document.querySelector('.ms-highlight');
    if (existingHighlight) {
      const parent = existingHighlight.parentNode;
      parent.replaceChild(document.createTextNode(existingHighlight.textContent), existingHighlight);
      parent.normalize(); // Merge adjacent text nodes
    }
  }

  // Function to handle text selection
  function handleSelection() {
    const selection = window.getSelection();
    if (selection.toString().length > 0) {
      removeExistingHighlight();

      const range = selection.getRangeAt(0);
      const selectedText = selection.toString();
      const textNodes = getAllTextNodes(document.body);
      const nodeIndex = textNodes.indexOf(range.startContainer);
      const textOffset = range.startOffset;

      // Create a unique identifier for the selection
      const selectionId = encodeSelection(selectedText, nodeIndex, textOffset);

      // Update URL with the selection parameter
      const url = new URL(window.location);
      url.searchParams.set('highlight', selectionId);
      window.history.pushState({}, '', url);

      // Highlight the selected text
      highlightText(selectionId, range);
    }
  }

  // Function to highlight text
  function highlightText(selectionId, range) {
    const span = document.createElement('span');
    span.className = 'ms-highlight';
    span.id = selectionId;
    range.surroundContents(span);
  }

  // Function to highlight and scroll to text based on URL parameter
  function highlightFromURL() {
    removeExistingHighlight();

    const url = new URL(window.location);
    const highlightId = url.searchParams.get('highlight');

    if (highlightId) {
      const { text, nodeIndex, textOffset } = decodeSelection(highlightId);
      const textNodes = getAllTextNodes(document.body);

      if (nodeIndex !== undefined && textOffset !== undefined) {
        // Use precise location if available
        if (nodeIndex < textNodes.length) {
          const node = textNodes[nodeIndex];
          if (node.textContent.substr(textOffset, text.length) === text) {
            const range = document.createRange();
            range.setStart(node, textOffset);
            range.setEnd(node, textOffset + text.length);
            highlightText(highlightId, range);
          }
        }
      } else {
        // Fall back to searching for the first occurrence of the text
        for (let node of textNodes) {
          const index = node.textContent.indexOf(text);
          if (index !== -1) {
            const range = document.createRange();
            range.setStart(node, index);
            range.setEnd(node, index + text.length);
            highlightText(highlightId, range);
            break;
          }
        }
      }

      const highlightedSpan = document.getElementById(highlightId);
      if (highlightedSpan) {
        highlightedSpan.scrollIntoView({ behavior: 'smooth', block: 'center' });
      }
    }
  }

  // Helper function to get all text nodes
  function getAllTextNodes(element) {
    const textNodes = [];
    const walk = document.createTreeWalker(element, NodeFilter.SHOW_TEXT, null, false);
    let node;
    while (node = walk.nextNode()) {
      textNodes.push(node);
    }
    return textNodes;
  }

  // Add event listener for text selection
  document.addEventListener('mouseup', handleSelection);

  // Call highlightFromURL when the page loads
  window.addEventListener('load', highlightFromURL);
</script>
Ver Memberscript
UX

#115 - Generar una contraseña aleatoria

Registro sin fricciones. Exigir o permitir que los miembros establezcan una contraseña en el futuro.


<!-- 💙 MEMBERSCRIPT #115 v0.1 💙 - GENERATE PASSWORD-->
<script>
document.addEventListener('DOMContentLoaded', function() {
    var passwordInput = document.querySelector('[data-ms-member="password"]');
    
    if (passwordInput) {
        // Function to generate random password
        function generatePassword() {
            var timestamp = Date.now().toString(36);
            var characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()_+{}[]|:;<>,.?/~';
            var randomChars = '';
            for (var i = 0; i < 16; i++) {
                randomChars += characters.charAt(Math.floor(Math.random() * characters.length));
            }
            return (timestamp + randomChars).slice(0, 32);
        }

        // Generate and set password
        passwordInput.value = generatePassword();

        // Block password managers and prevent editing
        passwordInput.setAttribute('autocomplete', 'off');
        passwordInput.setAttribute('readonly', 'readonly');

        // Prevent copy and paste
        passwordInput.addEventListener('copy', function(e) {
            e.preventDefault();
        });
        passwordInput.addEventListener('paste', function(e) {
            e.preventDefault();
        });

        // Prevent dragging
        passwordInput.addEventListener('dragstart', function(e) {
            e.preventDefault();
        });

        // Prevent context menu
        passwordInput.addEventListener('contextmenu', function(e) {
            e.preventDefault();
        });
    }
});
</script>
v0.1

<!-- 💙 MEMBERSCRIPT #115 v0.1 💙 - GENERATE PASSWORD-->
<script>
document.addEventListener('DOMContentLoaded', function() {
    var passwordInput = document.querySelector('[data-ms-member="password"]');
    
    if (passwordInput) {
        // Function to generate random password
        function generatePassword() {
            var timestamp = Date.now().toString(36);
            var characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()_+{}[]|:;<>,.?/~';
            var randomChars = '';
            for (var i = 0; i < 16; i++) {
                randomChars += characters.charAt(Math.floor(Math.random() * characters.length));
            }
            return (timestamp + randomChars).slice(0, 32);
        }

        // Generate and set password
        passwordInput.value = generatePassword();

        // Block password managers and prevent editing
        passwordInput.setAttribute('autocomplete', 'off');
        passwordInput.setAttribute('readonly', 'readonly');

        // Prevent copy and paste
        passwordInput.addEventListener('copy', function(e) {
            e.preventDefault();
        });
        passwordInput.addEventListener('paste', function(e) {
            e.preventDefault();
        });

        // Prevent dragging
        passwordInput.addEventListener('dragstart', function(e) {
            e.preventDefault();
        });

        // Prevent context menu
        passwordInput.addEventListener('contextmenu', function(e) {
            e.preventDefault();
        });
    }
});
</script>
Ver Memberscript
UX

#114 - Botón de desplazamiento al principio

Añade un botón que se desplazará a la parte superior de la página cuando se haga clic en él,


<!-- 💙 MEMBERSCRIPT #114 v0.1 💙 - SCROLL TO TOP BUTTON -->
<script>
document.addEventListener('DOMContentLoaded', function() {
    var scrollTopButton = document.querySelector('[ms-code-scroll-top="button"]');
    
    if (scrollTopButton) {
        // Set initial styles
        scrollTopButton.style.opacity = '0';
        scrollTopButton.style.visibility = 'hidden';
        scrollTopButton.style.transition = 'opacity 0.3s, visibility 0.3s';

        // Function to check scroll position and toggle button visibility
        function toggleButtonVisibility() {
            if (window.pageYOffset > 300) {
                scrollTopButton.style.opacity = '1';
                scrollTopButton.style.visibility = 'visible';
            } else {
                scrollTopButton.style.opacity = '0';
                scrollTopButton.style.visibility = 'hidden';
            }
        }

        // Initial check on page load
        toggleButtonVisibility();

        // Check on scroll
        window.addEventListener('scroll', toggleButtonVisibility);

        // Scroll to top when button is clicked
        scrollTopButton.addEventListener('click', function() {
            window.scrollTo({
                top: 0,
                behavior: 'smooth'
            });
        });
    }
});
</script>
v0.1

<!-- 💙 MEMBERSCRIPT #114 v0.1 💙 - SCROLL TO TOP BUTTON -->
<script>
document.addEventListener('DOMContentLoaded', function() {
    var scrollTopButton = document.querySelector('[ms-code-scroll-top="button"]');
    
    if (scrollTopButton) {
        // Set initial styles
        scrollTopButton.style.opacity = '0';
        scrollTopButton.style.visibility = 'hidden';
        scrollTopButton.style.transition = 'opacity 0.3s, visibility 0.3s';

        // Function to check scroll position and toggle button visibility
        function toggleButtonVisibility() {
            if (window.pageYOffset > 300) {
                scrollTopButton.style.opacity = '1';
                scrollTopButton.style.visibility = 'visible';
            } else {
                scrollTopButton.style.opacity = '0';
                scrollTopButton.style.visibility = 'hidden';
            }
        }

        // Initial check on page load
        toggleButtonVisibility();

        // Check on scroll
        window.addEventListener('scroll', toggleButtonVisibility);

        // Scroll to top when button is clicked
        scrollTopButton.addEventListener('click', function() {
            window.scrollTo({
                top: 0,
                behavior: 'smooth'
            });
        });
    }
});
</script>
Ver Memberscript
Integración

#113 - Canales RSS

Utilice una interfaz de usuario Webflow para mostrar un canal RSS directamente en su sitio web.


<!-- 💙 MEMBERSCRIPT #113 v0.2 💙 - RSS FEEDS IN WEBFLOW -->
<script>
(function() {
  // console.log('RSS Feed Script starting...');

  const CORS_PROXIES = [
    'https://corsproxy.io/?',
    'https://api.allorigins.win/raw?url=',
    'https://cors-anywhere.herokuapp.com/',
    'https://thingproxy.freeboard.io/fetch/',
    'https://yacdn.org/proxy/'
  ];

  function loadScript(src, onLoad, onError) {
    const script = document.createElement('script');
    script.src = src;
    script.onload = onLoad;
    script.onerror = onError;
    document.head.appendChild(script);
  }

  async function fetchWithFallback(url) {
    for (const proxy of CORS_PROXIES) {
      try {
        const response = await fetch(proxy + encodeURIComponent(url));
        if (response.ok) {
          return await response.text();
        }
      } catch (error) {
        console.warn(`Failed to fetch with proxy ${proxy}:`, error);
      }
    }
    throw new Error('All CORS proxies failed');
  }

  function initRSSFeed() {
    if (typeof RSSParser === 'undefined') {
      console.error('RSSParser is not defined.');
      return;
    }

    const parser = new RSSParser({
      customFields: {
        item: [
          ['media:content', 'mediaContent', {keepArray: true}],
          ['media:thumbnail', 'mediaThumbnail', {keepArray: true}],
          ['enclosure', 'enclosure', {keepArray: true}],
        ]
      }
    });

    document.querySelectorAll('[ms-code-rss-feed]').forEach(element => {
      const url = element.getAttribute('ms-code-rss-url');
      const limit = parseInt(element.getAttribute('ms-code-rss-limit')) || 5;

      fetchWithFallback(url)
        .then(str => parser.parseString(str))
        .then(feed => {
          renderRSSItems(element, feed.items.slice(0, limit), {
            showImage: element.getAttribute('ms-code-rss-show-image') !== 'false',
            showDate: element.getAttribute('ms-code-rss-show-date') !== 'false',
            dateFormat: element.getAttribute('ms-code-rss-date-format') || 'short',
            target: element.getAttribute('ms-code-rss-target') || '_self'
          });
        })
        .catch(err => {
          console.error('Error fetching or parsing RSS feed:', err);
          element.textContent = `Failed to load RSS feed from ${url}. Error: ${err.message}`;
        });
    });
  }

  function renderRSSItems(element, items, options) {
    const templateItem = element.querySelector('[ms-code-rss-item]');
    if (!templateItem) return;

    element.innerHTML = ''; // Clear existing items

    items.forEach(item => {
      const itemElement = templateItem.cloneNode(true);

      const title = itemElement.querySelector('[ms-code-rss-title]');
      if (title) {
        const titleLength = parseInt(title.getAttribute('ms-code-rss-title-length')) || Infinity;
        title.textContent = truncate(item.title, titleLength);
      }

      const description = itemElement.querySelector('[ms-code-rss-description]');
      if (description) {
        const descriptionLength = parseInt(description.getAttribute('ms-code-rss-description-length')) || Infinity;
        description.textContent = truncate(stripHtml(item.content || item.description), descriptionLength);
      }

      const date = itemElement.querySelector('[ms-code-rss-date]');
      if (date && options.showDate && item.pubDate) {
        date.textContent = formatDate(new Date(item.pubDate), options.dateFormat);
      }

      const img = itemElement.querySelector('[ms-code-rss-image]');
      if (img && options.showImage) {
        const imgUrl = getImageUrl(item);
        if (imgUrl) {
          img.src = imgUrl;
          img.alt = item.title;
          img.removeAttribute('srcset');
        }
      }

      const linkElement = itemElement.querySelector('[ms-code-rss-link]');
      if (linkElement) {
        linkElement.setAttribute('href', item.link);
        linkElement.setAttribute('target', options.target);
      }

      element.appendChild(itemElement);
    });
  }

  function getImageUrl(item) {
    const sources = ['mediaContent', 'mediaThumbnail', 'enclosure'];
    for (let source of sources) {
      if (item[source] && item[source][0]) {
        return item[source][0].$ ? item[source][0].$.url : item[source][0].url;
      }
    }
    return null;
  }

  function truncate(str, length) {
    if (!str) return '';
    if (length === Infinity) return str;
    return str.length > length ? str.slice(0, length) + '...' : str;
  }

  function stripHtml(html) {
    const tmp = document.createElement('DIV');
    tmp.innerHTML = html || '';
    return tmp.textContent || tmp.innerText || '';
  }

  function formatDate(date, format) {
    if (!(date instanceof Date) || isNaN(date)) return '';
    const options = format === 'long' ? 
          { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' } : 
    undefined;
    return format === 'relative' ? getRelativeTimeString(date) : date.toLocaleDateString(undefined, options);
  }

  function getRelativeTimeString(date, lang = navigator.language) {
    const timeMs = date.getTime();
    const deltaSeconds = Math.round((timeMs - Date.now()) / 1000);
    const cutoffs = [60, 3600, 86400, 86400 * 7, 86400 * 30, 86400 * 365, Infinity];
    const units = ['second', 'minute', 'hour', 'day', 'week', 'month', 'year'];
    const unitIndex = cutoffs.findIndex(cutoff => cutoff > Math.abs(deltaSeconds));
    const divisor = unitIndex ? cutoffs[unitIndex - 1] : 1;
    const rtf = new Intl.RelativeTimeFormat(lang, { numeric: 'auto' });
    return rtf.format(Math.floor(deltaSeconds / divisor), units[unitIndex]);
  }

  loadScript('https://cdn.jsdelivr.net/npm/rss-parser@3.12.0/dist/rss-parser.min.js', initRSSFeed, () => {
    console.error('Error loading RSS Parser script');
    loadScript('https://unpkg.com/rss-parser@3.12.0/dist/rss-parser.min.js', initRSSFeed, () => {
      console.error('Error loading RSS Parser script from backup CDN');
    });
  });

})();
</script>
v0.2

<!-- 💙 MEMBERSCRIPT #113 v0.2 💙 - RSS FEEDS IN WEBFLOW -->
<script>
(function() {
  // console.log('RSS Feed Script starting...');

  const CORS_PROXIES = [
    'https://corsproxy.io/?',
    'https://api.allorigins.win/raw?url=',
    'https://cors-anywhere.herokuapp.com/',
    'https://thingproxy.freeboard.io/fetch/',
    'https://yacdn.org/proxy/'
  ];

  function loadScript(src, onLoad, onError) {
    const script = document.createElement('script');
    script.src = src;
    script.onload = onLoad;
    script.onerror = onError;
    document.head.appendChild(script);
  }

  async function fetchWithFallback(url) {
    for (const proxy of CORS_PROXIES) {
      try {
        const response = await fetch(proxy + encodeURIComponent(url));
        if (response.ok) {
          return await response.text();
        }
      } catch (error) {
        console.warn(`Failed to fetch with proxy ${proxy}:`, error);
      }
    }
    throw new Error('All CORS proxies failed');
  }

  function initRSSFeed() {
    if (typeof RSSParser === 'undefined') {
      console.error('RSSParser is not defined.');
      return;
    }

    const parser = new RSSParser({
      customFields: {
        item: [
          ['media:content', 'mediaContent', {keepArray: true}],
          ['media:thumbnail', 'mediaThumbnail', {keepArray: true}],
          ['enclosure', 'enclosure', {keepArray: true}],
        ]
      }
    });

    document.querySelectorAll('[ms-code-rss-feed]').forEach(element => {
      const url = element.getAttribute('ms-code-rss-url');
      const limit = parseInt(element.getAttribute('ms-code-rss-limit')) || 5;

      fetchWithFallback(url)
        .then(str => parser.parseString(str))
        .then(feed => {
          renderRSSItems(element, feed.items.slice(0, limit), {
            showImage: element.getAttribute('ms-code-rss-show-image') !== 'false',
            showDate: element.getAttribute('ms-code-rss-show-date') !== 'false',
            dateFormat: element.getAttribute('ms-code-rss-date-format') || 'short',
            target: element.getAttribute('ms-code-rss-target') || '_self'
          });
        })
        .catch(err => {
          console.error('Error fetching or parsing RSS feed:', err);
          element.textContent = `Failed to load RSS feed from ${url}. Error: ${err.message}`;
        });
    });
  }

  function renderRSSItems(element, items, options) {
    const templateItem = element.querySelector('[ms-code-rss-item]');
    if (!templateItem) return;

    element.innerHTML = ''; // Clear existing items

    items.forEach(item => {
      const itemElement = templateItem.cloneNode(true);

      const title = itemElement.querySelector('[ms-code-rss-title]');
      if (title) {
        const titleLength = parseInt(title.getAttribute('ms-code-rss-title-length')) || Infinity;
        title.textContent = truncate(item.title, titleLength);
      }

      const description = itemElement.querySelector('[ms-code-rss-description]');
      if (description) {
        const descriptionLength = parseInt(description.getAttribute('ms-code-rss-description-length')) || Infinity;
        description.textContent = truncate(stripHtml(item.content || item.description), descriptionLength);
      }

      const date = itemElement.querySelector('[ms-code-rss-date]');
      if (date && options.showDate && item.pubDate) {
        date.textContent = formatDate(new Date(item.pubDate), options.dateFormat);
      }

      const img = itemElement.querySelector('[ms-code-rss-image]');
      if (img && options.showImage) {
        const imgUrl = getImageUrl(item);
        if (imgUrl) {
          img.src = imgUrl;
          img.alt = item.title;
          img.removeAttribute('srcset');
        }
      }

      const linkElement = itemElement.querySelector('[ms-code-rss-link]');
      if (linkElement) {
        linkElement.setAttribute('href', item.link);
        linkElement.setAttribute('target', options.target);
      }

      element.appendChild(itemElement);
    });
  }

  function getImageUrl(item) {
    const sources = ['mediaContent', 'mediaThumbnail', 'enclosure'];
    for (let source of sources) {
      if (item[source] && item[source][0]) {
        return item[source][0].$ ? item[source][0].$.url : item[source][0].url;
      }
    }
    return null;
  }

  function truncate(str, length) {
    if (!str) return '';
    if (length === Infinity) return str;
    return str.length > length ? str.slice(0, length) + '...' : str;
  }

  function stripHtml(html) {
    const tmp = document.createElement('DIV');
    tmp.innerHTML = html || '';
    return tmp.textContent || tmp.innerText || '';
  }

  function formatDate(date, format) {
    if (!(date instanceof Date) || isNaN(date)) return '';
    const options = format === 'long' ? 
          { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' } : 
    undefined;
    return format === 'relative' ? getRelativeTimeString(date) : date.toLocaleDateString(undefined, options);
  }

  function getRelativeTimeString(date, lang = navigator.language) {
    const timeMs = date.getTime();
    const deltaSeconds = Math.round((timeMs - Date.now()) / 1000);
    const cutoffs = [60, 3600, 86400, 86400 * 7, 86400 * 30, 86400 * 365, Infinity];
    const units = ['second', 'minute', 'hour', 'day', 'week', 'month', 'year'];
    const unitIndex = cutoffs.findIndex(cutoff => cutoff > Math.abs(deltaSeconds));
    const divisor = unitIndex ? cutoffs[unitIndex - 1] : 1;
    const rtf = new Intl.RelativeTimeFormat(lang, { numeric: 'auto' });
    return rtf.format(Math.floor(deltaSeconds / divisor), units[unitIndex]);
  }

  loadScript('https://cdn.jsdelivr.net/npm/rss-parser@3.12.0/dist/rss-parser.min.js', initRSSFeed, () => {
    console.error('Error loading RSS Parser script');
    loadScript('https://unpkg.com/rss-parser@3.12.0/dist/rss-parser.min.js', initRSSFeed, () => {
      console.error('Error loading RSS Parser script from backup CDN');
    });
  });

})();
</script>
Ver Memberscript
Marketing

#112 - Deslizadores Antes y Después

Añada fácilmente un deslizador de fotos de antes/después a su sitio Webflow.


<!-- 💙 MEMBERSCRIPT #112 v0.1 💙 - BEFORE & AFTER SLIDERS -->
<script>
document.addEventListener('DOMContentLoaded', () => {
    const wraps = document.querySelectorAll('[ms-code-ba-wrap]');

    wraps.forEach(wrap => {
        const before = wrap.querySelector('[ms-code-ba-before]');
        const after = wrap.querySelector('[ms-code-ba-after]');
        
        // Create slider element
        const slider = document.createElement('div');
        slider.setAttribute('ms-code-ba-slider', wrap.getAttribute('ms-code-ba-wrap'));
        wrap.appendChild(slider);

        let isDown = false;

        // Ensure proper positioning
        wrap.style.position = 'relative';
        wrap.style.overflow = 'hidden';
        before.style.width = '100%';
        before.style.display = 'block';
        after.style.position = 'absolute';
        after.style.top = '0';
        after.style.left = '0';
        after.style.width = '100%';
        after.style.height = '100%';
        slider.style.position = 'absolute';
        slider.style.top = '0';
        slider.style.bottom = '0';
        slider.style.width = '4px';
        slider.style.background = 'white';
        slider.style.cursor = 'ew-resize';
        slider.style.zIndex = '3';

        const setPosition = (position) => {
            const clampedPosition = Math.max(0, Math.min(1, position));
            slider.style.left = `${clampedPosition * 100}%`;
            after.style.clipPath = `inset(0 0 0 ${clampedPosition * 100}%)`;
        };

        const move = (e) => {
            if (!isDown && e.type !== 'mousemove') return;
            e.preventDefault();

            const x = e.type.includes('touch') ? e.touches[0].clientX : e.clientX;
            const rect = wrap.getBoundingClientRect();
            const position = (x - rect.left) / rect.width;
            
            setPosition(position);
        };

        const easeBack = () => {
            setPosition(0.5); // Move back to center
        };

        wrap.addEventListener('mousedown', () => isDown = true);
        wrap.addEventListener('mouseup', () => isDown = false);
        wrap.addEventListener('mouseleave', () => {
            isDown = false;
            easeBack();
        });
        wrap.addEventListener('mousemove', move);

        wrap.addEventListener('touchstart', (e) => {
            isDown = true;
            move(e);
        });
        wrap.addEventListener('touchmove', move);
        wrap.addEventListener('touchend', () => {
            isDown = false;
            easeBack();
        });

        // Initialize position
        setPosition(0.5);
    });
});
</script>
v0.1

<!-- 💙 MEMBERSCRIPT #112 v0.1 💙 - BEFORE & AFTER SLIDERS -->
<script>
document.addEventListener('DOMContentLoaded', () => {
    const wraps = document.querySelectorAll('[ms-code-ba-wrap]');

    wraps.forEach(wrap => {
        const before = wrap.querySelector('[ms-code-ba-before]');
        const after = wrap.querySelector('[ms-code-ba-after]');
        
        // Create slider element
        const slider = document.createElement('div');
        slider.setAttribute('ms-code-ba-slider', wrap.getAttribute('ms-code-ba-wrap'));
        wrap.appendChild(slider);

        let isDown = false;

        // Ensure proper positioning
        wrap.style.position = 'relative';
        wrap.style.overflow = 'hidden';
        before.style.width = '100%';
        before.style.display = 'block';
        after.style.position = 'absolute';
        after.style.top = '0';
        after.style.left = '0';
        after.style.width = '100%';
        after.style.height = '100%';
        slider.style.position = 'absolute';
        slider.style.top = '0';
        slider.style.bottom = '0';
        slider.style.width = '4px';
        slider.style.background = 'white';
        slider.style.cursor = 'ew-resize';
        slider.style.zIndex = '3';

        const setPosition = (position) => {
            const clampedPosition = Math.max(0, Math.min(1, position));
            slider.style.left = `${clampedPosition * 100}%`;
            after.style.clipPath = `inset(0 0 0 ${clampedPosition * 100}%)`;
        };

        const move = (e) => {
            if (!isDown && e.type !== 'mousemove') return;
            e.preventDefault();

            const x = e.type.includes('touch') ? e.touches[0].clientX : e.clientX;
            const rect = wrap.getBoundingClientRect();
            const position = (x - rect.left) / rect.width;
            
            setPosition(position);
        };

        const easeBack = () => {
            setPosition(0.5); // Move back to center
        };

        wrap.addEventListener('mousedown', () => isDown = true);
        wrap.addEventListener('mouseup', () => isDown = false);
        wrap.addEventListener('mouseleave', () => {
            isDown = false;
            easeBack();
        });
        wrap.addEventListener('mousemove', move);

        wrap.addEventListener('touchstart', (e) => {
            isDown = true;
            move(e);
        });
        wrap.addEventListener('touchmove', move);
        wrap.addEventListener('touchend', () => {
            isDown = false;
            easeBack();
        });

        // Initialize position
        setPosition(0.5);
    });
});
</script>
Ver Memberscript
No hemos podido encontrar ningún script para esa búsqueda... por favor, inténtalo de nuevo.
Slack

¿Necesitas ayuda con MemberScripts? ¡Únete a nuestra comunidad Slack de más de 5.500 miembros! 🙌

Los MemberScripts son un recurso comunitario de Memberstack - si necesitas ayuda para que funcionen con tu proyecto, ¡únete al Slack de Memberstack 2.0 y pide ayuda!

Únete a nuestro Slack
Escaparate

Explore empresas reales que han tenido éxito con Memberstack

No se fíe sólo de nuestra palabra: eche un vistazo a las empresas de todos los tamaños que confían en Memberstack para su autenticación y sus pagos.

Ver todas las historias de éxito
Incluso Webflow utiliza Memberstack.
Empezar a construir

Empieza a construir tus sueños

Memberstack es 100% gratis hasta que estés listo para lanzarla - así que, ¿a qué estás esperando? Crea tu primera aplicación y empieza a construir hoy mismo.