CodeMirrorEditorMain.tsx 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208
  1. import { useEffect, useState } from 'react';
  2. import type { Extension } from '@codemirror/state';
  3. import { keymap, scrollPastEnd } from '@codemirror/view';
  4. // TODO: import socket.io-client types wihtout lint error
  5. // import type { Socket, DefaultEventsMap } from 'socket.io-client';
  6. // eslint-disable-next-line @typescript-eslint/ban-ts-comment
  7. // @ts-ignore
  8. import { yCollab } from 'y-codemirror.next';
  9. import { SocketIOProvider } from 'y-socket.io';
  10. import * as Y from 'yjs';
  11. import { GlobalCodeMirrorEditorKey, userColor } from '../consts';
  12. import { useCodeMirrorEditorIsolated } from '../stores';
  13. import { CodeMirrorEditor } from '.';
  14. // TODO: use SocketEventName
  15. // import { SocketEventName } from '~/interfaces/websocket';
  16. // TODO: import { GLOBAL_SOCKET_NS } from '~/stores/websocket';
  17. const GLOBAL_SOCKET_NS = '/';
  18. const additionalExtensions: Extension[] = [
  19. scrollPastEnd(),
  20. ];
  21. type Props = {
  22. onChange?: (value: string) => void,
  23. onSave?: () => void,
  24. onUpload?: (files: File[]) => void,
  25. indentSize?: number,
  26. pageId?: string,
  27. userName?: string,
  28. socket?: any, // Socket<DefaultEventsMap, DefaultEventsMap>,
  29. initialValue: string,
  30. setMarkdownToPreview: React.Dispatch<React.SetStateAction<string>>,
  31. }
  32. export const CodeMirrorEditorMain = (props: Props): JSX.Element => {
  33. const {
  34. onSave, onChange, onUpload, indentSize, pageId, userName, initialValue, socket, setMarkdownToPreview,
  35. } = props;
  36. const { data: codeMirrorEditor } = useCodeMirrorEditorIsolated(GlobalCodeMirrorEditorKey.MAIN);
  37. const [ydoc, setYdoc] = useState<Y.Doc | null>(null);
  38. const [provider, setProvider] = useState<SocketIOProvider | null>(null);
  39. const [cPageId, setCPageId] = useState(pageId);
  40. // const [isGlobalSync, setIsGlobalSync] = useState<boolean>(false);
  41. // cleanup ydoc and socketIOProvider
  42. useEffect(() => {
  43. if (cPageId === pageId) {
  44. return;
  45. }
  46. if (!provider || !ydoc || socket == null) {
  47. return;
  48. }
  49. ydoc.destroy();
  50. setYdoc(null);
  51. provider.destroy();
  52. provider.disconnect();
  53. setProvider(null);
  54. // TODO: use SocketEventName
  55. socket.off('ydoc:sync');
  56. setCPageId(pageId);
  57. // setIsGlobalSync(false);
  58. }, [cPageId, pageId, provider, socket, ydoc]);
  59. // setup ydoc
  60. useEffect(() => {
  61. if (ydoc != null) {
  62. return;
  63. }
  64. const _ydoc = new Y.Doc();
  65. setYdoc(_ydoc);
  66. }, [initialValue, ydoc]);
  67. // setup socketIOProvider
  68. useEffect(() => {
  69. if (ydoc == null || provider != null || socket == null) {
  70. return;
  71. }
  72. const socketIOProvider = new SocketIOProvider(
  73. GLOBAL_SOCKET_NS,
  74. `yjs/${pageId}`,
  75. ydoc,
  76. { autoConnect: true },
  77. );
  78. socketIOProvider.awareness.setLocalStateField('user', {
  79. name: userName ? `${userName}` : `Guest User ${Math.floor(Math.random() * 100)}`,
  80. color: userColor.color,
  81. colorLight: userColor.light,
  82. });
  83. socketIOProvider.on('sync', (isSync: boolean) => {
  84. if (isSync) {
  85. // TODO: use SocketEventName
  86. socket.emit('ydoc:sync', { pageId, initialValue });
  87. }
  88. // setIsGlobalSync(isSync);
  89. });
  90. // TODO: delete this code
  91. socketIOProvider.on('status', ({ status: _status }: { status: string }) => {
  92. if (_status) console.log(_status);
  93. });
  94. setProvider(socketIOProvider);
  95. }, [initialValue, pageId, provider, socket, userName, ydoc]);
  96. // attach YDoc to CodeMirror
  97. useEffect(() => {
  98. if (ydoc == null || provider == null) {
  99. return;
  100. }
  101. const ytext = ydoc.getText('codemirror');
  102. const undoManager = new Y.UndoManager(ytext);
  103. const cleanup = codeMirrorEditor?.appendExtensions?.([
  104. yCollab(ytext, provider.awareness, { undoManager }),
  105. ]);
  106. return cleanup;
  107. }, [codeMirrorEditor, provider, setMarkdownToPreview, ydoc]);
  108. // initialize markdown and preview
  109. useEffect(() => {
  110. if (ydoc == null) {
  111. return;
  112. }
  113. // console.log('sync', globalIsSync);
  114. // // TODO: check behavior when ydoc diff is retrieved once from server but sync is false exactly 5 seconds later
  115. // // TODO: Note that empty characters will be entered.s
  116. // setTimeout(() => {
  117. // if (!globalIsSync) {
  118. // ydoc.getText('codemirror').insert(0, initialValue);
  119. // }
  120. // }, 5000);
  121. const ytext = ydoc.getText('codemirror');
  122. codeMirrorEditor?.initDoc(ytext.toString());
  123. setMarkdownToPreview(ytext.toString());
  124. // TODO: Check the reproduction conditions that made this code necessary and confirm reproduction
  125. // mutateIsEnabledUnsavedWarning(false);
  126. }, [codeMirrorEditor, initialValue, pageId, setMarkdownToPreview, socket, ydoc]);
  127. // setup additional extensions
  128. useEffect(() => {
  129. return codeMirrorEditor?.appendExtensions?.(additionalExtensions);
  130. }, [codeMirrorEditor]);
  131. // set handler to save with shortcut key
  132. useEffect(() => {
  133. if (onSave == null) {
  134. return;
  135. }
  136. const extension = keymap.of([
  137. {
  138. key: 'Mod-s',
  139. preventDefault: true,
  140. run: () => {
  141. const doc = codeMirrorEditor?.getDoc();
  142. if (doc != null) {
  143. onSave();
  144. }
  145. return true;
  146. },
  147. },
  148. ]);
  149. const cleanupFunction = codeMirrorEditor?.appendExtensions?.(extension);
  150. return cleanupFunction;
  151. }, [codeMirrorEditor, onSave]);
  152. return (
  153. <CodeMirrorEditor
  154. editorKey={GlobalCodeMirrorEditorKey.MAIN}
  155. onChange={onChange}
  156. onUpload={onUpload}
  157. indentSize={indentSize}
  158. />
  159. // <>
  160. // {isGlobalSync
  161. // ? (
  162. // <CodeMirrorEditor
  163. // editorKey={GlobalCodeMirrorEditorKey.MAIN}
  164. // onChange={onChange}
  165. // onUpload={onUpload}
  166. // indentSize={indentSize}
  167. // />
  168. // ) : <p className="text-danger font-weight-bold">connecting ...</p>}
  169. // </>
  170. );
  171. };