diff --git a/js/password-om.js b/js/password-om.js
new file mode 100644
index 0000000..b4c2955
--- /dev/null
+++ b/js/password-om.js
@@ -0,0 +1,452 @@
+/**
+ * POMjs 1.1.4
+ *
+ * password-om.js
+ *
+ * Plain vanilla javascript for the "Öppet Moln" ("Open Cloud") random password
+ * generator site (password.oppetmoln.se). For the sake of having a name, it
+ * shall be POMjs. There's really nothing fancy going on here :-)
+ *
+ * Copyright 2022, 2023 Joaquim Homrighausen; All rights reserved.
+ * Development sponsored by WebbPlatsen i Sverige AB
+ * https://www.webbplatsen.se
+ *
+ * This file is part of POMjs. POMjs is free software.
+ *
+ * You may redistribute it and/or modify it under the terms of the
+ * GNU General Public License version 2, as published by the Free Software
+ * Foundation.
+ *
+ * POMjs is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+ * details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with the POMjs package. If not, write to:
+ * The Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor
+ * Boston, MA 02110-1301, USA.
+ */
+
+/* Some globals, to be filled by script */
+
+var POM_cfgLanguage = 'noLang?';
+var POM_cfgTitle = 'noTitle?';
+var POM_cfgSlogan = 'noSlogan?';
+var POM_cfgGenPasswordHint = 'noHint?';
+const POM_strUppercase = POM_genAlphabet(true);
+const POM_strLowercase = POM_genAlphabet(false);
+const POM_strDigits = POM_genDigits();
+const POM_strSpecialOne = '-._#$@%!';
+const POM_strSpecialTwo = '"+(){}[]?&,*<>|:;^';
+
+/* Easy customization of defaults, etc */
+const POM_strUppercase_Default = true;
+const POM_strLowercase_Default = true;
+const POM_strDigits_Default = true;
+const POM_strSpecialOne_Default = true;
+const POM_strSpecialTwo_Default = false;
+const POM_mkPasswordOnLoad = true;
+const POM_changeIsClick = true;
+const POM_minLength = 8;
+const POM_maxLength = 24;
+const POM_sliderStep = 1;
+const POM_debug = false;
+
+/* Generate some strings */
+function POM_genAlphabet(isUpper = false) {
+ return [...Array(26)].map((_, i) => String.fromCharCode(i + (isUpper ? 65 : 97))).join('');
+}
+function POM_genDigits() {
+ return [...Array(10)].map((_, i) => String.fromCharCode(i + 48)).join('');
+}
+/* Distill string down to unique characters by using a set */
+function POM_distillString(s) {
+ s = s.split('');
+ s = new Set(s);
+ s = [...s].join('+');
+ return(s);
+}
+/* Escape special regex characters in string, from MDN */
+function POM_escapeRegExp(s) {
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
+}
+/* Validate "manual input" */
+function POM_validateNumInput() {
+ let mkLengthSlider = document.getElementById('mk-password-len-slider');
+ if (mkLengthSlider) {
+ if (this.value === '') {
+ this.value = POM_minLength;
+ }
+ mkLengthSlider.value = this.value;
+ if (mkLengthSlider.value < POM_minLength) {
+ mkLengthSlider.value = POM_minLength;
+ } else if (mkLengthSlider.value > POM_maxLength) {
+ mkLengthSlider.value = POM_maxLength;
+ }
+ this.value = mkLengthSlider.value;
+ }
+ if (POM_changeIsClick) {
+ document.getElementById('mk-password-btn').click();
+ }
+}
+/* Validate "manual input" */
+function POM_generatePassword() {
+ let strengthP = document.getElementById('mk-password-strength');
+ strengthP.style.background = 'inherit';
+ strengthP.style.width = '0';
+ strengthP.style.transition = 'none';
+ let passwordField = document.getElementById('mk-password-field');
+ passwordField.value = '';
+ passwordField.classList.remove('mk-password-field-focus');
+ let mkLengthField = document.getElementById('mk-length-field');
+ if (POM_debug) {
+ console.clear();
+ }
+ if (mkLengthField) {
+ if (mkLengthField.value >= POM_minLength && mkLengthField.value <= POM_maxLength) {
+ let passwordSource = '';
+ if (document.getElementById('mk-uppercase-select').checked) {
+ passwordSource += POM_strUppercase;
+ }
+ if (document.getElementById('mk-special-one-select').checked) {
+ passwordSource += (POM_strSpecialOne + POM_strSpecialOne);
+ }
+ if (document.getElementById('mk-digits-select').checked) {
+ passwordSource += POM_strDigits;
+ }
+ if (document.getElementById('mk-lowercase-select').checked) {
+ passwordSource += POM_strLowercase;
+ }
+ if (document.getElementById('mk-digits-select').checked) {
+ passwordSource += POM_strDigits;
+ }
+ if (document.getElementById('mk-special-two-select').checked) {
+ passwordSource += (POM_strSpecialTwo + POM_strSpecialTwo);
+ }
+ if (passwordSource.length>0) {
+ let passwordGen = '';
+ let theChar = '';
+ for (let i = 0; i < mkLengthField.value; i++) {
+ const charPos = Math.floor(Math.random() * passwordSource.length);
+ theChar = passwordSource[charPos];
+ passwordGen += theChar;
+ }
+ passwordField.value = passwordGen;
+
+ let scoreP = 0;/* Password strength score, max is 8 */
+ let searchStr;
+ let re;
+
+ /* See if password contains uppercase characters */
+ searchStr = '[' + POM_escapeRegExp(POM_strUppercase) + ']';
+ if (POM_debug) {
+ console.log('Using ' + searchStr + ' to scan password');
+ }
+ re = new RegExp(searchStr);
+ if (re.test(passwordGen)) {
+ scoreP++;
+ if (POM_debug) {
+ console.log('Password contains UPPERCASE');
+ }
+ } else {
+ if (POM_debug) {
+ console.log('Password does NOT contain UPPERCASE');
+ }
+ }
+ /* See if password contains lowercase characters */
+ searchStr = '[' + POM_escapeRegExp(POM_strLowercase) + ']';
+ if (POM_debug) {
+ console.log('Using ' + searchStr + ' to scan password');
+ }
+ re = new RegExp(searchStr);
+ if (re.test(passwordGen)) {
+ scoreP++;
+ if (POM_debug) {
+ console.log('Password contains lowercase');
+ }
+ } else {
+ if (POM_debug) {
+ console.log('Password does NOT contain lowercase');
+ }
+ }
+ /* See if password contains digits characters */
+ searchStr = '[' + POM_escapeRegExp(POM_strDigits) + ']';
+ if (POM_debug) {
+ console.log('Using ' + searchStr + ' to scan password');
+ }
+ re = new RegExp(searchStr);
+ if (re.test(passwordGen)) {
+ scoreP++;
+ if (POM_debug) {
+ console.log('Password contains digits');
+ }
+ } else {
+ if (POM_debug) {
+ console.log('Password does NOT contain digits');
+ }
+ }
+ /* See if password contains specialOne characters */
+ searchStr = '[' + POM_escapeRegExp(POM_strSpecialOne) + ']';
+ if (POM_debug) {
+ console.log('Using ' + searchStr + ' to scan password');
+ }
+ re = new RegExp(searchStr);
+ if (re.test(passwordGen)) {
+ scoreP++;
+ if (POM_debug) {
+ console.log('Password contains specialOne');
+ }
+ } else {
+ if (POM_debug) {
+ console.log('Password does NOT contain specialOne');
+ }
+ }
+ /* See if password contains specialtwo characters */
+ searchStr = '[' + POM_escapeRegExp(POM_strSpecialTwo) + ']';
+ if (POM_debug) {
+ console.log('Using ' + searchStr + ' to scan password');
+ }
+ re = new RegExp(searchStr);
+ if (re.test(passwordGen)) {
+ scoreP++;
+ if (POM_debug) {
+ console.log('Password contains specialTwo');
+ }
+ } else {
+ if (POM_debug) {
+ console.log('Password does NOT contain specialTwo');
+ }
+ }
+ /* Bump score if password is > 7 characters */
+ if (passwordGen.length > 7) {
+ scoreP++;
+ if (POM_debug) {
+ console.log('Password >7 characters, buping score');
+ }
+ }
+ /* Bump score if password is > 15 characters */
+ if (passwordGen.length > 15) {
+ scoreP++;
+ if (POM_debug) {
+ console.log('Password >15 characters, buping score');
+ }
+ }
+ /* Bump score if password is > 31 characters */
+ if (passwordGen.length > 31) {
+ scoreP += 2;
+ if (POM_debug) {
+ console.log('Password >31 characters, buping score');
+ }
+ }
+ /* Bump score if password is > 63 characters */
+ if (passwordGen.length > 63) {
+ scoreP += 4;
+ if (POM_debug) {
+ console.log('Password >63 characters, buping score');
+ }
+ }
+ /* Let's not overflooooow */
+ if (scoreP > 8) {
+ scoreP = 8;
+ }
+ if (scoreP > 6) {
+ strengthP.style.background = '#27ce60';
+ } else if (scoreP > 5) {
+ strengthP.style.background = '#27ae60';
+ } else if (scoreP > 3) {
+ strengthP.style.background = '#FFC300';
+ } else {
+ strengthP.style.background = '#C0392B';
+ }
+ strengthP.style.width = (scoreP * 12.5) + '%';
+ strengthP.style.transition = 'width 0.3s ease-in';
+ }
+ }
+ }
+}
+/* Copy content of generated password field, if any, to clipboard */
+function POM_copyPassword() {
+ let passwordField = document.getElementById('mk-password-field');
+ if (passwordField && passwordField.value.length > 0) {
+ if (navigator && navigator.clipboard && navigator.clipboard.writeText) {
+ /* Use "modern" method */
+ navigator.clipboard.writeText(passwordField.value).then(function() {
+ passwordField.classList.add('mk-password-field-focus');
+ }, function(e) {
+ alert("Could not copy text: " + e);
+ });
+ } else {
+ /* Use legacy method */
+ var theText = document.createElement('textarea');
+ theText.value = passwordField.value;
+ theText.setAttribute('readonly', '');
+ theText.style.position = 'absolute';
+ theText.style.top = '-6969px';
+ theText.style.left = '-6969px';
+ document.body.appendChild(theText);
+ theText.focus();
+ theText.select();
+ try {
+ var goodCopy = document.execCommand('copy');
+ if (goodCopy) {
+ passwordField.classList.add('mk-password-field-focus');
+ } else {
+ alert('That did not work');
+ }
+ } catch(e) {
+ alert('Could not copy text: ' + e.message);
+ }
+ document.body.removeChild(theText);
+ }
+ }
+}
+/* Initialize */
+function POM_initialSetup() {
+ /* Setup, language strings, etc. Ugly, but it works :-) */
+ if (navigator.language) {
+ POM_cfgLanguage = navigator.language;
+ if (POM_debug) {
+ console.log("Language: " + POM_cfgLanguage);
+ }
+ let dashPos = POM_cfgLanguage.indexOf('-');
+ if (dashPos && dashPos>0) {
+ POM_cfgLanguage = POM_cfgLanguage.substring(0, dashPos);
+ }
+ POM_cfgLanguage = POM_cfgLanguage.toLowerCase();
+ } else {
+ POM_cfgLanguage = '???';
+ }
+ /* Add your translation here */
+ switch(POM_cfgLanguage) {
+ case 'sv':
+ case 'se':
+ POM_cfgTitle = 'Slumpmässigt Lösenord';
+ POM_cfgSlogan = 'Inga kakor, ingen spårning.
Bara lösenord.';
+ POM_cfgGenPasswordHint = 'Generera lösenord';
+ break;
+ case 'de':
+ /* Thank you Peter Hampf */
+ POM_cfgTitle = 'Zufallsgenerator für Passwörter';
+ POM_cfgSlogan = 'Keine Cookies, keine Tracker.
Nur Passwörter.';
+ POM_cfgGenPasswordHint = 'Passwort generieren';
+ break;
+ case 'hu':
+ /* Thank you Mihaly Balassy */
+ POM_cfgTitle = 'Véletlenszerű jelszógenerátor';
+ POM_cfgSlogan = 'Cookie-k nélkül, nyomkövetők nélkül.
Csak jelszavak.';
+ POM_cfgGenPasswordHint = 'Jelszó generálása';
+ break;
+ case 'nl':
+ /* Thank you Jeroen van de Leur */
+ POM_cfgTitle = 'Willekeurig Wachtwoord Generator';
+ POM_cfgSlogan = 'Geen cookies, geen trackers.
Uitsluitend wachtwoorden.';
+ POM_cfgGenPasswordHint = 'Genereer wachtwoord';
+ break;
+ case 'lb':
+ /* Thank you Alain Fontaine */
+ POM_cfgTitle = 'Zoufallspasswuertgenerator';
+ POM_cfgSlogan = 'Keng Cookien, keng Trackeren.
Nemme Passwierder.';
+ POM_cfgGenPasswordHint = 'Passwuert genereieren';
+ break;
+ case 'fr':
+ /* Thank you Alain Fontaine */
+ POM_cfgTitle = 'Générateur de mot de passe au hazard';
+ POM_cfgSlogan = 'Pas de cookies, pas de traceurs.
Juste des mots de passe.';
+ POM_cfgGenPasswordHint = 'Générer mot de passe';
+ break;
+ case 'fi':
+ /* Thank you Thomas Raehalme */
+ POM_cfgTitle = 'Salasanageneraattori';
+ POM_cfgSlogan = 'Ei evästeitä, ei seurantaa.
Vain salasanoja.';
+ POM_cfgGenPasswordHint = 'Generoi salasana';
+ break;
+ case 'sl':
+ /* Thank you Gregor Godler */
+ POM_cfgTitle = 'Generator Naključnih Gesel';
+ POM_cfgSlogan = 'Brez piškotkov, nič sledilnikov.
Samo geslo.';
+ POM_cfgGenPasswordHint = 'Generiraj geslo';
+ break;
+ default:
+ POM_cfgTitle = 'Random Password Generator';
+ POM_cfgSlogan = 'No cookies, no trackers.
Just passwords.';
+ POM_cfgGenPasswordHint = 'Generate password';
+ break;
+ }
+ /* Paint "HTML" with language strings */
+ document.getElementById('cfg-page-title').innerHTML = POM_cfgTitle;
+ document.getElementById('cfg-page-slogan').innerHTML = POM_cfgSlogan;
+ document.getElementById('cfg-page-lang').innerHTML = '[' + POM_cfgLanguage + ']';
+ document.getElementById('mk-password-btn').title = POM_cfgGenPasswordHint;
+ /* Set reasonable defaults */
+ document.getElementById('mk-uppercase-select').checked = POM_strUppercase_Default;
+ document.getElementById('mk-lowercase-select').checked = POM_strLowercase_Default;
+ document.getElementById('mk-digits-select').checked = POM_strDigits_Default;
+ document.getElementById('mk-special-one-select').checked = POM_strSpecialOne_Default;
+ document.getElementById('mk-special-two-select').checked = POM_strSpecialTwo_Default;
+
+ document.getElementById('mk-password-field').value = '';
+ let mkPasswordSlider = document.getElementById('mk-password-len-slider');
+ if (mkPasswordSlider) {
+ mkPasswordSlider.value = POM_minLength;
+ mkPasswordSlider.setAttribute("min", POM_minLength);
+ mkPasswordSlider.setAttribute("max", POM_maxLength);
+ mkPasswordSlider.step = POM_sliderStep;
+ }
+ document.getElementById('mk-length-field').value = POM_minLength;
+ /* Paint "HTML" with password source */
+ document.getElementById("gen-uppercase").innerText = POM_strUppercase;
+ document.getElementById("gen-lowercase").innerText = POM_strLowercase;
+ document.getElementById("gen-digits").innerText = POM_strDigits;
+ document.getElementById("gen-special-one").innerText = POM_strSpecialOne;
+ document.getElementById("gen-special-two").innerText = POM_strSpecialTwo;
+ /* Add timestamp, for no good reason :) */
+ document.getElementById("gen-timestamp").innerText = Date.now();
+ /* Add some event handlers */
+ document.getElementById('mk-password-len-slider').addEventListener('input', function() {
+ let mkLengthField = document.getElementById('mk-length-field');
+ if (mkLengthField) {
+ mkLengthField.value = this.value;
+ if (mkLengthField.value < POM_minLength) {
+ mkLengthField.value = POM_minLength;
+ } else if (mkLengthField.value > POM_maxLength) {
+ mkLengthField.value = POM_maxLength;
+ }
+ this.value = mkLengthField.value;
+ }
+ });
+ document.getElementById('mk-length-field').addEventListener('click', function() {
+ this.select();
+ });
+ document.getElementById('mk-length-field').addEventListener('blur', POM_validateNumInput);
+ document.getElementById('mk-length-field').addEventListener('change', POM_validateNumInput);
+ document.getElementById('mk-password-btn').addEventListener('click', POM_generatePassword);
+ document.getElementById('mk-password-field').addEventListener('click', POM_copyPassword);
+ /* Possibly generate password on load */
+ if (POM_mkPasswordOnLoad) {
+ document.getElementById('mk-password-btn').click();
+ }
+ /* Possibly generate password on configuration changes */
+ if (POM_changeIsClick) {
+ document.getElementById('mk-uppercase-select').addEventListener('click', POM_generatePassword);
+ document.getElementById('mk-special-one-select').addEventListener('click', POM_generatePassword);
+ document.getElementById('mk-digits-select').addEventListener('click', POM_generatePassword);
+ document.getElementById('mk-lowercase-select').addEventListener('click', POM_generatePassword);
+ document.getElementById('mk-digits-select').addEventListener('click', POM_generatePassword);
+ document.getElementById('mk-special-two-select').addEventListener('click', POM_generatePassword);
+ document.getElementById('mk-password-len-slider').addEventListener('change', POM_generatePassword);
+ }
+
+}
+
+
+/* Wait for things to be loaded */
+if (document.readyState === "complete" ||
+ (document.readyState !== "loading" && !document.documentElement.doScroll)) {
+ POM_initialSetup();
+} else {
+ document.addEventListener("DOMContentLoaded", POM_initialSetup);
+}
+
+/* end of file "password-om.js" */