Convert `Ui/User/PasswordStrength` to TypeScript
authorTim Düsterhus <duesterhus@woltlab.com>
Fri, 30 Oct 2020 15:48:52 +0000 (16:48 +0100)
committerTim Düsterhus <duesterhus@woltlab.com>
Fri, 30 Oct 2020 15:48:52 +0000 (16:48 +0100)
global.d.ts
package-lock.json
package.json
wcfsetup/install/files/js/WoltLabSuite/Core/Ui/User/PasswordStrength.js
wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/User/PasswordStrength.js [deleted file]
wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/User/PasswordStrength.ts [new file with mode: 0644]

index a11ef48d52ea09306859554351ffd508db1100b9..20766bfff57a4d8ac1c2125675544fb4964fa093 100644 (file)
@@ -3,6 +3,7 @@ import Devtools from './wcfsetup/install/files/ts/WoltLabSuite/Core/Devtools';
 import DomUtil from './wcfsetup/install/files/ts/WoltLabSuite/Core/Dom/Util';
 import * as ColorUtil from './wcfsetup/install/files/ts/WoltLabSuite/Core/ColorUtil';
 import UiDropdownSimple from './wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/Dropdown/Simple';
+import "@woltlab/zxcvbn";
 
 declare global {
   interface Window {
index b61d84e3cbc0c5b468a9a38755fa954eabf13630..c0a4054589b3bdabc583abfd5974bee5580e7b94 100644 (file)
       "integrity": "sha512-zSLdgIcZXxqamFwIuogGVM22UwStYhWhHgzXXczW3GwNAv1LZdgL0XYGaufipf/FgB2Jj5jTT0ov0v5TVYnUjA==",
       "dev": true
     },
+    "@types/zxcvbn": {
+      "version": "4.4.0",
+      "resolved": "https://registry.npmjs.org/@types/zxcvbn/-/zxcvbn-4.4.0.tgz",
+      "integrity": "sha512-GQLOT+SN20a+AI51y3fAimhyTF4Y0RG+YP3gf91OibIZ7CJmPFgoZi+ZR5a+vRbS01LbQosITWum4ATmJ1Z6Pg==",
+      "dev": true
+    },
     "@typescript-eslint/eslint-plugin": {
       "version": "4.6.0",
       "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.6.0.tgz",
         "eslint-visitor-keys": "^2.0.0"
       }
     },
+    "@woltlab/zxcvbn": {
+      "version": "git+https://github.com/WoltLab/zxcvbn.git#6868f54f9d66073c83c1789e736c6c7f358c2da7",
+      "from": "git+https://github.com/WoltLab/zxcvbn.git#master",
+      "dev": true,
+      "requires": {
+        "@types/zxcvbn": "^4.4.0"
+      }
+    },
     "acorn": {
       "version": "7.4.1",
       "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz",
index f64c55651da68da6093da78d868ddaf7c5dcb2c6..18c8160b501f7e013df87f9318244719616c9a31 100644 (file)
@@ -6,6 +6,7 @@
     "@types/pica": "^5.1.1",
     "@typescript-eslint/eslint-plugin": "^4.6.0",
     "@typescript-eslint/parser": "^4.6.0",
+    "@woltlab/zxcvbn": "git+https://github.com/WoltLab/zxcvbn.git#master",
     "eslint": "^7.12.1",
     "eslint-config-prettier": "^6.15.0",
     "prettier": "^2.1.2",
index d0268fb835127289c1792a17417927f169411c8a..cabb16a7ec9d16c202c5ae2dd28db82c55287b51 100644 (file)
@@ -7,14 +7,18 @@
  * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
  * @module     WoltLabSuite/Core/Ui/User/PasswordStrength
  */
-define(['Core', 'Language'], function (Core, Language) {
-    'use strict';
-    var STATIC_DICTIONARY = [];
-    if (elBySel('meta[property="og:site_name"]')) {
-        STATIC_DICTIONARY.push(elBySel('meta[property="og:site_name"]').getAttribute('content'));
+define(["require", "exports", "tslib", "../../Language", "../../Dom/Util"], function (require, exports, tslib_1, Language, Util_1) {
+    "use strict";
+    var _a;
+    Language = tslib_1.__importStar(Language);
+    Util_1 = tslib_1.__importDefault(Util_1);
+    const STATIC_DICTIONARY = [];
+    const siteName = (_a = document.querySelector('meta[property="og:site_name"]')) === null || _a === void 0 ? void 0 : _a.getAttribute("content");
+    if (siteName) {
+        STATIC_DICTIONARY.push(siteName);
     }
     function flatMap(array, callback) {
-        return array.map(callback).reduce(function (carry, item) {
+        return array.map(callback).reduce((carry, item) => {
             return carry.concat(item);
         }, []);
     }
@@ -22,98 +26,68 @@ define(['Core', 'Language'], function (Core, Language) {
         return [].concat(value, value.split(/\W+/));
     }
     function initializeFeedbacker(Feedback) {
-        var phrases = Core.extend({}, Feedback.default_phrases);
-        for (var type in phrases) {
-            if (phrases.hasOwnProperty(type)) {
-                for (var phrase in phrases[type]) {
-                    if (phrases[type].hasOwnProperty(phrase)) {
-                        var languageItem = 'wcf.user.password.zxcvbn.' + type + '.' + phrase;
-                        var value = Language.get(languageItem);
-                        if (value !== languageItem) {
-                            phrases[type][phrase] = value;
-                        }
-                    }
-                }
-            }
-        }
-        return new Feedback(phrases);
-    }
-    /**
-     * @constructor
-     */
-    function PasswordStrength(input, options) {
-        require(['zxcvbn']).then(function (modules) {
-            var zxcvbn = modules[0];
-            this.init(zxcvbn, input, options);
-        }.bind(this));
-    }
-    PasswordStrength.prototype = {
-        /**
-         * @param      {*}             zxcvbn
-         * @param      {Element}       input
-         * @param      {object}        options
-         */
-        init: function (zxcvbn, input, options) {
-            this._zxcvbn = zxcvbn;
-            this._input = input;
-            this._options = Core.extend({
-                relatedInputs: [],
-                staticDictionary: []
-            }, options);
-            if (!this._options.feedbacker) {
-                this._options.feedbacker = initializeFeedbacker(zxcvbn.Feedback);
-            }
-            this._wrapper = elCreate('div');
-            this._wrapper.className = 'inputAddon inputAddonPasswordStrength';
-            this._input.parentNode.insertBefore(this._wrapper, this._input);
-            this._wrapper.appendChild(this._input);
-            var rating = elCreate('div');
-            rating.className = 'passwordStrengthRating';
-            var ratingLabel = elCreate('small');
-            ratingLabel.textContent = Language.get('wcf.user.password.strength');
-            rating.appendChild(ratingLabel);
-            this._score = elCreate('span');
-            this._score.className = 'passwordStrengthScore';
-            elData(this._score, 'score', '-1');
-            rating.appendChild(this._score);
-            this._wrapper.appendChild(rating);
-            this._feedback = elCreate('div');
-            this._feedback.className = 'passwordStrengthFeedback';
-            this._wrapper.appendChild(this._feedback);
-            this._verdictResult = elCreate('input');
-            this._verdictResult.type = 'hidden';
-            this._verdictResult.name = this._input.name + '_passwordStrengthVerdict';
-            this._wrapper.parentNode.insertBefore(this._verdictResult, this._wrapper);
-            var callback = this._evaluate.bind(this);
-            this._input.addEventListener('input', callback);
-            this._options.relatedInputs.forEach(function (input) {
-                input.addEventListener('input', callback);
+        const localizedPhrases = {};
+        Object.entries(Feedback.default_phrases).forEach(([type, phrases]) => {
+            localizedPhrases[type] = {};
+            Object.entries(phrases).forEach(([identifier, phrase]) => {
+                const languageItem = `wcf.user.password.zxcvbn.${type}.${identifier}`;
+                const localizedValue = Language.get(languageItem);
+                localizedPhrases[type][identifier] = localizedValue !== languageItem ? localizedValue : phrase;
             });
-            if (this._input.value.trim() !== '') {
-                this._evaluate();
-            }
-        },
-        /**
-         * @param {Event=} event
-         */
-        _evaluate: function (event) {
-            var dictionary = flatMap(STATIC_DICTIONARY.concat(this._options.staticDictionary, this._options.relatedInputs.map(function (input) {
-                return input.value.trim();
-            })), splitIntoWords).filter(function (value) {
-                return value.length > 0;
+        });
+        return new Feedback(localizedPhrases);
+    }
+    class PasswordStrength {
+        constructor(input, options) {
+            this.input = input;
+            this.wrapper = document.createElement("div");
+            this.score = document.createElement("span");
+            this.verdictResult = document.createElement("input");
+            void new Promise((resolve_1, reject_1) => { require(["zxcvbn"], resolve_1, reject_1); }).then(tslib_1.__importStar).then(({ default: zxcvbn }) => {
+                this.zxcvbn = zxcvbn;
+                if (options.relatedInputs) {
+                    this.relatedInputs = options.relatedInputs;
+                }
+                if (options.staticDictionary) {
+                    this.staticDictionary = options.staticDictionary;
+                }
+                this.feedbacker = initializeFeedbacker(zxcvbn.Feedback);
+                this.wrapper.className = "inputAddon inputAddonPasswordStrength";
+                this.input.parentNode.insertBefore(this.wrapper, this.input);
+                this.wrapper.appendChild(this.input);
+                const rating = document.createElement("div");
+                rating.className = "passwordStrengthRating";
+                const ratingLabel = document.createElement("small");
+                ratingLabel.textContent = Language.get("wcf.user.password.strength");
+                rating.appendChild(ratingLabel);
+                this.score.className = "passwordStrengthScore";
+                this.score.dataset.score = "-1";
+                rating.appendChild(this.score);
+                this.wrapper.appendChild(rating);
+                this.verdictResult.type = "hidden";
+                this.verdictResult.name = `${this.input.name}_passwordStrengthVerdict`;
+                this.wrapper.parentNode.insertBefore(this.verdictResult, this.wrapper);
+                this.input.addEventListener("input", (ev) => this.evaluate(ev));
+                this.relatedInputs.forEach((input) => input.addEventListener("input", (ev) => this.evaluate(ev)));
+                if (this.input.value.trim() !== "") {
+                    this.evaluate();
+                }
             });
-            var value = this._input.value.trim();
+        }
+        evaluate(event) {
+            const dictionary = flatMap(STATIC_DICTIONARY.concat(this.staticDictionary, this.relatedInputs.map((input) => input.value.trim())), splitIntoWords).filter((value) => value.length > 0);
+            const value = this.input.value.trim();
             // To bound runtime latency for really long passwords, consider sending zxcvbn() only
             // the first 100 characters or so of user input.
-            var verdict = this._zxcvbn(value.substr(0, 100), dictionary);
-            verdict.feedback = this._options.feedbacker.from_result(verdict);
-            elData(this._score, 'score', value.length === 0 ? '-1' : verdict.score);
+            const verdict = this.zxcvbn(value.substr(0, 100), dictionary);
+            verdict.feedback = this.feedbacker.from_result(verdict);
+            this.score.dataset.score = value.length === 0 ? "-1" : verdict.score.toString();
             if (event !== undefined) {
                 // Do not overwrite the value on page load.
-                elInnerError(this._wrapper, verdict.feedback.warning);
+                Util_1.default.innerError(this.wrapper, verdict.feedback.warning);
             }
-            this._verdictResult.value = JSON.stringify(verdict);
+            this.verdictResult.value = JSON.stringify(verdict);
         }
-    };
+    }
     return PasswordStrength;
 });
diff --git a/wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/User/PasswordStrength.js b/wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/User/PasswordStrength.js
deleted file mode 100644 (file)
index 00a4087..0000000
+++ /dev/null
@@ -1,145 +0,0 @@
-/**
- * Adds a password strength meter to a password input and exposes
- * zxcbn's verdict as sibling input.
- *
- * @author     Tim Duesterhus
- * @copyright  2001-2020 WoltLab GmbH
- * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @module     WoltLabSuite/Core/Ui/User/PasswordStrength
- */
-define(['Core', 'Language'], function (Core, Language) {
-       'use strict';
-       
-       var STATIC_DICTIONARY = [];
-       if (elBySel('meta[property="og:site_name"]')) {
-               STATIC_DICTIONARY.push(elBySel('meta[property="og:site_name"]').getAttribute('content'));
-       }
-       
-       function flatMap(array, callback) {
-               return array.map(callback).reduce(function (carry, item) {
-                       return carry.concat(item);
-               }, []);
-       }
-       
-       function splitIntoWords(value) {
-               return [].concat(value, value.split(/\W+/));
-       }
-       
-       function initializeFeedbacker(Feedback) {
-               var phrases = Core.extend({}, Feedback.default_phrases);
-               for (var type in phrases) {
-                       if (phrases.hasOwnProperty(type)) {
-                               for (var phrase in phrases[type]) {
-                                       if (phrases[type].hasOwnProperty(phrase)) {
-                                               var languageItem = 'wcf.user.password.zxcvbn.' + type + '.' + phrase;
-                                               var value = Language.get(languageItem);
-                                               if (value !== languageItem) {
-                                                       phrases[type][phrase] = value;
-                                               }
-                                       }
-                               }
-                       }
-               }
-               return new Feedback(phrases);
-       }
-       
-       /**
-        * @constructor
-        */
-       function PasswordStrength(input, options) {
-               require(['zxcvbn']).then(function (modules) {
-                       var zxcvbn = modules[0];
-                       this.init(zxcvbn, input, options);
-               }.bind(this));
-       }
-       
-       PasswordStrength.prototype = {
-               /**
-                * @param       {*}             zxcvbn
-                * @param       {Element}       input
-                * @param       {object}        options
-                */
-               init: function (zxcvbn, input, options) {
-                       this._zxcvbn = zxcvbn;
-                       this._input = input;
-                       
-                       this._options = Core.extend({
-                               relatedInputs: [],
-                               staticDictionary: []
-                       }, options);
-                       
-                       if (!this._options.feedbacker) {
-                               this._options.feedbacker = initializeFeedbacker(zxcvbn.Feedback);
-                       }
-                       
-                       this._wrapper = elCreate('div');
-                       this._wrapper.className = 'inputAddon inputAddonPasswordStrength';
-                       this._input.parentNode.insertBefore(this._wrapper, this._input);
-                       this._wrapper.appendChild(this._input);
-                       
-                       var rating = elCreate('div');
-                       rating.className = 'passwordStrengthRating';
-                       
-                       var ratingLabel = elCreate('small');
-                       ratingLabel.textContent = Language.get('wcf.user.password.strength');
-                       rating.appendChild(ratingLabel);
-                       
-                       this._score = elCreate('span');
-                       this._score.className = 'passwordStrengthScore';
-                       elData(this._score, 'score', '-1');
-                       rating.appendChild(this._score);
-                       
-                       this._wrapper.appendChild(rating);
-                       
-                       this._feedback = elCreate('div');
-                       this._feedback.className = 'passwordStrengthFeedback';
-                       this._wrapper.appendChild(this._feedback);
-                       
-                       this._verdictResult = elCreate('input');
-                       this._verdictResult.type = 'hidden';
-                       this._verdictResult.name = this._input.name + '_passwordStrengthVerdict';
-                       this._wrapper.parentNode.insertBefore(this._verdictResult, this._wrapper);
-                       
-                       var callback = this._evaluate.bind(this);
-                       this._input.addEventListener('input', callback);
-                       this._options.relatedInputs.forEach(function (input) {
-                               input.addEventListener('input', callback);
-                       });
-                       
-                       if (this._input.value.trim() !== '') {
-                               this._evaluate();
-                       }
-               },
-               
-               /**
-                * @param {Event=} event
-                */
-               _evaluate: function (event) {
-                       var dictionary = flatMap(STATIC_DICTIONARY.concat(this._options.staticDictionary,
-                               this._options.relatedInputs.map(function (input) {
-                                       return input.value.trim();
-                               })
-                       ), splitIntoWords).filter(function (value) {
-                               return value.length > 0;
-                       });
-                       
-                       var value = this._input.value.trim();
-                       
-                       // To bound runtime latency for really long passwords, consider sending zxcvbn() only
-                       // the first 100 characters or so of user input.
-                       var verdict = this._zxcvbn(value.substr(0, 100), dictionary);
-                       verdict.feedback = this._options.feedbacker.from_result(verdict);
-                       
-                       elData(this._score, 'score', value.length === 0 ? '-1' : verdict.score);
-                       
-                       if (event !== undefined) {
-                               // Do not overwrite the value on page load.
-                               elInnerError(this._wrapper, verdict.feedback.warning);
-                       }
-                       
-                       this._verdictResult.value = JSON.stringify(verdict);
-               }
-       };
-       
-       return PasswordStrength;
-});
diff --git a/wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/User/PasswordStrength.ts b/wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/User/PasswordStrength.ts
new file mode 100644 (file)
index 0000000..7e867d6
--- /dev/null
@@ -0,0 +1,136 @@
+/**
+ * Adds a password strength meter to a password input and exposes
+ * zxcbn's verdict as sibling input.
+ *
+ * @author     Tim Duesterhus
+ * @copyright  2001-2020 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module     WoltLabSuite/Core/Ui/User/PasswordStrength
+ */
+
+import * as Language from "../../Language";
+import DomUtil from "../../Dom/Util";
+
+// zxcvbn is imported for the types only. It is loaded on demand, due to its size.
+import zxcvbn from "zxcvbn";
+
+type StaticDictionary = string[];
+
+const STATIC_DICTIONARY: StaticDictionary = [];
+
+const siteName = document.querySelector('meta[property="og:site_name"]')?.getAttribute("content");
+if (siteName) {
+  STATIC_DICTIONARY.push(siteName);
+}
+
+function flatMap<T, U>(array: T[], callback: (x: T) => U[]): U[] {
+  return array.map(callback).reduce((carry, item) => {
+    return carry.concat(item);
+  }, [] as U[]);
+}
+
+function splitIntoWords(value: string): string[] {
+  return ([] as string[]).concat(value, value.split(/\W+/));
+}
+
+function initializeFeedbacker(Feedback: typeof zxcvbn.Feedback): zxcvbn.Feedback {
+  const localizedPhrases: typeof Feedback.default_phrases = {} as typeof Feedback.default_phrases;
+
+  Object.entries(Feedback.default_phrases).forEach(([type, phrases]) => {
+    localizedPhrases[type] = {};
+    Object.entries(phrases).forEach(([identifier, phrase]) => {
+      const languageItem = `wcf.user.password.zxcvbn.${type}.${identifier}`;
+      const localizedValue = Language.get(languageItem);
+      localizedPhrases[type][identifier] = localizedValue !== languageItem ? localizedValue : phrase;
+    });
+  });
+
+  return new Feedback(localizedPhrases);
+}
+
+class PasswordStrength {
+  private zxcvbn: typeof zxcvbn;
+  private relatedInputs: HTMLInputElement[];
+  private staticDictionary: StaticDictionary;
+  private feedbacker: zxcvbn.Feedback;
+
+  private readonly wrapper = document.createElement("div");
+  private readonly score = document.createElement("span");
+  private readonly verdictResult = document.createElement("input");
+
+  constructor(private readonly input: HTMLInputElement, options: Partial<Options>) {
+    void import("zxcvbn").then(({ default: zxcvbn }) => {
+      this.zxcvbn = zxcvbn;
+
+      if (options.relatedInputs) {
+        this.relatedInputs = options.relatedInputs;
+      }
+      if (options.staticDictionary) {
+        this.staticDictionary = options.staticDictionary;
+      }
+
+      this.feedbacker = initializeFeedbacker(zxcvbn.Feedback);
+
+      this.wrapper.className = "inputAddon inputAddonPasswordStrength";
+      this.input.parentNode!.insertBefore(this.wrapper, this.input);
+      this.wrapper.appendChild(this.input);
+
+      const rating = document.createElement("div");
+      rating.className = "passwordStrengthRating";
+
+      const ratingLabel = document.createElement("small");
+      ratingLabel.textContent = Language.get("wcf.user.password.strength");
+      rating.appendChild(ratingLabel);
+
+      this.score.className = "passwordStrengthScore";
+      this.score.dataset.score = "-1";
+      rating.appendChild(this.score);
+
+      this.wrapper.appendChild(rating);
+
+      this.verdictResult.type = "hidden";
+      this.verdictResult.name = `${this.input.name}_passwordStrengthVerdict`;
+      this.wrapper.parentNode!.insertBefore(this.verdictResult, this.wrapper);
+
+      this.input.addEventListener("input", (ev) => this.evaluate(ev));
+      this.relatedInputs.forEach((input) => input.addEventListener("input", (ev) => this.evaluate(ev)));
+      if (this.input.value.trim() !== "") {
+        this.evaluate();
+      }
+    });
+  }
+
+  private evaluate(event?: Event) {
+    const dictionary = flatMap(
+      STATIC_DICTIONARY.concat(
+        this.staticDictionary,
+        this.relatedInputs.map((input) => input.value.trim())
+      ),
+      splitIntoWords
+    ).filter((value) => value.length > 0);
+
+    const value = this.input.value.trim();
+
+    // To bound runtime latency for really long passwords, consider sending zxcvbn() only
+    // the first 100 characters or so of user input.
+    const verdict = this.zxcvbn(value.substr(0, 100), dictionary);
+    verdict.feedback = this.feedbacker.from_result(verdict);
+
+    this.score.dataset.score = value.length === 0 ? "-1" : verdict.score.toString();
+
+    if (event !== undefined) {
+      // Do not overwrite the value on page load.
+      DomUtil.innerError(this.wrapper, verdict.feedback.warning);
+    }
+
+    this.verdictResult.value = JSON.stringify(verdict);
+  }
+}
+
+export = PasswordStrength;
+
+interface Options {
+  relatedInputs: PasswordStrength["relatedInputs"];
+  staticDictionary: PasswordStrength["staticDictionary"];
+  feedbacker: PasswordStrength["feedbacker"];
+}