diff --git a/package-lock.json b/package-lock.json index 02eae2f96..17539454c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -939,7 +939,7 @@ "dependencies": { "chalk": { "version": "2.3.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.1.tgz", + "resolved": "http://registry.npmjs.org/chalk/-/chalk-2.3.1.tgz", "integrity": "sha512-QUU4ofkDoMIVO7hcx1iPTISs88wsO8jA92RQIm4JAwZvFGGAV2hSAA1NX7oVj2Ej2Q6NDTcRDjPTFrMCRZoJ6g==", "dev": true, "requires": { @@ -1239,7 +1239,7 @@ }, "@types/node": { "version": "8.9.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-8.9.5.tgz", + "resolved": "http://registry.npmjs.org/@types/node/-/node-8.9.5.tgz", "integrity": "sha512-jRHfWsvyMtXdbhnz5CVHxaBgnV6duZnPlQuRSo/dm/GnmikNcmZhxIES4E9OZjUmQ8C+HCl4KJux+cXN/ErGDQ==", "dev": true }, @@ -1555,7 +1555,7 @@ }, "acorn": { "version": "2.7.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-2.7.0.tgz", + "resolved": "http://registry.npmjs.org/acorn/-/acorn-2.7.0.tgz", "integrity": "sha1-q259nYhqrKiwhbwzEreaGYQz8Oc=", "dev": true }, @@ -1578,7 +1578,7 @@ }, "acorn-globals": { "version": "1.0.9", - "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-1.0.9.tgz", + "resolved": "http://registry.npmjs.org/acorn-globals/-/acorn-globals-1.0.9.tgz", "integrity": "sha1-VbtemGkVB7dFedBRNBMhfDgMVM8=", "dev": true, "requires": { @@ -1593,7 +1593,7 @@ }, "adjust-sourcemap-loader": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/adjust-sourcemap-loader/-/adjust-sourcemap-loader-1.2.0.tgz", + "resolved": "http://registry.npmjs.org/adjust-sourcemap-loader/-/adjust-sourcemap-loader-1.2.0.tgz", "integrity": "sha512-958oaHHVEXMvsY7v7cC5gEkNIcoaAVIhZ4mBReYVZJOTP9IgKmzLjIOhTtzpLMu+qriXvLsVjJ155EeInp45IQ==", "dev": true, "requires": { @@ -2426,7 +2426,7 @@ }, "chalk": { "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "resolved": "http://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", "dev": true, "requires": { @@ -2750,7 +2750,7 @@ }, "browserify-aes": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", + "resolved": "http://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", "dev": true, "requires": { @@ -2787,7 +2787,7 @@ }, "browserify-rsa": { "version": "4.0.1", - "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz", + "resolved": "http://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz", "integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=", "dev": true, "requires": { @@ -3747,7 +3747,7 @@ "dependencies": { "load-json-file": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", + "resolved": "http://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", "dev": true, "requires": { @@ -4051,7 +4051,7 @@ }, "create-hash": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", + "resolved": "http://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", "dev": true, "requires": { @@ -4064,7 +4064,7 @@ }, "create-hmac": { "version": "1.1.7", - "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", + "resolved": "http://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", "dev": true, "requires": { @@ -4607,7 +4607,7 @@ }, "detective-amd": { "version": "2.4.0", - "resolved": "https://registry.npmjs.org/detective-amd/-/detective-amd-2.4.0.tgz", + "resolved": "http://registry.npmjs.org/detective-amd/-/detective-amd-2.4.0.tgz", "integrity": "sha1-XrDfTvXBipQDOwfa8TbbzV/HXNU=", "dev": true, "requires": { @@ -4885,7 +4885,7 @@ }, "diffie-hellman": { "version": "5.0.3", - "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", + "resolved": "http://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==", "dev": true, "requires": { @@ -4955,7 +4955,7 @@ }, "dom-converter": { "version": "0.1.4", - "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.1.4.tgz", + "resolved": "http://registry.npmjs.org/dom-converter/-/dom-converter-0.1.4.tgz", "integrity": "sha1-pF71cnuJDJv/5tfIduexnLDhfzs=", "dev": true, "requires": { @@ -5072,7 +5072,7 @@ }, "readable-stream": { "version": "1.1.14", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", "dev": true, "requires": { @@ -5618,7 +5618,7 @@ }, "express": { "version": "4.16.3", - "resolved": "https://registry.npmjs.org/express/-/express-4.16.3.tgz", + "resolved": "http://registry.npmjs.org/express/-/express-4.16.3.tgz", "integrity": "sha1-avilAjUNsyRuzEvs9rWjTSL37VM=", "dev": true, "requires": { @@ -6834,7 +6834,6 @@ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.4.tgz", "integrity": "sha512-z8H8/diyk76B7q5wg+Ud0+CqzcAF3mBBI/bA5ne5zrRUUIvNkJY//D3BqyH571KuAC4Nr7Rw7CjWX4r0y9DvNg==", "dev": true, - "optional": true, "requires": { "nan": "2.11.0", "node-pre-gyp": "0.10.0" @@ -6843,8 +6842,7 @@ "abbrev": { "version": "1.1.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "ansi-regex": { "version": "2.1.1", @@ -6854,14 +6852,12 @@ "aproba": { "version": "1.2.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "are-we-there-yet": { "version": "1.1.4", "bundled": true, "dev": true, - "optional": true, "requires": { "delegates": "1.0.0", "readable-stream": "2.3.6" @@ -6884,8 +6880,7 @@ "chownr": { "version": "1.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "code-point-at": { "version": "1.1.0", @@ -6905,8 +6900,7 @@ "core-util-is": { "version": "1.0.2", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "debug": { "version": "2.6.9", @@ -6920,26 +6914,22 @@ "deep-extend": { "version": "0.5.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "delegates": { "version": "1.0.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "detect-libc": { "version": "1.0.3", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "fs-minipass": { "version": "1.2.5", "bundled": true, "dev": true, - "optional": true, "requires": { "minipass": "2.2.4" } @@ -6947,14 +6937,12 @@ "fs.realpath": { "version": "1.0.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "gauge": { "version": "2.7.4", "bundled": true, "dev": true, - "optional": true, "requires": { "aproba": "1.2.0", "console-control-strings": "1.1.0", @@ -6970,7 +6958,6 @@ "version": "7.1.2", "bundled": true, "dev": true, - "optional": true, "requires": { "fs.realpath": "1.0.0", "inflight": "1.0.6", @@ -6983,14 +6970,12 @@ "has-unicode": { "version": "2.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "iconv-lite": { "version": "0.4.21", "bundled": true, "dev": true, - "optional": true, "requires": { "safer-buffer": "2.1.2" } @@ -7008,7 +6993,6 @@ "version": "1.0.6", "bundled": true, "dev": true, - "optional": true, "requires": { "once": "1.4.0", "wrappy": "1.0.2" @@ -7022,8 +7006,7 @@ "ini": { "version": "1.3.5", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "is-fullwidth-code-point": { "version": "1.0.0", @@ -7036,8 +7019,7 @@ "isarray": { "version": "1.0.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "minimatch": { "version": "3.0.4", @@ -7065,7 +7047,6 @@ "version": "1.1.0", "bundled": true, "dev": true, - "optional": true, "requires": { "minipass": "2.2.4" } @@ -7088,7 +7069,6 @@ "version": "2.2.0", "bundled": true, "dev": true, - "optional": true, "requires": { "debug": "2.6.9", "iconv-lite": "0.4.21", @@ -7099,7 +7079,6 @@ "version": "0.10.0", "bundled": true, "dev": true, - "optional": true, "requires": { "detect-libc": "1.0.3", "mkdirp": "0.5.1", @@ -7143,7 +7122,6 @@ "version": "4.1.2", "bundled": true, "dev": true, - "optional": true, "requires": { "are-we-there-yet": "1.1.4", "console-control-strings": "1.1.0", @@ -7208,7 +7186,6 @@ "version": "1.2.7", "bundled": true, "dev": true, - "optional": true, "requires": { "deep-extend": "0.5.1", "ini": "1.3.5", @@ -7228,7 +7205,6 @@ "version": "2.3.6", "bundled": true, "dev": true, - "optional": true, "requires": { "core-util-is": "1.0.2", "inherits": "2.0.3", @@ -7243,7 +7219,6 @@ "version": "2.6.2", "bundled": true, "dev": true, - "optional": true, "requires": { "glob": "7.1.2" } @@ -7256,32 +7231,27 @@ "safer-buffer": { "version": "2.1.2", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "sax": { "version": "1.2.4", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "semver": { "version": "5.5.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "set-blocking": { "version": "2.0.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "signal-exit": { "version": "3.0.2", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "string-width": { "version": "1.0.2", @@ -7297,7 +7267,6 @@ "version": "1.1.1", "bundled": true, "dev": true, - "optional": true, "requires": { "safe-buffer": "5.1.1" } @@ -7313,14 +7282,12 @@ "strip-json-comments": { "version": "2.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "tar": { "version": "4.4.1", "bundled": true, "dev": true, - "optional": true, "requires": { "chownr": "1.0.1", "fs-minipass": "1.2.5", @@ -7334,14 +7301,12 @@ "util-deprecate": { "version": "1.0.2", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "wide-align": { "version": "1.1.2", "bundled": true, "dev": true, - "optional": true, "requires": { "string-width": "1.0.2" } @@ -7403,7 +7368,7 @@ }, "get-amd-module-type": { "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-amd-module-type/-/get-amd-module-type-2.0.5.tgz", + "resolved": "http://registry.npmjs.org/get-amd-module-type/-/get-amd-module-type-2.0.5.tgz", "integrity": "sha1-5nHsWpatX79To6IqKJ6SOMdy3bA=", "dev": true, "requires": { @@ -7479,7 +7444,7 @@ }, "load-json-file": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", + "resolved": "http://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", "dev": true, "requires": { @@ -7797,7 +7762,7 @@ }, "readable-stream": { "version": "1.0.34", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", "dev": true, "requires": { @@ -7960,7 +7925,7 @@ }, "lodash": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-1.0.2.tgz", + "resolved": "http://registry.npmjs.org/lodash/-/lodash-1.0.2.tgz", "integrity": "sha1-j1dWDIO1n8JwvT1WG2kAQ0MOJVE=", "dev": true }, @@ -7996,7 +7961,7 @@ "dependencies": { "minimist": { "version": "1.1.3", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.1.3.tgz", + "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.1.3.tgz", "integrity": "sha1-O+39kaktOQFvz6ocaB6Pqhoe/ag=", "dev": true } @@ -8046,7 +8011,7 @@ }, "chalk": { "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "resolved": "http://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", "dev": true, "requires": { @@ -8200,7 +8165,7 @@ }, "load-json-file": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", + "resolved": "http://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", "dev": true, "requires": { @@ -8387,7 +8352,7 @@ }, "chalk": { "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "resolved": "http://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", "dev": true, "requires": { @@ -8461,7 +8426,7 @@ }, "load-json-file": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", + "resolved": "http://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", "dev": true, "requires": { @@ -8601,7 +8566,7 @@ }, "readable-stream": { "version": "2.0.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz", + "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz", "integrity": "sha1-j5A0HmilPMySh4jaz80Rs265t44=", "dev": true, "requires": { @@ -8799,7 +8764,7 @@ }, "chalk": { "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "resolved": "http://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", "dev": true, "requires": { @@ -9216,7 +9181,7 @@ }, "html-webpack-plugin": { "version": "3.2.0", - "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-3.2.0.tgz", + "resolved": "http://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-3.2.0.tgz", "integrity": "sha1-sBq71yOsqqeze2r0SS69oD2d03s=", "dev": true, "requires": { @@ -9251,7 +9216,7 @@ }, "http-errors": { "version": "1.6.3", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "resolved": "http://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", "dev": true, "requires": { @@ -9280,7 +9245,7 @@ }, "http-proxy-middleware": { "version": "0.18.0", - "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-0.18.0.tgz", + "resolved": "http://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-0.18.0.tgz", "integrity": "sha512-Fs25KVMPAIIcgjMZkVHJoKg9VcXcC1C8yb9JUgeDvVXY0S/zgVIhMb+qVswDIgtJe2DfckMSY2d6TuTEutlk6Q==", "dev": true, "requires": { @@ -10077,7 +10042,7 @@ }, "is-obj": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", + "resolved": "http://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=", "dev": true }, @@ -11035,7 +11000,7 @@ }, "load-json-file": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", + "resolved": "http://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", "dev": true, "requires": { @@ -11312,7 +11277,7 @@ "dependencies": { "ansi-regex": { "version": "0.2.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-0.2.1.tgz", + "resolved": "http://registry.npmjs.org/ansi-regex/-/ansi-regex-0.2.1.tgz", "integrity": "sha1-DY6UaWej2BQ/k+JOKYUl/BsiNfk=", "dev": true }, @@ -11324,7 +11289,7 @@ }, "chalk": { "version": "0.5.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-0.5.1.tgz", + "resolved": "http://registry.npmjs.org/chalk/-/chalk-0.5.1.tgz", "integrity": "sha1-Zjs6ZItotV0EaQ1JFnqoN4WPIXQ=", "dev": true, "requires": { @@ -11346,7 +11311,7 @@ }, "mkdirp": { "version": "0.3.5", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.5.tgz", + "resolved": "http://registry.npmjs.org/mkdirp/-/mkdirp-0.3.5.tgz", "integrity": "sha1-3j5fiWHIjHh+4TaN+EmsRBPsqNc=", "dev": true }, @@ -11827,7 +11792,7 @@ }, "chalk": { "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "resolved": "http://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", "dev": true, "requires": { @@ -11875,7 +11840,7 @@ }, "chalk": { "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "resolved": "http://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", "dev": true, "requires": { @@ -12321,7 +12286,7 @@ }, "magic-string": { "version": "0.22.5", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.22.5.tgz", + "resolved": "http://registry.npmjs.org/magic-string/-/magic-string-0.22.5.tgz", "integrity": "sha512-oreip9rJZkzvA8Qzk9HFs8fZGF/u7H/gtrE8EN6RjKJ9kh2HlC+yQ2QezifqTZfGyiuAV0dRv5a+y/8gBb1m9w==", "dev": true, "requires": { @@ -12878,7 +12843,7 @@ }, "minimist": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", "dev": true }, @@ -12962,7 +12927,7 @@ }, "mkdirp": { "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "resolved": "http://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", "dev": true, "requires": { @@ -12971,7 +12936,7 @@ "dependencies": { "minimist": { "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "resolved": "http://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", "dev": true } @@ -13003,7 +12968,7 @@ }, "module-definition": { "version": "2.2.4", - "resolved": "https://registry.npmjs.org/module-definition/-/module-definition-2.2.4.tgz", + "resolved": "http://registry.npmjs.org/module-definition/-/module-definition-2.2.4.tgz", "integrity": "sha1-wKN3HeWM9rzxKu0kdnBsWWrUsss=", "dev": true, "requires": { @@ -13250,7 +13215,7 @@ "dependencies": { "buffer": { "version": "4.9.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz", + "resolved": "http://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz", "integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=", "dev": true, "requires": { @@ -13339,7 +13304,7 @@ }, "chalk": { "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "resolved": "http://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", "dev": true, "requires": { @@ -13407,7 +13372,7 @@ }, "load-json-file": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", + "resolved": "http://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", "dev": true, "requires": { @@ -14298,7 +14263,7 @@ }, "onetime": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz", + "resolved": "http://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz", "integrity": "sha1-ofeDj4MUxRbwXs78vEzP4EtO14k=", "dev": true }, @@ -14323,7 +14288,7 @@ "dependencies": { "minimist": { "version": "0.0.10", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz", + "resolved": "http://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz", "integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=", "dev": true } @@ -14450,7 +14415,7 @@ }, "os-locale": { "version": "1.4.0", - "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", + "resolved": "http://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", "integrity": "sha1-IPnxeuKe00XoveWDsT0gCYA8FNk=", "dev": true, "requires": { @@ -14549,7 +14514,7 @@ }, "parse-asn1": { "version": "5.1.1", - "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.1.tgz", + "resolved": "http://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.1.tgz", "integrity": "sha512-KPx7flKXg775zZpnp9SxJlz00gTd4BmJ2yJufSc44gMCRrRQ7NSzAcSJQfifuOLgW6bEi+ftrALtsgALeB2Adw==", "dev": true, "requires": { @@ -14940,7 +14905,7 @@ }, "chalk": { "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "resolved": "http://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", "dev": true, "requires": { @@ -15093,7 +15058,7 @@ }, "minimist": { "version": "1.1.3", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.1.3.tgz", + "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.1.3.tgz", "integrity": "sha1-O+39kaktOQFvz6ocaB6Pqhoe/ag=", "dev": true } @@ -15150,7 +15115,7 @@ }, "postcss-values-parser": { "version": "1.5.0", - "resolved": "https://registry.npmjs.org/postcss-values-parser/-/postcss-values-parser-1.5.0.tgz", + "resolved": "http://registry.npmjs.org/postcss-values-parser/-/postcss-values-parser-1.5.0.tgz", "integrity": "sha512-3M3p+2gMp0AH3da530TlX8kiO1nxdTnc3C6vr8dMxRLIlh8UYkz0/wcwptSXjhtx2Fr0TySI7a+BHDQ8NL7LaQ==", "dev": true, "requires": { @@ -15561,7 +15526,7 @@ }, "readable-stream": { "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "dev": true, "requires": { @@ -15957,7 +15922,7 @@ }, "readable-stream": { "version": "2.0.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz", + "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz", "integrity": "sha1-j5A0HmilPMySh4jaz80Rs265t44=", "dev": true, "requires": { @@ -16100,7 +16065,7 @@ }, "readable-stream": { "version": "1.0.34", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", "dev": true, "requires": { @@ -16385,7 +16350,7 @@ "dependencies": { "convert-source-map": { "version": "0.3.5", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-0.3.5.tgz", + "resolved": "http://registry.npmjs.org/convert-source-map/-/convert-source-map-0.3.5.tgz", "integrity": "sha1-8dgClQr33SYxof6+BZZVDIarMZA=", "dev": true } @@ -16433,7 +16398,7 @@ }, "rollup": { "version": "0.56.5", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-0.56.5.tgz", + "resolved": "http://registry.npmjs.org/rollup/-/rollup-0.56.5.tgz", "integrity": "sha512-IGPk5vdWrsc4vkiW9XMeXr5QMtxmvATTttTi59w2jBQWe9G/MMQtn8teIBAj+DdK51TrpVT6P0aQUaQUlUYCJA==", "dev": true }, @@ -16493,7 +16458,7 @@ }, "chalk": { "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "resolved": "http://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", "dev": true, "requires": { @@ -16555,7 +16520,7 @@ "dependencies": { "es6-promise": { "version": "3.3.1", - "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.3.1.tgz", + "resolved": "http://registry.npmjs.org/es6-promise/-/es6-promise-3.3.1.tgz", "integrity": "sha1-oIzd6EzNvzTQJ6FFG8kdS80ophM=", "dev": true } @@ -16591,7 +16556,7 @@ }, "load-json-file": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", + "resolved": "http://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", "dev": true, "requires": { @@ -16912,7 +16877,7 @@ }, "yargs": { "version": "11.1.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-11.1.0.tgz", + "resolved": "http://registry.npmjs.org/yargs/-/yargs-11.1.0.tgz", "integrity": "sha512-NwW69J42EsCSanF8kyn5upxvjp5ds+t3+udGBeTbFnERA+lF541DDpMawzo4z6W/QrzNM18D+BPMiOBibnFV5A==", "dev": true, "requires": { @@ -17125,7 +17090,7 @@ }, "sha.js": { "version": "2.4.11", - "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", + "resolved": "http://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", "dev": true, "requires": { @@ -17550,7 +17515,7 @@ }, "spdx-license-ids": { "version": "1.2.2", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-1.2.2.tgz", + "resolved": "http://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-1.2.2.tgz", "integrity": "sha1-yd96NCRZSt5r0RkA1ZZpbcBrrFc=", "dev": true } @@ -18421,7 +18386,7 @@ }, "through": { "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "resolved": "http://registry.npmjs.org/through/-/through-2.3.8.tgz", "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", "dev": true }, @@ -19529,7 +19494,7 @@ }, "readable-stream": { "version": "1.0.34", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", "dev": true, "requires": { @@ -20087,7 +20052,7 @@ }, "request": { "version": "2.85.0", - "resolved": "https://registry.npmjs.org/request/-/request-2.85.0.tgz", + "resolved": "http://registry.npmjs.org/request/-/request-2.85.0.tgz", "integrity": "sha512-8H7Ehijd4js+s6wuVPLjwORxD4zeuyjYugprdOXlPSqaApmL/QOy+EB/beICHVCHkGMKNh5rvihb5ov+IDw4mg==", "dev": true, "requires": { @@ -21509,7 +21474,7 @@ }, "wrap-ansi": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", + "resolved": "http://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", "dev": true, "requires": { diff --git a/src/cdk/keycodes/keycodes.ts b/src/cdk/keycodes/keycodes.ts index 156c0d1b9..04d8f70b2 100644 --- a/src/cdk/keycodes/keycodes.ts +++ b/src/cdk/keycodes/keycodes.ts @@ -109,6 +109,7 @@ export const SEMICOLON = 186; // Firefox (Gecko) fires 59 for SEMICOLON export const EQUALS = 187; // Firefox (Gecko) fires 61 for EQUALS export const COMMA = 188; export const DASH = 189; // Firefox (Gecko) fires 173 for DASH/MINUS +export const PERIOD = 190; export const SLASH = 191; export const APOSTROPHE = 192; export const TILDE = 192; diff --git a/src/cdk/testing/dispatch-events.ts b/src/cdk/testing/dispatch-events.ts index ebecde019..1548e0b02 100644 --- a/src/cdk/testing/dispatch-events.ts +++ b/src/cdk/testing/dispatch-events.ts @@ -14,24 +14,30 @@ export function dispatchEvent(node: Node | Window, event: Event): Event { } /** Shorthand to dispatch a fake event on a specified node. */ +// tslint:disable-next-line:no-reserved-keywords export function dispatchFakeEvent(node: Node | Window, type: string, canBubble?: boolean): Event { return dispatchEvent(node, createFakeEvent(type, canBubble)); } /** Shorthand to dispatch a keyboard event with a specified key code. */ -export function dispatchKeyboardEvent(node: Node, type: string, keyCode: number, target?: Element): +// tslint:disable-next-line:no-reserved-keywords +export function dispatchKeyboardEvent(node: Node, type: string, keyCode: number, target?: Element, + shiftKey = false, ctrlKey = false, altKey = false): KeyboardEvent { - return dispatchEvent(node, createKeyboardEvent(type, keyCode, target)) as KeyboardEvent; + const event = createKeyboardEvent(type, keyCode, target, undefined, shiftKey, ctrlKey, altKey); + + return dispatchEvent(node, event) as KeyboardEvent; } /** Shorthand to dispatch a mouse event on the specified coordinates. */ -// tslint:disable-next-line +// tslint:disable-next-line:no-reserved-keywords export function dispatchMouseEvent(node: Node, type: string, x = 0, y = 0, event = createMouseEvent(type, x, y)): MouseEvent { return dispatchEvent(node, event) as MouseEvent; } /** Shorthand to dispatch a touch event on the specified coordinates. */ +// tslint:disable-next-line:no-reserved-keywords export function dispatchTouchEvent(node: Node, type: string, x = 0, y = 0) { return dispatchEvent(node, createTouchEvent(type, x, y)); } diff --git a/src/cdk/testing/event-objects.ts b/src/cdk/testing/event-objects.ts index 108ebe67f..9d74e2be5 100644 --- a/src/cdk/testing/event-objects.ts +++ b/src/cdk/testing/event-objects.ts @@ -1,5 +1,5 @@ /** Creates a browser MouseEvent with the specified options. */ -// tslint:disable-next-line +// tslint:disable-next-line:no-reserved-keywords export function createMouseEvent(type: string, x = 0, y = 0) { const event = document.createEvent('MouseEvent'); @@ -23,27 +23,28 @@ export function createMouseEvent(type: string, x = 0, y = 0) { } /** Creates a browser TouchEvent with the specified pointer coordinates. */ -// tslint:disable-next-line +// tslint:disable-next-line:no-reserved-keywords export function createTouchEvent(type: string, pageX = 0, pageY = 0) { // In favor of creating events that work for most of the browsers, the event is created // as a basic UI Event. The necessary details for the event will be set manually. const event = document.createEvent('UIEvent'); - const touchDetails = {pageX, pageY}; + const touchDetails = { pageX, pageY }; event.initUIEvent(type, true, true, window, 0); // Most of the browsers don't have a "initTouchEvent" method that can be used to define // the touch details. Object.defineProperties(event, { - touches: {value: [touchDetails]} + touches: { value: [touchDetails] } }); return event; } /** Dispatches a keydown event from an element. */ -// tslint:disable-next-line -export function createKeyboardEvent(type: string, keyCode: number, target?: Element, key?: string) { +// tslint:disable-next-line:no-reserved-keywords +export function createKeyboardEvent(type: string, keyCode: number, target?: Element, key?: string, + shiftKey = false, ctrlKey = false, altKey = false) { const event = document.createEvent('KeyboardEvent') as any; // Firefox does not support `initKeyboardEvent`, but supports `initKeyEvent`. const initEventFn = (event.initKeyEvent || event.initKeyboardEvent).bind(event); @@ -54,9 +55,12 @@ export function createKeyboardEvent(type: string, keyCode: number, target?: Elem // Webkit Browsers don't set the keyCode when calling the init function. // See related bug https://bugs.webkit.org/show_bug.cgi?id=16735 Object.defineProperties(event, { - keyCode: {get: () => keyCode}, - key: {get: () => key}, - target: {get: () => target} + keyCode: { get: () => keyCode }, + key: { get: () => key }, + target: { get: () => target }, + shiftKey: { get: () => shiftKey }, + ctrlKey: { get: () => ctrlKey }, + altKey: { get: () => altKey } }); // IE won't set `defaultPrevented` on synthetic events so we need to do it manually. @@ -70,7 +74,7 @@ export function createKeyboardEvent(type: string, keyCode: number, target?: Elem } /** Creates a fake event object with any desired event type. */ -// tslint:disable-next-line +// tslint:disable-next-line:no-reserved-keywords export function createFakeEvent(type: string, canBubble = false, cancelable = true) { const event = document.createEvent('Event'); event.initEvent(type, canBubble, cancelable); diff --git a/src/lib-dev/input/module.ts b/src/lib-dev/input/module.ts index 971d846ad..a44343479 100644 --- a/src/lib-dev/input/module.ts +++ b/src/lib-dev/input/module.ts @@ -16,6 +16,7 @@ import { McInputModule } from '../../lib/input/'; }) export class InputDemoComponent { value: string = ''; + numberValue: number | null = null; } @@ -36,6 +37,7 @@ export class InputDemoComponent { }) export class InputDemoModule {} +// tslint:disable:no-console platformBrowserDynamic() .bootstrapModule(InputDemoModule) .catch((error) => console.error(error)); diff --git a/src/lib-dev/input/template.html b/src/lib-dev/input/template.html index 369ab302e..96fedee58 100644 --- a/src/lib-dev/input/template.html +++ b/src/lib-dev/input/template.html @@ -1,6 +1,62 @@
+ +
Number Value: {{numberValue}}
+
+ +
Without placeholder:
+ + + + + +

+ +
With placeholder:
+ + + + + +

+ +
Min = -5 Max = 7 Step = 0.5 Big step = 1.5
+ + + + + +

+ +
Min = -5
+ + + + + +

+ +
Step = 0.5
+ + + + + +

+
Without placeholder:
+ + + + +

+ + +
Value: {{value}}
+
+ +
Without placeholder:
diff --git a/src/lib/core/utils/__test__/utils.spec.ts b/src/lib/core/utils/__test__/utils.spec.ts index 9a277a6e4..b167f7c0a 100644 --- a/src/lib/core/utils/__test__/utils.spec.ts +++ b/src/lib/core/utils/__test__/utils.spec.ts @@ -1,5 +1,7 @@ +// tslint:disable:no-magic-numbers import { toBoolean } from '../utils'; + describe('[Core]::utils', () => { it('should work for null values', () => { expect(toBoolean(null)).toBe(false); diff --git a/src/lib/form-field/_form-field-theme.scss b/src/lib/form-field/_form-field-theme.scss index 8b082da00..2b1cec915 100644 --- a/src/lib/form-field/_form-field-theme.scss +++ b/src/lib/form-field/_form-field-theme.scss @@ -70,7 +70,7 @@ color: mc-color($second, 400); } - mc-cleaner { + mc-cleaner { .mc-cleaner__icon { color: mc-color($second, 200); } @@ -79,6 +79,16 @@ color: darken(mc-color($second, 200), $hover-darken); } } + + mc-stepper { + .mc-stepper-step-up, .mc-stepper-step-down { + color: mc-color($second, 200); + + &:hover { + color: darken(mc-color($second, 200), $hover-darken); + } + } + } } } diff --git a/src/lib/form-field/form-field-number-control.ts b/src/lib/form-field/form-field-number-control.ts new file mode 100644 index 000000000..6032239ba --- /dev/null +++ b/src/lib/form-field/form-field-number-control.ts @@ -0,0 +1,28 @@ +import { Observable } from 'rxjs'; + + +/** An interface which allows a control to work inside of a `MсFormField`. */ +export abstract class McFormFieldNumberControl { + /** The value of the control. */ + value: T | null; + + /** + * Stream that emits whenever the state of the control changes such that the parent `MсFormField` + * needs to run change detection. + */ + readonly stateChanges: Observable; + + /** the number step */ + step: number; + + /** the number big step */ + bigStep: number; + + /** Whether the control is focused. */ + readonly focused: boolean; + + /** Handles step up and down */ + abstract stepUp(step: number): void; + + abstract stepDown(step: number): void; +} diff --git a/src/lib/form-field/form-field.html b/src/lib/form-field/form-field.html index ced189617..9371c308c 100644 --- a/src/lib/form-field/form-field.html +++ b/src/lib/form-field/form-field.html @@ -17,6 +17,8 @@ (click)="clearValue($event)">
+ +
diff --git a/src/lib/form-field/form-field.module.ts b/src/lib/form-field/form-field.module.ts index 65d6456b6..4c1b3f15d 100644 --- a/src/lib/form-field/form-field.module.ts +++ b/src/lib/form-field/form-field.module.ts @@ -7,6 +7,7 @@ import { McCleaner } from './cleaner'; import { McFormField, McFormFieldWithoutBorders } from './form-field'; import { McHint } from './hint'; import { McPrefix } from './prefix'; +import { McStepper } from './stepper'; import { McSuffix } from './suffix'; @@ -17,7 +18,8 @@ import { McSuffix } from './suffix'; McHint, McPrefix, McSuffix, - McCleaner + McCleaner, + McStepper ], imports: [CommonModule, McIconModule], exports: [ @@ -26,7 +28,8 @@ import { McSuffix } from './suffix'; McHint, McPrefix, McSuffix, - McCleaner + McCleaner, + McStepper ] }) export class McFormFieldModule { diff --git a/src/lib/form-field/form-field.scss b/src/lib/form-field/form-field.scss index a774871f9..6ce4edf48 100644 --- a/src/lib/form-field/form-field.scss +++ b/src/lib/form-field/form-field.scss @@ -51,7 +51,8 @@ $mc-form-field-border-size: 1px; } .mc-form-field_has-suffix, -.mc-form-field_has-cleaner { +.mc-form-field_has-cleaner, +.mc-form-field_has-stepper { .mc-input { padding-right: 32px; } @@ -78,5 +79,25 @@ mc-cleaner { width: 32px; cursor: pointer; +} + +mc-stepper { + position: absolute; + + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + top: 0; + bottom: 0; + right: 0; + + width: 32px; + + .mc-stepper-step-up, .mc-stepper-step-down { + cursor: pointer; + width: 32px; + text-align: center; + } } diff --git a/src/lib/form-field/form-field.ts b/src/lib/form-field/form-field.ts index 4e2877868..503e8eb2d 100644 --- a/src/lib/form-field/form-field.ts +++ b/src/lib/form-field/form-field.ts @@ -20,8 +20,10 @@ import { startWith } from 'rxjs/operators'; import { McCleaner } from './cleaner'; import { McFormFieldControl } from './form-field-control'; import { getMcFormFieldMissingControlError } from './form-field-errors'; +import { McFormFieldNumberControl } from './form-field-number-control'; import { McHint } from './hint'; import { McPrefix } from './prefix'; +import { McStepper } from './stepper'; import { McSuffix } from './suffix'; @@ -51,7 +53,8 @@ export const _McFormFieldMixinBase: CanColorCtor & typeof McFormFieldBase '[class.mc-form-field_disabled]': '_control.disabled', '[class.mc-form-field_has-prefix]': 'hasPrefix', '[class.mc-form-field_has-suffix]': 'hasSuffix', - '[class.mc-form-field_has-cleaner]': 'hasCleaner', + '[class.mc-form-field_has-cleaner]': 'canShowCleaner', + '[class.mc-form-field_has-stepper]': 'canShowStepper', '[class.mc-focused]': '_control.focused', '[class.ng-untouched]': '_shouldForward("untouched")', '[class.ng-touched]': '_shouldForward("touched")', @@ -60,7 +63,9 @@ export const _McFormFieldMixinBase: CanColorCtor & typeof McFormFieldBase '[class.ng-valid]': '_shouldForward("valid")', '[class.ng-invalid]': '_shouldForward("invalid")', '[class.ng-pending]': '_shouldForward("pending")', - '(keydown)': 'onKeyDown($event)' + '(keydown)': 'onKeyDown($event)', + '(mouseenter)': 'onHoverChanged(true)', + '(mouseleave)': 'onHoverChanged(false)' }, inputs: ['color'], encapsulation: ViewEncapsulation.None, @@ -71,14 +76,18 @@ export class McFormField extends _McFormFieldMixinBase implements AfterContentInit, AfterContentChecked, AfterViewInit, CanColor { @ContentChild(McFormFieldControl) _control: McFormFieldControl; + @ContentChild(McFormFieldNumberControl) _numberControl: McFormFieldNumberControl; @ContentChildren(McHint) _hint: QueryList; @ContentChildren(McSuffix) _suffix: QueryList; @ContentChildren(McPrefix) _prefix: QueryList; @ContentChildren(McCleaner) _cleaner: QueryList; + @ContentChild(McStepper) _stepper: McStepper; // Unique id for the internal form field label. _labelId = `mc-form-field-label-${nextUniqueId++}`; + hovered: boolean = false; + constructor(public _elementRef: ElementRef, private _changeDetectorRef: ChangeDetectorRef) { super(_elementRef); } @@ -88,6 +97,11 @@ export class McFormField extends _McFormFieldMixinBase implements if (this._control.controlType) { this._elementRef.nativeElement.classList .add(`mc-form-field-type-${this._control.controlType}`); + + if (this._numberControl && this.hasStepper) { + this._stepper.stepUp.subscribe(this.onStepUp.bind(this)); + this._stepper.stepDown.subscribe(this.onStepDown.bind(this)); + } } // Subscribe to changes in the child control state in order to update the form field UI. @@ -96,9 +110,15 @@ export class McFormField extends _McFormFieldMixinBase implements this._changeDetectorRef.markForCheck(); }); + if (this._numberControl) { + this._numberControl.stateChanges.pipe(startWith()) + .subscribe(() => { + this._changeDetectorRef.markForCheck(); + }); + } + // Run change detection if the value changes. const valueChanges = this._control.ngControl && this._control.ngControl.valueChanges || EMPTY; - merge(valueChanges) .subscribe(() => this._changeDetectorRef.markForCheck()); } @@ -121,10 +141,13 @@ export class McFormField extends _McFormFieldMixinBase implements } onContainerClick($event) { - return this._control.onContainerClick && this._control.onContainerClick($event); + if (this._control.onContainerClick) { + this._control.onContainerClick($event); + } } onKeyDown(e: KeyboardEvent): void { + // tslint:disable-next-line:deprecation if (e.keyCode === ESCAPE && this._control.focused && this.hasCleaner) { @@ -137,6 +160,25 @@ export class McFormField extends _McFormFieldMixinBase implements } } + onHoverChanged(isHovered: boolean) { + if (isHovered !== this.hovered) { + this.hovered = isHovered; + this._changeDetectorRef.markForCheck(); + } + } + + onStepUp() { + if (this._numberControl) { + this._numberControl.stepUp(this._numberControl.step); + } + } + + onStepDown() { + if (this._numberControl) { + this._numberControl.stepDown(this._numberControl.step); + } + } + /** Determines whether a class from the NgControl should be forwarded to the host element. */ _shouldForward(prop: string): boolean { const ngControl = this._control ? this._control.ngControl : null; @@ -151,27 +193,46 @@ export class McFormField extends _McFormFieldMixinBase implements } } - get hasHint() { + get hasHint(): boolean { return this._hint && this._hint.length > 0; } - get hasSuffix() { + get hasSuffix(): boolean { return this._suffix && this._suffix.length > 0; } - get hasPrefix() { + get hasPrefix(): boolean { return this._prefix && this._prefix.length > 0; } - get hasCleaner() { + get hasCleaner(): boolean { return this._cleaner && this._cleaner.length > 0; } - get canShowCleaner() { + get hasStepper(): boolean { + return !!this._stepper; + } + + get canShowCleaner(): boolean { return this.hasCleaner && - this._control && this._control.ngControl - ? this._control.ngControl.value && !this._control.disabled - : false; + this._control && + this._control.ngControl + ? this._control.ngControl.value && !this._control.disabled + : false; + } + + + get disabled(): boolean { + return this._control && this._control.disabled; + } + + get canShowStepper(): boolean { + return this._numberControl && + !this.disabled && + ( + this._numberControl.focused || + this.hovered + ); } } diff --git a/src/lib/form-field/public-api.ts b/src/lib/form-field/public-api.ts index b0c69da79..c2e4d224c 100644 --- a/src/lib/form-field/public-api.ts +++ b/src/lib/form-field/public-api.ts @@ -1,8 +1,10 @@ export * from './form-field.module'; export * from './form-field'; export * from './form-field-control'; +export * from './form-field-number-control'; export * from './form-field-errors'; export * from './hint'; export * from './suffix'; export * from './prefix'; export * from './cleaner'; +export * from './stepper'; diff --git a/src/lib/form-field/stepper.ts b/src/lib/form-field/stepper.ts new file mode 100644 index 000000000..a190e3ba1 --- /dev/null +++ b/src/lib/form-field/stepper.ts @@ -0,0 +1,26 @@ +import { Component, EventEmitter, Output } from '@angular/core'; + + +@Component({ + selector: 'mc-stepper', + template: ` + + + ` +}) +export class McStepper { + @Output() + readonly stepUp: EventEmitter = new EventEmitter(); + @Output() + readonly stepDown: EventEmitter = new EventEmitter(); + + onStepUp($event: MouseEvent) { + this.stepUp.emit(); + $event.preventDefault(); + } + + onStepDown($event: MouseEvent) { + this.stepDown.emit(); + $event.preventDefault(); + } +} diff --git a/src/lib/icon/icon.scss b/src/lib/icon/icon.scss index d9db8d798..a70cb1599 100644 --- a/src/lib/icon/icon.scss +++ b/src/lib/icon/icon.scss @@ -1,2 +1,26 @@ @import 'icon-base'; @import 'icon-theme'; + +.mc-icon-rotate_90 { + transform: rotate(90deg); +} + +.mc-icon-rotate_180 { + transform: rotate(180deg); +} + +.mc-icon-rotate_270 { + transform: rotate(270deg); +} + +.mc-icon-flip-h { + transform: scaleY(-1); +} + +.mc-icon-flip-v { + transform: scaleX(-1); +} + +.mc-icon-flip-vh { + transform: scale(-1); +} diff --git a/src/lib/input/input-number-validators.ts b/src/lib/input/input-number-validators.ts new file mode 100644 index 000000000..5e655664e --- /dev/null +++ b/src/lib/input/input-number-validators.ts @@ -0,0 +1,79 @@ +import { Directive, forwardRef, Input, OnChanges, Provider, SimpleChanges } from '@angular/core'; +import { AbstractControl, NG_VALIDATORS, ValidationErrors, Validator, ValidatorFn, Validators } from '@angular/forms'; + + +export const MIN_VALIDATOR: Provider = { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => MinValidator), + multi: true +}; + +/** + * A directive which installs the {@link MinValidator} for any `formControlName`, + * `formControl`, or control with `ngModel` that also has a `min` attribute. + * + * @experimental + */ +@Directive({ + selector: '[min][formControlName],[min][formControl],[min][ngModel]', + providers: [MIN_VALIDATOR], + host: {'[attr.min]': 'min ? min : null'} +}) +export class MinValidator implements Validator, OnChanges { + + @Input() min: string; + private _validator: ValidatorFn; + private _onChange: () => void; + + ngOnChanges(changes: SimpleChanges): void { + if ('min' in changes) { + this._createValidator(); + if (this._onChange) { this._onChange(); } + } + } + + validate(c: AbstractControl): ValidationErrors | null { return this._validator(c); } + + registerOnValidatorChange(fn: () => void): void { this._onChange = fn; } + + private _createValidator(): void { this._validator = Validators.min(parseInt(this.min, 10)); } +} + + +export const MAX_VALIDATOR: Provider = { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => MaxValidator), + multi: true +}; + +/** + * A directive which installs the {@link MaxValidator} for any `formControlName`, + * `formControl`, or control with `ngModel` that also has a `min` attribute. + * + * @experimental + */ +@Directive({ + selector: '[max][formControlName],[max][formControl],[max][ngModel]', + providers: [MAX_VALIDATOR], + host: {'[attr.max]': 'max ? max : null'} +}) +export class MaxValidator implements Validator, + OnChanges { + + @Input() max: string; + private _validator: ValidatorFn; + private _onChange: () => void; + + ngOnChanges(changes: SimpleChanges): void { + if ('max' in changes) { + this._createValidator(); + if (this._onChange) { this._onChange(); } + } + } + + validate(c: AbstractControl): ValidationErrors | null { return this._validator(c); } + + registerOnValidatorChange(fn: () => void): void { this._onChange = fn; } + + private _createValidator(): void { this._validator = Validators.max(parseInt(this.max, 10)); } +} diff --git a/src/lib/input/input.module.ts b/src/lib/input/input.module.ts index 88928e538..f46326aa1 100644 --- a/src/lib/input/input.module.ts +++ b/src/lib/input/input.module.ts @@ -4,13 +4,14 @@ import { FormsModule } from '@angular/forms'; import { A11yModule } from '@ptsecurity/cdk/a11y'; import { McCommonModule } from '@ptsecurity/mosaic/core'; -import { McInput, McInputMono } from './input'; +import { McInput, McInputMono, McNumberInput } from './input'; +import { MaxValidator, MinValidator } from './input-number-validators'; @NgModule({ imports: [CommonModule, A11yModule, McCommonModule, FormsModule], - exports: [McInput, McInputMono], - declarations: [McInput, McInputMono] + exports: [McInput, McNumberInput, McInputMono, MinValidator, MaxValidator], + declarations: [McInput, McNumberInput, McInputMono, MinValidator, MaxValidator ] }) export class McInputModule { } diff --git a/src/lib/input/input.scss b/src/lib/input/input.scss index 29e107977..bc363fe94 100644 --- a/src/lib/input/input.scss +++ b/src/lib/input/input.scss @@ -6,3 +6,17 @@ display: inline-block; } + +input.mc-input[type="number"] { + -moz-appearance: textfield; +} + +input.mc-input[type="number"]::-webkit-inner-spin-button, +input.mc-input[type="number"]::-webkit-outer-spin-button { + -webkit-appearance: none; +} + +/* remove default red border for HTML5 validation invalid fields in Firefox */ +input.mc-input:invalid { + box-shadow: unset; +} diff --git a/src/lib/input/input.spec.ts b/src/lib/input/input.spec.ts index 3d49df5d7..673c81446 100644 --- a/src/lib/input/input.spec.ts +++ b/src/lib/input/input.spec.ts @@ -2,7 +2,7 @@ import { Component, Provider, Type } from '@angular/core'; import { ComponentFixture, fakeAsync, TestBed, ComponentFixtureAutoDetect } from '@angular/core/testing'; import { FormsModule } from '@angular/forms'; import { By } from '@angular/platform-browser'; -import { ESCAPE } from '@ptsecurity/cdk/keycodes'; +import { DOWN_ARROW, ESCAPE, UP_ARROW } from '@ptsecurity/cdk/keycodes'; import { dispatchFakeEvent, dispatchKeyboardEvent @@ -335,3 +335,371 @@ describe('McInput', () => { }); }); }); + + +// tslint:disable no-unnecessary-class +@Component({ + template: ` + + + + + ` +}) +class McNumberInput { + value: number | null = null; +} + +@Component({ + template: ` + + + + + ` +}) +class McNumberInputMaxMinStep { + value: number | null = null; +} +// tslint:enable no-unnecessary-class + +// tslint:disable no-magic-numbers +describe('McNumberInput', () => { + it('should have stepper on focus', fakeAsync(() => { + const fixture = createComponent(McNumberInput); + fixture.detectChanges(); + + const inputElementDebug = fixture.debugElement.query(By.directive(McInput)); + const inputElement = inputElementDebug.nativeElement; + + dispatchFakeEvent(inputElement, 'focus'); + fixture.detectChanges(); + + const mcStepper = fixture.debugElement.query(By.css('mc-stepper')); + const icons = mcStepper.queryAll(By.css('.mc-icon')); + + expect(mcStepper).not.toBeNull(); + expect(icons.length).toBe(2); + })); + + it('should not have stepper initially', fakeAsync(() => { + const fixture = createComponent(McNumberInput); + fixture.detectChanges(); + + const mcStepper = fixture.debugElement.query(By.css('mc-stepper')); + + expect(mcStepper).toBeNull(); + })); + + describe('empty value', () => { + it('should not step up when no max', fakeAsync(() => { + const fixture = createComponent(McNumberInput); + fixture.detectChanges(); + + const inputElementDebug = fixture.debugElement.query(By.directive(McInput)); + const inputElement = inputElementDebug.nativeElement; + + dispatchFakeEvent(inputElement, 'focus'); + fixture.detectChanges(); + + const mcStepper = fixture.debugElement.query(By.css('mc-stepper')); + const icons = mcStepper.queryAll(By.css('.mc-icon')); + const iconUp = icons[0]; + + dispatchFakeEvent(iconUp.nativeElement, 'mousedown'); + + fixture.detectChanges(); + + expect(fixture.componentInstance.value).toBeNull(); + })); + + it('should not step down when no min', fakeAsync(() => { + const fixture = createComponent(McNumberInput); + fixture.detectChanges(); + + const inputElementDebug = fixture.debugElement.query(By.directive(McInput)); + const inputElement = inputElementDebug.nativeElement; + + dispatchFakeEvent(inputElement, 'focus'); + fixture.detectChanges(); + + const mcStepper = fixture.debugElement.query(By.css('mc-stepper')); + const icons = mcStepper.queryAll(By.css('.mc-icon')); + const iconDown = icons[0]; + + dispatchFakeEvent(iconDown.nativeElement, 'mousedown'); + + fixture.detectChanges(); + + expect(fixture.componentInstance.value).toBeNull(); + })); + + + it('should step up when max is set', fakeAsync(() => { + const fixture = createComponent(McNumberInputMaxMinStep); + fixture.detectChanges(); + + const inputElementDebug = fixture.debugElement.query(By.directive(McInput)); + const inputElement = inputElementDebug.nativeElement; + + dispatchFakeEvent(inputElement, 'focus'); + fixture.detectChanges(); + + const mcStepper = fixture.debugElement.query(By.css('mc-stepper')); + const icons = mcStepper.queryAll(By.css('.mc-icon')); + const iconUp = icons[0]; + + dispatchFakeEvent(iconUp.nativeElement, 'mousedown'); + + fixture.detectChanges(); + + expect(fixture.componentInstance.value).toBe(3.5); + })); + + it('should step down when min is set', fakeAsync(() => { + const fixture = createComponent(McNumberInputMaxMinStep); + fixture.detectChanges(); + + const inputElementDebug = fixture.debugElement.query(By.directive(McInput)); + const inputElement = inputElementDebug.nativeElement; + + dispatchFakeEvent(inputElement, 'focus'); + + fixture.detectChanges(); + + const mcStepper = fixture.debugElement.query(By.css('mc-stepper')); + const icons = mcStepper.queryAll(By.css('.mc-icon')); + const iconDown = icons[1]; + + dispatchFakeEvent(iconDown.nativeElement, 'mousedown'); + + fixture.detectChanges(); + + expect(fixture.componentInstance.value).toBe(9.5); + })); + }); + + describe('not empty value', () => { + it('should step up when no min', fakeAsync(() => { + const fixture = createComponent(McNumberInput); + fixture.detectChanges(); + + const inputElementDebug = fixture.debugElement.query(By.directive(McInput)); + const inputElement = inputElementDebug.nativeElement; + + inputElement.value = 1; + dispatchFakeEvent(inputElement, 'input'); + dispatchFakeEvent(inputElement, 'focus'); + + fixture.detectChanges(); + + const mcStepper = fixture.debugElement.query(By.css('mc-stepper')); + const icons = mcStepper.queryAll(By.css('.mc-icon')); + const iconUp = icons[0]; + + dispatchFakeEvent(iconUp.nativeElement, 'mousedown'); + + fixture.detectChanges(); + + expect(fixture.componentInstance.value).toBe(2); + })); + + it('should step down when no max', fakeAsync(() => { + const fixture = createComponent(McNumberInput); + fixture.detectChanges(); + + const inputElementDebug = fixture.debugElement.query(By.directive(McInput)); + const inputElement = inputElementDebug.nativeElement; + + inputElement.value = 1; + dispatchFakeEvent(inputElement, 'input'); + dispatchFakeEvent(inputElement, 'focus'); + + fixture.detectChanges(); + + const mcStepper = fixture.debugElement.query(By.css('mc-stepper')); + const icons = mcStepper.queryAll(By.css('.mc-icon')); + const iconDown = icons[1]; + + dispatchFakeEvent(iconDown.nativeElement, 'mousedown'); + + fixture.detectChanges(); + + expect(fixture.componentInstance.value).toBe(0); + })); + }); + + describe('keys', () => { + it('should step up on up arrow key', fakeAsync(() => { + const fixture = createComponent(McNumberInput); + fixture.detectChanges(); + + const inputElementDebug = fixture.debugElement.query(By.directive(McInput)); + const inputElement = inputElementDebug.nativeElement; + + inputElement.value = 1; + dispatchFakeEvent(inputElement, 'input'); + + dispatchKeyboardEvent(inputElementDebug.nativeElement, 'keydown', UP_ARROW); + + fixture.detectChanges(); + + expect(fixture.componentInstance.value).toBe(2); + })); + + it('should step down on down arrow key', fakeAsync(() => { + const fixture = createComponent(McNumberInput); + fixture.detectChanges(); + + const inputElementDebug = fixture.debugElement.query(By.directive(McInput)); + const inputElement = inputElementDebug.nativeElement; + + inputElement.value = 1; + dispatchFakeEvent(inputElement, 'input'); + + dispatchKeyboardEvent(inputElementDebug.nativeElement, 'keydown', DOWN_ARROW); + + fixture.detectChanges(); + + expect(fixture.componentInstance.value).toBe(0); + })); + + it('should step up with bug step on shift and up arrow key', fakeAsync(() => { + const fixture = createComponent(McNumberInputMaxMinStep); + fixture.detectChanges(); + + const inputElementDebug = fixture.debugElement.query(By.directive(McInput)); + const inputElement = inputElementDebug.nativeElement; + + inputElement.value = 5; + dispatchFakeEvent(inputElement, 'input'); + + dispatchKeyboardEvent(inputElementDebug.nativeElement, 'keydown', UP_ARROW, undefined, true); + + fixture.detectChanges(); + + expect(fixture.componentInstance.value).toBe(7); + })); + + it('should step down with bug step on shift and down arrow key', fakeAsync(() => { + const fixture = createComponent(McNumberInputMaxMinStep); + fixture.detectChanges(); + + const inputElementDebug = fixture.debugElement.query(By.directive(McInput)); + const inputElement = inputElementDebug.nativeElement; + + inputElement.value = 6; + dispatchFakeEvent(inputElement, 'input'); + + dispatchKeyboardEvent(inputElementDebug.nativeElement, 'keydown', DOWN_ARROW, undefined, true); + + fixture.detectChanges(); + + expect(fixture.componentInstance.value).toBe(4); + })); + + it('should ignore wrong chars', fakeAsync(() => { + const fixture = createComponent(McNumberInputMaxMinStep); + fixture.detectChanges(); + + const inputElementDebug = fixture.debugElement.query(By.directive(McInput)); + const inputElement = inputElementDebug.nativeElement; + + inputElement.value = 123; + dispatchFakeEvent(inputElement, 'input'); + fixture.detectChanges(); + expect(fixture.componentInstance.value).toBe(123); + + inputElement.value = 'blahblah'; + dispatchFakeEvent(inputElement, 'input'); + fixture.detectChanges(); + expect(fixture.componentInstance.value).toBeNull(); + + inputElement.value = '1.2'; + dispatchFakeEvent(inputElement, 'input'); + fixture.detectChanges(); + expect(fixture.componentInstance.value).toBe(1.2); + + inputElement.value = '1..2'; + dispatchFakeEvent(inputElement, 'input'); + fixture.detectChanges(); + expect(fixture.componentInstance.value).toBeNull(); + + inputElement.value = '1..'; + dispatchFakeEvent(inputElement, 'input'); + fixture.detectChanges(); + expect(fixture.componentInstance.value).toBeNull(); + + inputElement.value = '--1'; + dispatchFakeEvent(inputElement, 'input'); + fixture.detectChanges(); + expect(fixture.componentInstance.value).toBeNull(); + + inputElement.value = '-1-'; + dispatchFakeEvent(inputElement, 'input'); + fixture.detectChanges(); + expect(fixture.componentInstance.value).toBeNull(); + + inputElement.value = '.'; + dispatchFakeEvent(inputElement, 'input'); + fixture.detectChanges(); + expect(fixture.componentInstance.value).toBeNull(); + + inputElement.value = '-'; + dispatchFakeEvent(inputElement, 'input'); + fixture.detectChanges(); + expect(fixture.componentInstance.value).toBeNull(); + })); + }); + + describe('truncate to bounds', () => { + it('should set max when value > max on step up', fakeAsync(() => { + const fixture = createComponent(McNumberInputMaxMinStep); + fixture.detectChanges(); + + const inputElementDebug = fixture.debugElement.query(By.directive(McInput)); + const inputElement = inputElementDebug.nativeElement; + + inputElement.value = 20; + dispatchFakeEvent(inputElement, 'input'); + dispatchFakeEvent(inputElement, 'focus'); + + fixture.detectChanges(); + + const mcStepper = fixture.debugElement.query(By.css('mc-stepper')); + const icons = mcStepper.queryAll(By.css('.mc-icon')); + const iconUp = icons[0]; + + dispatchFakeEvent(iconUp.nativeElement, 'mousedown'); + + fixture.detectChanges(); + + expect(fixture.componentInstance.value).toBe(10); + })); + + it('should set min when value < min on step down', fakeAsync(() => { + const fixture = createComponent(McNumberInputMaxMinStep); + fixture.detectChanges(); + + const inputElementDebug = fixture.debugElement.query(By.directive(McInput)); + const inputElement = inputElementDebug.nativeElement; + + inputElement.value = 1; + dispatchFakeEvent(inputElement, 'input'); + dispatchFakeEvent(inputElement, 'focus'); + + fixture.detectChanges(); + + const mcStepper = fixture.debugElement.query(By.css('mc-stepper')); + const icons = mcStepper.queryAll(By.css('.mc-icon')); + const iconDown = icons[1]; + + dispatchFakeEvent(iconDown.nativeElement, 'mousedown'); + + fixture.detectChanges(); + + expect(fixture.componentInstance.value).toBe(3); + })); + }); +}); +// tslint:enable no-magic-numbers diff --git a/src/lib/input/input.ts b/src/lib/input/input.ts index a2631c95d..7d2731ba8 100644 --- a/src/lib/input/input.ts +++ b/src/lib/input/input.ts @@ -1,11 +1,19 @@ import { + Attribute, Directive, DoCheck, ElementRef, Inject, Input, OnChanges, - OnDestroy, OnInit, Optional, Self + OnDestroy, Optional, Self } from '@angular/core'; -import { Subject } from 'rxjs'; - -import { FormGroupDirective, NgControl, NgForm } from '@angular/forms'; +import { + FormGroupDirective, + NgControl, + NgForm, NgModel +} from '@angular/forms'; import { coerceBooleanProperty } from '@ptsecurity/cdk/coercion'; +import { + END, C, V, X, A, DELETE, BACKSPACE, TAB, ENTER, + ESCAPE, ZERO, NINE, NUMPAD_ZERO, NUMPAD_NINE, NUMPAD_MINUS, DASH, + FF_MINUS, LEFT_ARROW, RIGHT_ARROW, HOME, UP_ARROW, DOWN_ARROW, F1, F12 +} from '@ptsecurity/cdk/keycodes'; import { getSupportedInputTypes, Platform } from '@ptsecurity/cdk/platform'; import { CanUpdateErrorState, @@ -13,10 +21,12 @@ import { ErrorStateMatcher, mixinErrorState } from '@ptsecurity/mosaic/core'; -import { McFormFieldControl } from '@ptsecurity/mosaic/form-field'; +import { McFormFieldControl, McFormFieldNumberControl } from '@ptsecurity/mosaic/form-field'; +import { Subject } from 'rxjs'; import { getMcInputUnsupportedTypeError } from './input-errors'; import { MC_INPUT_VALUE_ACCESSOR } from './input-value-accessor'; +import { stepDown, stepUp } from './stepperUtils'; const MC_INPUT_INVALID_TYPES = [ @@ -31,6 +41,9 @@ const MC_INPUT_INVALID_TYPES = [ 'submit' ]; +export const BIG_STEP = 10; +export const SMALL_STEP = 1; + let nextUniqueId = 0; export class McInputBase { @@ -45,11 +58,196 @@ export const _McInputMixinBase: CanUpdateErrorStateCtor & typeof McInputBase = mixinErrorState(McInputBase); +@Directive({ + selector: `input[mcInput][type="number"]`, + exportAs: 'mcNumericalInput', + providers: [NgModel, { provide: McFormFieldNumberControl, useExisting: McNumberInput }], + host: { + '(blur)': '_focusChanged(false)', + '(focus)': '_focusChanged(true)', + '(paste)': 'onPaste($event)', + '(keydown)': 'onKeyDown($event)' + } +}) +export class McNumberInput implements McFormFieldNumberControl { + /** + * Implemented as part of McFormFieldNumberControl. + * @docs-private + */ + value: any; + + /** + * Implemented as part of McFormFieldNumberControl. + * @docs-private + */ + focused: boolean = false; + + /** + * Implemented as part of McFormFieldNumberControl. + * @docs-private + */ + readonly stateChanges: Subject = new Subject(); + + private readonly _host: HTMLInputElement; + + /** + * Implemented as part of McFormFieldNumberControl. + * @docs-private + */ + private readonly _step: number; + get step() { + return this._step; + } + + /** + * Implemented as part of McFormFieldNumberControl. + * @docs-private + */ + private readonly _bigStep: number; + get bigStep() { + return this._bigStep; + } + + private readonly _min: number; + private readonly _max: number; + + constructor( + private _platform: Platform , + private _elementRef: ElementRef, + private _model: NgModel, + @Attribute('step') step: string, + @Attribute('big-step') bigStep: string, + @Attribute('min') min: string, + @Attribute('max') max: string + ) { + this._step = this.isDigit(step) ? parseFloat(step) : SMALL_STEP; + this._bigStep = this.isDigit(bigStep) ? parseFloat(bigStep) : BIG_STEP; + this._min = this.isDigit(min) ? parseFloat(min) : -Infinity; + this._max = this.isDigit(max) ? parseFloat(max) : Infinity; + + this._host = this._elementRef.nativeElement; + + const self = this; + + if ('valueAsNumber' in this._host) { + Object.defineProperty(Object.getPrototypeOf(this._host), 'valueAsNumber', { + // tslint:disable-next-line:no-reserved-keywords + get() { + const res = parseFloat(self.normalizeSplitter(this.value)); + + return isNaN(res) ? null : res; + } + }); + } + } + + _focusChanged(isFocused: boolean) { + if (isFocused !== this.focused) { + this.focused = isFocused; + this.stateChanges.next(); + } + } + + onKeyDown(event: KeyboardEvent) { + // tslint:disable-next-line:deprecation + const keyCode = event.keyCode; + + const isCtrlA = (e) => e.keyCode === A && (e.ctrlKey || e.metaKey); + const isCtrlC = (e) => e.keyCode === C && (e.ctrlKey || e.metaKey); + const isCtrlV = (e) => e.keyCode === V && (e.ctrlKey || e.metaKey); + const isCtrlX = (e) => e.keyCode === X && (e.ctrlKey || e.metaKey); + + const isFKey = (e) => e.keyCode >= F1 && e.keyCode <= F12; + + const isNumber = (e) => (e.keyCode >= ZERO && e.keyCode <= NINE) || + (e.keyCode >= NUMPAD_ZERO && e.keyCode <= NUMPAD_NINE); + + const minuses = [NUMPAD_MINUS, DASH, FF_MINUS]; + const serviceKeys = [DELETE, BACKSPACE, TAB, ESCAPE, ENTER]; + const arrows = [LEFT_ARROW, RIGHT_ARROW]; + const allowedKeys = [HOME, END].concat(arrows).concat(serviceKeys).concat(minuses); + + const isIEPeriod = (e) => e.key === '.' || e.key === 'Decimal'; + const isNotIEPeriod = (e) => e.key === '.' || e.key === ','; + + // Decimal is for IE + const isPeriod = (e) => this._platform.EDGE || this._platform.TRIDENT + ? isIEPeriod(e) + : isNotIEPeriod(e); + + if (allowedKeys.indexOf(keyCode) !== -1 || + isCtrlA(event) || + isCtrlC(event) || + isCtrlV(event) || + isCtrlX(event) || + isFKey(event) || + isPeriod(event) + ) { + // let it happen, don't do anything + return; + } + // Ensure that it is not a number and stop the keypress + if (event.shiftKey || !isNumber(event)) { + event.preventDefault(); + + // process steps + const step = event.shiftKey ? this._bigStep : this._step; + + if (keyCode === UP_ARROW) { + this.stepUp(step); + } + + if (keyCode === DOWN_ARROW) { + this.stepDown(step); + } + } + } + + onPaste(event) { + let value = event.clipboardData.getData('text'); + value = this.normalizeSplitter(value); + + if (!this.isDigit(value)) { + event.preventDefault(); + } + } + + stepUp(step: number) { + this._elementRef.nativeElement.focus(); + const res = stepUp(this._host.valueAsNumber, this._max, this._min, step); + this._host.value = res === null ? '' : res.toString(); + this._model.update.emit(this._host.valueAsNumber); + } + + stepDown(step: number) { + this._elementRef.nativeElement.focus(); + const res = stepDown(this._host.valueAsNumber, this._max, this._min, step); + this._host.value = res === null ? '' : res.toString(); + this._model.update.emit(this._host.valueAsNumber); + } + + private normalizeSplitter(value: string): string { + return value ? value.replace(/,/g, '.') : value; + } + + private isDigit(value: string): boolean { + return this.isFloat(value) || this.isInt(value); + } + + private isFloat(value: string): boolean { + return /^-?\d+\.\d+$/.test(value); + } + + private isInt(value: string): boolean { + return /^-?\d+$/.test(value); + } +} + @Directive({ selector: `input[mcInput]`, exportAs: 'mcInput', host: { - 'class': 'mc-input', + class: 'mc-input', // Native input properties that are overwritten by Angular inputs need to be synced with // the native input element. Otherwise property bindings for those don't work. '[attr.id]': 'id', @@ -196,7 +394,6 @@ export class McInput extends _McInputMixinBase implements McFormFieldControl { + it('stepUp common', () => { + expect(stepUp(10, 100, -100, 1)).toBe(11); + }); + + it('stepDown common', () => { + expect(stepDown(10, 100, -100, 1)).toBe(9); + }); + + + it('stepUp empty', () => { + expect(stepUp(null, 100, -100, 1)).toBe(-99); + }); + + it('stepDown empty', () => { + expect(stepDown(null, 100, -100, 1)).toBe(99); + }); + + + it('stepUp over step', () => { + expect(stepUp(99, 100, -100, 5)).toBe(100); + }); + + it('stepDown over step', () => { + expect(stepDown(-99, 100, -100, 5)).toBe(-100); + }); + + + it('stepUp no max', () => { + expect(stepUp(99, Infinity, -100, 5)).toBe(104); + }); + + it('stepDown no min', () => { + expect(stepDown(-99, 100, -Infinity, 5)).toBe(-104); + }); + + it('stepUp no min', () => { + expect(stepUp(null, 100, -Infinity, 5)).toBe(null); + }); + + it('stepDown no max', () => { + expect(stepDown(null, Infinity, -100, 5)).toBe(null); + }); +}); diff --git a/src/lib/input/stepperUtils.ts b/src/lib/input/stepperUtils.ts new file mode 100644 index 000000000..80a20537c --- /dev/null +++ b/src/lib/input/stepperUtils.ts @@ -0,0 +1,58 @@ +function sanitizeNumber(value: number): number | null { + return !isFinite(value) || isNaN(value) + ? null + : value; +} + +function getPrecision(value: number): number { + const arr = value.toString().split('.'); + + return arr.length === 1 + ? 1 + // tslint:disable-next-line:no-magic-numbers + : Math.pow(10, arr[1].length); +} + +function add(value1: number, value2: number) { + const precision = Math.max(getPrecision(value1), getPrecision(value2)); + + const res = (value1 * precision + value2 * precision) / precision; + + return sanitizeNumber(res); +} + +export const stepUp = (value: number | null, + max: number, + min: number, + step: number +): number | null => { + let res; + + if (value === null) { + res = add(min, step); + + return res === null ? null : Math.min(res, max); + } + + res = add(value, step); + + return res === null ? null : Math.max(Math.min(res, max), min); +}; + +export const stepDown = (value: number | null, + max: number, + min: number, + step: number +): number | null => { + let res; + + if (value === null) { + res = add(max, -step); + + return res === null ? null : Math.max(res, min); + } + + res = add(value, -step); + + return res === null ? null : Math.min(Math.max(res, min), max); +};