Просмотр исходного кода

Merge pull request #10472 from growilabs/imprv/use-event-target

imprv: Use EventTarget instead of EventEmitter on the client side
Yuki Takei 5 месяцев назад
Родитель
Сommit
51599c928d

+ 5 - 0
.changeset/clever-paws-wink.md

@@ -0,0 +1,5 @@
+---
+'@growi/core': minor
+---
+
+Add global EventTarget instance provider

+ 18 - 14
apps/app/src/client/components/PageEditor/EditorNavbarBottom/SavePageControls.tsx

@@ -2,9 +2,8 @@ import React, {
   useCallback, useState, useEffect, type JSX,
 } from 'react';
 
-import type EventEmitter from 'events';
-
 import { PageGrant } from '@growi/core';
+import { globalEventTarget } from '@growi/core/dist/utils';
 import { isTopPage, isUsersProtectedPages } from '@growi/core/dist/utils/page-path-utils';
 import { LoadingSpinner } from '@growi/ui/dist/components';
 import { useTranslation } from 'next-i18next';
@@ -25,16 +24,11 @@ import loggerFactory from '~/utils/logger';
 
 import { NotAvailable } from '../../NotAvailable';
 import { SlackNotification } from '../../SlackNotification';
+import type { SaveOptions } from '../PageEditor';
 
 import { GrantSelector } from './GrantSelector';
 
 
-declare global {
-  // eslint-disable-next-line vars-on-top, no-var
-  var globalEmitter: EventEmitter;
-}
-
-
 const logger = loggerFactory('growi:SavePageControls');
 
 
@@ -45,25 +39,35 @@ const SavePageButton = (props: { slackChannels: string, isSlackEnabled?: boolean
   const [isSavePageModalShown, setIsSavePageModalShown] = useState<boolean>(false);
   const { data: selectedGrant } = useSelectedGrant();
 
-  const { slackChannels, isSlackEnabled, isDeviceLargerThanMd } = props;
+  const { slackChannels, isSlackEnabled = false, isDeviceLargerThanMd } = props;
 
   const isWaitingSaveProcessing = _isWaitingSaveProcessing === true; // ignore undefined
 
   const save = useCallback(async(): Promise<void> => {
     // save
-    globalEmitter.emit('saveAndReturnToView', { wip: false, slackChannels, isSlackEnabled });
+    globalEventTarget.dispatchEvent(new CustomEvent<SaveOptions>('saveAndReturnToView', {
+      detail: {
+        wip: false, slackChannels, isSlackEnabled,
+      },
+    }));
   }, [isSlackEnabled, slackChannels]);
 
   const saveAndOverwriteScopesOfDescendants = useCallback(() => {
     // save
-    globalEmitter.emit('saveAndReturnToView', {
-      wip: false, overwriteScopesOfDescendants: true, slackChannels, isSlackEnabled,
-    });
+    globalEventTarget.dispatchEvent(new CustomEvent<SaveOptions>('saveAndReturnToView', {
+      detail: {
+        wip: false, overwriteScopesOfDescendants: true, slackChannels, isSlackEnabled,
+      },
+    }));
   }, [isSlackEnabled, slackChannels]);
 
   const saveAndMakeWip = useCallback(() => {
     // save
-    globalEmitter.emit('saveAndReturnToView', { wip: true, slackChannels, isSlackEnabled });
+    globalEventTarget.dispatchEvent(new CustomEvent<SaveOptions>('saveAndReturnToView', {
+      detail: {
+        wip: true, slackChannels, isSlackEnabled,
+      },
+    }));
   }, [isSlackEnabled, slackChannels]);
 
   const labelSubmitButton = t('Update');

+ 5 - 11
apps/app/src/client/components/PageEditor/PageEditor.tsx

@@ -3,12 +3,11 @@ import React, {
   useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState,
 } from 'react';
 
-import type EventEmitter from 'events';
 import nodePath from 'path';
 
 import { Origin } from '@growi/core';
 import type { IPageHasId } from '@growi/core/dist/interfaces';
-import { pathUtils } from '@growi/core/dist/utils';
+import { pathUtils, globalEventTarget } from '@growi/core/dist/utils';
 import { GlobalCodeMirrorEditorKey } from '@growi/editor';
 import { CodeMirrorEditorMain } from '@growi/editor/dist/client/components/CodeMirrorEditorMain';
 import { useCodeMirrorEditorIsolated } from '@growi/editor/dist/client/stores/codemirror-editor';
@@ -59,11 +58,6 @@ import '@growi/editor/dist/style.css';
 const logger = loggerFactory('growi:PageEditor');
 
 
-declare global {
-  // eslint-disable-next-line vars-on-top, no-var
-  var globalEmitter: EventEmitter;
-}
-
 export type SaveOptions = {
   wip: boolean,
   slackChannels: string,
@@ -219,10 +213,10 @@ export const PageEditorSubstance = (props: Props): JSX.Element => {
     }
   }, [pageId, selectedGrant, mutateWaitingSaveProcessing, updatePage, mutateIsGrantNormalized, t]);
 
-  const saveAndReturnToViewHandler = useCallback(async(opts: SaveOptions) => {
+  const saveAndReturnToViewHandler = useCallback(async(evt: CustomEvent<SaveOptions>) => {
     const markdown = codeMirrorEditor?.getDocString();
     const revisionId = isRevisionIdRequiredForPageUpdate ? currentRevisionId : undefined;
-    const page = await save(revisionId, markdown, opts, onConflict);
+    const page = await save(revisionId, markdown, evt.detail, onConflict);
     if (page == null) {
       return;
     }
@@ -280,10 +274,10 @@ export const PageEditorSubstance = (props: Props): JSX.Element => {
 
   // set handler to save and return to View
   useEffect(() => {
-    globalEmitter.on('saveAndReturnToView', saveAndReturnToViewHandler);
+    globalEventTarget.addEventListener('saveAndReturnToView', saveAndReturnToViewHandler);
 
     return function cleanup() {
-      globalEmitter.removeListener('saveAndReturnToView', saveAndReturnToViewHandler);
+      globalEventTarget.removeEventListener('saveAndReturnToView', saveAndReturnToViewHandler);
     };
   }, [saveAndReturnToViewHandler]);
 

+ 6 - 12
apps/app/src/client/components/ReactMarkdownComponents/DrawioViewerWithEditButton.tsx

@@ -1,7 +1,6 @@
 import React, { useCallback, useState, type JSX } from 'react';
 
-import type EventEmitter from 'events';
-
+import { globalEventTarget } from '@growi/core/dist/utils';
 import {
   DrawioViewer,
   type DrawioEditByViewerProps,
@@ -19,12 +18,6 @@ import '@growi/remark-drawio/dist/style.css';
 import styles from './DrawioViewerWithEditButton.module.scss';
 
 
-declare global {
-  // eslint-disable-next-line vars-on-top, no-var
-  var globalEmitter: EventEmitter;
-}
-
-
 export const DrawioViewerWithEditButton = React.memo((props: DrawioViewerProps): JSX.Element => {
   const { t } = useTranslation();
 
@@ -40,10 +33,11 @@ export const DrawioViewerWithEditButton = React.memo((props: DrawioViewerProps):
   const [mxfile, setMxfile] = useState('');
 
   const editButtonClickHandler = useCallback(() => {
-    const data: DrawioEditByViewerProps = {
-      bol, eol, drawioMxFile: mxfile,
-    };
-    globalEmitter.emit('launchDrawioModal', data);
+    globalEventTarget.dispatchEvent(new CustomEvent<DrawioEditByViewerProps>('launchDrawioModal', {
+      detail: {
+        bol, eol, drawioMxFile: mxfile,
+      },
+    }));
   }, [bol, eol, mxfile]);
 
   const renderingStartHandler = useCallback(() => {

+ 9 - 10
apps/app/src/client/components/ReactMarkdownComponents/Header.tsx

@@ -2,8 +2,7 @@ import {
   useCallback, useEffect, useState, type JSX,
 } from 'react';
 
-import type EventEmitter from 'events';
-
+import { globalEventTarget } from '@growi/core/dist/utils';
 import type { Element } from 'hast';
 import { useRouter } from 'next/router';
 
@@ -11,6 +10,7 @@ import { NextLink } from '~/components/ReactMarkdownComponents/NextLink';
 import {
   useIsGuestUser, useIsReadOnlyUser, useIsSharedUser, useShareLinkId,
 } from '~/stores-universal/context';
+import type { ReservedNextCaretLineEventDetail } from '~/stores/editor';
 import { useCurrentPageYjsData } from '~/stores/yjs';
 import loggerFactory from '~/utils/logger';
 
@@ -21,15 +21,14 @@ import styles from './Header.module.scss';
 const logger = loggerFactory('growi:components:Header');
 const moduleClass = styles['revision-head'] ?? '';
 
-declare global {
-  // eslint-disable-next-line vars-on-top, no-var
-  var globalEmitter: EventEmitter;
-}
-
 
-function setCaretLine(line?: number): void {
-  if (line != null) {
-    globalEmitter.emit('reservedNextCaretLine', line);
+function setCaretLine(lineNumber?: number): void {
+  if (lineNumber != null) {
+    globalEventTarget.dispatchEvent(new CustomEvent<ReservedNextCaretLineEventDetail>('reservedNextCaretLine', {
+      detail: {
+        lineNumber,
+      },
+    }));
   }
 }
 

+ 10 - 9
apps/app/src/client/components/ReactMarkdownComponents/TableWithEditButton.tsx

@@ -1,9 +1,9 @@
 import React, { useCallback, type JSX } from 'react';
 
-import type EventEmitter from 'events';
-
+import { globalEventTarget } from '@growi/core/dist/utils';
 import type { Element } from 'hast';
 
+import type { LaunchHandsonTableModalEventDetail } from '~/client/interfaces/handsontable-modal';
 import {
   useIsGuestUser, useIsReadOnlyUser, useIsSharedUser, useShareLinkId,
 } from '~/stores-universal/context';
@@ -12,10 +12,6 @@ import { useCurrentPageYjsData } from '~/stores/yjs';
 
 import styles from './TableWithEditButton.module.scss';
 
-declare global {
-  // eslint-disable-next-line vars-on-top, no-var
-  var globalEmitter: EventEmitter;
-}
 
 type TableWithEditButtonProps = {
   children: React.ReactNode,
@@ -33,11 +29,16 @@ const TableWithEditButtonNoMemorized = (props: TableWithEditButtonProps): JSX.El
   const { data: isRevisionOutdated } = useIsRevisionOutdated();
   const { data: currentPageYjsData } = useCurrentPageYjsData();
 
-  const bol = node.position?.start.line;
-  const eol = node.position?.end.line;
+  const bol = node.position?.start.line ?? 0;
+  const eol = node.position?.end.line ?? 0;
 
   const editButtonClickHandler = useCallback(() => {
-    globalEmitter.emit('launchHandsonTableModal', bol, eol);
+    globalEventTarget.dispatchEvent(new CustomEvent<LaunchHandsonTableModalEventDetail>('launchHandsonTableModal', {
+      detail: {
+        bol,
+        eol,
+      },
+    }));
   }, [bol, eol]);
 
   const isNoEditingUsers = currentPageYjsData?.awarenessStateSize == null || currentPageYjsData?.awarenessStateSize === 0;

+ 4 - 0
apps/app/src/client/interfaces/handsontable-modal.ts

@@ -0,0 +1,4 @@
+export type LaunchHandsonTableModalEventDetail = {
+  bol: number,
+  eol: number,
+}

+ 6 - 12
apps/app/src/client/services/side-effects/drawio-modal-launcher-for-view.ts

@@ -1,8 +1,7 @@
 import { useCallback, useEffect } from 'react';
 
-import type EventEmitter from 'events';
-
 import { Origin } from '@growi/core';
+import { globalEventTarget } from '@growi/core/dist/utils';
 import type { DrawioEditByViewerProps } from '@growi/remark-drawio';
 
 import { replaceDrawioInMarkdown } from '~/client/components/Page/markdown-drawio-util-for-view';
@@ -17,12 +16,6 @@ import loggerFactory from '~/utils/logger';
 const logger = loggerFactory('growi:cli:side-effects:useDrawioModalLauncherForView');
 
 
-declare global {
-  // eslint-disable-next-line vars-on-top, no-var
-  var globalEmitter: EventEmitter;
-}
-
-
 export const useDrawioModalLauncherForView = (opts?: {
   onSaveSuccess?: () => void,
   onSaveError?: (error: any) => void,
@@ -68,7 +61,7 @@ export const useDrawioModalLauncherForView = (opts?: {
       logger.error('failed to save', error);
       opts?.onSaveError?.(error);
     }
-  }, [closeConflictDiffModal, currentPage, opts, shareLinkId]);
+  }, [_updatePage, closeConflictDiffModal, currentPage, opts, shareLinkId]);
 
   // eslint-disable-next-line max-len
   const generateResolveConflictHandler = useCallback((revisionId: string, onConflict: (conflictData: RemoteRevisionData, newMarkdown: string) => void) => {
@@ -107,13 +100,14 @@ export const useDrawioModalLauncherForView = (opts?: {
       return;
     }
 
-    const handler = (data: DrawioEditByViewerProps) => {
+    const handler = (evt: CustomEvent<DrawioEditByViewerProps>) => {
+      const data = evt.detail;
       openDrawioModal(data.drawioMxFile, drawioMxFile => saveByDrawioModal(drawioMxFile, data.bol, data.eol));
     };
-    globalEmitter.on('launchDrawioModal', handler);
+    globalEventTarget.addEventListener('launchDrawioModal', handler);
 
     return function cleanup() {
-      globalEmitter.removeListener('launchDrawioModal', handler);
+      globalEventTarget.removeEventListener('launchDrawioModal', handler);
     };
   }, [openDrawioModal, saveByDrawioModal, shareLinkId]);
 };

+ 7 - 12
apps/app/src/client/services/side-effects/handsontable-modal-launcher-for-view.ts

@@ -1,11 +1,11 @@
 import { useCallback, useEffect } from 'react';
 
-import type EventEmitter from 'events';
-
 import { Origin } from '@growi/core';
+import { globalEventTarget } from '@growi/core/dist/utils';
 import type { MarkdownTable } from '@growi/editor';
 
 import { getMarkdownTableFromLine, replaceMarkdownTableInMarkdown } from '~/client/components/Page/markdown-table-util-for-view';
+import type { LaunchHandsonTableModalEventDetail } from '~/client/interfaces/handsontable-modal';
 import { extractRemoteRevisionDataFromErrorObj, useUpdatePage } from '~/client/services/update-page';
 import { useShareLinkId } from '~/stores-universal/context';
 import { useHandsontableModal, useConflictDiffModal } from '~/stores/modal';
@@ -17,12 +17,6 @@ import loggerFactory from '~/utils/logger';
 const logger = loggerFactory('growi:cli:side-effects:useHandsontableModalLauncherForView');
 
 
-declare global {
-  // eslint-disable-next-line vars-on-top, no-var
-  var globalEmitter: EventEmitter;
-}
-
-
 export const useHandsontableModalLauncherForView = (opts?: {
   onSaveSuccess?: () => void,
   onSaveError?: (error: any) => void,
@@ -68,7 +62,7 @@ export const useHandsontableModalLauncherForView = (opts?: {
       logger.error('failed to save', error);
       opts?.onSaveError?.(error);
     }
-  }, [closeConflictDiffModal, currentPage, opts, shareLinkId]);
+  }, [_updatePage, closeConflictDiffModal, currentPage, opts, shareLinkId]);
 
   // eslint-disable-next-line max-len
   const generateResolveConflictHandler = useCallback((revisionId: string, onConflict: (conflictData: RemoteRevisionData, newMarkdown: string) => void) => {
@@ -106,17 +100,18 @@ export const useHandsontableModalLauncherForView = (opts?: {
       return;
     }
 
-    const handler = (bol: number, eol: number) => {
+    const handler = (evt: CustomEvent<LaunchHandsonTableModalEventDetail>) => {
       if (currentPage.revision == null) return;
 
       const markdown = currentPage.revision.body;
+      const { bol, eol } = evt.detail;
       const currentMarkdownTable = getMarkdownTableFromLine(markdown, bol, eol);
       openHandsontableModal(currentMarkdownTable, false, table => saveByHandsontableModal(table, bol, eol));
     };
-    globalEmitter.on('launchHandsonTableModal', handler);
+    globalEventTarget.addEventListener('launchHandsonTableModal', handler);
 
     return function cleanup() {
-      globalEmitter.removeListener('launchHandsonTableModal', handler);
+      globalEventTarget.removeEventListener('launchHandsonTableModal', handler);
     };
   }, [currentPage, openHandsontableModal, saveByHandsontableModal, shareLinkId]);
 };

+ 0 - 11
apps/app/src/pages/[[...path]].page.tsx

@@ -12,7 +12,6 @@ import type {
 } from '@growi/core';
 import { isIPageInfo } from '@growi/core';
 import { isClient, pagePathUtils, pathUtils } from '@growi/core/dist/utils';
-import EventEmitter from 'events';
 import ExtensibleCustomError from 'extensible-custom-error';
 import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
 import superjson from 'superjson';
@@ -101,11 +100,6 @@ import {
   useInitSidebarConfig,
 } from './utils/commons';
 
-declare global {
-  // eslint-disable-next-line vars-on-top, no-var
-  var globalEmitter: EventEmitter;
-}
-
 const GrowiContextualSubNavigationSubstance = dynamic(
   () => import('~/client/components/Navbar/GrowiContextualSubNavigation'),
   { ssr: false },
@@ -311,11 +305,6 @@ type Props = CommonProps & {
 };
 
 const Page: NextPageWithLayout<Props> = (props: Props) => {
-  // register global EventEmitter
-  if (isClient() && window.globalEmitter == null) {
-    window.globalEmitter = new EventEmitter();
-  }
-
   const router = useRouter();
 
   useCurrentUser(props.currentUser ?? null);

+ 0 - 6
apps/app/src/stores-universal/context.tsx

@@ -1,4 +1,3 @@
-import type EventEmitter from 'node:events';
 import type { ColorScheme, IUserHasId } from '@growi/core';
 import { AcceptedUploadFileType } from '@growi/core';
 import { useSWRStatic } from '@growi/core/dist/swr';
@@ -11,11 +10,6 @@ import type { RendererConfig } from '~/interfaces/services/renderer';
 
 import { useContextSWR } from './use-context-swr';
 
-declare global {
-  // eslint-disable-next-line vars-on-top, no-var
-  var globalEmitter: EventEmitter;
-}
-
 type Nullable<T> = T | null;
 
 export const useAppTitle = (

+ 9 - 4
apps/app/src/stores/editor.tsx

@@ -5,6 +5,7 @@ import {
   useSWRStatic,
   withUtils,
 } from '@growi/core/dist/swr';
+import { globalEventTarget } from '@growi/core/dist/utils';
 import type { EditorSettings } from '@growi/editor';
 import useSWR, { type SWRResponse } from 'swr';
 import useSWRImmutable from 'swr/immutable';
@@ -136,6 +137,10 @@ export const useIsEnabledUnsavedWarning = (): SWRResponse<boolean, Error> => {
   return useSWRStatic<boolean, Error>('isEnabledUnsavedWarning');
 };
 
+export type ReservedNextCaretLineEventDetail = {
+  lineNumber: number;
+};
+
 export const useReservedNextCaretLine = (
   initialData?: number,
 ): SWRResponse<number> => {
@@ -145,14 +150,14 @@ export const useReservedNextCaretLine = (
   const { mutate } = swrResponse;
 
   useEffect(() => {
-    const handler = (lineNumber: number) => {
-      mutate(lineNumber);
+    const handler = (evt: CustomEvent<ReservedNextCaretLineEventDetail>) => {
+      mutate(evt.detail.lineNumber);
     };
 
-    globalEmitter.on('reservedNextCaretLine', handler);
+    globalEventTarget.addEventListener('reservedNextCaretLine', handler);
 
     return function cleanup() {
-      globalEmitter.removeListener('reservedNextCaretLine', handler);
+      globalEventTarget.removeEventListener('reservedNextCaretLine', handler);
     };
   }, [mutate]);
 

+ 12 - 0
packages/core/src/utils/global-event-target.ts

@@ -0,0 +1,12 @@
+class GlobalEventTarget extends EventTarget {
+  private static instance: GlobalEventTarget;
+
+  static getInstance(): GlobalEventTarget {
+    if (!GlobalEventTarget.instance) {
+      GlobalEventTarget.instance = new GlobalEventTarget();
+    }
+    return GlobalEventTarget.instance;
+  }
+}
+
+export const globalEventTarget = GlobalEventTarget.getInstance();

+ 1 - 0
packages/core/src/utils/index.ts

@@ -4,6 +4,7 @@ import * as _envUtils from './env-utils';
 export const envUtils = _envUtils;
 
 export * from './browser-utils';
+export * from './global-event-target';
 export * from './growi-theme-metadata';
 export * as deepEquals from './is-deep-equals';
 export * as objectIdUtils from './objectid-utils';