I have an input with type=text which I want to show stars like an input with type=password using only CSS.
Basically I
I ended up in this thread a lot of times recently. My solution is utilizing JQuery input event (though it can also be written in raw JS or even in C# (Blazor) should you need it, the idea would be the same):
The core part is:
if (isPasswordVisible) { // if password is visible, then simply update value stored in the dictionary
value = newValue;
passwordInputsValues[$passwordInput.attr("my-guid")] = value;
} else { // else compute and update stored value
const newValueUntilCaret = newValue.take(caretPosition); // take chars before the caret
const unchangedCharsAtStart = newValueUntilCaret.takeWhile(c => c === "●").length; // count unchanged chars from the beginning
const unchangedCharsAtEnd = newValue.skip(caretPosition).length; // count unchanged chars after the caret
const insertedValue = newValueUntilCaret.skip(unchangedCharsAtStart); // get newly added string if any
value = oldValue.take(unchangedCharsAtStart) + insertedValue + oldValue.takeLast(unchangedCharsAtEnd); // create new value as concatenation of old value left part, new string and old value right part
passwordInputsValues[$passwordInput.attr("my-guid")] = value; // store newly created value in the dictionary
$passwordInput.prop("value", value.split("").map(_ => "●").join("")); // set value of the input to new masked value
$passwordInput[0].setSelectionRange(caretPosition, caretPosition); // set caret position to match the appropriate position
}
Below is the complete code of an example password control (I will try to update it if any problems arise):
// Utils
var guid = () => {
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) {
const r = Math.random() * 16 | 0;
const v = c === "x" ? r : r & 0x3 | 0x8;
return v.toString(16);
});
}
// Array Extensions
Object.defineProperty(Array.prototype, "skip", {
value: function (n) {
if (typeof (n) !== "number") {
throw new Error("n is not a number");
}
return this.slice(n);
},
writable: true,
configurable: true
});
Object.defineProperty(Array.prototype, "take", {
value: function (n) {
if (typeof (n) !== "number") {
throw new Error("n is not a number");
}
return this.slice(0, n);
},
writable: true,
configurable: true
});
Object.defineProperty(Array.prototype, "takeLast", {
value: function (n) {
if (typeof (n) !== "number") {
throw new Error("n is not a number");
}
return this.slice(Math.max(this.length - n, 0));
},
writable: true,
configurable: true
});
Object.defineProperty(Array.prototype, "takeWhile", {
value: function (condition) {
if (typeof (condition) !== "function") {
throw new Error("condition is not a function");
}
const arr = [];
for (let el of this) {
if (condition(el))
arr.push(el);
else
break;
}
return arr;
},
writable: true,
configurable: true
});
// String Extensions
Object.defineProperty(String.prototype, "skip", {
value: function (n) {
return this.split("").skip(n).join("");
},
writable: true,
configurable: true
});
Object.defineProperty(String.prototype, "take", {
value: function (n) {
return this.split("").take(n).join("");
},
writable: true,
configurable: true
});
Object.defineProperty(String.prototype, "takeLast", {
value: function (n) {
return this.split("").takeLast(n).join("");
},
writable: true,
configurable: true
});
Object.defineProperty(String.prototype, "takeWhile", {
value: function (condition) {
return this.split("").takeWhile(condition).join("");
},
writable: true,
configurable: true
});
// JQuery Document Ready
$(document).ready(function() {
let isPasswordVisible = false;
const passwordInputsValues = {};
for (let $pi of $(".my-password-input").toArray().map(pi => $(pi))) {
const uid = guid();
$pi.attr("my-guid", uid);
passwordInputsValues[uid] = $pi.prop("value");
}
$(document).on("input", ".my-password-input", async function(e) {
const $passwordInput = $(this);
const newValue = $passwordInput.prop("value");
const oldValue = passwordInputsValues[$passwordInput.attr("my-guid")] || ""; // first time it will be undefined
const caretPosition = Math.max($passwordInput[0].selectionStart, $passwordInput[0].selectionEnd);
let value;
if (isPasswordVisible) {
value = newValue;
passwordInputsValues[$passwordInput.attr("my-guid")] = value;
} else {
const newValueUntilCaret = newValue.take(caretPosition);
const unchangedCharsAtStart = newValueUntilCaret.takeWhile(c => c === "●").length;
const unchangedCharsAtEnd = newValue.skip(caretPosition).length;
const insertedValue = newValueUntilCaret.skip(unchangedCharsAtStart);
value = oldValue.take(unchangedCharsAtStart) + insertedValue + oldValue.takeLast(unchangedCharsAtEnd);
passwordInputsValues[$passwordInput.attr("my-guid")] = value;
$passwordInput.prop("value", value.split("").map(_ => "●").join(""));
$passwordInput[0].setSelectionRange(caretPosition, caretPosition);
}
});
$(document).on("click", ".my-btn-toggle-password-visibility", function() {
const $btnTogglePassword = $(this);
const $iconPasswordShown = $btnTogglePassword.find(".my-icon-password-shown");
const $iconPasswordHidden = $btnTogglePassword.find(".my-icon-password-hidden");
const $passwordInput = $btnTogglePassword.parents(".my-input-group").first().children(".my-password-input").first();
const value = passwordInputsValues[$passwordInput.attr("my-guid")];
if (!isPasswordVisible) {
$iconPasswordHidden.removeClass("my-d-flex").addClass("my-d-none");
$iconPasswordShown.removeClass("my-d-none").addClass("my-d-flex");
$passwordInput.prop("value", value);
isPasswordVisible = true;
} else {
$iconPasswordShown.removeClass("my-d-flex").addClass("my-d-none");
$iconPasswordHidden.removeClass("my-d-none").addClass("my-d-flex");
$passwordInput.prop("value", value.split("").map(_ => "●").join(""));
isPasswordVisible = false;
}
});
});
body {
padding-top: 0;
color: white;
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
font-size: 16px;
font-weight: 400;
line-height: 1.5;
text-align: left;
height: 100%;
max-height: 100%;
background-image: linear-gradient(rgba(0,0,0,0.2), rgba(0,0,0,0.2)), url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEYAAABGCAAAAABURb1YAAAFnklEQVR4AWWXCY4cwXbE6h4JMhCvMPc/ov+3Ox9QsLCGpKlFHILd+UQ559AGDkBSFXWmomonqkL/XuUcIPUcONjHCJyE36KaqNiaRnVCG9TGpgIU9hIfohCBVGj24ammo04lU3Fy0NZDAyZi5AElHjgEGlRbG1WjraIt7TmADQkAaoEHsOXAAaeqat9RVTqjosyEH5W+DQCQt8BzIJMfXJI3e5tGRefSlRn3NrO3scnvNl26FN5RITGNfjm3GM/+FhQgnlQeImC+dEm8rzHVTNGL9EADVsTAIfGJBziRy7RlmcZU1IYpHIj+4JrWym8/XSwzompmFNVOFdTOAAfs61KZ6eXcp57fykQUOxNBmTesz2HpCnCg3UV/bA7VRpWGNqKtrYLTs0ilCjgxiZxjeLC5dDPBFn4Pn2ja6BRUL10DTgET4Focq8rn8casxV4E5tIlZvytqtfivCOKZrqc38G7egnkXecyPZdzubc57VtUnbfL+a0u3b1N3d9Mci7cKocHoh3xihLQ6fXUlgTOOfVkXSUFINoWHhqxE23RTmCqtoEJaITkIrUCkU9fnqpqm+40VSXS3CLaSzfGBb4rPuOSfN0ylLve6/jpcrbL2dTlnOdv5AamKjLvLN1XruMNS/cul/hx8twKpKYRaE2DOp8+c+lW7uVXVQ7Vx0x1G3HXjNocsHCac7NbWc6egwJVHu2kRTVNC2pqv2U4XNsv5/yWJlV9+NJ1C7xluInlcr7rK+Kg+qy7onQqgnbi7UHcS3qXjdw3bO5tumycYlVssMI5EXXpRnatPK362KodtVU60cm+eQT10sVLFwJYlVaf7UGSoi5dQF3DvnTX3UvXTPO8quKH7jsXhm/PxfKWZXqBM5sS33kmKtC31Q05fI095i5IPpyzRXqSy1Sr17p1F7NE8vMtnNbLpVVtk8c0OrfF0sKxHnIpYgHW4puMomTz8mgmrSjpvrxhX97l7LqLpl7bW9BH9/MNzF8XwRqL8/m7c30eFYV5VX3QTquK06XbLt16F4ks+8Z9m6gPtpcuk3OvibCekng4lzMHi9MoqZmKj9tdbQCbc7bcEA+oEJduRNNL1waf1tuI9MDBUK8t3k9uFz3rrqadO9Ln9TL/ezlwDvky3TW7ZmXfpc77TN0yhOXnP939LvVDV9s+tgLTm0TqYd1lea3PRFuVjLa562HdPbCfascGrvO3DMeIATVqVRO8vfHrbupdfo1dzlXUdkVLZsS1mLPG3rWcl+4B1l1537h0VXzU1tyLG7cCu+LSrSqo06hocy3WCcil65qHDbuEYzQN2HpT3jFt9LEFvHTP1nxbXOEkbhkKmd+6BTfVZ/I143z/Fyz6Za92o0I7I6gzfeYAHPwy/Rb43L9TLtMt8CtwW1xvQvbR1g9n7qJRNN1kdxcdH7cM58Szxq679MdLOhE72t6VNEpHHkzwRyjBcGth+dE99pIkVbWaEWmvnQ8Yl7Mp59zpRWXdNo6odrO754Zzvxf/010nP7onb1TR+bgbVPB9PcADJvH/043Kvk0UzLRxQ6UiaeTAcyxYl2lyrrvZv8v2wE7U1uVcST0821jJeIDKWuyxiqnuko7q3+zJYXJ46nV3D67EyL+lQ9F2qkr+3kUafQpwYE8T329p6y7OX1TF97fQv/c+l87zj7Hsgpig4qRLd9fMPs74rLGyCzRbhmVqpy7nqvPmTD1gvKe7b2U8u1QzmtuDuEtbgT3c8QBJveo6cueFkUwv59nVmfFw1jl4/unuOT9qERV76ers8n23z/c8/Xy7a4lwwLplmGusbRp33Up13+ZDN3KPxNVMhY62vUQyRTse6+cKHtZYjIfDh260Cjbb3Vy6R8H8SMBzqSwrsB1UTacoknlnOb8V4JD9Njfpwz/fHjpBUeevcAtcl65sqXPX9D87RUTLc/dfOgAAAABJRU5ErkJggg==);
background-clip: border-box;
background-origin: padding-box;
background-attachment: scroll;
background-repeat: repeat;
background-size: auto;
background-position: left top;
}
.snippet-container {
background: linear-gradient(135deg, #202020, black);
position: relative;
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 200px;
}
.my-password-input {
background: linear-gradient(to bottom, #303030, #000000);
color: white;
display: block;
position: relative;
box-sizing: border-box;
padding: 5px 9px;
line-height: 24px;
height: 34px;
box-shadow: inset 0 0 0 1px #404040;
font-size: 16px;
font-weight: 400;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
transition: all .15s ease-in-out;
width: 100%;
border: none;
}
.my-password-input:enabled:focus {
color: white;
box-shadow: inset 0 0 0 1px #404040, 0 0 6px 2px blue;
outline: none;
}
.my-input-group {
position: relative;
}
.my-input-group > .my-input-group-prepend {
display: flex;
position: absolute;
left: 0;
top: 0;
}
.my-input-group > .my-input-group-append {
display: flex;
position: absolute;
right: 0;
top: 0;
}
.my-icon {
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
.my-input-group > .my-input-group-prepend > .my-icon,
.my-input-group > .my-input-group-append > .my-icon {
width: auto;
height: 16px;
max-width: none;
max-height: 16px;
flex: 0 0 auto;
margin: 9px;
}
.my-input-group > .my-input-group-prepend > .my-icon > svg,
.my-input-group > .my-input-group-append > .my-icon > svg {
height: 100%;
width: auto;
margin: 0;
padding: 0;
overflow: hidden;
}
.my-input-group > .my-input-group-prepend > .my-btn,
.my-input-group > .my-input-group-append > .my-btn {
height: 100% !important;
width: auto;
}
button:enabled {
cursor: pointer;
}
.my-btn {
background: linear-gradient(to bottom, #303030, #000000);
color: white;
position: relative;
box-sizing: border-box;
padding: 5px;
line-height: 24px;
height: 34px;
font-size: 16px;
font-weight: 400;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
transition: all .15s ease-in-out;
width: 100%;
display: flex;
justify-content: center;
align-items: center;
border: none;
box-shadow: 0 0 0 0 #FFFFFF, inset 0 0 0 1px #404040;
}
.my-btn-primary {
color: #fff;
background: linear-gradient(to bottom, #00008B, #000000);
box-shadow: 0 0 0 0 #FFFFFF, inset 0 0 0 1px #0000FF;
}
.my-btn-primary:hover:enabled {
box-shadow: 0 0 6px 2px #FFFFFF, inset 0 0 0 1px #FFFFFF;
background: linear-gradient(to top, #00008B, #000000);
}
.my-btn > .my-icon {
margin: 4px;
width: auto;
height: 16px;
max-width: none;
max-height: 16px;
flex: 0 0 auto;
}
.my-btn > .my-icon > svg {
height: 100%;
width: auto;
}
.my-d-none {
display: none !important;
}
.my-d-flex {
display: flex !important;
}
::-webkit-input-placeholder {
color: #404040;
font-style: italic;
}
:-moz-placeholder {
color: #404040;
font-style: italic;
}
::-moz-placeholder {
color: #404040;
font-style: italic;
}
:-ms-input-placeholder {
color: #404040;
font-style: italic;
}
::-moz-selection {
background-color: #f8b700;
color: #352011;
}
::selection {
background-color: #f8b700;
color: #352011;
}