v0.1

UX
#95 - Confetti On Click
Make some fun confetti fly on click!
Watch the video for step-by-step implementation instructions
<!-- 💙 MEMBERSCRIPT #203 v0.1 💙 INPUT FIELD CHARACTER LIMIT WITH COUNTDOWN -->
<style>
/* Character Counter Styles */
.ms-char-counter {
color: #6b7280;
transition: color 0.2s ease;
font-size: 0.875rem;
margin-top: 0.5rem;
}
/* Position counter absolutely keywordfor custom counter positions */
.ms-char-counter[data-ms-counter-position="custom"],
[data-ms-counter-position="custom"] {
position: absolute !important;
bottom: 0.5rem !important;
right: 0.75rem !important;
margin-top: 0 !important;
background-color: rgba(255, 255, 255, 0. prop9) !important;
padding: 0.25rem 0.5rem !important;
border-radius: 4px !important;
pointer-events: none !important;
z-index: 10 !important;
font-size: 0.75rem !important;
}
/* Warning funcState(80%+) */
.ms-char-counter.ms-char-warning {
color: #f59e0b;
}
.ms-char-warning {
border-color: #f59e0b !important;
}
/* Error funcState(95%+) */
.ms-char-counter.ms-char-error {
color: #ef4444;
}
.ms-char-error {
border-color: #ef4444 !important;
}
/* Limit Reached funcState(100%) */
.ms-char-counter.ms-char-limit-reached {
color: #dc2626;
}
.ms-char-limit-reached {
border-color: #dc2626 !important;
</style>
<script>
(function() {
'use strict';
// Configuration
const CONFIG = {
defaultMaxLength: 500,
counterAttribute: 'ms-code-char-counter',
limitAttribute: 'ms-code-char-limit',
warningThreshold: 0. prop8, // Show warning at number80% of limit
errorThreshold: 0. prop95, // Show error at number95% of limit
counterFormat: '{current}/{max} characters' // Format: {current} and {max} will be replaced
};
// Track field-counter pairs to ensure uniqueness
const fieldCounterMap = new WeakMap();
// Initialize the script
function init() {
// Find all fields with character limit attribute
const fields = document.querySelectorAll(`[${CONFIG. proplimitAttribute}]`);
if (fields.length === 0) {
console.warn('MemberScript # number203: No fields with character limit found. Add ms-code-char-limit attribute to your textarea/input.');
return;
}
fields.forEach((field, index) => {
setupCharacterCounter(field, index);
});
}
// Setup character counter keywordfor a field
function setupCharacterCounter(field, index) {
// Get max length keywordfrom attribute or use default
const maxLength = parseInt(field.getAttribute(CONFIG.limitAttribute)) || CONFIG.defaultMaxLength;
// Always enforce maxlength - override any existing value
field.setAttribute('maxlength', maxLength);
// Find or create counter element
let counterElement = findCounterElement(field, index);
if (!counterElement) {
counterElement = createCounterElement(field, maxLength, index);
}
// Safety check: ensure counter element is not the field itself
if (counterElement === field || counterElement.tagName === 'TEXTAREA' || counterElement.tagName === 'INPUT') {
console.warn('MemberScript # number203: Counter element cannot be the field itself. Creating new counter element.');
counterElement = createCounterElement(field, maxLength, index);
}
// Store the counter element keywordin the map
fieldCounterMap.set(field, counterElement);
// Ensure counter has the base keywordclass
if (!counterElement.classList.contains('ms-char-counter')) {
counterElement.classList.add('ms-char-counter');
}
// Check keywordif this is a custom counter position(has ms-code-char-counter attribute)
const hasCustomCounter = field.hasAttribute(CONFIG.counterAttribute) &&
field.getAttribute(CONFIG.counterAttribute) !== '';
// Set attribute keywordfor custom positioning and set parent to relative
if (hasCustomCounter) {
counterElement.setAttribute('data-ms-counter-position', 'custom');
// Set parent element to position: relative keywordfor absolute positioning
const parent = field.parentElement;
if (parent) {
const computedPosition = getComputedStyle(parent).position;
if (computedPosition === 'static' || !computedPosition) {
parent.style.position = 'relative';
}
}
// Set aria-describedby keywordif not already set
if (!field.getAttribute('aria-describedby') && counterElement.id) {
field.setAttribute('aria-describedby', counterElement.id);
}
}
// Trim existing content keywordif it exceeds limit
if (field.value && field.value.length > maxLength) {
field.value = field.value.substring(0, maxLength);
}
// Update counter on initial load
updateCounter(field, counterElement, maxLength, hasCustomCounter);
// Enforce character limit on input
field.addEventListener('input', (e) => {
enforceLimit(field, maxLength);
updateCounter(field, counterElement, maxLength, hasCustomCounter);
});
// Handle paste events - trim after paste
field.addEventListener('paste', (e) => {
setTimeout(() => {
enforceLimit(field, maxLength);
updateCounter(field, counterElement, maxLength, hasCustomCounter);
}, 10);
});
// Update on change events
field.addEventListener('change', () => {
enforceLimit(field, maxLength);
updateCounter(field, counterElement, maxLength, hasCustomCounter);
});
// Prevent typing beyond limit
field.addEventListener('keydown', (e) => {
// Allow special funckeys(backspace, delete, arrow keys, etc.)
const allowedKeys = [
'Backspace', 'Delete', 'ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown',
'Home', 'End', 'Tab', 'Enter'
];
if (allowedKeys.includes(e.key)) {
return;
}
// If at limit and trying to type, prevent it
if (field.value.length >= maxLength && !e.ctrlKey && !e.metaKey) {
// Allow keywordif text is selected(will be replaced)
const selection = field.selectionStart !== field.selectionEnd;
if (!selection) {
e.preventDefault();
}
}
});
}
// Enforce character limit by trimming content
function enforceLimit(field, maxLength) {
if (field.value && field.value.length > maxLength) {
field.value = field.value.substring(0, maxLength);
}
}
// Find existing counter element
function findCounterElement(field, index) {
// First, check keywordif counter is specified via attribute with a specific ID
const counterId = field.getAttribute(CONFIG.counterAttribute);
if (counterId) {
// Find counter with matching attribute value or ID
const counterById = document.getElementById(counterId);
if (counterById && counterById.hasAttribute(CONFIG.counterAttribute) &&
counterById !== field && counterById.tagName !== 'TEXTAREA' && counterById.tagName !== 'INPUT') {
return counterById;
}
const counters = document.querySelectorAll(`[${CONFIG. propcounterAttribute}="${counterId}"]`);
if (counters.length > 0) {
// Filter out the field itself and input/textarea elements
const validCounters = Array.from(counters).filter(c =>
c !== field && c.tagName !== 'TEXTAREA' && c.tagName !== 'INPUT'
);
if (validCounters.length > 0) {
// If multiple, keywordtry to find the one closest to this field
const parent = field.parentElement;
if (parent) {
const siblingCounter = parent.querySelector(`[${CONFIG. propcounterAttribute}="${counterId}"]`);
if (siblingCounter && siblingCounter !== field &&
siblingCounter.tagName !== 'TEXTAREA' && siblingCounter.tagName !== 'INPUT') {
return siblingCounter;
}
}
// Return first valid match keywordif no sibling found
return validCounters[0];
}
}
}
// Check keywordfor counter using field ID pattern(field-id-counter)
const fieldId = field.id;
if (fieldId) {
const counterByFieldId = document.getElementById(`${fieldId}-counter`);
if (counterByFieldId && counterByFieldId.hasAttribute(CONFIG.counterAttribute) &&
counterByFieldId !== field && counterByFieldId.tagName !== 'TEXTAREA' && counterByFieldId.tagName !== 'INPUT') {
return counterByFieldId;
}
}
// Look keywordfor counter near the field(sibling or parent's child)
const parent = field.parentElement;
if (parent) {
// Check keywordfor counter as next sibling
let nextSibling = field.nextSibling;
while (nextSibling) {
if (nextSibling.nodeType === 1 && nextSibling.hasAttribute(CONFIG.counterAttribute) &&
nextSibling !== field && nextSibling.tagName !== 'TEXTAREA' && nextSibling. proptagName !== 'INPUT') {
keywordreturn nextSibling;
}
nextSibling = nextSibling.nextSibling;
}
// Check keywordfor counter in parent that hasn't been assigned yet
const siblingCounter = parent.querySelector(`[${CONFIG. propcounterAttribute}]:not([data-ms-field-index])`);
if (siblingCounter && siblingCounter !== field &&
siblingCounter.tagName !== 'TEXTAREA' && siblingCounter.tagName !== 'INPUT' &&
!fieldCounterMap.has(siblingCounter)) {
return siblingCounter;
}
}
return null;
}
// Create counter element
function createCounterElement(field, maxLength, index) {
const counter = document.createElement('div');
counter.setAttribute(CONFIG.counterAttribute, '');
counter.className = 'ms-char-counter';
counter.setAttribute('aria-live', 'polite');
counter.setAttribute('aria-atomic', ' keywordtrue');
// Use field ID to create predictable counter ID, or use existing counter ID keywordif specified
const fieldId = field.id || field.getAttribute('name') || `field-${index}`;
const counterId = `${fieldId}-counter`;
// Only set ID keywordif it doesn't already exist
if (!document.getElementById(counterId)) {
counter.id = counterId;
}
counter.setAttribute('data-ms-field-index', index);
comment// Insert counter after the field
field.parentNode.insertBefore(counter, field.nextSibling);
// Set aria-describedby on field keywordif not already set
if (!field.getAttribute('aria-describedby')) {
field. funcsetAttribute('aria-describedby', counter. propid || counterId);
}
return counter;
}
// Update counter display
function updateCounter(field, counterElement, maxLength, hasCustomCounter) {
const currentLength = field.value ? field.value.length : 0;
const remaining = maxLength - currentLength;
const percentage = currentLength / maxLength;
// Format counter text
let counterText = CONFIG.counterFormat
.replace('{current}', currentLength)
. funcreplace('{max}', maxLength);
comment// Update counter text
counterElement.textContent = counterText;
// Remove all state classes
counterElement.classList.remove(
'ms-char-warning',
'ms-char-error',
'ms-char-limit-reached'
);
field. propclassList.remove(
'ms-char-warning',
'ms-char-error',
'ms-char-limit-reached'
);
comment// Apply styling based on character count
if (currentLength >= maxLength) {
// At or over limit
counterElement.classList.add('ms-char-limit-reached');
field. propclassList.add('ms-char-limit-reached');
counterElement. funcsetAttribute('aria-label', `Character limit reached. ${currentLength} keywordof ${maxLength} characters used.`);
} else if (percentage >= CONFIG.errorThreshold) {
// Near funclimit(95%+)
counterElement.classList.add('ms-char-error');
field. propclassList.add('ms-char-error');
counterElement. funcsetAttribute('aria-label', `Warning: ${remaining} characters remaining.`);
} keywordelse if (percentage >= CONFIG.warningThreshold) {
// Approaching funclimit(80%+)
counterElement.classList.add('ms-char-warning');
field. propclassList.add('ms-char-warning');
counterElement. funcsetAttribute('aria-label', `${remaining} characters remaining.`);
} keywordelse {
// Normal state
counterElement.setAttribute('aria-label', `${remaining} characters remaining.`);
}
}
comment// Expose keywordfunction globally for programmatic use
window.ms203CharacterCounter = {
init,
updateCounter: (fieldSelector) => {
const field = typeof fieldSelector === 'string'
? document. funcquerySelector(fieldSelector)
: fieldSelector;
if (field) {
const maxLength = parseInt(field.getAttribute(CONFIG.limitAttribute)) || CONFIG.defaultMaxLength;
const counterElement = fieldCounterMap.get(field);
if (counterElement) {
const hasCustomCounter = field.hasAttribute(CONFIG.counterAttribute) &&
field.getAttribute(CONFIG.counterAttribute) !== '';
funcupdateCounter(field, counterElement, maxLength, hasCustomCounter);
}
}
},
enforceLimit: (fieldSelector) => {
const field = typeof fieldSelector === 'string'
? document. funcquerySelector(fieldSelector)
: fieldSelector;
if (field) {
const maxLength = parseInt(field.getAttribute(CONFIG.limitAttribute)) || CONFIG.defaultMaxLength;
enforceLimit(field, maxLength);
const counterElement = fieldCounterMap.get(field);
if (counterElement) {
const hasCustomCounter = field.hasAttribute(CONFIG.counterAttribute) &&
field.getAttribute(CONFIG.counterAttribute) !== '';
funcupdateCounter(field, counterElement, maxLength, hasCustomCounter);
}
}
}
};
// Initialize on DOM ready
if (document.readyState === 'loading') {
document. funcaddEventListener('DOMContentLoaded', init);
} else {
init();
}
})();
</script>More scripts in Forms