bootstrap_five_layouts-1.0.x-dev/js/behaviour.bsflPillBoxCounter.js
js/behaviour.bsflPillBoxCounter.js
/**
* @file
* Character counter behavior for inputs.
*/
(function (Drupal) {
'use strict';
class CharacterCounter {
constructor(inputElement) {
this.input = inputElement;
this.maxLength = this.parseMaxLength(inputElement.getAttribute('maxlength'));
this.counterEl = this.createCounterElement();
this.insertAfterInput();
this.update();
this.attachEvents();
}
parseMaxLength(attrValue) {
if (!attrValue) {
return null;
}
const parsed = parseInt(attrValue, 10);
return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
}
createCounterElement() {
const el = document.createElement('div');
el.className = 'bsfl-pillbox-counter';
el.id = 'bsfl-counter-' + Math.random().toString(36).substr(2, 9);
el.setAttribute('aria-live', 'polite');
el.setAttribute('aria-atomic', 'true');
el.setAttribute('role', 'status');
el.setAttribute('aria-label', Drupal.t('Character count'));
return el;
}
insertAfterInput() {
let wrapper = null;
if (this.input && typeof this.input.closest === 'function') {
wrapper = this.input.closest('.bsfl-pillbox-wrapper, .form-item, .js-form-item, .form-group, .fieldset-wrapper');
}
if (!wrapper && this.input) {
wrapper = this.input.parentNode || null;
}
if (wrapper) {
wrapper.appendChild(this.counterEl);
// Connect the input to the counter for better screen reader navigation
const describedBy = this.input.getAttribute('aria-describedby') || '';
this.input.setAttribute('aria-describedby', describedBy + ' ' + this.counterEl.id);
}
}
formatMessage(current, max) {
if (max !== null) {
const message = Drupal.t('@current of @max characters', { '@current': current, '@max': max });
// Add context for screen readers
return Drupal.t('@current/@max', { '@current': current, '@max': max });
}
return Drupal.t('@current characters', { '@current': current });
}
update() {
const currentLength = (this.input && typeof this.input.value === 'string') ? this.input.value.length : 0;
const message = this.formatMessage(currentLength, this.maxLength);
this.counterEl.textContent = message;
// Hide counter when empty, show when has content
if (currentLength === 0) {
this.counterEl.style.display = 'none';
this.counterEl.setAttribute('aria-hidden', 'true');
// Remove counter from aria-describedby when hidden
this.updateAriaDescribedBy('');
} else {
this.counterEl.style.display = '';
this.counterEl.removeAttribute('aria-hidden');
// Restore counter in aria-describedby when visible
this.updateAriaDescribedBy(this.counterEl.id);
}
// Announce when approaching limit for better accessibility
if (this.maxLength && currentLength > this.maxLength * 0.8) {
this.counterEl.setAttribute('aria-live', 'assertive');
} else {
this.counterEl.setAttribute('aria-live', 'polite');
}
}
updateAriaDescribedBy(counterId) {
const describedBy = this.input.getAttribute('aria-describedby') || '';
let ids = describedBy.split(' ').filter(id => id.trim() !== '' && id !== this.counterEl.id);
if (counterId) {
ids.push(counterId);
}
if (ids.length > 0) {
this.input.setAttribute('aria-describedby', ids.join(' '));
} else {
this.input.removeAttribute('aria-describedby');
}
}
attachEvents() {
this.input.addEventListener('input', () => this.update());
this.input.addEventListener('change', () => this.update());
}
}
function findInputs(context) {
const results = [];
if (context && typeof context.matches === 'function' && context.matches('input[data-pillbox-counter]')) {
results.push(context);
}
const descendants = context.querySelectorAll ? context.querySelectorAll('input[data-pillbox-counter]') : [];
descendants.forEach(el => results.push(el));
return results;
}
Drupal.behaviors.bsflPillBoxCounter = {
attach: function (context) {
const inputs = findInputs(context);
inputs.forEach(input => {
if (input.disabled) {
return;
}
if (input.hasAttribute('data-bsfl-pillbox-counter-processed')) {
return;
}
input.setAttribute('data-bsfl-pillbox-counter-processed', 'true');
input.bsflCharacterCounter = new CharacterCounter(input);
});
}
};
Drupal.bsflPillBoxCounter = {
enhance: function(selector) {
const elements = document.querySelectorAll(selector || 'input[data-pillbox-counter]');
elements.forEach(element => {
if (!element.hasAttribute('data-bsfl-pillbox-counter-processed')) {
element.setAttribute('data-pillbox-counter', 'true');
Drupal.behaviors.bsflPillBoxCounter.attach(document);
}
});
}
};
})(Drupal);
