mirror of
https://github.com/owncast/owncast.git
synced 2024-10-10 19:16:02 +00:00
fix(chat): refactor chat input to not use slatejs. Closes #3094
This commit is contained in:
parent
683f73c379
commit
ad814a8802
@ -25,11 +25,17 @@
|
|||||||
background-color: var(--theme-color-components-form-field-background);
|
background-color: var(--theme-color-components-form-field-background);
|
||||||
box-shadow: inset 0px 0px 2px 2px var(--theme-color-palette-3);
|
box-shadow: inset 0px 0px 2px 2px var(--theme-color-palette-3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Size of custom emoji.
|
||||||
|
img {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.maxCharacters {
|
.maxCharacters {
|
||||||
border-style: solid;
|
border-style: solid;
|
||||||
border-width: 1px;
|
border-width: 2px;
|
||||||
border-color: red;
|
border-color: red;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -41,6 +47,8 @@
|
|||||||
border-color: var(--theme-color-components-form-field-border);
|
border-color: var(--theme-color-components-form-field-border);
|
||||||
box-shadow: 0;
|
box-shadow: 0;
|
||||||
transition: box-shadow 50ms ease-in-out;
|
transition: box-shadow 50ms ease-in-out;
|
||||||
|
max-height: 40px; // 2 lines of text
|
||||||
|
min-height: 30px;
|
||||||
&:focus {
|
&:focus {
|
||||||
outline: 1px solid var(--color-owncast-gray-500) !important;
|
outline: 1px solid var(--color-owncast-gray-500) !important;
|
||||||
}
|
}
|
||||||
@ -48,13 +56,20 @@
|
|||||||
margin: 0px;
|
margin: 0px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Placeholder styling
|
||||||
|
:empty:before {
|
||||||
|
content: attr(placeholder);
|
||||||
|
display: block;
|
||||||
|
color: #aaa;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.emojiButton {
|
.emojiButton {
|
||||||
border: none;
|
border: none;
|
||||||
background: none;
|
background: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
padding: 0 .25rem;
|
padding: 0 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sendButton {
|
.sendButton {
|
||||||
|
@ -44,8 +44,7 @@ export default {
|
|||||||
component: `
|
component: `
|
||||||
- This is a element using \`contentEditable\` in order to support rendering emoji images inline.
|
- This is a element using \`contentEditable\` in order to support rendering emoji images inline.
|
||||||
- Emoji button shows emoji picker.
|
- Emoji button shows emoji picker.
|
||||||
- Should show one line by default, but grow to two lines as needed.
|
- Should show one line by default, but grow to two lines as needed.`,
|
||||||
- The Send button should be hidden for desktop layouts and be shown for mobile layouts.`,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -1,16 +1,7 @@
|
|||||||
import { Popover } from 'antd';
|
import { Popover } from 'antd';
|
||||||
import React, { FC, useMemo, useState } from 'react';
|
import React, { FC, useReducer, useRef, useState } from 'react';
|
||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
import { Transforms, createEditor, BaseEditor, Text, Descendant, Editor } from 'slate';
|
import ContentEditable from 'react-contenteditable';
|
||||||
import {
|
|
||||||
Slate,
|
|
||||||
DefaultPlaceholder,
|
|
||||||
Editable,
|
|
||||||
withReact,
|
|
||||||
ReactEditor,
|
|
||||||
useSelected,
|
|
||||||
useFocused,
|
|
||||||
} from 'slate-react';
|
|
||||||
import dynamic from 'next/dynamic';
|
import dynamic from 'next/dynamic';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import WebsocketService from '../../../services/websocket-service';
|
import WebsocketService from '../../../services/websocket-service';
|
||||||
@ -32,102 +23,6 @@ const SmileOutlined = dynamic(() => import('@ant-design/icons/SmileOutlined'), {
|
|||||||
ssr: false,
|
ssr: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
type CustomElement = { type: 'paragraph' | 'span'; children: CustomText[] } | ImageNode;
|
|
||||||
type CustomText = { text: string };
|
|
||||||
|
|
||||||
type EmptyText = {
|
|
||||||
text: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type ImageNode = {
|
|
||||||
type: 'image';
|
|
||||||
alt: string;
|
|
||||||
src: string;
|
|
||||||
name: string;
|
|
||||||
children: EmptyText[];
|
|
||||||
};
|
|
||||||
|
|
||||||
declare module 'slate' {
|
|
||||||
interface CustomTypes {
|
|
||||||
Editor: BaseEditor & ReactEditor;
|
|
||||||
Element: CustomElement;
|
|
||||||
Text: CustomText;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const Image = p => {
|
|
||||||
const { attributes, element, children } = p;
|
|
||||||
|
|
||||||
const selected = useSelected();
|
|
||||||
const focused = useFocused();
|
|
||||||
return (
|
|
||||||
<span {...attributes} contentEditable={false}>
|
|
||||||
<img
|
|
||||||
alt={element.alt}
|
|
||||||
src={element.src}
|
|
||||||
title={element.name}
|
|
||||||
style={{
|
|
||||||
display: 'inline',
|
|
||||||
maxWidth: '50px',
|
|
||||||
maxHeight: '20px',
|
|
||||||
boxShadow: `${selected && focused ? '0 0 0 3px #B4D5FF' : 'none'}`,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{children}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const withImages = editor => {
|
|
||||||
const { isVoid } = editor;
|
|
||||||
|
|
||||||
// eslint-disable-next-line no-param-reassign
|
|
||||||
editor.isVoid = element => (element.type === 'image' ? true : isVoid(element));
|
|
||||||
// eslint-disable-next-line no-param-reassign
|
|
||||||
editor.isInline = element => element.type === 'image';
|
|
||||||
|
|
||||||
return editor;
|
|
||||||
};
|
|
||||||
|
|
||||||
const serialize = node => {
|
|
||||||
if (Text.isText(node)) {
|
|
||||||
const string = node.text;
|
|
||||||
return string;
|
|
||||||
}
|
|
||||||
|
|
||||||
let children;
|
|
||||||
if (node.children.length === 0) {
|
|
||||||
children = [{ text: '' }];
|
|
||||||
} else {
|
|
||||||
children = node.children?.map(n => serialize(n)).join('');
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (node.type) {
|
|
||||||
case 'paragraph':
|
|
||||||
return `<p>${children}</p>`;
|
|
||||||
case 'image':
|
|
||||||
return `<img src="${node.src}" alt="${node.alt}" title="${node.name}" class="emoji"/>`;
|
|
||||||
default:
|
|
||||||
return children;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getCharacterCount = node => {
|
|
||||||
if (Text.isText(node)) {
|
|
||||||
return node.text.length;
|
|
||||||
}
|
|
||||||
if (node.type === 'image') {
|
|
||||||
return 5;
|
|
||||||
}
|
|
||||||
|
|
||||||
let count = 0;
|
|
||||||
node.children.forEach(child => {
|
|
||||||
count += getCharacterCount(child);
|
|
||||||
});
|
|
||||||
|
|
||||||
return count;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ChatTextFieldProps = {
|
export type ChatTextFieldProps = {
|
||||||
defaultText?: string;
|
defaultText?: string;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
@ -136,18 +31,88 @@ export type ChatTextFieldProps = {
|
|||||||
|
|
||||||
const characterLimit = 300;
|
const characterLimit = 300;
|
||||||
|
|
||||||
|
function getCaretPosition(node) {
|
||||||
|
const selection = window.getSelection();
|
||||||
|
|
||||||
|
if (selection.rangeCount === 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const range = selection.getRangeAt(0);
|
||||||
|
const preCaretRange = range.cloneRange();
|
||||||
|
const tempElement = document.createElement('div');
|
||||||
|
|
||||||
|
preCaretRange.selectNodeContents(node);
|
||||||
|
preCaretRange.setEnd(range.endContainer, range.endOffset);
|
||||||
|
tempElement.appendChild(preCaretRange.cloneContents());
|
||||||
|
|
||||||
|
return tempElement.innerHTML.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setCaretPosition(editableDiv, position) {
|
||||||
|
try {
|
||||||
|
const range = document.createRange();
|
||||||
|
const sel = window.getSelection();
|
||||||
|
range.selectNode(editableDiv);
|
||||||
|
range.setStart(editableDiv.childNodes[0], position);
|
||||||
|
range.collapse(true);
|
||||||
|
|
||||||
|
sel.removeAllRanges();
|
||||||
|
sel.addRange(range);
|
||||||
|
} catch (e) {
|
||||||
|
console.debug(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function convertToText(str = '') {
|
||||||
|
// Ensure string.
|
||||||
|
let value = String(str);
|
||||||
|
|
||||||
|
// Convert encoding.
|
||||||
|
value = value.replace(/ /gi, ' ');
|
||||||
|
value = value.replace(/&/gi, '&');
|
||||||
|
|
||||||
|
// Replace `<br>`.
|
||||||
|
value = value.replace(/<br>/gi, '\n');
|
||||||
|
|
||||||
|
// Replace `<div>` (from Chrome).
|
||||||
|
value = value.replace(/<div>/gi, '\n');
|
||||||
|
|
||||||
|
// Replace `<p>` (from IE).
|
||||||
|
value = value.replace(/<p>/gi, '\n');
|
||||||
|
|
||||||
|
// Cleanup the emoji titles.
|
||||||
|
value = value.replace(/\u200C{2}/gi, '');
|
||||||
|
|
||||||
|
// Trim each line.
|
||||||
|
value = value
|
||||||
|
.split('\n')
|
||||||
|
.map((line = '') => line.trim())
|
||||||
|
.join('\n');
|
||||||
|
|
||||||
|
// No more than 2x newline, per "paragraph".
|
||||||
|
value = value.replace(/\n\n+/g, '\n\n');
|
||||||
|
|
||||||
|
// Clean up spaces.
|
||||||
|
value = value.replace(/[ ]+/g, ' ');
|
||||||
|
value = value.trim();
|
||||||
|
|
||||||
|
// Expose string.
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
export const ChatTextField: FC<ChatTextFieldProps> = ({ defaultText, enabled, focusInput }) => {
|
export const ChatTextField: FC<ChatTextFieldProps> = ({ defaultText, enabled, focusInput }) => {
|
||||||
const [showEmojis, setShowEmojis] = useState(false);
|
const [showEmojis, setShowEmojis] = useState(false);
|
||||||
const [characterCount, setCharacterCount] = useState(defaultText?.length);
|
const [characterCount, setCharacterCount] = useState(defaultText?.length);
|
||||||
const websocketService = useRecoilValue<WebsocketService>(websocketServiceAtom);
|
const websocketService = useRecoilValue<WebsocketService>(websocketServiceAtom);
|
||||||
const editor = useMemo(() => withReact(withImages(createEditor())), []);
|
const text = useRef(defaultText || '');
|
||||||
|
const [savedCursorLocation, setSavedCursorLocation] = useState(0);
|
||||||
|
|
||||||
const defaultEditorValue: Descendant[] = [
|
// This is a bit of a hack to force the component to re-render when the text changes.
|
||||||
{
|
// By default when updating a ref the component doesn't re-render.
|
||||||
type: 'paragraph',
|
const [, forceUpdate] = useReducer(x => x + 1, 0);
|
||||||
children: [{ text: defaultText || '' }],
|
|
||||||
},
|
const getCharacterCount = () => text.current.length;
|
||||||
];
|
|
||||||
|
|
||||||
const sendMessage = () => {
|
const sendMessage = () => {
|
||||||
if (!websocketService) {
|
if (!websocketService) {
|
||||||
@ -155,51 +120,71 @@ export const ChatTextField: FC<ChatTextFieldProps> = ({ defaultText, enabled, fo
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let message = serialize(editor);
|
let message = text.current;
|
||||||
// Strip the opening and closing <p> tags.
|
// Strip the opening and closing <p> tags.
|
||||||
message = message.replace(/^<p>|<\/p>$/g, '');
|
message = message.replace(/^<p>|<\/p>$/g, '');
|
||||||
websocketService.send({ type: MessageType.CHAT, body: message });
|
websocketService.send({ type: MessageType.CHAT, body: message });
|
||||||
|
|
||||||
// Clear the editor.
|
// Clear the input.
|
||||||
Transforms.delete(editor, {
|
text.current = '';
|
||||||
at: {
|
|
||||||
anchor: Editor.start(editor, []),
|
|
||||||
focus: Editor.end(editor, []),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
setCharacterCount(0);
|
setCharacterCount(0);
|
||||||
|
forceUpdate();
|
||||||
};
|
};
|
||||||
|
|
||||||
const createImageNode = (alt, src, name): ImageNode => ({
|
const insertTextAtCursor = (textToInsert: string) => {
|
||||||
type: 'image',
|
const output = [
|
||||||
alt,
|
text.current.slice(0, savedCursorLocation),
|
||||||
src,
|
textToInsert,
|
||||||
name,
|
text.current.slice(savedCursorLocation),
|
||||||
children: [{ text: '' }],
|
].join('');
|
||||||
});
|
text.current = output;
|
||||||
|
forceUpdate();
|
||||||
|
};
|
||||||
|
|
||||||
const insertImage = (url, name) => {
|
const convertOnPaste = (event: React.ClipboardEvent) => {
|
||||||
if (!url) return;
|
// Prevent paste.
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
const image = createImageNode(name, url, name);
|
// Set later.
|
||||||
|
let value = '';
|
||||||
|
|
||||||
Transforms.insertNodes(editor, image);
|
// Does method exist?
|
||||||
Editor.normalize(editor, { force: true });
|
const hasEventClipboard = !!(
|
||||||
|
event.clipboardData &&
|
||||||
|
typeof event.clipboardData === 'object' &&
|
||||||
|
typeof event.clipboardData.getData === 'function'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Get clipboard data?
|
||||||
|
if (hasEventClipboard) {
|
||||||
|
value = event.clipboardData.getData('text/plain');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert into temp `<textarea>`, read back out.
|
||||||
|
const textarea = document.createElement('textarea');
|
||||||
|
textarea.innerHTML = value;
|
||||||
|
value = textarea.innerText;
|
||||||
|
|
||||||
|
// Clean up text.
|
||||||
|
value = convertToText(value);
|
||||||
|
|
||||||
|
// Insert text.
|
||||||
|
insertTextAtCursor(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Native emoji
|
// Native emoji
|
||||||
const onEmojiSelect = (emoji: string) => {
|
const onEmojiSelect = (emoji: string) => {
|
||||||
ReactEditor.focus(editor);
|
insertTextAtCursor(emoji);
|
||||||
Transforms.insertText(editor, emoji);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Custom emoji images
|
||||||
const onCustomEmojiSelect = (name: string, emoji: string) => {
|
const onCustomEmojiSelect = (name: string, emoji: string) => {
|
||||||
ReactEditor.focus(editor);
|
const html = `<img src="${emoji}" alt="${name}" title=${name} class="emoji" />`;
|
||||||
insertImage(emoji, name);
|
insertTextAtCursor(html);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onKeyDown = (e: React.KeyboardEvent) => {
|
const onKeyDown = (e: React.KeyboardEvent) => {
|
||||||
const charCount = getCharacterCount(editor) + 1;
|
const charCount = getCharacterCount() + 1;
|
||||||
|
|
||||||
// Send the message when hitting enter.
|
// Send the message when hitting enter.
|
||||||
if (e.key === 'Enter') {
|
if (e.key === 'Enter') {
|
||||||
@ -207,37 +192,49 @@ export const ChatTextField: FC<ChatTextFieldProps> = ({ defaultText, enabled, fo
|
|||||||
sendMessage();
|
sendMessage();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Always allow backspace.
|
// Always allow backspace.
|
||||||
if (e.key === 'Backspace') {
|
if (e.key === 'Backspace') {
|
||||||
setCharacterCount(charCount - 1);
|
setCharacterCount(charCount - 1);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Always allow delete.
|
||||||
|
if (e.key === 'Delete') {
|
||||||
|
setCharacterCount(charCount - 1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always allow ctrl + a.
|
||||||
|
if (e.key === 'a' && e.ctrlKey) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Limit the number of characters.
|
// Limit the number of characters.
|
||||||
if (charCount + 1 > characterLimit) {
|
if (charCount + 1 > characterLimit) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
setCharacterCount(charCount + 1);
|
setCharacterCount(charCount + 1);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onPaste = (e: React.ClipboardEvent) => {
|
const handleChange = evt => {
|
||||||
const text = e.clipboardData.getData('text/plain');
|
text.current = evt.target.value;
|
||||||
|
|
||||||
const { length } = text;
|
|
||||||
if (characterCount + length > characterLimit) {
|
|
||||||
e.preventDefault();
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderElement = p => {
|
const handleBlur = () => {
|
||||||
switch (p.element.type) {
|
// Save the cursor location.
|
||||||
case 'image':
|
setSavedCursorLocation(
|
||||||
return <Image {...p} />;
|
getCaretPosition(document.getElementById('chat-input-content-editable')),
|
||||||
default:
|
);
|
||||||
return <p {...p} />;
|
};
|
||||||
|
|
||||||
|
const handleFocus = () => {
|
||||||
|
if (!savedCursorLocation) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Restore the cursor location.
|
||||||
|
setCaretPosition(document.getElementById('chat-input-content-editable'), savedCursorLocation);
|
||||||
|
setSavedCursorLocation(0);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -248,44 +245,30 @@ export const ChatTextField: FC<ChatTextFieldProps> = ({ defaultText, enabled, fo
|
|||||||
characterCount >= characterLimit && styles.maxCharacters,
|
characterCount >= characterLimit && styles.maxCharacters,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Slate editor={editor} initialValue={defaultEditorValue}>
|
|
||||||
<Editable
|
|
||||||
className="chat-text-input"
|
|
||||||
onKeyDown={onKeyDown}
|
|
||||||
onPaste={onPaste}
|
|
||||||
disabled={!enabled}
|
|
||||||
readOnly={!enabled}
|
|
||||||
renderElement={renderElement}
|
|
||||||
renderPlaceholder={({ children, attributes }) => (
|
|
||||||
<DefaultPlaceholder
|
|
||||||
attributes={{
|
|
||||||
...attributes,
|
|
||||||
style: { ...attributes.style, top: '15%' },
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</DefaultPlaceholder>
|
|
||||||
)}
|
|
||||||
placeholder={enabled ? 'Send a message to chat' : 'Chat is currently unavailable.'}
|
|
||||||
style={{ width: '100%' }}
|
|
||||||
role="textbox"
|
|
||||||
aria-label="Chat text input"
|
|
||||||
autoFocus={focusInput}
|
|
||||||
/>
|
|
||||||
<Popover
|
<Popover
|
||||||
content={
|
content={
|
||||||
<EmojiPicker
|
<EmojiPicker onEmojiSelect={onEmojiSelect} onCustomEmojiSelect={onCustomEmojiSelect} />
|
||||||
onEmojiSelect={onEmojiSelect}
|
|
||||||
onCustomEmojiSelect={onCustomEmojiSelect}
|
|
||||||
/>
|
|
||||||
}
|
}
|
||||||
trigger="click"
|
trigger="click"
|
||||||
placement="topRight"
|
placement="topRight"
|
||||||
onOpenChange={open => setShowEmojis(open)}
|
onOpenChange={open => setShowEmojis(open)}
|
||||||
open={showEmojis}
|
open={showEmojis}
|
||||||
/>
|
/>
|
||||||
</Slate>
|
<ContentEditable
|
||||||
|
id="chat-input-content-editable"
|
||||||
|
html={text.current}
|
||||||
|
placeholder={enabled ? 'Type a message...' : 'Chat is disabled'}
|
||||||
|
disabled={!enabled}
|
||||||
|
onKeyDown={onKeyDown}
|
||||||
|
onPaste={convertOnPaste}
|
||||||
|
onChange={handleChange}
|
||||||
|
onBlur={handleBlur}
|
||||||
|
onFocus={handleFocus}
|
||||||
|
autoFocus={focusInput}
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
role="textbox"
|
||||||
|
aria-label="Chat text input"
|
||||||
|
/>
|
||||||
{enabled && (
|
{enabled && (
|
||||||
<div style={{ display: 'flex', paddingLeft: '5px' }}>
|
<div style={{ display: 'flex', paddingLeft: '5px' }}>
|
||||||
<button
|
<button
|
||||||
|
111
web/package-lock.json
generated
111
web/package-lock.json
generated
@ -35,6 +35,7 @@
|
|||||||
"postcss-flexbugs-fixes": "5.0.2",
|
"postcss-flexbugs-fixes": "5.0.2",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-chartjs-2": "^5.2.0",
|
"react-chartjs-2": "^5.2.0",
|
||||||
|
"react-contenteditable": "^3.3.7",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
"react-error-boundary": "^4.0.0",
|
"react-error-boundary": "^4.0.0",
|
||||||
"react-hotkeys-hook": "4.4.0",
|
"react-hotkeys-hook": "4.4.0",
|
||||||
@ -43,8 +44,6 @@
|
|||||||
"react-virtuoso": "4.3.10",
|
"react-virtuoso": "4.3.10",
|
||||||
"recoil": "0.7.7",
|
"recoil": "0.7.7",
|
||||||
"sharp": "0.32.1",
|
"sharp": "0.32.1",
|
||||||
"slate": "0.94.1",
|
|
||||||
"slate-react": "0.97.0",
|
|
||||||
"ua-parser-js": "1.0.35",
|
"ua-parser-js": "1.0.35",
|
||||||
"video.js": "^8.3.0",
|
"video.js": "^8.3.0",
|
||||||
"workbox-precaching": "^7.0.0",
|
"workbox-precaching": "^7.0.0",
|
||||||
@ -4137,11 +4136,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz",
|
||||||
"integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw=="
|
"integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw=="
|
||||||
},
|
},
|
||||||
"node_modules/@juggle/resize-observer": {
|
|
||||||
"version": "3.4.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@juggle/resize-observer/-/resize-observer-3.4.0.tgz",
|
|
||||||
"integrity": "sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA=="
|
|
||||||
},
|
|
||||||
"node_modules/@kurkle/color": {
|
"node_modules/@kurkle/color": {
|
||||||
"version": "0.3.2",
|
"version": "0.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.2.tgz",
|
||||||
@ -13526,11 +13520,6 @@
|
|||||||
"integrity": "sha512-A79HEEiwXTFtfY+Bcbo58M2GRYzCr9itHWzbzHVFNEYCcoU/MMGwYYf721gBrnhpj1s6RGVVha/IgNFnR0Iw/Q==",
|
"integrity": "sha512-A79HEEiwXTFtfY+Bcbo58M2GRYzCr9itHWzbzHVFNEYCcoU/MMGwYYf721gBrnhpj1s6RGVVha/IgNFnR0Iw/Q==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/@types/is-hotkey": {
|
|
||||||
"version": "0.1.7",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/is-hotkey/-/is-hotkey-0.1.7.tgz",
|
|
||||||
"integrity": "sha512-yB5C7zcOM7idwYZZ1wKQ3pTfjA9BbvFqRWvKB46GFddxnJtHwi/b9y84ykQtxQPg5qhdpg4Q/kWU3EGoCTmLzQ=="
|
|
||||||
},
|
|
||||||
"node_modules/@types/istanbul-lib-coverage": {
|
"node_modules/@types/istanbul-lib-coverage": {
|
||||||
"version": "2.0.4",
|
"version": "2.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz",
|
||||||
@ -13617,7 +13606,8 @@
|
|||||||
"node_modules/@types/lodash": {
|
"node_modules/@types/lodash": {
|
||||||
"version": "4.14.195",
|
"version": "4.14.195",
|
||||||
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.195.tgz",
|
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.195.tgz",
|
||||||
"integrity": "sha512-Hwx9EUgdwf2GLarOjQp5ZH8ZmblzcbTBC2wtQWNKARBSxM9ezRIAUpeDTgoQRAFB0+8CNWXVA9+MaSOzOF3nPg=="
|
"integrity": "sha512-Hwx9EUgdwf2GLarOjQp5ZH8ZmblzcbTBC2wtQWNKARBSxM9ezRIAUpeDTgoQRAFB0+8CNWXVA9+MaSOzOF3nPg==",
|
||||||
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/@types/markdown-it": {
|
"node_modules/@types/markdown-it": {
|
||||||
"version": "12.2.3",
|
"version": "12.2.3",
|
||||||
@ -20122,18 +20112,6 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/direction": {
|
|
||||||
"version": "1.0.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/direction/-/direction-1.0.4.tgz",
|
|
||||||
"integrity": "sha512-GYqKi1aH7PJXxdhTeZBFrg8vUBeKXi+cNprXsC1kpJcbcVnV9wBsrOu1cQEdG0WeQwlfHiy3XvnKfIrJ2R0NzQ==",
|
|
||||||
"bin": {
|
|
||||||
"direction": "cli.js"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://github.com/sponsors/wooorm"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/doctrine": {
|
"node_modules/doctrine": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
|
||||||
@ -24252,15 +24230,6 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/immer": {
|
|
||||||
"version": "9.0.21",
|
|
||||||
"resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz",
|
|
||||||
"integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==",
|
|
||||||
"funding": {
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/immer"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/immutable": {
|
"node_modules/immutable": {
|
||||||
"version": "4.3.0",
|
"version": "4.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.0.tgz",
|
||||||
@ -24866,11 +24835,6 @@
|
|||||||
"url": "https://github.com/sponsors/wooorm"
|
"url": "https://github.com/sponsors/wooorm"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/is-hotkey": {
|
|
||||||
"version": "0.1.8",
|
|
||||||
"resolved": "https://registry.npmjs.org/is-hotkey/-/is-hotkey-0.1.8.tgz",
|
|
||||||
"integrity": "sha512-qs3NZ1INIS+H+yeo7cD9pDfwYV/jqRh1JG9S9zYrNudkoUQg7OL7ziXqRKu+InFjUIDoP2o6HIkLYMh1pcWgyQ=="
|
|
||||||
},
|
|
||||||
"node_modules/is-inside-container": {
|
"node_modules/is-inside-container": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz",
|
||||||
@ -38051,6 +38015,18 @@
|
|||||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-contenteditable": {
|
||||||
|
"version": "3.3.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-contenteditable/-/react-contenteditable-3.3.7.tgz",
|
||||||
|
"integrity": "sha512-GA9NbC0DkDdpN3iGvib/OMHWTJzDX2cfkgy5Tt98JJAbA3kLnyrNbBIpsSpPpq7T8d3scD39DHP+j8mAM7BIfQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"fast-deep-equal": "^3.1.3",
|
||||||
|
"prop-types": "^15.7.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-docgen": {
|
"node_modules/react-docgen": {
|
||||||
"version": "5.4.3",
|
"version": "5.4.3",
|
||||||
"resolved": "https://registry.npmjs.org/react-docgen/-/react-docgen-5.4.3.tgz",
|
"resolved": "https://registry.npmjs.org/react-docgen/-/react-docgen-5.4.3.tgz",
|
||||||
@ -40454,53 +40430,6 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/slate": {
|
|
||||||
"version": "0.94.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/slate/-/slate-0.94.1.tgz",
|
|
||||||
"integrity": "sha512-GH/yizXr1ceBoZ9P9uebIaHe3dC/g6Plpf9nlUwnvoyf6V1UOYrRwkabtOCd3ZfIGxomY4P7lfgLr7FPH8/BKA==",
|
|
||||||
"dependencies": {
|
|
||||||
"immer": "^9.0.6",
|
|
||||||
"is-plain-object": "^5.0.0",
|
|
||||||
"tiny-warning": "^1.0.3"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/slate-react": {
|
|
||||||
"version": "0.97.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/slate-react/-/slate-react-0.97.0.tgz",
|
|
||||||
"integrity": "sha512-C/e0l5hg/z+JZvGlfQJ4MDNJTjQxi4g6vUsSFmUhZXByDwsU0d0+jcbHO2KUjMe+sUKuFrcZmgzoByaeW9UjqA==",
|
|
||||||
"dependencies": {
|
|
||||||
"@juggle/resize-observer": "^3.4.0",
|
|
||||||
"@types/is-hotkey": "^0.1.1",
|
|
||||||
"@types/lodash": "^4.14.149",
|
|
||||||
"direction": "^1.0.3",
|
|
||||||
"is-hotkey": "^0.1.6",
|
|
||||||
"is-plain-object": "^5.0.0",
|
|
||||||
"lodash": "^4.17.4",
|
|
||||||
"scroll-into-view-if-needed": "^2.2.20",
|
|
||||||
"tiny-invariant": "1.0.6"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"react": ">=16.8.0",
|
|
||||||
"react-dom": ">=16.8.0",
|
|
||||||
"slate": ">=0.65.3"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/slate-react/node_modules/is-plain-object": {
|
|
||||||
"version": "5.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz",
|
|
||||||
"integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.10.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/slate/node_modules/is-plain-object": {
|
|
||||||
"version": "5.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz",
|
|
||||||
"integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.10.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/slice-ansi": {
|
"node_modules/slice-ansi": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz",
|
||||||
@ -42271,16 +42200,6 @@
|
|||||||
"node": ">=0.6.0"
|
"node": ">=0.6.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/tiny-invariant": {
|
|
||||||
"version": "1.0.6",
|
|
||||||
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.0.6.tgz",
|
|
||||||
"integrity": "sha512-FOyLWWVjG+aC0UqG76V53yAWdXfH8bO6FNmyZOuUrzDzK8DI3/JRY25UD7+g49JWM1LXwymsKERB+DzI0dTEQA=="
|
|
||||||
},
|
|
||||||
"node_modules/tiny-warning": {
|
|
||||||
"version": "1.0.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz",
|
|
||||||
"integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA=="
|
|
||||||
},
|
|
||||||
"node_modules/tinycolor2": {
|
"node_modules/tinycolor2": {
|
||||||
"version": "1.6.0",
|
"version": "1.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz",
|
||||||
|
@ -40,6 +40,7 @@
|
|||||||
"postcss-flexbugs-fixes": "5.0.2",
|
"postcss-flexbugs-fixes": "5.0.2",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-chartjs-2": "^5.2.0",
|
"react-chartjs-2": "^5.2.0",
|
||||||
|
"react-contenteditable": "^3.3.7",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
"react-error-boundary": "^4.0.0",
|
"react-error-boundary": "^4.0.0",
|
||||||
"react-hotkeys-hook": "4.4.0",
|
"react-hotkeys-hook": "4.4.0",
|
||||||
@ -48,8 +49,6 @@
|
|||||||
"react-virtuoso": "4.3.10",
|
"react-virtuoso": "4.3.10",
|
||||||
"recoil": "0.7.7",
|
"recoil": "0.7.7",
|
||||||
"sharp": "0.32.1",
|
"sharp": "0.32.1",
|
||||||
"slate": "0.94.1",
|
|
||||||
"slate-react": "0.97.0",
|
|
||||||
"ua-parser-js": "1.0.35",
|
"ua-parser-js": "1.0.35",
|
||||||
"video.js": "^8.3.0",
|
"video.js": "^8.3.0",
|
||||||
"workbox-precaching": "^7.0.0",
|
"workbox-precaching": "^7.0.0",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user