flyonui
Version:
The easiest, free and open-source Tailwind CSS component library with semantic classes.
349 lines (282 loc) • 11.7 kB
text/typescript
/*
* HSStrongPassword
* @version: 3.2.2
* @author: Preline Labs Ltd.
* @license: Licensed under MIT and Preline UI Fair Use License (https://preline.co/docs/license.html)
* Copyright 2024 Preline Labs Ltd.
*/
import { isEnoughSpace, dispatch, htmlToElement, classToClassList } from '../../utils'
import { IStrongPasswordOptions, IStrongPassword } from './interfaces'
import HSBasePlugin from '../base-plugin'
import { ICollectionItem } from '../../interfaces'
class HSStrongPassword extends HSBasePlugin<IStrongPasswordOptions> implements IStrongPassword {
private readonly target: string | HTMLInputElement | null
private readonly hints: string | HTMLElement | null
private readonly stripClasses: string | null
private readonly minLength: number
private readonly mode: string
private readonly popoverSpace: number
private readonly checksExclude: string[] | null
private readonly specialCharactersSet: string | null
public isOpened: boolean = false
private strength: number = 0
private passedRules: Set<string> = new Set<string>()
private weakness: HTMLElement | null
private rules: HTMLElement[] | null
private availableChecks: string[] | null
private onTargetInputListener: (evt: InputEvent) => void
private onTargetFocusListener: () => void
private onTargetBlurListener: () => void
private onTargetInputSecondListener: () => void
private onTargetInputThirdListener: () => void
constructor(el: HTMLElement, options?: IStrongPasswordOptions) {
super(el, options)
const data = el.getAttribute('data-strong-password')
const dataOptions: IStrongPasswordOptions = data ? JSON.parse(data) : {}
const concatOptions = {
...dataOptions,
...options
}
this.target = concatOptions?.target
? typeof concatOptions?.target === 'string'
? (document.querySelector(concatOptions.target) as HTMLInputElement)
: concatOptions.target
: null
this.hints = concatOptions?.hints
? typeof concatOptions?.hints === 'string'
? (document.querySelector(concatOptions.hints) as HTMLElement)
: concatOptions.hints
: null
this.stripClasses = concatOptions?.stripClasses || null
this.minLength = concatOptions?.minLength || 6
this.mode = concatOptions?.mode || 'default'
this.popoverSpace = concatOptions?.popoverSpace || 10
this.checksExclude = concatOptions?.checksExclude || []
this.availableChecks = ['lowercase', 'uppercase', 'numbers', 'special-characters', 'min-length'].filter(
el => !this.checksExclude.includes(el)
)
this.specialCharactersSet = concatOptions?.specialCharactersSet || '!"#$%&\'()*+,-./:;<=>?@[\\\\\\]^_`{|}~'
if (this.target) this.init()
}
private targetInput(evt: InputEvent) {
this.setStrength((evt.target as HTMLInputElement).value)
}
private targetFocus() {
this.isOpened = true
;(this.hints as HTMLElement).classList.remove('hidden')
;(this.hints as HTMLElement).classList.add('block')
this.recalculateDirection()
}
private targetBlur() {
this.isOpened = false
;(this.hints as HTMLElement).classList.remove('block', 'bottom-full', 'top-full')
;(this.hints as HTMLElement).classList.add('hidden')
;(this.hints as HTMLElement).style.marginTop = ''
;(this.hints as HTMLElement).style.marginBottom = ''
}
private targetInputSecond() {
this.setWeaknessText()
}
private targetInputThird() {
this.setRulesText()
}
private init() {
this.createCollection(window.$hsStrongPasswordCollection, this)
if (this.availableChecks.length) this.build()
}
private build() {
this.buildStrips()
if (this.hints) this.buildHints()
this.setStrength((this.target as HTMLInputElement).value)
this.onTargetInputListener = evt => this.targetInput(evt)
;(this.target as HTMLInputElement).addEventListener('input', this.onTargetInputListener)
}
private buildStrips() {
this.el.innerHTML = ''
if (this.stripClasses) {
for (let i = 0; i < this.availableChecks.length; i++) {
const newStrip = htmlToElement('<div></div>')
classToClassList(this.stripClasses, newStrip)
this.el.append(newStrip)
}
}
}
private buildHints() {
this.weakness = (this.hints as HTMLElement).querySelector('[data-pw-strength-hint]') || null
this.rules = Array.from((this.hints as HTMLElement).querySelectorAll('[data-pw-strength-rule]')) || null
this.rules.forEach(rule => {
const ruleValue = rule.getAttribute('data-pw-strength-rule')
if (this.checksExclude?.includes(ruleValue)) rule.remove()
})
if (this.weakness) this.buildWeakness()
if (this.rules) this.buildRules()
if (this.mode === 'popover') {
this.onTargetFocusListener = () => this.targetFocus()
this.onTargetBlurListener = () => this.targetBlur()
;(this.target as HTMLInputElement).addEventListener('focus', this.onTargetFocusListener)
;(this.target as HTMLInputElement).addEventListener('blur', this.onTargetBlurListener)
}
}
private buildWeakness() {
this.checkStrength((this.target as HTMLInputElement).value)
this.setWeaknessText()
this.onTargetInputSecondListener = () => setTimeout(() => this.targetInputSecond())
;(this.target as HTMLInputElement).addEventListener('input', this.onTargetInputSecondListener)
}
private buildRules() {
this.setRulesText()
this.onTargetInputThirdListener = () => setTimeout(() => this.targetInputThird())
;(this.target as HTMLInputElement).addEventListener('input', this.onTargetInputThirdListener)
}
private setWeaknessText() {
const weaknessText = this.weakness.getAttribute('data-pw-strength-hint')
const weaknessTextToJson = JSON.parse(weaknessText as string)
this.weakness.textContent = weaknessTextToJson[this.strength]
}
private setRulesText() {
this.rules.forEach(rule => {
const ruleValue = rule.getAttribute('data-pw-strength-rule')
this.checkIfPassed(rule, this.passedRules.has(ruleValue))
})
}
private togglePopover() {
const popover = this.el.querySelector('.popover')
if (popover) popover.classList.toggle('show')
}
private checkStrength(val: string): { strength: number; rules: Set<string> } {
const passedRules = new Set<string>()
const regexps = {
lowercase: /[a-z]+/,
uppercase: /[A-Z]+/,
numbers: /[0-9]+/,
'special-characters': new RegExp(`[${this.specialCharactersSet}]`)
}
let strength = 0
if (this.availableChecks.includes('lowercase') && val.match(regexps['lowercase'])) {
strength += 1
passedRules.add('lowercase')
}
if (this.availableChecks.includes('uppercase') && val.match(regexps['uppercase'])) {
strength += 1
passedRules.add('uppercase')
}
if (this.availableChecks.includes('numbers') && val.match(regexps['numbers'])) {
strength += 1
passedRules.add('numbers')
}
if (this.availableChecks.includes('special-characters') && val.match(regexps['special-characters'])) {
strength += 1
passedRules.add('special-characters')
}
if (this.availableChecks.includes('min-length') && val.length >= this.minLength) {
strength += 1
passedRules.add('min-length')
}
if (!val.length) {
strength = 0
}
if (strength === this.availableChecks.length) this.el.classList.add('accepted')
else this.el.classList.remove('accepted')
this.strength = strength
this.passedRules = passedRules
return {
strength: this.strength,
rules: this.passedRules
}
}
private checkIfPassed(el: HTMLElement, isRulePassed = false) {
const check = el.querySelector('[data-check]')
const uncheck = el.querySelector('[data-uncheck]')
if (isRulePassed) {
el.classList.add('active')
check.classList.remove('hidden')
uncheck.classList.add('hidden')
} else {
el.classList.remove('active')
check.classList.add('hidden')
uncheck.classList.remove('hidden')
}
}
private setStrength(val: string) {
const { strength, rules } = this.checkStrength(val)
const payload = {
strength,
rules
}
this.hideStrips(strength)
this.fireEvent('change', payload)
dispatch('change.strongPassword', this.el, payload)
}
private hideStrips(qty: number) {
Array.from(this.el.children).forEach((el: HTMLElement, i: number) => {
if (i < qty) el.classList.add('passed')
else el.classList.remove('passed')
})
}
// Public methods
public recalculateDirection() {
if (isEnoughSpace(this.hints as HTMLElement, this.target as HTMLInputElement, 'bottom', this.popoverSpace)) {
;(this.hints as HTMLElement).classList.remove('bottom-full')
;(this.hints as HTMLElement).classList.add('top-full')
;(this.hints as HTMLElement).style.marginBottom = ''
;(this.hints as HTMLElement).style.marginTop = `${this.popoverSpace}px`
} else {
;(this.hints as HTMLElement).classList.remove('top-full')
;(this.hints as HTMLElement).classList.add('bottom-full')
;(this.hints as HTMLElement).style.marginTop = ''
;(this.hints as HTMLElement).style.marginBottom = `${this.popoverSpace}px`
}
}
public destroy() {
// Remove listeners
;(this.target as HTMLInputElement).removeEventListener('input', this.onTargetInputListener)
;(this.target as HTMLInputElement).removeEventListener('focus', this.onTargetFocusListener)
;(this.target as HTMLInputElement).removeEventListener('blur', this.onTargetBlurListener)
;(this.target as HTMLInputElement).removeEventListener('input', this.onTargetInputSecondListener)
;(this.target as HTMLInputElement).removeEventListener('input', this.onTargetInputThirdListener)
window.$hsStrongPasswordCollection = window.$hsStrongPasswordCollection.filter(
({ element }) => element.el !== this.el
)
}
// Static methods
static getInstance(target: HTMLElement | string, isInstance?: boolean) {
const elInCollection = window.$hsStrongPasswordCollection.find(
el => el.element.el === (typeof target === 'string' ? document.querySelector(target) : target)
)
return elInCollection ? (isInstance ? elInCollection : elInCollection.element.el) : null
}
static autoInit() {
if (!window.$hsStrongPasswordCollection) window.$hsStrongPasswordCollection = []
if (window.$hsStrongPasswordCollection)
window.$hsStrongPasswordCollection = window.$hsStrongPasswordCollection.filter(({ element }) =>
document.contains(element.el)
)
document.querySelectorAll('[data-strong-password]:not(.--prevent-on-load-init)').forEach((el: HTMLElement) => {
if (!window.$hsStrongPasswordCollection.find(elC => (elC?.element?.el as HTMLElement) === el)) {
const data = el.getAttribute('data-strong-password')
const options: IStrongPasswordOptions = data ? JSON.parse(data) : {}
new HSStrongPassword(el, options)
}
})
}
}
declare global {
interface Window {
HSStrongPassword: Function
$hsStrongPasswordCollection: ICollectionItem<HSStrongPassword>[]
}
}
window.addEventListener('load', () => {
HSStrongPassword.autoInit()
// Uncomment for debug
// console.log('Strong password collection:', window.$hsStrongPasswordCollection);
})
document.addEventListener('scroll', () => {
if (!window.$hsStrongPasswordCollection) return false
const target = window.$hsStrongPasswordCollection.find(el => el.element.isOpened)
if (target) target.element.recalculateDirection()
})
if (typeof window !== 'undefined') {
window.HSStrongPassword = HSStrongPassword
}
export default HSStrongPassword