From 13a347ce37ea43357bb4717270fbe959e66885e7 Mon Sep 17 00:00:00 2001 From: Lim Chee Aun Date: Fri, 23 Dec 2022 16:45:02 +0800 Subject: [PATCH] New: char count for Compose field Uses pre-compiled regex for perf --- package-lock.json | 65 ++++++++++++++++++++++++++++++++++++++ package.json | 1 + scripts/extract-url.js | 48 ++++++++++++++++++++++++++++ src/app.css | 59 ++++++++++++++++++++++++++++++++++ src/components/compose.jsx | 52 ++++++++++++++++++++++++++++++ src/data/url-regex.json | 1 + 6 files changed, 226 insertions(+) create mode 100644 scripts/extract-url.js create mode 100644 src/data/url-regex.json diff --git a/package-lock.json b/package-lock.json index f1d303a0..0790becd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,6 +29,7 @@ "autoprefixer": "~10.4.13", "postcss": "~8.4.20", "postcss-dark-theme-class": "~0.7.3", + "twitter-text": "~3.1.0", "vite": "~4.0.3", "vite-plugin-pwa": "~0.14.0", "workbox-cacheable-response": "~6.5.4", @@ -2930,6 +2931,14 @@ "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", "dev": true }, + "node_modules/core-js": { + "version": "2.6.12", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.12.tgz", + "integrity": "sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==", + "deprecated": "core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js.", + "dev": true, + "hasInstallScript": true + }, "node_modules/core-js-compat": { "version": "3.26.1", "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.26.1.tgz", @@ -5075,6 +5084,30 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==" }, + "node_modules/twemoji-parser": { + "version": "11.0.2", + "resolved": "https://registry.npmjs.org/twemoji-parser/-/twemoji-parser-11.0.2.tgz", + "integrity": "sha512-5kO2XCcpAql6zjdLwRwJjYvAZyDy3+Uj7v1ipBzLthQmDL7Ce19bEqHr3ImSNeoSW2OA8u02XmARbXHaNO8GhA==", + "dev": true + }, + "node_modules/twitter-text": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/twitter-text/-/twitter-text-3.1.0.tgz", + "integrity": "sha512-nulfUi3FN6z0LUjYipJid+eiwXvOLb8Ass7Jy/6zsXmZK3URte043m8fL3FyDzrK+WLpyqhHuR/TcARTN/iuGQ==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.3.1", + "core-js": "^2.5.0", + "punycode": "1.4.1", + "twemoji-parser": "^11.0.2" + } + }, + "node_modules/twitter-text/node_modules/punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==", + "dev": true + }, "node_modules/type-fest": { "version": "0.16.0", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.16.0.tgz", @@ -7752,6 +7785,12 @@ "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", "dev": true }, + "core-js": { + "version": "2.6.12", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.12.tgz", + "integrity": "sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==", + "dev": true + }, "core-js-compat": { "version": "3.26.1", "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.26.1.tgz", @@ -9316,6 +9355,32 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==" }, + "twemoji-parser": { + "version": "11.0.2", + "resolved": "https://registry.npmjs.org/twemoji-parser/-/twemoji-parser-11.0.2.tgz", + "integrity": "sha512-5kO2XCcpAql6zjdLwRwJjYvAZyDy3+Uj7v1ipBzLthQmDL7Ce19bEqHr3ImSNeoSW2OA8u02XmARbXHaNO8GhA==", + "dev": true + }, + "twitter-text": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/twitter-text/-/twitter-text-3.1.0.tgz", + "integrity": "sha512-nulfUi3FN6z0LUjYipJid+eiwXvOLb8Ass7Jy/6zsXmZK3URte043m8fL3FyDzrK+WLpyqhHuR/TcARTN/iuGQ==", + "dev": true, + "requires": { + "@babel/runtime": "^7.3.1", + "core-js": "^2.5.0", + "punycode": "1.4.1", + "twemoji-parser": "^11.0.2" + }, + "dependencies": { + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==", + "dev": true + } + } + }, "type-fest": { "version": "0.16.0", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.16.0.tgz", diff --git a/package.json b/package.json index ff907812..7ef73a72 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "autoprefixer": "~10.4.13", "postcss": "~8.4.20", "postcss-dark-theme-class": "~0.7.3", + "twitter-text": "~3.1.0", "vite": "~4.0.3", "vite-plugin-pwa": "~0.14.0", "workbox-cacheable-response": "~6.5.4", diff --git a/scripts/extract-url.js b/scripts/extract-url.js new file mode 100644 index 00000000..b1ab1335 --- /dev/null +++ b/scripts/extract-url.js @@ -0,0 +1,48 @@ +import fs from 'fs'; +import regexSupplant from 'twitter-text/dist/lib/regexSupplant.js'; +import validDomain from 'twitter-text/dist/regexp/validDomain.js'; +import validPortNumber from 'twitter-text/dist/regexp/validPortNumber.js'; +import validUrlPath from 'twitter-text/dist/regexp/validUrlPath.js'; +import validUrlPrecedingChars from 'twitter-text/dist/regexp/validUrlPrecedingChars.js'; +import validUrlQueryChars from 'twitter-text/dist/regexp/validUrlQueryChars.js'; +import validUrlQueryEndingChars from 'twitter-text/dist/regexp/validUrlQueryEndingChars.js'; + +// The difference with twitter-text's extractURL is that the protocol isn't +// optional. + +const urlRegex = regexSupplant( + '(' + // $1 total match + '(#{validUrlPrecedingChars})' + // $2 Preceeding chracter + '(' + // $3 URL + '(https?:\\/\\/)' + // $4 Protocol (optional) <-- THIS IS THE DIFFERENCE, MISSING '?' AFTER PROTOCOL + '(#{validDomain})' + // $5 Domain(s) + '(?::(#{validPortNumber}))?' + // $6 Port number (optional) + '(\\/#{validUrlPath}*)?' + // $7 URL Path + '(\\?#{validUrlQueryChars}*#{validUrlQueryEndingChars})?' + // $8 Query String + ')' + + ')', + { + validUrlPrecedingChars, + validDomain, + validPortNumber, + validUrlPath, + validUrlQueryChars, + validUrlQueryEndingChars, + }, + 'gi', +); + +const filePath = 'src/data/url-regex.json'; +fs.writeFile( + filePath, + JSON.stringify({ + source: urlRegex.source, + flags: urlRegex.flags, + }), + (err) => { + if (err) { + console.error(err); + } + console.log(`Wrote ${filePath}`); + }, +); diff --git a/src/app.css b/src/app.css index e3732225..e918cc9e 100644 --- a/src/app.css +++ b/src/app.css @@ -658,6 +658,65 @@ button.carousel-dot[disabled].active { background-color: var(--link-color); } +/* DONUT METER */ + +meter.donut { + appearance: none; +} + +meter.donut:is( + ::-webkit-progress-inner-element, + ::-webkit-progress-bar, + ::-webkit-progress-value, + ::-webkit-meter-bar, + ::-webkit-meter-optimum-value, + ::-webkit-meter-suboptimum-value, + ::-webkit-meter-even-less-good-value + ) { + display: none; +} + +meter.donut:is(::-moz-progress-bar, ::-moz-meter-bar) { + background: transparent; +} + +meter.donut { + position: relative; + width: 24px; + height: 24px; + border-radius: 50%; + --fill: calc(var(--percentage) * 1%); + --color: var(--link-color); + --middle-circle: radial-gradient( + circle at 50% 50%, + var(--bg-faded-color) 10px, + transparent 10px + ); + background-image: var(--middle-circle), + conic-gradient(var(--color) var(--fill), var(--bg-faded-blur-color) 0); +} +meter.donut.warning { + --color: var(--orange-color); +} +meter.donut.danger { + --color: var(--red-color); +} +meter.donut.explode { + background-image: none; +} +meter.donut:is(.warning, .danger, .explode):after { + content: attr(data-left); + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + font-size: 12px; + color: var(--text-insignificant-color); +} +meter.donut:is(.danger, .explode):after { + color: var(--red-color); +} + @media (min-width: 40em) { html, body { diff --git a/src/components/compose.jsx b/src/components/compose.jsx index b14ba6ae..64b0789d 100644 --- a/src/components/compose.jsx +++ b/src/components/compose.jsx @@ -4,6 +4,7 @@ import '@github/text-expander-element'; import { useEffect, useMemo, useRef, useState } from 'preact/hooks'; import stringLength from 'string-length'; +import urlRegex from '../data/url-regex'; import emojifyText from '../utils/emojify-text'; import openCompose from '../utils/open-compose'; import store from '../utils/store'; @@ -380,6 +381,20 @@ function Compose({ }); }, []); + const [charCount, setCharCount] = useState( + textareaRef.current?.value?.length + + spoilerTextRef.current?.value?.length || 0, + ); + const leftChars = maxCharacters - charCount; + const getCharCount = () => { + const { value } = textareaRef.current; + const { value: spoilerText } = spoilerTextRef.current; + return stringLength(countableText(value)) + stringLength(spoilerText); + }; + const updateCharCount = () => { + setCharCount(getCharCount()); + }; + return (
@@ -543,6 +558,7 @@ function Compose({ sensitive = sensitive === 'on'; // checkboxes return "on" if checked // Validation + /* Let the backend validate this if (stringLength(status) > maxCharacters) { alert(`Status is too long! Max characters: ${maxCharacters}`); return; @@ -556,6 +572,7 @@ function Compose({ ); return; } + */ if (poll) { if (poll.options.length < 2) { alert('Poll must have at least 2 options'); @@ -664,6 +681,9 @@ function Compose({ opacity: sensitive ? 1 : 0, pointerEvents: sensitive ? 'auto' : 'none', }} + onInput={() => { + updateCharCount(); + }} />