diff --git a/apps/web-server/package.json b/apps/web-server/package.json index 0c8cd73d6..e1f30bca8 100644 --- a/apps/web-server/package.json +++ b/apps/web-server/package.json @@ -63,7 +63,6 @@ "localforage": "^1.10.0", "moment": "^2.29.1", "pino": "^9.4.0", - "quill": "^1.3.7", "re-resizable": "^6.9.1", "react": "^18.3.1", "react-dnd": "^16.0.0", @@ -72,7 +71,6 @@ "react-draggable": "^4.4.4", "react-konva": "^18.1.1", "react-markdown": "^8.0.0", - "react-quilljs": "^1.2.17", "react-rnd": "^10.3.7", "react-use": "^17.3.1", "react-virtuoso": "^4.0.0", @@ -105,7 +103,6 @@ "@types/color-name": "1.1.5", "@types/howler": "2.2.12", "@types/html-escaper": "3.0.2", - "@types/quill": "2.0.14", "@types/react": "18.3.12", "@types/react-color": "3.0.12", "@types/react-dom": "18.3.1", diff --git a/apps/web-server/src/components/models/room/Room/subcomponents/components/BoardEditorModal/BoardEditorModal.tsx b/apps/web-server/src/components/models/room/Room/subcomponents/components/BoardEditorModal/BoardEditorModal.tsx index 716da282e..a0540a124 100644 --- a/apps/web-server/src/components/models/room/Room/subcomponents/components/BoardEditorModal/BoardEditorModal.tsx +++ b/apps/web-server/src/components/models/room/Room/subcomponents/components/BoardEditorModal/BoardEditorModal.tsx @@ -193,15 +193,12 @@ export const BoardEditorModal: React.FC = () => { bufferDuration="default" size="small" value={board.name} - onChange={e => { - if (e.previousValue === e.currentValue) { - return; - } + onChange={currentValue => { updateBoard(board => { if (board == null) { return; } - board.name = e.currentValue; + board.name = currentValue; }); }} /> diff --git a/apps/web-server/src/components/models/room/Room/subcomponents/components/CharacterEditorModal/CharacterEditorModal.tsx b/apps/web-server/src/components/models/room/Room/subcomponents/components/CharacterEditorModal/CharacterEditorModal.tsx index 06e70a99d..7779ec448 100644 --- a/apps/web-server/src/components/models/room/Room/subcomponents/components/CharacterEditorModal/CharacterEditorModal.tsx +++ b/apps/web-server/src/components/models/room/Room/subcomponents/components/CharacterEditorModal/CharacterEditorModal.tsx @@ -466,15 +466,12 @@ export const CharacterEditorModal: React.FC = () => { bufferDuration="default" size="small" value={character.name} - onChange={e => { - if (e.previousValue === e.currentValue) { - return; - } + onChange={currentValue => { updateCharacter(character => { if (character == null) { return; } - character.name = e.currentValue; + character.name = currentValue; }); }} /> @@ -719,12 +716,12 @@ export const CharacterEditorModal: React.FC = () => { size="small" bufferDuration="default" value={character.memo} - onChange={e => { + onChange={currentValue => { updateCharacter(character => { if (character == null) { return; } - character.memo = e.currentValue; + character.memo = currentValue; }); }} /> diff --git a/apps/web-server/src/components/models/room/Room/subcomponents/components/CharacterListPanelContent/CharacterListPanelContent.tsx b/apps/web-server/src/components/models/room/Room/subcomponents/components/CharacterListPanelContent/CharacterListPanelContent.tsx index 9ad733564..233a26ee4 100644 --- a/apps/web-server/src/components/models/room/Room/subcomponents/components/CharacterListPanelContent/CharacterListPanelContent.tsx +++ b/apps/web-server/src/components/models/room/Room/subcomponents/components/CharacterListPanelContent/CharacterListPanelContent.tsx @@ -454,17 +454,14 @@ const CharacterListTabPane: React.FC = ({ bufferDuration="default" size="small" value={character.state.name} - onChange={e => { - if (e.previousValue === e.currentValue) { - return; - } + onChange={currentValue => { setRoomState(state => { const targetCharacter = state.characters?.[character.stateId]; if (targetCharacter == null) { return; } - targetCharacter.name = e.currentValue; + targetCharacter.name = currentValue; }); }} /> diff --git a/apps/web-server/src/components/models/room/Room/subcomponents/components/CharacterParameterNamesEditorModal/CharacterParameterNamesEditorModal.tsx b/apps/web-server/src/components/models/room/Room/subcomponents/components/CharacterParameterNamesEditorModal/CharacterParameterNamesEditorModal.tsx index ec81c81aa..c49a26b1d 100644 --- a/apps/web-server/src/components/models/room/Room/subcomponents/components/CharacterParameterNamesEditorModal/CharacterParameterNamesEditorModal.tsx +++ b/apps/web-server/src/components/models/room/Room/subcomponents/components/CharacterParameterNamesEditorModal/CharacterParameterNamesEditorModal.tsx @@ -97,16 +97,13 @@ export const CharacterParameterNamesEditorModal: React.FC = () => { size="small" value={state.name} bufferDuration={200} - onChange={e => { - if (e.previousValue === e.currentValue) { - return; - } + onChange={currentValue => { setRoomState(state => { const targetNumParamName = state.numParamNames?.[key]; if (targetNumParamName == null) { return; } - targetNumParamName.name = e.currentValue; + targetNumParamName.name = currentValue; }); }} /> @@ -152,16 +149,13 @@ export const CharacterParameterNamesEditorModal: React.FC = () => { size="small" value={state.name} bufferDuration={200} - onChange={e => { - if (e.previousValue === e.currentValue) { - return; - } + onChange={currentValue => { setRoomState(state => { const targetBoolParamName = state.boolParamNames?.[key]; if (targetBoolParamName == null) { return; } - targetBoolParamName.name = e.currentValue; + targetBoolParamName.name = currentValue; }); }} /> @@ -207,16 +201,13 @@ export const CharacterParameterNamesEditorModal: React.FC = () => { size="small" value={state.name} bufferDuration={200} - onChange={e => { - if (e.previousValue === e.currentValue) { - return; - } + onChange={currentValue => { setRoomState(state => { const targetStrParamName = state.strParamNames?.[key]; if (targetStrParamName == null) { return; } - targetStrParamName.name = e.currentValue; + targetStrParamName.name = currentValue; }); }} /> diff --git a/apps/web-server/src/components/models/room/Room/subcomponents/components/CharacterTagNamesEditorModal/CharacterTagNamesEditorModal.tsx b/apps/web-server/src/components/models/room/Room/subcomponents/components/CharacterTagNamesEditorModal/CharacterTagNamesEditorModal.tsx index c61c035b6..8ee5da730 100644 --- a/apps/web-server/src/components/models/room/Room/subcomponents/components/CharacterTagNamesEditorModal/CharacterTagNamesEditorModal.tsx +++ b/apps/web-server/src/components/models/room/Room/subcomponents/components/CharacterTagNamesEditorModal/CharacterTagNamesEditorModal.tsx @@ -30,7 +30,7 @@ export const CharacterTagNamesEditorModal: React.FC = () => { disabled={characterTagName == null} value={characterTagName ?? ''} bufferDuration="default" - onChange={({ currentValue }) => { + onChange={currentValue => { setRoomState(roomState => { roomState[`characterTag${index}Name`] = currentValue; }); diff --git a/apps/web-server/src/components/models/room/Room/subcomponents/components/CharacterVarInput/CharacterVarInput.tsx b/apps/web-server/src/components/models/room/Room/subcomponents/components/CharacterVarInput/CharacterVarInput.tsx index 97f5d471d..083344b1b 100644 --- a/apps/web-server/src/components/models/room/Room/subcomponents/components/CharacterVarInput/CharacterVarInput.tsx +++ b/apps/web-server/src/components/models/room/Room/subcomponents/components/CharacterVarInput/CharacterVarInput.tsx @@ -25,11 +25,11 @@ export const CharacterVarInput: React.FC = ({ bufferDuration="default" disabled={character == null} value={character?.privateVarToml ?? ''} - onChange={e => { + onChange={currentValue => { if (character == null) { return; } - onChange(e.currentValue); + onChange(currentValue); }} /> ); diff --git a/apps/web-server/src/components/models/room/Room/subcomponents/components/ChatPalettePanelContent/ChatPalettePanelContent.tsx b/apps/web-server/src/components/models/room/Room/subcomponents/components/ChatPalettePanelContent/ChatPalettePanelContent.tsx index 23e047706..890ed600e 100644 --- a/apps/web-server/src/components/models/room/Room/subcomponents/components/ChatPalettePanelContent/ChatPalettePanelContent.tsx +++ b/apps/web-server/src/components/models/room/Room/subcomponents/components/ChatPalettePanelContent/ChatPalettePanelContent.tsx @@ -73,7 +73,7 @@ const ChatPaletteList: React.FC = ({ size="small" bufferDuration="default" value={chatPaletteText ?? ''} - onChange={e => onChange(e.currentValue)} + onChange={currentValue => onChange(currentValue)} /> ); } diff --git a/apps/web-server/src/components/models/room/Room/subcomponents/components/ImportBoardModal/ImportBoardModal.tsx b/apps/web-server/src/components/models/room/Room/subcomponents/components/ImportBoardModal/ImportBoardModal.tsx index bba75e716..844496d54 100644 --- a/apps/web-server/src/components/models/room/Room/subcomponents/components/ImportBoardModal/ImportBoardModal.tsx +++ b/apps/web-server/src/components/models/room/Room/subcomponents/components/ImportBoardModal/ImportBoardModal.tsx @@ -90,8 +90,8 @@ export const ImportBoardModal: React.FC = () => { { - setValue(e.currentValue); + onChange={currentValue => { + setValue(currentValue); }} bufferDuration="short" placeholder="ここにJSONをペーストしてください。" diff --git a/apps/web-server/src/components/models/room/Room/subcomponents/components/ImportCharacterModal/ImportCharacterModal.tsx b/apps/web-server/src/components/models/room/Room/subcomponents/components/ImportCharacterModal/ImportCharacterModal.tsx index 374e77863..01f3a3ee9 100644 --- a/apps/web-server/src/components/models/room/Room/subcomponents/components/ImportCharacterModal/ImportCharacterModal.tsx +++ b/apps/web-server/src/components/models/room/Room/subcomponents/components/ImportCharacterModal/ImportCharacterModal.tsx @@ -92,8 +92,8 @@ export const ImportCharacterModal: React.FC = () => { { - setValue(e.currentValue); + onChange={currentValue => { + setValue(currentValue); }} bufferDuration="short" placeholder="ここにJSONをペーストしてください。" diff --git a/apps/web-server/src/components/models/room/Room/subcomponents/components/MemosPanelContent/MemosPanelContent.tsx b/apps/web-server/src/components/models/room/Room/subcomponents/components/MemosPanelContent/MemosPanelContent.tsx index 9482428dd..4ac174ede 100644 --- a/apps/web-server/src/components/models/room/Room/subcomponents/components/MemosPanelContent/MemosPanelContent.tsx +++ b/apps/web-server/src/components/models/room/Room/subcomponents/components/MemosPanelContent/MemosPanelContent.tsx @@ -160,11 +160,11 @@ const Memo: React.FC = ({ memoId, memo }: MemoProps) => { value={memo.name} style={{ width: '100%' }} placeholder="名前" - onChange={e => + onChange={currentValue => setRoomState(prevState => { const memo = prevState.memos?.[memoId]; if (memo != null) { - memo.name = e.currentValue; + memo.name = currentValue; } }) } @@ -177,7 +177,7 @@ const Memo: React.FC = ({ memoId, memo }: MemoProps) => { bufferDuration="default" value={memo.text} placeholder="本文" - onChange={e => { + onChange={currentValue => { setRoomState(roomState => { if (roomState.memos == null) { roomState.memos = {}; @@ -186,7 +186,7 @@ const Memo: React.FC = ({ memoId, memo }: MemoProps) => { if (memo == null) { return; } - memo.text = e.currentValue; + memo.text = currentValue; }); }} /> diff --git a/apps/web-server/src/components/models/room/Room/subcomponents/components/OverriddenParameterNameEditor/OverriddenParameterNameEditor.tsx b/apps/web-server/src/components/models/room/Room/subcomponents/components/OverriddenParameterNameEditor/OverriddenParameterNameEditor.tsx index 6d6a292a1..3ed05021d 100644 --- a/apps/web-server/src/components/models/room/Room/subcomponents/components/OverriddenParameterNameEditor/OverriddenParameterNameEditor.tsx +++ b/apps/web-server/src/components/models/room/Room/subcomponents/components/OverriddenParameterNameEditor/OverriddenParameterNameEditor.tsx @@ -53,7 +53,7 @@ export const OverriddenParameterNameEditor: React.FC = ({ size="small" value={overriddenParameterName ?? ''} bufferDuration="default" - onChange={({ currentValue }) => { + onChange={currentValue => { onOverriddenParameterNameChange(currentValue); }} /> diff --git a/apps/web-server/src/components/models/room/Room/subcomponents/components/PieceEditorMemoRow/PieceEditorMemoRow.tsx b/apps/web-server/src/components/models/room/Room/subcomponents/components/PieceEditorMemoRow/PieceEditorMemoRow.tsx index 2d6ea42a4..b00307338 100644 --- a/apps/web-server/src/components/models/room/Room/subcomponents/components/PieceEditorMemoRow/PieceEditorMemoRow.tsx +++ b/apps/web-server/src/components/models/room/Room/subcomponents/components/PieceEditorMemoRow/PieceEditorMemoRow.tsx @@ -14,11 +14,8 @@ export const PieceEditorMemoRow: React.FC<{ size="small" bufferDuration="default" value={state ?? ''} - onChange={e => { - if (e.previousValue === e.currentValue) { - return; - } - onChange(e.currentValue); + onChange={currentValue => { + onChange(currentValue); }} /> diff --git a/apps/web-server/src/components/models/room/Room/subcomponents/components/PieceEditorNameRow/PieceEditorNameRow.tsx b/apps/web-server/src/components/models/room/Room/subcomponents/components/PieceEditorNameRow/PieceEditorNameRow.tsx index fb8949a8a..6bcd4e655 100644 --- a/apps/web-server/src/components/models/room/Room/subcomponents/components/PieceEditorNameRow/PieceEditorNameRow.tsx +++ b/apps/web-server/src/components/models/room/Room/subcomponents/components/PieceEditorNameRow/PieceEditorNameRow.tsx @@ -12,11 +12,8 @@ export const PieceEditorNameRow: React.FC<{ bufferDuration="default" size="small" value={state ?? ''} - onChange={e => { - if (e.previousValue === e.currentValue) { - return; - } - onChange(e.currentValue); + onChange={currentValue => { + onChange(currentValue); }} /> diff --git a/apps/web-server/src/components/models/room/Room/subcomponents/components/RectEditor/RectEditor.tsx b/apps/web-server/src/components/models/room/Room/subcomponents/components/RectEditor/RectEditor.tsx index 6bf85994b..68685a4c5 100644 --- a/apps/web-server/src/components/models/room/Room/subcomponents/components/RectEditor/RectEditor.tsx +++ b/apps/web-server/src/components/models/room/Room/subcomponents/components/RectEditor/RectEditor.tsx @@ -28,7 +28,7 @@ const NameRow = ({ value, onChange }: PropsBase onChange={e => { const newValue = produce(value, state => { // nameがない状態をあらわす値として '' と undefined の2種類が混在するのは後々仕様変更があった際に困るかもしれないため、undefinedで統一させるようにしている - state.name = e.currentValue === '' ? undefined : e.currentValue; + state.name = e === '' ? undefined : e; }); onChange(newValue); }} diff --git a/apps/web-server/src/components/models/room/Room/subcomponents/components/RoomMessagesPanelContent/RoomMessagesPanelContent.tsx b/apps/web-server/src/components/models/room/Room/subcomponents/components/RoomMessagesPanelContent/RoomMessagesPanelContent.tsx index a4cb869c0..2c62f11e3 100644 --- a/apps/web-server/src/components/models/room/Room/subcomponents/components/RoomMessagesPanelContent/RoomMessagesPanelContent.tsx +++ b/apps/web-server/src/components/models/room/Room/subcomponents/components/RoomMessagesPanelContent/RoomMessagesPanelContent.tsx @@ -380,12 +380,9 @@ const ChannelNamesEditor: React.FC = (props: ChannelName { - if (e.previousValue === e.currentValue) { - return; - } + onChange={currentValue => { operateAsStateWithImmer(state => { - state[key] = e.currentValue; + state[key] = currentValue; }); }} /> diff --git a/apps/web-server/src/components/models/room/Room/subcomponents/components/StringParameterInput/StringParameterInput.tsx b/apps/web-server/src/components/models/room/Room/subcomponents/components/StringParameterInput/StringParameterInput.tsx index 43ff97707..6c328ff85 100644 --- a/apps/web-server/src/components/models/room/Room/subcomponents/components/StringParameterInput/StringParameterInput.tsx +++ b/apps/web-server/src/components/models/room/Room/subcomponents/components/StringParameterInput/StringParameterInput.tsx @@ -67,14 +67,14 @@ export const StringParameterInput: React.FC = ({ bufferDuration="default" disabled={disabled} value={parameter?.value ?? ''} - onChange={e => { + onChange={currentValue => { // valueで??演算子を使用しているため、e.previousValueは使えない。そのため代わりにparameter?.valueを使用している const previousValue = parameter?.value; - if (previousValue === e.currentValue) { + if (previousValue === currentValue) { return; } - const diff2 = nullableTextDiff({ prev: previousValue, next: e.currentValue }); + const diff2 = nullableTextDiff({ prev: previousValue, next: currentValue }); const operation: CharacterUpOperation = { $v: 2, $r: 1, diff --git a/apps/web-server/src/components/models/room/Room/subcomponents/components/StringPieceEditor/StringPieceEditor.tsx b/apps/web-server/src/components/models/room/Room/subcomponents/components/StringPieceEditor/StringPieceEditor.tsx index f314996da..f5a66f5bc 100644 --- a/apps/web-server/src/components/models/room/Room/subcomponents/components/StringPieceEditor/StringPieceEditor.tsx +++ b/apps/web-server/src/components/models/room/Room/subcomponents/components/StringPieceEditor/StringPieceEditor.tsx @@ -154,7 +154,7 @@ export const useStringPieceEditor = ({ bufferDuration="default" size="small" value={state.value} - onChange={({ currentValue }) => { + onChange={currentValue => { updateState(state => { if (state == null) { return; diff --git a/apps/web-server/src/components/ui/CollaborativeInput/CollaborativeInput.css b/apps/web-server/src/components/ui/CollaborativeInput/CollaborativeInput.css new file mode 100644 index 000000000..2f9e271af --- /dev/null +++ b/apps/web-server/src/components/ui/CollaborativeInput/CollaborativeInput.css @@ -0,0 +1,34 @@ + + .collaborative-input { + /* Ant Design の Input の色と等しい */ + background-color: #141414; + /* Ant Design の Input の色と等しい */ + color: rgba(255, 255, 255, 0.85); + border: 1px solid #333; + border-radius: 6px; + } + + .collaborative-input:focus { +outline: none; + border: 1px solid #0078d4; + } + + .collaborative-input.very-small { + font-size: 0.7rem; + padding: 2px 4px; + } + + .collaborative-input.small { + font-size: 0.75rem; + padding: 2px 4px; + } + + .collaborative-input.medium { + font-size: 0.8rem; + padding: 4px 8px; + } + + .collaborative-input.disabled { + color: gray; + cursor: not-allowed; + } \ No newline at end of file diff --git a/apps/web-server/src/components/ui/CollaborativeInput/CollaborativeInput.stories.tsx b/apps/web-server/src/components/ui/CollaborativeInput/CollaborativeInput.stories.tsx index 3306f3f4d..17fea8dd1 100644 --- a/apps/web-server/src/components/ui/CollaborativeInput/CollaborativeInput.stories.tsx +++ b/apps/web-server/src/components/ui/CollaborativeInput/CollaborativeInput.stories.tsx @@ -1,49 +1,36 @@ import { Meta, StoryObj } from '@storybook/react'; +import { Input } from 'antd'; import classNames from 'classnames'; import React from 'react'; -import { interval } from 'rxjs'; -import { CollaborativeInput, OnChangeParams } from './CollaborativeInput'; +import { CollaborativeInput, Props } from './CollaborativeInput'; import { StorybookProvider } from '@/components/behaviors/StorybookProvider'; -import { flex, flex1, flexColumn, flexInitial } from '@/styles/className'; +import { flex, flexColumn, flexInitial } from '@/styles/className'; const Main: React.FC<{ - bufferDuration: number | 'default' | 'short' | null; + bufferDuration: number | 'default' | 'short'; + multiline?: boolean; + disabled?: boolean; + size?: Props['size']; placeholder?: string; - disabled: boolean; - multiline: boolean; - testUpdate: boolean; testBottomElement: boolean; -}> = ({ bufferDuration, placeholder, disabled, multiline, testUpdate, testBottomElement }) => { - const [changelog, setChangelog] = React.useState([]); - const [value, setValue] = React.useState('init text'); +}> = ({ bufferDuration, multiline, disabled, size, placeholder, testBottomElement }) => { + const [value, setValue] = React.useState(placeholder == null ? 'init text' : ''); const [bottomElement, setBottomElement] = React.useState(); - React.useEffect(() => { - if (!testUpdate) { - return; - } - const subscription = interval(4000).subscribe(i => { - setValue('new text ' + i); - }); - return () => subscription.unsubscribe(); - }, [testUpdate]); return ( -
+
+

CollaborativeInput

{ - setChangelog(state => [...state, e]); + setValue(e); }} bufferDuration={bufferDuration} - placeholder={placeholder} + multiline={multiline} disabled={disabled} + size={size} + placeholder={placeholder} onSkipping={ testBottomElement ? e => @@ -56,13 +43,12 @@ const Main: React.FC<{ } /> {testBottomElement ? bottomElement : null} -
- {changelog.slice(-3).map((log, i) => ( -
{`previousValue: ${log.previousValue}, currentValue: ${log.currentValue}`}
- ))} -
+

↓ これでCollaborativeInput.roomStateTextを変更できる

+ {multiline === true ? ( + setValue(e.target.value)} /> + ) : ( + setValue(e.target.value)} /> + )}
); @@ -73,10 +59,6 @@ const meta = { component: Main, args: { bufferDuration: 'default', - placeholder: 'placeholderです', - multiline: false, - disabled: false, - testUpdate: false, testBottomElement: true, }, } satisfies Meta; @@ -86,10 +68,17 @@ export default meta; type Story = StoryObj; export const Default: Story = {}; +export const PlaceHolder: Story = { args: { placeholder: 'placeholder' } }; -export const DefaultMultiline: Story = { +export const Medium: Story = { args: { size: 'medium' } }; + +export const Small: Story = { args: { size: 'small' } }; + +export const VerySmall: Story = { args: { size: 'verySmall' } }; + +export const FiveSeconds: Story = { args: { - multiline: true, + bufferDuration: 5000, }, }; @@ -99,8 +88,18 @@ export const Short: Story = { }, }; -export const NoBuffer: Story = { +export const Disabled: Story = { args: { - bufferDuration: null, + disabled: true, }, }; + +export const Multiline: Story = { + args: { + multiline: true, + }, +}; + +export const MultilinePlaceHolder: Story = { + args: { multiline: true, placeholder: 'placeholder' }, +}; diff --git a/apps/web-server/src/components/ui/CollaborativeInput/CollaborativeInput.test.tsx b/apps/web-server/src/components/ui/CollaborativeInput/CollaborativeInput.test.tsx deleted file mode 100644 index 6397af313..000000000 --- a/apps/web-server/src/components/ui/CollaborativeInput/CollaborativeInput.test.tsx +++ /dev/null @@ -1,199 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unsafe-argument */ -/* eslint-disable @typescript-eslint/no-unsafe-assignment */ -// @vitest-environment jsdom - -import { act, render } from '@testing-library/react'; -import Quill from 'quill'; -import React from 'react'; -import { describe, expect, test, vi } from 'vitest'; -import { CollaborativeInput, OnChangeParams } from './CollaborativeInput'; - -const delayTime = 1100; - -const delay = async (delayTime: number) => { - await new Promise(r => setTimeout(r, delayTime)); -}; - -const quillNotFoundErrorMessage = 'Make sure Quill instance is prepared'; - -describe('CollaborativeInput', () => { - test.each` - bufferDuration | invokeUpdate1 | invokeUpdate2 - ${'default' as const} | ${false} | ${false} - ${'default' as const} | ${true} | ${false} - ${'default' as const} | ${true} | ${true} - ${'short' as const} | ${false} | ${false} - ${'short' as const} | ${true} | ${false} - ${'short' as const} | ${true} | ${true} - ${1000} | ${false} | ${false} - ${1000} | ${true} | ${false} - ${1000} | ${true} | ${true} - `('テキスト変更直後のonChange', ({ bufferDuration, invokeUpdate1, invokeUpdate2 }) => { - const onChange = vi.fn<(_: OnChangeParams) => void>(); - let quill: Quill | undefined; - const onGetQuill = (newQuill: Quill | undefined) => { - quill = newQuill; - }; - const { unmount } = render( - , - ); - if (quill == null) { - throw new Error(quillNotFoundErrorMessage); - } - if (invokeUpdate1) { - quill.setText('TEXT_VALUE2'); - } - if (invokeUpdate2) { - quill.setText('TEXT_VALUE3'); - } - expect(onChange).not.toHaveBeenCalled(); - unmount(); - if (invokeUpdate1 || invokeUpdate2) { - expect(onChange).toHaveBeenCalled(); - } else { - expect(onChange).not.toHaveBeenCalled(); - } - }); - - test.each(['default', 'short', 1000] as const)( - '何も変更がなかったときのonChange', - async bufferDuration => { - const onChangeHistory: OnChangeParams[] = []; - const onChange = (params: OnChangeParams) => { - onChangeHistory.push(params); - }; - render( - , - ); - await act(async () => await delay(delayTime)); - expect(onChangeHistory).toEqual([]); - }, - ); - - test.each(['default', 'short', 1000] as const)( - '2回変更があったときのonChange', - async bufferDuration => { - const onChangeHistory: OnChangeParams[] = []; - const onChange = (params: OnChangeParams) => { - onChangeHistory.push(params); - }; - let quill: Quill | undefined; - const onGetQuill = (newQuill: Quill | undefined) => { - quill = newQuill; - }; - render( - , - ); - if (quill == null) { - throw new Error(quillNotFoundErrorMessage); - } - quill.setText('TEXT_VALUE2'); - quill.setText('TEXT_VALUE3'); - await act(async () => await delay(delayTime)); - expect(onChangeHistory).toEqual([ - { previousValue: 'TEXT_VALUE1', currentValue: 'TEXT_VALUE3' }, - ]); - }, - ); - - test.each` - bufferDuration | newValue - ${'default' as const} | ${'TEXT_VALUE2'} - ${'default' as const} | ${'TEXT_VALUE3'} - ${'short' as const} | ${'TEXT_VALUE2'} - ${'short' as const} | ${'TEXT_VALUE3'} - ${1000} | ${'TEXT_VALUE2'} - ${1000} | ${'TEXT_VALUE3'} - `('入力中に、valueが更新されたときのonChange', async ({ bufferDuration, newValue }) => { - const onChangeHistory: OnChangeParams[] = []; - const onChange = (params: OnChangeParams) => { - onChangeHistory.push(params); - }; - let quill: Quill | undefined; - const onGetQuill = (newQuill: Quill | undefined) => { - quill = newQuill; - }; - const { rerender } = render( - , - ); - if (quill == null) { - throw new Error(quillNotFoundErrorMessage); - } - quill.setText(newValue); - rerender( - , - ); - await act(async () => await delay(delayTime)); - expect(onChangeHistory).toEqual([]); - }); - - test.each` - bufferDuration | newValue - ${'default' as const} | ${'TEXT_VALUE2'} - ${'default' as const} | ${'TEXT_VALUE3'} - ${'short' as const} | ${'TEXT_VALUE2'} - ${'short' as const} | ${'TEXT_VALUE3'} - ${1000} | ${'TEXT_VALUE2'} - ${1000} | ${'TEXT_VALUE3'} - `( - '入力中に、valueが更新された直後に再び入力したときのonChange', - async ({ bufferDuration, newValue }) => { - const onChangeHistory: OnChangeParams[] = []; - const onChange = (params: OnChangeParams) => { - onChangeHistory.push(params); - }; - let quill: Quill | undefined; - const onGetQuill = (newQuill: Quill | undefined) => { - quill = newQuill; - }; - const { rerender } = render( - , - ); - if (quill == null) { - throw new Error(quillNotFoundErrorMessage); - } - quill.setText(newValue); - rerender( - , - ); - quill.setText('TEXT_VALUE4'); - expect(onChangeHistory).toEqual([]); - await act(async () => await delay(delayTime)); - expect(onChangeHistory).toEqual([ - { previousValue: 'TEXT_VALUE3', currentValue: 'TEXT_VALUE4' }, - ]); - }, - ); -}); diff --git a/apps/web-server/src/components/ui/CollaborativeInput/CollaborativeInput.tsx b/apps/web-server/src/components/ui/CollaborativeInput/CollaborativeInput.tsx index 2e782e602..30252a1f0 100644 --- a/apps/web-server/src/components/ui/CollaborativeInput/CollaborativeInput.tsx +++ b/apps/web-server/src/components/ui/CollaborativeInput/CollaborativeInput.tsx @@ -1,92 +1,17 @@ -/** @jsxImportSource @emotion/react */ -import { SerializedStyles, css } from '@emotion/react'; import { loggerRef } from '@flocon-trpg/utils'; -import { diff, serializeUpOperation, toUpOperation } from '@kizahasi/ot-string'; -import Quill from 'quill'; -import QuillDelta from 'quill-delta'; -import React from 'react'; -import { useQuill } from 'react-quilljs'; -import { useLatest, usePrevious } from 'react-use'; -// react-quilljs などを使わず直接 Quill を使うと、next build 時に ReferenceError: document is not defined というエラーが出てビルドできない。おそらくawait importでも回避できそうだが、react-quilljs を利用することで解決している。 -import { Subject, Subscription, debounceTime } from 'rxjs'; -import useConstant from 'use-constant'; - -/* -quill.bubble.css:389 に、下のようにplaceholderに関するstyleが記述されている。 - -.ql-editor.ql-blank::before { - color: rgba(0,0,0,0.6); - content: attr(data-placeholder); - font-style: italic; - left: 15px; - pointer-events: none; - position: absolute; - right: 15px; -} - -だが、これには次の問題点があるので一部変更している。 -- color: デフォルトだと黒っぽくてほぼ見えない。変更後の色は適当なので後で見直したほうがいいかも。 -- font-style: 日本語などは斜体にならないため、英数字と混ざると不格好である。 - -borderはantdになるべく合わせている。 -*/ -const generateBaseCss = ({ size }: { size: 'verySmall' | 'small' | 'medium' }) => { - let fontSize: string; - let padding: string; - let paddingX: string; - switch (size) { - case 'verySmall': { - fontSize = '0.7rem'; - padding = '2px 4px'; - paddingX = '4px'; - break; - } - case 'small': { - fontSize = '0.75rem'; - padding = '2px 4px'; - paddingX = '4px'; - break; - } - case 'medium': { - fontSize = '0.8rem'; - padding = '4px 8px'; - paddingX = '8px'; - break; - } - } - return css` - .ql-editor.ql-blank::before { - color: rgb(140, 140, 140); - font-style: normal; - left: ${paddingX}; - right: ${paddingX}; - } - - .ql-editor { - font-size: ${fontSize}; - padding: ${padding}; - border: 1px solid #434343; - border-radius: 2px; - } - `; -}; - -const verySmallCss = generateBaseCss({ size: 'verySmall' }); - -const smallCss = generateBaseCss({ size: 'small' }); - -const mediumCss = generateBaseCss({ size: 'medium' }); - -const disabledCss = css` - * { - background-color: rgb(40, 40, 40); - cursor: not-allowed; - } - - .ql-editor { - color: gray; - } -`; +import { + UpOperation, + UpOperationUnit, + apply, + diff, + serializeUpOperation, + toUpOperation, + transformUpOperation, +} from '@kizahasi/ot-string'; +import classNames from 'classnames'; +import React, { useState } from 'react'; +import { useInterval, useLatest } from 'react-use'; +import './CollaborativeInput.css'; export type OnSkippingParams = | { @@ -100,143 +25,24 @@ export type OnSkippingParams = currentValue?: undefined; }; -export type OnChangeParams = { - previousValue: string; - currentValue: string; -}; - -const createDelta = ({ prev, next }: { prev: string; next: string }): QuillDelta => { - /* - 単純にdiffを取ってDeltaを生成しているだけ。そのため、厳密には編集者が編集した部分と異なる部分が編集されたとみなされる可能性がある。 - 例えば'abababab'という文字を他の人が'ababab'にした場合、どこのabが削除されたかはdiffを取るだけではわからない。自分のカーソルの位置を|として'abab|abab'となっている場合、どこのabが削除されたかによって次のカーソルの位置は本来は変わるはずである。この場合は'ab|abab'もしくは'abab|ab'のいずれかが考えられる(厳密には他にも例えばababが削除されて別の場所にabが挿入されるケースもあるため、これら以外の場合も取りうる)。 - だが、このようなことが起こるのはそう多くないと考えられるし、起こっても不便さは感じないと思われるので問題なしとしている。 - */ - - const result = new QuillDelta(); - const diffResult = diff({ prevState: prev, nextState: next }); - const upOperation = toUpOperation(diffResult); - const serializedUpOperation = serializeUpOperation(upOperation); - for (const unit of serializedUpOperation) { - switch (unit.t) { - case 'r': - result.retain(unit.r); - break; - case 'd': - result.delete(unit.d); - break; - case 'i': - result.insert(unit.i); - break; - } - } - return result; -}; - -function useBuffer({ - value, - bufferDuration, - onChangeOutput, - setValueToComponent, -}: { - value: TValue; - bufferDuration: number | null; - onChangeOutput: (params: { previousValue: TValue; currentValue: TValue }) => void; - setValueToComponent: (params: { value: TValue; component: TComponent }) => void; -}) { - if (bufferDuration != null && bufferDuration < 0) { - throw new Error('bufferDuration < 0'); - } - - const onChangeRef = useLatest(onChangeOutput); - const setValueToComponentRef = useLatest(setValueToComponent); - - const ref = React.useRef(null); - const subject = useConstant(() => new Subject()); - const latestOnChangeInputValueRef = React.useRef(value); - const onChangeInput: (value: TValue) => void = useConstant(() => { - return x => { - latestOnChangeInputValueRef.current = x; - subject.next(x); - }; - }); - const [, setSubscription] = React.useState(); - const [changeParams, setChangeParams] = React.useState<{ - previousValue?: TValue; - currentValue: TValue; - }>({ currentValue: value }); - const changeParamsRef = useLatest(changeParams); - const [subscriptionUpdateKey, setSubscriptionUpdateKey] = React.useState(0); - - React.useEffect(() => { - if (ref.current != null) { - setValueToComponentRef.current({ value, component: ref.current }); - } - - setSubscriptionUpdateKey(oldState => oldState + 1); - setChangeParams({ currentValue: value }); - }, [setValueToComponentRef, value]); - - React.useEffect(() => { - const newSubscription = ( - bufferDuration == null ? subject : subject.pipe(debounceTime(bufferDuration)) - ).subscribe(newValue => { - setChangeParams(oldResult => { - return { - previousValue: oldResult.currentValue, - currentValue: newValue, - }; - }); - }); - setSubscription(oldSubscription => { - oldSubscription?.unsubscribe(); - return newSubscription; - }); - return () => { - newSubscription.unsubscribe(); - }; - }, [subject, bufferDuration, subscriptionUpdateKey]); - - React.useEffect(() => { - if (changeParams.previousValue !== undefined) { - onChangeRef.current({ - previousValue: changeParams.previousValue, - currentValue: changeParams.currentValue, - }); - } - }, [changeParams, onChangeRef]); - - // unmount時にonChangeを実行させている - React.useEffect(() => { - const $changeParamsRef = changeParamsRef; - const $latestOnChangeInputValueRef = latestOnChangeInputValueRef; - const $onChangeRef = onChangeRef; - return () => { - const previousValue = $changeParamsRef.current.currentValue; - const currentValue = $latestOnChangeInputValueRef.current; - if (previousValue !== currentValue) { - $onChangeRef.current({ previousValue, currentValue }); - } - }; - }, [changeParamsRef, onChangeRef]); - - return { - onChangeInput, - ref, - }; -} - export type Props = { + /** 現在の部屋の State 由来の文字列。この値が変わっても CollaborativeInput にはすぐには反映されず、適当なときに CollaborativeInput コンポーネント内の input 等に反映されなおかつ `onChange` が実行されます。これらの文字列は一致します。 */ value: string; - onChange: (e: OnChangeParams) => void; + + /** `value` が変更されるべきときに実行されます。これが実行されたとき、`value` を即座にその値に変更してください。もしそうしないと、例えば `value` が 'a' のときに 'onChange('b')' が実行されたとき、もし `value` を 'b' に変更する前に Collaborative 内で定期実行される突合処理(`matchData` 関数)が実行されてしまうと、「`value` が API サーバー等により 'a' に戻った」と判断され、`onChange('a')` が実行されてしまい、望まない状態を引き起こします。 */ + onChange: (newValue: string) => void; + // コードエディターなどを作る際に「解析中」のメッセージを出せるようにするためのプロパティ。 // 当初は createBottomElement という名前であり戻り値の型も void ではなく JSX.Element | null で、返された値をCollaborativeInput 側で表示するようにしていた。 // だが、そうするとメインのElementとBottomElementの2つを返すことになるため、React.Fragmentもしくはdivで包む必要がある。どちらの場合でもstyleやclassNameの設定で混乱する可能性があるため、ボツにした。 onSkipping?: (params: OnSkippingParams) => void; - onGetQuill?: (nextQuill: Quill | undefined) => void; + + /** 0以下の値にしてはならない。0より大きい値であっても小さい値にしてしまうと、文字列比較が頻繁に行われてしまいパフォーマンスが悪化するのでこれも避けるべき。*/ + bufferDuration: number | 'default' | 'short'; + // trueならばtextareaのように、そうでなければinputのようにふるまう multiline?: boolean; - bufferDuration: number | 'default' | 'short' | null; - // placeholderの変更は反映されない。最初の値が常に使われる。 + placeholder?: string; disabled?: boolean; className?: string; @@ -244,217 +50,290 @@ export type Props = { size?: 'verySmall' | 'small' | 'medium' | undefined; }; -const useWarnPlaceholderChanges = ({ - quill, - placeholderProp, +namespace ClassNames { + export const collaborativeInput = 'collaborative-input'; + const small = 'small'; + const verySmall = 'very-small'; + const medium = 'medium'; + export const getSize = (size: Props['size']) => { + switch (size) { + case 'verySmall': + return verySmall; + case 'small': + return small; + case 'medium': + return medium; + default: + return medium; + } + }; + export const disabled = 'disabled'; +} + +const useParseBufferDuration = (value: Props['bufferDuration']): number => { + if (value === 'default') { + return 1000; + } + if (value === 'short') { + return 333; + } + if (value <= 0) { + throw new Error(`bufferDuration must be greater than 0. but got ${value}`); + } + return value; +}; + +// TODO: 文字列に変化がないときは処理を高速化できると思われる。文字列に変化がないときでもこの関数はよく呼ばれるため、高速化が望まれる。 +const ot = ({ + rootText, + currentMyText, + currentTheirText, }: { - quill: Quill | undefined; - placeholderProp: string | undefined; + rootText: string; + currentMyText: string; + currentTheirText: string; }) => { - const currentQuillRef = React.useRef(quill); - const prevQuill = usePrevious(quill); - const prevQuillRef = React.useRef(prevQuill); - const currentPlaceholderRef = useLatest(placeholderProp); - const prevPlaceholder = usePrevious(placeholderProp); - const prevPlaceholderRef = useLatest(prevPlaceholder); + const first = diff({ prevState: rootText, nextState: currentTheirText }); + const second = diff({ prevState: rootText, nextState: currentMyText }); + + const firstUpOperation = toUpOperation(first); + const secondUpOperation = toUpOperation(second); + const xform = transformUpOperation({ first: firstUpOperation, second: secondUpOperation }); + if (xform.isError) { + loggerRef.fatal( + { + rootText, + currentMyText, + currentTheirText, + first: firstUpOperation, + second: secondUpOperation, + error: xform.error, + }, + 'OT failed at CollaborativeInput.tsx', + ); + throw new Error('OT failed at CollaborativeInput.tsx. See the log for details.'); + } + const result = apply({ prevState: currentMyText, upOperation: xform.value.firstPrime }); + if (result.isError) { + loggerRef.fatal( + { + rootText, + currentMyText, + currentTheirText, + first: firstUpOperation, + second: secondUpOperation, + error: result.error, + }, + 'Applying operation is failed at CollaborativeInput.tsx', + ); + throw new Error( + 'Applying operation is failed at CollaborativeInput.tsx. See the log for details.', + ); + } + return { state: result.value, upOperation: xform.value.firstPrime }; +}; - React.useEffect(() => { - if (prevQuillRef.current !== currentQuillRef.current) { - return; +function* toOperationUnitByChar(operation: readonly UpOperationUnit[]) { + for (const unit of operation) { + switch (unit.t) { + case 'r': + for (let i = 0; i < unit.r; i++) { + yield { type: 'retain' } as const; + } + break; + case 'i': + for (const char of unit.i) { + yield { type: 'insert', char } as const; + } + break; + case 'd': + for (let i = 0; i < unit.d; i++) { + yield { type: 'delete' } as const; + } + break; + } + } +} + +const moveCursorByUpOperation = ( + input: HTMLInputElement | HTMLTextAreaElement, + operation: UpOperation, + newValue: string, +) => { + if (input.selectionStart == null) { + return; + } + + // value のセットの前にカーソル位置を取得しないとカーソル位置がリセットされてしまうのでここで取得。 + const selectionStart = input.selectionStart; + const selectionEnd = input.selectionEnd; + + input.value = newValue; + + let oldSelectionStart = 0; + let newSelectionStart = 0; + + for (const unit of toOperationUnitByChar(serializeUpOperation(operation))) { + switch (unit.type) { + case 'retain': + oldSelectionStart++; + newSelectionStart++; + break; + case 'insert': + newSelectionStart++; + break; + case 'delete': + oldSelectionStart++; + break; + } + if (oldSelectionStart >= selectionStart) { + break; + } + } + + if (selectionEnd != null) { + let oldSelectionEnd = 0; + let newSelectionEnd = 0; + + for (const unit of toOperationUnitByChar(serializeUpOperation(operation))) { + switch (unit.type) { + case 'retain': + oldSelectionEnd++; + newSelectionEnd++; + break; + case 'insert': + newSelectionEnd++; + break; + case 'delete': + oldSelectionEnd++; + break; + } + if (oldSelectionEnd >= selectionEnd) { + break; + } } - if (prevPlaceholderRef.current !== currentPlaceholderRef.current) { - loggerRef.warn( - 'placeholderプロパティの値が更新されましたが、CollaborativeInputではplaceholderの更新に対応していないため無視されます。', - ); + + if (selectionStart !== newSelectionStart || selectionEnd !== newSelectionEnd) { + input.setSelectionRange(newSelectionStart, newSelectionEnd); } - }, [currentPlaceholderRef, prevPlaceholderRef]); + return; + } + + if (selectionStart !== newSelectionStart) { + input.setSelectionRange(newSelectionStart, newSelectionStart); + } }; +/** + * 他のユーザーと文字列を共同で編集可能なコンポーネントを表します。カーソルの位置も適当な位置に自動で移動されます。 + */ export const CollaborativeInput: React.FC = ({ value, onChange, - onSkipping: onSkippingProp, - onGetQuill, - multiline: multilineProp, + onSkipping, bufferDuration: bufferDurationProp, + multiline, placeholder, - disabled: disabledProp, - className, + disabled, + className: classNameProp, style, size, }) => { - const multiline = multilineProp === true; - const disabled = disabledProp === true; + // コンポーネントの ref + const inputRef = React.useRef(null); + const textareaRef = React.useRef(null); + + // props から受け取った関数の ref + const onChangeRef = useLatest(onChange); + const onSkippingRef = useLatest(onSkipping); - const [isOnComposition, setIsOnComposition] = React.useState(false); - const prevIsOnComposition = usePrevious(isOnComposition); - const isOnCompositionRef = useLatest(isOnComposition); const valueRef = useLatest(value); - const onGetQuillRef = useLatest(onGetQuill); - - const { quill, quillRef } = useQuill({ - modules: { - toolbar: false, - // https://github.com/quilljs/quill/issues/1432#issuecomment-486659920 - keyboard: - multiline === true - ? undefined - : { - bindings: { - enter: { - key: 13, - handler: () => false, - }, - }, - }, - }, - placeholder, - // プレーンテキスト以外を無効化している - formats: [], - theme: 'bubble', - }); - const prevQuill = usePrevious(quill); - - let bufferDuration: number | null; - switch (bufferDurationProp) { - case 'default': - bufferDuration = 500; - break; - case 'short': - bufferDuration = 100; - break; - default: - bufferDuration = bufferDurationProp === 0 ? null : bufferDurationProp; - break; - } - const onSkipping = (params: OnSkippingParams): void => { - if (onSkippingProp == null) { - return; - } - onSkippingProp(params); - }; - const onSkippingRef = useLatest(onSkipping); + const [inputText, setInputText] = useState(value); + const inputTextRef = useLatest(inputText); - const { ref: bufferRef, onChangeInput } = useBuffer({ - value, - bufferDuration, - onChangeOutput: params => { - onSkippingRef.current({ - isSkipping: false, - previousValue: params.previousValue, - currentValue: params.currentValue, - }); - if (multiline) { - onChange(params); - } else { - onChange({ - ...params, - currentValue: params.currentValue.replaceAll('\r', '').replaceAll('\n', ''), - }); - } - }, - setValueToComponent: ({ value, component }) => { - const prev = component.getText(); - const delta = createDelta({ prev, next: value }); - component.updateContents(delta); - }, - }); + const [inputTextAtLastDataMatch, setInputTextAtLastDataMatch] = useState(value); + const inputTextAtLastDataMatchRef = useLatest(inputTextAtLastDataMatch); - React.useEffect(() => { - bufferRef.current = quill ?? null; - if (onGetQuillRef.current != null) { - onGetQuillRef.current(quill); - } - }, [bufferRef, onGetQuillRef, quill]); + const isSkipping = inputText !== inputTextAtLastDataMatch; - const prevTextRef = React.useRef(); - React.useEffect(() => { - if (quill == null) { - return; - } + const [valueAtLastDataMatch, setValueAtLastDataMatch] = useState(value); + const valueAtLastDataMatchRef = useLatest(valueAtLastDataMatch); - const onTextChange = () => { - const prevText = prevTextRef.current; - if (prevText == null) { - return; - } - const currentText = quill.getText(); - if (prevText !== currentText) { - onSkippingRef.current({ isSkipping: true }); - onChangeInput(currentText); - } - prevTextRef.current = currentText; - }; - if (prevQuill !== quill) { - quill.setText(valueRef.current); - prevTextRef.current = quill.getText(); - quill.on('text-change', () => { - if (isOnCompositionRef.current) { - // 漢字変換前のひらがなの入力などの際は関数を実行しない(onCompositionEndが実行された際に実行する)ようにする処理。 - // これにより、漢字変換前のひらがなが、しばしば二重で入力されることがある不具合を回避している。 - return; - } - onTextChange(); - }); + const matchData = React.useCallback(() => { + const { state: nextInputText, upOperation } = ot({ + rootText: valueAtLastDataMatchRef.current, + currentMyText: inputTextRef.current, + currentTheirText: valueRef.current, + }); + if (valueRef.current !== nextInputText) { + onChangeRef.current(nextInputText); + } + setInputText(nextInputText); + setInputTextAtLastDataMatch(nextInputText); + setValueAtLastDataMatch(nextInputText); + if (inputRef.current != null) { + moveCursorByUpOperation(inputRef.current, upOperation, nextInputText); } - if (prevIsOnComposition === true && isOnComposition === false) { - // 漢字変換前のひらがななどを入力していた場合は、onCompositionEndが実行された際に初めて変更を送信する処理。 - onTextChange(); + if (textareaRef.current != null) { + moveCursorByUpOperation(textareaRef.current, upOperation, nextInputText); } - }, [ - isOnComposition, - isOnCompositionRef, - onChangeInput, - onSkippingRef, - prevIsOnComposition, - prevQuill, - quill, - valueRef, - ]); + }, [inputTextRef, valueRef, onChangeRef, valueAtLastDataMatchRef]); React.useEffect(() => { - if (quill == null) { + if (isSkipping) { + onSkippingRef.current?.({ isSkipping }); return; } - if (disabled) { - quill.disable(); - } else { - quill.enable(); - } - }, [disabled, quill]); + onSkippingRef.current?.({ + isSkipping, + previousValue: inputTextAtLastDataMatchRef.current, + currentValue: inputTextRef.current, + }); + }, [inputTextRef, isSkipping, onSkippingRef, inputTextAtLastDataMatchRef]); - useWarnPlaceholderChanges({ quill, placeholderProp: placeholder }); + const bufferDuration = useParseBufferDuration(bufferDurationProp); + // bufferDuration ミリ秒ごとに matchData を実行することで CollaborativeInput の機能を実現させているという仕組み。「文字列の変更があり、なおかつ変更がおさまったときにのみ matchData を実行する」という方法も考えられるが、その場合はロジックが複雑になるため、単純な方法を採用している。 + useInterval(matchData, bufferDuration); - let sizeCss: SerializedStyles; - switch (size) { - case 'verySmall': - sizeCss = verySmallCss; - break; - case 'small': - sizeCss = smallCss; - break; - default: - sizeCss = mediumCss; + const className = classNames( + ClassNames.collaborativeInput, + ClassNames.getSize(size), + disabled ? ClassNames.disabled : null, + classNameProp, + ); + + // Ant Design の Input コンポーネントでは uncontrolled だと何故かうまく扱えなかったため、代わりに input 要素を直接使っている。 + if (multiline === true) { + // Ant Design の Input コンポーネントは上のコメントのとおり正常に動作しなかったため、Input.TextArea も同様と考えて代わりに textarea 要素を直接使っている。 + return ( +