mirror of
https://github.com/cheeaun/phanpy.git
synced 2024-11-24 10:15:37 +03:00
New: char count for Compose field
Uses pre-compiled regex for perf
This commit is contained in:
parent
a2e55eca90
commit
13a347ce37
6 changed files with 226 additions and 0 deletions
65
package-lock.json
generated
65
package-lock.json
generated
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
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);
|
||||
}
|
||||
|
||||
/* 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 {
|
||||
|
|
|
@ -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 (
|
||||
<div id="compose-container" class={standalone ? 'standalone' : ''}>
|
||||
<div class="compose-top">
|
||||
|
@ -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();
|
||||
}}
|
||||
/>
|
||||
<label
|
||||
class="toolbar-button"
|
||||
|
@ -738,6 +758,7 @@ function Compose({
|
|||
e.target.style.height = value
|
||||
? scrollHeight + offset + 'px'
|
||||
: null;
|
||||
updateCharCount();
|
||||
}}
|
||||
style={{
|
||||
maxHeight: `${maxCharacters / 50}em`,
|
||||
|
@ -848,6 +869,27 @@ function Compose({
|
|||
</button>{' '}
|
||||
<div class="spacer" />
|
||||
{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'}>
|
||||
{replyToStatus ? 'Reply' : editStatus ? 'Update' : 'Post'}
|
||||
</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;
|
||||
|
|
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