blob: c755c085c4bf48051a0d89ecef2b92c0e8c05c93 [file] [edit]
/**
* --------------------------------------------------------------------------
* Bootstrap strength.ts
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* --------------------------------------------------------------------------
*/
import BaseComponent from './base-component.js'
import EventHandler from './dom/event-handler.js'
import SelectorEngine from './dom/selector-engine.js'
/**
* Constants
*/
const NAME = 'strength'
const DATA_KEY = 'bs.strength'
const EVENT_KEY = `.${DATA_KEY}`
const DATA_API_KEY = '.data-api'
const EVENT_STRENGTH_CHANGE = `strengthChange${EVENT_KEY}`
const SELECTOR_DATA_STRENGTH = '[data-bs-strength]'
const STRENGTH_LEVELS = ['weak', 'fair', 'good', 'strong']
type StrengthConfig = {
input: string | HTMLInputElement | null
minLength: number
messages: Record<string, string>
weights: Record<string, number>
thresholds: number[]
scorer: ((password: string) => number) | null
}
const Default: StrengthConfig = {
input: null, // Selector or element for password input
minLength: 8,
messages: {
weak: 'Weak',
fair: 'Fair',
good: 'Good',
strong: 'Strong'
},
weights: {
minLength: 1,
extraLength: 1,
lowercase: 1,
uppercase: 1,
numbers: 1,
special: 1,
multipleSpecial: 1,
longPassword: 1
},
thresholds: [2, 4, 6], // weak ≤2, fair ≤4, good ≤6, strong >6
scorer: null // Custom scoring function (password) => number
}
const DefaultType = {
input: '(string|element|null)',
minLength: 'number',
messages: 'object',
weights: 'object',
thresholds: 'array',
scorer: '(function|null)'
}
/**
* Class definition
*/
class Strength extends BaseComponent {
protected declare _config: StrengthConfig
protected declare _input: HTMLInputElement | null
protected declare _segments: HTMLElement[]
protected declare _textElement: HTMLElement | null
protected declare _currentStrength: string | null
constructor(element?: string | Element | null, config?: Partial<StrengthConfig> | null) {
super(element, config)
this._input = this._getInput()
this._segments = SelectorEngine.find('.strength-segment', this._element)
this._textElement = SelectorEngine.findOne('.strength-text', this._element.parentElement!)
this._currentStrength = null
if (this._input) {
this._addEventListeners()
// Check initial value
this._evaluate()
}
}
// Getters
static override get Default(): StrengthConfig {
return Default
}
static override get DefaultType(): Record<string, string> {
return DefaultType
}
static override get NAME(): string {
return NAME
}
// Public
getStrength(): string | null {
return this._currentStrength
}
evaluate(): void {
this._evaluate()
}
// Private
protected _getInput(): HTMLInputElement | null {
if (this._config.input) {
return typeof this._config.input === 'string' ?
SelectorEngine.findOne<HTMLInputElement>(this._config.input) :
this._config.input
}
// Look for preceding password input
const parent = this._element.parentElement
return SelectorEngine.findOne<HTMLInputElement>('input[type="password"]', parent!)
}
protected _addEventListeners(): void {
EventHandler.on(this._input, 'input', () => this._evaluate())
EventHandler.on(this._input, 'change', () => this._evaluate())
}
protected _evaluate(): void {
const password = this._input!.value
const score = this._calculateScore(password)
const strength = this._scoreToStrength(score)
if (strength !== this._currentStrength) {
this._currentStrength = strength
this._updateUI(strength)
EventHandler.trigger(this._element, EVENT_STRENGTH_CHANGE, {
strength,
score,
password: password.length > 0 ? '***' : '' // Don't expose actual password
})
}
}
protected _calculateScore(password: string): number {
if (!password) {
return 0
}
// Use custom scorer if provided
if (typeof this._config.scorer === 'function') {
return this._config.scorer(password)
}
const { weights } = this._config
let score = 0
// Length scoring
if (password.length >= this._config.minLength) {
score += weights.minLength
}
if (password.length >= this._config.minLength + 4) {
score += weights.extraLength
}
// Character variety
if (/[a-z]/.test(password)) {
score += weights.lowercase
}
if (/[A-Z]/.test(password)) {
score += weights.uppercase
}
if (/\d/.test(password)) {
score += weights.numbers
}
// Special characters
if (/[!@#$%^&*(),.?":{}|<>]/.test(password)) {
score += weights.special
}
// Extra points for more special chars or length
if (/[!@#$%^&*(),.?":{}|<>].*[!@#$%^&*(),.?":{}|<>]/.test(password)) {
score += weights.multipleSpecial
}
if (password.length >= 16) {
score += weights.longPassword
}
return score
}
protected _scoreToStrength(score: number): string | null {
if (score === 0) {
return null
}
const [weak, fair, good] = this._config.thresholds
if (score <= weak) {
return 'weak'
}
if (score <= fair) {
return 'fair'
}
if (score <= good) {
return 'good'
}
return 'strong'
}
protected _updateUI(strength: string | null): void {
// Update data attribute on element
if (strength) {
this._element.dataset.bsStrength = strength
} else {
delete this._element.dataset.bsStrength
}
// Update segmented meter
const strengthIndex = strength ? STRENGTH_LEVELS.indexOf(strength) : -1
for (const [index, segment] of this._segments.entries()) {
if (index <= strengthIndex) {
segment.classList.add('active')
} else {
segment.classList.remove('active')
}
}
// Update text feedback
if (this._textElement) {
if (strength && this._config.messages[strength]) {
this._textElement.textContent = this._config.messages[strength]
this._textElement.dataset.bsStrength = strength
// Also set the color via inheriting from parent or using CSS variable
const colorMap: Record<string, string> = {
weak: 'danger',
fair: 'warning',
good: 'info',
strong: 'success'
}
this._textElement.style.setProperty('--strength-color', `var(--${colorMap[strength]}-text)`)
} else {
this._textElement.textContent = ''
delete this._textElement.dataset.bsStrength
}
}
}
}
/**
* Data API implementation
*/
EventHandler.on(document, `DOMContentLoaded${EVENT_KEY}${DATA_API_KEY}`, () => {
for (const element of SelectorEngine.find(SELECTOR_DATA_STRENGTH)) {
Strength.getOrCreateInstance(element)
}
})
export default Strength
export type { StrengthConfig }