Prettified Code!

This commit is contained in:
gabek
2022-11-17 18:32:58 +00:00
committed by GitHub Action
parent 17bf41461c
commit ec42aedb73

View File

@@ -6,395 +6,395 @@ import { EmojiButton } from '/js/web_modules/@joeattardi/emoji-button.js';
import ContentEditable, { replaceCaret } from './content-editable.js'; import ContentEditable, { replaceCaret } from './content-editable.js';
import { import {
generatePlaceholderText, generatePlaceholderText,
getCaretPosition, getCaretPosition,
convertToText, convertToText,
convertOnPaste, convertOnPaste,
createEmojiMarkup, createEmojiMarkup,
trimNbsp, trimNbsp,
emojify, emojify,
} from '../../utils/chat.js'; } from '../../utils/chat.js';
import { import {
getLocalStorage, getLocalStorage,
setLocalStorage, setLocalStorage,
classNames, classNames,
} from '../../utils/helpers.js'; } from '../../utils/helpers.js';
import { import {
URL_CUSTOM_EMOJIS, URL_CUSTOM_EMOJIS,
KEY_CHAT_FIRST_MESSAGE_SENT, KEY_CHAT_FIRST_MESSAGE_SENT,
CHAT_CHAR_COUNT_BUFFER, CHAT_CHAR_COUNT_BUFFER,
CHAT_OK_KEYCODES, CHAT_OK_KEYCODES,
CHAT_KEY_MODIFIERS, CHAT_KEY_MODIFIERS,
} from '../../utils/constants.js'; } from '../../utils/constants.js';
export default class ChatInput extends Component { export default class ChatInput extends Component {
constructor(props, context) { constructor(props, context) {
super(props, context); super(props, context);
this.formMessageInput = createRef(); this.formMessageInput = createRef();
this.emojiPickerButton = createRef(); this.emojiPickerButton = createRef();
this.messageCharCount = 0; this.messageCharCount = 0;
this.prepNewLine = false; this.prepNewLine = false;
this.modifierKeyPressed = false; // control/meta/shift/alt this.modifierKeyPressed = false; // control/meta/shift/alt
this.state = { this.state = {
inputHTML: '', inputHTML: '',
inputCharsLeft: props.inputMaxBytes, inputCharsLeft: props.inputMaxBytes,
hasSentFirstChatMessage: getLocalStorage(KEY_CHAT_FIRST_MESSAGE_SENT), hasSentFirstChatMessage: getLocalStorage(KEY_CHAT_FIRST_MESSAGE_SENT),
emojiPicker: null, emojiPicker: null,
emojiList: null, emojiList: null,
emojiNames: null, emojiNames: null,
}; };
this.handleEmojiButtonClick = this.handleEmojiButtonClick.bind(this); this.handleEmojiButtonClick = this.handleEmojiButtonClick.bind(this);
this.handleEmojiSelected = this.handleEmojiSelected.bind(this); this.handleEmojiSelected = this.handleEmojiSelected.bind(this);
this.getCustomEmojis = this.getCustomEmojis.bind(this); this.getCustomEmojis = this.getCustomEmojis.bind(this);
this.handleMessageInputKeydown = this.handleMessageInputKeydown.bind(this); this.handleMessageInputKeydown = this.handleMessageInputKeydown.bind(this);
this.handleMessageInputKeyup = this.handleMessageInputKeyup.bind(this); this.handleMessageInputKeyup = this.handleMessageInputKeyup.bind(this);
this.handleMessageInputBlur = this.handleMessageInputBlur.bind(this); this.handleMessageInputBlur = this.handleMessageInputBlur.bind(this);
this.handleSubmitChatButton = this.handleSubmitChatButton.bind(this); this.handleSubmitChatButton = this.handleSubmitChatButton.bind(this);
this.handlePaste = this.handlePaste.bind(this); this.handlePaste = this.handlePaste.bind(this);
this.handleContentEditableChange = this.handleContentEditableChange =
this.handleContentEditableChange.bind(this); this.handleContentEditableChange.bind(this);
} }
componentDidMount() { componentDidMount() {
this.getCustomEmojis(); this.getCustomEmojis();
} }
getCustomEmojis() { getCustomEmojis() {
fetch(URL_CUSTOM_EMOJIS) fetch(URL_CUSTOM_EMOJIS)
.then((response) => { .then((response) => {
if (!response.ok) { if (!response.ok) {
throw new Error(`Network response was not ok ${response.ok}`); throw new Error(`Network response was not ok ${response.ok}`);
} }
return response.json(); return response.json();
}) })
.then((json) => { .then((json) => {
const emojiList = json; const emojiList = json;
const emojiNames = emojiList.map((emoji) => emoji.name); const emojiNames = emojiList.map((emoji) => emoji.name);
const emojiPicker = new EmojiButton({ const emojiPicker = new EmojiButton({
zIndex: 100, zIndex: 100,
theme: 'owncast', // see chat.css theme: 'owncast', // see chat.css
custom: json, custom: json,
initialCategory: 'custom', initialCategory: 'custom',
showPreview: false, showPreview: false,
autoHide: false, autoHide: false,
autoFocusSearch: false, autoFocusSearch: false,
showAnimation: false, showAnimation: false,
emojiSize: '24px', emojiSize: '24px',
position: 'right-start', position: 'right-start',
strategy: 'absolute', strategy: 'absolute',
}); });
emojiPicker.on('emoji', (emoji) => { emojiPicker.on('emoji', (emoji) => {
this.handleEmojiSelected(emoji); this.handleEmojiSelected(emoji);
}); });
emojiPicker.on('hidden', () => { emojiPicker.on('hidden', () => {
this.formMessageInput.current.focus(); this.formMessageInput.current.focus();
replaceCaret(this.formMessageInput.current); replaceCaret(this.formMessageInput.current);
}); });
this.setState({ emojiNames, emojiList, emojiPicker }); this.setState({ emojiNames, emojiList, emojiPicker });
}) })
.catch((error) => { .catch((error) => {
// this.handleNetworkingError(`Emoji Fetch: ${error}`); // this.handleNetworkingError(`Emoji Fetch: ${error}`);
}); });
} }
handleEmojiButtonClick() { handleEmojiButtonClick() {
const { emojiPicker } = this.state; const { emojiPicker } = this.state;
if (emojiPicker) { if (emojiPicker) {
emojiPicker.togglePicker(this.emojiPickerButton.current); emojiPicker.togglePicker(this.emojiPickerButton.current);
} }
} }
handleEmojiSelected(emoji) { handleEmojiSelected(emoji) {
const { inputHTML, inputCharsLeft } = this.state; const { inputHTML, inputCharsLeft } = this.state;
// if we're already at char limit, don't do anything // if we're already at char limit, don't do anything
if (inputCharsLeft < 0) { if (inputCharsLeft < 0) {
return; return;
} }
let content = ''; let content = '';
if (emoji.url) { if (emoji.url) {
content = createEmojiMarkup(emoji, false); content = createEmojiMarkup(emoji, false);
} else { } else {
content = emoji.emoji; content = emoji.emoji;
} }
const position = getCaretPosition(this.formMessageInput.current); const position = getCaretPosition(this.formMessageInput.current);
const newHTML = const newHTML =
inputHTML.substring(0, position) + inputHTML.substring(0, position) +
content + content +
inputHTML.substring(position); inputHTML.substring(position);
const charsLeft = this.calculateCurrentBytesLeft(newHTML); const charsLeft = this.calculateCurrentBytesLeft(newHTML);
this.setState({ this.setState({
inputHTML: newHTML, inputHTML: newHTML,
inputCharsLeft: charsLeft, inputCharsLeft: charsLeft,
}); });
// a hacky way add focus back into input field // a hacky way add focus back into input field
setTimeout(() => { setTimeout(() => {
const input = this.formMessageInput.current; const input = this.formMessageInput.current;
input.focus(); input.focus();
replaceCaret(input); replaceCaret(input);
}, 100); }, 100);
} }
// autocomplete text from the given "list". "token" marks the start of word lookup. // autocomplete text from the given "list". "token" marks the start of word lookup.
autoComplete(token, list) { autoComplete(token, list) {
const { inputHTML } = this.state; const { inputHTML } = this.state;
const position = getCaretPosition(this.formMessageInput.current); const position = getCaretPosition(this.formMessageInput.current);
const at = inputHTML.lastIndexOf(token, position - 1); const at = inputHTML.lastIndexOf(token, position - 1);
if (at === -1) { if (at === -1) {
return false; return false;
} }
let partial = inputHTML.substring(at + 1, position).trim(); let partial = inputHTML.substring(at + 1, position).trim();
if (this.partial === undefined) { if (this.partial === undefined) {
this.partial = []; this.partial = [];
} }
if (partial === this.suggestion) { if (partial === this.suggestion) {
partial = this.partial[token]; partial = this.partial[token];
} else { } else {
this.partial[token] = partial; this.partial[token] = partial;
} }
const possibilities = list.filter(function (item) { const possibilities = list.filter(function (item) {
return item.toLowerCase().startsWith(partial.toLowerCase()); return item.toLowerCase().startsWith(partial.toLowerCase());
}); });
if (this.completionIndex === undefined) { if (this.completionIndex === undefined) {
this.completionIndex = []; this.completionIndex = [];
} }
if ( if (
this.completionIndex[token] === undefined || this.completionIndex[token] === undefined ||
++this.completionIndex[token] >= possibilities.length ++this.completionIndex[token] >= possibilities.length
) { ) {
this.completionIndex[token] = 0; this.completionIndex[token] = 0;
} }
if (possibilities.length > 0) { if (possibilities.length > 0) {
this.suggestion = possibilities[this.completionIndex[token]]; this.suggestion = possibilities[this.completionIndex[token]];
const newHTML = const newHTML =
inputHTML.substring(0, at + 1) + inputHTML.substring(0, at + 1) +
this.suggestion + this.suggestion +
' ' + ' ' +
inputHTML.substring(position); inputHTML.substring(position);
this.setState({ this.setState({
inputHTML: newHTML, inputHTML: newHTML,
inputCharsLeft: this.calculateCurrentBytesLeft(newHTML), inputCharsLeft: this.calculateCurrentBytesLeft(newHTML),
}); });
} }
return true; return true;
} }
// replace :emoji: with the emoji <img> // replace :emoji: with the emoji <img>
injectEmoji() { injectEmoji() {
const { inputHTML, emojiList } = this.state; const { inputHTML, emojiList } = this.state;
const textValue = convertToText(inputHTML); const textValue = convertToText(inputHTML);
const processedHTML = emojify(inputHTML, emojiList); const processedHTML = emojify(inputHTML, emojiList);
if (textValue != convertToText(processedHTML)) { if (textValue != convertToText(processedHTML)) {
this.setState({ this.setState({
inputHTML: processedHTML, inputHTML: processedHTML,
}); });
return true; return true;
} }
return false; return false;
} }
handleMessageInputKeydown(event) { handleMessageInputKeydown(event) {
const key = event && event.key; const key = event && event.key;
if (key === 'Enter') { if (key === 'Enter') {
if (!this.prepNewLine) { if (!this.prepNewLine) {
this.sendMessage(); this.sendMessage();
event.preventDefault(); event.preventDefault();
this.prepNewLine = false; this.prepNewLine = false;
return; return;
} }
} }
// allow key presses such as command/shift/meta, etc even when message length is full later. // allow key presses such as command/shift/meta, etc even when message length is full later.
if (CHAT_KEY_MODIFIERS.includes(key)) { if (CHAT_KEY_MODIFIERS.includes(key)) {
this.modifierKeyPressed = true; this.modifierKeyPressed = true;
} }
if (key === 'Control' || key === 'Shift') { if (key === 'Control' || key === 'Shift') {
this.prepNewLine = true; this.prepNewLine = true;
} }
if (key === 'Tab') { if (key === 'Tab') {
const { chatUserNames } = this.props; const { chatUserNames } = this.props;
const { emojiNames } = this.state; const { emojiNames } = this.state;
if (this.autoComplete('@', chatUserNames)) { if (this.autoComplete('@', chatUserNames)) {
event.preventDefault(); event.preventDefault();
} }
if (this.autoComplete(':', emojiNames)) { if (this.autoComplete(':', emojiNames)) {
event.preventDefault(); event.preventDefault();
} }
} }
// if new input pushes the potential chars over, don't do anything // if new input pushes the potential chars over, don't do anything
const formField = this.formMessageInput.current; const formField = this.formMessageInput.current;
const tempCharsLeft = this.calculateCurrentBytesLeft(formField.innerHTML); const tempCharsLeft = this.calculateCurrentBytesLeft(formField.innerHTML);
if (tempCharsLeft <= 0 && !CHAT_OK_KEYCODES.includes(key)) { if (tempCharsLeft <= 0 && !CHAT_OK_KEYCODES.includes(key)) {
if (!this.modifierKeyPressed) { if (!this.modifierKeyPressed) {
event.preventDefault(); // prevent typing more event.preventDefault(); // prevent typing more
} }
return; return;
} }
} }
handleMessageInputKeyup(event) { handleMessageInputKeyup(event) {
const { key } = event; const { key } = event;
if (key === 'Control' || key === 'Shift') { if (key === 'Control' || key === 'Shift') {
this.prepNewLine = false; this.prepNewLine = false;
} }
if (CHAT_KEY_MODIFIERS.includes(key)) { if (CHAT_KEY_MODIFIERS.includes(key)) {
this.modifierKeyPressed = false; this.modifierKeyPressed = false;
} }
if (key === ':' || key === ';') { if (key === ':' || key === ';') {
this.injectEmoji(); this.injectEmoji();
} }
} }
handleMessageInputBlur() { handleMessageInputBlur() {
this.prepNewLine = false; this.prepNewLine = false;
this.modifierKeyPressed = false; this.modifierKeyPressed = false;
} }
handlePaste(event) { handlePaste(event) {
// don't allow paste if too much text already // don't allow paste if too much text already
if (this.state.inputCharsLeft < 0) { if (this.state.inputCharsLeft < 0) {
event.preventDefault(); event.preventDefault();
return; return;
} }
convertOnPaste(event, this.state.emojiList); convertOnPaste(event, this.state.emojiList);
this.handleMessageInputKeydown(event); this.handleMessageInputKeydown(event);
} }
handleSubmitChatButton(event) { handleSubmitChatButton(event) {
event.preventDefault(); event.preventDefault();
this.sendMessage(); this.sendMessage();
} }
sendMessage() { sendMessage() {
const { handleSendMessage, inputMaxBytes } = this.props; const { handleSendMessage, inputMaxBytes } = this.props;
const { hasSentFirstChatMessage, inputHTML, inputCharsLeft } = this.state; const { hasSentFirstChatMessage, inputHTML, inputCharsLeft } = this.state;
if (inputCharsLeft < 0) { if (inputCharsLeft < 0) {
return; return;
} }
const message = convertToText(inputHTML); const message = convertToText(inputHTML);
const newStates = { const newStates = {
inputHTML: '', inputHTML: '',
inputCharsLeft: inputMaxBytes, inputCharsLeft: inputMaxBytes,
}; };
handleSendMessage(message); handleSendMessage(message);
if (!hasSentFirstChatMessage) { if (!hasSentFirstChatMessage) {
newStates.hasSentFirstChatMessage = true; newStates.hasSentFirstChatMessage = true;
setLocalStorage(KEY_CHAT_FIRST_MESSAGE_SENT, true); setLocalStorage(KEY_CHAT_FIRST_MESSAGE_SENT, true);
} }
// clear things out. // clear things out.
this.setState(newStates); this.setState(newStates);
} }
handleContentEditableChange(event) { handleContentEditableChange(event) {
const value = event.target.value; const value = event.target.value;
this.setState({ this.setState({
inputHTML: value, inputHTML: value,
inputCharsLeft: this.calculateCurrentBytesLeft(value), inputCharsLeft: this.calculateCurrentBytesLeft(value),
}); });
} }
calculateCurrentBytesLeft(inputContent) { calculateCurrentBytesLeft(inputContent) {
const { inputMaxBytes } = this.props; const { inputMaxBytes } = this.props;
const curBytes = new Blob([trimNbsp(inputContent)]).size; const curBytes = new Blob([trimNbsp(inputContent)]).size;
return inputMaxBytes - curBytes; return inputMaxBytes - curBytes;
} }
render(props, state) { render(props, state) {
const { hasSentFirstChatMessage, inputCharsLeft, inputHTML, emojiPicker } = const { hasSentFirstChatMessage, inputCharsLeft, inputHTML, emojiPicker } =
state; state;
const { inputEnabled, inputMaxBytes } = props; const { inputEnabled, inputMaxBytes } = props;
const emojiButtonStyle = { const emojiButtonStyle = {
display: emojiPicker && inputCharsLeft > 0 ? 'block' : 'none', display: emojiPicker && inputCharsLeft > 0 ? 'block' : 'none',
}; };
const extraClasses = classNames({ const extraClasses = classNames({
'display-count': inputCharsLeft <= CHAT_CHAR_COUNT_BUFFER, 'display-count': inputCharsLeft <= CHAT_CHAR_COUNT_BUFFER,
}); });
const placeholderText = generatePlaceholderText( const placeholderText = generatePlaceholderText(
inputEnabled, inputEnabled,
hasSentFirstChatMessage hasSentFirstChatMessage
); );
return html` return html`
<div <div
id="message-input-container" id="message-input-container"
class="relative shadow-md bg-gray-900 border-t border-gray-700 border-solid p-4 z-20 ${extraClasses}" class="relative shadow-md bg-gray-900 border-t border-gray-700 border-solid p-4 z-20 ${extraClasses}"
> >
<div <div
id="message-input-wrap" id="message-input-wrap"
class="flex flex-row justify-end appearance-none w-full bg-gray-200 border border-black-500 rounded py-2 px-2 pr-20 my-2 overflow-auto" class="flex flex-row justify-end appearance-none w-full bg-gray-200 border border-black-500 rounded py-2 px-2 pr-20 my-2 overflow-auto"
> >
<${ContentEditable} <${ContentEditable}
id="message-input" id="message-input"
aria-role="textbox" aria-role="textbox"
class="appearance-none block w-full bg-transparent text-sm text-gray-700 h-full focus:outline-none" class="appearance-none block w-full bg-transparent text-sm text-gray-700 h-full focus:outline-none"
aria-placeholder=${placeholderText} aria-placeholder=${placeholderText}
innerRef=${this.formMessageInput} innerRef=${this.formMessageInput}
html=${inputHTML} html=${inputHTML}
disabled=${!inputEnabled} disabled=${!inputEnabled}
onChange=${this.handleContentEditableChange} onChange=${this.handleContentEditableChange}
onKeyDown=${this.handleMessageInputKeydown} onKeyDown=${this.handleMessageInputKeydown}
onKeyUp=${this.handleMessageInputKeyup} onKeyUp=${this.handleMessageInputKeyup}
onBlur=${this.handleMessageInputBlur} onBlur=${this.handleMessageInputBlur}
onPaste=${this.handlePaste} onPaste=${this.handlePaste}
/> />
</div> </div>
<div <div
id="message-form-actions" id="message-form-actions"
class="absolute flex flex-col justify-end items-end mr-4" class="absolute flex flex-col justify-end items-end mr-4"
> >
<span class="flex flex-row justify-center"> <span class="flex flex-row justify-center">
<button <button
ref=${this.emojiPickerButton} ref=${this.emojiPickerButton}
id="emoji-button" id="emoji-button"
class="text-3xl leading-3 cursor-pointer text-purple-600" class="text-3xl leading-3 cursor-pointer text-purple-600"
type="button" type="button"
style=${emojiButtonStyle} style=${emojiButtonStyle}
onclick=${this.handleEmojiButtonClick} onclick=${this.handleEmojiButtonClick}
aria-label="Select an emoji" aria-label="Select an emoji"
disabled=${!inputEnabled} disabled=${!inputEnabled}
> >
<img src="../../../img/smiley.png" /> <img src="../../../img/smiley.png" />
</button> </button>
<button <button
id="send-message-button" id="send-message-button"
class="text-sm text-white rounded bg-gray-600 hidden p-1 ml-1 -mr-2" class="text-sm text-white rounded bg-gray-600 hidden p-1 ml-1 -mr-2"
type="button" type="button"
onclick=${this.handleSubmitChatButton} onclick=${this.handleSubmitChatButton}
disabled=${inputHTML === '' || inputCharsLeft < 0} disabled=${inputHTML === '' || inputCharsLeft < 0}
aria-label="Send message" aria-label="Send message"
> >
Send Send
</button> </button>
</span> </span>
<span id="message-form-warning" class="text-red-600 text-xs" <span id="message-form-warning" class="text-red-600 text-xs"
>${inputCharsLeft} bytes</span >${inputCharsLeft} bytes</span
> >
</div> </div>
</div> </div>
`; `;
} }
} }