mirror of
https://github.com/cheeaun/phanpy.git
synced 2024-11-25 10:45:41 +03:00
Basic text highlighting for composer
This will probably be very buggy
This commit is contained in:
parent
51ddf9b030
commit
1882338078
3 changed files with 166 additions and 2 deletions
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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;
|
||||||
|
|
Loading…
Reference in a new issue