bootstrap-italia
Version:
Bootstrap Italia è un tema Bootstrap 5 per la creazione di applicazioni web nel pieno rispetto delle linee guida di design per i siti internet e i servizi digitali della PA
491 lines (432 loc) • 14.8 kB
JavaScript
/**
* --------------------------------------------------------------------------
* Bootstrap Italia (https://italia.github.io/bootstrap-italia/)
* Authors: https://github.com/italia/bootstrap-italia/blob/main/AUTHORS
* Licensed under BSD-3-Clause license (https://github.com/italia/bootstrap-italia/blob/main/LICENSE)
* --------------------------------------------------------------------------
*/
import BaseComponent from './base-component'
import EventHandler from './dom/event-handler'
import SelectorEngine from './dom/selector-engine'
import Manipulator from './dom/manipulator'
import InputLabel from './input-label'
const NAME = 'inputpassword'
const DATA_KEY = 'bs.inputpassword'
const EVENT_KEY = `.${DATA_KEY}`
const DATA_API_KEY = '.data-api'
const Default = {
shortPass: 'Password troppo breve',
badPass: 'Password debole.',
goodPass: 'Password abbastanza sicura.',
strongPass: 'Password sicura.',
minimumLength: 8,
suggestionsLabel: 'Suggerimenti per una buona password:',
suggestionFollowed: 'suggerimenti seguito',
suggestionFollowedPlural: 'suggerimenti seguiti',
suggestionOf: 'di',
suggestionMetLabel: 'Soddisfatto: ',
suggestionMetIconPath: `
M9.6 16.9 4 11.4l.8-.7 4.8 4.8 8.5-8.4.7.7-9.2 9.1z
`,
suggestionLength: 'Almeno {minLength} caratteri.',
suggestionUppercase: 'Una o più maiuscole.',
suggestionLowercase: 'Una o più minuscole.',
suggestionNumber: 'Uno o più numeri.',
suggestionSpecial: 'Uno o più caratteri speciali.',
}
const EVENT_CLICK = `click${EVENT_KEY}`
const EVENT_KEYUP = `keyup${EVENT_KEY}`
const EVENT_SCORE = `score${EVENT_KEY}`
const EVENT_TEXT = `text${EVENT_KEY}`
const EVENT_SUGGS = `suggs${EVENT_KEY}`
const EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`
const EVENT_MOUSEDOWN_DATA_API = `mousedown${EVENT_KEY}${DATA_API_KEY}`
const EVENT_KEYUP_DATA_API = `keyup${EVENT_KEY}${DATA_API_KEY}`
const CLASS_NAME_PASSWORD = 'input-password'
const SELECTOR_PASSWORD = 'input[data-bs-input][type="password"]'
const SELECTOR_BTN_SHOW_PWD = '.password-icon'
const SELECTOR_METER = '.password-strength-meter'
const SELECTOR_METER_GRAYBAR = '.password-meter'
const SELECTOR_METER_COLBAR = '.progress-bar'
const SELECTOR_METER_TEXT = '.strength-meter-info'
const SELECTOR_METER_SUGGS = '.strenght-meter-suggestions'
class InputPassword extends BaseComponent {
constructor(element, config) {
super(element)
this._config = this._getConfig(config)
this._isCustom = this._element.classList.contains(CLASS_NAME_PASSWORD)
this._meter = this._element.parentNode.querySelector(SELECTOR_METER)
this._grayBarElement = null
this._colorBarElement = null
this._textElement = null
this._suggsElement = null
this._showPwdElement = null
this._text = {}
this._label = new InputLabel(element)
this._suggestions = [
{
key: 'length',
text: (config) => config.suggestionLength.replace('{minLength}', config.minimumLength.toString()),
test: (password, config) => password.length >= config.minimumLength,
},
{
key: 'uppercase',
text: (config) => config.suggestionUppercase,
test: (password) => /[A-Z]/.test(password),
},
{
key: 'lowercase',
text: (config) => config.suggestionLowercase,
test: (password) => /[a-z]/.test(password),
},
{
key: 'number',
text: (config) => config.suggestionNumber,
test: (password) => /[0-9]/.test(password),
},
{
key: 'special',
text: (config) => config.suggestionSpecial,
test: (password) => /[^A-Za-z0-9]/.test(password),
},
]
this._init()
this._bindEvents()
}
// Getters
static get NAME() {
return NAME
}
// Private
_getConfig(config) {
config = {
...Default,
...Manipulator.getDataAttributes(this._element),
...(typeof config === 'object' ? config : {}),
}
return config
}
_init() {
if (this._isCustom) {
this._handleAutofill()
this._showPwdElement = SelectorEngine.findOne(SELECTOR_BTN_SHOW_PWD, this._element.parentNode)
if (this._meter) {
this._grayBarElement = this._meter.querySelector(SELECTOR_METER_GRAYBAR)
this._colorBarElement = this._meter.querySelector(SELECTOR_METER_COLBAR)
this._textElement = this._meter.querySelector(SELECTOR_METER_TEXT)
this._suggsElement = this._meter.querySelector(SELECTOR_METER_SUGGS)
if (this._textElement) {
this._config = Object.assign({}, this._config, { ...Manipulator.getDataAttributes(this._textElement) })
}
if (this._suggsElement) {
this._createsuggestionsList()
}
}
}
}
_bindEvents() {
if (this._isCustom) {
if (this._showPwdElement) {
EventHandler.on(this._showPwdElement, EVENT_CLICK, () => this._toggleShowPassword())
}
if (this._meter) {
EventHandler.on(this._element, EVENT_KEYUP, () => this._checkPassword())
EventHandler.on(this._element, 'input', () => this._checkPassword())
}
}
}
_handleAutofill() {
const checkAndActivate = () => {
if (this._element.value !== '') {
this._label._labelOut()
this._checkPassword()
}
}
checkAndActivate()
setTimeout(checkAndActivate, 100)
EventHandler.on(this._element, 'animationstart', (event) => {
if (event.animationName === 'onAutoFillStart') {
checkAndActivate()
}
})
}
_toggleShowPassword() {
const toShow = this._element.getAttribute('type') === 'password'
SelectorEngine.find('[class^="password-icon"]', this._showPwdElement).forEach((icon) => icon.classList.toggle('d-none'))
this._element.setAttribute('type', toShow ? 'text' : 'password')
this._showPwdElement.setAttribute('aria-checked', toShow.toString())
}
_checkPassword() {
const password = this._element.value
const score = this._calculateScore(password)
this._updateMeter(score)
this._updateText(score, password)
this._updateSuggestions(password)
}
_updateMeter(score) {
const perc = score < 0 ? 0 : score
if (this._colorBarElement) {
this._colorBarElement.classList.forEach((className) => {
if (className.match(/(^|\s)bg-\S+/g)) {
this._colorBarElement.classList.remove(className)
}
})
this._colorBarElement.classList.add(`bg-${this._scoreColor(score)}`)
this._colorBarElement.style.width = `${perc}%`
this._colorBarElement.setAttribute('aria-valuenow', perc)
}
EventHandler.trigger(this._element, EVENT_SCORE)
}
_updateText(score, password) {
if (this._textElement) {
let text = this._scoreText(score)
if (this._suggsElement) {
const { completedCount, totalCount } = this._getCompletedSuggestions(password)
const suggestionText = completedCount === 1 ? this._config.suggestionFollowed : this._config.suggestionFollowedPlural
text += ` ${completedCount} ${this._config.suggestionOf} ${totalCount} ${suggestionText}.`
}
if (this._textElement.textContent !== text) {
this._textElement.textContent = text
this._textElement.classList.forEach((className) => {
if (className.match(/(^|\s)text-\S+/g)) {
this._textElement.classList.remove(className)
}
})
this._textElement.classList.add(`text-${this._scoreColor(score)}`)
EventHandler.trigger(this._element, EVENT_TEXT)
}
}
}
_updateSuggestions(password) {
if (this._suggsElement) {
this._updateSuggestionsList(password)
EventHandler.trigger(this._element, EVENT_SUGGS)
}
}
_getCompletedSuggestions(password) {
let completedCount = 0
const totalCount = this._suggestions.length
this._suggestions.forEach((sugg) => {
if (sugg.test(password, this._config)) completedCount++
})
return { completedCount, totalCount }
}
_createsuggestionsList() {
if (this._suggsElement.querySelector('.password-suggestions')) {
return
}
const suggLabel = document.createElement('label')
suggLabel.className = 'visually-hidden'
suggLabel.htmlFor = 'suggestions'
suggLabel.textContent = Default.suggestionsLabel
const suggContainer = document.createElement('div')
suggContainer.id = 'suggestions'
suggContainer.className = 'password-suggestions'
this._suggestions.forEach((sugg) => {
const suggElement = document.createElement('div')
suggElement.className = 'suggestion'
suggElement.dataset.suggestion = sugg.key
const checkIcon = this._createIconCheck()
const textSpan = document.createElement('span')
textSpan.textContent = sugg.text(this._config)
suggElement.appendChild(checkIcon)
suggElement.appendChild(textSpan)
suggContainer.appendChild(suggElement)
})
this._suggsElement.appendChild(suggLabel)
this._suggsElement.appendChild(suggContainer)
}
_createIconCheck() {
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
svg.setAttribute('class', `icon icon-xs me-1 d-none`)
svg.setAttribute('aria-label', this._config.suggestionMetLabel)
svg.setAttribute('viewBox', '0 0 24 24')
svg.style.width = '1em'
svg.style.height = '1em'
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path')
path.setAttribute('d', this._config.suggestionMetIconPath.trim())
svg.appendChild(path)
return svg
}
_updateSuggestionsList(password) {
if (!this._suggsElement) return
this._suggestions.forEach((sugg) => {
const suggElement = this._suggsElement.querySelector(`[data-suggestion="${sugg.key}"]`)
if (suggElement) {
const isMet = sugg.test(password, this._config)
const checkIcon = suggElement.querySelector('.icon')
if (checkIcon) {
checkIcon.classList.toggle('d-none', !isMet)
}
}
})
}
/**
* Returns strings based on the score given.
*
* @param int score Score base.
* @return string
*/
_scoreText(score) {
if (score === -1) {
return this._config.shortPass
}
if (score === -2) {
return ''
}
score = score < 0 ? 0 : score
if (score < 26) {
return this._config.badPass
}
if (score < 51) {
return this._config.badPass
}
if (score < 76) {
return this._config.goodPass
}
return this._config.strongPass
}
_scoreColor(score) {
if (score === -1 || score === -2 || score < 26) {
return 'danger'
}
if (score < 51) {
return 'warning'
}
if (score < 76) {
return 'success'
}
return 'success'
}
/**
* Returns a value between -1 and 100 to score
* the user's password.
*
* @param string password The password to be checked.
* @return int
*/
_calculateScore(password) {
var score = 0
// empty password
if (password.trim().length === 0) {
return -2
}
// password < this._config.minimumLength
if (password.length < this._config.minimumLength) {
return -1
}
// password length
score += password.length * 4
score += this._checkRepetition(1, password).length - password.length
score += this._checkRepetition(2, password).length - password.length
score += this._checkRepetition(3, password).length - password.length
score += this._checkRepetition(4, password).length - password.length
// password has 3 numbers
if (password.match(/(.*[0-9].*[0-9].*[0-9])/)) {
score += 5
}
// password has at least 2 symbols
var symbols = '.*[!,@,#,$,%,^,&,*,?,_,~]'
symbols = new RegExp('(' + symbols + symbols + ')')
if (password.match(symbols)) {
score += 5
}
// password has Upper and Lower chars
if (password.match(/([a-z].*[A-Z])|([A-Z].*[a-z])/)) {
score += 10
}
// password has number and chars
if (password.match(/([a-zA-Z])/) && password.match(/([0-9])/)) {
score += 15
}
// password has number and symbol
if (password.match(/([!,@,#,$,%,^,&,*,?,_,~])/) && password.match(/([0-9])/)) {
score += 15
}
// password has char and symbol
if (password.match(/([!,@,#,$,%,^,&,*,?,_,~])/) && password.match(/([a-zA-Z])/)) {
score += 15
}
// password is just numbers or chars
if (password.match(/^\w+$/) || password.match(/^\d+$/)) {
score -= 10
}
if (score > 100) {
score = 100
}
if (score < 0) {
score = 0
}
return score
}
/**
* Checks for repetition of characters in
* a string
*
* @param int rLen Repetition length.
* @param string str The string to be checked.
* @return string
*/
_checkRepetition(rLen, str) {
var res = '',
repeated = false
for (var i = 0; i < str.length; i++) {
repeated = true
for (var j = 0; j < rLen && j + i + rLen < str.length; j++) {
repeated = repeated && str.charAt(j + i) === str.charAt(j + i + rLen)
}
if (j < rLen) {
repeated = false
}
if (repeated) {
i += rLen - 1
repeated = false
} else {
res += str.charAt(i)
}
}
return res
}
}
/**
* ------------------------------------------------------------------------
* Data Api implementation
* ------------------------------------------------------------------------
*/
if (typeof window !== 'undefined' && typeof document !== 'undefined') {
const createInput = (element) => {
if (element && element.matches(SELECTOR_PASSWORD)) {
return InputPassword.getOrCreateInstance(element)
}
return null
}
const initInputPassword = () => {
const element = SelectorEngine.findOne(SELECTOR_PASSWORD)
if (element) {
InputPassword.getOrCreateInstance(element)
}
}
if (document.readyState !== 'loading') {
initInputPassword()
} else {
document.addEventListener('DOMContentLoaded', initInputPassword)
}
EventHandler.on(document, EVENT_MOUSEDOWN_DATA_API, SELECTOR_PASSWORD + ', label', function () {
const target = InputLabel.getInputFromLabel(this) || this
createInput(target)
})
EventHandler.on(document, EVENT_KEYUP_DATA_API, SELECTOR_PASSWORD + ', label', function () {
const target = InputLabel.getInputFromLabel(this) || this
const element = createInput(target)
if (element && element._label) {
element._label._labelOut()
}
})
EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_BTN_SHOW_PWD, function () {
const target = this.parentNode && this.parentNode.querySelector(SELECTOR_PASSWORD)
if (target) {
InputPassword.getOrCreateInstance(target)
}
})
}
export default InputPassword