New: char count for Compose field
Uses pre-compiled regex for perf
This commit is contained in:
parent
a2e55eca90
commit
13a347ce37
65
package-lock.json
generated
65
package-lock.json
generated
|
@ -29,6 +29,7 @@
|
||||||
"autoprefixer": "~10.4.13",
|
"autoprefixer": "~10.4.13",
|
||||||
"postcss": "~8.4.20",
|
"postcss": "~8.4.20",
|
||||||
"postcss-dark-theme-class": "~0.7.3",
|
"postcss-dark-theme-class": "~0.7.3",
|
||||||
|
"twitter-text": "~3.1.0",
|
||||||
"vite": "~4.0.3",
|
"vite": "~4.0.3",
|
||||||
"vite-plugin-pwa": "~0.14.0",
|
"vite-plugin-pwa": "~0.14.0",
|
||||||
"workbox-cacheable-response": "~6.5.4",
|
"workbox-cacheable-response": "~6.5.4",
|
||||||
|
@ -2930,6 +2931,14 @@
|
||||||
"integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==",
|
"integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==",
|
||||||
"dev": true
|
"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": {
|
"node_modules/core-js-compat": {
|
||||||
"version": "3.26.1",
|
"version": "3.26.1",
|
||||||
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.26.1.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz",
|
||||||
"integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA=="
|
"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": {
|
"node_modules/type-fest": {
|
||||||
"version": "0.16.0",
|
"version": "0.16.0",
|
||||||
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.16.0.tgz",
|
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.16.0.tgz",
|
||||||
|
@ -7752,6 +7785,12 @@
|
||||||
"integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==",
|
"integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==",
|
||||||
"dev": true
|
"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": {
|
"core-js-compat": {
|
||||||
"version": "3.26.1",
|
"version": "3.26.1",
|
||||||
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.26.1.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz",
|
||||||
"integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA=="
|
"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": {
|
"type-fest": {
|
||||||
"version": "0.16.0",
|
"version": "0.16.0",
|
||||||
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.16.0.tgz",
|
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.16.0.tgz",
|
||||||
|
|
|
@ -31,6 +31,7 @@
|
||||||
"autoprefixer": "~10.4.13",
|
"autoprefixer": "~10.4.13",
|
||||||
"postcss": "~8.4.20",
|
"postcss": "~8.4.20",
|
||||||
"postcss-dark-theme-class": "~0.7.3",
|
"postcss-dark-theme-class": "~0.7.3",
|
||||||
|
"twitter-text": "~3.1.0",
|
||||||
"vite": "~4.0.3",
|
"vite": "~4.0.3",
|
||||||
"vite-plugin-pwa": "~0.14.0",
|
"vite-plugin-pwa": "~0.14.0",
|
||||||
"workbox-cacheable-response": "~6.5.4",
|
"workbox-cacheable-response": "~6.5.4",
|
||||||
|
|
48
scripts/extract-url.js
Normal file
48
scripts/extract-url.js
Normal file
|
@ -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}`);
|
||||||
|
},
|
||||||
|
);
|
59
src/app.css
59
src/app.css
|
@ -658,6 +658,65 @@ button.carousel-dot[disabled].active {
|
||||||
background-color: var(--link-color);
|
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) {
|
@media (min-width: 40em) {
|
||||||
html,
|
html,
|
||||||
body {
|
body {
|
||||||
|
|
|
@ -4,6 +4,7 @@ import '@github/text-expander-element';
|
||||||
import { useEffect, useMemo, useRef, useState } from 'preact/hooks';
|
import { useEffect, useMemo, useRef, useState } from 'preact/hooks';
|
||||||
import stringLength from 'string-length';
|
import stringLength from 'string-length';
|
||||||
|
|
||||||
|
import urlRegex from '../data/url-regex';
|
||||||
import emojifyText from '../utils/emojify-text';
|
import emojifyText from '../utils/emojify-text';
|
||||||
import openCompose from '../utils/open-compose';
|
import openCompose from '../utils/open-compose';
|
||||||
import store from '../utils/store';
|
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 (
|
return (
|
||||||
<div id="compose-container" class={standalone ? 'standalone' : ''}>
|
<div id="compose-container" class={standalone ? 'standalone' : ''}>
|
||||||
<div class="compose-top">
|
<div class="compose-top">
|
||||||
|
@ -543,6 +558,7 @@ function Compose({
|
||||||
sensitive = sensitive === 'on'; // checkboxes return "on" if checked
|
sensitive = sensitive === 'on'; // checkboxes return "on" if checked
|
||||||
|
|
||||||
// Validation
|
// Validation
|
||||||
|
/* Let the backend validate this
|
||||||
if (stringLength(status) > maxCharacters) {
|
if (stringLength(status) > maxCharacters) {
|
||||||
alert(`Status is too long! Max characters: ${maxCharacters}`);
|
alert(`Status is too long! Max characters: ${maxCharacters}`);
|
||||||
return;
|
return;
|
||||||
|
@ -556,6 +572,7 @@ function Compose({
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
if (poll) {
|
if (poll) {
|
||||||
if (poll.options.length < 2) {
|
if (poll.options.length < 2) {
|
||||||
alert('Poll must have at least 2 options');
|
alert('Poll must have at least 2 options');
|
||||||
|
@ -664,6 +681,9 @@ function Compose({
|
||||||
opacity: sensitive ? 1 : 0,
|
opacity: sensitive ? 1 : 0,
|
||||||
pointerEvents: sensitive ? 'auto' : 'none',
|
pointerEvents: sensitive ? 'auto' : 'none',
|
||||||
}}
|
}}
|
||||||
|
onInput={() => {
|
||||||
|
updateCharCount();
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<label
|
<label
|
||||||
class="toolbar-button"
|
class="toolbar-button"
|
||||||
|
@ -738,6 +758,7 @@ function Compose({
|
||||||
e.target.style.height = value
|
e.target.style.height = value
|
||||||
? scrollHeight + offset + 'px'
|
? scrollHeight + offset + 'px'
|
||||||
: null;
|
: null;
|
||||||
|
updateCharCount();
|
||||||
}}
|
}}
|
||||||
style={{
|
style={{
|
||||||
maxHeight: `${maxCharacters / 50}em`,
|
maxHeight: `${maxCharacters / 50}em`,
|
||||||
|
@ -848,6 +869,27 @@ function Compose({
|
||||||
</button>{' '}
|
</button>{' '}
|
||||||
<div class="spacer" />
|
<div class="spacer" />
|
||||||
{uiState === 'loading' && <Loader abrupt />}{' '}
|
{uiState === 'loading' && <Loader abrupt />}{' '}
|
||||||
|
{uiState !== 'loading' && charCount > maxCharacters / 2 && (
|
||||||
|
<>
|
||||||
|
<meter
|
||||||
|
class={`donut ${
|
||||||
|
leftChars <= -10
|
||||||
|
? 'explode'
|
||||||
|
: leftChars <= 0
|
||||||
|
? 'danger'
|
||||||
|
: leftChars <= 20
|
||||||
|
? 'warning'
|
||||||
|
: ''
|
||||||
|
}`}
|
||||||
|
value={charCount}
|
||||||
|
max={maxCharacters}
|
||||||
|
data-left={leftChars}
|
||||||
|
style={{
|
||||||
|
'--percentage': (charCount / maxCharacters) * 100,
|
||||||
|
}}
|
||||||
|
/>{' '}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<button type="submit" class="large" disabled={uiState === 'loading'}>
|
<button type="submit" class="large" disabled={uiState === 'loading'}>
|
||||||
{replyToStatus ? 'Reply' : editStatus ? 'Update' : 'Post'}
|
{replyToStatus ? 'Reply' : editStatus ? 'Update' : 'Post'}
|
||||||
</button>
|
</button>
|
||||||
|
@ -1065,4 +1107,14 @@ function encodeHTML(str) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// https://github.com/mastodon/mastodon/blob/c4a429ed47e85a6bbf0d470a41cc2f64cf120c19/app/javascript/mastodon/features/compose/util/counter.js
|
||||||
|
const urlRegexObj = new RegExp(urlRegex.source, urlRegex.flags);
|
||||||
|
const usernameRegex = /(^|[^\/\w])@(([a-z0-9_]+)@[a-z0-9\.\-]+[a-z0-9]+)/gi;
|
||||||
|
const urlPlaceholder = '$2xxxxxxxxxxxxxxxxxxxxxxx';
|
||||||
|
function countableText(inputText) {
|
||||||
|
return inputText
|
||||||
|
.replace(urlRegexObj, urlPlaceholder)
|
||||||
|
.replace(usernameRegex, '$1@$3');
|
||||||
|
}
|
||||||
|
|
||||||
export default Compose;
|
export default Compose;
|
||||||
|
|
1
src/data/url-regex.json
Normal file
1
src/data/url-regex.json
Normal file
File diff suppressed because one or more lines are too long
Loading…
Reference in a new issue