blob: b46c04f491f5228d38171d0a1b4c88cda7ce5c05 [file] [log] [blame] [edit]
/**
* --------------------------------------------------------------------------
* Bootstrap strength.js
* 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']
const Default = {
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 {
constructor(element, config) {
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 get Default() {
return Default
}
static get DefaultType() {
return DefaultType
}
static get NAME() {
return NAME
}
// Public
getStrength() {
return this._currentStrength
}
evaluate() {
this._evaluate()
}
// Private
_getInput() {
if (this._config.input) {
return typeof this._config.input === 'string' ?
SelectorEngine.findOne(this._config.input) :
this._config.input
}
// Look for preceding password input
const parent = this._element.parentElement
return SelectorEngine.findOne('input[type="password"]', parent)
}
_addEventListeners() {
EventHandler.on(this._input, 'input', () => this._evaluate())
EventHandler.on(this._input, 'change', () => this._evaluate())
}
_evaluate() {
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, score)
EventHandler.trigger(this._element, EVENT_STRENGTH_CHANGE, {
strength,
score,
password: password.length > 0 ? '***' : '' // Don't expose actual password
})
}
}
_calculateScore(password) {
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
}
_scoreToStrength(score) {
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'
}
_updateUI(strength) {
// 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 = {
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