drupal/core/themes/claro/js/user.es6.js

345 lines
11 KiB
JavaScript

/**
* @file
* Overrides Drupal core user.js that provides password strength indicator.
*
* @todo remove these overrides after
* https://www.drupal.org/project/drupal/issues/3067523 has been resolved.
*/
(($, Drupal) => {
/**
* This overrides the default Drupal.behaviors.password functionality.
*
* - Markup has been moved to theme functions so that to enable customizations
* needed for matching Claro's design requirements
* (https://www.drupal.org/project/drupal/issues/3067523).
* - Modified classes so that same class names are not being used for different
* elements (https://www.drupal.org/project/drupal/issues/3061265).
*/
Drupal.behaviors.password = {
attach(context, settings) {
const $passwordInput = $(context)
.find('input.js-password-field')
.once('password');
if ($passwordInput.length) {
// Settings and translated messages added by
// user_form_process_password_confirm().
const translate = settings.password;
// The form element object of the password input.
const $passwordInputParent = $passwordInput.parent();
// The password_confirm form element object.
const $passwordWidget = $passwordInput.closest(
'.js-form-type-password-confirm',
);
// The password confirm input.
const $passwordConfirmInput = $passwordWidget.find(
'input.js-password-confirm',
);
// The strength feedback element for the password input.
const $passwordInputHelp = $(
Drupal.theme.passwordInputHelp(translate.strengthTitle),
);
// The password match feedback for the password confirm input.
const $passwordConfirmHelp = $(
Drupal.theme.passwordConfirmHelp(translate.confirmTitle),
);
const $passwordInputStrengthBar = $passwordInputHelp.find(
'.js-password-strength-bar',
);
const $passwordInputStrengthMessageWrapper = $passwordInputHelp.find(
'.js-password-strength-text',
);
const $passwordConfirmMatch = $passwordConfirmHelp.find(
'.js-password-match-text',
);
let $passwordSuggestionsTips = $(
Drupal.theme.passwordSuggestionsTips('', ''),
).hide();
// If the password strength indicator is enabled, add its markup.
if (settings.password.showStrengthIndicator) {
$passwordConfirmInput
.after($passwordConfirmHelp)
.parent()
.after($passwordSuggestionsTips);
$passwordInputParent.append($passwordInputHelp);
}
// Check that password and confirmation inputs match.
const passwordCheckMatch = confirmInputVal => {
if (confirmInputVal) {
const success = $passwordInput.val() === confirmInputVal;
const confirmClass = success ? 'ok' : 'error';
const confirmMatchMessage = success
? translate.confirmSuccess
: translate.confirmFailure;
// Update the success message and set the class accordingly if
// needed.
if (
!$passwordConfirmMatch.hasClass(confirmClass) ||
!$passwordConfirmMatch.html() === confirmMatchMessage
) {
$passwordConfirmMatch
.html(confirmMatchMessage)
.removeClass('ok error')
.addClass(confirmClass);
}
}
};
// Check the password strength.
const passwordCheck = () => {
if (settings.password.showStrengthIndicator) {
// Evaluate the password strength.
const result = Drupal.evaluatePasswordStrength(
$passwordInput.val(),
settings.password,
);
const $newSuggestions = $(
Drupal.theme.passwordSuggestionsTips(
translate.hasWeaknesses,
result.tips,
),
);
// Update the suggestions for how to improve the password.
if ($newSuggestions.html() !== $passwordSuggestionsTips.html()) {
$passwordSuggestionsTips.replaceWith($newSuggestions);
$passwordSuggestionsTips = $newSuggestions;
// Only show the description box if a weakness exists in the
// password.
$passwordSuggestionsTips.toggle(result.strength !== 100);
}
// Adjust the length of the strength indicator.
$passwordInputStrengthBar
.css('width', `${result.strength}%`)
.removeClass('is-weak is-fair is-good is-strong')
.addClass(result.indicatorClass);
// Update the strength indication text if needed.
if (
!$passwordInputStrengthMessageWrapper.hasClass(
result.indicatorClass,
) ||
!$passwordInputStrengthMessageWrapper.html() ===
result.indicatorText
) {
$passwordInputStrengthMessageWrapper
.html(result.indicatorText)
.removeClass('is-weak is-fair is-good is-strong')
.addClass(result.indicatorClass);
}
}
$passwordWidget
.removeClass('is-initial')
.removeClass('is-password-empty is-password-filled')
.removeClass('is-confirm-empty is-confirm-filled');
// Check the value of the password input and add the proper classes.
$passwordWidget.addClass(
$passwordInput.val() ? 'is-password-filled' : 'is-password-empty',
);
// Check the value in the confirm input and show results.
passwordCheckMatch($passwordConfirmInput.val());
$passwordWidget.addClass(
$passwordConfirmInput.val()
? 'is-confirm-filled'
: 'is-confirm-empty',
);
};
// Add initial classes.
$passwordWidget
.addClass(
$passwordInput.val() ? 'is-password-filled' : 'is-password-empty',
)
.addClass(
$passwordConfirmInput.val()
? 'is-confirm-filled'
: 'is-confirm-empty',
);
// Monitor input events.
$passwordInput.on('input', passwordCheck);
$passwordConfirmInput.on('input', passwordCheck);
}
},
};
/**
* Override the default Drupal.evaluatePasswordStrength.
*
* The default implementation of this function hard codes some markup inside
* this function. Rendering markup is now handled by
* Drupal.behaviors.password.
*
* @param {string} password
* Password to evaluate the strength.
*
* @param {Array.<string>} translate
* Settings and translated messages added by user_form_process_password_confirm().
*
* @return {Array.<string>}
* Array containing the strength, tips, indicators text and class.
*/
Drupal.evaluatePasswordStrength = (password, translate) => {
password = password.trim();
let indicatorText;
let indicatorClass;
let weaknesses = 0;
let strength = 100;
const tips = [];
const hasLowercase = /[a-z]/.test(password);
const hasUppercase = /[A-Z]/.test(password);
const hasNumbers = /[0-9]/.test(password);
const hasPunctuation = /[^a-zA-Z0-9]/.test(password);
// If there is a username edit box on the page, compare password to that,
// otherwise use value from the database.
const $usernameBox = $('input.username');
const username =
$usernameBox.length > 0 ? $usernameBox.val() : translate.username;
// Lose 5 points for every character less than 12, plus a 30 point penalty.
if (password.length < 12) {
tips.push(translate.tooShort);
strength -= (12 - password.length) * 5 + 30;
}
// Count weaknesses.
if (!hasLowercase) {
tips.push(translate.addLowerCase);
weaknesses += 1;
}
if (!hasUppercase) {
tips.push(translate.addUpperCase);
weaknesses += 1;
}
if (!hasNumbers) {
tips.push(translate.addNumbers);
weaknesses += 1;
}
if (!hasPunctuation) {
tips.push(translate.addPunctuation);
weaknesses += 1;
}
// Apply penalty for each weakness (balanced against length penalty).
switch (weaknesses) {
case 1:
strength -= 12.5;
break;
case 2:
strength -= 25;
break;
case 3:
strength -= 40;
break;
case 4:
strength -= 40;
break;
default:
// Default: 0. Nothing to do.
break;
}
// Check if password is the same as the username.
if (password !== '' && password.toLowerCase() === username.toLowerCase()) {
tips.push(translate.sameAsUsername);
// Passwords the same as username are always very weak.
strength = 5;
}
// Based on the strength, work out what text should be shown by the
// password strength meter.
if (strength < 60) {
indicatorText = translate.weak;
indicatorClass = 'is-weak';
} else if (strength < 70) {
indicatorText = translate.fair;
indicatorClass = 'is-fair';
} else if (strength < 80) {
indicatorText = translate.good;
indicatorClass = 'is-good';
} else if (strength <= 100) {
indicatorText = translate.strong;
indicatorClass = 'is-strong';
}
return {
strength,
tips,
indicatorText,
indicatorClass,
};
};
/**
* Password strength feedback for password confirm's main input.
*
* @param {string} message
* The prefix text for the strength feedback word.
*
* @return {string}
* The string representing the DOM fragment.
*/
Drupal.theme.passwordInputHelp = message =>
`<div class="password-strength">
<div class="password-strength__track">
<div class="password-strength__bar js-password-strength-bar"></div>
</div>
<div aria-live="polite" aria-atomic="true" class="password-strength__title">
${message} <span class="password-strength__text js-password-strength-text"></span>
</div>
</div>`;
/**
* Password match feedback for password confirm input.
*
* @param {string} message
* The message that precedes the yes|no text.
*
* @return {string}
* A string representing the DOM fragment.
*/
Drupal.theme.passwordConfirmHelp = message =>
`<div aria-live="polite" aria-atomic="true" class="password-match-message">${message} <span class="password-match-message__text js-password-match-text"></span></div>`;
/**
* Password suggestions tips.
*
* @param {string} title
* The title that precedes tips.
* @param {Array.<string>} tips
* Array containing the tips.
*
* @return {string}
* A string representing the DOM fragment.
*/
Drupal.theme.passwordSuggestionsTips = (title, tips) =>
`<div class="password-suggestions">${
tips.length
? `${title}<ul class="password-suggestions__tips"><li class="password-suggestions__tip">${tips.join(
'</li><li class="password-suggestions__tip">',
)}</li></ul>`
: ''
}</div>`;
})(jQuery, Drupal);