UNPKG

flyonui

Version:

The easiest, free and open-source Tailwind CSS component library with semantic classes.

349 lines (282 loc) 11.7 kB
/* * 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