preline
Version:
Preline UI is an open-source set of prebuilt UI components based on the utility-first Tailwind CSS framework.
462 lines (391 loc) • 11.9 kB
text/typescript
/*
* HSStrongPassword
* @version: 3.0.1
* @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 '../strong-password/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-hs-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-hs-strong-password-hints-weakness-text]',
) || null;
this.rules =
Array.from(
(this.hints as HTMLElement).querySelectorAll(
'[data-hs-strong-password-hints-rule-text]',
),
) || null;
this.rules.forEach((rule) => {
const ruleValue = rule.getAttribute(
'data-hs-strong-password-hints-rule-text',
);
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-hs-strong-password-hints-weakness-text',
);
const weaknessTextToJson = JSON.parse(weaknessText as string);
this.weakness.textContent = weaknessTextToJson[this.strength];
}
private setRulesText() {
this.rules.forEach((rule) => {
const ruleValue = rule.getAttribute(
'data-hs-strong-password-hints-rule-text',
);
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.hs.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-hs-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-hs-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;