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

Merge pull request #6154 from weseek/imprv/confirm-adding-and-removing-beforeunload-event-lister-timing

Imprv/confirm adding and removing beforeunload event lister timing
cao 3 лет назад
Родитель
Сommit
871cda90af

+ 2 - 1
packages/app/public/static/locales/en_US/translation.json

@@ -387,7 +387,8 @@
     "overwrite_scopes": "{{operation}} and Overwrite scopes of all descendants",
     "notice": {
       "conflict": "Couldn't save the changes you made because someone else was editing this page. Please re-edit the affected section after reloading the page."
-    }
+    },
+    "changes_not_saved": "Changes you made may not be saved."
   },
   "page_comment": {
     "display_the_page_when_posting_this_comment": "Display the page when posting this comment",

+ 2 - 1
packages/app/public/static/locales/ja_JP/translation.json

@@ -387,7 +387,8 @@
     "overwrite_scopes": "{{operation}}と同時に全ての配下ページのスコープを上書き",
     "notice": {
       "conflict": "すでに他の人がこのページを編集していたため保存できませんでした。ページを再読み込み後、自分の編集箇所のみ再度編集してください。"
-    }
+    },
+    "changes_not_saved": "変更が保存されていない可能性があります。"
   },
   "page_comment": {
     "display_the_page_when_posting_this_comment": "投稿時のページを表示する",

+ 2 - 1
packages/app/public/static/locales/zh_CN/translation.json

@@ -366,7 +366,8 @@
 		"overwrite_scopes": "{{operation}和覆盖所有子体的作用域",
 		"notice": {
 			"conflict": "无法保存您所做的更改,因为其他人正在编辑此页。请在重新加载页面后重新编辑受影响的部分。"
-		}
+		},
+    "changes_not_saved": "您所做的更改可能不会保存。"
   },
   "page_comment": {
     "display_the_page_when_posting_this_comment": "Display the page when posting this comment",

+ 0 - 24
packages/app/src/client/services/EditorContainer.js

@@ -21,8 +21,6 @@ export default class EditorContainer extends Container {
       tags: null,
     };
 
-    this.isSetBeforeunloadEventHandler = false;
-
     this.initDrafts();
 
   }
@@ -59,28 +57,6 @@ export default class EditorContainer extends Container {
     }
   }
 
-
-  // See https://developer.mozilla.org/en-US/docs/Web/API/WindowEventHandlers/onbeforeunload#example
-  showUnsavedWarning(e) {
-    // Cancel the event
-    e.preventDefault();
-    // display browser default message
-    e.returnValue = '';
-    return '';
-  }
-
-  disableUnsavedWarning() {
-    window.removeEventListener('beforeunload', this.showUnsavedWarning);
-    this.isSetBeforeunloadEventHandler = false;
-  }
-
-  enableUnsavedWarning() {
-    if (!this.isSetBeforeunloadEventHandler) {
-      window.addEventListener('beforeunload', this.showUnsavedWarning);
-      this.isSetBeforeunloadEventHandler = true;
-    }
-  }
-
   clearDraft(path) {
     delete this.drafts[path];
     window.localStorage.setItem('drafts', JSON.stringify(this.drafts));

+ 1 - 0
packages/app/src/client/util/editor.ts

@@ -24,3 +24,4 @@ export const getOptionsToSave = (
     grantUserGroupName,
   };
 };
+

+ 1 - 0
packages/app/src/components/BasicLayout.tsx

@@ -1,6 +1,7 @@
 import React, { ReactNode } from 'react';
 
 import dynamic from 'next/dynamic';
+import Link from 'next/link';
 
 import { GrowiNavbar } from './Navbar/GrowiNavbar';
 import { RawLayout } from './RawLayout';

+ 6 - 5
packages/app/src/components/Page.jsx

@@ -2,7 +2,6 @@ import React, { useEffect, useRef } from 'react';
 
 import PropTypes from 'prop-types';
 
-
 import MarkdownTable from '~/client/models/MarkdownTable';
 import AppContainer from '~/client/services/AppContainer';
 import EditorContainer from '~/client/services/EditorContainer';
@@ -76,7 +75,7 @@ class Page extends React.Component {
 
   async saveHandlerForHandsontableModal(markdownTable) {
     const {
-      isSlackEnabled, slackChannels, pageContainer, editorContainer, grant, grantGroupId, grantGroupName, pageTags,
+      isSlackEnabled, slackChannels, pageContainer, editorContainer, grant, grantGroupId, grantGroupName, pageTags, mutateIsEnabledUnsavedWarning,
     } = this.props;
     const optionsToSave = getOptionsToSave(isSlackEnabled, slackChannels, grant, grantGroupId, grantGroupName, pageTags);
 
@@ -89,7 +88,7 @@ class Page extends React.Component {
 
     try {
       // disable unsaved warning
-      editorContainer.disableUnsavedWarning();
+      mutateIsEnabledUnsavedWarning(false)
 
       // eslint-disable-next-line no-unused-vars
       const { page, tags } = await pageContainer.save(newMarkdown, this.props.editorMode, optionsToSave);
@@ -108,7 +107,7 @@ class Page extends React.Component {
 
   async saveHandlerForDrawioModal(drawioData) {
     const {
-      isSlackEnabled, slackChannels, pageContainer, pageTags, grant, grantGroupId, grantGroupName, editorContainer,
+      isSlackEnabled, slackChannels, pageContainer, pageTags, grant, grantGroupId, grantGroupName, editorContainer, mutateIsEnabledUnsavedWarning,
     } = this.props;
     const optionsToSave = getOptionsToSave(isSlackEnabled, slackChannels, grant, grantGroupId, grantGroupName, pageTags);
 
@@ -121,7 +120,7 @@ class Page extends React.Component {
 
     try {
       // disable unsaved warning
-      editorContainer.disableUnsavedWarning();
+      mutateIsEnabledUnsavedWarning(false);
 
       // eslint-disable-next-line no-unused-vars
       const { page, tags } = await pageContainer.save(newMarkdown, this.props.editorMode, optionsToSave);
@@ -194,6 +193,7 @@ const PageWrapper = (props) => {
   const { data: grant } = useSelectedGrant();
   const { data: grantGroupId } = useSelectedGrantGroupId();
   const { data: grantGroupName } = useSelectedGrantGroupName();
+  const { mutate: mutateIsEnabledUnsavedWarning } = useIsEnabledUnsavedWarning();
 
   const pageRef = useRef(null);
 
@@ -244,6 +244,7 @@ const PageWrapper = (props) => {
       grant={grant}
       grantGroupId={grantGroupId}
       grantGroupName={grantGroupName}
+      mutateIsEnabledUnsavedWarning={mutateIsEnabledUnsavedWarning}
     />
   );
 };

+ 4 - 2
packages/app/src/components/PageEditor.tsx

@@ -18,6 +18,7 @@ import {
 } from '~/stores/context';
 import {
   useCurrentIndentSize, useSWRxSlackChannels, useIsSlackEnabled, useIsTextlintEnabled, usePageTagsForEditors,
+  useIsEnabledUnsavedWarning
 } from '~/stores/editor';
 import {
   EditorMode,
@@ -96,6 +97,7 @@ const PageEditor = (props: Props): JSX.Element => {
   const { data: isTextlintEnabled } = useIsTextlintEnabled();
   const { data: isIndentSizeForced } = useIsIndentSizeForced();
   const { data: indentSize, mutate: mutateCurrentIndentSize } = useCurrentIndentSize();
+  const { mutate: mutateIsEnabledUnsavedWarning } = useIsEnabledUnsavedWarning();
 
   // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
   const [markdown, setMarkdown] = useState<string>(pageContainer.state.markdown!);
@@ -129,7 +131,7 @@ const PageEditor = (props: Props): JSX.Element => {
 
     try {
       // disable unsaved warning
-      editorContainer.disableUnsavedWarning();
+      mutateIsEnabledUnsavedWarning(false)
 
       // eslint-disable-next-line no-unused-vars
       const { tags } = await pageContainer.save(markdown, editorMode, optionsToSave);
@@ -355,7 +357,7 @@ const PageEditor = (props: Props): JSX.Element => {
   useEffect(() => {
     // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
     if (pageContainer.state.markdown! !== markdown) {
-      editorContainer.enableUnsavedWarning();
+      mutateIsEnabledUnsavedWarning(true);
     }
   }, [editorContainer, markdown, pageContainer.state.markdown]);
 

+ 3 - 4
packages/app/src/components/PageEditorByHackmd.jsx

@@ -3,14 +3,13 @@ import React from 'react';
 import { useTranslation } from 'next-i18next';
 import PropTypes from 'prop-types';
 
-
 import AppContainer from '~/client/services/AppContainer';
 import EditorContainer from '~/client/services/EditorContainer';
 import PageContainer from '~/client/services/PageContainer';
 import { apiPost } from '~/client/util/apiv1-client';
 import { getOptionsToSave } from '~/client/util/editor';
 import { useCurrentPagePath, useCurrentPageId } from '~/stores/context';
-import { useSWRxSlackChannels, useIsSlackEnabled, usePageTagsForEditors } from '~/stores/editor';
+import { useSWRxSlackChannels, useIsSlackEnabled, usePageTagsForEditors,useIsEnabledUnsavedWarning } from '~/stores/editor';
 import {
   useEditorMode, useSelectedGrant, useSelectedGrantGroupId, useSelectedGrantGroupName,
 } from '~/stores/ui';
@@ -178,7 +177,7 @@ class PageEditorByHackmd extends React.Component {
 
     try {
       // disable unsaved warning
-      editorContainer.disableUnsavedWarning();
+      mutateIsEnabledUnsavedWarning(false)
 
       // eslint-disable-next-line no-unused-vars
       const { page, tags } = await pageContainer.save(markdown, this.props.editorMode, optionsToSave);
@@ -213,7 +212,7 @@ class PageEditorByHackmd extends React.Component {
     }
 
     // enable unsaved warning
-    editorContainer.enableUnsavedWarning();
+    mutateIsEnabledUnsavedWarning(true);
 
     const params = {
       pageId: pageContainer.state.pageId,

+ 6 - 3
packages/app/src/components/SavePageControls.jsx

@@ -15,7 +15,7 @@ import { getOptionsToSave } from '~/client/util/editor';
 
 // TODO: remove this when omitting unstated is completed
 import { useIsEditable, useCurrentPageId } from '~/stores/context';
-import { usePageTagsForEditors } from '~/stores/editor';
+import { usePageTagsForEditors, useIsEnabledUnsavedWarning } from '~/stores/editor';
 import {
   useEditorMode, useSelectedGrant, useSelectedGrantGroupId, useSelectedGrantGroupName,
 } from '~/stores/ui';
@@ -54,7 +54,7 @@ class SavePageControls extends React.Component {
       isSlackEnabled, slackChannels, grant, grantGroupId, grantGroupName, pageContainer, editorContainer, pageTags,
     } = this.props;
     // disable unsaved warning
-    editorContainer.disableUnsavedWarning();
+    mutateIsEnabledUnsavedWarning(false)
 
     try {
       // save
@@ -80,7 +80,7 @@ class SavePageControls extends React.Component {
       isSlackEnabled, slackChannels, grant, grantGroupId, grantGroupName, pageContainer, editorContainer, pageTags,
     } = this.props;
     // disable unsaved warning
-    editorContainer.disableUnsavedWarning();
+    mutateIsEnabledUnsavedWarning(false)
     // save
     const currentOptionsToSave = getOptionsToSave(isSlackEnabled, slackChannels, grant, grantGroupId, grantGroupName, pageTags);
     const optionsToSave = Object.assign(currentOptionsToSave, {
@@ -146,6 +146,7 @@ const SavePageControlsWrapper = (props) => {
   const { data: grantGroupName, mutate: mutateGrantGroupName } = useSelectedGrantGroupName();
   const { data: pageId } = useCurrentPageId();
   const { data: pageTags } = usePageTagsForEditors(pageId);
+  const { mutate: mutateIsEnabledUnsavedWarning} = useIsEnabledUnsavedWarning();
 
 
   if (isEditable == null || editorMode == null) {
@@ -167,6 +168,7 @@ const SavePageControlsWrapper = (props) => {
       mutateGrant={mutateGrant}
       mutateGrantGroupId={mutateGrantGroupId}
       mutateGrantGroupName={mutateGrantGroupName}
+      mutateIsEnabledUnsavedWarning={mutateIsEnabledUnsavedWarning}
       pageTags={pageTags}
     />
   );
@@ -190,6 +192,7 @@ SavePageControls.propTypes = {
   mutateGrant: PropTypes.func,
   mutateGrantGroupId: PropTypes.func,
   mutateGrantGroupName: PropTypes.func,
+  mutateIsEnabledUnsavedWarning: PropTypes.func,
 };
 
 export default SavePageControlsWrapper;

+ 59 - 0
packages/app/src/pages/UnsavedAlertDialog.tsx

@@ -0,0 +1,59 @@
+import { useCallback, useEffect } from 'react';
+
+import { useRouter } from 'next/router';
+import { useTranslation } from 'next-i18next';
+
+import { useIsEnabledUnsavedWarning } from '~/stores/editor';
+
+const UnsavedAlertDialog = (): void => {
+  const { t } = useTranslation();
+  const router = useRouter();
+  const { data: isEnabledUnsavedWarning } = useIsEnabledUnsavedWarning();
+
+  const alertUnsavedWarningByBrowser = useCallback((e) => {
+    if (isEnabledUnsavedWarning) {
+      e.preventDefault();
+      // returnValue should be set to show alert dialog
+      // default alert message cannot be changed.
+      // See -> https://developer.mozilla.org/ja/docs/Web/API/Window/beforeunload_event
+      e.returnValue = '';
+      return;
+    }
+  }, [isEnabledUnsavedWarning]);
+
+  const alertUnsavedWarningByNextRouter = useCallback(() => {
+    if (isEnabledUnsavedWarning) {
+    // eslint-disable-next-line no-alert
+      window.alert(t('page_edit.changes_not_saved'));
+    }
+    return;
+  }, [isEnabledUnsavedWarning, t]);
+
+  /*
+  * Route changes by Browser
+  * Example: window.location.href, F5
+  */
+  useEffect(() => {
+    window.addEventListener('beforeunload', alertUnsavedWarningByBrowser);
+    return () => {
+      window.removeEventListener('beforeunload', alertUnsavedWarningByBrowser);
+    };
+  }, [alertUnsavedWarningByBrowser]);
+
+
+  /*
+  * Route changes by Next Router
+  * https://nextjs.org/docs/api-reference/next/router
+  */
+  useEffect(() => {
+    router.events.on('routeChangeStart', alertUnsavedWarningByNextRouter);
+    return () => {
+      router.events.off('routeChangeStart', alertUnsavedWarningByNextRouter);
+    };
+  }, [alertUnsavedWarningByNextRouter, router.events]);
+
+
+  return;
+};
+
+export default UnsavedAlertDialog;

+ 5 - 1
packages/app/src/pages/[[...path]].page.tsx

@@ -1,4 +1,4 @@
-import React, { useEffect } from 'react';
+import React from 'react';
 
 import { pagePathUtils } from '@growi/core';
 import { isValidObjectId } from 'mongoose';
@@ -7,6 +7,7 @@ import {
 } from 'next';
 import Head from 'next/head';
 import { useRouter } from 'next/router';
+import dynamic from 'next/dynamic';
 
 // import { PageAlerts } from '~/components/PageAlert/PageAlerts';
 // import { PageComments } from '~/components/PageComment/PageComments';
@@ -85,6 +86,7 @@ type Props = CommonProps & {
 
 const GrowiPage: NextPage<Props> = (props: Props) => {
   // const { t } = useTranslation();
+  const UnsavedAlertDialog = dynamic(() => import('./UnsavedAlertDialog'), { ssr: false });
   const router = useRouter();
 
   const { data: currentUser } = useCurrentUser(props.currentUser != null ? JSON.parse(props.currentUser) : null);
@@ -216,6 +218,8 @@ const GrowiPage: NextPage<Props> = (props: Props) => {
           PageComments
         </footer>
 
+        <UnsavedAlertDialog />
+
       </BasicLayout>
     </>
   );

+ 4 - 0
packages/app/src/stores/editor.tsx

@@ -108,3 +108,7 @@ export const usePageTagsForEditors = (pageId: Nullable<string>): SWRResponse<str
     },
   };
 };
+
+export const useIsEnabledUnsavedWarning = (): SWRResponse<boolean, Error> => {
+ return useStaticSWR<boolean, Error>('isEnabledUnsavedWarning', undefined, { fallbackData: false });
+};