Basic text highlighting for composer

This will probably be very buggy
This commit is contained in:
Lim Chee Aun 2023-11-08 23:16:16 +08:00
parent 51ddf9b030
commit 1882338078
3 changed files with 166 additions and 2 deletions

View file

@ -619,3 +619,86 @@
#custom-emojis-sheet .custom-emojis-list button:is(:hover, :focus) img { #custom-emojis-sheet .custom-emojis-list button:is(:hover, :focus) img {
transform: scale(1.5); transform: scale(1.5);
} }
.compose-field-container {
display: grid !important;
&.debug {
grid-template-columns: 1fr 1fr;
}
> * {
grid-area: 1 / 1 / 2 / 2;
}
.compose-highlight {
user-drag: none;
user-select: none;
pointer-events: none;
touch-action: none;
padding: 8px;
color: transparent;
background-color: transparent;
border: 2px solid transparent;
line-height: 1.4;
overflow: auto;
unicode-bidi: plaintext;
-webkit-rtl-ordering: logical;
rtl-ordering: logical;
overflow-wrap: break-word;
white-space: pre-wrap;
min-height: 5em;
max-height: 50vh;
/* Follow textarea styles */
@media (min-width: 40em) {
max-height: 65vh;
}
@media (width < 30em) {
margin-inline: calc(-1 * var(--form-padding-inline));
width: 100vw !important;
max-width: 100vw;
border: 0;
}
mark {
color: inherit;
}
.compose-highlight-url,
.compose-highlight-hashtag {
background-color: transparent;
text-decoration: underline;
text-decoration-color: var(--link-faded-color);
text-decoration-thickness: 2px;
text-underline-offset: 2px;
}
.compose-highlight-mention,
.compose-highlight-emoji-shortcode,
.compose-highlight-exceeded {
mix-blend-mode: multiply;
border-radius: 4px;
box-shadow: 0 0 0 1px;
}
.compose-highlight-mention {
background-color: var(--orange-light-bg-color);
box-shadow-color: var(--orange-light-bg-color);
}
.compose-highlight-emoji-shortcode {
background-color: var(--bg-faded-color);
box-shadow-color: var(--bg-faded-color);
}
.compose-highlight-exceeded {
background-color: var(--red-bg-color);
box-shadow-color: var(--red-bg-color);
}
@media (prefers-color-scheme: dark) {
.compose-highlight-mention,
.compose-highlight-emoji-shortcode,
.compose-highlight-exceeded {
mix-blend-mode: screen;
}
}
}
}

View file

@ -104,6 +104,55 @@ function countableText(inputText) {
.replace(usernameRegex, '$1@$3'); .replace(usernameRegex, '$1@$3');
} }
// https://github.com/mastodon/mastodon/blob/c03bd2a238741a012aa4b98dc4902d6cf948ab63/app/models/account.rb#L69
const USERNAME_RE = /[a-z0-9_]+([a-z0-9_.-]+[a-z0-9_]+)?/i;
const MENTION_RE = new RegExp(
`(?<![=\\/\\w])@((${USERNAME_RE.source})(?:@[\\w.-]+[\\w]+)?)`,
'ig',
);
// AI-generated, all other regexes are too complicated
const HASHTAG_RE = new RegExp(
`(?<![=\\/\\w])#([a-z0-9_]+([a-z0-9_.-]+[a-z0-9_]+)?)(?![\\/\\w])`,
'ig',
);
// https://github.com/mastodon/mastodon/blob/23e32a4b3031d1da8b911e0145d61b4dd47c4f96/app/models/custom_emoji.rb#L31
const SHORTCODE_RE_FRAGMENT = '[a-zA-Z0-9_]{2,}';
const SCAN_RE = new RegExp(
`(?<=[^A-Za-z0-9_:\\n]|^):(${SHORTCODE_RE_FRAGMENT}):(?=[^A-Za-z0-9_:]|$)`,
'g',
);
function highlightText(text, { maxCharacters = Infinity }) {
// Accept text string, return formatted HTML string
let html = text;
// Exceeded characters limit
const { composerCharacterCount } = states;
let leftoverHTML = '';
if (composerCharacterCount > maxCharacters) {
const leftoverCount = composerCharacterCount - maxCharacters;
leftoverHTML = html.slice(-leftoverCount);
html = html.slice(0, -leftoverCount);
// Highlight exceeded characters
leftoverHTML = leftoverHTML.replace(
new RegExp(`(.{${leftoverCount}})$`),
'<mark class="compose-highlight-exceeded">$1</mark>',
);
}
html = html
.replace(urlRegexObj, '$2<mark class="compose-highlight-url">$3</mark>') // URLs
.replace(MENTION_RE, '<mark class="compose-highlight-mention">$&</mark>') // Mentions
.replace(HASHTAG_RE, '<mark class="compose-highlight-hashtag">#$1</mark>') // Hashtags
.replace(
SCAN_RE,
'<mark class="compose-highlight-emoji-shortcode">$&</mark>',
); // Emoji shortcodes
return html + leftoverHTML;
}
function Compose({ function Compose({
onClose, onClose,
replyToStatus, replyToStatus,
@ -1387,6 +1436,11 @@ const Textarea = forwardRef((props, ref) => {
handleCommited = (e) => { handleCommited = (e) => {
const { input } = e.detail; const { input } = e.detail;
setText(input.value); setText(input.value);
// fire input event
if (ref.current) {
const event = new Event('input', { bubbles: true });
ref.current.dispatchEvent(event);
}
}; };
textExpanderRef.current.addEventListener( textExpanderRef.current.addEventListener(
@ -1413,8 +1467,14 @@ const Textarea = forwardRef((props, ref) => {
}; };
}, []); }, []);
const composeHighlightRef = useRef();
return ( return (
<text-expander ref={textExpanderRef} keys="@ # :"> <text-expander
ref={textExpanderRef}
keys="@ # :"
class="compose-field-container"
>
<textarea <textarea
class="compose-field" class="compose-field"
autoCapitalize="sentences" autoCapitalize="sentences"
@ -1466,15 +1526,30 @@ const Textarea = forwardRef((props, ref) => {
}} }}
onInput={(e) => { onInput={(e) => {
const { target } = e; const { target } = e;
setText(target.value); const text = target.value;
setText(text);
autoResizeTextarea(target); autoResizeTextarea(target);
props.onInput?.(e); props.onInput?.(e);
composeHighlightRef.current.innerHTML =
highlightText(text, {
maxCharacters,
}) + '\n';
// Newline to prevent multiple line breaks at the end from being collapsed, no idea why
}} }}
style={{ style={{
width: '100%', width: '100%',
height: '4em', height: '4em',
// '--text-weight': (1 + charCount / 140).toFixed(1) || 1, // '--text-weight': (1 + charCount / 140).toFixed(1) || 1,
}} }}
onScroll={(e) => {
const { scrollTop } = e.target;
composeHighlightRef.current.scrollTop = scrollTop;
}}
/>
<div
ref={composeHighlightRef}
class="compose-highlight"
aria-hidden="true"
/> />
</text-expander> </text-expander>
); );

View file

@ -18,7 +18,13 @@
--purple-color: blueviolet; --purple-color: blueviolet;
--green-color: darkgreen; --green-color: darkgreen;
--orange-color: darkorange; --orange-color: darkorange;
--orange-light-bg-color: color-mix(
in srgb,
var(--orange-color) 20%,
transparent
);
--red-color: orangered; --red-color: orangered;
--red-bg-color: color-mix(in lch, var(--red-color) 40%, transparent);
--bg-color: #fff; --bg-color: #fff;
--bg-faded-color: #f0f2f5; --bg-faded-color: #f0f2f5;
--bg-blur-color: #fff9; --bg-blur-color: #fff9;